京东手机商品详情页技术解密
作者:陈保安,2011年加入京东,目前主要负责手机京东核心业务(搜索、商品、购物车、结算、收银台、我的京东)的后端研发工作。带领团队在一线奋战多年,积累了非常丰富的大促备战经验,也见证了核心系统从一分钟几千单到几十万单的质的蜕变。
京东手机单品页在每次大促时承载所有流量的入口,它被天然赋予的一个标签就是抗压,对系统的稳定性、性能方面要求极其苛刻,另外单品页本身业务复杂度较高,单品页有几十种垂直流程业务,并且展示上都要求个性化的单品页,加上依赖有50+的基础服务,稍有抖动,对整体服务质量都会有比较大的影响,因此之前大促也出现过各种问题,不断打磨,持续优化升级,当前系统架构可支撑接近百万的QPS瞬时访问,并且今年双11表现非常平稳,借此机会一块和大家做一次分享。
一、先聊聊APP接口开发的特点
1、 手机网络、流量受限
- 手机单品页提供给APP的API受限于运营商的网络,手机的信号时有时无、时好时坏极其不稳定,为了减少客户端和后端建连握手的过程,因此接口下发内容大而全,涵盖了页面上的所有内容,没办法像浏览器BS的结构可以有大量的ajax请求;
- API接口依赖了几十个基础服务,任何接口的抖动对整体接口性能影响很大,因此必须是并发请求依赖,减少接口抖动叠加的影响;
- 单品页有大量的图片信息,商品主图、插图、推荐商品、手机配件商品、排行榜等等图片信息量比较大,单个图片的大小对手机流量影响较大,所有下发的图片采用是webp格式,极大减少网络传输流量。
2、 手机不同分辨率、网络环境、系统版本的适配
- 不同环境下用户的体验存在差异,比如在弱网、低版本、分辨率差的手机会保持最基本的购物车流程,会减少一些增值的体验;
- 图片的展示尺寸也会根据网络环境、分辨率大小进行适配。
3、 APP版本兼容
- 新业务需求变更尽可能兼容老版本,但有些业务很难兼容老版本,因此系统里面存在很多版本适配的逻辑,增加了系统的复杂度;
- 客户端如果出现重大bug并且没办法进行hotfix的情况下,需要服务端针对特定版本进行打补丁,也增加代码复杂度以及后期的维护成本。
因此APP的接口开发逻辑复杂度和后续的维护成本被放大很多。
二、单品页业务系统架构
这是当前单品页系统的整体架构图,其他的核心交易流程,比如购物车、下单等也都基本类似,单品页系统主要有三个进程:OpenResty、Tracer-Collect、Tomcat,以及包含几个旁路系统。OpenResty是nginx层的web容器,主要职责是做静态化和限流防刷,只有经过清洗过的流量才会流转到tomcat的java进程真正的业务处理,Tracer-Collect进程是通过旁路的方式异步埋点到统一的监控平台,进行实时的数据分析。
三、核心技术点
1、 OpenResty
这个是在今年618之前架构上做的一个变化,主要有以下几点考虑:
- 业务需要,业务流量到一定程度,需要把静态化数据以及限流策略前置,更多把流量挡在前端,减少业务系统压力;
- ngx_openresty模块有效地把Nginx 服务器转变为一个强大的 Web 应用服务器,在其他0级系统中已经很好验证了带来的高可用、高并发的能力。
使用规范上
- Lua属于脚本语言,开发相对java语言比较开放,比如方法可以返回多对象,这对长期java开发人员就有很多不适应,在灰度过程中及时发现并进行修复,因此利用lua来满足特殊需求外,不会进行过多业务逻辑处理;
- 任何技术上的变动都要极度谨慎,不断的进行灰度测试,我们是从一台机器逐步灰度到一个set,再扩散到一个渠道,最后全量,并且具备实时的异常数据埋点能力,及时发现灰度过程中问题,一旦发现问题要有开关能实时降级。
Lua语言对于很多团队都使用过程中都遇到各种问题,今年双11的总结会上也有团队分享大促期间lua死锁问题,我们这里遇到的一个场景是zk的配置数据同步到lua时一定概率出现死锁。
原因:lua运行在nginx主线程中,但zk在nginx主线程外启动新的线程watch,当zk更新时通过这个新线程通知数据更新,这时我们在这个新的线程中直接调用lua代码,会有概率产生死锁。
解决方案:在这个新线程中不直接调用lua代码,而是通过http协议直接进入nginx主线程更新配置数据。
2、 数据静态化
单品页给APP提供的API重点包含两个,一个是静态接口,一个是动态接口数据,这里提到的静态化重点是针对静态接口数据,包含商品图片、基本信息、店铺商家信息、颜色尺码、延保…..等,去年双11期间,由于一些热点商品访问量过大,对jimdb集群单个分片的连接数和操作数都非常高,服务压力过大,整体集群服务性能变差,因此针对此进行了三级热点的优化:
- CDN
众所周知,CDN本来就是替业务静态流量扛热点数据,但是上边提到后端有很多的适配工作,包括平台、网络环境、分辨率尺寸,要知道Android的分辨率五花八门,所以这种逻辑的话CDN很难发挥作用,因此今年针对这个逻辑做了优化,接口下发给APP的数据都是标准数据格式,同时会下发对应的适配规则给APP,由APP根据规则进行动态适配,极大地提升了缓存命中率,另外别忘了还要加上各种开关控制和数据的埋点监控,这也是APP开发的一个重要特征,APP发出去的版本如果出现各种未知情况将会是灾难,在618之前版本经过各种灰度最终还是顺利上线,在618期间发挥了重要作用,CDN的命中率达到60%以上,大促的0点开始大部分人还是集中在少数的爆款商品上。这是第一层的保护。
- OpenResty(Nginx+Lua)层静态化
上边提到这一层重点还是数据静态化和防刷,您可能有疑问,CDN已经替挡住了大部分流量,为什么还需要这一层?CDN只是挡住了App的最新版本热点流量,还有M渠道是通过内网网关过来的,不经过CDN,以及App的老版本也是不走CDN,因此这一层主要依赖Nginx的共享缓存,分配100M的共享空间,在大促时命中率也可以到接近20%。
- JVM本地缓存
JVM的堆内存主要是针对商品的基本信息和特殊属性信息进行本地缓存,支持动态接口商品热点数据,依赖Guava组件实现的LRU缓存淘汰算法,大致5000个热点sku数据,数据量在5M左右,这是第三层的数据保护,大促时命中率在27%左右,另外强调一下,这里的java对象可动态配置成弱引用或者是软引用,一般建议采用弱引用,这样避免内存增长过快,导致频繁的GC。
3、 数据异构,减少强依赖
数据异构带来的好处是可以减少一些基础服务的强依赖,之前老板提的一个目标就是基础服务挂了,上层业务还能很好的活着,但是京东这个数据体量来看成本是非常巨大的,因此APP单品页选择部分数据异构,减少基础服务接口的强依赖,主要是商品的基础数据、扩展属性信息、商品的详情数据,全量数据同步一次之后通过中间件JMQ进行增量的数据同步变更,存储使用的是缓存中间件jimdb(redis缓存)。
4、 并发请求异步化
APP单品页前期属于野蛮发展,很多RPC的依赖极其不合理,比如依赖关系没有层次概念,超时时间设置超长、内外网接口同时依赖,造成任何的服务质量变差和网络抖动对整体API影响非常大,因此进行了一次SOA化改造,主要工作是把单品页系统从大网关分离出来,然后制定服务接入标准并进行改造,第三方面就是上游基础服务调用并行化,系统整体并发能力及稳定性得到了极大的提升。
服务依赖的标准
- 依赖接口必须是内网服务,不允许依赖外网服务;
- 接口超时时间不超过100ms,并且除了一些核心数据,比如商品、价格、库存,其他都不进行重试;
- 核心接口必须可支持跨机房的双活容灾,client端出现问题必须可切换,并且要有降级方案;
- RPC调用最好是依赖中间件JSF,这样是点对点的长连接服务,减少每次建连的开销,HTTP依赖需要经过内网的LB,增加一层代理的开销,会出现一些不可控的问题。
随着流量不断增加,并行化遇到了瓶颈,每次请求会创建大量的线程,线程的维护和上下文切换成本本身比较消耗CPU资源,因此基于现有HttpClient和JSF基础组件的异步化支持,进一步进行异步化的改造,单机压测效果还是比较明显,并发能力提升40%。
5、 监控
系统流量到一定程度,系统的各维度监控尤为重要,可以帮助我们缩短排查、定位问题的时间,甚至可以帮助预警风险,当前APP业务从用户到后端整个服务链条的监控都已经非常完善,包括各运营商入口流量的监控、内外部网络质量、负载均衡、以及网关流量的监控以外,我重点介绍下单品页业务层的监控,下边是业务监控系统数据异步埋点的架构,主要分为两类数据,第一业务指标数据比如单品页各渠道访问数据,通过UDP协议实时埋点到Kafka,然后storm实时在线分析形成最终需要的数据落地,另一类是大流量数据,比如系统异常信息落到磁盘日志中,然后通过logCollector异步发送到Kafka中,这类数据对磁盘IO、网卡IO的流量占比大,针对磁盘IO,会按照文件大小100M滚动生成日志文件,数据搬走之后进行删除操作,网卡IO在数据传输过程中进行了限速,按照1m/s的速度进行传输,可进行动态调整,基本对业务不产生任何影响,大促峰值期间会针对一定比例降级。
业务系统除了基本的服务器各项指标CPU、MEM监控,服务的性能、可用率监控以外,介绍几个比较实用的业务能力监控:
- 方法tree监控,一次请求在单品页SOA系统内部所经过的每一个类的方法作为结点形成这么一颗树,这棵树非常直观看到系统内部的依赖结构关系和任何一个节点的请求量的大小,如果性能、可用率、异常量、访问量出现异常可第一时间标红报警,出现任何故障可第一时间聚焦到具体某一个点上,极大提升了定位和处理故障的时间;
- 异常监控,系统依赖的外部服务以及系统内部抛出的任何一条异常的堆栈信息都被异步埋点记录下来,进行实时的统计和报警,对系统健康度的把控起到至关重要的作用;
- 用户行为的跟踪,详细记录用户在什么时间做了什么事情以及当时的网络情况,方便用户出现问题时回放出问题时的现场,快速帮助用户解决问题。
监控细节还有很多,以上几个监控手段对当前业务系统帮助非常大也是非常实用的一些能力,现在业务系统已经做到非常透明,任何人都可以清晰看到系统的健康度,并且部分服务具备通过故障自动容错的能力,对系统整体的稳定性提供了非常大的保障。
6、 限流手段
京东APP上所有商品价格库存都是分区域的,因此很多刷子以及爬虫不断的来爬京东各区域的价格、库存和促销信息等,有一个很明显的特征就是大量刷子刷时用户登录态的占比会明显下降,因此单品页针对用户的行为实时行为数据进行分析和控制:
- 流量清洗能力,根据用户的pin、IP的实时分析和清洗,并可以根据已登录和未登录做不同策略;
- 系统过载保护能力,任何时候不能让系统挂掉,把明显的恶意流量清洗之后进行按一定策略进行排队,保障流量不超过系统极限负载,同时给用户比较友好的一些交互体验。
7、 压测
单品页压测比较麻烦,一方面压测的流量大,对线上可能会造成很多不可预知的问题,另一方面涉及的基础服务比较多,牵涉的人就多,每次压测要协调上下游几十号人支持,协调成本比较高,第三方面压测的商品数量都在上百万的商品,每次压测的SKU会变更,脚本变更比较大,第四每次压测完成之后需要人工形成压测报告并分析其中的薄弱环节问题,因此APP端产出了一个自己的压测平台,通过流程方面来协助解决以上几个问题:
- 启动压测任务可自动收集压测数据并产出需要的压测脚本;
- 线上流量的隔离;
- 通知相关方,确认压测时间和压测方案;
- 按照压测脚本逐步进行压测任务执行;
- 形成压测报告,并分析压测过程中问题点。
压测数据准备方面:
- 线上流量日志进行回放,并且按照压测目标放大到一定倍数来回放;
- 按照各品类的流量占比选出一部分商品作为热点数据来进行压测,检验各环节对热点数据的处理是否合理;
- 针对一些埋点以及统计要能清洗掉这部分数据,目前主要是根据请求的一些固定特征,比如设备号和特殊标识来进行区分,并且上下游都要能做到一致的数据清洗规则。
四、未来方向
单品页还有很大的一些优化空间,比如为适应快速的业务迭代进行系统重构、jvm垃圾收回策略和堆内存分配大小的调整、异步化的改造等等优化正在进行,未来单品页最重要的几个方向:
- 动态配置化:不同品类商品可根据单品页元素动态形成一个个性化的单品页,做到完全楼层可配置化;
- 精细化:流控、自动化降级等方面能够根据用户特征,比如用户级别、地域等可执行不同策略;
- 智能化:根据用户画像数据展示更多个性化推荐的数据,千人千面,给用户提供更有价值的信息。