京东商品详情页技术实现
⼤家来京东打开商品页⼀般会看到如通⽤版、闪购、全球购等不同的页⾯风格,这⾥⾯会牵扯到各种各样垂直化的模板页⾯渲染。以前的解决⽅案是做静态化,但是静态化⼀个很⼤的问题就是页⾯改版时需要重新全量⽣成新的静态页。我们有⼏亿个商品,对于这么多商品,你如果⽣成页⾯的话需要跑很多天,⽽且还⽆法应对⼀些突发情况。
⽐如新的《⼴告法》,需要对⼀些数据进⾏清洗,后端清洗时间和成本来不及,那么很多时候就是从前台展⽰系统来进⾏数据过滤。因此需要⾮常灵活的前端展⽰架构来⽀持这种需求。
⾸先这是我们前端⾸屏⼤体的结构。⾸屏有标题、价格、价格、库存服务,服务⽀持,延保服务等,对于中⼼区有很多很多种服务。⽽这么多的服务只是⾸屏⾥的⼀部分。对于这么多服务如何在这个页⾯⾥,或者在⼀个页⾯⾥让它⾮常⾮常好的融合进来,这是我们要去解决的问题。
⽽第⼆屏⼤家看到的就是⼴告等等的。在这⼉会有品牌服务,因为京东有第三⽅商家,我们会提供⼴告位,叫商家模板。还有像商品介绍、评价、咨询等等,这⼀屏也包含了很多的服务。
商品详情页涉及的服务
对于商品详情页涉及了如下主要服务:
商品详情页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毫秒。
作者介绍:张开涛,京东资深Java⼯程师,2014年加⼊京东,主要负责商品详情页、详情页统⼀服务架构与开发⼯作,设计并开发了多个亿级访问量系统。⼯作之余喜欢写技术博客,有《跟我学 Spring》、《跟我学Spring MVC》、《跟我学Shiro》、《跟我学Nginx+Lua开发》等系列教程,⽬前博客访问量有460万+。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。