这篇文章是在shopee参与feed项目设计和开发过程的一些思考。
在feed系统设计中,很多设计算是实践了《DDIA》里面的一些论点。
自认为《DDIA》中谈及的理论和在项目中得意实践其中的部分理论,让我在这两年里成长不少。
一.什么是数据系统
现在的互联网产品,大部分都可以归纳为是这样一种数据系统:它使用了多种数据存储系统:mysql/redis/kafka/es/hdfs
涉及到关系数据库,缓存,消息队列,搜索引擎,分布式文件系统等等,如果需要推荐,还可能有hbase等数据存储工具,
每种工具各司其职,分工合作,而代码更像是连接各个系统的粘合剂。
api
+----------------------------------------------------+--------------------------------------+
| | |
| +-----------------+ read request .................... asyncjob |
| | in-memory cache | --------------- . application code . ------------------+ |
| +--------+--------+ check if data .................... | |
| | in cache first | | cache miss | |
| | +------------------+ | or writes | |
| | |search request | |
| | +-------+--------+ +---------+--------+ +-------+------+ |
| | | full_text_q | | primary db | | message q | |
| | +-------+--------+ +---------+--------+ +-------+------+ |
| | | sync to full text | capture change | |
| | +----------+-----------+ +---+ |
| | | | |
| | sync ...................... ...................... |
| +-------------- . application code . . application code . |
| ...................... ...................... |
| | |
+-----------------------------------------------------------------------------+-------------+
|
thrid api
以twitter/weibo为代表的feed流产品就是这样一个典型的数据系统产品:
它使用mysql/cassandra来持久化用户数据
使用redis加速访问速度
使用消息队列削峰填谷
使用搜索引擎支持搜索特定用户/内容
使用hbase等列式存储来做推荐/数据分析
…
二.数据系统系统设计方法论
2.0 数据系统设计方法和软件方法的收敛
对于数据系统设计方法,先抛出结论:
数据系统设计主要可以从三个角度给出问题,然后通过回答这些问题,基本可以得出一个比较合理的架构设计。
数据模型数据的访问模式
结构性/半结构性/非结构性
局部性 or 多对多关系(访问)系统负载
读多写少(负载类型,读写扩散)..
冷热数据(负载)..
数据量..
索引类型..数据的正确性和时效性
正确性:持久化保证
正确性:一致性保证(限行一致性/顺序一致性/因果一致性…)
时效性:低时效 == 最终一致
除了上面的三个角度,其实还应该考虑上人的因素,比如团队对于某种数据系统的熟悉程度。
潘家宇的《软件方法》里提供了几个软件建模思路,大体上遵循
在思考数据模型的时候其实需要我们充分理解需求,抽象出领域模型,通过对象关系图(1:1/1:n/n:n),正向反向关系等
区分出核心数据模型和派生的数据模型
数据模型就是判断数据是属于结构性/半结构性还是非结构性(blob)
blob数据不用说,大部分都是使用分布式文件系统存储
半结构性数据,比如一则feed的json
{
"feedid": "xx",
"feed_uid": 123,
"type": 1,
"source: 2,
"feed_username": "",
"content": {
"voucher_stickers": [{
}],
"caption": ""
},
"comments": [{
}]
}
因为这种数据很少需要关联查询,很适合使用mongodb等文档结构模型数据库存储,也可以采用关系型数据库存储
半结构性具有更加好的局部性,有利于一对多关系,不利于多对多关系,也容易造成冗余
结构性(关系型)数据利于表示多对多关系,但是局部性更差
结构性数据库表可以使用一定的犯范式设计,提高了冗余,降低一致性,但是可以提高局部性。
在做feed需求里,明显feed数据更适合使用半结构化的数据库,比如mongodb/cassandra
但是shopee历史原因只支持mysql/tidb做持久化的存储。所以我们的feed内容数据是以非范式化的方式存储在表个的一个字段里的。
2.2 系统负载
思考系统负载的时候其实就是思考系统的用例,系统需要对外提供哪些能力,这些接口具有哪些特性,核心难题在哪里
这时候可以列出系统的关键接口,帮组我们思考。
2.2.1 读负载类型还是写负载类型
负载有很多种,互联网典型的是读多写少,读多,导致了索引计数的诞生
b+树类型的索引的innodb很适合读多写少的场景,但是当读的负载更高,如读放大情况,光靠innodb也不行
这时候需要redis做缓存。
但是使用缓存之后,一般需要代码里双写,那么是使用cache aside模式,是淘汰缓存,都是第三点:数据的正确性和时效性考虑的
这里只是根据负载特性,确定需不需要引入缓存
2.2.2 冷热数据
有时候需要考虑热点数据问题,比如大v发文,评论点赞会比普通人更多,比如新旧数据,新的feed被访问概率大于旧的feed
这些会影响到分表方式,kafka 分区数据等。
2.2.3 数据量
决定了分表的大小和缓存的开销
2.2.4 索引
选用什么类型的索引往往也是负载决定的,
b+树/hash/lsm
全文索引…
2.3 数据的正确性和时效性
数据的正确性和时效性其实算是CA(CAP)的一种选取,需要进一步考虑各个用例的流程图(时序图)。
在分布式系统里,不可靠体现在:
- 网络延迟
- 节点故障
- 网络超时
其中网络超时是网络延迟的另一种形式,因为分布式系统里,识别异常通常通过超时机制来实现,而超时机制的缺点就是网络超时,
此时你不知道请求是否成功到达了另一个节点。所以它是最麻烦的一种异常。
因为种种的不确定性,我们可以使用逐步推断的方式建立一个正确的数据系统(如果一下子假设三种情况都成立,则考虑的分支太多,不利于思考)
然后思考哪些一致性是需要得到保证的
2.3.1 正确性
如持久化保证
顺序保证对于持久化也很重要
比如CDC(change data capture)为核心的数据总线系统,使用binlog解析,导入kafka,消费者消费kafka到异构数据系统实现全文索引/推荐等功能,这时候,消息的顺序就显得很重要
顺序可以通过id发号器来保证,分布式系统中,兰伯特时间序就是很重要的一个结论
2.3.2 时效性
只有强一致性才能保证实时生效,在异构系统里,时效性的保证需要牺牲性能。而且大部分业务场景其实并不需要这么强的时效性,这时候就需要做权衡了。
三.举个栗子
常见的社交场景中的点赞功能落地
四.总结
无论是分布式系统,还是其他架构的设计,都是一种权衡的艺术,分布式面临的很多问题其实在古老的操作系统,数据库中早就有了类似的场景。