分布式、系统一致性以及高并发

如果你想在校招中顺利拿到更好的offer,阿秀建议你多看看前人的经验 ,比如准备简历实习上岸经历校招总结阿里、字节、腾讯、美团等一二线大厂真实面经也欢迎来一起参加秋招打卡活动 等;如果你是计算机小白,学习/转行/校招路上感到迷茫或者需要帮助,可以点此联系阿秀;免费分享阿秀个人学习计算机以来的收集到的好资源,点此白嫖;如果你需要《阿秀的学习笔记》网站中求职相关知识点的PDF版本的话,可以点此下载

# 1、分布式系统基本概念

# 1、CAP理论基础

分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。

1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:

  • Consistency
  • Availability
  • Partition tolerance

它们的第一个字母分别是 CAP。Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。

它指出对于一个分布式计算系统来说,不可能同时满足以下三点:

  • 一致性(Consistency) :等同于所有节点访问同一份最新的数据副本,或者说同一数据在不同节点上的副本在同一逻辑时钟应当是相同的内容。
  • 可用性(Availability):每次请求都能获取到非错的响应,以及尽量保证低延迟,但是不保证获取的数据为最新数据。
  • 分区容错性(Partition tolerance):以实际效果而言,分区相当于对通信的时限要求。要求任意节点故障时,系统仍然可以对外服务。

# 2、数据一致性(C侧)

一些分布式系统通过复制数据来提高系统的可靠性和容错性,并且将数据的不同的副本存放在不同的机器,由于维护数据副本的一致性代价高,因此许多系统采用弱一致性来提高性能,一些不同的一致性模型也相继被提出。

  • 强一致性: 要求无论更新操作实在哪一个副本执行,之后所有的读操作都要能获得最新的数据。
  • 弱一致性:用户读到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。
  • 最终一致性:是弱一致性的一种特例,保证用户**最终(即窗口尽量长)**能够读取到某操作对系统特定数据的更新。

# 一致性解决方案

  1. 分布式事务:两段提交
  2. 分布式锁
  3. 消息队列、消息持久化、重试、幂等操作
  4. Raft / Paxos 等一致性算法

# 3、服务可用性(A侧)

可用性,意思是只要收到用户的请求,服务器就必须给出回应。

# 高可用解决方案

  • 负载均衡:尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性。
  • 降级:当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
  • 熔断:对于目标服务的请求和调用大量超时或失败,这时应该熔断该服务的所有调用,并且对于后续调用应直接返回,从而快速释放资源。确保在目标服务不可用的这段时间内,所有对它的调用都是立即返回的、不会阻塞的,等到目标服务好转后进行接口恢复。
  • 流量控制:流量控制可以有效的防止由于网络中瞬间的大量数据对网络带来的冲击,保证用户网络高效而稳定的运行,类似于TCP拥塞控制方法。
  • 异地多活:在不同地区维护不同子系统,并保证子系统的可用性

熔断是减少由于下游服务故障对自己的影响;而降级则是在整个系统的角度上,考虑业务整体流量,保护核心业务稳定。

# 4、分区容错性(P侧)

大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 CA 无法同时做到。

# 2、系统一致性

# 1、基本要求

规范的说,理想的分布式系统一致性应该满足:

  1. 可终止性(Termination):一致的结果在有限时间内能完成;
  2. 共识性(Consensus):不同节点最终完成决策的结果应该相同;
  3. 合法性(Validity):决策的结果必须是其它进程提出的提案。

第一点很容易理解,这是计算机系统可以被使用的前提。需要注意,在现实生活中这点并不是总能得到保障的,例如取款机有时候会是 服务中断 状态,电话有时候是 无法连通 的。

第二点看似容易,但是隐藏了一些潜在信息。算法考虑的是任意的情形,凡事一旦推广到任意情形,就往往有一些惊人的结果。例如现在就剩一张票了,中关村和西单的电影院也分别刚确认过这张票的存在,然后两个电影院同时来了一个顾客要买票,从各自观察看来,自己的顾客都是第一个到的……怎么能达成结果的共识呢?记住我们的唯一秘诀:核心在于需要把两件事情进行排序,而且这个顺序还得是合理的、大家都认可的

第三点看似绕口,但是其实比较容易理解,即达成的结果必须是节点执行操作的结果。仍以卖票为例,如果两个影院各自卖出去一千张,那么达成的结果就是还剩八千张,决不能认为票售光了。

# 2、强一致性

# 线性一致性

线性一致性或称 原子一致性严格一致性 指的是程序在执行的历史中在存在可线性化点P的执行模型,这意味着一个操作将在程序的调用和返回之间的某个点P起作用。这里“起作用”的意思是被系统中并发运行的所有其他线程所感知。要求如下:

  1. 写后读 这里写和读是两个操作,如果写操作在完成之后,读才开始,读要能读到最新的数据,而且保证以后也能读操作也都能读到这个最新的数据。
  2. 所有操作的时序与真实物理时间一致,要求即使不相关的两个操作,如果执行有先后顺序,线性一致性要求最终执行的结果也需要满足这个先后顺序。比如,操作序列(写A,读A,写B,读B),那么不仅,读A,读B能读到最新A值和B值;而且要保证,如果读B读到最新值时,读A一定也能读到最新值,也就是需要保证执行时序与真实时序相同。
  3. 如果两个操作是并发的(比如读A没有结束时,写B开始了),那么这个并发时序不确定,但从最终执行的结果来看,要确保所有线程(进程,节点)看到的执行序列是一致的。

# 顺序一致性

相比线性一致性,主要区别在于,对于物理上有先后顺序的操作,不保证这个时序。具体而言,对于单个线程,操作的顺序仍然要保留,对于多个线程(进程,节点),执行的事件的先后顺序与物理时钟顺序不保证。但是要求,从执行结果来看,所有线程(进程,节点)看到的执行序列是一样的。

# 因果一致性

因果一致性,被认为是比顺序一致性更弱的一致性,在因果一致性中,只对有因果关系的事件有顺序要求。

# 3、带约束的一致性

绝对理想的 强一致性(Strong Consistency) 代价很大。除非不发生任何故障,所有节点之间的通信无需任何时间,这个时候其实就等价于一台机器了。实际上,越强的一致性要求往往意味着越弱的性能、越低的可用性。

强一致的系统往往比较难实现。很多时候,人们发现实际需求并没有那么强,可以适当放宽一致性要求,降低系统实现的难度。例如在一定约束下实现所谓 最终一致性(Eventual Consistency),即总会存在一个时刻(而不是立刻),系统达到一致的状态,这对于大部分的 Web 系统来说已经足够了。这一类弱化的一致性,被笼统称为 弱一致性(Weak Consistency)

# 最终一致性

最终一致性也被称为 乐观复制(optimistic replication),用户只能读到某次更新后的值,但系统保证数据将最终达到完全一致的状态,只是所需时间不能保障。这个达成一致所需要的时间,我们称为 窗口时间

我们常见的 异步复制的主从架构实现的是最终一致性 。它的一个典型常见是用户读取异步从库时,可能读取到较旧的信息,因为该从库尚未完全与主库同步。注意,同步复制的主从架构会出现任一节点宕机导致的单点问题。

# 4、一致性(Consistency)与共识(Consensus)的关系

我们常说的 一致性(Consistency) 在分布式系统中指的是 副本(Replication) 问题中对于同一个数据的多个副本,其对外表现的数据一致性,如 线性一致性因果一致性最终一致性等,都是用来描述副本问题中的一致性的。

共识(Consensus) 则不同,共识问题中所有的节点要最终达成共识,由于最终目标是所有节点都要达成一致,所以根本 不存在一致性强弱 之分。

# 3、高并发系统的设计

# 1、系统拆分

将一个系统拆分为多个子系统,用 RPC 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。

# 2、缓存

大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟 Redis 轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。

# 3、消息队列

可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改。那高并发绝对搞挂你的系统,你要是用 Redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 MySQL 还得用 MySQL 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,后边系统消费后慢慢写,控制在 MySQL 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。

# 4、分库分表

分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高 SQL 跑的性能。

# 5、读写分离

读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。