目录标题
参考微信朋友圈技术之道:三个人的后台团队与每日十亿的发布量,朋友圈数据有四个核心要素:
- 发布。发布数据记录了来自所有用户所有的 feed,比如一个用户发布了几张图片,每张图片的 URL 是什么,在 CDN 里的 URL 是什么,它有哪些元属性,谁可以看,谁不可以看等等。
- 相册。相册是每个用户独立的,记录了该用户所发布的所有内容。
- 评论。评论就是针对某个具体发布的朋友评论和点赞操作。
- 时间线。所谓“刷朋友圈”,就是刷时间线,就是一个用户所有朋友的发布内容。
微信朋友圈的主要数据类型
从上面可以看出,既有结构化数据又有非结构化数据:
- 非结构化数据:图片\视频和文字,数据量较大。
- 结构化数据:评论以及时间线。
根据数据的结构特征对数据分类
- 结构化数据通常是指关系模型数据,其特征是数据关联较大、格式固定。结构化数据具有格式固定的特征,因此一般采用分布式关系数据库进行存储和查询。
- 半结构化数据通常是指非关系模型的,有基本固定结构模式的数据,其特征是数据之间关系比较简单。比如 HTML 文档。一般采用分布式键值系统进行存储和使用。
- 非结构化数据是指没有固定模式的数据,其特征是数据之间关联不大。比如文本数据就是一种非结构化数据。这种数据可以存储到文档中,通过 ElasticSearch(一个分布式全文搜索引擎)等进行检索。
根据数据的存储特性对数据分类
- 关系型
- 键值对性
- 面向列的
- 面向文档的
存储的种类
- (分布式)数据库,通过表格来存储结构化数据,方便查找。
- 分布式键值系统,通过键值对来存储半结构化数据。常用的分布式键值系统有 Redis、Memcache 等,可用作缓存系统。
- 分布式存储系统,通过文件、块、对象等来存储非结构化数据。常见的分布式存储系统有 Ceph、GFS、HDFS、Swift 等。
最后,选择存储可以参照Visual Guide to NoSQL Systems,根据 CAP 的特性来选择存储
微信朋友圈数据模型
读写模型
基本上是一个人写,多个人读,相当于发布订阅模型。这里的读包括自己读自己的朋友圈,在集群部署的情况下,用户会往集群的主节点写入数据。主节点负责将数据复制到备份节点。所有需要考虑数据的复制格式和复制方式,虽然这两个概念华仔放在高可用模型中讲了,但是对朋友圈还是很重要的,参见本节“一致性模型”部分。
读写冲突模型——隔离级别
- 发朋友圈。写操作,“自己”发朋友圈不会和自己冲突。
- 刷朋友圈。多人读操作,也不存在冲突。
- 点赞、评论,增量操作,也不会存在冲突。但对数据的顺序上存在一致性的要求。
综上,虽然在 2015 年朋友圈每天的发表量(包括赞和评论)超过 10 亿,浏览量超过 100 亿,并发量极高,但数据冲突不多,相比事务型数据,处理起来相对容易一些。
一致性模型
这里的一致性是指用户发朋友圈,朋友点赞、评论朋友圈,即从使用者出发而不是从服务器端出发来看的概念,即和会话有关的一致性。
在集群部署的情况下,同一个用户的不同请求可能会被发送到不同的机器上处理。虽然这时候是多台机器在处理你的请求,但是从用户的角度来看,需要保证最后的处理结果,和在一台机器上处理的结果是一样的。
写后读一致性
“写后读一致性”(Read after Write Consistency),它也称为“读写一致性”,或“读自己写一致性”(Read My Writes Consistency)。还可以称为“自读自写一致性”。
发朋友圈的人,也会读自己发的朋友圈。自己发布成功的朋友圈,下一刻一定能刷到,这就是“读自己写一致性”名字的由来。当然,从旁观者角度看,可以称为“读你写一致性”(Read Your Writes Consistency)。
单调读一致性
在小明发布照片后的瞬间,小红也刷新了朋友圈,此时读取到副本 R1,所以小红看到了照片;片刻之后,小红再次刷新,此时读取到的副本是 R2,于是照片消失了。小红以为小明删除了照片,但实际上这完全是程序错误造成的,数据向后回滚,出现了“时光倒流”。
想要排除这种异常,系统必须实现单调读一致性(Monotonic Read Consistency)。关于单调读一致性的定义,常见的解释是这样的:一个用户一旦读到某个值,不会读到比这个值更旧的值。
实现单调读一致性的方式,可以是将用户与副本建立固定的映射关系,比如使用哈希算法将用户 ID 映射到固定副本上,这样避免了在多个副本中切换,也就不会出现上面的异常了。
单调写一致性
单调写一致的英文名是 Monotonic Write。如果你往有容灾的集群里写了多次数据,单调写一致要求所有的节点的写入顺序和你的写入顺序完全一致。这样我们就能保证对于任何一个节点,它看到的别人的写操作和自己的写操作是完全一致的。
一个反例:
这个对应发朋友圈中“时间线”的概念,所有好友,不论在什么地方,看到的朋友圈的发布顺序应该是一致的,否则会看不明白。
先读后写一致性
前面三个一致性规定了一个会话的行为应该是怎样的。先读后写不同,它规定了多个会话之间互动应该满足怎样的一致性要求。
先读后写要求比较严格。假如你曾经读到了另一个人写入的结果,那么你想再写数据的话,你的写入一定要在另一个人的写入之后发生。也就是说,你们俩之间的写入有个先后顺序。
你如果看到了另一个人的结果,就表示另一个人的写入是过去发生的事情,这时候如果你想再写点新东西进去,那么整个集群需要保证你们俩写入的先后顺序。
这个适合于评论和点赞,必须先有朋友去圈发布,后有评论和点赞,否则会让人不知所措。
前缀一致性
小明和小红的评论分别写入了节点 N1 和 N2,但是它们与 N3 同步数据时,由于网络传输的问题,N3 节点接收数据的顺序与数据写入的顺序并不一致,所以小刚是先看到答案后看到问题。
显然,问题与答案之间是有因果关系的,但这种关系在复制的过程中被忽略了,于是出现了异常。
保持这种因果关系的一致性,被称为前缀读或前缀一致性(Consistent Prefix)。要实现这种一致性,可以考虑在原有的评论数据上增加一种显式的因果关系,这样系统可以据此控制在其他进程的读取顺序。
发朋友圈功能实现
数据特性
如下描述引自微信朋友圈技术之道:三个人的后台团队与每日十亿的发布量
比如有两个用户小王和 Mary。小王和 Mary 各自有各自的相册,可能在同一台服务器上,也可能在不同的服务器上。现在小王上传了一张图片到自己的朋友圈。上传图片不经过微信后台服务器,而是直接上传到最近的腾讯 CDN 节点,所以非常快。图片上传到该 CDN 后,小王的微信客户端会通知微信的朋友圈 CDN:这里有一个新的发布(比如叫 K2),这个发布的图片 URL 是什么,谁能看到这些图片,等等此类的元数据,来把这个发布写到发布的表里。
在发布的表写完之后,会把这个 K2 的发布索引到小王的相册表里。所以相册表其实是很小的,里面只有索引指针。相册表写好了之后,会触发一个批处理的动作。这个动作就是去跟小王的每个好友说,小王有一个新的发布,请把这个发布插入到每个好友的时间线里面去。
可以总结出,有如下特点:
- 发朋友圈的数据含有图片和视频,是非结构化数据,使用 CDN 进行存储
- 发布动作本身对应一个新的数据结构——“发布”,包含一些元信息——比如图片的 URL 是啥,谁能看到这些图片。“发布”本身是结构化的信息,需要持久化。
- 发布内容的索引需要写到发布人的相册表里。
- 发布本身的相关信息需要插入到每个好友的“时间线”中去(每个好友的时间线可能分布在不同的存储上)
- 发布是一个写扩散的过程——首先写自己的数据副本(自己的相册表),还要把副本的指针插到好友的时间线里面去。所以写比较复杂,会慢一些,但是好友的读取比较简单,只要读取自己的时间线就可以了。(数据同步:写扩散)
(摘自引文) 为什么选择这样一个写扩散的模型?因为读是有很多失败的。一个用户如果要去读两百个好友的相册表,极端情况下可能要去两百个服务器上去问,这个失败的可能性是很大的。但是写失败了就没关系,因为写是可以等待的,写失败了就重新去拷贝,直到插入成功为止。所以这样一个模型可以很大的减少服务的开销。
- 数据操作冲突
- 读写冲突:不存在。读发朋友圈的发布信息,写入到好友的时间线里。写入时间线不存在回滚的问题,因为会一直尝试知道写入成功。好友间写时间线不存在写冲突——各写各的。单独一个时间线同时接收好友的发布信息,是新增即追加写,也不存在冲突。
- 写写冲突:不存在,发朋友圈和刷朋友圈的人各写各的存储。
写读冲突:发朋友圈的人在刷朋友圈的人评论后,又删除了自己的朋友圈,已经读过的人怎么办?把删除操作也包装成一个特殊的“发布”,再“写扩散”到好友的时间线里去即可。但这是需要额外考虑处理手机端的缓存
。
- 一致性分析
- 需要满足前述的所有一致性。使用任务分解,将数据读写请求固定到同一个节点即可。
性能方案
架构图
说明:
- 根据对数据模型的描述,这里的数据读写冲突较少,所以关系数据库不是最好的选择,应该根据团队的技术栈选择:
- 如果对关系数据库比较熟悉,那就选用;
- 如果有现成的键值存储那么用键值存储应该是更好的选择。
- 此处主要追求 AP 特性。
- 主要需要处理各种数据一致性问题。但是朋友圈数据的事务特征不那么强,所以不需要复杂的分布式数据库,用任务分解,将同一个朋友圈的请求固定到同一个节点即可。
点赞和评论
至于赞和评论的实现,是相对简单的。上面说了微信后台有一个专门的表存储评论和赞的数据,比如 Kate 是 Mary 和小王的朋友的话,刷到了 K2 这一条发布,就会同时从评论表里面拉取对应 K2 的、Mary 留下的评论内容,插入到 K2 内容的下方。而如果另一个人不是 Mary 和小王的共同朋友,则不会看到这条评论。
数据特性
- 可以看出点赞和评论是依赖于发布数据的:先读取自己的时间线里的发布信息,然后再去评论表里拉取评论。这里评论表应该业务要水平拆分,拆分的键值首先应该包括“发布”的唯一标识,其次考虑到数据的冷热,还应该包括日期。
- 评论和点赞数据是结构化的数据。
- 需要持久化。
- 数据操作冲突分析
- 大家对发布者的评论是追加写的过程,相当于往数据库里插入一条数据,不存在冲突
- 数据可见性。刷朋友圈的人读取自己的时间线,然后拿着发布的 ID 去评论表里拉取评论,应该都是可见的。
- 一致性分析
- 如何保证评论表里评论的顺序?后发表的评论可以先保存到评论表中么?
- 比如小明和小红都去评论小刚的朋友圈,那么他们的顺序是无所谓的。但如果一但顺序确定了,他们自己或其他人再次读取到这两条评论时,顺序必须是一致的,这就是单调写一致性,如果大家都去同一个评论表里拉取,那么自然会满足。此时需要“任务分解器”将这个任务分配到同一个库中。
- 如果小明先评论小刚,小红又去评论小明,那么小红一定是读取到了小明的评论,然后才评论小明,所以他们一定是有序的。这就是先读后写一致性得到满足。此时需要“任务分解器”将这个任务分配到同一个库中。
性能方案
经过上述分析,可以看出和发朋友圈的性能方案一样。
架构图
经过上述分析,可以看出和发朋友圈的架构图一样。
架构设计复杂度模型
可以按照业务复杂度和质量复杂度两个维度对复杂度建模:
- 业务复杂度
- 业务固有的复杂度,主要体现为难以理解、难以扩展,比如
- 业务数量多(微信)
- 业务流程长(支付宝)
- 业务之间关系复杂(例如 ERP)
- 质量复杂度
- 主要是对质量属性的要求
- 高性能
- 高可用
- 成本
- 安全
MECE 法则
按照两个维度对架构设计复杂度建模,实际是 MECE 法则的一次具体应用。
MECE 法则,是麦肯锡咨询顾问芭芭拉·明托在《金字塔原理》中提出的一个思考工具,它是 Mutually Exclusive
Collectively Exhaustive 的缩写,意思是“相互独立,完全穷尽”,也常被称为“不重叠,不遗漏”。穷尽分解后,可以根据分解的两个维度(比如业务复杂度和质量复杂度)把问题空间划分为 4
个象限,形成四象限坐标图,把各种系统按照复杂度分类分布到其中。四象限坐标图是最简单、最常用的管理、思考工具。
针对每个象限都有自己的应对之策
可以看到,DDD 并不能降低质量复杂度。因为 DDD 的主要流程是分解业务域成子域、子子域,即它是针对复杂业务问题的。
资料:https://xie.infoq.cn/article/bc8a70a7b4b6d8581baf843fd