1 - 分布式常用的设计模式

分布式系统的设计与实现过程中,常用到的一些协议和设计模式介绍,本专题将作为分布式的理论知识科普,为各位看官打开视野

主要内容将包含并不限于以下内容:

  • 如CAP、BASE,gossip协议
  • 如主从、切片、热备、Leader选举策略等
  • 如一致性hash、布隆过滤器、全局自增序列号、心跳机制等

1.1 - 理论基础篇

分布式系统相关的理论基础

1.1.1 - raft共识算法

Raft共识算法

推荐有兴趣的小伙伴,查看

为了解决paxos的复杂性,raft算法提供了一套更易理解的算法基础

角色划分:

  • Leader:领导者,接受客户端请求,并向Follower同步请求,当数据同步到大多数节点上后告诉Follower提交日志
  • Follow: 接受并持久化Leader同步的数据,在Leader告之日志可以提交之后,提交
  • Candidate:Leader选举过程中的临时角色,向其他节点拉选票,得到多数的晋升为leader,选举完成之后不存在这个角色

Raft角色

Follower只响应其他服务器的请求。如果Follower超时没有收到Leader的消息,它会成为一个Candidate并且开始一次Leader选举。收到大多数服务器投票的Candidate会成为新的Leader。Leader在宕机之前会一直保持Leader的状态。

选举

Raft算法将时间分为一个个的任期(term),每一个term的开始都是Leader选举。在成功选举Leader之后,Leader会在整个term内管理整个集群。如果Leader选举失败,该term就会因为没有Leader而结束。

Raft 使用心跳(heartbeat)来检测Leader是否存活,Leader向所有Followers周期性发送heartbeat,表示自己还活着

如果Follower在选举超时时间内没有收到Leader的heartbeat,就会等待一段随机的时间后发起一次Leader选举

Raft选举

选举的核心要点在于:

  • follower一段时间没有接受到leader的心跳,认为leader挂了,变成candidate状态。 为了避免选举冲突,这个超时时间是一个150/300ms之间的随机数
  • candidate,会重置计时器,先投自己一票,向其他节点拉选票
  • 得到多数选票的晋升为主节点
  • 当多个节点的选票相同,则选举失败;之后等待计时器超时的follower会变成candidate,将任期加一并开始新一轮的投票。

日志同步

Leader接受外部请求,并将请求作为LogEntries加入日志中,然后复制给其他的Follow节点,

  • 大部分结点响应时才提交日志
  • 通知所有follower结点日志已提交
  • 所有follower也提交日志

Raft日志同步

脑裂问题

指在一个高可用(HA)系统中,当联系着的两个节点断开联系时,本来为一个整体的系统,分裂为两个独立节点,这时两个节点开始争抢共享资源, 结果会导致系统混乱,数据损坏。

假设A~E五个结点,B是leader。 如果发生“脑裂”,A、B成为一个子分区,C、D、E成为一个子分区。

  • 此时C、D、E会发生选举,选出C作为新term的leader。这样我们在两个子分区内就有了不同term的两个leader
  • 这时如果有客户端写A时, 因为B无法复制日志到大部分follower所以日志处于uncommitted未提交状态。
  • 而同时另一个客户端对C的写操作却能够正确完成,因为C是新的leader,它只知道D和E。
  • 当网络通信恢复,B能够发送心跳给C、D、E了,却发现有新的leader了,因为C的term值更大,所以B自动降格为follower。 然后A和B都回滚未提交的日志,并从新leader那里复制最新的日志。

1.1.2 - zab协议

Zookeeper Atomic Broadcast, ZK原子广播协议

ZAB(Zookeeper Atomic Broadcast) 协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的一致性协议,基于该协议,ZooKeeper 实现了一种 主从模式的系统架构来保持集群中各个副本之间的数据一致性。

角色划分

  • Leader: 负责整个Zookeeper 集群工作机制中的核心
    • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
    • 集群内部各服务器的调度者
  • Follower:Leader的追随者
    • 处理客户端的非实物请求,转发事务请求给 Leader 服务器
    • 参与事务请求 Proposal 的投票
    • 参与 Leader 选举投票
  • Observer:是 zookeeper 自 3.3.0 开始引入的一个角色,
    • 它不参与事务请求 Proposal 的投票,
    • 也不参与 Leader 选举投票
    • 只提供非事务的服务(查询),通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力。

消息广播

ZAB消息广播

leader再接收到事务请求之后,将请求转换为事务Proposal提案,leader会为每个follower创建一个队列,将该事务proposal放入响应队列,保证事务的顺序性;

然后再在队列中按照顺序向其它节点广播该提案;

follower收到后会将其以事务的形式写入到本地日志中,并且向leader发送Ack信息确认

有一半以上的follower返回Ack信息时, leader会提交该提案并且向其它节点发送commit信息

事务有序性

队列 + 事务递增ID(ZXID)来保证提案的有序性,

ZXID:

  • 高32位:纪元epoch,新选举一个leader,纪元+1
  • 低32位:计数器counter,单调递增的数字

1.1.3 - 三阶段协议

在两阶段的基础上进行扩展,将第一阶段划分两部,cancommit + precommit,第三阶段则为 docommit

第一阶段 cancommit

该阶段协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的执行事务,这个过程是轻量的

第二阶段 precommit

本阶段协调者会根据第一阶段的询盘结果采取相应操作,询盘结果主要有 3 种:

第一阶段响应 步骤
所有的参与者都返回确定 1.协调者向所有的事务参与者发送事务执行通知
2.参与者收到通知后执行事务但不提交
3.参与者将事务执行情况返回给客户端
一个或多个参与者返回否定信息 无法执行,向各个参与者发出 abort 通知,请求退出预备状态
协调者等待超时 同上

3PC回滚

第三阶段 docommit

如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为 3 种情况:

第二阶段响应 步骤
所有的参与者都能正常执行事务 1.向所有参与者提交commit
2.所有参与者在收到通知之后执行 commit 操作释放资源
3.参与者向协调者反馈事务提交结果
一个或多个参与者执行事务失败 协调者认为事务无法成功执行
1.向所有参与者提交rollback
2.所有参与者执行rollback回滚
3.参与者向协调者反馈事务回滚结果
协调者等待超时 同上

事务提交流程图:

3PC提交

事务回滚流程图:

3PC回滚

在本阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的 commit 或 rollback 请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续 commit,相对于两阶段提交虽然降低了同步阻塞,但仍然无法完全避免数据的不一致

特点

  • 降低了阻塞与单点故障:
    • 参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若等待超时/协调者宕机,则自动 abort,降低了阻塞;
    • 参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若等待超时/协调者宕机,则自动 commit 事务,也降低了阻塞;
  • 数据不一致问题依然存在
    • 比如第三阶段协调者发出了 abort 请求,然后有些参与者没有收到 abort,那么就会自动 commit,造成数据不一致

1.2 - 基础架构

分布式系统常见的基础架构设计说明

1.3 - 分布式设计模式综述

天天说分布式分布式,那么我们是否知道什么是分布式,分布式会遇到什么问题,有哪些理论支撑,有哪些经典的应对方案,业界是如何设计并保证分布式系统的高可用呢?

1.架构设计

这一节将从一些经典的开源系统架构设计出发,来看一下,如何设计一个高质量的分布式系统;

而一般的设计出发点,无外乎

  • 冗余:简单理解为找个备胎,现任挂掉之后,备胎顶上
  • 拆分:不能让一个人承担所有的重任,拆分下,每个人负担一部分,压力均摊

1.1 主备架构

给现有的服务搭建一个备用的服务,两者功能完全一致,区别在于平时只有主应用对外提供服务能力;而备应用则只需要保证与主应用能力一致,随时待机即可,并不用对外提供服务;当主应用出现故障之后,将备应用切换为主应用,原主应用下线;迅速的主备切换可以有效的缩短故障时间

基于上面的描述,主备架构特点比较清晰

  • 采用冗余的方案,加一台备用服务
  • 缺点就是资源浪费

其次就是这个架构模型最需要考虑的则是如何实现主备切换?

  • 人工
  • VIP(虚拟ip) + keepalived 机制

1.2 主从架构

主从一般又叫做读写分离,主提供读写能力,而从则只提供读能力

鉴于当下的互联网应用,绝大多数都是读多写少的场景;读更容易成为性能瓶颈,所以采用读写分离,可以有效的提高整个集群的响应能力

主从架构可以区分为:一主多从 + 一主一从再多从,以mysql的主从架构模型为例进行说明

MySql主从

主从模式的主要特点在于

  • 添加从,源头依然是数据冗余的思想
  • 读写分离:主负责读写,从只负责读,可以视为负载均衡策略
  • 从需要向主同步数据,所若有的从都同步与主,对主的压力依然可能很大;所以就有了主从从的模式

关键问题则在于

  • 主从延迟
  • 主的写瓶颈
  • 主挂之后如何选主

1.3 多主多从架构

一主多从面临单主节点的瓶颈问题,那就考虑多主多从的策略,同样是主负责提供读写,从提供读;

但是这里有一个核心点在于多主之间的数据同步,如何保证数据的一致性是这个架构模型的重点

如MySql的双主双从可以说是一个典型的应用场景,在实际使用的时候除了上面的一致性之外,还需要考虑主键id冲突的问题

1.4 普通集群模式

无主节点,集群中所有的应用职能对等,没有主次之分(当下绝大多数的业务服务都属于这种),一个请求可以被集群中任意一个服务响应;

这种也可以叫做去中心化的设计模式,如redis的集群模式,eureka注册中心,以可用性为首要目标

对于普通集群模式而言,重点需要考虑的点在于

  • 资源竞争:如何确保一个资源在同一时刻只能被一个业务操作
    • 如现在同时来了申请退款和货物出库的请求,如果不对这个订单进行加锁,两个请求同时响应,将会导致发货又退款了,导致财货两失
  • 数据一致性:如何确保所有的实例数据都是一致的,或者最终是一致的
    • 如应用服务使用jvm缓存,那么如何确保所有实例的jvm缓存一致?
    • 如Eureka的分区导致不同的分区的注册信息表不一致

1.5 数据分片架构

这个分片模型的描述可能并不准确,大家看的时候重点理解一下这个思想

前面几个的架构中,采用的是数据冗余的方式,即所有的实例都有一个全量的数据,而这里的数据分片,则从数据拆分的思路来处理,将全量的数据,通过一定规则拆分到多个系统中,每个系统包含部分的数据,减小单个节点的压力,主要用于解决数据量大的场景

比如redis的集群方式,通过hash槽的方式进行分区

如es的索引分片存储

1.6 一灰灰的小结

这一节主要从架构设计层面对当前的分布式系统所采用的方案进行了一个简单的归类与小结,并不一定全面,欢迎各位大佬留言指正

基于冗余的思想:

  • 主备
  • 主从
  • 多主多从
  • 无中心集群

基于拆分的思想:

  • 数据分片

对于拆分这一块,我们常说的分库分表也体现的是这一思想

2.理论基础

这一小节将介绍分布式系统中的经典理论,如广为流程的CAP/BASE理论,一致性理论基础paxios,raft,信息交换的Gossip协议,两阶段、三阶段等

本节主要内容参考自

2.1 CAP定理

CAP 定理指出,分布式系统 不可能 同时提供下面三个要求:

  • Consistency:一致性
    • 操作更新完成并返回客户端之后,所有节点数据完全一致
  • Availability:可用性
    • 服务一直可用
  • Partition tolerance:分区容错性
    • 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性可用性的服务

通常来讲P很难不保证,当服务部署到多台实例上时,节点异常、网络故障属于常态,根据不同业务场景进行选择

对于服务有限的应用而言,首选AP,保证高可用,即使部分机器异常,也不会导致整个服务不可用;如绝大多数的前台应用都是这种

对于数据一致性要求高的场景,如涉及到钱的支付结算,CP可能更重要了

对于CAP的三种组合说明如下

选择 说明
CA 放弃分区容错性,加强一致性和可用性,其实就是传统的单机场景
AP 放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,例如很多NoSQL系统就是如此
CP 放弃可用性,追求一致性和分区容错性,基本不会选择,网络问题会直接让整个系统不可用

2.2 BASE理论

base理论作为cap的延伸,其核心特点在于放弃强一致性,追求最终一致性

  • Basically Available: 基本可用
    • 指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用
    • 如大促时降级策略
  • Soft State:软状态
    • 允许系统存在中间状态,而该中间状态不会影响系统整体可用性
    • MySql异步方式的主从同步,可能导致的主从数据不一致
  • Eventual Consistency:最终一致性
    • 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态

基于上面的描述,可以看到BASE理论适用于大型高可用可扩展的分布式系统

注意其不同于ACID的强一致性模型,而是通过牺牲强一致性 来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态

2.3 PACELEC 定理

这个真没听说过,以下内容来自:

  • 如果有一个分区(‘P’),分布式系统可以在可用性和一致性(即’A’和’C’)之间进行权衡;
  • 否则(‘E’),当系统在没有分区的情况下正常运行时,系统可以在延迟(‘L’)和一致性(‘C’)之间进行权衡。

定理(PAC)的第一部分与CAP定理相同,ELC是扩展。整个论点假设我们通过复制来保持高可用性。因此,当失败时,CAP定理占上风。但如果没有,我们仍然必须考虑复制系统的一致性和延迟之间的权衡。

2.4 Paxos共识算法

Paxos算法解决的问题是分布式共识性问题,即一个分布式系统中的各个进程如何就某个值(决议)通过共识达成一致

基于上面这个描述,可以看出它非常适用于选举;其工作流程

  • 一个或多个提议进程 (Proposer) 可以发起提案 (Proposal),
  • Paxos算法使所有提案中的某一个提案,在所有进程中达成一致。 系统中的多数派同时认可该提案,即达成了一致

角色划分:

  • Proposer: 提出提案Proposal,包含编号 + value
  • Acceptor: 参与决策,回应Proposers的提案;当一个提案,被半数以上的Acceptor接受,则该提案被批准
    • 每个acceptor只能批准一个提案
  • Learner: 不参与决策,获取最新的提案value

2.5 Raft算法

推荐有兴趣的小伙伴,查看

为了解决paxos的复杂性,raft算法提供了一套更易理解的算法基础,其核心流程在于:

leader接受请求,并转发给follow,当大部分follow响应之后,leader通知所有的follow提交请求、同时自己也提交请求并告诉调用方ok

角色划分:

  • Leader:领导者,接受客户端请求,并向Follower同步请求,当数据同步到大多数节点上后告诉Follower提交日志
  • Follow: 接受并持久化Leader同步的数据,在Leader告之日志可以提交之后,提交
  • Candidate:Leader选举过程中的临时角色,向其他节点拉选票,得到多数的晋升为leader,选举完成之后不存在这个角色

raft共识流程

2.6 ZAB协议

ZAB(Zookeeper Atomic Broadcast) 协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的一致性协议,基于该协议,ZooKeeper 实现了一种 主从模式的系统架构来保持集群中各个副本之间的数据一致性。

主要用于zk的数据一致性场景,其核心思想是Leader再接受到事务请求之后,通过给Follower,当半数以上的Follower返回ACK之后,Leader提交提案,并向Follower发送commit信息

角色划分

  • Leader: 负责整个Zookeeper 集群工作机制中的核心
    • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
    • 集群内部各服务器的调度者
  • Follower:Leader的追随者
    • 处理客户端的非实物请求,转发事务请求给 Leader 服务器
    • 参与事务请求 Proposal 的投票
    • 参与 Leader 选举投票
  • Observer:是 zookeeper 自 3.3.0 开始引入的一个角色,
    • 它不参与事务请求 Proposal 的投票,
    • 也不参与 Leader 选举投票
    • 只提供非事务的服务(查询),通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力。

ZAB消息广播

2.7 2PC协议

two-phase commit protocol,两阶段提交协议,主要是为了解决强一致性,中心化的强一致性协议

角色划分

  • 协调节点(coordinator):中心化
  • 参与者节点(partcipant):多个

执行流程

协调节点接收请求,然后向参与者节点提交 precommit,当所有的参与者都回复ok之后,协调节点再给所有的参与者节点提交commit,所有的都返回ok之后,才表明这个数据确认提交

当第一个阶段,有一个参与者失败,则所有的参与者节点都回滚

2pc流程

特点

优点在于实现简单

缺点也很明显

  • 协调节点的单点故障
  • 第一阶段全部ack正常,第二阶段存在部分参与者节点异常时,可能出现不一致问题

2.8 3PC协议

分布式事务:两阶段提交与三阶段提交 - SegmentFault 思否

在两阶段的基础上进行扩展,将第一阶段划分两部,cancommit + precommit,第三阶段则为 docommit

第一阶段 cancommit

该阶段协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的执行事务,这个过程是轻量的

第二阶段 precommit

本阶段协调者会根据第一阶段的询盘结果采取相应操作,若所有参与者都返回ok,则协调者向参与者提交事务执行(单不提交)通知;否则通知参与者abort回滚

第三阶段 docommit

如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,若所有参与者正常执行,则提交;否则协调者+参与者回滚

在本阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的 commit 或 rollback 请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续 commit,相对于两阶段提交虽然降低了同步阻塞,但仍然无法完全避免数据的不一致

特点

  • 降低了阻塞与单点故障:
    • 参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若等待超时/协调者宕机,则自动 abort,降低了阻塞;
    • 参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若等待超时/协调者宕机,则自动 commit 事务,也降低了阻塞;
  • 数据不一致问题依然存在
    • 比如第三阶段协调者发出了 abort 请求,然后有些参与者没有收到 abort,那么就会自动 commit,造成数据不一致

2.9 Gossip协议

Gossip 协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。Gossip 协议通过上面的特性,可以保证系统能在极端情况下(比如集群中只有一个节点在运行)也能运行

主要用在分布式数据库系统中各个副本节点同步数据之用,这种场景的一个最大特点就是组成的网络的节点都是对等节点,是非结构化网络

工作流程

  • 周期性的传播消息,通常周期时间为1s
  • 被感染的节点,随机选择n个相邻节点,传播消息
  • 每次传播消息都选择还没有发送过的节点进行传播
  • 收单消息的节点,不会传播给向它发送消息的节点

Gossip传播示意图

特点

  • 扩展性:允许节点动态增加、减少,新增的节点状态最终会与其他节点一致
  • 容错:网络中任意一个节点宕机重启都不会影响消息传播
  • 去中心化:不要求中心节点,所有节点对等,任何一个节点无需知道整个网络状况,只要网络连通,则一个节点的消息最终会散播到整个网络
  • 一致性收敛:协议中的消息会以一传十、十传百一样的指数级速度在网络中快速传播,因此系统状态的不一致可以在很快的时间内收敛到一致。消息传播速度达到了 logN
  • 简单:Gossip 协议的过程极其简单,实现起来几乎没有太多复杂性

缺点

  • 消息延迟:节点只会随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网的,因此使用 Gossip 协议会造成不可避免的消息延迟
  • 消息冗余:节点会定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤,导致消息的冗余

2.10 一灰灰的小结

本节主要介绍的是分布式系统设计中的一些常见的理论基石,如分布式中如何保障一致性,如何对一个提案达成共识

  • BASE,CAP,PACELEC理论:构建稳定的分布式系统应该考虑的方向
  • paxos,raft共识算法
  • zab一致性协议
  • gossip消息同步协议

3.算法

这一节将主要介绍下分布式系统中的经典的算法,比如常用于分区的一致性hash算法,适用于一致性的Quorum NWR算法,PBFT拜占庭容错算法,区块链中大量使用的工作量证明PoW算法等

3.1 一致性hash算法

一致性hash算法,主要应用于数据分片场景下,有效降低服务的新增、删除对数据复制的影响

通过对数据项的键进行哈希处理映射其在环上的位置,然后顺时针遍历环以查找位置大于该项位置的第一个节点,将每个由键标识的数据分配给hash环中的一个节点

一致性hash算法

一致散列的主要优点是增量稳定性; 节点添加删除,对整个集群而言,仅影响其直接邻居,其他节点不受影响。

注意:

  • redis集群实现了一套hash槽机制,其核心思想与一致性hash比较相似

3.2 Quorum NWR算法

用来保证数据冗余和最终一致性的投票算法,其主要数学思想来源于鸽巢原理

  • N 表示副本数,又叫做复制因子(Replication Factor)。也就是说,N 表示集群中同一份数据有多少个副本
  • W,又称写一致性级别(Write Consistency Level),表示成功完成 W 个副本更新写入,才会视为本次写操作成功
  • R 又称读一致性级别(Read Consistency Level),表示读取一个数据对象时需要读 R 个副本, 才会视为本次读操作成功

Quorum NWR算法要求每个数据拷贝对象 都可以投1票,而每一个操作的执行则需要获取最小的读票数,写票数;通常来讲写票数W一般需要超过N/2,即我们通常说的得到半数以上的票才表示数据写入成功

事实上当W=N、R=1时,即所谓的WARO(Write All Read One)。就是CAP理论中CP模型的场景

3.3 PBFT拜占庭算法

拜占庭算法主要针对的是分布式场景下无响应,或者响应不可信的情况下的容错问题,其核心分三段流程,如下

拜占庭算法

假设集群节点数为 N,f个故障节点(无响应)和f个问题节点(无响应或错误响应),f+1个正常节点,即 3f+1=n

  • 客户端向主节点发起请求,主节点接受请求之后,向其他节点广播 pre-prepare 消息
  • 节点接受pre-prepare消息之后,若同意请求,则向其他节点广播 prepare 消息;
  • 当一个节点接受到2f+1个prepare新消息,则进入commit阶段,并广播commit消息
  • 当收到 2f+1 个 commit 消息后(包括自己),代表大多数节点已经进入 commit 阶段,这一阶段已经达成共识,于是节点就会执行请求,写入数据

相比 Raft 算法完全不适应有人作恶的场景,PBFT 算法能容忍 (n 1)/3 个恶意节点 (也可以是故障节点)。另外,相比 PoW 算法,PBFT 的优点是不消耗算 力。PBFT 算法是O(n ^ 2) 的消息复杂度的算法,所以以及随着消息数 的增加,网络时延对系统运行的影响也会越大,这些都限制了运行 PBFT 算法的分布式系统 的规模,也决定了 PBFT 算法适用于中小型分布式系统

3.4 PoW算法

工作量证明 (Proof Of Work,简称 PoW),同样应用于分布式下的一致性场景,区别于前面的raft, pbft, paxos采用投票机制达成共识方案,pow采用工作量证明

客户端需要做一定难度的工作才能得出一个结果,验证方却很容易通过结果来检查出客户端是不是做了相应的工作,通过消耗一定工作浪,增加消息伪造的成本,PoW以区块链中广泛应用而广为人知,下面以区块链来简单说一下PoW的算法应用场景

以BTC的转账为例,A转n个btc给B,如何保证不会同时将这n个币转给C?

  • A转账给B,交易信息记录在一个区块1中
  • A转账给C,交易信息被记录在另一个区块2中
  • 当区块1被矿工成功提交到链上,并被大多数认可(通过校验区块链上的hash值验证是否准确,而这个hash值体现的是矿工的工作量),此时尚未提交的区块2则会被抛弃
  • 若区块1被提交,区块2也被提交,各自有部分人认可,就会导致分叉,区块链中采用的是优选最长的链作为主链,丢弃分叉的部分(这就属于区块链的知识点了,有兴趣的小伙伴可以扩展下相关知识点,这里就不展开了)

PoW的算法,主要应用在上面的区块提交验证,通过hash值计算来消耗算力,以此证明矿工确实有付出,得到多数认可的可以达成共识

3.5 一灰灰的小结

本节主要介绍了下当前分布式下常见的算法,

  • 分区的一致性hash算法: 基于hash环,减少节点动态增加减少对整个集群的影响;适用于数据分片的场景
  • 适用于一致性的Quorum NWR算法: 投票算法,定义如何就一个提案达成共识
  • PBFT拜占庭容错算法: 适用于集群中节点故障、或者不可信的场景
  • 区块链中大量使用的工作量证明PoW算法: 通过工作量证明,认可节点的提交

4.技术思想

这一节的内容相对前面几个而言,并不太容易进行清晰的分类;主要包含一些高质量的分布式系统的实践中,值得推荐的设计思想、技术细节

4.1 CQRS

Command Query Responsibility Segregation 即我们通俗理解的读写分离,其核心思想在于将两类不同操作进行分离,在独立的服务中实现

cqrs

用途在于将领域模型与查询功能进行分离,让一些复杂的查询摆脱领域模型的限制,以更为简单的 DTO 形式展现查询结果。同时分离了不同的数据存储结构,让开发者按照查询的功能与要求更加自由的选择数据存储引擎

4.2 复制负载平衡服务

复制负载平衡服务(Replication Load Balancing Service, RLBS),可以简单理解为我们常说的负载均衡,多个相同的服务实例构建一个集群,每个服务都可以响应请求,负载均衡器负责请求的分发到不同的实例上,常见的负载算法

算法 说明 特点
轮询 请求按照顺序依次分发给对应的服务器 优点简单,缺点在于未考虑不同服务器的实际性能情况
加权轮询 权重高的被分发更多的请求 优点:充分利用机器的性能
最少连接数 找连接数最少的服务器进行请求分发,若所有服务器相同的连接数,则找第一个选择的 目的是让优先让空闲的机器响应请求
少连接数慢启动时间 刚启动的服务器,在一个时间段内,连接数是有限制且缓慢增加 避免刚上线导致大量的请求分发过来而超载
加权最少连接 平衡服务性能 + 最少连接数
基于代理的自适应负载均衡 载主机包含一个自适用逻辑用来定时监测服务器状态和该服务器的权重
源地址哈希法 获取客户端的IP地址,通过哈希函映射到对应的服务器 相同的来源请求都转发到相同的服务器上
随机 随机算法选择一台服务器
固定权重 最高权重只有在其他服务器的权重值都很低时才使用。然而,如果最高权重的服务器下降,则下一个最高优先级的服务器将为客户端服务 每个真实服务器的权重需要基于服务器优先级来配置
加权响应 服务器响应越小其权重越高,通常是基于心跳来判断机器的快慢 心跳的响应并不一定非常准确反应服务情况

4.3 心跳机制

在分布式环境里中,如何判断一个服务是否存活,当下最常见的方案就是心跳

比如raft算法中的leader向所有的follow发送心跳,表示自己还健在,避免发生新的选举;

比如redis的哨兵机制,也是通过ping/pong的心跳来判断节点是否下线,是否需要选新的主节点;

再比如我们日常的业务应用得健康监测,判断服务是否正常

4.4 租约机制

租约就像一个锁,但即使客户端离开,它也能工作。客户端请求有限期限的租约,之后租约到期。如果客户端想要延长租约,它可以在租约到期之前续订租约。

租约主要是了避免一个资源长久被某个对象持有,一旦对方挂了且不会主动释放的问题;在实际的场景中,有两个典型的应用

case1 分布式锁

业务获取的分布式锁一般都有一个有效期,若有效期内没有主动释放,这个锁依然会被释放掉,其他业务也可以抢占到这把锁;因此对于持有锁的业务方而言,若发现在到期前,业务逻辑还没有处理完,则可以续约,让自己继续持有这把锁

典型的实现方式是redisson的看门狗机制

case2 raft算法的任期

在raft算法中,每个leader都有一个任期,任期过后会重新选举,而Leader为了避免重新选举,一般会定时发送心跳到Follower进行续约

4.5 Leader & Follow

这个比较好理解,上面很多系统都采用了这种方案,特别是在共识算法中,由领导者负责代表整个集群做出决策,并将决策传播到所有其他服务器

领导者选举在服务器启动时进行。每个服务器在启动时都会启动领导者选举,并尝试选举领导者。除非选出领导者,否则系统不接受任何客户端请求

4.6 Fencing

在领导者-追随者模式中,当领导者失败时,不可能确定领导者已停止工作,如慢速网络或网络分区可能会触发新的领导者选举,即使前一个领导者仍在运行并认为它仍然是活动的领导者

Fencint是指在以前处于活动状态的领导者周围设置围栏,使其无法访问集群资源,从而停止为任何读/写请求提供服务

  • 资源屏蔽:系统会阻止以前处于活动状态的领导者访问执行基本任务所需的资源。
  • 节点屏蔽:系统会阻止以前处于活动状态的领导者访问所有资源。执行此操作的常见方法是关闭节点电源或重置节点。

4.7 Quorum法定人数

法定人数,常见于选举、共识算法中,当超过Quorum的节点数确认之后,才表示这个提案通过(数据更新成功),通常这个法定人数为 = 半数节点 + 1

4.8 High-Water mark高水位线

高水位线,跟踪Leader(领导者)上的最后一个日志条目,且该条目已成功复制到>quorum(法定人数)的Follow(跟谁者),即表示这个日志被整个集群接受

日志中此条目的索引称为高水位线索引。领导者仅公开到高水位线索引的数据。

如Kafka:为了处理非可重复读取并确保数据一致性,Kafka broker会跟踪高水位线,这是特定分区的最大偏移量。使用者只能看到高水位线之前的消息。

4.9 Phi 累计故障检测

Phi Accrual Failure Detection,使用历史检测信号信息使阈值自适应

通用的应计故障检测器不会判断服务器是否处于活动状态,而是输出有关服务器的可疑级别。

如Cassandra(Facebook开源的分布式NoSql数据库)使用 Phi 应计故障检测器算法来确定群集中节点的状态

4.10 Write-ahead Log预写日志

预写日志记录是解决操作系统中文件系统不一致的问题的高级解决方案,当我们提交写到操作系统的文件缓存,此时业务会认为已经提交成功;但是在文件缓存与实际写盘之间会有一个时间差,若此时机器宕机,会导致缓存中的数据丢失,从而导致完整性缺失

为了解决这个问题,如mysql,es等都采用了预写日志的机制来避免这个问题

MySql:

  • 事务提交的流程中,先写redolog precommit, 然后写binlog,最后再redolog commit;当redolog记录成功之后,才表示事务执行成功;
  • 因此当出现上面的宕机恢复时,则会加载redologo,然后重放对应的命令,来恢复未持久化的数据

ElasticSearch:

  • 在内存中数据生成段写到操作系统文件缓存前,会先写事务日志,出现异常时,也是从事务日志进行恢复

4.11 分段日志

将日志拆分为多个较小的文件,而不是单个大文件,以便于操作。

单个日志文件在启动时读取时可能会增长并成为性能瓶颈。较旧的日志会定期清理,并且很难对单个大文件执行清理操作。

单个日志拆分为多个段。日志文件在指定的大小限制后滚动。使用日志分段,需要有一种将逻辑日志偏移量(或日志序列号)映射到日志段文件的简单方法。

这个其实也非常常见,比如我们实际业务应用配置的log,一般都是按天、固定大小进行拆分,并不会把所有的日志都放在一个日志文件中

再比如es的分段存储,一个段就是一个小的存储文件

4.12 checksum校验

在分布式系统中,在组件之间移动数据时,从节点获取的数据可能会损坏。

计算校验和并将其与数据一起存储。

要计算校验和,请使用 MD5、SHA-1、SHA-256 或 SHA-512 等加密哈希函数。哈希函数获取输入数据并生成固定长度的字符串(包含字母和数字);此字符串称为校验和。

当系统存储某些数据时,它会计算数据的校验和,并将校验和与数据一起存储。当客户端检索数据时,它会验证从服务器接收的数据是否与存储的校验和匹配。如果没有,则客户端可以选择从另一个副本检索该数据。

HDFS和Chubby将每个文件的校验和与数据一起存储。

4.13 一灰灰的小结

这一节很多内容来自下面这篇博文,推荐有兴趣的小伙伴查看原文

这一节主要简单的介绍了下分布式系统中应用到的一些技术方案,如有对其中某个技术有兴趣的小伙伴可以留言,后续会逐一进行补全

5.分布式系统解决方案

最后再介绍一些常见的分布式业务场景及对应的解决方案,比如全局唯一的递增ID-雪花算法,分布式系统的资源抢占-分布式锁,分布式事务-2pc/3pc/tcc ,分布式缓存等

5.1 缓存

缓存实际上并不是分布式独有的,这里把它加进来,主要是因为实在是应用得太广了,无论是应用服务、基础软件工具还是操作系统,大量都可以见到缓存的身影

缓存的核心思想在于: 借助更高效的IO方式,来替代代价昂贵的IO方式

如:

  • redis的性能高于mysql
  • 如内存的读写,远高于磁盘IO,文件IO
  • 磁盘顺序读写 > 随机读写

用好缓存可以有效提高应用性能,下面以一个普通的java前台应用为例说明

  • JVM缓存 -> 分布式缓存(redis/memcache) -> mysql缓存 -> 操作系统文件缓存 -> 磁盘文件

缓存面临的核心问题,则在于

  • 一致性问题:缓存与db的一致性如何保障(相信大家都听说过或者实际处理过这种问题)
  • 数据完整性:比如常见的先写缓存,异步刷新到磁盘,那么缓存到磁盘刷新这段时间内,若宕机导致数据丢失怎么办?
    • TIP: 上面这个问题可以参考mysql的redolog

5.2 全局唯一ID

在传统的单体架构中,业务id基本上是依赖于数据库的自增id来处理;当我们进入分布式场景时,如我们常说的分库分表时,就需要我们来考虑如何实现全局唯一的业务id了,避免出现在分表中出现冲突

全局唯一ID解决方案:

  • uuid
  • 数据库自增id表
  • redis原子自增命令
  • 雪花算法 (原生的,扩展的百度UidGenerator, 美团Leaf等)
  • Mist 薄雾算法

5.3 分布式锁

常用于分布式系统中资源控制,只有持有锁的才能继续操作,确保同一时刻只会有一个实例访问这个资源

常见的分布式锁有

5.4 分布式事务

事务表示一组操作,要么全部成功,要么全部不成功;单机事务通常说的是数据库的事务;而分布式事务,则可以简单理解为多个数据库的操作,要么同时成功,要么全部不成功

更确切一点的说法,分布式事务主要是要求事务的参与方,可能涉及到多个系统、多个数据资源,要求它们的操作要么都成功,要么都回滚;

一个简单的例子描述下分布式事务场景:

下单扣库存

  • 用户下单,付钱
  • 此时订单服务,会生成订单信息
  • 支付网关,会记录付款信息,成功or失败
  • 库存服务,扣减对应的库存

一个下单支付操作,涉及到三个系统,而分布式事务则是要求,若支付成功,则上面三个系统都应该更新成功;若有一个操作失败,如支付失败,则已经扣了库存的要回滚(还库存),生成的订单信息回滚(删掉–注:现实中并不会去删除订单信息,这里只是用于说明分布式事务,请勿带入实际的实现方案)

分布式事务实现方案:

  • 2PC: 前面说的两阶段提交,就是实现分布式事务的一个经典解决方案
  • 3PC: 三阶段提交
  • TCC:补偿事务,简单理解为应用层面的2PC
  • SAGA事务
  • 本地消息表
  • MQ事务方案

5.5 分布式任务

分布式任务相比于我们常说单机的定时任务而言,可以简单的理解为多台实例上的定时任务,从应用场景来说,可以区分两种

  • 互斥性的分布式任务
    • 即同一时刻,集群内只能有一个实例执行这个任务
  • 并存式的分布式任务
    • 同一时刻,所有的实例都可以执行这个任务
    • 续考虑如何避免多个任务操作相同的资源

分布式任务实现方案:

  • Quartz Cluster
  • XXL-Job
  • Elastic-Job
  • 自研:
    • 资源分片策略
    • 分布式锁控制的唯一任务执行策略

5.6 分布式Session

Session一般叫做会话,Session技术是http状态保持在服务端的解决方案,它是通过服务器来保持状态的。我们可以把客户端浏览器与服务器之间一系列交互的动作称为一个 Session。是服务器端为客户端所开辟的存储空间,在其中保存的信息就是用于保持状态。因此,session是解决http协议无状态问题的服务端解决方案,它能让客户端和服务端一系列交互动作变成一个完整的事务。

单机基于session/cookie来实现用户认证,那么在分布式系统的多实例之间,如何验证用户身份呢?这个就是我们说的分布式session

分布式session实现方案:

  • session stick:客户端每次请求都转发到同一台服务器(如基于ip的hash路由转发策略)
  • session复制: session生成之后,主动同步给其他服务器
  • session集中保存:用户信息统一存储,每次需要时统一从这里取(也就是常说的redis实现分布式session方案)
  • cookie: 使用客户端cookie存储session数据,每次请求时携带这个

5.7 分布式链路追踪

分布式链路追踪也可以叫做全链路追中,而它可以说是每个开发者的福音,通常指的是一次前端的请求,将这个请求过程中,所有涉及到的系统、链路都串联起来,可以清晰的知道这一次请求中,调用了哪些服务,有哪些IO交互,瓶颈点在哪里,什么地方抛出了异常

当前主流的全链路方案大多是基于google的Dapper 论文实现的

全链路实现方案

  • zipkin
  • pinpoint
  • SkyWalking
  • CAT
  • jaeger

5.8 布隆过滤器

Bloom过滤器是一种节省空间的概率数据结构,用于测试元素是否为某集合的成员。

布隆过滤器由一个长度为 m 比特的位数组(bit array)与 k 个哈希函数(hash function)组成的数据结构。

原理是当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点,把它们置为 1。

检索时,我们只要看看这些点是不是都是 1 就大约知道集合中有没有它了,也就是说,如果这些点有任何一个 0 ,则被检元素一定不在;如果都是 1 ,则被检元素很可能在。

关于布隆过滤器,请牢记一点

  • 判定命中的,不一定真的命中
  • 判定没有命中的,则一定不在里面

布隆过滤器

常见的应用场景,如

  • 防止缓存穿透
  • 爬虫时重复检测

5.9 一灰灰的小结

分布式系统的解决方案当然不局限于上面几种,比如分布式存储、分布式计算等也属于常见的场景,当然在我们实际的业务支持过程中,不太可能需要让我们自己来支撑这种大活;而上面提到的几个点,基本上或多或少会与我们日常工作相关,这里列出来当然是好为了后续的详情做铺垫

6.一灰灰的总结

6.1 综述

这是一篇概括性的综述类文章,可能并没有很多的干货,当然也限于“一灰灰”我个人的能力,上面的总结可能并不准确,如有发现,请不吝赐教

全文总结如下

常见的分布式架构设计方案:

  • 主备,主从,多主多从,普通无中心集群,数据分片架构

分布式系统中的理论基石:

  • CAP, BASE, PACELEC
  • 共识算法:paxos, raft, zab
  • 一致性协议:2pc, 3pc
  • 数据同步:gossip

分布式系统中的算法:

  • 分区的一致性hash算法: 基于hash环,减少节点动态增加减少对整个集群的影响;适用于数据分片的场景
  • 适用于一致性的Quorum NWR算法: 投票算法,定义如何就一个提案达成共识
  • PBFT拜占庭容错算法: 适用于集群中节点故障、或者不可信的场景
  • 区块链中大量使用的工作量证明PoW算法: 通过工作量证明,认可节点的提交

分布式系统解决方案:

  • 分布式缓存
  • 全局唯一ID
  • 分布式锁
  • 分布式事务
  • 分布式任务
  • 分布式会话
  • 分布式链路追踪
  • 布隆过滤器

6.2 题外话

最后总结一下这篇耗时两周写完的“心血巨作”(有点自吹了哈),准备这篇文章确实花了很大的精力,首先我个人对于分布式这块的理解并不能算深刻,其次分布式这块的理论+实践知识特别多,而且并不是特别容易上手理解,在输出这篇文章的同时,遇到一些疑问点我也会去查阅相关资料去确认,整个过程并不算特别顺利; 那么为什么还要去做这个事情呢?

  1. 咸鱼太久了,想做一些有意思的东西,活跃一下大脑
  2. 准备依托于《分布式专栏》来将自己的知识体系进行归纳汇总,让零散分布在大脑中的知识点能有一个脉络串联起来
  3. 不想做架构的码农不是好码农,而想成为一个好的架构,当然得做一些基础准备,向业务精品学习取经

1.4 - 分布式系统的8个谬误

你在分布式系统上工作吗?微服务,Web API,SOA,Web服务器,应用服务器,数据库服务器,缓存服务器,负载均衡器 - 如果这些描述了系统设计中的组件,那么答案是肯定的。分布式系统由许多计算机组成,这些计算机协调以实现共同的目标。

20多年前,Peter Deutsch和James Gosling定义了分布式计算的8个谬误。这些是许多开发人员对分布式系统做出的错误假设。从长远来看,这些通常被证明是错误的,导致难以修复错误。

8个谬误是:

  1. 网络可靠。
  2. 延迟为零。
  3. 带宽是无限的。
  4. 网络是安全的。
  5. 拓扑不会改变。
  6. 有一个管理员。
  7. 运输成本为零。
  8. 网络是同质的。

让我们来看看每个谬误,讨论问题和潜在的解决方案。

1.网络可靠

问题

通过网络呼叫将失败。

今天的大多数系统都会调用其他系统。您是否正在与第三方系统(支付网关,会计系统,CRM)集成?你在做网络服务电话吗?如果呼叫失败会发生什么?如果您要查询数据,则可以进行简单的重试。但是如果您发送命令会发生什么?我们举一个简单的例子:

var creditCardProcessor = new CreditCardPaymentService();
creditCardProcessor.Charge(chargeRequest);

如果我们收到HTTP超时异常会怎么样?如果服务器没有处理请求,那么我们可以重试。但是,如果它确实处理了请求,我们需要确保我们不会对客户进行双重收费。您可以通过使服务器具有幂等性来实现此目的。这意味着如果您使用相同的收费请求拨打10次,则客户只需支付一次费用。如果您没有正确处理这些错误,那么您的系统是不确定的。处理所有这些情况可能会非常复杂。

解决方案

因此,如果网络上的呼叫失败,我们能做什么?好吧,我们可以自动重试。排队系统非常擅长这一点。它们通常使用称为存储和转发的模式。它们在将消息转发给收件人之前在本地存储消息。如果收件人处于脱机状态,则排队系统将重试发送邮件。MSMQ是这种排队系统的一个例子。

但是这种变化将对您的系统设计产生重大影响。您正在从请求/响应模型转移到触发并忘记。由于您不再等待响应,因此您需要更改系统中的用户行程。您不能只使用队列发送替换每个Web服务调用。

结论

你可能会说网络现在更可靠 - 而且它们是。但事情发生了。硬件和软件可能会出现故障 - 电源,路由器,更新或补丁失败,无线信号弱,网络拥塞,啮齿动物或鲨鱼。是的,鲨鱼:在一系列鲨鱼叮咬之后,谷歌正在加强与Kevlar的海底数据线。

还有人为因素。人们可以开始DDOS攻击,也可以破坏物理设备。

这是否意味着您需要删除当前的技术堆栈并使用消息传递系统?并不是的!您需要权衡失败的风险与您需要进行的投资。您可以通过投资基础架构和软件来最小化失败的可能性。在许多情况下,失败是一种选择。但在设计分布式系统时,您确实需要考虑失败的问题。

2.延迟是零

问题

通过网络拨打电话不是即时的。

内存呼叫和互联网呼叫之间存在七个数量级的差异。您的应用程序应该是网络感知。这意味着您应该清楚地将本地呼叫与远程呼叫分开。让我们看看我在代码库中看到的一个例子:

var viewModel = new ViewModel();
var documents = new DocumentsCollection();
foreach (var document in documents)
{
	var snapshot = document.GetSnapshot();
	viewModel.Add(snapshot);
}

没有进一步检查,这看起来很好。但是,有两个远程呼叫。第2行进行一次调用以获取文档摘要列表。在第5行,还有另一个调用,它检索有关每个文档的更多信息。这是一个经典的Select n + 1问题。为了解决网络延迟问题,您应该在一次调用中返回所有必需的数据。一般的建议是本地调用可以细粒度,但远程调用应该更粗粒度。这就是为什么分布式对象和网络透明度的想法死了。但是,即使每个人都同意分布式对象是一个坏主意,有些人仍然认为延迟加载总是一个好主意:

var employee = EmployeeRepository.GetBy(someCriteria)
var department = employee.Department;
var manager = department.Manager;
foreach (var peer in manager.Employees;)
{
// do something
}

您不希望财产获取者进行网络呼叫。但是,每个。 在上面的代码中调用实际上可以触发数据库之旅。

解决方案

带回您可能需要的所有数据

如果您进行远程呼叫,请确保恢复可能需要的所有数据。网络通信不应该是唠叨的。

将Data Closer移动到客户端

另一种可能的解决方案是将数据移近客户端。如果您正在使用云,请根据客户的位置仔细选择可用区。缓存还可以帮助最小化网络呼叫的数量。对于静态内容,内容交付网络(CDN)是另一个不错的选择。

反转数据流

删除远程调用的另一个选项是反转数据流。我们可以使用Pub / Sub并在本地存储数据,而不是查询其他服务。这样,我们就可以在需要时获取数据。当然,这会带来一些复杂性,但它可能是工具箱中的一个很好的工具。

结论

虽然延迟可能不是LAN中的问题,但当您转移到WAN或Internet时,您会注意到延迟。这就是为什么将网络呼叫与内存中的呼叫明确分开是很重要的。在采用微服务架构模式时,您应该牢记这一点。您不应该只使用远程调用替换本地呼叫。这可能会使你的系统变成分布式的大泥球。

3.带宽是无限的

问题

带宽是有限的。

带宽是网络在一段时间内发送数据的容量。到目前为止,我还没有发现它是一个问题,但我可以看到为什么它在某些条件下可能是一个问题。虽然带宽随着时间的推移而有所改善,但我们发送的数据量也有所增加。与通过网络传递简单DTO的应用相比,视频流或VoIP需要更多带宽。带宽对于移动应用程序来说更为重要,因此开发人员在设计后端API时需要考虑它。

错误地使用ORM也会造成伤害。我见过开发人员在查询中过早调用.ToList()的示例,因此在内存中加载整个表。

解决方案

领域驱动的设计模式

那么我们怎样才能确保我们不会带来太多数据呢?域驱动设计模式可以帮助:

  • 首先,您不应该争取单一的企业级域模型。您应该将域划分为有界上下文。
  • 要避免有界上下文中的大型复杂对象图,可以使用聚合模式。聚合确保一致性并定义事务边界。

命令和查询责任隔离

我们有时会加载复杂的对象图,因为我们需要在屏幕上显示它的一部分。如果我们在很多地方这样做,我们最终会得到一个庞大而复杂的模型,对于写作和阅读来说都是次优的。另一种方法可以是使用命令和查询责任隔离 - CQRS。这意味着将域模型分为两部分:

  • 在写模式将确保不变保持真实的数据是一致的。由于写模型不关心视图问题,因此可以保持较小且集中。
  • 该读取模型是视图的担忧进行了优化,所以我们可以获取所有所需的特定视图中的数据(例如,我们的应用程序的屏幕)。

结论

在第二个谬误(延迟不是0)和第三个谬误(带宽是无限的)之间有延伸,您应该传输更多数据,以最大限度地减少网络往返次数。您应该传输较少的数据以最小化带宽使用。您需要平衡这两种力量,并找到通过线路发送的_正确_数据量。

虽然您可能不会经常遇到带宽限制,但考虑传输的数据非常重要。更少的数据更容易理解。数据越少意味着耦合越少。因此,只传输您可能需要的数据。

4.网络是安全的

问题

网络并不安全。

这是一个比其他人更多的媒体报道的假设。您的系统仅与最薄弱的链接一样安全。坏消息是分布式系统中有很多链接。您正在使用HTTPS,除非与不支持它的第三方遗留系统进行通信。您正在查看自己的代码,寻找安全问题,但正在使用可能存在风险的开源库。一个OpenSSL的漏洞允许人们通过盗取SSL / TLS保护的数据。Apache Struts中的一个错误允许攻击者在服务器上执行代码。即使你正在抵御所有这些,仍然存在人为因素。恶意DBA可能错放数据库备份。今天的攻击者掌握着大量的计算能力和耐心。所以问题不在于他们是否会攻击你的系统,而是什么时候。

解决方案

深度防御

您应该使用分层方法来保护您的系统。您需要在网络,基础架构和应用程序级别进行不同的安全检查。

安全心态

在设计系统时要牢记安全性。十大漏洞列表在过去5年中没有发生太大变化。您应遵循安全软件设计的最佳实践,并检查常见安全漏洞的代码。您应该定期搜索第三方库以查找新漏洞。常见漏洞和暴露列表可以提供帮助。

威胁建模

威胁建模是一种识别系统中可能存在的安全威胁的系统方法。首先确定系统中的所有资产(数据库中的用户数据,文件等)以及如何访问它们。之后,您可以识别可能的攻击并开始执行它们。我建议阅读高级API安全性的第2章,以便更好地概述威胁建模。

结论

唯一安全的系统是关闭电源的系统,不连接到任何网络(理想情况下是在一个有形模块中)。它是多么有用的系统!事实是,安全是艰难而昂贵的。分布式系统中有许多组件和链接,每个组件和链接都是恶意用户的可能目标。企业需要平衡攻击的风险和概率与实施预防机制的成本。

攻击者手上有很多耐心和计算能力。我们可以通过使用威胁建模来防止某些类型的攻击,但我们无法保证100%的安全性。因此,向业务部门明确表示这一点是个好主意,共同决定投资安全性的程度,并制定安全漏洞何时发生的计划。

5.拓扑不会改变

问题

网络拓扑不断变化。

网络拓扑始终在变化。有时它会因意外原因而发生变化 - 当您的应用服务器出现故障并需要更换时。很多时候它是故意的 - 在新服务器上添加新进程。如今,随着云和容器的增加,这一点更加明显。弹性扩展 - 根据工作负载添加或删除服务器的能力 - 需要一定程度的网络灵活性。

解决方案

摘要网络的物理结构

您需要做的第一件事是抽象网络的物理结构。有几种方法可以做到这一点:

  • 停止硬编码IP - 您应该更喜欢使用主机名。通过使用URI,我们依靠DNS将主机名解析为IP。
  • 当DNS不够时(例如,当您需要映射IP和端口时),则使用发现服务。
  • Service Bus框架还可以提供位置透明性。

无价值的,而非重要的

通过将您的服务器视为没有价值的,而不是很重要的,您确保没有服务器是不可替代的。这一点智慧可以帮助您进入正确的思维模式:任何服务器都可能出现故障(从而改变拓扑结构),因此您应该尽可能地自动化。

测试

最后一条建议是测试你的假设。停止服务或关闭服务器,看看您的系统是否仍在运行。像Netflix的Chaos Monkey这样的工具可以通过随机关闭生产环境中的VM或容器来实现这一目标。通过带来痛苦,您更有动力构建一个可以处理拓扑更改的更具弹性的系统。

结论

十年前,大多数拓扑结构并没有经常改变。但是当它发生时,它可能发生在生产中并引入了一些停机时间。如今,随着云和容器的增加,很难忽视这种谬误。你需要为失败做好准备并进行测试。不要等到它在生产中发生!

6.有一位管理员

问题

这个知道一切的并不存在。

嗯,这个看起来很明显。当然,没有一个人知道一切。这是一个问题吗?只要应用程序运行顺利,它就不是。但是,当出现问题时,您需要修复它。因为很多人触摸了应用程序,知道如何解决问题的人可能不在那里。

有很多事情可能会出错。一个例子是配置。今天的应用程序在多个商店中存储配置:配置文件,环境变量,数据库,命令行参数。没有人知道每个可能的配置值的影响是什么。

另一件可能出错的事情是系统升级。分布式应用程序有许多移动部件,您需要确保它们是同步的。例如,您需要确保当前版本的代码适用于当前版本的数据库。如今,人们关注DevOps和持续交付。但支持零停机部署并非易事。

但是,至少这些东西都在你的控制之下。许多应用程序与第三方系统交互。这意味着,如果它们失效,你可以做的事情就不多了。因此,即使您的系统有一名管理员,您仍然无法控制第三方系统。

解决方案

每个人都应对释放过程负责

这意味着从一开始就涉及Ops人员或系统管理员。理想情况下,他们将成为团队的一员。尽早让系统管理员了解您的进度可以帮助您发现限制因素。例如,生产环境可能具有与开发环境不同的配置,安全限制,防火墙规则或可用端口。

记录和监控

系统管理员应该拥有用于错误报告和管理问题的正确工具。你应该从一开始就考虑监控。分布式系统应具有集中式日志。访问十个不同服务器上的日志以调查问题是不可接受的方法。

解耦

您应该在系统升级期间争取最少的停机时间。这意味着您应该能够独立升级系统的不同部分。通过使组件向后兼容,您可以在不同时间更新服务器和客户端。

通过在组件之间放置队列,您可以暂时将它们分离。这意味着,例如,即使后端关闭,Web服务器仍然可以接受请求。

隔离第三方依赖关系

您应该以不同于您拥有的组件的方式处理控制之外的系统。这意味着使您的系统更能适应第三方故障。您可以通过引入抽象层来减少外部依赖的影响。这意味着当第三方系统出现故障时,您将找到更少的地方来查找错误。

结论

要解决这个谬论,您需要使系统易于管理。DevOps,日志记录和监控可以提供帮助。您还需要考虑系统的升级过程。如果升级需要数小时的停机时间,则无法部署每个sprint。没有一个管理员,所以每个人都应该对发布过程负责。

7.运输成本为零

问题

运输成本_不是_零。

这种谬论与第二个谬误有关,即 延迟为零。通过网络传输内容在时间和资源上都有代价。如果第二个谬误讨论了时间方面,那么谬误#7就会解决资源消耗问题。

这种谬论有两个不同的方面:

网络基础设施的成本

网络基础设施需要付出代价。服务器,SAN,网络交换机,负载平衡器以及负责此设备的人员 - 所有这些都需要花钱。如果您的系统是在内部部署的,那么您需要预先支付这个价格。如果您正在使用云,那么您只需为您使用的内容付费,但您仍然需要付费。

序列化/反序列化的成本

这种谬误的第二个方面是在传输级别和应用程序级别之间传输数据的成本。序列化和反序列化会消耗CPU时间,因此需要花钱。如果您的应用程序是内部部署的,那么如果您不主动监视资源消耗,则会隐藏此成本。但是,如果您的应用程序部署在云端,那么这笔费用就会非常明显,因为您需要为使用的内容付费。

解决方案

关于基础设施的成本,你无能为力。您只能确保尽可能高效地使用它。SOAP或XML比JSON更昂贵。JSON比像Google的Protocol Buffers这样的二进制协议更昂贵。根据系统的类型,这可能或多或少重要。例如,对于与视频流或VoIP有关的应用,传输成本更为重要。

结论

您应该注意运输成本以及应用程序正在执行的序列化和反序列化程度。这并不意味着您应该优化,除非需要它。您应该对资源消耗进行基准测试和监控,并确定运输成本是否对您有用。

8.网络是同质的

问题

网络_不是_同质的。

同质网络是使用类似配置和相同通信协议的计算机网络。拥有类似配置的计算机是一项艰巨的任务。例如,您几乎无法控制哪些移动设备可以连接到您的应用。这就是为什么重点关注标准协议。

解决方案

您应该选择标准格式以避免供应商锁定。这可能意味着XML,JSON或协议缓冲区。有很多选择可供选择。

结论

您需要确保系统的组件可以相互通信。使用专有协议会损害应用程序的互操作性。

设计分布式系统很难

这些谬论发表于20多年前。但他们今天仍然坚持,其中一些比其他人更多。我认为今天许多开发人员都知道它们,但我们编写的代码并没有显示出来。

我们必须接受这些事实:网络不可靠,不安全并且需要花钱。带宽有限。网络的拓扑结构将发生变化。其组件的配置方式不同。意识到这些限制将有助于我们设计更好的分布式系统。

参考文章

原文标题 《Understanding the 8 Fallacies of Distributed Systems》

作者:Victor Chircu

译者:February

译文: https://cloud.tencent.com/developer/article/1370391

1.5 - 什么是分布式系统

分布式的概念存在年头有点久了,在正式进入我们《分布式专栏》之前,感觉有必要来聊一聊,什么是分布式,分布式特点是什么,它又有哪些问题,在了解完这个概念之后,再去看它的架构设计,理论奠基可能帮助会更大

本文将作为专栏的第0篇,将从三个方面来讲述一下我理解的"分布式系统"

  • 分布式系统的特点
  • 分布式系统面临的挑战
  • 如何衡量一个分布式系统

1.分布式系统特点

什么是分布式系统,看一下wiki上的描述

1.1 定义

分布式系统(distributed system)是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件

分布式操作系统(Distributed operating system),是一种软件,它是许多独立的,网络连接的,通讯的,并且物理上分离的计算节点的集合[1]。每个节点包含全局总操作系统的一个特定的软件子集。每个软件子集是两个不同的服务置备的复合物[2]。第一个服务是一个普遍存在的最小的内核,或微内核,直接控制该节点的硬件。第二个服务是协调节点的独立的和协同的活动系统管理组件的更高级别的集合。这些组件抽象微内核功能,和支持用户应用程序[3]。

Distributed system: is a system in which components located on networked computers communicate and coordinate their actions by passing messages. The components interact with each other in order to achieve a common goal[3].

虽然上面几个描述不完全相同,但是含义其实也差不了太多;基于我个人的理解,用大白话来描述一下分布式系统,就是“一个系统的服务能力,由网络上多个节点共同提供”,正如其名的“分布一词”

在了解完分布式系统的概念之后,接下来抓住其主要特点,来加深这个分布式的理解

1.2 分布性

分布式系统分布在多个节点(可以理解为多个计算机),这些节点可以是网络上任意的一台计算机,即在空间上没有原则性的限制

1.3 对等性

分布式系统中有很多的节点,这些节点之间没有主从、优劣直说,它们应该是对等的,从服务能力来说,访问分布式系统中的任何一个节点,整个服务请求应该都是等价的

看到这里可能就会有一个疑问了,分布式系统中经典主从架构,数据拆分架构,就不满足这个对等特性了啊(这个问题先留着,后续再详情中进行解答)

1.4 自治性

分布式系统中的各个节点都有自己的计算能力,各自具有独立的处理数据的功能。通常,彼此在地位上是平等的,无主次之分,既能自治地进行工作,又能利用共享的通信线路来传送信息,协调任务处理。

1.5 并发性

分布式系统既然存在多个节点,那么天然就存在多个节点的同事响应请求的能力,即并发性支持,如何做好分布式系统的并发控制则是所有分布式系统需要解决的一个问题

2. 分布式系统面临的问题

当系统分布在多个节点之上时,自然而然就带来了很多单机场景下不会有问题,如经典的 分布式系统的8个谬误 | 一灰灰Learning

2.1 全局时钟

分布式系统的多个节点,如何保证每个节点的时钟一致?这个是需要重点考虑的问题

我们知道大名鼎鼎的分布式主键生成算法 “雪花算法” 就是利用了机器时钟来作为算法因子,如果一个系统的多个节点不能保证时钟统一,那这个算法的唯一性将无法得到保障

2.2 网络延迟、异常

网络是有开销的,多个节点之间的通信是有成本的,既然存在网络的开销、或异常状况,那么如何保证多个节点的数据一致性呢? 当无法保证数据的一致性时,如何提供分布式系统的对等性呢?

在经典的CAP理论中,对于P(网络分区)一般都是需要保障的,一个系统存在多个计算节点,那么网络问题将不可避免,网络分区必然会存在

2.3 数据一致性

如何保证所有节点中的数据完全一致是一个巨大的挑战,这个问题比较好理解,我们操作分布式系统中的一个节点实现数据修改,如果要保证数据一致性,则要求所有的节点,同步收到这个修改

但是我们需要注意的时,网络是不可靠的,且网络的传输是存在延迟的,如何衡量数据的一致性和服务的可用性则是我们在设计一个分布式系统中需要取舍的

2.4 节点异常

机器宕机属于不可抗力因素,如果分布式系统中的一个节点宕机了,整个系统会怎么样?要如何确保机器宕机也不会影响系统的可用性呢? 机器恢复之后,又应该如何保证数据的一致性呢? 又应该如何判断一个节点是否正常呢?

2.5 资源竞争

前面说到分布式系统天然支持并发,那么随之而来的问题则是如何资源竞争的问题;当一个资源同一时刻只允许一个实例访问时,怎么处理?多个系统同时访问一个资源是否会存在数据版本差异性(如经典的ABA问题)、数据一致性问题?

基于这个问题,分布式锁可以说是应运而生,相信各位开发大佬都不会陌生这个知识点

2.6 全局协调

这个协调怎么理解呢? 举几个简单的实例

  • 如何判断分布式系统中那些节点正常提供服务,那些节点故障
  • 如一个任务希望在分布式系统中只执行一次,那么应该哪个节点执行这个任务呢?
  • 如一组有先后顺序的请求发送给分布式系统,但是由于网络问题,可能出现后面的请求先被系统接收到,这种场景怎么处理呢?
  • 一个用户已经登录,如何在所有节点中确认他的身份呢?

2.7 一灰灰的小结

实际上分布式系统面临的挑战并不止于上面这些,一个具体的系统面临的问题可能各不相同,但总的来说,分布式系统的理论基础会给我们非常好的指引方向,这一节推荐查看 * 分布式设计模式综述 | 一灰灰Learning

3. 分布式系统的衡量指标

最后再来看一下如何衡量一个分布式系统的“好差”,它的指标有哪些

3.1 性能指标

常见的性能指标如rt, QPS, TPS来判断一个系统的承载能力,重点关注是哪个要点

  • 响应延迟
  • 并发能力
  • 事务处理能力

3.2 可用性

这个就是传说中你的系统达到几个9的那个指标,即系统的异常时间占总的可用时间的比例

统的可用性可以用系统停服务的时间与正常服务的时间的比例来衡量,也可以用某功能的失败次数与成功次数的比例来衡量。可用性是分布式的重要指标,衡量了系统的鲁棒性,是系统容错能力的体现。

3.3 扩展性

系统的可扩展性(scalability)指分布式系统通过扩展集群机器规模提高系统性能(吞吐、延迟、并发)、存储容量、计算能力的特性

最简单来讲,就是你的系统能不能直接加机器,来解决性能瓶颈,如果能加机器,有没有上限(如由于数据库的连接数限制了机器的数量上限, 如机器加到某个程度之后,服务能力没有明显提升)

3.4 一致性

分布式系统为了提高可用性,总是不可避免的使用副本的机制,从而引发副本一致性的问题。越是强的一致的性模型,对于用户使用来说使用起来越简单

4. 总结

这一篇文章相对来说比较干燥,全是文字描述,介绍下什么是分布式系统,分布系统的特点及面对的问题和衡量指标,提炼一下关键要素,如下

分布式系统的特点

  • 分布性
  • 对等性
  • 并发性
  • 自治性

分布式系统面临的挑战

  • 全局时钟
  • 网络延迟、异常
  • 数据一致性
  • 节点异常
  • 资源竞争
  • 全局协调

分布式系统衡量指标

  • 性能指标
  • 可用性
  • 扩展性
  • 一致性

最后强烈推荐阅读下面两个万字干货

2 - 分布式系统高可用详解

本专栏将主要讲解当前常用的各大系统的高可用方案

2.1 - 01.常见的高可用技术方案

大家好我是一灰灰,本文将接着前文 1w5字详细介绍分布式系统的那些技术方案 文章基础上,进行实际的案例解析

高可用对于当下的系统而言,可以说是一个硬指标,常年专注于业务开发的我们,对于高可用最直观的感觉可能就是祈祷应用不要出问题,不要报错;即便有问题,也最好不是我们的业务代码逻辑导致的,如果是服务器、DB、中间件(如注册中心、配置中心等)的异常那就抛给对应的sre, dba;然而常在河边走,哪有不湿鞋,为了保障服务的高可用,我们可以从哪些方面进行努力呢?

本文将作为高可用的开篇,通过简述一些常用的系统的高可用方案,给大家介绍一下我们可以从哪些方面努力让我们的系统达到高可用,主要涉及到的系统如下

  • 缓存:Redis
  • 数据库:MySql
  • 消息队列:RabbitMQ
  • 搜索: ElasticSearch

1 redis高可用策略

redis广泛应用于缓存的业务场景,当然也有将其当做持久化存储的nosql数据库使用,这些都不重要,重点是redis在提供服务的时候,是如何支持高可用的呢?

redis官方支持了四种策略:

  • 数据持久化
  • 主从同步
  • 哨兵模式
  • 集群

除以上姿势之外,我们自己在使用时还可以选择根据业务场景使用不同的redis实例(即传说中的不把所有鸡蛋放在一个篮子里)

接下来将针对redis的几种高可用策略进行简述说明

1.1 数据持久化

官方手册: Redis persistence

持久化是在高可用、一致性的场景中经常会看到的一种技术手段;

在高可用的场景中,数据的持久化主要是为了解决在服务出现问题(如宕机)之后,可以快速恢复并对外继续提供服务能力;

redis官方提供了两种持久化策略

  • AOF: 将更新的操作命令记录在对应的日志文件中,在重启的时候采用“回放”策略,将所有的命令重新执行一遍来实现场景恢复
  • RDB: 定时存储redis中的数据快照到数据文件中,在重启的时候,加载rdb文件,恢复所有的数据

简单来讲AOF记录的是操作动作,采用回放执行的机制进行恢复;RDB则相当于数据落盘,重新读取加载的机制进行恢复

注:AOF RDB可以一起工作,没有排他性

1.2 主从方式

虽然redis性能爆炸,但是单机依然存在性能瓶颈;当我们遇到单机的性能瓶颈的时候,一般怎么做?

没错,加机器

redis也支持多机服务,比如常见的一主多从策略:

  • 主机:提供读写能力
  • 从机:只提供读

针对绝大多数读多写少的场景,我们可以起多个redis实例,其中一个设置为主,提供所有的写请求;其他的实例则设置为从,客户端通过负载策略路由到不同的从redis,从而实现流量分摊;

同时也因为有多个实例,所以单台或几台实例下线,对整个服务的可用性影响并不会太大(及时摘除故障机器,其他的实例依然可以正常提供服务;当然前提是流量所示太大把其他的实例也打挂,那就gg了)

redis主从模式

主从模式还有一个变种,叫做从从模式,主要是为了解决主redis的同步压力,改成主 -> 从,然后由一个从同步给其他的从实例,具体架构图如下

redis主从从模式

使用主从、主从从模式实现高可用可算是分布式系统的经典策略,其主要思想在于:

  • 多实例提供服务,实现负载均衡
  • 每个实例冗余一份全量数据

1.3 哨兵模式

官方手册: High availability with Redis Sentinel

哨兵模式主要是为了解决主从模式中,主机宕机的场景,由于主机本身存在单点,所以主节点对成了高可用的关键因素了;那么如果实现主节点宕机之后,自动选择一个新的主节点,这样不就可以提高系统的可用性了么; redis官方提供的机制就是 - 哨兵模式

主要工作原理:

  • 哨兵:监听redis实例,判断是否存活(不太对外提供服务能力)
  • 通过 PING 命令,检查与主从服务器之间的连接情况,若正常相应,则认为存活;否则认为主观下线
  • n/2 + 1半数以上哨兵认为主节点下线,则认为主节点客观下线,尝试选新的主节点
  • 从所有从节点中,选择与之前主库相似度最高的从节点作为新的主库

哨兵模式

哨兵模式,可以理解为探活 + 选主,而这也常见于各大分布式系统的技术方案中

1.4 集群模式

官方手册: Scaling with Redis Cluster

相比于主从模式的全量冗余,redis的集群策略在在于数据分片,每个实例上存储部分的数据;而不是全量数据,从而解决数据量大的场景下,对于redis服务本身以及数据同步的压力

集群模式的特点在于多个实例,构建成一个实例,每个实例上存储部分的数据;redis并没有采用一致性hash来做数据分布,而是使用特有的slots插槽机制,来实现数据的hash映射

集群模式

集群模式,主要特点在于数据分片,每个实例存部分数据,其思路在于拆分

从上面的图中也可以看出,集群一般与主从搭配使用,集群中的每个分片对应的是主从模式的redis服务,从而加强高可用

1.5 小结

这一节主要介绍的是redis的高可用策略,从中也可以看到很多经典的技术方案

  • 持久化:RDB数据落盘加载方式 + AOF记录操作命令用于回放策略
  • 主从,主从从:全量数据冗余、读写请求分离,负载均衡的思想;核心问题在于主节点挂掉之后需要人工参与手动指定主库
  • 哨兵机制:PING/PONG的探活机制,监听主节点,宕机之后自动选主,确保高可用;核心问题在于所有的实例冗余相同的一份数据,数据量大时不友好
  • 集群:数据分片,每个实例提供部分服务能力

看到这里的小伙伴自然会想到,为什么redis会提供这些不同的策略?它们各自的应用场景是什么,优缺点是啥?这些疑问就放在后续的redis高可用详解中介绍

相关博文:

2 MySql高可用策略

MySql数据库的高可用策略就比较多了,同样也非常的经典;仅仅主节点的保活策略就非常多了;在这里将主要的重心放在MySql的高可用架构主备、主从、一主多从,多主多从上,至于主节点故障时转移策略则放在后续详细的文章中进行介绍

2.1 数据持久化

对于每个开发者而言,大多都听说过数据库的ACID特性,其中的D对应的就是这里说到的持久化;区别于redis的持久化,以MySql的InnoDB引擎为例,其持久化涉及到多个日志文件(undo log,redo log,binlog),缓存区(buffer),磁盘(idb文件)

接下来看一下完整的数据更新/插入的流程

mysql数据持久化流程图

接下来描述一下核心思想:

  • 数据更新策略:总是更新缓存的内容(缓存未命中,则从磁盘加载到缓存)
  • 先写undolog日志文件:记录之前的数据,支持mvcc、支持回滚就靠它
  • redolog记录的两阶段提交:(先是prepare,待binlog写完之后,再次更新状态为commit)
  • 最后异步刷新缓存数据到磁盘

虽然上面的描述比较简单,但是这里的知识点非常多,如

  • 为什么先更新缓存,最后异步刷磁盘?
    • 核心在于操作内存的速度 » 操作磁盘
  • undolog作用是什么,怎么支持mvcc,实现事务回滚的?
    • 保障事务原子性的关键所在,数据行非主键变更时,记录修改前的数据到undolog,并指向它,其他sql读这个undolog中的副本数据从而支持mvcc,回滚时则是根据undo log进行逻辑恢复
  • redolog作用是什么,为什么两阶段方案?
    • 主要保障事务的持久性,当数据库异常宕机之后,可以通过重新执行redo log来恢复未及时落盘的数据;两阶段的主要目的是为了解决redolog与binlog的一致性问题,避免出现redolog第一阶段成功,但是binlog失败导致不一致问题
    • redolog属于innodb引擎,固定大小,环形结构覆盖写策略;内部同样是先写缓存,再刷磁盘的策略

更多详情内容,后面到mysql的专题时再详细介绍

2.2 主备架构

保证高可用的一个最简单策略就是“冗余”,也就是我们这里说到的主备架构,对mysql而言,就是我启动两个实例;一个主库对外提供读写服务,一个备库,冗余主库的所有数据内容,并不对外提供服务;

当主库gg之后,然后备库升级,切换为主库

话说这个思想和古代的储君制非常像了,平时都是皇帝总领朝堂,太子就当吉祥物;皇帝驾崩之后,太子就晋升为皇帝(论备胎的重要性)

MySql主备

主备的最大特点就是多备一台实例,在出问题时顶上,当然缺点就很明显了,严重的资源浪费

2.3 主从架构

主从和前面mysql的思路差不多,主从模式一般又叫做读写分离,即写主库,读从库;相比于主备而言,最主要的突破点在于另外一个mysql实例不会干放着,而是尤其来承担读请求

MySql主从

主从的核心思想在于读写分离

2.4 一主多从

在前面主从的基础上多挂几个从库,主要出发点在于当前的互联网场景下,绝大多数的应用都是读多写少,通过挂多个从库,可以有效提供整体服务的性能指标

同样一主多从的模式,也会区分为主从 + 主从从两种,后者则主要是为了减少主库的同步压力,下图为核心4架构模型

MySql主从

2.5 多主多从

一主多从可以解决读多写少的场景,但总会出现写瓶颈的场景;在不考虑分库分表的业务手段之前(这种方式也可以理解为数据分片,类似上面说到的redis集群模式),仅仅从mysql的架构模式出发,自然会想到的策略就是多个主库提供写能力,这就是我们说的多主多从的架构了

MySql主从

多主多从,其中每个主库都可以独立对外提供写请求;从库则对外提供读请求

需要注意的是主库之间的数据同步,即一个写请求落到一个任意一个主库之后,所有的主库都会同步这个写操作

2.6 主库切换策略、主从同步策略

前面介绍的是几种不同的主从架构特点,主要通过主、备/从来新增实例来提高可用性;但是还有两个非常重要的点没有细说,一个是故障之后,如何确定新的主库;另外一个则是主从/主主之间的数据如何同步,如何保证数据的一致性;

接下来我们将简单的介绍下mysql中常见的一些做法(更详细的当然留在后面的专题)

主库切换策略

VIP + KeepAlived

  • vip: 即virtual ip虚拟ip
  • KeepAlived: 保活脚本

其主要思路在于外部通过VIP访问mysql实例(主从/主主),而KeepAlived用于检测主库是否存活,当挂掉之后,VIP偏移到另外一个主库(或者选一个从库作为主库)上,从而实现自动的切主流程

缺点:

  • 级联复制(主->从->从这种复制模式叫做级联复制)或者一主多从在切换之后,其他从实例需要重新配置连接新主

MHA

Master High Avaliable 主库高可用机制,也是当下很多公司采用的策略;其包含一套完整的工具,在检测到主库不可用后,会自动将同步到最接近主库的slave提升为master,然后将其他的slave指向新的master

其优点非常明显,通常可以实现十秒内的主从切换,扩展MySql节点也非常方便;而缺点则在于主要监控主库

MXC

PXC(Percona XtraDB Cluster)是一个完全开源的 MySQL 高可用解决方案。它将 Percona Server、Percona XtraBackup 与 Galera 库集成在一起,以实现多主复制的 MySQL 集群

其核心特点在于写请求会自动同步到其他节点,要求在所有的节点都验证之后才会提交,保证数据的强一致性

因此缺点就在于木桶效应,性能取决于最差的那个节点

MGR/InnoDB Cluste

MySQL 5.7 推出了 MGR(MySQL Group Replication),与 PXC 类似,也实现了多节点数据写入和强一致性的特点。MGR 采用 GCS(Group Communication System)协议同步数据,GCS 可保证消息的原子性

外部连接通过 MySql router与一组mysql实例进行交互,当主库切换时,mysql router会自动切换到新的主节点

Xenon

给予Raft协议的MySql高可用和复制性管理工具,无中心化选主,支持秒级切换

主从同步策略

当存在主从库时,必然会存在同步问题,如何保障主库与从库数据的一致性呢?

主从同步流程

主从同步主要借助Binlog来实现,这个在前面的图中有简单的体现,下面则是相对完整的同步流程

MySql主从数据同步

  • 主库生成binlog日志文件
    • statement:记录具体引起改动的操作语句,比如insert xxxxx,缺点是某些函数会导致数据不一致(如now())
    • row:基于数据行的,原来数据行是xx值改为了yy 值,缺点是数据量大
    • mixed: 上面两个混用
  • 从库的io线程拉主库的binlog日志,写入自己的relaylog(中继日志),然后由sql线程读取relaylog日志进行回放,实现数据同步

主从同步策略

使用主从之后,在实际的业务开发中,最最常遇到的问题就是主从延迟,即主库数据已经写入了,但是读从库却读不到对应的数据,这个就是主从延迟了,它直接导致数据的不一致;当然一般这种影响还好,但是如果因为主从延迟,现在主库挂了,所有的从库都没有最新的记录,这不就导致数据丢失了么,会导致严重的数据一致性问题

所以在主从同步的策略上,有下面几种

case1:异步复制

主库完成写请求之后,理解返回结果,并不关心从库是否同步接收处理,此时就可能出现上面说的,主库挂了之后,所有从库还存在未同步的数据,导致数据丢失

case2:半同步复制

为了避免出现上面的问题,我们要求最少有一个从库同步完之后,才响应用户端请求,这样表明主库宕机之后还有个兜底的

case3:全同步复制

这个更激进一点,要求所有的从库都同步完,才算真正的ok,保证强一致性,缺点则在于性能会受到影响

2.7 小结

这一小节主要介绍的是MySql的高可用策略,从架构方面出发,有主备,主从,一主多从,多主多从,同时也简单的介绍了下如何实现主库的自动切换(MHA,MXC,MGR等)、主从数据同步流程,同步策略;如果想了解更详细的内容,请移步到mysql的高可用专题

下面小结一下保持高可用的主要思路

  • 通过冗余来实现高可用:如主备
  • 读写分离,实现负载均衡:主从、主从从模式
  • 数据持久化策略:操作内存(buffer),异步刷盘,两阶段提交保障一致性

相关博文:

3. RabbitMq高可用方案

消息中间件也是大家或多或少会接触的一类系统,接下来将以RabbitMq来看一下它的高可用是如何实现的

3.1 数据持久化

不同于前面MySql必然会持久化,RabbitMq的数据持久化是可选的,当我们对数据的完整性要求高时,最好开启持久化

首先简单看一下rabbitmq的模型

RabbitMq架构

我们这里说的持久化主要指

  • exchange持久化: 即exchange本身不会因为rabbitmq宕机而被删除,需要手动指定durable=true
  • topic持久化:消费者通过topic从exchange中读取消息,需要指定durable=true,避免出现宕机后队列中的消息丢失
  • msg消息持久化:即生产者投递到echange的消息,需要持久化到磁盘

注意rabbitmq的消息持久化也是先写到buffer,然后再定时刷新到磁盘;

当我们为了保障数据的完整性时,一般会开启消息的确认机制/事务机制,每次投递等到mq回复一个确认ack之后,才表示真正的投递成功,而mq的应答则是在消息的持久化之后进行

3.2 主备模式

同前面的MySql的主备,主节点提供读写,备节点同步主节点的数据,不对外提供服务能力;当主节点挂了之后,启用备节点对外服务,原主节点恢复之后则作为备节点存在

3.3 Shovel远程模式

官方文档: * Shovel Plugin — RabbitMQ

远程模式可以实现双活的一种模式,简称 shovel 模式,所谓的 shovel 就是把消息进行不同数据中心的复制工作,可以跨地域的让两个 MQ 集群互联,远距离通信和复制。

  • Shovel 就是我们可以把消息进行数据中心的复制工作,我们可以跨地域的让两个 MQ 集群互联。

RabbitMq远程模式

如上图,有两个异地的 MQ 集群(可以是更多的集群),当用户在地区 1 这里下单了,系统发消息到 1 区的 MQ 服务器,发现 MQ 服务已超过设定的阈值,负载过高,这条消息就会被转到 地区 2 的 MQ 服务器上,由 2 区的去执行后面的业务逻辑,相当于分摊我们的服务压力。

3.4 镜像模式

如下图,用 KeepAlived 做了 HA-Proxy 的高可用,然后有 3 个节点的 MQ 服务,消息发送到主节点上,主节点通过 mirror 队列把数据同步到其他的 MQ 节点,这样来实现其高可靠

RabbitMq镜像模式

镜像模式的主要特点在于每个mq实例都包含一份完整的数据镜像,内部有一个master选举算法,通过VIP对外提供连接

  • consumer,任意连接一个节点,若连上的不是master,请求会转发给master,为了保证消息的可靠性,consumer回复ack给master后,master删除消息并广播所有的slaver去删除。
  • publisher ,任意连接一个节点,若连上的不是master,则转发给master,由master存储并转发给其他的slaver存储。 如果master挂掉,则从slaver中选择消息队列最长的为master,

3.5 普通集群模式

exchange,buindling再所有的节点上都会保存一份,但是queue只会存储在其中的一个节点上,但是所有的节点都会存储一份queue的meta信息

如果生产者连接的是另外一个节点,将会把消息转发到存储该队列的节点上。如果消费者连接了非存储队列的节点取数据,则从存储消息的节点拉取数据。

其核心特点在于:

  • 数据拆分存储,若纯消息的节点挂了,则只能等待它恢复之后才能正常工作

3.6 多活模式

这个模式我的理解也不够深刻,以下内容来自于网上摘录,待后面到rabbitmq专题之后调研后进一步阐述

rabbitMQ 部署架构采用双中心模式(多中心),那么在两套(或多套)数据中心各部署一套 rabbitMQ 集群,各中心的rabbitMQ 服务除了需要为业务提供正常的消息服务外,中心之间还需要实现部分队列消息共享

RabbitMq镜像模式

federation 插件是一个不需要构建 cluster ,而在 brokers 之间传输消息的高性能插件,federation 插件可以在 brokers 或者 cluster 之间传输消息,连接的双方可以使用不同的 users 和 virtual hosts,双方也可以使用不同版本的 rabbitMQ 和 erlang。

federation 插件使用 AMQP 协议通信,可以接受不连续的传输。federation 不是建立在集群上的,而是建立在单个节点上的,如图上黄色的 rabbit node 3 可以与绿色的 node1、node2、node3 中的任意一个利用 federation 插件进行数据同步。

3.7 小结

rabbitmq的高可用机制的方案也比较好理解

  • 主备模式
  • 镜像模式:全量冗余一份数据,主对外提供服务,可以实现自动切主
  • 普通集群模式:数据拆分到集群的实例中,consumer/publisher连接到实例之后,会从具体持有exchange/topic的实例上拉数据
  • 远程模式:适用于多中心的场景,将消息转发给其他中心的实例

这里采用的高可用思路也无外乎常见的几种:持久化 + 数据冗余 + 拆分

相关博文:

4. ElasticSearch高可用方案

接下来我们再看一下现在非常流行的分布式搜索引擎ElasticSearch是如何保证高可用的

4.1 集群

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎

by 官网描述

对于es而言,通常都是集群方式对外提供服务,每启动一个实例叫做一个节点(Node),每个节点会定义一个节点名(Node Name),集群名(Cluster Name),相同集群名的节点会构建为一个集群;

ES集群

上图包含了es集群的核心要素:

  • 每个节点包含集群名 + 节点名两个属性,相同集群名的节点挂在一个集群内
  • 节点启动之后,开始PING其他节点(连接上后会得到对应节点所在集群的所有信息)
  • 节点发现主要靠Zen Discover来实现,选举也是靠它来实现

选举主要流程如下

ES选举

  • 选举同样也是依赖Zen Discover来实现
  • 每个节点上报自己任务的主节点,然后票数最多的就是主节点;票数相同的情况下,根据ID排序,选第一个

上面就是es集群的构建与主节点的选举过程;es支持任意节点数目的集群(1- N),无法完全依赖投票的机制来选主,而是通过一个规则。

只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。

但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题。

大多数解决方案就是设置一个 Quorum 值,要求可用节点必须大于 Quorum(一般是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个 Quorum 的配置就是 discovery.zen.minimum_master_nodes,当候选主节点的个数超过这个参数值时,开始选举,选主完成之后对外提供服务

ES作为分布式、近实时搜索系统,天然支持集群的服务能力,通过Zen Discover来实现节点通信、集群管理、选主

4.2 脑裂问题

上面提到了脑裂,接下来简单看一下ES是如何解决脑裂问题的

脑裂:由于网络或者集群健康监测问题,导致整个集群出现多个master节点,这种现象就是脑裂

es对节点进行了角色划分

  • 数据节点:负责数据的存储和相关的操作(CURD,聚合)等,因此对机器性能要求较高
  • 候选主节点:拥有选举权和被选举权,主节点在候选主节点中评选出来,负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给哪些的节点、追踪集群中节点的状态等

一个节点,可以即是数据节点,又是候选主节点,但是注意它们两者的定位,主节点对机器性能要求没有数据节点高,当一台机器既是数据节点又是主节点时,可能出现长耗时、耗资源的请求导致主节点服务异常;

通常更推荐的方案是使用性能低一点的作为候选主节点,性能高的作为数据节点

ES角色划分

接下来看下脑裂出现的情况

  • 网络问题,导致分区:即部分节点连接不到主节点,认为它挂了,然后选举出现的主节点
  • 主节点负载、响应延迟:主节点由于负载过高、或者响应超时,导致重新选举新的主节点

解决方案:

  • 适当调大ping timeout响应时间,避免因为网络、主节点性能问题导致的选举
  • 设置最少选举节点数大于候选主节点的半数,这样只要有半数以上的候选节点存活,则可以选举出一个主节点;而当可用节点数小于半数时,不参与选举,集群无法使用,也不会出现状态异常的情况
  • 角色分离:数据节点 + 候选主节点不放在一台机器上;

在有主节点的系统中,一般都需要考虑脑裂问题,常见的策略无非是:

  • 半数节点以上的投票才算有效
  • es额外提供了节点的角色定位,数据节点和候选主节点,其中只有候选主节点才有选举权和被选举权,提供一种角色分离的可选方案,来避免主节点被其他数据服务影响

4.3 数据分片

当数据量过大时,es支持自动拆分,将一个索引的上数据水平拆分到不同的数据块–分片(Shards),为了提供可用性,每个索引在定义时除了分片之外,还会定义副本数量,这里的副本可以理解为数据冗余,其中副本和分片必然不在一个节点上,在主节点异常时,副本可以提供数据查询能力

es默认在创建索引时,分片数为5,每个分片对应一个副本

ES分片

ES通过分片,将索引数据水平拆分,分片数越多,每个分片上的数据量就越少;而副本则是对应的每个分片的冗余,可以理解为主备,副本越多,消耗则越大

两点小说明

  • 对应副本的概念,上面的分片也叫做主分片
  • 当一个数据写入/更新到分片时,只有所有的副本都更新完毕之后,才算完成(可以MySql的全同步)

4.4 数据持久化

最后再说一下es的持久化机制,与前面先说持久化不同,es这里则需要先了解上面的基本流程,索引数据需要保存到主分片上,最终落盘,接下来看一下完整的流程

主分片数据更新流程

ES数据更新流程

简述一下上面的流程

  • 首先请求随机连一个es节点(这个节点叫做协调节点),然后通过路由算法,确定数据对应的主分片
  • 写数据到主分片,然后同步到副本(多个副本时采用并发同步,乐观锁控制)
  • 所有副本同步完成之后,主分片节点告诉协调节点最终结果,然后协调节点告诉调用者响应

当数据写入到主分片上之后,接下来再看一下这个数据时如何刷新到磁盘上的

分段存储

索引文档以段的形式存储磁盘,即一个索引文件会划分为很多个子文件,这里的子文件就是段

每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改;段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件

段的特性,有下面几个有点

  • 分段存储,可以有效避免读写时加锁的问题
  • 不变性,数据只读可以高效缓存,无需考虑更新
  • 一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索

由于段不可变,所以在更新时需要额外处理

  • 新增:当前文档新增一个段
  • 删除:新增一个.del文件,记录被删除的文档信息;被标记删除的文档仍然可以被检索到,只是最终返回时被移除
  • 更新:删除文件中标记旧的文档删除,插入新的段

延迟写

ES并不会实时将内存中的数据写入段,而是采用延迟写的策略(类似前面的写buffer,然后异步定时刷盘)

es先将内存数据,写入文件缓存系统(操作系统内存),

ES文档写入流程

上图来自 * 两万字教程,带你遨游ElasticSearch

注意几个事项

  • 写入文件缓存系统,之后异步落盘,可能导致丢数据,es采用事务日志的方式来处理恢复策略(即mysql的先写日志,崩溃之后做回放恢复)
  • es对外服务时,检索文件缓存系统 + 段中的文档,而内存中的数据不会被检索到(所以所es是近实时搜索引擎,因为最新写入的数据还在内存中,没有提交,立马查就查不到)
  • 为了避免段过多,es会定时做合并,将很多小的段合并成大的段(合并过程中会自动移除被标记删除的文档)

最后小结一下es的持久化

  • 索引分段存储,段生成checkpoint之后,则只读,因此可以全量缓存,不用考虑更新修改
  • 延迟写策略:先更新内存数据,异步提交文件缓存系统,最后再由操作系统刷盘
  • 内存中的数据不能被检索;文件缓存 + 段中的数据提供查询聚合,最终的结果会过滤已标记删除的文档

参考博文

4.5 小结

这一小节主要介绍的是ES的高可用机制,包括ES的集群工作原理,选举策略;采用数据分片支持大数据场景的支持,借助副本来提高可用性;

ES原生支持集群

  • 角色划分:候选主节点 + 数据节点
  • 数据节点:负责数据的存储和相关的操作(CURD,聚合)等,因此对机器性能要求较高
  • 候选主节点:拥有选举权和被选举权,主节点在候选主节点中评选出来,负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给哪些的节点、追踪集群中节点的状态等

ES数据持久化策略

  • 索引分段存储,段生成checkpoint之后,则只读,因此可以全量缓存,不用考虑更新修改;当出现修改时,标记原来段中文档删除,在新的段写入数据
  • 延迟写策略:先更新内存数据,异步提交文件缓存系统,最后再由操作系统刷盘
  • 内存中的数据不能被检索;文件缓存 + 段中的数据提供查询聚合,最终的结果会过滤已标记删除的文档

5.一灰灰的总结

5.1 综述

本片文章主要是分析当下不同应用场景下的几个主流系统的高可用策略,来看一下如何来保障的系统的高可用

常见的高可用思路

  • 冗余 (如数据副本、主备服务等)
  • 拆分 (数据拆分、服务能力拆分等)
  • 持久化

redis

  • 持久化:RDB数据落盘加载方式 + AOF记录操作命令用于回放策略
  • 主从,主从从:全量数据冗余、读写请求分离,负载均衡的思想;核心问题在于主节点挂掉之后需要人工参与手动指定主库
  • 哨兵机制:PING/PONG的探活机制,监听主节点,宕机之后自动选主,确保高可用;核心问题在于所有的实例冗余相同的一份数据,数据量大时不友好
  • 集群:数据分片,每个实例提供部分服务能力

mysql

  • 通过冗余来实现高可用:如主备
  • 读写分离,实现负载均衡:主从、主从从模式
  • 数据持久化策略:操作内存(buffer),异步刷盘,两阶段提交保障一致性

rabbitmq

  • 主备模式
  • 镜像模式:全量冗余一份数据,主对外提供服务,可以实现自动切主
  • 普通集群模式:数据拆分到集群的实例中,consumer/publisher连接到实例之后,会从具体持有exchange/topic的实例上拉数据
  • 远程模式:适用于多中心的场景,将消息转发给其他中心的实例

ElasticSearch

  • ES集群:数据节点 + 候选主节点
  • ES持久化:
    • 延迟写策略,先更新内存,然后提交操作系统缓存,最后异步刷新到磁盘;
    • 索引分段存储:段生成checkpoint之后,则只读,因此可以全量缓存,不用考虑更新修改;当出现修改时,标记原来段中文档删除,在新的段写入数据

5.2 主题无关

在准备写本文时,原计划针对不同业务场景各挑一个经典的系统来分析下各自的高可用方案,实际写下来发现工作量有点大;就把最后的一个分布式文件系统hdfs给暂缓了(对于大多数业务开发而言,接触的机会也不会太多),这个会放在《分布式系统-案例剖析》中进行介绍

最近会花大量的时间精力,准备做一个高质量的《分布式专栏》,欢迎有兴趣收藏关注 一灰灰的主站

2.2 - 02.Redis、ES、Hbase的高可用方案

以下内容来自同事的内部分享,经得同意分享给各位小伙伴

我们常说的高可用是怎么实现的呢?单机向集群的演进中遵循哪些原则,注意哪些事项呢?集群如何协同工作?集群之间的一致性如何保障?

纯干货,推荐看到的小伙伴仔细认证的阅读一下,相信会有不少的收获

一.nosql发展历史

1.关系型数据库

上世纪60年代以来至今,传统的关系型数据库一直被互联网应用的作为首选数据存储系统,典型的代表产品包括有oracle、mysql等。

关系型数据库的核心优势在于:第一,具备事务属性,注重数据一致性,内部实现有复杂的锁机制等还包含有其它一系列机制来保障数据一致性,能够基于AID(原子性、隔离性和持久性)的基础能力而带来事务强一致性来保证我们的数据存、取安全;第二,关系型数据模式其支持的二维表格模式比较契合现实中大部分的业务场景且易于理解,因此得以快速应用和发展。

关系型数据库最大的缺陷在于扩展性不足,在面对大量用户的并发访问以及海量存储的存取场景下,往往很难平滑的去做到性能升级,而使得DB经常作为整个系统应用的发展瓶颈。通常情况下,关系型数据库的扩展思路分为以下两种:

(1)纵向扩展。纵向扩展即提升单机硬件基础设施来提升处理能力。这种方式下虽然可以换来一定的性能提升,但是单机终归是存在有性能上限的,且升级过程中往往需要停机处理而无法做到平滑升级。其整体收益成本比比较低。

(2)横向扩展。横向扩展即通过分片,将数据分散至多台物理节点,降低单点压力,来提升处理能力。这种方式通常是对上层应用抽象出一个逻辑数据库,背后则是将数据分散到不同的物理数据库上。整体上来说,这种方式虽然可以在大部分常规场景下带来较大的性能提升,但与此同时又会引入另一个新的问题分布式事务问题,当然还包括有跨库join、非路由键查询等其它一系列问题。

2.NoSql的诞生

在随着互联网业务与场景不断发展的背景下,由于在应对海量存储数据时传统的关系型数据在扩展能力上的不足,以及出现了越来越多的场景在关系型模式下显得并不适用,典型的如OLAP数据分析类型场景。促使Nosql技术开始诞生,Nosql的核心思想在于放弃传统关系型数据库的事务强一致性与关系模式,以此换取更高的可扩展性以及面对高并发海量数据时具备更强的处理能力。

对于Nosql而言,其定位并不是取代关系型数据库,而是作为关系型数据库的一种补充,两者分别有各自适合的领域场景。典型的nosql产品包括:基于kv的redis、列存储的hbase、文档型数据库ES等。

3.NewSql

nosql虽然具备高扩展性的优势但其实在放弃了传统关系型数据库的强事务一致性的代价下换来的。因此,在关系型数据库与nosql均存在明显局限的背景下,NewSql概念开始应运而生。NewSQL可以说是传统的RDBMS与NoSQL技术结合之下的产物。这些系统既拥有NoSQL数据库的高扩展性,又希望能保持传统数据库的事务特性。典型的产品代表如google的spanner和国内的TiDB。

二.常见的Nosql产品简介

常见的nosql产品如下:

产品名称 特点 适用场景
redis 1.k-v结构; 2.内存数据库; 3.高性能,单机2C4G下读可达10WQPS; 1.缓存; 2.分布式锁、延时队列、限流等;
ES 1.文档型数据库; 2.结构化查询。支持多字段查询,以及复杂的过滤和聚合统计功能; 3.近实时查询。默认1s refresh一次将内存中的数据固化生成一个新的segment,此时为该segment创建倒排索引,外部读请求才能访问到这个segment的内容; 1.大数量背景下的检索类场景。例如日志搜索、大宽表解决mysql跨库join问题以及作为辅助索引解决分表下的非路由键查询问题; 2.数据统计、分析; 3.全文检索;
Hbase 1.列存储; 2.采用块存储机制,底层数据结构采用的是LSM合并树,将随机IO写转变为一次性顺序写,相比于B+树在写性能上表现更加优秀。但读性能会更弱; 1.PB级数据存储规模; 2.适合写多读少的场景,例如下沉的冷数据存储; 3.OLAP数据分析类场景;

三. 集群工作原理

3.1 集群模式

对于大规模数据存储系统都会面临一个问题就是如何进行横向扩展,当数据集越来越大时,一主多从的模式无法支持这么大的数据存储与访问量,此时一般情况下就会考虑进行横向扩展,将多个主从模式组合在一起对外提供服务。但是这里有两个首要问题就是如何实现数据的分片逻辑以及分片逻辑放在哪里。于是在这种背景下就会衍生出两种不同的集群模式,一种就是集中式模式,一种则是去中心化的模式。

1.集中式

集中式集群模式下,通常会引入一个中心节点作为集群的管理者,由管理者来进行集群状态管理、故障处理以及元数据维护等,其它节点只需响应数据请求,而无需知道集群中其它节点的情况。典型的解决方案都会借助于zookeeper分布式协调服务来进行集群管理,比如Hbase、kafka等。

Zookeeper:维护集群中的服务状态,并提供服务故障通知;

master:存储和维护集群元数据,以及故障转移等集群事务处理;

2.去中心化

即P2P交互模式,客户端与集群节点直接进行交互,而非之前业界的Proxy方式。典型的集群代表如redis cluster、es集群。

redis Cluster介绍

redis3.0版本开始,官方正式支持集群模式。redis官方集群模式最大的两个特点在于:

(1)去中心化。即P2P交互模式,客户端与集群节点直接进行交互,而非业界之前的Proxy方式。

(2)内部自治。redis 集群模式并未像Hbase、Kafka等引入第三方组件比如ZK,来实现对集群的节点状态管理、故障转移以及元数据管理等,而是基于Gssiop协议实现集群内节点监控、状态同步,并内置选举算法实现故障自动转移,在集群内部高度自治。

如下图是一个三主三从的redis cluster,三个机房部署,其中一主一从构成一个分片,之间通过异步复制同步数据。节点之间基于ping-pong心跳机制相互通信感知对方状态,一旦某个机房掉线,则分片上位于另一个机房的slave会被提升为master继续对外提供服务。

3.2 数据分片

分布式集群在进行横向扩展时,首要问题就是如何实现数据的分片逻辑。

1.分片策略

常见分片策略如下:

(1)hash分片。hash分片也是我们最常用的分片策略。例如ES默认采用的就是这种方式。hash分片的好处在于数据会被打的比较分散,其次不用额外存储映射关系,客户端与服务端以约定好的hash公式进行路由。但是它的问题在于如果一旦需要进行扩缩容,那么整个映射关系都会被打破,此时需要进行一次全量的rehash数据迁移,工作量非常大。所以一般情况下,在设计的时候会尽可能的让这个hash模值大一点,避免频繁的进行扩容。

(2)基于某一key值的范围划分。例如基于时间范围或者id范围分片。这种分片方式的优劣势其实与hash的方式是相反的。它的好处在于,当需要进行扩缩容时,不会像hash一样破坏掉全局的映射关系,只需要对部分分片的映射关系产生影响。但是这种方式的问题在于它会存在一定的热点数据问题,导致整个集群各个节点的负载不均衡。例如Habse采用的就是这种方式,HBase 表根据 RowKey 的开始和结束范围水平拆分为多个 Region,一个region就是分片。每个 Region 都包含了 StartKey 和 EndKey 之间的所有行。每个 Region 都会分配到集群的一个节点上,即 RegionServer,由它们提供读写服务。

(3)一致性hash。一致性hash是通过构建一个环形的hash空间,对于用户的请求,先经过hash映射到这个环上,这就是第一层的映射关系,只要这个hash的模值不变,这层关系就不会变。其次,顺着环的顺时针方向找到的第一个节点,就是负责该请求对应的节点。

一致性hash的优势在于当进行扩缩容时,不会破坏全局的映射关系,而导致整个rehash,发起全局的数据迁移,而只会影响局部数据的映射关系。比如缩容减少一个节点,因为第一层映射依然保持不变,原来的请求该分配到哪个节点还是在哪个节点上,只是改变了第二层从环上到节点之间的一个局部映射关系。从环上来看,只会影响这个节点的上一个节点到这个这个节点的这一段弧区间上,整个环上的其它区间由于第一层关系不变,其映射关系不会受到影响。原来去掉的这个节点之间负责的那一段弧上请求,会全部顺移到它的下一个节点,我们只需要把去掉节点负责的数据迁移到下一个节点即可,其它的所有节点不用做任何变更。

2.基于Hash槽的数据分片

redis cluster中,数据分片借助与hash槽slot来实现,集群预先划分16384个slot,对于每个请求集群的键值对,根据key 按CRC hash算法散列生成的值唯一匹配一个slot。在redis集群中每个master节点分片负责其中一部分槽位的读写请求,而且当且仅当每个slot都有对应节点负责时,集群才会进入可用状态。当动态扩缩容时,需求将16384个slot做一次再分配,相应数据也要进行迁移。

redis hash槽的算法与一致性hash算法的本质思想是一样的,通过不直接建立请求到节点的映射关系,而是建立一种间接的映射关系。避免在发生扩缩容时对于传统hash算法而言因为模值的变化而打乱整个映射关系。如下图所示,将映射关系分为两层,hash槽通过槽位路由表作为中间映射,因为槽位数量是16384不会变,这样当发生扩缩容时,对于请求而言该映射到哪个槽位还是映射到哪个槽位,即Part1映射不变,只用针对Part2部分中需要迁移的slot产生影响,而并非会让全部请求受到影响;

3.3 客户端交互流程

Redis集群交互

redis客户端与集群之间的交互是基于槽位映射表来进行的,该映射表类似于集群的数据分布图,其中维护着槽位与负责该槽位的节点地址信息,客户端根据该映射关系与节点进行直连交互。

redis客户端首次连接集群时,会从集群中拉取一份完整的槽位映射表,缓存在本地。在进行请求访问时,首先会采用CRC16冗余校验法的值对16384取模,映射到具体一个槽位,随之通过查询槽位映射表定位到具体负责该槽位的节点,进而直接与节点进行通信。对于服务端节点来说在收到请求后首先会判断该槽位是否是自己负责的槽位,如果是,则会响应客户端请求。如果不是,例如集群发生扩缩容,此时槽位发生迁移,则会返回Moved/ask指令,引导客户端重定向至正确的节点进行访问。

Moved指令:当迁移已经全部完成,此时该slot已经永久转交给另一个节点时,A节点会返回Moved指令。当Client收到Moved指令后,则会重定向至正确的节点再次进行访问,同时更新本地的槽位映射表,下次直接访问到正确的节点。

ASK指令:ASK指令主要是在迁移过程中,此时该slot的数据可能一部分位于B,而另一部分key可能还在源节点A上。此时对于读请求而言,源节点A在收到请求后,会先在自己的数据库中查找,如果存在则直接返回结果;如果不存在则说明可能已经迁移至B,则会返回ask错误指令,引导client转向目的地节点查询key;

当Client收到Moved/AKS指令后,会去重定向至新的节点访问,同时还会更新本地的槽位映射表,在下次访问时直接定位至正确的节点上;

ES集群交互过程

es作为搜索引擎而言,其支持的查询条件不局限于路由key,还包括其它关键字作为条件进行查询,因此其在查询流程不太一样。

es默认的查询模式为query then fetch模式,此模式下整个查询分为query 和 fetch两个步骤,query步骤负责查询符合条件文档id以及汇总排序截取limit等,fetch阶段则是查询完整数据,查询过程中需要进行两次交互。

(1)client首先会将查询请求发送至任一协调节点;

(2)协调节点在收到请求后,会并发的将请求发送至所有的数据节点;

(3)数据节点在收到请求后根据查询条件在自己负责的分片上查询符合条件的文档集合,不过只取文档 id和排名相关的字段信息,并将数据集返回至协调节点;

(4)协调节点在收到数据节点返回的结果集后,进行汇总排序取limit等,随着得到需要返回的结果集docId集合;

(5)此时query阶段结束,进入fetch阶段,协调节点会根据hash算法对docId进行路由,得到对应结果分别在哪些分片节点后,再次发送请求至数据节点,fetch数据;

(6)数据节点根据docId查询完整结果数据,并将数据再次返回至协调节点;

(7)协调结果进行完数据汇总后,将数据返回至客户端;

除了query then fetch之外,es还有另外一种比较常见的查询模式:query and fetch**。**此模式下向索引的所有分片 ( shard)都发出查询请求, 各分片执行完query 后再执行fetch,即在分片节点中做完查询、排序和截取后将完整的数据一并返回至协调节点。这种搜索方式是最快的。 因为这种查询方法只需要去 shard查询一次。 但是各个 shard 返回的结果的数量之和可能是用户要求的 size 的 n 倍。

Hbase集群交互过程

Hbase集群与redis集群不一样,其基于ZK进行集群状态管理以及元数据维护,集群中数据节点只知道自己负责的数据分片而不知其他节点。因此,在客户端进行集群访问时,通常需要先于ZK进行一次访问,在获取路由表后,再与集群节点直连访问。kafka也是同理。

如下图所示,HBase集群中的读取流程大致如下所示:

(1)client首先会访问一次zk,查询集群中master节点;

(2)在查询到master地址信息后,Client第二次发起请求访问master,查询路由信息表,路由表中记录着每个region节点负责处理哪个范围的rowkey;

(3)client在查询到路由信息后,会将其缓存在本地,随之基于路由信息表,查询rowkey对应的节点地址信息;

(4)直连数据节点服务器,发送查询请求获取数据;

(5)节点服务器在收到请求后,查询对应的完整数据并将结果返回至客户端;

   

3.4 集群管理

1.集群元数据管理

在集中式集群中,通常情况下会直接基于第三方协调服务zk来管理和维护集群元数据,zk在作为分布式协调服务之外,本身也是一个内存数据库。不过通常为减轻zk压力以及降低对zk的依赖,因此一般情况下,集群还会基于zk选举出一个master节点,代理zk进行元数据管理和维护以及非master节点的故障转移等相关事务处理。同时,zk中也会备份一份集群的元数据信息,避免master故障后集群元数据丢失,当选举出来的新master,会从zk中拉取一份集群元数据继续进行维护。

在去中心化的集群中,例如redis集群下每个节点都存储有整个集群的元数据信息,包括自己以及其它节点的存活状态、负责的slot槽位信息等。各节点间基于 Gossip 协议来相互交换信息,Gossip协议又叫病毒协议,是基于流行病传播的方式在节点或者进程之间信息交换的协议,在P2P去中心化的分布式系统中应用比较广泛。

Gossip协议的特点在于:

1.去中心化。Gossip 协议不要求任何中心节点,所有节点都可以是对等的,任何一个节点无需知道整个网络状况,只要网络是连通的,就可以把消息散播到全集群。

2.最终一致性。数据的传播过程是由一传十十传百逐步流散开来,整个传播过程需要经历多个周期,可能需要一定的时间,不过在一个处于有界网络的集群里,理论上集群各个节点对该份信息的认知最终都将会收敛一致。

Redis Cluster 中的每个节点都维护一份自己视角下的当前整个集群的状态,主要包括:

a.集群中各节点所负责的slots信息;

b.集群中各节点的存活状态信息;

对于集群中每个节点而言,会按照一定的频率周期,从自己的节点列表中随机挑选部分最长时间没有与它进行过通信的节点,对这些节点发送ping消息,并附加上自己视角下的集群状态信息,节点在收到其他节点发送的ping消息后再回复一个pong,以交换彼此的状态信息,对于差异化数据则版本决定是否更新本地状态数据,最终集群内所有节点达成统一认知。

优点:

(1)容错。Gossip 协议具有天然的分布式系统容错特性,集群中任何节点的状态发生变化,例如上下线都不会影响 Gossip 消息的传播,且当节点重新上线后,依然会接收集群内其他节点的状态数据,并最终与其他节点达成一致。

缺点:

(1)Gossip是最终一致性,当集群状态发生变更时,变更数据需要经过多伦同步,整个集群的节点才会达成一致,相比于ZK而言其感知会出现明显延迟;

(2)Gossip协议下,每个节点按自己的节奏频率周期性的发送消息,而由于同步全量状态信息使得Gossip包体积较大,会存在一定的网络压力。其次由于随机的发送消息,而收到消息的节点也会重复该步骤,不可避免的引起同一节点消息多次接收,增加消息处理压力。

2.集群状态检测

对于集中式集群模式的Hbase、kafka来说,对于集群的状态检测也是基于ZooKeeper 来做的,每台节点机器在启动时,都需要事先在zookeeper中注册一个节点,zk会与该节点维持一个会话关系,基于心跳检测来感知节点的状态变化。

具体来说,客户端会周期性的向服务端发送PING请求来保持心跳,一旦客户端发生故障,超过限定时间后,Zookeeper服务器会判定会话超时,并基于Watch机制实时通知给Master节点,master进行元数据更新以及后续的故障转移,以此来完成对集群中节点的状态检测。

跟大多数分布式系统一样,Redis cluster也是基于heart beat来进行节点状态检测。redis内部节点基于Gossip协议通信交互,具体来说,每个节点会定期会与其它节点发送ping-pong消息进行交互,以此来感知对方是否状态发生变化。对于集群中每个节点而言,每次随机挑选5个最长时间没有与它进行过通信的节点,对这些节点发送ping消息,节点在收到其他节点发送的ping消息后再回复一个pong。每个节点根据自己是否收到pong消息的结果来感知其它节点的存活状态。

节点上线

Redis Cluster 加入新节点时,首先需要在客户端需要执行 CLUSTER MEET 命令,命令中需要指定新增节点的地址信息。

redis集群中任一节点在收到 MEET命令后,会根据据 MEET 命令中的 IP 地址和端口号,向新节点发送一条 MEET 消息。

接着,新节点在收到Meet消息后,会向节点一返回一条PONG消息。

节点一接收到新节点返回的PONG消息后,得知新节点已经成功的接收了自己发送的MEET消息。随着将该新节点加入自己的元数据信息库中,从而完成了新节点接入的握手操作。

Meet成功之后,节点一会在下次周期性信息交互过程中,将新节点加入的消息传递出去。因为节点之间基于Gossip协议进行工作,在随着时间的推移,最终集群的所有节点都感知它的存在。

节点下线

redis集群中节点会周期性心跳同步,当某一节点在发其ping请求后,发现某个节点超过一定未给出回复,那么它会把这个节点的状态标记为pfail预下线的状态。

节点一会在下一轮交互中,会将节点二疑似下线消息同步出去。对于节点三在同步到这条消息后,并不会直接把自己的节点列表中该故障节点的状态也标记为预下线,因为这时候可能只是该节点一个人的主观认为下线,只是先记录下来节点一在XX时间认为节点二疑似下线;同时在节点三的下一轮ping-pong中,会优先选择节点二进行交互;

随着时间的推移,经过多轮同步后,对于节点X也超时未收到节点二的PONG,也认为节点二疑似下线,此时节点X发现集群中大部分超过一半的节点都认为它下线时,节点X会把该节点二标记为fail下线状态,并同时在集群中广播该节点fail。所有收到该消息的节点在发现某节点已经被标记为fail状态时,都会更新自己的节点列表将它标记为下线状态,如果该节点是leader副本的节点,则其对应的slave节点在收到下线消息会开始进行选举,进入故障转移流程。

3.5 高可用性

对于分布式存储系统而言,集群高可用保证在于解决两个前提,第一个是要保证数据的可靠性,即当节点机器出现故障时,数据不能因此出现丢失。第二,在故障发生后集群需具备自动故障转移机制。

1.数据的可靠性保证

通常而言,数据的可靠性都是基于多副本机制来解决的,即构建主从模式,为每个主节点部署多个slave从节点,当主节点故障时由从节点顶替。

对于多副本机制而言,其核心问题在于如何解决多副本之间的一致性。在多副本数据一致性问题上,一般会有两种解决方案。一种是基于ACK应答机制下的主从复制机制;另一种是目前业界更为主流的方案,基于分布式共识算法Paxos或者Raft来解决多副本之间的一致性问题。

主从复制+ACK机制

基于ACK的应答机制十分常见,首先从同步方式上来说又分为推模式和拉模式,拉模式相对而言十分常见,例如mysql的主从复制就是拉模式。两种模式的比较如下:

模式 代表产品 优劣势
mysql、es\kafka等 优势: 从节点可基于自身消费能力处理同步数据; 劣势: 数据同步及时性相对差一点
redis 优势: 数据同步相对更加及时; 劣势: 从节点一旦同步过程中出现重启,则重新启动后需要再次完整的同步一次全量数据。因此,在这种模式下一般还需要配备相关的缓冲区机制。例如redis中会配置同步缓冲区,在commit之前同时会先在缓冲区中备份一份。从节点重启后同步位移还在缓冲区中,则从缓冲区增量同步进行对齐。

从应答机制的角度上来说又分为异步复制、半同步复制与全同步复制。

(1)异步复制。主节点在收到写请求后,会将数据写入内存以及同步至中继日志后,进行commit提交。随后通知slave节点过来复制,slave是否成功复制主节点并不关心。对于异步复制而言,它是存在有数据丢失风险的,当master宕机时,从节点可能还没来得及复制数据。

(2)半同步复制。半同步复制每次都会至少有一个从节点ack应答,相对而言它可以有更强的一个数据一致性保证。但还是会存在不一致的问题的场景,比如脑裂问题,导致数据丢失。当发生网络分区时,master节点和一个从节点被划分到一个区域与其它的从节点分离,这时其它从节点发现与master失联后就会选出一个新的master来提供服务,但是原来的master并不知道自己被失联了,而且每次依然会有一个从节点给它ack应答,因此它也可以正常处理客户端请求,这个时候就会存在两个master同时对外提供服务,接收客户端的写请求,而当网络分区结束后旧的master发现有新的master了,就会向新的master看齐,丢弃掉脑裂期间客户端提交的数据了。

(3)全同步复制。全同步复制则是必须每个从节点都给出ack应答才提交数据,这样可以避免脑裂情况发生,因为当发生脑裂时旧master因为不能得不到所有从节点的ack应答,所以是不会处理客户端的请求写从而旧可以避免脑裂问题。但是它的问题是在于性能较低,因为需要全部副本的响应,如果其中一个节点响应较慢则会拖慢整体的提交时间。

分布式共识算法

对于paxos、raft这类共识算法来说,因为它采用的多数决的机制,在出现网络分区时,只会存在有一个大多数而不会同时出现两个大多数。如果master位于网络分区后的少数派中,那这个master在接收到用户请求后,由于与它连通的只有少数节点达不到超过一半节点的支持,因此它是无法提交数据的。只会由多数节点构成的集群选举出来的新master这一个master对外提供服务;如果master处于多数节点构成的集群中,对于分隔出去的少数派节点构成的集群中因为节点数量不超过一半,所以根本就选取不出来一个新master。因此对于共识算法来说天然不会出现脑裂现象,相比于主从复制+ack的做法来说它能够带来更强的一致性保证。

分布式共识算法核心优势在于:

(1)容错。因为其多数派的原则,在出现网络分区时,只要不要超过半数以上的节点不可用,整个共识系统仍然是满足大多数原则的,仍然可以正常运转,在可用性方面具备非常强的一个容错能力。

(2)在强一致性的同时具备一定的性能优势。相比于全同步复制而言,因为多数决的机制,每次commit并不需要全部的节点同意,因此性能上而言相比于全同步复制更具有优势。

因为共识算法它所带来的强一致性保证和对集群节点的超强的容错能力,所以现在越来越多的分布式存储系统在解决多副本一致性问题上都在使用共识算法,比如new sql的tidb,内部就是基于raft算法以及mysql自身也推出了MGR集群,内部就是使用的mutil-paxos算法取代传统的半同步复制来解决多副本的一致性问题。

2.故障转移

一般来说,对于引入第三方协调服务的存储系统来说,会事先在集群中选举一个Master,此master并非我们所说的主从复制中的leader副本节点。以kafka为例,在Kafka集群中这类节点称之为Controller。当节点发生故障时,会由ZK将故障通知至Controller节点,此时触发controller节点进行故障转移。

按故障节点类型来说分为以下几类:

(1)leader副本节点故障。当故障节点为某分片的leader副本节点时,则直接会由Controller负责为该分区重新选举新的Leader副本;Controller在watch关于某leader副本节点故障后,则会直接从该leader副本节点的从节点列表中找到位移提交最大也就是数据最新的节点作为新的master。

(2)Controller节点故障。当故障节点为Controller自身时,则由借助于ZK从集群中的其他leader节点中选取一个新的controller节点。整个选举过程本质上也是ZK的一次分布式锁的抢占过程。当controller产生时,会从ZK中拉取一份集群元数据备份存储到本地。同时一般来说Controller节点并不是单独的物理节点实例,而是由集群中某leader分片节点担任。当controller节点故障时,同时也是leader副本节点故障,因此当新master产生后,同时还会为旧master节点的slave节点中选举新的leader副本。

对于redis、ES这些在集群内部实现自治的集群系统而言,则通常会在集群内部实现选举算法,来实现故障转移。

当集群中某节点在发现半数以上的节点都认为某节点疑似下线后,会将该节点标记为确定下线并在集群中进行广播。当slave收到节点下线通知后,判断如果是自己的master节点,则触发选举流程,开始进行故障转移。

以redis为例其选举算法流程如下:

(1)slave收到master下线通知,开启一个纪元,将currentEpoch+1,开启选举;

(2)slave计算发起投票的延时时间。对于所有有资格参选的节点来说,并不会一收到选举通知后立马就开始发起选举,而是会先延迟一段时间。其延时时间的计算基于当前slave复制的数据总量,如果总量越高比较数据越接近master,那么它的延时时间会越短,被选中的概率也就越大。

(3)发起投票。slave在延时时间到期后,会向集群广播投票请求;

(4)投票。集群中只有master节点具备投票权利,且在每个纪元中只有一次投票机会,master的投票原则是先到先得。当master收到投票请求后,会先基于自身的元数据审查该节点是否为故障节点的slave节点,如果是且当前还未给其他的slave节点投过票,则会将票投给该节点,因此理论上而言,数据越新的从节点获得票数会越高;

(5)票数统计。每个节点在达到指定时间后会统计自身的票数,因为每个节点只能投一次票,所以得票超过一半以上的只会有一个节点。

(6)广播通知。当该从节点发现自己得票一半以后,就会像整个集群中广播新master节点的消息,让其它节点都知道它已经是最新的主节点,其它的主节点在收到后会更新自己的节点表,从节点则会将它设为新的主节点,此时选举结束。如果有一些从节点发现自己既没有达到半数以上的投票,又在指定时间内没有收到新master的消息,则会开启新的纪元,再次发起选票,但是此次其它的主节点发现如果直接的节点列表中该主节点的状态不是fail状态或者对该纪元已经进行过投票,不会再进行投票。