京东占领首页项目架构揭密
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 写在最后
后续我们团队会继续努力,在性能上进一步提升,在功能上进一步丰富,在架构设计上进一步优化改进,希望在现有的产品上继续深耕,同时也期望能给集团带来更多的贡献。