架构实战营第二课作业——微信朋友圈的高性能复杂度分析
作业
作业图在“开始正式写作业”小节。
分析一下微信朋友圈的高性能复杂度.
注:本文使用的理论框架来自极客课程《分布式数据库 30 讲》和《分布式金融架构课》。
参考微信朋友圈技术之道:三个人的后台团队与每日十亿的发布量,朋友圈数据有四个核心要素:
发布。发布数据记录了来自所有用户所有的 feed,比如一个用户发布了几张图片,每张图片的 URL 是什么,在 CDN 里的 URL 是什么,它有哪些元属性,谁可以看,谁不可以看等等。
相册。相册是每个用户独立的,记录了该用户所发布的所有内容。
评论。评论就是针对某个具体发布的朋友评论和点赞操作。
时间线。所谓“刷朋友圈”,就是刷时间线,就是一个用户所有朋友的发布内容。
微信朋友圈的主要数据类型
从上面可以看出,既有结构化数据又有非结构化数据:
非结构化数据:图片\视频和文字,数据量较大。
结构化数据:评论以及时间线。
根据数据的结构特征对数据分类
结构化数据通常是指关系模型数据,其特征是数据关联较大、格式固定。结构化数据具有格式固定的特征,因此一般采用分布式关系数据库进行存储和查询。
半结构化数据通常是指非关系模型的,有基本固定结构模式的数据,其特征是数据之间关系比较简单。比如 HTML 文档。一般采用分布式键值系统进行存储和使用。
非结构化数据是指没有固定模式的数据,其特征是数据之间关联不大。比如文本数据就是一种非结构化数据。这种数据可以存储到文档中,通过 ElasticSearch(一个分布式全文搜索引擎)等进行检索。
根据数据的存储特性对数据分类
关系型
键值对性
面向列的
面向文档的
存储的种类
(分布式)数据库,通过表格来存储结构化数据,方便查找。
分布式键值系统,通过键值对来存储半结构化数据。常用的分布式键值系统有 Redis、Memcache 等,可用作缓存系统。
分布式存储系统,通过文件、块、对象等来存储非结构化数据。常见的分布式存储系统有 Ceph、GFS、HDFS、Swift 等。
最后,选择存储可以参照Visual Guide to NoSQL Systems,根据 CAP 的特性来选择存储
微信朋友圈数据模型
读写模型
基本上是一个人写,多个人读,相当于发布订阅模型。这里的读包括自己读自己的朋友圈,在集群部署的情况下,用户会往集群的主节点写入数据。主节点负责将数据复制到备份节点。所有需要考虑数据的复制格式和复制方式,虽然这两个概念华仔放在高可用模型中讲了,但是对朋友圈还是很重要的,参见本节“一致性模型”部分。
读写冲突模型——隔离级别
发朋友圈。写操作,“自己”发朋友圈不会和自己冲突。
刷朋友圈。多人、读操作,也不存在冲突。
点赞、评论,增量操作,也不会存在冲突。
综上,虽然在 2015 年朋友圈每天的发表量(包括赞和评论)超过 10 亿,浏览量超过 100 亿(引自微信朋友圈技术之道:三个人的后台团队与每日十亿的发布量),并发量极高,但数据冲突不多,相比事务型数据,处理起来相对容易一些。
一致性模型
图片都引自极客时间专栏 《分布式数据库 30 讲》和《分布式金融架构课》。
这里的一致性是指用户发朋友圈,朋友点赞、评论朋友圈,即从使用者出发而不是从服务器端出发来看的概念,即和会话有关的一致性。
在集群部署的情况下,同一个用户的不同请求可能会被发送到不同的机器上处理。虽然这时候是多台机器在处理你的请求,但是从用户的角度来看,需要保证最后的处理结果,和在一台机器上处理的结果是一样的。
写后读一致性
“写后读一致性”(Read after Write Consistency),它也称为“读写一致性”,或“读自己写一致性”(Read My Writes Consistency)。还可以称为“自读自写一致性”。
发朋友圈的人,也会读自己发的朋友圈。自己发布成功的朋友圈,下一刻一定能刷到,这就是“读自己写一致性”名字的由来。当然,从旁观者角度看,可以称为“读你写一致性”(Read Your Writes Consistency)。
单调读一致性
在小明发布照片后的瞬间,小红也刷新了朋友圈,此时读取到副本 R1,所以小红看到了照片;片刻之后,小红再次刷新,此时读取到的副本是 R2,于是照片消失了。小红以为小明删除了照片,但实际上这完全是程序错误造成的,数据向后回滚,出现了“时光倒流”。
想要排除这种异常,系统必须实现单调读一致性(Monotonic Read Consistency)。关于单调读一致性的定义,常见的解释是这样的:一个用户一旦读到某个值,不会读到比这个值更旧的值。
实现单调读一致性的方式,可以是将用户与副本建立固定的映射关系,比如使用哈希算法将用户 ID 映射到固定副本上,这样避免了在多个副本中切换,也就不会出现上面的异常了。
这里其实是涉及到了华仔实战营课程中的任务分解(没有把它归为任务分配是因为这里根据用户的不同,把请求分发到不同的服务器处理,属于 AKF 立方体的 Z 轴扩展)中任务分解器的算法。
单调写一致性
单调写一致的英文名是 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 的主要流程是分解业务域成子域、子子域,即它是针对复杂业务问题的。
可扩展复杂度模型
首先要区分两个易混淆的概念
可扩展:extensibility,系统适应变化的能力,包含可理解和可复用两个部分。
可伸缩:scalability,系统通过添加更多资源来提升性能的能力。
结合自己经历的项目,谈谈对可理解和可复用的理解。
可理解。华仔在课上从需求动态变化的的角度出发对可理解性做了说明:当需求来了后,如果系统很难理解,就不会知道会影响哪些地方,哪些地方需要改,这样就导致不容易扩展。
感悟:就像自己负责维护的微信公众号一样,设计时根据很多业务场景提炼共性需求,对子模块做了正交的分解(其实这也是 MECE 法则的引用,就像矢量分解一样,只要分解为 X 轴和 Y 轴两个分量,就可以在 360 度的空间内随意组合方向),各个模块之间的交互也比较明确。这样当来了一个新的需求时,首先采取克制的态度,充分发掘现有模块是否可以组合出新的功能点,避免系统变得越来越复杂。
有时这需要对需求有深刻的理解,有时需求一样看不出来怎么实现,但经过仔细分析,可以以非常简洁的手段去实现。比如微信的朋友圈、订阅号和聊天,都可以用消息来实现,只是展现形式和场景有很大的差异罢了,但内核是一致的。
可复用。指针对新的需求,当可复用性不高的时候,就会导致需要改动的地方非常多,从效率上影响了可扩展性。
可扩展性的分类
可分解为如下三个方面:
架构可扩展
主要解决架构的可理解性,当架构变得越来越复杂的时候,就需要将其拆分
拆分的形态,比如是微服务或分层的形式
拆分的粒度,比如服务拆成多细
应用可扩展和代码可扩展
可理解性也是靠拆分来进行,同样也要考虑拆分的形态和粒度,比如 module/package/class 等
可复用依赖“封装”——将变化封装起来,比如面向对象、设计模式就是干这个的
DDD 分为战略设计和战术设计,同时涵盖了可理解和可复用
拆分法则——鸡蛋篮子理论第一法则
如果一个篮子里的鸡蛋数不清楚,拆分到多个篮子再数。
拆分的两个方面——形态和粒度
拆分形态
自顶向下拆分可以分为如下几个层级
服务
模块
插件
package
拆分粒度
拆分的粒度决定了如下两类复杂度在系统总复杂度中的占比
内部复杂度:又称为“单体复杂度”,指单个对象内部的复杂度。可以用参与的开发人数来衡量单个拆分对象的复杂度。例如:3 个人负责一个子系统/子模块是比较合理的,20 个人来在同一个子系统上开发,则内部复杂度过高。
外部复杂度:又称为“群体复杂度”,指拆分后的多个对象之间的关系复杂度。可以用业务流程涉及对象数量来衡量外部复杂度。例如:一次用户请求需要 5 个子系统参与是比较合理的,如果需要 20 个子系统参与,则外部复杂度过高。
外部复杂度引入的复杂性高于内部复杂度,因为外部复杂度与对象的数量成指数关系。
拆分形态和粒度之间的关系
后者决定了前者,这其中额逻辑如下:
拆分是为了降低系统的复杂度。
复杂度分为内部复杂度和外部复杂度,其中外部复杂度的一个衡量指标就是系统内部对象的数量。
对某一个系统来说,处理复杂性的结果应该是有一个最优的内外复杂度的平衡(如何衡量最优?架构设计三原则)
拆分第一原则:内外平衡原则,内部复杂度和外部复杂度是天平的两端,一方降低,另一方必然升高,关键在于平衡。
拆分第二原则:先粗后细原则,如果你把握不准,那么就先拆少一些,后面发现有问题再继续拆分。反过来不可以先细后粗即先拆后合,因为外部复杂度引入的问题更多,先细后粗的拆分比先粗后细的拆分难度更高。
外部复杂度决定后,对象的数量基本就确定了,在哪个层级拆分也就确定了
封装复杂度模型
封装复杂度就是封装变化,将易变的和不变的隔离开。封装变化的前提是预测变化。
预测原则
第一原则,只预测 2 年内的可能变化,不要试图预测 10 年后的变化。例如:你准备接入微信支付,那么预测接入支付宝是很自然的,但数字钱包就没那么必要了。
第二原则,3 次法则,预测没有把握就不要封装,等到需要的时候重构即可。
(Martin Fowler)Rule of three: Three strikes and you refactor (1 写 2 抄 3 封装)。即第一次开发是不考虑封装,第二次同类开发抄过来然后改改,再有同类需求时,再考虑封装。
总结起来预测的时候要务实,遵循演化原则。
思考题
微信和支付宝复杂度对比,哪个更复杂,为什么?
课上华仔以微信和支付宝为例说明了复杂度的种类
它们的质量复杂度是差不多的
它们的业务复杂度,微信是业务数量多,支付宝是业务流程长
但如果以内部复杂度和外部复杂度再去进一步衡量后,微信的复杂度可能更高:
支付宝是一个交易系统,交易主体之间没有明显的、固定的关系,用关系数据库就可以描述这种关系;
微信不同,微信是社交产品(先不说微信也有支付功能),微信用户之间的联系有高频的,也有低频的,需要用图去描述这种关系,相当于外部复杂度高于支付宝,而外部复杂度引入的复杂性多于内部复杂度。
高性能复杂度模型
分类
两个维度分类,共四种
虽然有两个维度,但是结合自己的实际工作经验而言,重点是考虑集群的存储性能,因为对于单机的计算模型来说,往往是比较容易确定的,比如采用 Spring 技术栈,往往就确定了单机的计算模型;反而是单机存储性能(比如数据库选择),集群存储性能(比如分库分表、NoSQL 的选型等)是最重要也最耗费时间的。
单机/集群高性能
计算/存储高性能
单机的高性能具体而言有如下分析
计算高性能:进程模型、网络模型、缓存模型
存储高性能:存储模型
华仔老师将单机高性能中的缓存模型(本地缓存/独立缓存)列为计算高性能而不是存储高性能。初步给出的结论是缓存架构本质上属于计算架构而不属于存储架构,第五模块会详细讲解,此处先记录下来。
重点说说集群高性能
鸡蛋篮子理论第二法则——叠加法则
如果一个篮子装不下你的鸡蛋,用多个篮子!
所谓用多个篮子就是把一个大任务分成多个小任务,达成这个目标有两种手段
任务分配:个人理解就是每个小任务是相同的
任务分解:个人理解就是每个小任务是不同的。每个小任务不同,可以是处理逻辑不同(读写分离),也可以是数据不相同(数据分片)。
这里的分配和分解可以应该可以对应 AKF 立方体理论
AKF 把系统扩展分为以下三个维度:
X 轴:直接水平复制应用进程来扩展系统。
Y 轴:将功能拆分出来扩展系统。
Z 轴:基于用户信息扩展系统。
日常见到的各种系统扩展方案,都可以归结到 AKF 立方体的这三个维度上。而且,我们可以同时组合这 3 个方向上的扩展动作,使得系统可以近乎无限地提升性能。
任务分配
将同样的任务分配给多个服务器执行。
复杂度分析:
增加“任务分配器”节点,可以是独立的服务器,也可以是 SDK。
任务分配器需要管理所有的服务器,可以通过配置文件,也可以通过配置服务器(例如 ZooKeeper)。
任务分配器需要根据不同的需求采用不同的算法分配。
设计关键点:
运行形态
SDK
独立服务器
配置获取方式
配置文件
配置中心
任务分配算法
随机/轮训/权重
Hash/负载
任务分解
将服务器拆分为不同角色,不同服务器处理不同的业务。
复杂度分析:
相比任务分配复杂度分析,多了第三条——任务分解时需要记录“任务”和“服务器”的映射关系。
增加“任务分解器”节点,可以是独立的服务器,也可以是 SDK。
任务分解器需要管理所有的服务器,可以通过配置文件,也可以通过配置服务器(例如 ZooKeeper)。
需要设计任务拆分的方式,任务分解器需要记录“任务”和“服务器”的映射关系。
任务分解器需要根据不同的需求采用不同的算法分配。
设计关键点
和任务分配类似,只是多了“任务拆分”这一点。
任务拆分
任务分类,例如读写分离(AKF 立方体 Y 轴)
任务分段,例如数据库分表(AKF 立方体 Z 轴)
运行形态
SDK
独立服务器
配置获取方式
配置文件
配置中心
任务分配算法
随机/轮训/权重
Hash/负载
路由/sharding
如何设计高可用架构
高可用的基本原理就是冗余,所以高可用方案必然是集群方案。此外,高可用仍然分为
计算高可用
存储高可用
鸡蛋篮子理论第三法则——冗余法则
鸡蛋篮子理论第三法则(冗余法则):不要把所有鸡蛋装在一个篮子,放到多个篮子!
计算高可用
和计算高性能类似,也分为任务分配和任务分解。
任务分配
复杂度分析前三点和集群计算高性能一样,只是增加了对业务服务器状态的监控,以便做到在故障时对其进行切换。
正是因为集群计算高性能和(集群)计算高可用在形式上非常类似,所以下面的笔记在形式上将计算高性能作为一个“子集”,计算高可用作为一个“超集”。
但是计算高性能和计算高可用有一个关键的区别:高性能任务分配考虑的是正常处理,高可用任务分配考虑的是异常处理。
复杂度分析:
集群计算高性能任务分配分析要点
增加“任务分配器”节点,可以是独立的服务器,也可以是 SDK。
任务分配器需要管理所有的服务器,可以通过配置文件,也可以通过配置服务器(例如 ZooKeeper)。
任务分配器需要根据不同的需求采用不同的算法分配。
任务分配器需要监控业务服务器的状态,在故障时进行切换。
设计关键点
状态监测
集群计算高性能人物分配设计关键点
运行形态
SDK
独立服务器
配置获取方式
配置文件
配置中心
任务分配算法
随机/轮训/权重
Hash/负载
计算高性能架构一定就是计算高可用架构么
还是根据两者的关键区别——是否监测服务状态来区分,有状态监测的则是高可用,否则只是高性能。
任务分解
复杂度分析
计算高性能任务分解分析要点
增加“任务分解器”节点,可以是独立的服务器,也可以是 SDK。
任务分解器需要管理所有的服务器,可以通过配置文件,也可以通过配置服务器(例如 ZooKeeper)。
需要设计任务拆分的方式,任务分解器需要记录“任务”和“服务器”的映射关系。
任务分解器需要根据不同的需求采用不同的算法分配。
任务分解器需要监控业务服务器的状态,在故障时进行切换。
设计关键点
和计算高性能类似,只是额外增加了“状态检测”。
任务分解高可用之所以在把不同的业务拆分后可以提高可用性,在于做到了不同的服务之间的“资源”做到了隔离。比如 A 服务有 BUG,占用了绝大多数 CPU 时间,如果 B 服务拆分出去了,就不会受到影响。
存储高可用
使得存储高可用,方案可以分为数据复制和状态决策两个大的方面。对于存储这种有状态的节点说,主要评估点有数据一致性、性能、复杂度等。
数据复制
复制格式
复制命令
比如 MySQL 的高可用主要依靠 binlog,复制命令对应 binlog 使用 statement 格式;又比如 Redis 的 AOF 日志。此时复制的数据量比较小,但有可能数据不一致,适合增量复制。
复制数据
比如 MySQL 将 binlog 格式设置为 ROW,此时里面记录的就是 SQL 语句执行前后的数据的值。此时数据量较大,但数据是强一致的。
复制文件
文件相当于数据的一个快照,比如 Redis 的 RDB 文件,对已复制的数据可以保证数据的一致性,但是对于文件生成后仍然变化的数据就不管用了。
复制方式
同步:强一致性,故障容忍度低:成本高了,结果出故障的概率和单机的故障率一样,一个节点故障,则全部故障;写入性能低,必须等全部节点都复制成功后才算复制成功。所以适应于节点不多的情况,比如主备/主从架构。
异步:和同步复制相反,数据容易不一致;但是故障容忍度高,写入性能高。适合存储集群。
半同步:同步和异步方式的折中办法,适合集群。
多数同步:数据强一致,最强可用性,故障容忍度高;但是由于也要写入多个节点,所以写入性能也不高,比如 ZooKeeper 的性能在做配置中心时不如 Nacos。适合分布式一致性和分布式协同(OceanBase、ZooKeeper)。
状态决策
这里的状态决策就是检测存储节点的状态,并作出相应的动作。主要记住它们对应的数据一致性强度。
独裁式
场景就是由额外的节点检测存储节点的状态,比如 Redis 哨兵模式中哨兵来决定主从的切换。
决策逻辑简单
决策者要做到高可用,整体架构复杂,常用 ZooKeeper /Raft/Keepalived;这里有点递归的意味:存储集群要高可用,决策者也需要高可用,决策者往往采用民主式的策略来保证高可用
数据一致性强度中等
应用场景:绝大部分业务都可以应用
协商式
场景就是存储集群内部节点自己来决定自己的角色,比如主备模式,当备机检测到主机下线后,自己变为主机。
比较简单,一般使用心跳进行决策
网络分区时,会导致双主,可以用双通道(比如网线和串口)来解决
数据一致性弱
应用场景:内部系统,网络设备
民主式
很多分布式存储系统都会使用这种方式,比如 ZooKeeper 等
决策过程复杂,一般使用标准算法进行选举,比如 Raft、ZAB、Paxos
可用性最高,数据一致性最强
用 quorum 解决“脑裂”问题:即必须获取半数以上的选票才可以当选
应用场景:对数据一致性要求很高的场景,比如余额、库存
思考题
对比一下高性能架构和高可用架构,你觉得哪个更复杂,为什么?
有状态的服务比无状态的服务,设计起来更加复杂,因为有状态的服务涉及到数据一致性问题。高可用架构和集群高性能架构形式上非常类似,都是从计算和存储两个方面进行分析。
但是在复杂度分析时,高可用架构相比高性能架构,需要额外分析应用节点的状态,特别是存储高可用的分析中,要考虑节点间数据的一致性,而在分布式集群环境中,数据一致性的处理是非常复杂的。
所以,从数据的角度看,高可用架构更加复杂。
全面提升架构设计的质量
常见的架构质量属性
低成本
低成本本质上是对架构的一种约束,与高性能/高可用/可扩展等架构是冲突的!
优先设计架构方案,再看如何降低成本
安全性
架构安全,更多的是依赖外部的力量,比如网络运营商或网络设备(防火墙)。
业务安全,业务安全更多是编码和管理方面的措施!
架构设计只能解决架构安全问题,不能解决业务安全
可测试性/可维护性/可观测性
可测试性,指软件系统在测试环境下能否方便的支持测试各种场景的能力。
可维护性,指软件系统支持定位问题、修复问题的能力。
可观测性,指软件对外展现内部状态的能力。
信息输出
信息展现
可观测性本质上是应用输出信息,运维平台/管理平台聚合展现信息
其中,可观测性是可测试性和可维护性的基础。
可测试性和可维护性非常的类似,他们都可以从架构和应用两方面衡量。
可测试性(可维护性)
架构可测试(可维护)
全链路压测(跟踪)
可调整系统状态
比如对可测试来说,可以切换主备、出发选举等,以满足某种状态下的测试
比如对可维护来说,可以触发系统的降级、下线、切换等
业务可测试(可维护)
变量可修改
状态可见
行为可手动触发,比如管理后台停用某个队列
个人理解,可测试性和可维护性这么相似,他们更多的是环境和操作人员的不同:
前者是测试环境,后者是生产环境;
前者是测试人员进行操作,后者是运维人员进行操作
设计更好架构的步骤
分析系统的复杂度(高性能、高可用、可扩展)
设计备选架构
根据架构三原则挑选一个架构
优化架构,以提升架构质量,包括这里提到的常见架构质量属性
根据上面的步骤,可以得出,如果要给系统增加管理后台,应该在最后一步,因为根据“常见的架构质量属性”小节的描述,管理后台的本质是聚合、展现应用输出的信息,是架构可观测性的一部分,目标是提高架构的质量,是对架构的一种优化。
思考题
架构设计三原则帮助我们设计好的架构,这节课讲述了如何全面提升架构设计的质量,那么这两部分的区别和联系是什么?
根据设计更好架构的步骤,首先要分析复杂度、设计备选架构、挑选总体架构,然后再优化、全面提升架构涉及的质量。其中分析复杂度、设计备选架构、挑选总体架构这三个环节就是使用架构设计三原则的过程。所以设计三原则和全面提升架构设计质量是是夹头设计中前后关联的两部分,前者是基础,后者可以让架构锦上添花、更加完善。
微信红包高性能复杂度分析实战
高性能的分析就是按照高性能复杂度框架,按图索骥即可。框架就是本笔记的《高性能复杂度》小节,可以分为计算和存储两大部分
发红包分析
单机计算模型不用额外考虑了,“计算高性能——分类”处的注解所说,现行使用的开发框架或已有系统已经决定了。集群计算性能模型简单的使用负载来分配任务就可以了。
单机存储模型,因为涉及到事务,所以采用关系数据库就可以了;
至于 LSM 的原理,是利用了磁盘 IO 批量写入性能高于多次随机写入的特性,先将数据暂存在内存中,凑够一块后再写入磁盘;LSM 不仅利用了批量写,而且也使用了追加写即 WAL 技术(Write Ahead Log)来写 log 文件来保证内存中的数据在系统崩溃后能恢复;
而关系数据库如 MySQL 往往只利用了追加写即 WAL 技术(比如 redolog)来提高写入性能(其实 redolog 也是有缓存的),有保证事务的持久化。
集群存储高性能模型使用任务分解,使用分片(比如 Sharding-JDBC)来存储红包数据。
抢红包分析
抢红包和发红包不同,虽然它们都是高并发,但是在架构设计上有所不同。
从数据的角度来说,它们都是要解决数据一致性的问题;
发红包不需要处理任何冲突,因为将红包放入数据库中,甚至连行锁都不需要,只是并发线程数很高,所以需要分库用来抗住这么多并发。
抢红包不同,需要处理读写冲突和写写冲突,所以使用了 redis。
写读冲突发生的前提是先发生了写操作,后发生了读操作,此时要处理的冲突是如果之前的写操作回滚了,现在的读操作该怎么办。即过去影响现在的问题。
读写冲突发生的前提是先发生了读操作,后发生了写操作。此时要处理的冲突是如果后面的写操作提交了,之前发生的读操作是否可以读取到这个后写入的值。即未来影响现在的问题。
两者的共性是由于写操作改变了数据的状态,它们要处理这个改变对过去和现在的影响。
解决写写冲突——读未提交。最低的隔离级别 Read Uncommitted 解决了脏写(Dirty Write)的问题。脏写指的是两个事务写了同一份资源,这样后写的事务会覆盖先写的内容。如果可以读取到还没有提交的数据,在它的基础上再进行更新,这样就不会脏写,即不会覆盖别的写操作了。写写冲突应该也是需要锁的,让前面的写操作完成,再让后面的写操作开始。其实脏写就是写写冲突,示意图如下:
解决写读冲突——读已提交。这个隔离级别除了解决脏写问题以外,还解决了脏读(Dirty Read)问题。 脏读指的是当一个未结束的事务写了一个值之后,另一个事务读取了这个值。一旦前面的事务通过回滚取消了自己的所有操作,那么后面的事务就会读取到一个不应该存在的值,也就是读了一份脏数据。
解决读写冲突——可重复读。读写冲突和写读冲突刚好相反。读写冲突发生的时候需要负责写的事务提交,而写读冲突需
要写的事务回滚。那为什么要叫这个名字呢?原因是读的事务如果再读一次的话,会将另一个事务写入的值读回来,因此前后两次读到的结果会不一致。示意图如下:
抢红包需要用到锁(影响性能)或者 MVCC 多版本控制(会记录大量回滚数据,也会影响性能),所以类似秒杀的抢红包场景用到了 Redis 缓存。
单机计算高性能,使用 Redis 缓存记录已领个数。个人理解这是整个架构都需要缓存了,所以单机顺理成章的需要缓存。
单机存储高性能,使用 Redis List 记录领取记录。
和发红包不同,因为涉及的数据的一致性和解决数据写、读之间的冲突问题,所以这里单机的高性能就不想发红包那么简单了,而是变成了集群高性能的基础;集群高性能在单机的基础上再进行任务分配或分解。所以涉及到数据的时候,不论按什么标准分类计算高性能和存储高性能,都会变得复杂。
集群计算高性能,业务简单,只需负载均衡分配任务即可
集群存储高性能,使用 Redis Cluster 来将数据分解到不同的节点,并路由交易请求。
看红包分析
经过抢红包后,看红包可以看做是自然的延伸,直接沿用抢红包的架构就可以了。此处有一个重要的结论:
QPS 的业务很大程度上依赖 TPS 的业务!
整体架构
此时主要是在之前分场景讨论的基础上给出集群方面的架构。
任务分配方面,计算高性能和存储高性能都是异地多机房。即相当于每个机房的功能是一样的。
任务分解方面
计算高性能不再拆分发红包、抢红包、看红包到不同的服务,即应用服务器上同时可以完成 3 种任务。
存储高性能部分,发红包的数据(使用关系数据库)和抢红包的数据(使用 Redis)已经分开。
成本对高性能架构的约束
性能需求 = 资源(处理能力) * 数量 <===> 成本 = 资源(成本) * 数量
上面公式展示出,性能需求和成本在资源需求上是互相制约的。同时还可以看出,如果在降低成本的同时,还要保持性能不变,有两个方向:
提升资源处理能力(即减少资源数量)
降低资源成本(此时数量可以不变)
替换一定是用低成本资源替换高成本资源么?
这个不一定,还是要看“性能/成本”比,如果性能提高的比例高于成本提高的比例,就是值得的。比如 64 核服务器价格比 32 核服务器的成本高出 30%左右,但是性能可以提高 80%,此时总成本就是下降的。
思考题
微信红包实际的架构是怎样的,如果有差异,原因可能是什么?
微信红包实际的架构是怎样的?
在红包大战考验支付系统,揭秘微信摇一摇背后的技术细节中,给出了红包支付的总体架构,总体上有三部分组成:信息流、业务流和资金流,与组织架构对应,分别为微信后台、微信支付后台、财付通后台,如下
整体架构涵盖了信息流、业务流和资金流,而在百亿级微信红包的高并发资金交易系统设计方案中针对业务流提到了一些细节,非常有意思
微信红包业务形态上类似普通商品“秒杀”活动:
发红包,相当于商品上架
抢红包相当于查询库存
抢到后拆红包的动作相当于最后的“秒杀”动作
但是红包业务并发要求更高:有十万个群在发红包就相当于有 10 万个秒杀活动在继续
由于涉及到资金,所以安全级别更高
微信红包本质上是资金交易
微信红包是微信支付的一个商户
发红包相当于在微信红包这个商户上使用微信支付购买“钱”,收货地址是微信群
支付成功后,红包“发货”到微信群里
拆开红包相当于将“钱”转账到拆红包用户的微信零钱账户里
锁、事务
由于抢红包时要涉及处理数据的写操作冲突,所以要引入锁库存、事务,其余请求只能等待,这样会引起 DB 上排队严重,性能下降
如果用缓存代替实时的 DB 事务操作,在内存中进行库存扣减,成功后一步落 DB 持久化,但这样在 DB 持久失败的情况中,会丢失数据,不适合涉资金类交易系统
如果用乐观锁代替悲观锁,会导致大数量的无效更新请求、事务回滚(发现冲突后进行的回滚),给 DB 造成压力。此外,由于并发量太大,会用很多请求在使用乐观锁时,记录的版本号都是相同的,只有一个可以成功,其它报错,影响体验。
最后给出的方案如下:
系统垂直 SET 化,分而治之
发红包时为红包生成一个唯一 ID,下面的所有操作都和这个 ID 关联
根据 ID 做一致性哈希,将相关请求固定发送到同一个 SET(相当于 sticky 客户端会话)
每个 SET 既有应用服务器,又有 DB。相当于 DB 做了分库分表。
分库时考虑数据的冷热,即按照红包 ID,又按照日期,这样每个表的数据量就不会很大了。
逻辑 SERVER 层将请求排队,用串行化解决 DB 并发难题。stick 到同一台 Server 上的所有请求由接收进程处理,按红包 ID 排队,串行地由 worker 进程处理,达到排队效果
用 memcached 控制并发,防止队列被降级:用 memcached 的 CAS 原子操作控制同时进入 DB 中执行事务的请求数,超过预先设定好的数值则直接拒绝服务。
差异如何
基本和老师给出的架构类似,只是针对组织架构给出了更详细的信息流、业务流、资金流的划分,但老师给的方案更加符合“合适”原则,更适合课程内讲解;
真实的架构根据支付的特点,给出了更具体的分析。但仍然和课程里的讲解基本一致。
钱包高可用复杂度分析实战
高可用要求来源 - 容忍度
与使用性能指标进行高性能分析不同,进行高可用分析时,使用的是容忍度。
定义:用户可接受的业务不可用程度,包括时长和影响。
容忍度的决定因素:文化、法律、用户、业务。
考虑容忍度的通用因素(按容忍度从低到高进行排序):
生命
安全
金钱
付费用户
免费用户
内部用户
所以,钱包高可用复杂度分析就是容忍度分析。
整体上,因为涉及到账务交易,钱包高可用的复杂点还是在于存储的高可用,即在于处理数据。此时的计算高可用,因为业务比较简单,所以只需做任务分配即可。下面重点说说存储高可用。
这里的场景有如下的两个主要考虑点:
需要保证冗余节点间数据存储的强一致性,所以不适合使用协商式的状态决策方式。
在线交易,每次一般只涉及到一个账户,所以数据更新的范围不大。
注意,高可用的前提一定是存在集群。
余额转账高可用
一致性要求最高,容忍度中等,决定其储存高可用如下:
数据复制
复制格式:命令
复制方式:半同步或多数复制。不使用同步复制方式,因为同步复制的故障容忍度太低。
状态决策
如果是独裁式,可以用 MySQL 现有机制,比如 MySQL Router
大规模集群,可以用民主式,比如 OceanBase
银行卡支付高可用
此时的涉及到的数据,有余额变成了银行卡信息,因为此时的支付是通过银行进行的,账户状态的一致性由银行来完成,所以一致性要求没有那么高了。容忍度最低。
数据复制
复制格式:命令
复制方式:异步复制,因为银行卡信息变化频率较低,且可以容忍丢失。不使用同步复制方式,因为同步复制的故障容忍度太低,且不需要最强的一致性。
状态决策
独裁式即可,比如 MySQL Router
民主式,无需使用,因为一致性要求没有那么高,不需要引入额外的复杂度。
运营后台高可用
一致性要求最低,容忍度要求最高,设计起来最简单。
数据复制
复制格式:命令或数据格式都可,因为数据量不大,一致性要求也最低
复制方式:同步复制即可
状态决策
协商式,即 MySQL 主备即可
钱包整体高可用架构
余额转账、银行卡支付、内部运营系统做任务分解(分成不同的服务和不同的存储);
所有的业务使用双机房部署,即不同的机房之间是任务分配的关系。
高可用架构的成本优化
可用性需求 = 资源(可靠性) * 数量 <===> 成本 = 资源(成本) * 数量
主要是降低资源的成本
为啥不可以用高可靠性的资源替换低可用资源?因为高可靠性资源可靠性提高的程度和价格提升的程度不成正比。
思考题
可以通过优化提升处理性能从而降低成本,为何不能通过优化提升可用性来降低成本?
所谓提升性能是指提高单位资源的处理能力,即相同的资源,但是能处理更多的业务,所以这能降低成本;
而可用性则不同,优化可用性本身可能就需要投入很多成本,反而使得总成本上升。
评论