2015.12.28 丨 壹佰案例
案例|京东商品详情页应对“双11”大流量的技术实践
2015.12.28 丨 壹佰案例
此文是根据京东资深Java工程师张开涛11月21日在msup主办的into100沙龙第14期《京东商品详情页应对大流量的一些实践》演讲中的分享内容整理而成,转发请注明出处。
作者简介:
张开涛,京东资深Java工程师,2014年加入京东,主要负责商品详情页、详情页统一服务架构与开发工作,设计并开发了多个亿级访问量系统。工作之余喜欢写技术博客,有《跟我学 Spring》、《跟我学Spring MVC》、《跟我学Shiro》、《跟我学Nginx+Lua开发》等系列教程,目前博客访问量有460万+。他的
微信账号为“开涛的博客”,请大家关注。
演讲主题:
京东商品详情页应对大流量的一些实践
正文:
张开涛:大家好。我今天带来的是京东商品详情页应对大流量的一些实践。大家来京东打开商品页一般会看到如通用版、闪购、全球购等不同的页面风格,这里面会牵扯到各种各样垂直化的模板页面渲染。以前的解决方案是做静态化,但是静态化一个很大的问题就是页面改版时需要重新全量生成新的静态页。我们有几亿个商品,对于这么多商品,你如果生成页面的话需要跑很多天,而且还无法应对一些突发情况。
比如新的《广告法》,需要对一些数据进行清洗,后端清洗时间和成本来不及,那么很多时候就是从前台展示系统来进行数据过滤。因此需要非常灵活的前端展示架构来支持这种需求。
首先这是我们前端首屏大体的结构。首屏有标题、价格、价格、库存服务,服务支持,延保服务等,对于中心区有很多很多种服务。而这么多的服务只是首屏里的一部分。对于这么多服务如何在这个页面里,或者在一个页面里让它非常非常好的融合进来,这是我们要去解决的问题。
而第二屏大家看到的就是广告等等的。在这儿会有品牌服务,因为京东有第三方商家,我们会提供广告位,叫商家模板。还有像商品介绍、评价、咨询等等,这一屏也包含了很多的服务。
商品详情页涉及的服务
对于商品详情页涉及了如下主要服务:
商品详情页HTML页面渲染
价格服务
促销服务
库存状态/配送至服务
广告词服务
预售/秒杀服务
评价服务
试用服务
推荐服务
商品介绍服务
各品类相关的一些特殊服务
对于详情页我们采用了KV结构存储,但它是长尾,即数据是离散数据。这种方式的话,如果你做一般缓存的话,可能效率并不是特别高,只会缓存一些热点,像一些秒杀的商品放在缓存会有效果。这里还涉及到很多爬虫和一些软件会抓取我们页面,如果你缓存有问题的话,你的数据很快就会从缓存中刷出去。所以设计的时候要考虑离散数据问题。
详情页技术架构的发展
最早期的时候,我们商品详情页采用.NET技术,但是随着商品数量增加,而且随着商品数据库结构设计复杂性的变化,后来我们就生成了静态页,通过JAVA生成页面的片段,像商品介绍等等,都是通过一个一个片段输送出去的。在这一层我们其实遇到过很多问题,比如这里会生成很多的小文件,小文件如果你的磁盘用EXT3或者其他的话,会受到INODE的限制。
另外一个问题,我们生成这种页面片段的话,经常会涉及到,如果页面整体风格改变的话需要进行全量的数据刷新。比如要支持闪购单品也。对于这种的话,我们就需要把所有闪购页面重新生成静态页。如果我们业务变化很快,说这个页面不是我要的,就需要重新生成静态页,再重新刷一下。这对几万数量的商品没问题,但是现在我们的商品规模量很庞大,这样的话,可能会把依赖的系统刷挂,因为你调用的依赖方会非常多。假设我们现在依赖的有二十个,每一个页面要调动二十多个来源来拿到相应的数据。
后来我们发现这个问题,其实最主要的就是页面模板变更的速度不能满足我们需求;另一个,静态页我们用的机械盘,当遇到大流量时会非常非常慢。后来我们将它动态化,通过JAVA Worker把数据存到KV存储里,前端就是Nginx+Lua,这样模板就是数据全动态化。对于这套架构我们现在已经在线跑了一年多,整体的性能非常稳定,平均响应时间在50毫秒之内,基本可以保持在30~40ms左右。对于这套设计,现在变更需求可以非常迅速的去响应。
我们有一个商品详情页异构系统,依赖的服务非常多。我们用它把相关的数据源抓过来,同步Worker会把数据按照维度进行聚合。有商品维度,还有其他维度,比如商品介绍、分类、商家、品牌,对于这些维度我们都会分开进行存储。比如展示商品详情页时,读取商品信息、商品相关信息:分类,商家,品牌等等信息然后渲染页面即可;而商品介绍读出来吐出去就可以了。
这个其实本质也是静态化思想,是把数据做的静态化,而没有把页面静态化,这样的好处是页面模块可以随时变更。另外你只要保证数字是原子化,原子化就是你没有对它进行再加工,这样就可以对它再利用再处理。
商品详情页统一服务系统的建立
商品详情页上异步加载的服务非常多,因此我们做了一套统一服务系统。为什么做这个系统?我们的目标就是所有在页面中接入的请求或者接入的服务,都必须经过我们这个系统。
监控,监控每个服务的服务质量;
随时通过我们自己的开关去做一些降级的处理。比如促销慢了,可以随时对它降级,保证后端的服务不被异常的流量打出问题来。这个系统前端是用的Nginx+Lua。
数据异构系统。像我们的库存,大家可能看到我们的库存,跟淘宝的库存不太一样。因为京东有自营的和第三方的,看库存的话显示的有如有货还是没货,是否有预订,以及第三方可能还有运费的概念,第三方还存在配送时效问题,比如你买了多少天之后发货。对于这些数据我们可以做异构,异构过来我们只依赖于自己不依赖其他人。其他人服务出问题了,抖动了或者响应慢了,对我们是没有影响的。
核心的设计思想
异构的思想。我们把别人的数据按照我们自己的维度,或者按照我们自己想要消费的数据的格式进行存储。存储之后我们只消费我们自己的数据,其他人的数据我们都不依赖了。相当于别人的接口怎么抖动对我不影响的。像双十一我们有一个集群,比如商品挂了,前端还是可以提供服务,只是数据不更新了。还有一个如双十一期间一些商品不更新但是要做秒杀,我们可以通过前端逻辑处理,在系统里进行人工打上标签,打上之后就可以进行秒杀了。
服务闭环的思想。假设我们在设计页面的时候有很多服务依赖于别人,出问题之后肯定先找我们。找我们的时候我们又需要去联系其他的部门,就会存在沟通的问题。如果我们能够及早发现这个问题,进行预案处理,比如降级,如库存出问题了,让我们第一时间知道,我们可以降级为全部有货,让大家都有货可买,这就形成了服务闭环。所有服务接入都通过我们的系统接入,出现问题我们及时发现,进行降级处理。
维度化存储。在存储数据的时候我们都是按照维度进行存储的。然后我们按照使用方式获取。比如我们进行一个详情页的时候只需要两次获取,一次是拿商品信息,另外是拿商家分类等等。
统一接入层和代理层
统一入口,形成闭环。所有接入通过我们系统接入,这样出问题后我非常容易找。
做监控。比如这个接口响应慢了,我可以督促我这个依赖的业务。还有缓存前置,在前端有5-10秒缓存,对于这个时间大家是可以忍受的。我们把缓存前置,我们Nginx+Lua,它的并发是非常高的。缓存前置后很多流量导不到你的业务层;即我们尽量让流量在前端处理掉,而不到达我们的业务层。
业务前置,像库存封装,我们会在Nginx+Lua做一些简单的处理。做一些简单的数据处理,像一些人为非法传入的数据,都会在这一层过滤掉。
新版测试。像我们做了一个延保服务,我想知道它的之前和之后的效果怎么样的,我就需要对一部分人用A版,一部分人用B版,在我们这层可以实现。比如根据用户的ID,或者每次用户访问的时候都会用UUID。而且在这里通过Nginx+Lua,通过Lua写一些程序,在这里都是通过程序控制AB测试的。还有像引流,发布,流量切换都是在这层完成的。
比如我们在上线的时候都会有一些开关的概念,在Nginx+Lua这一层我们会通过写代码的方式,有50%的用户用新版,然后慢慢一步一步往上加,而且大多数流量控制在我们的前端。
做一些线上压测,通过Lua协程机制,把一个请求并发分成两个请求打到后端,然后你再做一些逻辑的验证。
降级开关前置
监控服务质量
限流等
我们做实践的时候会做服务的隔离。为什么做隔离呢?非常简单,假设你的一个系统里进行http调用,而忘了设超时时间,此时流量很大时,http服务出问题了,这很可能会导致应用挂掉。所以我们设计的时候会把我们的业务进行分级,在一个应用里对业务分级:0级业务,1级业务;如库存,这里面库存就是必须的,没有这个业务,页面不会进行下一步流程,我们设置为0级服务;而如延保服务没有也不影响,我们设置为1级。在这里我们用了servlet3异步化,通过异步化我们把请求接收到,然后存到隔离的池子里,然后这些池子的请求是相互隔离的,假如一个池子出问题了不会对另一个产生影响的。之前在做的时候其实是遇到过,比如在开发试用报告,没有加超时时间,把我们的应用打挂了。
部署和分组隔离。比如我们有一个业务,这个业务可能非常非常多人依赖我,我就可以进行分组。A部门调这个分组,B部门调那个分组。为什么这么做呢?因为你不能保证所有人按照你的流程来做。像压测没有告诉你,导致你没有增加流量等等。对于这种情况我尽量分离,你这样了对其他人是不受影响的。分组,就是不同的部门调不同的分组,或者按照调用方分级进行不同的分组。
到最后的时候,假设一个应用里面牵扯的服务特别特别多,但是这些服务又特别重要,像价格一天可能几百亿的量,这个时候就可以做一个单独服务。像促销、库存等等都可以单独拆出来做一个服务。如果前期没有问题的话,大家更多时候是把它做成一个大的项目。大项目一重启就会产生抖动,而抖动是对所有服务的。因此我们需要拆应用隔离。
对于分布式缓存大家应用比较多的可能是Redis、Memcached。这里我们前端Nginx会用一致性哈希的概念,如通过分类进行一致性哈希,让它一致性哈希到不同的Nginx实例增加命中率。还有对于一些错误数据或者一些兜底的数据是不做缓存的。
对于突发流量,我们使用比较多的是高效缓存,最有效的就是把数据拿到你这边缓存,这样这个数据就受你控制了。还有如你一个机房有一套数据,这样的话没有跨机房,整体的效率可能会有提升。这里用的比较多的就是多级缓存,先做本地缓存,本地缓存没有命中就走分布式。另外我们会做一些自动降级处理,像一些不是特别重要,我们自动根据超时时间降级,如第三方的配送时效,对于这个信息几秒钟或者几分钟没有给用户展示,并不会影响他的购买,对于这种数据我们会做一个,比如超过500毫秒或者200毫秒就自动降级,就是这个数据不输出了。还有一些数据没法儿降级的,比如价格,没有的话可能页面就是空,我们不会对它进行缓存。还有库存,我们没法儿做很大的缓存。还有我们尽量减少回源量,就是用一致性哈希。我们还会用非阻塞锁和304响应,如304响应适合如秒杀时一直点刷新按钮,而此时的一些异步加载数据没必要请求到服务端重新计算,此时就适合设置过期时间,如10s,10s内都返回304。还有对一些恶意访问,这个我们只能更多的去提升我们的扛恶意的。比如我们通过KV存储数据,这样在KV命中的情况下是不怕刷的,因为我们流量是足够的,除非它们把我们带宽打满。还有就是提升缓存命中率,减少回源冲击。还有我们会考虑把一些恶意的流量导流到另外一个分组,就是给一些恶意的用户使用的,就是它也能用,但是慢。还有就是对N页以后的请求做特殊处理,比如访问一个列表的时候,像大家访问更多的是前十页,对后十页就可以做特殊处理,比如限速,比如这个服务正常10毫秒就出来了,我给它放到100毫秒,这个我们都是在Nginx上做的,让他把刷你的速度给降下来。
还有一些就是我们的兜底的数据,一种就是做静态化。像我们会对前几页数据进行数据静态化,像服务挂了,可以把这个静态化的数据给大家提出来,不至于大家看到503页面或404的状况。还有就是没法儿做缓存,就是说我们没有降级方案的。
对于降级的话我们有两种:
第一,人工降级。比如一些库存,对于这种服务我们都是人工去监控,我们后台都会有报警系统,像超过多少毫秒都会有报警,都会通过人工来控制。还有自动降级。刚才提到了像超时降级,还有大访问量的时候会自动降级,因为访问量你的系统承载不住了,否则的就会挂掉。我们做这个就是对一些用户可用,对一些就是降级掉。
还有连接池超时时间,像大家都不去设置或者设置比较大,像一般访问都没有问题,但是一旦发生异常情况,像网络抖动或者其他的情况,你的整个系统可能就会挂掉。还有就是重试时机和次数。重试时机,第一次访问已经挂,接着第二次、第三次访问,其实这个请求是没有作用的。通过阶梯式的方式或者阶程式的方法慢慢做恢复。
还有CDN回源,我们做了版本化,现在评价也是版本化,为什么做版本化呢?因为之前双十一导致评价量非常非常大,你直接回源的话是扛不住的。所以我们现在做了评价版本化,有了版本号,这个页面可以缓存很长时间,比如可以缓存一天、两天;如果没有版本号,只能缓存几分钟,然后回源。对于这种方式可以更高效的做CDN缓存。爬虫不回源,不让它到后端服务。返回历史数据,非阻塞锁。
这里会做监控和报警,首先要知道系统的状况,还应用实例存活,调用量,响应时间和可用率。调用量大了,可能就有恶意人刷你,你就要提前预警。这个降了,可能你依赖的服务出问题了,你要查哪些出问题了。
对于日志,像我们看的比较多的就是Nginx的访问日志,访问日志看的比较多的就是IP,或者它的UA,看这些信息你就知道哪些是爬虫,哪些是恶意访问的,哪些是正常流量。出问题的时候,你可以干预或者通过其他的机制拒绝掉,不让他请求。还有就是应用日志,因为业务的话会在这里写业务代码,所以可以看到。还有应用日志,应用的话比较多的就是业务的日志和异常日志。我们其实发现问题,更多的是通过日志去发现,还有一些在开发,在记录日志的时候没有任何含义,就一条,出错了,什么错不知道。所以我们在内部的时候,要求把一些日志要记清楚,什么问题,哪些位置发生了,什么异常都要记录下来。对于比较重要的议程都直接报警。监控日志会用调用量、响应时间和可用率。
我们在做系统的时候肯定要压测,第一就是吞吐量压测,就是看你系统最大压测是多少。对于这种我们可能压的是一个URL。这种方式存在一个很大的问题,如果是单个URL肯定是热点,热点压没有很大的意义。还有一种用的比较多的就是把线上的真实流量复制出来,然后在线上直接压测。我们直接把线上的流量定向一份来压测,来压测你的极限。还有页面埋点。压测量的时候要考虑是读还是写,还是读写压测。我们在压测的时候,读和写性能非常好,一旦读写混合的时候在某一个点会抖动,它的响应时候会非常非常慢。像有人压测的时候,顺序非常好,一旦离散(所谓离散,就是有的人访问1,有的人访问2,这个没有顺序去访问,这个是离散的)在压测的时候你要知道你压测的场景是什么样子的。
还有其他的,就是响应头记录服务器真实IP,前端JS瘦身,业务逻辑服务化后置,接入层数据过滤,数据校验,缓存前置,一些业务逻辑前置,智能DNS,减少跨机房调用,提供刷数据接口进行异常数据更新或删除,并发化提升性能。我们这里用的比较多的,一个商品页在拿数据的时候调了十几、二十个接口,这些接口是有规则的,就是先拿商品的,拿其他的,这些接口可以并行的调用。假如之前调用需要1-2秒,通过并发化我们提升了300-400毫秒。
以上是我今天分享的内容。