京东三级列表页持续架构优化 — Golang + Lua (OpenResty) 最佳实践

3,057 阅读7分钟
原文链接: mp.weixin.qq.com

承接上篇《京东三级列表页持续架构优化—前端优化实践》。

分类列表入口


分类列表入口,可以通过京东首页首屏左侧导航进入,是用户购买商品的几大入口之一。

分类列表,展示各个分类的商品,有综合排序、价格排序、销量排序、上架时间排序、图书还有出版时间排序。可以按照品牌、价格和各种扩展属性筛选出想要的商品。下图以空调列表为例。


分类列表特点

  • 分类多,全站大概几千个分类;

  • 商品多,每个分类商品多,有的分类达上千万的商品;

  • 需求多样化,不同分类需求不一样,例如大家电、图书需求各不一样;

  • 请求量大,实时性要求高。

旧架构


旧架构,前端是用nodejs做模板渲染,后端服务是调用搜索接口。旧架构缺点:

  • 响应时间比较长;

  • 因为是搜索返回的数据,数据二次加工不方便。

升级新架构

* 新架构设计目标

  • 分布式,数据可以做多个分片,服务各层可以做到水平扩容;

  • 高可用,双机房双活部署;

  • 响应迅速;

  • 数据闭环,线上服务主要数据不依赖于外部API;

  • 运维便捷,方便切换集群,方便分类管理配置;

  • 数据提升,通过优化排序算法,提升GMV、订单转化率、客单价等。

* 新架构


新架构功能模块如上图所示:

  • 页面渲染:采用OpenResty(Nginx+Lua)来作模板渲染,方便页面逻辑的调整;

  • 业务处理:采用golang,所有的筛选、过滤逻辑都是在这一层处理的;

  • 数据异构:页面渲染需要相关的数据、过滤筛选需要的数据,都是通过异构过来的;

  • 消息处理:通过接入MQ消息,可以实时处理商品上下架、库存更新、价格修改等消息;

  • 质量分计算:通过大数据平台计算商品质量分,为综合排序提供依据;

  • 配置管理中心:负责后台调度、分类配置等。

新架构线上流程如下图所示


新架构离线数据流如下图所示

其中:

  • 数据集市,使用的是京东的大数据平台;

  • JSS,是京东自研分布式文件存储系统;

  • JIMDB,是京东自研KV存储系统,可当分布式Redis使用。

详解各个模块

* 质量分计算

由于每个分类的商品非常多,个别分类达千万量级的SKU,而用户浏览的SKU有限,我们需要将用户最可能买商品排在前面;为每个分类的所有sku进行质量分计算,涉及到几十个指标(包括销量、评价、浏览、转化率等);根据质量分的高低进行排序;由于涉及数据量很大,所有计算都在大数据平台完成;将计算结果推送到JSS。

由于还有一些特殊规则,例如品牌穿插、店铺穿插、特殊排序等,这些规则的实现是通过worker实现,读取jss,并进行特殊规则处理。将处理后的数据推送到MYSQL。

* 异构服务

异构服务主要是异构过滤和展示需要的商品数据;调用外部各个接口,形成一张商品宽表。如下图所示:


* 业务处理子系统

上图展示了列表各种筛选逻辑,排序逻辑。

业务处理子系统提供前端所需要的所有过滤筛选接口,以及展示数据。该系统采用golang开发,所有筛选数据都存在内存中,提高检索速度;展示的数据都放在jimdb中,目的减少占用内存大小,缩短golang的GC时间。下图展示了内存中存储的数据。


* 消息处理系统

该系统接收处理相关消息(商品变更,上下架,价格变更,库存变更),并实时更新到线上,如下图所示:


* 页面展示子系统

页面展示子系统,采用Nginx+Lua实现,负责模板的渲染,如下图所示。

为了提高页面的渲染速度,有一部分页面采用异步渲染,例如:页面小图聚合的可以让js渲染小图;超过5个的扩展属性,让js异步渲染。页面需要的价格数据、库存数据、广告数据,采用异步加载;保证这些数据的实时性。

页面渲染优化:

  • HTML文档精简,越简单渲染越快,性能越好;例如:页面小图聚合的可以让js渲染小图;超过5个的扩展属性,让js 异步渲染;

  • 懒加载数据,例如:滚屏加载图片和页尾;

  • 资源加载排序,对每种资源定优先级,对必需的资源优先加载,而低优先级的请求保存在队列中延时加载或等必需资源加载完再加载。例如:搜索推荐热词、顶部三个热卖商品接口、价格优先加载。对于库存、促销信息、广告词、预售商品、店铺信息等,延后加载; 对于点击流,广告统计数据则延时两秒再加载;

  • 页面更多优化参考:《京东三级列表页持续架构优化—前端优化实践》。


Golang+Lua(OpenResty)的应用

* Golang–遇到的坑

  • JSON的序列化性能低下:Golang内置的encoding/json、encoding/gob,采用ffjson;

  • GC问题:减少内存对象。减少对象申请,两个作用:减少内存使用,减少内存碎片;

  • 字符串拼接:尽量使用byte数组,不要用String,由于String会创建新对象;

  • Go占用OS内存释放慢:执行:debug.freeOSMemory();

  • Goroutine闪退:goroutine闪退,导致应用进程闪退,异常捕获;

  • 并发处理map:必须加读写锁(sync.RWMutex)。

* 选择Lua(OpenResty)

  • Lua:轻量级、协程、嵌入式、开发效率高;

  • OpenResty:OpenResty将Nginx核心、LuaJIT、许多有用的Lua库和Nginx第三方模块打包在一起的web应用开发框架。

** 模板渲染

使用的模板引擎https://github.com/bungle/lua-resty-template。Nginx配置如下所示。


模板如下所示。


** 缓存

缓存:

  • 为后端服务异常提供托底数据;

  • 当流量太大时,可以开启缓存,减少后端服务压力。

缓存流程:

  • 解析url,对url做hash,得到相应的key,从后端服务获取数据,如果数据完整,则渲染模版,将对应的数据放入对应的缓存,并将key放入keycache,并设置缓存时间;

  • 页面缓存是永不过期的,当key过期时,主动替换掉;

  • 为什么分为两类缓存:firstpage cache只缓存每个分类首页的数据,这样可以缓存全部分类的首页,保证所有分类都有托底数据。Otherpage cache 缓存除首页以外页面,这样保证热点数据都在缓存中。如果超过容量,通过lru淘汰;

  • 为什么每类缓存多个分片:因为lua_shared_dict存在自旋锁,单片读写压力大时,会有一定的瓶颈,因此采用多个分片,每个分片大小设置,根据具体缓存数据来定;

  • Firstpage和otherpage 是缓存在每台nginx服务器上,缓存的内容有限;

  • Redis缓存,可以集中缓存,能够缓存更多的数据;


** 异常处理

异常处理分为两层托底,保证每层报错,均可对异常进行处理,无5xx等错误,提高用户体验,第一层托底,展示各个分类首页的缓存;第二层托底,跳转京东首页。

Lua执行问题,通过nginx配置error_page,进入异常处理。接口响应问题,通过ngx.exec内部跳转,进入异常处理。

注:error_page默认只匹配一次,匹配多次需配置recursive_error_pageson;

ngx.exec为内部跳转,类似于流水线,数据流动方向单一,无额外http请求。


新版性能

* 页面渲染性能

页面响应时间:模板渲染+业务筛选接口(go),平均在30ms左右,tp99在80ms以内,提高6倍以上;页面渲染(NGINX+LUA)TPS,在并发100时,16核单机在3500笔/秒,提高10倍左右。

* 业务筛选接口(GO)性能


业务筛选接口(GO):平均在10ms以内,tp99在50ms左右,响应时间提高6倍以上。