京东零售系统在2018年实现前中台架构的调整与划分。中台实现基础服务组件开发,前台主要对接用户请求,通过对中台RPC数据的聚合,来满足用户多样化需求。其前台系统又分为首页系统、单品系统、搜索系统、列表系统、订单系统等等。
作为PC首页研发,本文主要讲解PC首页的技术实现。
PC首页业务逻辑从最初的模板渲染,到后来的SSI模块加载以及现在的前后端分离。开发语言也从之前的ASP、php逐步过渡到以LUA为主的技术框架。通过技术迭代升级,首页页面打开速度从之前的200ms缩减到30ms内,API性能从之前的500ms优化至100ms左右。
京东零售系统,每天承载着亿万网民的购物需求。PC首页又是京东商城的一级入口,所以系统必须达到以下3方面要求:页面完整性(容灾、兜底、降级);流畅的加载速度(高并发、页面加载优化、API加载优化);监控&告警。
下面将根据系统设定目标逐步讲解首页系统实现方案。
1.页面完整性
页面完整性指任何时候访问页面均需呈现正常的页面样式,不得出现天窗以及非200状态码(40x、50x等)。如下图楼层,若将单模块缺失直接呈现给用户,将导致页面天窗,影响体验。在单模块异常时,系统可对整楼层隐藏操作,优先保证页面完整显示。
下面从6方面讲解容灾降级的业务逻辑:
页面的容灾:前端页面主要承载页面骨架,也会包含部分开关配置及必要的兜底数据,即使后端API服务异常,html依然可以提供基础的用户体验。前端页面由模板渲染生成,其中模板变量通过配置中心(蜂巢系统)维护,定时worker触发生成前端页面,然后推送到静态资源服务器,最终通过CDN加速提供服务。其中在worker触发生成html阶段执行多层验证逻辑,保证html的完整性。
接口的容灾:一般接口逻辑均是优先读取缓存数据,若缓存失效则继续读取上游RPC数据来输出。但上游系统均不保证100%可用,如何保证在上游异常的情况下提供可靠服务使页面正常渲染呢?PC首页系统在接口层面做了2方面工作:
第一、接口在读取&聚合上游RPC数据后会保存两部分缓存,一个为正常用户加速缓存,一般会设置5min过期时间,一个是兜底缓存,永不过期。当缓存或者上游RPC服务均不可用时,接口会读取兜底缓存保证正常的数据输出。
第二、如果API服务由于自身问题(例如宿主机异常、redis服务异常、用户网络抖动等)导致无法提供正常的服务怎么办。为了避免这个问题,系统为API提供了一条新的访问途径-CDN API。CDN API会访问由Worker定时生成的静态结果集(File)输出数据。当前端异步加载数据感知常规线路异常后自动触发CDN API线路来保证接口可用性。
依据上面2种兜底逻辑,绘制如下流程图,此逻辑将首页API服务的可靠率提高至100%。
接口的降级:如下图所示,正常情况接口通过读取Cache、RPC数据、兜底Cache提供服务。但如果Cache和RPC服务均异常,接口的响应时间就大大浪费在前两阶段(10ms+100ms)。为了避免此情况的发生,系统通过配置中心(蜂巢系统)设置降级开关,当系统感知降级开关打开时,将直接读取兜底Cache,保证API的响应速度。
Nginx的容灾:NGINX容灾指系统监控到接口非200状态码的时候在NGINX层面执行兜底逻辑,此兜底逻辑可以读取(或代理)远程静态资源实现透明容灾。具体实现通过error_page指令完成,error_page指令可以在特定的状态码设置一个named location,并在其代码块中执行相应的兜底业务逻辑。
楼层容灾:在多模块楼层,前端会调用多个API进行页面渲染。如下图所示,此楼层一共包含4个模块,每个模块均提供独立的API接口。当其中一个模块API异常,即触发楼层隐藏动作,避免天窗的出现。(这里执行的是页面可以有损但必须完整的策略)
终极容灾(页面CDN兜底):在2016年,启动“永不消失的首页”项目,即不论情况如何极端(包括Redis服务异常、服务器异常、RPC异常、网络异常等等),系统均可快速响应并切换至历史页面来保证页面完整。
为实现历史快照,项目借鉴爬虫技术,定时抓取PC首页的完整数据,并将快照数据推送至静态服务器。当监控到系统异常时开启降级开关将页面及时回滚至特定历史版本。此降能绕开正常的服务流程,直接读取兜底的静态资源。具体流程如下:
(此项目自上线后尚未触发一次。在其他的业务场景中,需要考虑各自实际情况)
2.流畅的加载速度
既要保证页面完整性,也要保证页面以及API的加载速度。下面从性能方面讲解系统的优化方向。
技术选型
首页是一种重性能、轻逻辑的业务场景,没有过多的基础服务依赖,主要与Redis和上游RPC做交互。系统通过读取上游RPC数据后聚合、过滤、验证后输出,最终完成页面展示。用户请求简化流程图如下:
结合业务场景,京东首页在2015年开始调研并尝试使用OpenResty服务,基于NGINX的异步事件模型以及高性能的脚本语言LUA,OpenResty完美胜任京东首页的高并发场景。经过近几年的沉淀,OpenResty成为京东首页的基础架构,也以此沉淀了OpenLua开发框架。
如上图所示,OpenResty(Nginx)服务的请求处理拥有11个阶段之多,每个阶段都有其特定的业务场景。在init_by_lua*阶段,框架初始化环境变量、init_worker_by_lua*阶段初始化降级开关以及基础配置,并将其同步至共享cache(ngx.shared.DICT)、 init_write_by_lua*阶段初始化路由策略、access_by_lua*阶段实现访问权限验证,加解密等、content_by_lua*阶段实现主体业务逻辑、log_by_lua*阶段实现日志以及Ump信息的输出。
Redis HotKey(热key)优化:在多数的业务场景中都使用Redis集群为服务加速,通过集群的横向扩展能力增加系统吞吐率。但在特殊的业务场景会有热点数据的出现,导致流量倾斜,增加单个分片的压力,这样就极大制约API的效率,极端情况甚至可以拖垮Redis集群。如何避免热点数据有2种方案:1、通过复制热点数据将数据存放到不同的Redis分片,每次通过随机算法读取;2、热点数据前置、减少Redis的压力。由于第一种方案实现繁琐,系统使用第二种方案解决热点数据,也就是将发现的热点数据存储到本地cache中(ngx.shard.DICT),通过本地cache服务直接对外提供服务,由于ngx.shard.DICT效率极高,极大优化API的响应时间。
Redis BigKey优化:项目开发中BigKey是不可忽略的性能点,可能在前期开发以及压测时均可以完美达到设定值,但是线上环境效率不理想,这时候就要考虑bigkey的因素了。由于bigkey占用内存较多,不但会使网卡成为瓶颈,在set、get也会阻塞其他操作,直接导致Redis吞吐量下降。
在PC首页系统bigkey的定义范围为>5k,只要key的值大于5k均需要单独处理。处理方案依然是2种。1、业务上做切割。例如一个业务场景需要将一个大的商品列表放到Cache中, 一般做法是通过kv(set key value)保存到Redis中。这时业务优化方案是将一个key扩展到1+n个key,一个key存储id list,其余n个key存储id对应的详情信息,这样就将bigkey打散为多个key。2、技术上做切割。例如系统定义的bigkey为5k,通过算法将value以5k的界限做切割,然后系统存储一个关系key以及n个小key实现bigkey转小key的过程。
不管使用哪个方案,目标是一致的,避免bigkey的出现。在将bigkey转小key的过程中,合理使用管道技术减少redis的网路消耗。
分布式任务的使用:随着个性化推荐算法的推广,商城首页全面接入推荐算法。由于个性化算法基于实时计算,耗时较高,为了保证用户浏览的流畅度,系统引入分布式服务。如下图所示,用户在请求第一屏的时候将用户信息push到分布式任务系统,分布式任务利用时间差计算下面楼层的个性化数据并存储到Redis。当用户访问到特定楼层即可快速加载数据。
并发请求:每个API都会涉及多个上游RPC服务,为了避免串行请求带来的耗时累加,系统将每个上游RPC封装成单个子API服务,然后通过ngx.location.capture_multi命令实现并发请求来优化API耗时。
磁盘IO优化:为了减少磁盘io,系统日志模块使用批量写入策略来减少磁盘io的消耗。在OpenResty中每个请求(包含自请求)都会初始化一个 ngx.ctx全局表,首页应用将当前请求在不同阶段产生的日志内容统一写入ngx.ctx.Log表中,并在log_by_lua*阶段统一触发io操作将日志写入文件系统。
图片优化:电商系统页面会引入大量的图片,虽然图片系统引入CDN服务,但单个域在高并发的时候依然会有堵塞。为了解决单域问题,我们将图片部署到多个域上,例如img10.360buyimg.com、img11.360buyimg.com、img12.360buyimg.com等等。但多域会叠加DNS解析耗时,此时页面可以引入预解析指令(dns-prefetch)来减少dns的解析时间,提高图片加载速度。另外使用懒加载也是优化的一个技巧。
3.监控&报警
原有的报警信息通过 “系统日志->研发->产品->运营”的方式层层传递,不但处理问题时效长,而且占用研发资源。为了简化报警流程,现有系统直接将报警信息同步给产品&运营,减少信息流通环节,提高效率。
增加报警逻辑的同时不影响系统性能,系统通过异步非阻塞的ngx.socket.tcp组件实现消息同步。具体流程为系统将日志通过ngx.socket.tcp组件传递给分布式任务,分布式任务通过异步方式将日志同步到Elastic集群。报警系统根据报警规则触发报警信息发送。(注:使用ngx.socket.tcp组件请合理使用连接池,避免频繁建立连接)
小结
随着首页系统的迭代升级,团队沉淀了蜂巢系统平台以及OpenLua框架。蜂巢系统通过积木赋能、组件化思想搭建,可快速响应业务需求。OpenLua框架实现Lua的MVC分层结构,并融入京东内部组件,在满足API性能的同时提升开发效率。
继续前行,永不止步。在之后的的研发道路上,将继续沉淀业务需求,强化组件化、标准化思想,深耕蜂巢系统、OpenLua框架,使之更加灵活、高效、易用。