【MIT 6.824】学习笔记 2: RPC and Threads

▲  点击上方”多颗糖” 关注公众号

点击“ 阅读原文 ”查看 MIT 6.824 2021年的教学视频,和 2020 年相比,换了个老师。

线程

“进程与线程的区别”是面试者要背下的八股文,这里简单复习下线程。

线程是操作系统能够进行运算调度的最小单位。。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务。

关于线程主要注意:

  • 同一进程中的多个线程共享该进程地址空间、文件描述符等,它们都可以访问全局变量;

  • 每个线程有自己的调用栈、程序计数器和寄存器。

为什么需要线程?

  • 线程实现并发,这是分布式系统所需要的。并发允许我们在一个处理器上调度多个任务,例如:线程可以让我们在等待 I/O 操作时执行其他任务,而不是等待 I/O 操作完成再继续执行;

  • 并行。我们可以在多个核心上并行执行多个任务。不同于单纯的并发,在同一时间只有一个任务在进行(取决于哪个任务在那一瞬间拥有它的 CPU 时间),并行允许多个任务在同一时间进行处理,因为它们是在不同的 CPU 核上执行的。

  • 方便。线程提供了一种在后台执行任务的便捷返回,例如:在后台每秒一次检查 worker 是否正常运行。

Go 有 Goroutines,它是轻量级线程。

线程带来的挑战

  • 死锁

  • 访问共享数据

  • 线程之间的协调。例如:一个线程在生产数据,另一个线程在消费数据,消费者如何等待数据的生产并释放 CPU?生产者如何唤醒消费者?

Go 通过 channelsync.CondWaitGroup 来处理这些问题。

另外,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 流程一般如下:

  1. 客户端调用 client stub,并将调用参数 push 到栈(stack)中,这个调用是在本地的

  2. client stub 将这些参数包装,并通过系统调用发送到服务端机器。打包的过程叫 marshalling (常见方式:XML、JSON、二进制编码)。

  3. 客户端操作系统将消息传给传输层,传输层发送信息至服务端;

  4. 服务端的传输层将消息传递给 server stub

  5. server stub 解析信息。该过程叫 unmarshalling

  6. server stub 调用程序,并通过类似的方式返回给客户端。

  7. 客户端拿到数据解析后,将执行结果返回给调用者。

这样做的主要好处是它简化了编写分布式应用的过程,因为 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/