【MIT 6.824】学习笔记 2: RPC and Threads
▲ 点击上方”多颗糖” 关注公众号
点击“ 阅读原文 ”查看 MIT 6.824 2021年的教学视频,和 2020 年相比,换了个老师。
线程
“进程与线程的区别”是面试者要背下的八股文,这里简单复习下线程。
线程是操作系统能够进行运算调度的最小单位。。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务。
关于线程主要注意:
-
同一进程中的多个线程共享该进程地址空间、文件描述符等,它们都可以访问全局变量;
-
每个线程有自己的调用栈、程序计数器和寄存器。
为什么需要线程?
-
线程实现并发,这是分布式系统所需要的。并发允许我们在一个处理器上调度多个任务,例如:线程可以让我们在等待 I/O 操作时执行其他任务,而不是等待 I/O 操作完成再继续执行;
-
并行。我们可以在多个核心上并行执行多个任务。不同于单纯的并发,在同一时间只有一个任务在进行(取决于哪个任务在那一瞬间拥有它的 CPU 时间),并行允许多个任务在同一时间进行处理,因为它们是在不同的 CPU 核上执行的。
-
方便。线程提供了一种在后台执行任务的便捷返回,例如:在后台每秒一次检查 worker 是否正常运行。
Go 有 Goroutines,它是轻量级线程。
线程带来的挑战
-
死锁
-
访问共享数据
-
线程之间的协调。例如:一个线程在生产数据,另一个线程在消费数据,消费者如何等待数据的生产并释放 CPU?生产者如何唤醒消费者?
Go 通过 channel
、 sync.Cond
、 WaitGroup
来处理这些问题。
另外,Go 还有一个内置的竞态数据检测器:https://golang.org/doc/articles/race_detector
Event-Driven
除了线程,还提到了事件驱动编程,一个进程只有一个线程,它监听事件,并在事件发生时执行用户指定的函数。Node.js 就使用了 Event-Driven,被称为 event loop。
在我看来,Event-Driven 实现比较困难(这很主观),在分布式系统用得较少,不展开。
RPC
远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议。该协议允许客户端通过 RPC 执行服务端的函数,就像调用本地函数一样。
一个 RPC 流程一般如下:
-
客户端调用 client stub,并将调用参数 push 到栈(stack)中,这个调用是在本地的
-
client stub 将这些参数包装,并通过系统调用发送到服务端机器。打包的过程叫
marshalling
(常见方式:XML、JSON、二进制编码)。 -
客户端操作系统将消息传给传输层,传输层发送信息至服务端;
-
服务端的传输层将消息传递给 server stub
-
server stub 解析信息。该过程叫
unmarshalling
。 -
server stub 调用程序,并通过类似的方式返回给客户端。
-
客户端拿到数据解析后,将执行结果返回给调用者。
这样做的主要好处是它简化了编写分布式应用的过程,因为 RPC 将所有的网络相关的代码都隐藏到了 stub 函数中,程序员不必担心数据转换和解析、打开和关闭连接等细节。
处理失败
从客户端的角度来看,失败是指向服务端发送请求,在特定的时间内没有得到响应。这可能是由多种原因造成的,包括:数据包丢失、服务端处理速度慢、服务端宕机和网络故障。
处理这种情况很棘手,因为客户端不会知道服务端具体的情况,可能导致请求失败的原因有:
-
服务端没有收到这个请求
-
服务端执行了请求,但响应之前宕机了
-
服务端执行了请求并发送了响应,但在响应之前网络故障了
最简单的办法就是重试,但是如果服务端之前已经执行了请求,重复发送请求可能导致服务端执行两次相同的请求,这也可能会导致问题。这种方法对幂等的请求很有效,但非幂等的请求需要别的方法来处理失败。
RPC 可以实现三种语义:
-
At-Most-Once
:客户端不会自动重试一个请求。在这种情况下,重新发送请求是客户端的选择。 -
At-Least-Once
:客户端会不断重试请求,直到收到请求被执行的肯定确认。这适用于幂等操作。 -
Exactly-Once
:在这种模式下,请求既不能重复,也不能丢失。这一点比较难实现,也是容错率最低的,因为它要求必须从服务器上收到响应,不能有重复。如果我们有多台服务器,而处理初始请求的那台服务器故障了,其他服务器可能无法判断请求是否被执行了。
Go RPC 实现了 At-Most-Once
语义,如果没有得到响应,只会返回一个错误。客户端可以选择重试一个失败请求,但服务端要自己处理重复的请求。
我想到的是,可以给请求做个唯一 ID,这样重复的请求能够被检测到,就不再执行,直接返回对应的响应。但也要处理一些细节问题:
-
如何保证多个客户端的 ID 是唯一的?可以带上客户端 ID,类似于:
(和 Raft 客户端交互那部分内容对应上了!)
-
但我们不可能无期限地保存所有的请求 ID,保存多长时间?可以在客户端的请求中包含一个额外的标识符 X,告诉服务端删除 X 之前的所有请求 ID 是安全的
-
当原始请求还在执行时,如何处理重复的请求?可以等待它完成,也可以直接忽略新的请求。
-
为了避免服务器宕机,ID 信息还需要写入到磁盘,也许还要跨机器多副本存储。
Reference
-
6.824 2021 Lecture 2: Infrastructure: RPC and threads: https://pdos.csail.mit.edu/6.824/notes/l-rpc.txt
-
Go 内置的竞态数据检测器:https://golang.org/doc/articles/race_detector
-
Remote Procedure Calls:https://www.cs.rutgers.edu/~pxk/417/notes/03-rpc.html
-
Go RPC:https://timilearning.com/posts/mit-6.824/lecture-2-rpc-and-threads/