架构实战营 1 期模块八作业——中间件设计
模块八——中间件设计笔记及作业
作业部分
设计消息队列存储消息数据的 MySQL 表格
【作业要求】
包括表名、字段、索引;
用文字描述设计思路和理由,例如:为什么设计某个索引?
一页 PPT 即可
【提示】
需要考虑每个消息队列一张表,还是所有消息放一张表,里面加一个“队列名称”的字段。
作业前提
采用“发布-订阅”模型。
使用 Kafka 的“分区”概念而不是 RocketMQ 的“队列”概念,觉得“分区”这个词更准确的描述“分片”的架构。
每个主题下可以有多个分区,分布在不同的 Broker 上。
每个消费者组在每个队列上的当前消费位置用独立的表维护,每个 Broker 上都有这个表。
每个分区只能由消费者组内的一个消费者来消费(保证消息的有序性)‘
整体架构就是华仔课上给出的,如下:

作答
主题与分区的实现
这里的分析以 Kafka 的概念为蓝本进行。
首先,主题是一个逻辑概念,是存储消息的容器,可以被不同的消费者组一起消费,它可以作为“分区对象”的一个属性;
其次,队列才是真正的存放消息的地方,对应 MySQL 数据库中的表,是“分片架构”中“片”的体现。
队列在 Kafka 中的对应物叫分区。
此外,消息队列中的存储节点叫做 Broker,相当于数据库分库中的一个库。
消息队列中间件的分片设计
分片规则
常用的分片规则有哈希分片和范围分片,但是在消息队列中,“分片”设计与传统的哈希分片和范围分片都不相同,Broker、主题、队列之间没有对应关系,比如

从上面可以看出,消息队列中的队列和 Broker(存储节点)没有对应关系,分布也可以不均匀。
每个生产者可以在 5 个队列中轮询发送,也可以随机选一个队列发送,或者只往某个队列发送。
一个消费组中可以包含多个消费者的实例。比如说消费组 G1,包含了 2 个消费者 C0 和 C1,那这 2 个消费者又是怎么和主题 MyTopic 的 5 个队列对应的呢?由于消费确认机制的限制,同时为了保证消息的有序性,每个队列只能被一个消费者实例占用。所以,必须做到保证每个队列分配一个消费者就行了,即多对一的关系。
此处不考虑消息队列中分片(即队列)的调度机制,或者说采用静态的调度机制,移动分片时采用人工的方式进行。
路由规则
基于上述分片规则,要有配套的路由规则。
由于静态路由无法动态扩容,所以不采用静态路由的方案;
动态路由又分为配置中心和动态路由两种形式,动态路由需要分片服务器之间互相知道数据的分布情况,一般还需要用 gossip 协议来保证集群状态信息的一致性,比较复杂;同时,动态路由针对的是节点频繁变化的场景,这里采用数据库作为消息队列的底层,节点并不会频繁变化。
所以,采用配置中心的形式做动态路由。
配置中心集中管理数据和队列服务器之间的对应关系。
进一步,进行分库分表的设计
队列对应数据库的表;
Broker 对应数据库实例

主题对应于配置中心,保存队列和 Broker(这里对应数据库实例)的映射关系,实现上对应一个配置表,配置表逻辑上全局一份,依赖于数据库的主备复制来保证高可用。
理想情况下,Consumer 实例的数量应该等于该 Group 订阅主题的队列总数,一般不推荐设置大于总队列数的 Consumer 实例。每个主题的数据量是不一样的,需要的消费者数量也是不一样的,所以每个主题中的队列数也应该根据需要进行设置。
队列是逻辑上的,还是物理上的?也即是作业中的问题:
每个消息队列一张表,还是所有消息放一张表,里面加一个“队列名称”的字段?
如果每个队列一张表,那在创建主题、新建表的时候,需要考虑表放在哪个数据库实例(Broker)上,因为队列的数量每个主题都不是固定的,而是根据需要实际设定的,为了考虑数据分布的比较平衡,即数据库实例的压力分布均匀,需要进行比较复杂的计算;
但如果涉及到消费者的扩缩容,会比较简单。
如果所有消息队列一张表,将数据放置在哪个数据库实例上仍然需要考虑;
如果涉及到消费者的扩缩容时,会设计到数据的迁移,会非常的麻烦。
综合上述考虑,采用每个消息队列一张表的形式。
最终实现
配置中心表设计
主题表设计
配置中心表设计
Broker 表设计
队列表设计
队列元数据表设计
队列数据表设计
笔记部分
单机高性能网络模型
传统网络模型
PPC(Process per connection) 和 prefork(processes are forked before connection)
Apache MPM prefork 模式,默认 256 个连接。
TPC:Thread per connection 和 prethread:thread are created before connection.
Apache 服务器 MPM worker 模式就是 prethread 模 式的变种(多进程 + prethread),默认支持 16 × 25 = 400 个并发处理线程。
Reactor 网络模型
基于多路复用的事件响应网络编程模型。
多路复用:多个连接复用同一个阻塞对象,例如 Java 的 Selector、epoll 的 epoll_fd(epoll_create 函数创建)。这些连接由若干线程处理,这些线程阻塞在阻塞对象上。
事件响应:阻塞对象返回后,需要通知被阻塞的线程,通知是通过事件进行的。事件由操作系统内核分发给应用线程,但是对于像 JAVA 这样的语言,是看不到系统内核的。
被阻塞的线程的数量远远少于连接的数量,所以客户支持海量连接。
单 Reactor 单进程/单线程

案例:redis

到了 6.0 版本,Redis 变为多线程,但是和下面的单 Reactor 多线程又不是完全一样,其子线程池只是用来进行 IO 的。


单 Reactor 多线程

案例:参见上节 Redis6.0 模型。
多 Reactor 多进程/线程

Proactor 网络模型

三类网络模型实战技巧
“多 Reactor 多线程” 是目前已有技术中接近完美的技术方案!
所有场景;
所有平台;
性能和 Proactor 接近。
直接用开源框架,千万不要自己去实现,例如 Netty、libevent (memcached 网络框架)、libuv(node.js 底层网络框架)。
思考题
如果开发消息队列,可以选用哪些网络模型?
考虑到消息队列需要持久化,本地 IO 是比较耗时的,所以“单 Reactor 多线程”和“多 Reactor 多进程/线程”都应该是可行的。
如何基于 ZooKeeper 实现高可用架构
ZooKeeper 高可用相关特性
ZooKeeper 技术本质

ZAB is not Paxos, it is primarily designed for primary-backup systems, like Zookeeper, rather than for state machine replication.
说起 state machine replication,把模块 7 作业的一些内容摘过来。
解决在分布式系统中,动态数据如何在不可靠的网络通讯条件下,依然能在各个节点之间正确复制的问题。即复制数据,使其可靠的过程要保证系统的可用性。
数据同步——状态转移
系统里的每个节点都反馈成功的完成磁盘写入后,数据的变化才能宣告成功,比如 MySQL 的主从复制。虽然能保证数据是一致的,但任何一个节点(即可用场景下,系统整体的可用性与每个节点可用性之间是“或”的关系)因为任何原因没有响应都会阻塞这个过程,导致系统不可用。相当于节点越多,成本越高,可用性相比单个节点确没有提高。此时可用与可靠出现了矛盾。
以同步为代表的数据复制方法,叫做状态转移(State Transfer),是一种牺牲可用性的可靠性保障手段。
数据同步——操作转移
为了缓解系统高可用和高可靠之间的矛盾,分布式系统里主流的数据复制方法,是以**操作转移(Operation Transfer)**为基础的。想要改变数据的状态,除了直接将目标状态赋予它以外,还可以通过某种操作,把源状态转移为目标状态。
这里的操作往往就是日志记录操作,比如 Etcd 中就使用了 WAL 日志(就是和 MySQL 上使用的 WAL 是同一种性质的日志,虽然格式不同,但是目的是一样的,比如 crash safe、顺序写等)。
使用确定的操作,促使状态之间产生确定的转移结果的计算模型,就是状态机(State Machine)。要让多台机器的最终状态一致,只要确保它们的初始状态和接收到的操作指令都是完全一致的。还是拿 MySQL 做对比,虽然 MySQL 里没有状态机的概念,但是 MySQL 在提交的时候,也会有 Change Buffer 和 redolog 做配合。
这里操作指令就是一连串的、在系统内传播的消息。“消息”就此登场,它还会在下面的 FLP 不可能原理中扮演重要角色。
消息的传播与处理期间,允许系统内部状态存在不一致的情况,但是此期间的状态不能被外部观察到;但是消息序列执行完成的时候,所有节点的最终状态是一致的。这种模型,就是状态复制机(State Machine Replication)。且最终状态采用“少数服从多数”的原则。这样就可以容忍少数节点失联,使得增加机器数量可以用来提升系统整体的可用性,即 Quorum 机制。
ZooKeeper 数据模型
Watches
Clients can set watches on znodes. Changes to that znode trigger the watch and then clear the watch. When a watch triggers, ZooKeeper sends the client a notification.
Data Access
The data stored at each znode in a namespace is read and written atomically.
Ephemeral Nodes
These znodes exists as long as the session that created the znode is active.
Sequence Nodes
When creating a znode you can also request that ZooKeeper append a monotonicly increasing counter to the end of path. This counter is unique to the parent znode.
ZooKeeper 设计步骤

ZooKeeper 实现主备切换架构
设计 path。使其与角色对应:主节点 path:/com/taobao/book/operating/master;备节点路径:/com/taobao/book/operating/slave。
选择节点类型。znode 的存在与否对应于主备节点的存活与否,所以选择 ephemeral 类型的 znode。
设计节点数据。由于 slave 成为 master 后,会成为新的复制源,可能出现数据冲突,因此 slave 成为 master 后,节点写入成为 master 的时间,这样方便人工修复冲突数据。
设计 watch。
节点启动时,尝试成为 master,即创建 master znode,创建成功则成为 master 节点,否则成为 slave 节点;
如果 slave 节点收到 master znode 的删除事件,则再次尝试称为 master,即创建 master znode;如果创建成功,则成为新的 master 节点,并删除之前的 slave znode。
ZooKeeper 实现集群选举
方案 1 - 最小节点获胜
设计 path。每个集群用一个节点来表示,集群成员是这个节点的子节点,在 parent 目录下创建自己的 znode。
选择节点类型。当选举发生时,编号最小的 znode 节点成为新的 Leader,因此用 ephemeral_sequential 类型 znode。
设计节点数据。根据需要灵活设计。
设计 watch。监控的是整个集群,所以只能监控代表集群的 parent znode,即其所有子节点的状态变化。
节点启动或者重连后,在 parent 目录下创建 ephemeral_sequential znode;
然后扫描 parent 目录下所有 znode,如果自己的 znode 编号是最小的,则成为 leader,否则 watch 整个 parent 目录;
当 parent 目录有节点删除的时候,首先判断其是否是 leader 节点,再看其节点编号是否正好比自己小 1:
如果是,则自己成为 leader;
否则继续 watch。
方案 2 - 抢建唯一节点。
和方案 1 非常类似,都是抢先建立成功具有某种唯一特征的 znode 节点成功的为 leader:方案一是抢建顺序号最小的节点;方案二是抢建唯一的节点。
设计 path。主节点只有一个 leader node,本质就是一个分布式锁。
选择 znode 类型。可以表征 Leader 节点的状态,所以选择 ephemeral 类型的节点。
设计节点数据。灵活根据业务需要写入数据。
设计 watch。
节点启动或重连后,尝试创建 leader znode
成功为 leader
否则 watch leader znode
收到 leader znode 被删除的事件后,再次尝试创建 leader znode,即执行上一步。
方案 3 - 法官判决
和方案 1 类似,只是持有最小编号 znode 的集群节点不再是主节点,而是成为法官,然后根据一定的规则在 parent 目录下选择 Leader Node,比如选择具有最大事务 ID 的节点成为主节点。
方案 1、方案 2 比较简单,但是灵活性比较低;适合于计算集群;
方案 3 比较复杂,但是灵活性比较高;适合于存储集群。
思考题
Redis 为何通过 Sentinel 来自己实现集群选举的功能,而不基于 ZooKeeper 来实现?Redis 这种做法有什么优缺点?
Sentinel 选主的过程
来自专栏《Redis 核心技术与实战》。
先按照一定的筛选条件,把不符合条件的从库去掉。
已经下线的从库肯定不符合条件;
网路总是断连的从库不符合条件。
对于剩下的从库,按照一定的规则,给它们打分,得分最高的选为新主库。
按照三个规则依次进行三轮打分,这三个规则分别是从库优先级、从库复制进度以及从库 ID 号。只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。
第一轮,优先级最高的从库得分高。
第二轮,和旧主库同步程度最接近的从库得分高。
第三轮:ID 号小的从库得分高。
从上面的过程可以看出,如果采用 ZooKeeper 来实现集群选举的话,与筛选从库条件相干的几个特征都可以写入从库对应的 znode,然后在找一个第三方去对这些指标作出评价,从而选择一个主节点。
这个过程和 ZooKeeper 实现集群选举方案中的“法官判决”非常像,哨兵的角色其实就是一个“法官”。
两种方案的对比
使用常见架构评估维度对两种架构进行评估。
按照模块三的讲解,对两种选主方案按照如下几个维度进行比对:
性能:两个方案差不多
可用性:两个方案差不多
可扩展:两个方案差不多
成本:两个方案差不多
安全:两个方案差不多
技术复杂度
不论采用哪个方案,都存在哨兵或法官这个仲裁角色。
哨兵选主的方案对使用方来说,部署起来更加单。但如果采用 ZooKeeper 方案,因为很多入有 ZooKeeper 经验,所以可运维性更好。
如果采用 ZooKeeper 方案,那么主从集群中,每个节点需要将复制进度写入自己在 ZooKeeper 上对应的节点中;哨兵方案应该是哨兵通过订阅数据节点的相关频道来获取。
如果采用 ZooKeeper 方案,客户端需要 watch parent znode 以便及时获取主节点变更的消息;哨兵方案中客户端是订阅哨兵的对应频道获取主节点变更的消息。
从上述两个方面可以看出,对实现团队来讲,方案复杂度差不多,但是对人员的要求不一样,哨兵选主方案更优一些。
复制集群架构设计技巧
Redis Sentinel 设计技巧
Redis 集群架构模式对比

MongoDB Replication 设计技巧

主从异步复制时复制的是 oplog;
新节点同步流程和 Redis 非常像
首先全量复制数据和 oplog
全量完成后,再进行增量复制
和 Redis 的不同之处在于复制源不一定是 Primary,而是通过算法选出来的。
读取数据也很有特点:默认读 Primary,但可以指定 read preference 来读取 Secondary;事务必须读 Primary
类似于 Redis 有哨兵,MongoDB 有一个 Arbiter,只投票,不复制数据。
直接两个节点做主备而不使用 Arbiter 无法避免双主或者脑裂的现象,这是双机节点无法避免的。
思考题
对比一下 Redis sentinel 和 MongoDB replication 的实现异同和优缺点。
新节点同步流程非常像:
首先全量复制数据和 oplog
全量完成后,再进行增量复制
但是复制的方式不太一样
MongoDB 支持数据复制和 oplog 复制
Redis sentinel 只支持命令复制,相当于 oplog 复制。
选主的方式非常像
MongoDB 在 3.2.0 版本后使用 Raft 算法
哨兵选主也使用类似 Raft 的算法。
分片架构设计技巧
Elasticsearch 集群设计技巧

有几个如下特点:
节点的角色是配置的,可以同时具有多个角色;
高可用的基本单位是数据分片,而不是节点,一个节点上可以有多个分片的副本。
选主算法从 7.0 版本开始也(从类 Bully)转换为了类 Raft 算法。
部署模式
Master 和 Data 混合部署。
只有主节点可以处理写请求,但是写请求最开始可能是由从节点接收,通过路由转发的方式将其转发到主节点。
每个节点可以存储的数据大小是节点存储容量除以副本的数量。
Master 和 Data 分离部署
少量的 Master 节点和大量的 Data 节点,Data 节点数量多了,所以可以存储较大数量的数据
Coordinating 分离部署
相对于 Master 和 Data 分离部署架构,增加了 Coordinating 节点(2 个以上),负责读写聚合。相当于做了计算和存储相分离的架构。这样在可以存储大量数据的基础上,还可以处理读写请求比较复杂的业务。
在第二个和第三个架构模式之间,优选第二个,这样更符合合适原则和演进原则。
Cross cluster replication
相比于上面三个单集群的部署方式,这个模式是多集群的一种部署方式,数据在多集群间进行复制。
Redis cluster 设计分析

Cluster 分为多个分片,不同分片保存不同数据
每个分片内部通过主备复制来保证可用性;
分片内部自动实现 Master 选举,但不依赖 Sentinel,Cluster 本身具备分片选举的能力;
客户端连接集群需要特定的实现,例如 jedisCluster,因为 Cluster 有特有的 Redis 命令。
数据路由使用客户端重定向进行动态转发
所有分片(的 Master 节点)都可以接收客户请求,Client 连接任意节点,如果当前节点没有需要的数据,由节点用 move 指令来告诉实际的数据位置;
需要每个节点都有所有 key 的分布信息;
节点之间通过 Gossip 交换信息,节点变化的时候会自动更新集群信息;
MongoDB sharding 架构

和 Redis Cluster 所有分片都可以接收任意请求不同,只有 mongos 可以接收客户端的请求;mongos 可以和应用程序部署在一起,也可以和 Shard 服务器部署在一起;
元信息保存
和 Redis Cluster 所有分片都保存所有 key 的信息不同,由 Config Server 专门保存集群的元数据;
元信息高可用
Config Server 通过多副本保证高可用。Config Server 一旦挂掉,整个集群进入只读状态。
数据分片的原理和 Redis Cluster 一样,每个分片的高可用原理也一样。
HDFS 架构

元信息保存
由 NameNode 进行,同时 NameNode 还负责管理集群(平衡、分配)。
元信息高可用
NameNode 采用类似主备的复制架构,主备复制通过 JournalNode 集群复制日志的方式进行;
JournalNode 集群的高可用与日志一致性采用多数复制的方案
NameNode 的切换由与 NameNode 同节点的 FailoverController 进行;
FailoverController 的高可用依赖于 ZooKeeper。
思考题
HDFS 采取 JournalNode 这种模式的可能原因是什么?
性能么?真的好像是可以不这么设计。
常见集群算法解析
Gossip 协议
点到点的一种通讯协议,各个节点平等。节点之间的地位是平等的。
因为节点是对等的
优点
所以可以任意的增加和修改;
所以任意节点宕机都不影响协议运行
可以向任意节点发送请求
缺点
节点通讯成本大,所以限制了集群的规模不可能超大
达成一致性的时间较长
消息有冗余
恶意节点会传播垃圾信息
几种模式
直邮模式
消息只会通知到邻居,邻居收到消息后就不再转发了。
【应用场景】社交网络;
反熵模式
定期随机选择某一个节点,全量交换数据进行同步,以此消除数据的不一致。
数据量交换比较大,可以使用校验和的形式来先确认两个几点是否需要交换数据的方法减少交换的需要。
【应用场景】节点数量不多,可以接受最终一致性的场景,例如存储系统多副本一致性。
谣言传播:收到更新消息后,自己成为“受感染节点”,周期性的传播更新消息,如果发现其它节点已经知道了消息,则按照一定概率将自己变为 removed,不再传播消息。
Redis 分片集群采用。传播信息少,达到一致性所需时间较少。
有一定的概率数据可能不一致。
【应用场景】节点经常变化的集群。
Bully 选举算法
当一个进程(应该是集群中的某个节点)发现协调者(或 Leader)不再响应请求时,就判定其出现故障,于是它就发起选举,选出新的协调者,即当前活动进程中进程号最大者(找最小的节点也可以,关键点在于“最")。
关键假设:
系统是同步的:所以可以发现 Leader 不再响应请求。
进程在任何时候都可能失败,包括算法在执行的过程中:一旦发现就发起选举,选举的完成时间是随机的,所以节点上运行的服务在开发时要可以使用这个特性。
进程失败后停止工作,重启后重新工作;有失败监控者,它可以发现失败的进程;
进程之间消息传递是可靠的:如果不可靠,那 Leader 不响应请求,有可能是因为网络原因?
每一个进程知道自己和其他每一个进程的 ID 以及地址:否则发起选举时,不知道向谁 6 发送 Election 消息。
Raft 选举算法
Raft is a consensus algorithm that is designed to be easy to understand. It's equivalent to Paxos in fault-tolerance and performance. The difference is that it's decomposed into relatively independent subproblems, and it cleanly addresses all major pieces needed for practical systems.
在模块七作业中记录了很多关于 Raft 的笔记,可以参考,此处不再重复。补充华仔课件中的几个关键图例。
leader 选举

日志复制

状态复制的两种方式:
State machine replication:复制状态机,复制的是命令而不是数据,典型代表:Raft。
Primary-backup system:主备复制,复制的是命令执行后的数据,典型代表:ZooKeeper 的 ZAB。
Raft 的实现

Raft vs ZooKeeper
如果你想内嵌分布式选举或者一致性功能,或者基于业务特性做一些小调整,选择 Raft,例如 MongoDB、etcd 等;
如果你想实现分布式选举或者一致性,但是不想自己去实现协议代码,选择 ZooKeeper,例如 HDFS、Cassandra 等;
如果你不确定,请选择 ZooKeeper。
思考题
为什么 Paxos 是最好分布式协同算法,但应用却不广?
因为算法太复杂了,太难以理解。
评论