【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,类似于:
<client_id, seq>
(和 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/
相关阅读
【MIT 6.824】学习笔记 1:MapReduce
欢迎关注我的公众号: