内容纲要

作者:谢刚,京东商城架构师,负责京东分类列表、凤凰等系统的架构开发工作;之前在搜狐视频负责 UGC 视频架构开发工作。

*** 招聘:京东列表页目前有 Java、Golang、Lua(OpenResty)、数据挖掘等职位,欢迎投简历到 xiegang@jd.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 倍以上。

======= 京东网站招聘 =======

京东列表页目前有 Java、Golang、Lua(OpenResty)、数据挖掘等职位,欢迎投简历到 xiegang@jd.com,期待与您的合作~

====== 个人公众号推荐 ======

春天的旁边,springside 作者,唯品会一线老司机,by 江南白衣。

发表评论

电子邮件地址不会被公开。 必填项已用*标注