2018年,JD.COM零售系统将实现前中台结构的调整和分工。中间站实现基础服务组件的开发,前台主要满足用户的请求,通过中间站RPC数据的聚合,满足用户的多样化需求。其前台系统分为
2018年,JD.COM零售系统将实现前中台结构的调整和分工。中间站实现基础服务组件的开发,前台主要满足用户的请求,通过中间站RPC数据的聚合,满足用户的多样化需求。其前台系统分为主页系统、项目系统、搜索系统、列表系统、订单系统等。
作为PC主页的研发,本文主要阐述了PC主页的技术实现。
PC主页的业务逻辑是从最初的模板渲染,到后来的SSI模块加载,再到现在的前端分离。开发语言也逐渐从ASP和PHP转变为基于LUA的技术框架。通过技术迭代升级,首页打开速度从200ms降低到30ms,API性能从500ms优化到100ms。
JD。COM的零售系统每天承载着数亿网民的购物需求。PC主页也是JD.COM商城的一级入口,所以系统必须满足以下三个要求:页面完整性(容灾、备份、降级);加载速度流畅(高并发、页面加载优化、API加载优化);监控和报警。
下面将根据系统设定的目标,一步一步的说明主页系统的实现方案。
1.页面完整性
页面完整性是指访问页面时,需要呈现正常的页面样式,不能有天窗或者非200状态码(40x,50x等。)是允许的。如下图,如果单个模块缺失,直接呈现给用户,会导致页面天窗,影响体验。当单个模块出现异常时,系统可以隐藏整层,优先保证页面的完整显示。
从以下六个方面阐述容灾降级的业务逻辑:
页面容灾:前端页面主要承载页面骨架,也包含一些交换机配置和必要的底层数据。即使后端API服务异常,html仍然可以提供基本的用户体验。前端页面通过模板渲染生成,其中模板变量由配置中心(蜂巢系统)维护,前端页面由正规工人触发,然后推送到静态资源服务器,最后通过CDN提供服务。其中在由工作人员触发生成html的阶段执行多层验证逻辑,以确保html的完整性。
接口容灾:一般情况下,接口逻辑优先读取缓存数据,如果缓存失效,会继续读取上游RPC数据进行输出。然而,没有一个上游系统能够保证100%的可用性。如何保证在上游异常的情况下,能够提供可靠的服务使页面正常呈现?PC主页系统在界面层做了两件事:
一、接口正在读取&上游RPC数据聚合后,会保存两个缓存,一个是普通用户加速缓存,通常设置5分钟的过期时间,另一个是底层缓存,永远不会过期。当缓存和上游RPC服务都不可用时,接口将读取底层缓存,以确保正常的数据输出。
二、API服务因自身问题(如主机异常、Redis服务异常、用户网络抖动等)无法提供正常服务怎么办?)?为了避免这个问题,系统为API提供了一种新的访问方式——CDN API。API会定期访问Worker生成的静态结果集(文件)的输出数据。目前终端异步加载数据,在感知到常规线路异常后,自动触发CDN API线路,保证接口的可用性。
根据以上两种底层逻辑,画出以下流程图。这个逻辑会把主页API服务的可靠性提高到100%。
接口降级:如下图所示,正常情况下,接口通过读取缓存、RPC数据、后台缓存来提供服务。但是如果Cache和RPC服务都异常,那么接口的响应时间在前两个阶段(10ms+100ms)会被大大浪费。为了避免这种情况,系统通过配置中心(蜂巢系统)设置降级开关。当系统感应到降级开关开启时,会直接读取底层缓存,保证API的响应速度。
NGINX容灾:NGINX容灾是指当系统监测到接口不是200状态码时,在NGINX级别执行底层逻辑。这个底层逻辑可以读取(或代理)远程静态资源,实现透明的灾难恢复。具体实现由error_page指令完成,它可以在特定的状态代码中设置一个命名位置,并在其代码块中执行相应的业务逻辑。
楼层容灾:多模块楼层,前端会调用多个API渲染页面。如下图所示,该层由四个模块组成,每个模块提供一个独立的API接口。当一个模块API异常时,触发地板隐藏动作,避开天窗。(这里是页面可以损坏但必须完整的策略)
终极容灾(页面CDN在最下面):2016年,启动“永不消失的主页”项目,即无论情况多么极端(包括Redis服务异常、服务器异常、RPC异常、网络异常等。),系统可以快速响应并切换到历史页面,保证页面的完整性。
为了实现历史快照,该项目借鉴了爬虫技术,定时抓取PC主页的完整数据,并将快照数据推送到静态服务器。当系统出现异常时,打开降级开关,及时将页面回滚到特定的版本历史。这样可以绕过正常的服务流程,直接读取底层的静态资源。具体流程如下:
(这个项目上线至今没有触发过一次。在其他业务场景下,需要考虑他们的实际情况)。
2.平稳装载速度
既要保证页面的完整性,又要保证页面和API的加载速度。下面从性能方面说明系统的优化方向。
技术选择
主页是一个强调性能而忽略逻辑的业务场景。不太依赖基础服务,主要与Redis和上游RPC交互。系统读取上游RPC数据后,进行汇总、过滤、验证、输出,最终完成页面展示。用户请求简化流程图如下:
结合业务场景,JD.COM首页在2015年开始调研并尝试使用OpenResty服务。凭借基于NGINX和高性能脚本语言LUA的异步事件模型,OpenResty完全胜任JD.COM主页的高并发场景。经过几年的沉淀,OpenResty已经成为京东的基础设施。COM的主页,其中也沉淀了OpenLua开发框架。
如上图所示,OpenResty(Nginx)服务的请求处理有11个阶段,每个阶段都有自己特定的业务场景。在init_by_lua*阶段,框架初始化环境变量,init_worker_by_lua*阶段初始化降级开关和基本配置,并同步到共享缓存(ngx.shared.DICT),init_write_by_lua*阶段初始化路由策略,access_by_lua*阶段实现访问权限验证。加密和解密等。,content_by_lua* stage实现主要业务逻辑,log_by_lua* stage实现log和Ump信息输出。
Redis Hotkey(热键)优化:Redis集群用于大部分业务场景的服务加速,通过集群的横向扩展能力增加系统吞吐量。但在特殊的业务场景下,会出现热点数据,导致流量倾斜,增加单个切片的压力,极大地制约API的效率,极端情况下甚至会使Redis集群宕机。避免热数据有两种方法:1。复制热数据并存储在不同的Redis切片中,每次用随机算法读取;2.预先定位热点数据,减轻Redis的压力。由于第一种方案实现复杂,系统采用第二种方案解决热数据,即把找到的热数据存储在本地缓存(ngx.shard.DICT)中,本地缓存服务直接对外提供服务。因为ngx.shard.DICT的效率极高,大大优化了API的响应时间。
Redis BigKey优化:BigKey是项目开发中不可忽视的性能点。在前期开发和压力测试中可能完美达到设定值,但在线环境的效率并不理想。这时候就要考虑bigkey因素了。由于bigkey占用内存较多,不仅会使网卡成为瓶颈,还会阻塞set和get中的其他操作,直接导致Redis吞吐量降低。
PC主页系统中bigkey的定义范围是>:5k,只要key的值大于5k,就需要单独处理。还有两个解决方案。1.切入正题。比如一个业务场景,需要在缓存中放一个很大的商品清单,一般的做法是通过kv(set key value)保存在Redis中。此时的业务优化方案是将一个键扩展为1+n个键,一个键存储id列表,另外n个键存储id对应的详细信息,从而将bigkey分解为多个键。2.技术上切。比如系统定义的bigkey是5k,通过算法把值切割成5k极限。然后,系统存储一个关系密钥和N个小密钥,实现bigkey转化为小密钥的过程。
不管用哪种方案,目标都是一样的,避免bigkey。在bigkey转化为small key的过程中,合理使用管道技术可以减少redis的网络消耗。
分布式任务的使用:随着个性化推荐算法的普及,商城首页全面接入推荐算法。因为个性化算法是基于实时计算的,所以需要很多时间。为了保证用户浏览的流畅性,系统引入了分布式服务。如下图所示,用户在fold以上请求时,将用户信息推送给分布式任务系统,分布式任务利用时间差计算出下面楼层的个性化数据,存储在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的解析时间,提高图片加载速度。另外使用懒加载也是优化的一个技巧。图片优化:电子商务系统页面会引入大量图片。虽然镜像系统引入了CDN服务,但是在高并发的情况下,单个域还是会被封锁。为了解决单个域的问题,我们将图片部署到多个域,如img10.360buyimg.com、img11.360buyimg.com、img12.360buyimg.com等。但是,多域dns解析将非常耗时。这时可以在页面中引入dns-prefetch,减少dns解析时间,提高镜像加载速度。此外,延迟加载也是一种优化技术。
3.监控并向警方报告
原始报警信息通过“系统日志->:研发->:产品->:“运营”的方式层层传递,不仅处理问题时间长,而且占用R&d资源。为简化报警流程,现有系统直接将报警信息同步到生产和运营,减少信息流通环节,提高效率。
添加报警逻辑不会影响系统的性能。系统通过异步无阻塞的ngx.socket.tcp组件实现消息同步。具体流程是系统通过ngx.socket.tcp组件将日志传输给分布式任务,分布式任务以异步方式将日志同步到弹性集群。报警系统根据报警规则触发报警信息的发送。(注意:使用ngx.socket.tcp组件时,请合理使用连接池,避免频繁连接)
总结
随着主页系统的迭代升级,团队沉淀了蜂巢系统平台和OpenLua框架。蜂巢系统通过积木式赋能和组件化构建,可以快速响应业务需求。OpenLua框架实现了Lua的MVC层次结构,并与JD集成。COM的内部组件来满足API性能和提高开发效率。
坚持走下去,不要停下来。在后续的R&D路上,我们将继续沉淀业务需求,强化组件化和标准化的思想,深化蜂巢系统和OpenLua框架,使其更加灵活、高效和易用。