Skip to content

Commit

Permalink
feat: update articles
Browse files Browse the repository at this point in the history
  • Loading branch information
wx-chevalier committed Nov 9, 2024
1 parent 2598314 commit 4480397
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 42 deletions.
85 changes: 71 additions & 14 deletions 01~分布式基础/01~不可靠的分布式系统/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,84 @@
# 不可靠的分布式系统

传统集中式的系统,譬如单个计算机上的软件,当硬件正常工作时,相同的操作总是产生相同的结果(即保证了确定性);如果存在硬件问题(例如,内存损坏或连接器松动),其后果通常是整个系统故障(例如,内核崩溃,蓝屏死机,启动失败)。此类系统通常要么功能完好,要么完全失效,而不是介于两者之间;很多时候如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回难以处理的错误的结果,此类系统我们也往往认为其是稳定的。
## 集中式系统 vs 分布式系统

在分布式系统中,我们不再处于理想化的系统模型中,它与运行在单台计算机上的程序的不同之处在于:没有共享内存,只有通过可变延迟的不可靠网络传递的消息;系统可能遭受部分失效;不可靠的时钟和处理暂停。譬如单个数据中心(DC)中长期存在的网络分区,配电单元 PDU 故障,交换机故障,整个机架的意外重启,整个数据中心主干网络故障,整个数据中心的电源故障,乃至于光缆被挖断等等。在分布式系统中,尽管系统的其他部分工作正常,但系统的某些部分可能会以某种不可预知的方式被破坏。这被称为部分失效(partial failure)。难点在于部分失效是不确定性的(nonderterministic):如果你试图做任何涉及多个节点和网络的事情,它有时可能会工作,有时会出现不可预知的失败。正如我们将要看到的,你甚至不知道是否成功了,因为消息通过网络传播的时间也是不确定的。
### 集中式系统的特点

# NPC 问题
- 确定性:相同操作产生相同结果
- 故障模式简单:要么完全正常,要么完全失效
- 稳定性:硬件正常时系统表现可预测

在分布式系统中,最典型的问题可以归纳为所谓的 NPC 问题,即 Network Delay, Process Pause, Clock Drift 的首字母缩写:
### 分布式系统的特点

- Network Delay,网络延迟,当您尝试通过网络发送数据包时,数据包可能会丢失或任意延迟。同样,答复可能会丢失或延迟,所以如果你没有得到答复,你不知道消息是否通过。虽然网络在多数情况下工作的还可以,虽然**TCP 保证传输顺序和不会丢失,但它无法消除网络延迟**问题。一个糟糕的例子是:客户正紧张的坐在屏幕前等待下单结果,服务器也已下单成功,但在返回的确认响应在网络上迷失了,也就是走丢了。
- 无共享内存,仅通过不可靠网络通信
- 存在部分失效的可能
- 面临不可靠的时钟和处理暂停问题

- Process Pause,进程暂停,一个进程可能会在其执行的任何时候暂停一段相当长的时间,被其他节点宣告死亡,然后再次复活,却没有意识到它被暂停了。有很多种原因可以导致进程暂停:比如编程语言中的 GC(垃圾回收机制)会暂停所有正在运行的线程;再比如,我们有时会暂停云服务器,从而可以在不重启的情况下将云服务器从一台主机迁移到另一台主机。我们**无法确定性预测进程暂停的时长**,你以为持续几百毫秒已经很长了,但实际上持续数分钟之久进程暂停并不罕见。
常见故障示例:

- Clock Drift,时钟漂移。现实生活中我们通常认为时间是平稳流逝,单调递增的,但在计算机中不是。计算机使用时钟硬件计时,通常是石英钟,计时精度有限,同时受机器温度影响。为了在一定程度上同步网络上多个机器之间的时间,通常使用 NTP 协议将本地设备的时间与专门的时间服务器对齐,这样做的一个直接结果是**设备的本地时间可能会突然向前或向后跳跃**
- 数据中心网络分区
- PDU(配电单元)故障
- 交换机故障
- 机架意外重启
- 数据中心网络或电源故障
- 物理设施损坏(如光缆被挖断)

![NPC 问题速览](https://pic.imgdb.cn/item/606bde378322e6675c603e33.jpg)
## NPC 问题详解

# 分布式系统中的抽象
### Network Delay(网络延迟)

分布式系统中的许多事情可能会出错。处理这种故障的最简单方法是简单地让整个服务失效,并向用户显示错误消息。如果无法接受这个解决方案,我们就需要找到容错的方法,即使某些内部组件出现故障,服务也能正常运行。不过在前文我们讨论的所有问题都发生的情况下:网络中的数据包可能会丢失,重新排序,重复递送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃,我们需要找到一些带有实用保证的通用抽象,仅实现一次而后让应用依赖这些保证,最终构建我们期望的容错系统。
- 数据包可能丢失或任意延迟
- 即使使用 TCP 也无法完全解决延迟问题
- 响应丢失导致状态不确定

典型的抽象就是事务与共识,通过使用事务,应用可以假装没有崩溃(原子性),没有其他人同时访问数据库(隔离),存储设备是完全可靠的(持久性)。即使发生崩溃,竞态条件和磁盘故障,事务抽象隐藏了这些问题,因此应用不必担心它们。分布式系统的设计核心之一就在一致性的实现和妥协,我们需要选择合适的算法来保证不同节点之间的通信和数据达到无限趋向一致性。实际情况下,保证不同节点在充满不确定性网络环境下能达成相同副本的一致性是非常困难的,无法保证网络的正常连接和信息的传送,于是发展出了 CAP/FLP/DLS 这三个重要的理论:
### Process Pause(进程暂停)

- CAP: 分布式计算系统不可能同时确保一致性(Consistency)、可用性(Availablity)和分区容忍性(Partition)。
- FLP: 在异步环境中,如果节点间的网络延迟没有上限,只要有一个恶意的节点存在,就没有算法能在有限的时间内达成共识。
- DLS: 网络延时可以保证小于已知时间的同步模型中的协议可以 100% 容错,网络延时有界限但是我们并不知道在哪里的部分同步网络模型可以容忍拜占庭错误,异步模型中的确定性的协议(没有网络延时上限)不能容错。
- 进程可能在任意时刻暂停
- 暂停原因多样:
- 垃圾回收(GC)
- 云服务器迁移
- 暂停时长不可预测(可能持续数分钟)

### Clock Drift(时钟漂移)

- 计算机时钟不完全可靠
- 受硬件限制和环境影响
- NTP 同步可能导致时间跳跃

## 分布式系统的抽象

### 处理故障的方法

1. 简单处理:服务失效并显示错误
2. 容错处理:通过抽象机制保证服务可用

### 关键抽象概念

1. 事务

- 原子性:避免崩溃影响
- 隔离性:处理并发访问
- 持久性:确保数据可靠

2. 共识
- 确保节点间数据一致性
- 在不可靠网络中实现困难

### 重要理论

1. CAP 理论

- 一致性(Consistency)
- 可用性(Availability)
- 分区容忍性(Partition Tolerance)
- 三者无法同时满足

2. FLP 理论

- 异步环境中共识问题的局限性
- 存在恶意节点时的限制

3. DLS 理论
- 同步模型的容错能力
- 部分同步网络的特性
- 异步模型的限制
Original file line number Diff line number Diff line change
@@ -1,51 +1,86 @@
# 不可靠进程

# 休眠的进程
## 休眠的进程问题

让我们考虑在分布式系统中使用危险时钟的另一个例子。假设你有一个数据库,每个分区只有一个领导者。只有领导被允许接受写入。一个节点如何知道它仍然是领导者(它并没有被别人宣告为死亡),并且它可以安全地接受写入?一种选择是领导者从其他节点获得一个租约(lease),类似一个带超时的锁。任一时刻只有一个节点可以持有租约——因此,当一个节点获得一个租约时,它知道它在某段时间内自己是领导者,直到租约到期。为了保持领导地位,节点必须在周期性地在租约过期前续期。
### 问题引入

如果节点发生故障,就会停止续期,所以当租约过期时,另一个节点可以接管。可以想象,请求处理循环看起来像这样:
在分布式系统中,进程可能会出现意外的休眠或暂停,这会导致严重的系统问题。让我们通过一个领导者选举的例子来说明这个问题。

### 示例:基于租约的领导者选举

在分布式数据库中,每个分区只能有一个领导者节点处理写入请求。节点通过获取租约(带超时的锁)来确认自己的领导者身份。以下是一个简化的处理循环:

```java
while(true){
request=getIncomingRequest();
// 确保租约还剩下至少10秒
if (lease.expiryTimeMillis-System.currentTimeMillis()< 10000){
lease = lease.renew();
}

if(lease.isValid()){
process(request);
}}
while(true) {
request = getIncomingRequest();
// 确保租约还剩下至少10秒
if (lease.expiryTimeMillis - System.currentTimeMillis() < 10000) {
lease = lease.renew();
}

if (lease.isValid()) {
process(request);
}
}
```

这个代码有什么问题?首先,它依赖于同步时钟:租约到期时间由另一台机器设置(例如,当前时间加上 30 秒,计算到期时间),并将其与本地系统时钟进行比较。如果时钟超过几秒不同步,这段代码将开始做奇怪的事情。其次,即使我们将协议更改为仅使用本地单调时钟,也存在另一个问题:代码假定在执行剩余时间检查 System.currentTimeMillis()和实际执行请求 process(request)中间的时间间隔非常短。通常情况下,这段代码运行得非常快,所以 10 秒的缓冲区已经足够确保租约在请求处理到一半时不会过期。
### 代码中的问题

1. **时钟依赖问题**:代码依赖于同步时钟,如果时钟不同步可能导致异常行为
2. **执行暂停风险**:代码假设检查时间和处理请求之间的间隔很短,但实际上进程可能在任何时候暂停

## 进程暂停的原因

### 1. 垃圾收集(GC)暂停

- 运行时的"停止世界"GC 可能持续数分钟
- 即使是"并行"GC 也需要定期停止所有线程

### 2. 虚拟化环境影响

- 虚拟机可能被挂起和恢复
- 实时迁移过程中的暂停

### 3. 硬件和操作系统层面

但是,如果程序执行中出现了意外的停顿呢?例如,想象一下,线程在 lease.isValid()行周围停止 15 秒,然后才终止。在这种情况下,在请求被处理的时候,租约可能已经过期,而另一个节点已经接管了领导。然而,没有什么可以告诉这个线程已经暂停了这么长时间了,所以这段代码不会注意到租约已经到期了,直到循环的下一个迭代 ——到那个时候它可能已经做了一些不安全的处理请求。
- 笔记本电脑合盖等用户操作
- 操作系统上下文切换
- CPU 时间被其他虚拟机窃取

假设一个线程可能会暂停很长时间,这是疯了吗?不幸的是,这种情况发生的原因有很多种:
### 4. I/O 相关暂停

- 许多编程语言运行时(如 Java 虚拟机)都有一个垃圾收集器(GC),偶尔需要停止所有正在运行的线程。这些“停止世界(stop-the-world)”GC 暂停有时会持续几分钟!甚至像 HotSpot JVM 的 CMS 这样的所谓的“并行”垃圾收集器也不能完全与应用程序代码并行运行,它需要不时地停止世界。尽管通常可以通过改变分配模式或调整 GC 设置来减少暂停,但是如果我们想要提供健壮的保证,就必须假设最坏的情况发生。
- 磁盘 I/O 操作
- 类加载造成的意外 I/O
- 网络文件系统延迟

- 在虚拟化环境中,可以挂起(suspend)虚拟机(暂停执行所有进程并将内存内容保存到磁盘)并恢复(恢复内存内容并继续执行)。这个暂停可以在进程执行的任何时候发生,并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移,而不需要重新启动,在这种情况下,暂停的长度取决于进程写入内存的速率。
### 5. 内存管理

- 在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
- 页面错误和内存交换
- 系统抖动问题

- 当操作系统上下文切换到另一个线程时,或者当管理程序切换到另一个虚拟机时(在虚拟机中运行时),当前正在运行的线程可以在代码中的任意点处暂停。在虚拟机的情况下,在其他虚拟机中花费的 CPU 时间被称为窃取时间(steal time)。如果机器处于沉重的负载下(即,如果等待运行的线程很长),暂停的线程再次运行可能需要一些时间。
### 6. 进程控制

- 如果应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘 I/O 操作完成。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能出乎意料地发生——例如,Java 类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。I/O 暂停和 GC 暂停甚至可能合谋组合它们的延迟。如果磁盘实际上是一个网络文件系统或网络块设备(如亚马逊的 EBS),I/O 延迟进一步受到网络延迟变化的影响。
- SIGSTOP 信号导致的暂停
- 运维操作造成的意外暂停

- 如果操作系统配置为允许交换到磁盘(分页),则简单的内存访问可能导致页面错误(page fault),要求将磁盘中的页面装入内存。当这个缓慢的 I/O 操作发生时,线程暂停。如果内存压力很高,则可能需要将不同的页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为抖动(thrashing))。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。
## 缓解措施

- 可以通过发送 SIGSTOP 信号来暂停 Unix 进程,例如通过在 shell 中按下 Ctrl-Z。这个信号立即阻止进程继续执行更多的 CPU 周期,直到 SIGCONT 恢复为止,此时它将继续运行。即使你的环境通常不使用 SIGSTOP,也可能由运维工程师意外发送。
### 垃圾收集影响的控制

所有这些事件都可以随时抢占(preempt)正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时机做任何假设,因为随时可能发生上下文切换,或者出现并行运行。当在一台机器上编写多线程代码时,我们有相当好的工具来实现线程安全:互斥量,信号量,原子计数器,无锁数据结构,阻塞队列等等。不幸的是,这些工具并不能直接转化为分布式系统操作,因为分布式系统没有共享内存,只有通过不可靠网络发送的消息。
1. **计划性 GC**

分布式系统中的节点,必须假定其执行可能在任意时刻暂停相当长的时间,即使是在一个函数的中间。在暂停期间,世界的其它部分在继续运转,甚至可能因为该节点没有响应,而宣告暂停节点的死亡。最终暂停的节点可能会继续运行,在再次检查自己的时钟之前,甚至可能不会意识到自己进入了睡眠。
- 提前警告应用程序即将进行 GC
- 暂停节点的请求处理
- 等待现有请求处理完成
- 执行 GC

## 限制垃圾收集的影响
2. **对象生命周期管理**
- 只对短命对象进行垃圾收集
- 定期重启进程以避免完整 GC
- 采用滚动重启策略

过程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。语言运行时在计划垃圾回收时具有一定的灵活性,因为它们可以跟踪对象分配的速度和随着时间的推移剩余的空闲内存。一个新兴的想法是将 GC 暂停视为一个节点的短暂计划中断,并让其他节点处理来自客户端的请求,同时一个节点正在收集其垃圾。如果运行时可以警告应用程序一个节点很快需要 GC 暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行 GC。这个技巧隐藏了来自客户端的 GC 暂停,并降低了响应时间的高百分比。一些对延迟敏感的金融交易系统使用这种方法。
### 最佳实践

这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象要快速收集),并定期在积累大量长寿对象(因此需要完整 GC)之前重新启动进程。一次可以重新启动一个节点,在计划重新启动之前,流量可以从节点移开,就像滚动升级一样。这些措施不能完全阻止垃圾回收暂停,但可以有效地减少它们对应用的影响。
- 在服务器上禁用页面调度
- 合理规划内存使用
- 实施监控和告警机制
56 changes: 56 additions & 0 deletions 02~一致性与共识/一致性模型/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,59 @@
# 一致性模型

分布式系统常常通过数据的复制来提高系统的可靠性和容错性,并且将数据的副本存放到不同的机器上,由于多个副本的存在,使得维护副本一致性的代价很高。因此,许多分布式系统都采用弱一致性或者是最终一致性,来提高系统的性能和吞吐能力,这样不同的一致性模型也相继被提出。

## 主要的一致性模型

### 强一致性(Strong Consistency)

- 也称为线性一致性(Linearizability)
- 所有节点在同一时间看到的数据完全一致
- 任何读操作都能读到最近一次写操作的结果
- 优点:对用户友好,容易理解
- 缺点:性能较差,可用性降低

### 最终一致性(Eventual Consistency)

- BASE 理论的核心
- 在一段时间后,所有节点的数据最终会达到一致
- 常见变体:
- 读自己写一致性(Read-your-writes Consistency)
- 会话一致性(Session Consistency)
- 因果一致性(Causal Consistency)

### 顺序一致性(Sequential Consistency)

- 所有节点看到的操作顺序是一致的
- 但不要求这个顺序与全局时钟完全对应
- 比强一致性的要求略低,但仍然提供较强的一致性保证

### 因果一致性(Causal Consistency)

- 有因果关系的写操作以相同的顺序被观察到
- 无因果关系的写操作可以以不同顺序被观察到
- 是顺序一致性的进一步放松

### 读写一致性(Read-Write Consistency)

- 读自己写一致性:保证进程可以立即看到自己的写入
- 单调读一致性:如果进程读到某个值,后续不会读到更旧的值
- 单调写一致性:写操作按照顺序执行

## 实际应用

不同的分布式系统根据其业务需求选择不同的一致性模型:

- 分布式数据库(如 Google Spanner):通常需要强一致性
- NoSQL 数据库(如 Cassandra):通常采用最终一致性
- 分布式缓存(如 Redis Cluster):可配置的一致性级别
- 社交网络:通常采用最终一致性

## 选择考虑因素

在选择一致性模型时,需要考虑以下因素:

1. 业务需求
2. 性能要求
3. 可用性要求
4. CAP 理论权衡
5. 实现复杂度

0 comments on commit 4480397

Please sign in to comment.