京东占领首页项目架构揭密

60天从零开始到上线一个UGC项目,揭秘京东占领首页项目背后的故事。

项目背景

京东APP当前调性高冷,大部分用户都是抱着购物的目的来且买完即走,希望在京东内打造一款由普通用户发起内容至大流量频道,引起其他用户关注的产品,从而改变京东在用户心中工具型产品印象,提升品牌温度,拉近用户距离。

我们在2019年十月末接到产品需求,要求在十二月底之前开发完成上线,也就是说要在六十天内,完成从零开始开发到保证测试上线,压力不可谓不大。

架构介绍

整个占领首页项目被分为三部分:

  • 前台:

    负责和客户端进行数据格式化交换。

  • 数据中台:

    存储数据,提供RPC服务。

  • CMS后台:

    运营人员操作,提供诸如发帖信息管理、活动和任务信息管理等功能。

整体架构图如下:

依靠集团内部的基础组件和稳固的中间件,我们从零开始搭建了前、中台项目,以及CMS管理后台项目。前台部分又分为SOA和H5前台两个工程,SOA项目主要负责对接主站APP客户端,而H5项目则是负责对微信分享、用户传播方向赋能。

中台方面选择使用JSF为前台提供RPC服务,而CMS后台方面不需要关心中台业务逻辑、缓存规则,只需要将关键操作作为JMQ消息发送,中台进行消费,实现了逻辑上的解耦。数据存储方面选用了集团提供的JimDB和基于Mysql引擎的弹性数据库JED进行数据存储。

同时借助ForceBot平台进行了压力测试,使用UMP和JEX平台分析了项目中存在的优化点和问题点。对于系统中存在的配置点,借助DUCC实现了配置化,可以实现线上配置热修改。

技术实践

3.1 JimDB结构化数据

数据存储方面,将Mysql中表结构数据转化为了JimDB中的组合数据结构。

JimDB本身是基于Redis的K-V型数据库,同时JimDB也提供了和Redis一样的数据结构。

在占领首页项目中,本身存在两个热点数据:

  • 热门帖子列表,根据点赞数正序排所有发帖数据。

  • 最新帖子列表,根据发表时间倒序排序所有发帖数据。

根据需求,存储数据时采用了zset存储帖子唯一ID,利用zset自动排序的特性,可以做到插入数据实时变化、数据查询实时更新的特点。例如对于最新帖子列表中的数据,使用发帖时间作为zset中的score,帖子的ID则作为value,这样就实现了根据发帖时间自动排序帖子ID。

发帖实体数据则单独存储为String类型的数据结构,可以选择序列化为Json字符串或者序列化为二进制数据,根据项目中相关实践,综合考虑了性能和开发便捷性后,我们选用了将对象序列化为Json字符串,主要原因是其可读性较高,便于问题的排查。

优劣对比

序列化成二进制(protostuff)

序列化成JSON

序列化速度

相对慢

序列后大小

相对大

序列化后可读性

完全不可读

可读

Redis 中数据类型

String

String

发帖实体数据的Redis-Key,则根据帖子ID根据一定命名规范生成,例如:post:info:9527,帖子实体数据只需要保存一份,热门帖子和最新帖子列表共享帖子实体数据。示例图如下:

进行数据查询时,使用Redis提供的ZRANGEBYSCORE可以实现倒序分页查询、范围查询,然后通过pipeline批量查询帖子实体数据,替代了SQL语句进行查询。

在进行对比测试时,共插入帖子数据10万条,每页查询20条帖子数据,forcebot进行压力测试,Mysql和JimDB的性能对比如下:

类型

单机QPS

TP99

并发线程

Mysql

3000 左右

150ms 左右

100

JimDB

6000 左右

10ms 以内

100

Mysql

5000 左右

200ms 左右

200

JimDB

10000 左右

10ms 以内

200

使用JimDB相较于Mysql,最大的提升就是TP99的降低。主要原因有两方面:

  • Mysql对于分页查询的页码增大以后,查询效率的明显降低。虽然这里我们也对查询进行了优化,包括针对于SQL语句的EXPLAIN分析优化、回传偏移量等,但是由于SQL型数据库的原因在这种查询情况下性能不可避免的会下降,TP99会随着并发查询数量上升。

  • JimDB理论上只做了时间复杂度为O(log(N)+M)的查询,和20次时间复杂度为O(1)的查询,这里N是zset的总大小,M则是分页大小。使用Pipeline以后也只有两次网络传输开销。因此TP99保持了稳定而且非常低的效果,直到达到JimDB分片的性能上限。

在项目开发完成期,中台整体数据做到了缓存覆盖率99%,只有少数判断操作会穿透缓存。依靠JimDB的高性能,整个系统的QPS和TP99得到了保障。

3.2 JMQ解耦业务

在项目开发中期,我们遇到了CMS后台和中台数据不共享,导致了业务开发受阻的情况,经过评审分析后,决定引入JMQ实现CMS后台和中台的完全解耦,因为中台大量使用了JimDB作为缓存,而CMS后台则接触不到这些数据,引入JMQ以后,CMS后台只需要通过和中台进行约定的消息交换,从而做到不需要关心中台数据存储和业务逻辑,却可以对中台数据进行修改。

这里需要注意,我们在使用MQ的时候出现了一个JED主库和从库不一致的问题,JED中的读写分离的实现方式的是主从分离,主库写入数据,从库查询数据,因此主从数据库的同步需要一定时间。在项目中,多次出现主库数据修改以后,从库数据没有在极短时间内同步修改的情况,由于JMQ消息传输和消费速度可能会比JED的同步速度快,因此建议某些必要操作的读写统一在JED主库(写库)进行,防止出现数据不一致的情况。

3.3 网络IO异步、并行化

我们对项目内存在的网络IO请求进行了梳理,将没有逻辑上先后关系的接口调用和数据库请求,抽取进行了并行化、异步化改造。

对比了现有的技术框架,最后决定选择了上手难度较低、可靠性较高的java.util.concurrent包中提供并发工具类,例如使用CompletableFuture类实现异步执行、编排任务,使用ExecutorCompletionService工具类实现并行化数据查询等。

例如帖子列表接口,每页每次会查询20条帖子数据,对于每条帖子数据,需要查询上游接口获取点赞信息和用户信息,点赞组件的调用又需要调用点状态和点赞用户列表两个接口,如果采用遍历列表后串行调用接口的方式,需要调用近百次上游接口,针对于这个接口,进行了如下改造:

  • 首先对于系统内涉及到的用户信息进行了缓存,将近百次的用户组件接口的RPC调用转变为了对于JimDB的批量查询。

  • 其次采用了多线程的方式,并行的调用点赞接口,极大的缩短了网络IO串行带来的累加的等待时间。

对于点赞状态接口,实际上和点赞用户信息是没有逻辑关系的接口,但该接口如果请求失败,就不需要拼接点赞用户信息,因此采用了异步任务的方式,成功后回调拼接数据。改造后的接口请求时间大大下降。

状态

单机QPS

TP99

CPU 使用率

改造前

800 左右

350ms 左右

75% 左右

改造后

1200 左右

40ms 以内

85% 左右

压测时单机的QPS并没有多大提升,是因为该接口背后的业务逻辑比较复杂,在八核16G的机器上进行测试,也已经达到了CPU瓶颈。同时需要注意的是,项目中线程池的配置需要经过严格的验证和测试,针对于不同配置的机器,和不同的业务场景需要进行不同的配置。

总结&展望

占领首页项目是京东在UGC产品上的一次深入尝试。从第一次的线上效果来看,收获了不错的成绩,有超过1.2万的用户发帖,霸屏首页的帖子有1500万的查看量,是京东UGC产品中比较成功的一款项目。作为研发人员,对于系统的改进也有更多的展望。

4.1 模块化、配置化

因为开发周期较短,占领首页项目无论是SOA还是中台,目前从代码层面上来看存在着扩展性不足、模块之间交叉较多的情况,我们还需要进一步的努力,做到使用配置化来实现扩展功能,使用开关能管控模块的能力。

4.2 能力组件化

在中台的开发过程中,我们发现有一些可以复用甚至对外赋能的组件,比如说任务组件,目前整个任务的流程都严格和各部分代码逻辑耦合,但是经过复盘分析后发现整个任务模块是可以组件化剥离出来的,后续甚至可以进一步做成通用任务组件对外赋能。

4.3 写在最后

后续我们团队会继续努力,在性能上进一步提升,在功能上进一步丰富,在架构设计上进一步优化改进,希望在现有的产品上继续深耕,同时也期望能给集团带来更多的贡献。