流量洪峰下的亿级商品详情页架构解密

2020-06-11 15:00:06 浏览数 (1)

围观改变微博,大促改变电商,网络洪峰进攻猛烈,相对于微博不定时的流量炸弹,电商平台每年大促时间则稳定得多,对电商平台来说下一个大促已经打响,抗洪抢险责任重大,让我们近距离观摩一下,京东平台如何筑起高可用大堤应对。 本文选自《高可用架构(第1卷)》

伴随着网站业务发展,需求日趋复杂多样并随时变化。传统静态化方案会遇到业务瓶颈,不能满足瞬变的需求。因此,需要一种能高性能实时渲染的动态化模板技术来解决这些问题。本文和大家分享一下最近一年做的京东商品详情页的架构升级的心路历程。

  • 商品详情页发展史

下图展示了我们的架构历史,本文将重点介绍架构3.0。(微信后台回复“历史”了解更多架构版本资讯)

  • 架构3.0

对于架构3.0,我们的痛点如下:

  • 之前架构的问题存在容量问题,很快就会出现无法全量静态化,还是需要动态渲染(对于全量静态化可以通过分布式文件系统解决该问题,这种方案没有尝试)。
  • 最主要的问题是随着业务的发展,无法满足迅速变化、还有一些变态的需求。

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

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

  • 能迅速响应瞬变和各种变态的需求。
  • 支持各种垂直化页面改版。
  • 页面模块化。
  • AB 测试。
  • 高性能、水平扩容。
  • 多机房多活、异地多活。

下图展示了我们新的系统:3 个子系统。

主要思路如下:

  • 还是通过MQ 通知数据变更。
  • 数据异构Worker 得到通知,然后按照一些维度进行数据存储,存储到数据异构JIMDB 集群(JIMDB:Redis 持久化引擎,它是基于Redis 改造的一个加了持久化引擎的KV 存储),存储的数据都是未加工的原子化数据,如商品基本信息、商品扩展属性、商品其他一些相关信息、商品规格参数、分类、商家信息等。
  • 数据异构Worker 存储成功后,会发送一个MQ 给数据同步Worker,数据同步Worker也可以叫作数据聚合Worker,按照相应的维度聚合数据存储到相应的JIMDB 集群。3 个维度是基本信息(基本信息 扩展属性等的一个聚合)、商品介绍(PC 版、移动版)、其他信息(分类、商家等维度,数据量小,直接Redis 存储)。
  • 前端展示分为2 部分:商品详情页和商品介绍,使用Nginx Lua 技术获取数据并渲染模板输出。

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

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

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

  • 数据闭环。
  • 数据维度化。
  • 拆分系统。
  • Worker 无状态化 任务化。
  • 异步化 并发化。
  • 多级缓存化。
  • 动态化。
  • 弹性化。
  • 降级开关。
  • 多机房多活。
  • 多种压测方案。

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

1 . 数据闭环

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

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

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

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

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

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

2 . 数据维度化

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

  • 商品基本信息,标题、扩展属性、特殊属性、图片、颜色尺码、规格参数等;这些信息都是商品维度的。
  • 商品介绍信息,商品维度商家模板、商品介绍等;京东的商品比较特殊:自营和第三方。

自营的商品可以任意组合,选择其中一个作为主商品,因此他的商品介绍是商品维度。

第三方的组合是固定的,有一个固定的主商品,商品介绍是主商品维度。

  • 非商品维度其他信息,分类信息、商家信息、店铺信息、店铺头、品牌信息等;这些数据量不是很大,一个Redis 实例就能存储。
  • 商品维度其他信息(异步加载),价格、促销、配送至、广告词、推荐配件、最佳组合等。

这些数据有很多部门在维护,只能异步加载。目前这些服务比较稳定,性能也不错,我们在把这些服务在服务端聚合,然后一次性吐出去。现在已经这么做了几个,比如下图就是在服务端聚合吐出去的情况。

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

http://c.3.cn/recommend?callback=jQuery4132621&methods=accessories,suit&p=103003&sku=1217499&cat=9987,653,655&lid=1&uuid=1156941855&pin=zhangkaitao1987&ck=pin,ipLocation,atw,aview&lim=6&cuuid=1156941855&csid=122270672.4.1156941855|91.1440679162&c1=9987&c2=653&c3=655&_=1440679196326

3 . 拆分系统

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

4 . Worker 无状态化 任务化

  • 数据异构和数据同步Worker 无状态化设计,这样可以水平扩展,如下图所示。
  • 应用虽然是无状态化的,但是配置文件还是有状态的,每个机房一套配置,这样每个机房只读取当前机房数据。
  • 任务多队列化,等待队列、排重队列、本地执行队列、失败队列。
  • 队列优先级化,分为普通队列、刷数据队列、高优先级队列。例如,一些秒杀商品会走高优先级队列保证快速执行。
  • 副本队列,当上线后业务出现问题时,修正逻辑可以回放,从而修复数据。可以按照比如固定大小队列或者小时队列设计。
  • 在设计消息时,按照维度更新,比如商品信息变更和商品上下架分离,减少每次变更接口的调用量,通过聚合Worker 去做聚合。

5 . 异步化 并发化

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

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

6 . 多级缓存化

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

浏览器缓存,当页面之间来回跳转时走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 持久化存储。

7 . 动态化

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

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

重启应用秒级化,使用Nginx Lua 架构,重启速度快,重启不丢共享字典缓存数据。

其实我们有一些是Tomcat 应用,我们也在考虑使用如Tomcat Local Redis 或Tomcat NginxLocal Shared Dict 做一些本地缓存,防止出现重启堆缓存失效的问题。

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

8 . 弹性化

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

9 . 降级开关

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

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

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

可降级的业务线程池隔离,从Servlet 3 开始支持异步模型,Tomcat 7/Jetty 8 开始支持,相同的概念是Jetty 6 的Continuations。我们可以把处理过程分解为一个个的事件。

通过这种将请求划分为事件的方式我们可以进行更多的控制。例如,我们可以为不同的业务再建立不同的线程池进行控制:即我们只依赖tomcat 线程池进行请求的解析,对于请求的处理我们交给自己的线程池去完成,如下图所示。这样tomcat 线程池就不是我们的瓶颈,从而造成现在无法优化的状况。通过使用这种异步化事件模型,可以提高整体的吞吐量,不让慢速的A 业务处理影响到其他业务处理。慢的还是慢,但是不影响其他的业务。

我们通过这种机制还可以把tomcat 线程池的监控拿出来,出问题时可以直接清空业务线程池,另外还可以自定义任务队列来支持一些特殊的业务。

在2014 年时,我们使用的是JDK 7 Tomcat 7,目前是JDK 8 Tomcat 8。

10 . 多机房多活

对于我们这种核心系统,需要考虑多机房多活的问题。目前是应用无状态,通过在配置文件中配置各自机房的数据集群来完成数据读取,如下图所示。

其实我们系统只要存储多机房就多活了,因为系统天然就是。数据集群采用一主三从结构,如下图所示,防止当一个机房挂了,另一个机房因压力大而产生抖动。各个机房都是读本机房副本数据,且每个机房都是2 份副本数据,不会因为机房突然中断而受影响。

11 . 多种压测方案

我们在验证系统时需要进行压测。

线下压测,Apache ab,Apache Jmeter,这种方式是固定URL 压测,一般通过访问日志收集一些URL 进行压测,可以简单压测单机峰值吞吐量,但是不能将其作为最终的压测结果,因为这种压测会存在热点问题。

线上压测,可以使用TCPcopy 直接把线上流量导入到压测服务器,这种方式可以压测出机器的性能,还能把流量放大,也可以使用Nginx Lua 协程机制把流量分发到多台压测服务器或者直接在页面埋点,让用户压测,此种压测方式可以不给用户返回内容。服务刚开始的时候大量使用TCPcopy 做验证,对于一些新服务,如果无法使用TCPcopy 我们就在页面埋URL 让用户来压。

另外在压测时,要考虑同时压读、写、读或写。只压某一种场景可能都会不真实。

  • 我们将在后续的推送中和大家分享“实践中遇到的一些问题和解决方案”

0 人点赞