亿级商品详情页架构演进技术解密 | 高可用架构系列

此文是开涛在【三体高可用架构群】之分享内容,“三体”是为了纪念三体一书对技术人的伟大影响而冠名。

转载:http://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=210272034&idx=1&sn=3be9d2b53c7fec88716ee8affd2515f8&scene=5&srcid=FtID3LDZmQvJW8mQ86jr#rd

张开涛:2014年加入京东,主要负责商品详情页、详情页统一服务架构与开发工作,设计并开发了多个亿级访问量系统。工作之余喜欢写技术博客,有《跟我学Spring》、《跟我学Spring MVC》、《跟我学Shiro》、《跟我学Nginx+Lua开发》等系列教程,博客 http://jinnianshilongnian.iteye.com/  的访问量超过500W。


京东618的硝烟虽已散去,可开发和备战618期间总结过的一些设计原则和遇到的一些坑还历历在目。伴随着网站业务发展,需求日趋复杂多样并随时变化;传统静态化方案会遇到业务瓶颈,不能满足瞬变的需求。因此,需要一种能高性能实时渲染的动态化模板技术来解决这些问题。


今夜(编者:指8/27),我们将进行服装品类的垂直详情页的AB测试和切新库存服务的1/n流量。就此机会,和大家分享一下最近一年做的京东商品详情页的架构升级的心路历程。


商品详情页是什么

商品详情页是展示商品详细信息的一个页面,承载在网站的大部分流量和订单的入口。京东商城目前有通用版、全球购、闪购、易车、惠买车、服装、拼购、今日抄底等许多套详情页模板,通过一些特殊属性、商家类型和打标来区分,每套模板数据是一样的,核心逻辑基本一样,但是一些前端逻辑是有差别的。


目前商品详情页个性化需求非常多,数据来源也是非常多的(目前统计后端有差不多数十个依赖服务),而且许多基础服务做不了的不想做的或者说需要紧急处理的都放我们这处理,比如一些屏蔽商品需求等。因此我们需要一种架构能快速响应和优雅的解决这些需求问题,来了问题能在5~10分钟内搞定。我们这边经还常收到一些紧急需求,比如工商的一些投诉等需要及时响应。之前架构是静态化的,肯定无法满足这种日趋复杂和未知的需求。静态化时做屏蔽都是通过js,所以我们重新设计了商品详情页的架构。


它主要包括以下三部分:

商品详情页系统
负责静的部分(整个页面)


商品详情页动态服务系统和商品详情页统一服务系统
统一服务系统 负责动的部分,比如实时库存。目前已经上线了几个核心服务,今晚计划切新库存服务的1/n流量。
动态服务系统 负责给内网其他系统提供一些数据服务(比如大客户系统需要商品数据),目前商品详情页系统已经稳定运行半年了,目前主要给列表页提供一些数据。


键值结构的异构数据集群

商品主数据因为是存储在DB中,对于一些聚合数据需要联合查询非常多,会导致查询性能差的问题,因此对于键值类型的查询,我们这套异构数据非常有用。我们这次架构的调整的主要目的是满足日趋复杂的业务需求,能及时开发业务方的需求。我们的系统主要处理键值数据的逻辑,关系查询我们有另一套异构系统。


下图是我们的模板页,核心数据都是一样的,只是展示方式和一些前端逻辑不太一样。

wKioL1Xk-DnxDBJ8AALAHtAswSU723.jpg


wKiom1Xk9iewHvRXAAL0gU6Uv50064.jpg


我们详情页的前端展示主要分为这么几个维度:

  • 商品维度(标题、图片、属性等)

  • 主商品维度(商品介绍、规格参数)

  • 分类维度

  • 商家维度

  • 店铺维度

另外还有一些实时性要求比较高的如实时价格、实时促销、广告词、配送至、预售等是通过异步加载。


我们目前把数据按维度化存储,比如一些维度直接redis存,性能好。

wKioL1Xk-F7imE4jAAGfBRpWbUM950.jpg

京东商城还有一些特殊维度数据:比如套装、手机合约机等,这些数据是主商品数据外挂的,通过异步加载来实现的逻辑。还有一些与第三方合作的,如易车,很多数据都是无法异构的,都是直接异步加载的。目前有易车、途牛等一些公司有这种合作。


我们618当天PV数亿,服务器端TOP99响应时间低于38ms(此处是第1000次中第99次排名的时间,PV具体数据不便公开,但TOP99基本在40ms之内)。
wKiom1Xk9kqDdku2AAEIqH2FGg8944.jpg

上图是我们的一个监控图。我们详情页流量特点是离散数据,热点少,各种爬虫、比价软件抓取;所以如果直接查库,防刷没做好,很容易被刷挂。


商品详情页发展史


这是我们的一个架构历史。

wKioL1Xk-HuirG7qAAEi48K_i5w913.jpg

架构1.0

wKiom1Xk9m3y2RAcAACfwm0BeEA764.jpgIIS+C#+Sql Server,最原始的架构,直接调用商品库获取相应的数据,扛不住时加了一层memcached来缓存数据。


这种方式经常受到依赖的服务不稳定而导致的性能抖动。基本发展初期都是这个样子的,扛不住加层缓存。因此我们设计了架构2.0。


架构2.0

wKioL1Xk-Jniu-5aAAFCK9Z6CB8850.jpg该方案使用了静态化技术,按照商品维度生成静态化HTML,这就是一个静态化方案。

主要思路:

  1. 通过MQ得到变更通知;

  2. 通过Java Worker调用多个依赖系统生成详情页HTML;

  3. 通过rsync同步到其他机器;

  4. 通过Nginx直接输出静态页;

  5. 接入层负责负载均衡。


主要缺点:

  1. 假设只有分类、面包屑变更了,那么所有相关的商品都要重刷;

  2. 随着商品数量的增加,rsync会成为瓶颈;

  3. 无法迅速响应一些页面需求变更,大部分都是通过JavaScript动态改页面元素。


之前需求没那么多,因此页面变更不是很频繁,基本没什么问题。但是随着商品数量的增加这种架构的存储容量到达了瓶颈,而且按照商品维度生成整个页面会存在如分类维度变更就要全部刷一遍这个分类下所有信息的问题,因此我们又改造了一版按照尾号路由到多台机器。这种生成整个页面的方案会存在比如只有分类信息变了,也需要把这个分类下的商品重新刷一遍。


架构2.1


wKiom1Xk9oaQqknlAAGCtN8axBw571.jpg 主要思路:

  1. 容量问题通过按照商品尾号做路由分散到多台机器,按照自营商品单独一台,第三方商品按照尾号分散到11台;

  2. 按维度生成HTML片段(框架、商品介绍、规格参数、面包屑、相关分类、店铺信息),而不是一个大HTML;

  3. 通过Nginx SSI合并片段输出;

  4. 接入层负责负载均衡;

  5. 多机房部署也无法通过rsync同步,而是使用部署多套相同的架构来实现。


这种方式通过尾号路由的方式分散到多台机器扩容,然后生成HTML片段,按需静态化;当时我们做闪购的时候,需要加页头,都是通过js搞定的。但对于大的页面结构变更,需要全量生成。尤其像面包屑不一样的话会很麻烦,需要生成多个版本。


主要缺点:

  1. 碎片文件太多,导致如无法rsync;

  2. 机械盘做SSI合并时,高并发时性能差,此时我们还没有尝试使用SSD;

  3. 模板如果要变更,数亿商品需要数天才能刷完;

  4. 到达容量瓶颈时,我们会删除一部分静态化商品,然后通过动态渲染输出,动态渲染系统在高峰时会导致依赖系统压力大,抗不住;

  5. 还是无法迅速响应一些业务需求。


当时我记得印象最深的就是碎片文件太多,我们的inode不够了,经常要半夜去公司删文件。因为存在删除问题,每台服务器并不是全量,所以我们需要一个动态生成的服务,当静态化不存在的时候还原到动态服务;但这样双十一时压力非常大,我们依赖的系统随时都给我们降级。


架构3.0

我们的痛点:

  1. 之前架构的问题存在容量问题,很快就会出现无法全量静态化,还是需要动态渲染;(对于全量静态化可以通过分布式文件系统解决该问题,这种方案没有尝试)

  2. 最主要的问题是随着业务的发展,无法满足迅速变化、还有一些变态的需求。


其实最痛快的是业务来说我们要搞垂直,我们要模块化,我们要个性化;这些统统不好搞,因此我们就考虑做一版全动态的。其实思路和静态化差不多, 数据静态化聚合、页面模板化。


我们要考虑和要解决的问题:

  1. 能迅速响瞬变的需求,各种变态需求;

  2. 支持各种垂直化页面改版;

  3. 页面模块化;

  4. AB测试;

  5. 高性能、水平扩容;

  6. 多机房多活、异地多活。

wKiom1Xk9rrRmDI2AAERoCPfJyE265.jpg这是我们新的系统:三个子系统。

主要思路:

  1. 数据变更还是通过MQ通知;

  2. 数据异构Worker得到通知,然后按照一些维度进行数据存储,存储到数据异构JIMDB集群(JIMDB:Redis+持久化引擎,是基于Redis改造的一个加了持久化引擎的KV存储),存储的数据都是未加工的原子化数据,如商品基本信息、商品扩展属性、商品其他一些相关信息、商品规格参数、分类、商家信息等;

  3. 数据异构Worker存储成功后,会发送一个MQ给数据同步 Worker,数据同步Worker也可以叫做数据聚合Worker,按照相应的维度聚合数据存储到相应的JIMDB集群;三个维度:基本信息(基本信息+扩展属性等的一个聚合)、商品介绍(PC版、移动版)、其他信息(分类、商家等维度,数据量小,直接Redis存储);

  4. 前端展示分为两个:商品详情页和商品介绍,使用Nginx+Lua技术获取数据并渲染模板输出。


思路差不多: MQ得到变更通知,Worker刷元数据到JIMDB,前端展示系统取数据渲染模板。另外我们当时架构的目标是详情页上有的数据,我们都可以提供服务出去,主要提供单个商品的查询服务,所以我们把这个系统叫做动态服务系统。

wKiom1Xk9v_Rh5VcAADPz5dpYck952.jpg该动态服务分为前端和后端,即公网还是内网,如目前该动态服务为列表页、商品对比、微信单品页、总代等提供相应的数据来满足和支持其业务。


目前每天为列表页提供增量数据服务。微信上京东入口看到的详情页 也是我们这个服务提供的数据。APP的数据暂时没走我们的系统,不过我们目前系统实现的是平常流量的50倍左右,性能和流量基本不是问题。我们详情页架构设计的一些原则:

  1. 数据闭环

  2. 数据维度化

  3. 拆分系统

  4. Worker无状态化+任务化

  5. 异步化+并发化

  6. 多级缓存化

  7. 动态化

  8. 弹性化

  9. 降级开关

  10. 多机房多活

  11. 多种压测方案


因为我们这边主要是读服务,因此我们架构可能偏读为主的设计;目前我设计的几个系统都遵循这些原则去设计:


数据闭环
wKioL1Xk-OiDyvZAAAHnbC52kKQ347.jpg数据闭环,即数据的自我管理,或者说是数据都在自己系统里维护,不依赖于任何其他系统,去依赖化,这样得到的好处就是别人抖动跟我没关系。因此我们要先数据异构。


数据异构,是数据闭环的第一步,将各个依赖系统的数据拿过来,按照自己的要求存储起来;我们把很多数据划分为三个主要维度进行异构:商品信息、商品介绍和其他信息(分类、商家、店铺等)。


数据原子化处理,数据异构的数据是原子化数据,这样未来我们可以对这些数据再加工再处理而响应变化的需求。我们有了一份原子化异构数据虽然方便处理新需求,但恰恰因为第一份数据是原子化的,那么它会很分散,前端读取时mget的话 性能不是很好,因此我们又做了数据聚合。


数据聚合,是将多个原子数据聚合为一个大JSON数据,这样前端展示只需要一次get,当然要考虑系统架构,比如我们使用的Redis改造,Redis又是单线程系统,我们需要部署更多的Redis来支持更高的并发,另外存储的值要尽可能的小。


数据存储,我们使用JIMDB,Redis加持久化存储引擎,可以存储超过内存N倍的数据量,我们目前一些系统是Redis+LMDB引擎的存储,目前是配合SSD进行存储;另外我们使用Hash Tag机制把相关的数据哈希到同一个分片,这样mget时不需要跨分片合并。分片逻辑使用的是Twemproxy,和应用端混合部署在一起;减少了一层中间层,也节约一部分机器。


我们目前的异构数据是键值结构的,用于按照商品维度查询,还有一套异构时关系结构的用于关系查询使用。


数据维度化
对于数据应该按照维度和作用进行维度化,这样可以分离存储,进行更有效的存储和使用。我们数据的维度比较简单:

  1. 商品基本信息,标题、扩展属性、特殊属性、图片、颜色尺码、规格参数等;
    这些信息都是商品维度的。

  2. 商品介绍信息,商品维度商家模板、商品介绍等;
    京东的商品比较特殊:自营和第三方。
    自营的商品可以任意组合,选择其中一个作为主商品,因此他的商品介绍是商品维度。
    第三方的组合是固定的,有一个固定的主商品,商品介绍是主商品维度。

  3. 非商品维度其他信息,分类信息、商家信息、店铺信息、店铺头、品牌信息等;
    这些数据量不是很大,一个redis实例就能存储。

  4. 商品维度其他信息(异步加载),价格、促销、配送至、广告词、推荐配件、最佳组合等。


这些数据很多部门在维护,只能异步加载;目前这些服务比较稳定,性能也不错,我们在把这些服务在服务端聚合,然后一次性吐出去。现在已经这么做了几个,比如下面这个就是在服务端聚合吐出去的情况。
wKioL1Xk-T_CYuXWAAEDI3AOBCo686.jpghttp://c.3.cn/recommend?callback=jQuery4132621&methods=accessories%2Csuit&p=103003&sku=1217499&cat=9987%2C653%2C655&lid=1&uuid=1156941855&pin=zhangkaitao1987&ck=pin%2CipLocation%2Catw%2Caview&lim=6&cuuid=1156941855&csid=122270672.4.1156941855%7C91.1440679162&c1=9987&c2=653&c3=655&_=1440679196326

这是我们url的一些规则,methods指定聚合的服务。我们还对系统按照其作用做了拆分。


拆分系统
wKiom1Xk9yuBpEHSAAD_r2DCJDY810.jpg

将系统拆分为多个子系统虽然增加了复杂性,但是可以得到更多的好处。比如,数据异构系统存储的数据是原子化数据,这样可以按照一些维度对外提供服务;而数据同步系统存储的是聚合数据,可以为前端展示提供高性能的读取。而前端展示系统分离为商品详情页和商品介绍,可以减少相互影响;目前商品介绍系统还提供其他的一些服务,比如全站异步页脚服务。我们后端还是一个任务系统。


Worker无状态化+任务化
wKioL1Xk-VfzNS0HAAFMtBLAAdk557.jpg

  1. 数据异构和数据同步Worker无状态化设计,这样可以水平扩展;

  2. 应用虽然是无状态化的,但是配置文件还是有状态的,每个机房一套配置,这样每个机房只读取当前机房数据;

  3. 任务多队列化,等待队列、排重队列、本地执行队列、失败队列;

  4. 队列优先级化,分为:普通队列、刷数据队列、高优先级队列;
    例如,一些秒杀商品会走高优先级队列保证快速执行。

  5. 副本队列,当上线后业务出现问题时,修正逻辑可以回放,从而修复数据;可以按照比如固定大小队列或者小时队列设计;

  6. 在设计消息时,按照维度更新,比如商品信息变更和商品上下架分离,减少每次变更接口的调用量,通过聚合Worker去做聚合。


异步化+并发化

我们系统大量使用异步化,通过异步化机制提升并发能力。首先我们使用了消息异步化进行系统解耦合,通过消息通知我变更,然后我再调用相应接口获取相关数据;之前老系统使用同步推送机制,这种方式系统是紧耦合的,出问题需要联系各个负责人重新推送还要考虑失败重试机制。数据更新异步化,更新缓存时,同步调用服务,然后异步更新缓存。

可并行任务并发化,商品数据系统来源有多处,但是可以并发调用聚合,这样本来串行需要1s的经过这种方式我们提升到300ms之内。异步请求合并,异步请求做合并,然后一次请求调用就能拿到所有数据。前端服务异步化/聚合,实时价格、实时库存异步化,使用如线程或协程机制将多个可并发的服务聚合。异步化还一个好处就是可以对异步请求做合并,原来N次调用可以合并为一次,还可以做请求的排重。


多级缓存化
因之前的消息粒度较粗,我们目前在按照一些维度拆分消息,因此读服务肯定需要大量缓存设计,所以我们是一个多级缓存的系统。


浏览器缓存,当页面之间来回跳转时走local cache,或者打开页面时拿着Last-Modified去CDN验证是否过期,减少来回传输的数据量;


CDN缓存,用户去离自己最近的CDN节点拿数据,而不是都回源到北京机房获取数据,提升访问性能;


服务端应用本地缓存,我们使用Nginx+Lua架构,使用HttpLuaModule模块的shared dict做本地缓存( reload不丢失)或内存级Proxy Cache,从而减少带宽。


我们的应用就是通过Nginx+Lua写的,每次重启共享缓存不丢,这点我们受益颇多,重启没有抖动,另外我们还使用使用一致性哈希(如商品编号/分类)做负载均衡内部对URL重写提升命中率;我们对mget做了优化,如去商品其他维度数据,分类、面包屑、商家等差不多8个维度数据,如果每次mget获取性能差而且数据量很大,30KB以上;而这些数据缓存半小时也是没有问题的,因此我们设计为先读local cache,然后把不命中的再回源到remote cache获取,这个优化减少了一半以上的remote cache流量;这个优化减少了这个数据获取的一半流量;


服务端分布式缓存,我们使用内存+SSD+JIMDB持久化存储。


动态化
我们整个页面是动态化渲染,输出的数据获取动态化,商品详情页:按维度获取数据,商品基本数据、其他数据(分类、商家信息等);而且可以根据数据属性,按需做逻辑,比如虚拟商品需要自己定制的详情页,那么我们就可以跳转走,比如全球购的需要走jd.hk域名,那么也是没有问题的;未来比如医药的也要走单独域名。


模板渲染实时化,支持随时变更模板需求;我们目前模板变更非常频繁,需求非常多,一个页面8个开发。


重启应用秒级化, 使用Nginx+Lua架构,重启速度快,重启不丢共享字典缓存数据;其实我们有一些是Tomcat应用,我们也在考虑使用如Tomcat+Local Redis 或 Tomcat+Nginx Local Shared Dict 做一些本地缓存,防止重启堆缓存失效的问题。


需求上线速度化,因为我们使用了Nginx+Lua架构,可以快速上线和重启应用,不会产生抖动;另外Lua本身是一种脚本语言,我们也在尝试把代码如何版本化存储,直接内部驱动Lua代码更新上线而不需要重启Nginx。


弹性化
我们所有应用业务都接入了Docker容器,存储还是物理机;我们会制作一些基础镜像,把需要的软件打成镜像,这样不用每次去运维那安装部署软件了;未来可以支持自动扩容,比如按照CPU或带宽自动扩容机器,目前京东一些业务支持一分钟自动扩容,下个月会进行弹性调度尝试。


降级开关
一个前端提供服务的系统必须考虑降级,推送服务器推送降级开关,开关集中化维护,然后通过推送机制推送到各个服务器;


可降级的多级读服务,前端数据集群―->数据异构集群―->动态服务(调用依赖系统);这样可以保证服务质量,假设前端数据集群坏了一个磁盘,还可以回源到数据异构集群获取数据;基本不怕磁盘坏或一些机器故障、或者机架故障。


开关前置化,如Nginx代替Tomcat,在Nginx上做开关,请求就到不了后端,减少后端压力;我们目前很多开关都是在Nginx上。


可降级的业务线程池隔离,从Servlet3开始支持异步模型,Tomcat7/Jetty8开始支持,相同的概念是Jetty6的Continuations。我们可以把处理过程分解为一个个的事件。


通过这种将请求划分为事件方式我们可以进行更多的控制。如,我们可以为不同的业务再建立不同的线程池进行控制:即我们只依赖tomcat线程池进行请求的解析,对于请求的处理我们交给我们自己的线程池去完成;这样tomcat线程池就不是我们的瓶颈,造成现在无法优化的状况。通过使用这种异步化事件模型,我们可以提高整体的吞吐量,不让慢速的A业务处理影响到其他业务处理。慢的还是慢,但是不影响其他的业务。我们通过这种机制还可以把tomcat线程池的监控拿出来,出问题时可以直接清空业务线程池,另外还可以自定义任务队列来支持一些特殊的业务。


去年使用的是JDK7+Tomcat7 最近一个月我们升级到了JDK8+Tomcat8+G1。
wKiom1Xk90nS3RF5AAHAC6Nby4Y497.jpg



本文策划陈刚@北京智识,内容由刘世杰@猎聘网编辑,四正@大连华信校对与发布,其他多位志愿者对本文亦有贡献。更多关于架构方面的内容,读者可以通过搜索"ArchNotes"或点击页首的蓝字,关注"高可用架构"公众号,查看更多架构方面内容,获取通往架构师之路的宝贵经验。转载请注明来自"高可用架构 (ArchNotes)"微信公众号。


未完待续 后面精彩部分请阅读第二部分:http://mengphilip.blog.51cto.com/2243393/1690373

你可能感兴趣的:(亿级架构)