本章主要讲了持久化的相关知识。
I/O设备
典型系统的架构。其中,CPU 通过某种内存总线(memory bus)或互连电缆连接到系统内存。图像或者其他高性能 I/O 设备通过常规的 I/O 总线(I/O bus)连接到系统,在许多现代系统中会是 PCI 或它的衍生形式。最后,更下面是外围总线(peripheral bus),比如 SCSI、SATA 或者 USB。它们将最慢的设备连接到系统,包括磁盘、鼠标及其他类似设备。
这样的分层布局首先节省了成本,对于访问速度快的设备不会有太多,可以有效的减少高性能高成本总线的长度。其次对于慢的设备,可以在外围总线上连接更多的设备。
标准
下面是一个简化的标准设备:
首先是对外开放的硬件接口部分,其次是具体实现的内部结构。
一个简化的设备结构有三个寄存器:状态(status),命令(command),数据(data)寄存器。
下面是一个OS和设备典型交互的示例:
// 此时CPU在不断的轮询
While (STATUS == BUSY)
; // wait until device is not busy
Write data to DATA register
Write command to COMMAND register
(Doing so starts the device and executes the command)
While (STATUS == BUSY)
; // wait until device is done with your request
如果CPU参与数据的移动,那么就称之为可编程I/O((programmed I/O,PIO)。
如何减少CPU的占用,降低IO操作的成本呢?
中断
正如CPU章节所述,可以在程序进行I/O操作时,将其切换到睡眠状态,当I/O操作结束后,设备会抛出一个硬件中断,OS会执行一段中断服务代码,唤醒对应的程序继续运行。
但是中断也不一定性能就好于PIO,可以采用混合策略,即先轮询一小段时间,然后再中断。
DMA
对于从内存向磁盘中写入数据,时间线如下:
c所代表的就是将内存中的数据拷贝(copy)到磁盘,然后在进行I/O操作,大量的重复工作同样会加大CPU的负载,于是直接存储器访问(Direct Memory Access,DMA)出现了。
操作系统会通过编程告诉 DMA 引擎数据在内存的位置,要拷贝的大小以及要拷贝到哪个设备。在此之后,操作系统就可以 处理其他请求了。当 DMA 的任务完成后,DMA 控制器会抛出一个中断来告诉操作系统自己已经完成数据传输。
设备交互
第一种办法是用明确的 I/O 指令。这些指令规定了操作系统将数据发送到特定设备寄存器的方法,从而允许构造上文提到的协议。 例如在 x86 上,in 和 out 指令可以用来与设备进行交互。当需要发送数据给设备时,调用者指定一个存入数据的特定寄存器及一个代表设备的特定端口。执行这个指令就可以实 现期望的行为。 这些指令通常是特权指令(privileged)。操作系统是唯一可以直接与设备交互的实体。
第二种方法是内存映射 I/O(memory- mapped I/O)。通过这种方式,硬件将设备寄存器作为内存地址提供。当需要访问设备寄存器时,操作系统装载(读取)或者存入(写入) 到该内存地址;然后硬件会将装载/存入转移到设备上,而不是物理内存。
设备驱动程序
将设备交互的细节封装起来,称为设备驱动程序(device driver),它为上层提供了抽象,上层系统不需要对设备的读写请求细节有很多的了解。下图是Linux的文件系统栈:
文件系统层不需要了解读取的是什么类型的磁盘,只需要向通用块层发出请求即可。
磁盘
基本介绍
对于一个盘片(platter),有两个盘面,所有盘片都围绕主轴(spindle)连接在一起,主轴连接到一个电机,以一个恒定(固定) 的速度旋转盘片(当驱动器接通电源时)。旋转速率通常以每分钟转数(Rotations Per Minute, RPM)来测量。数据在扇区的同心圆中的每个表面上被编码。我们称这样的同心圆为一个磁道(track)。读写过程由磁头(disk head)完成;驱动器的每 个表面有一个这样的磁头。磁头连接到单个磁盘臂(disk arm)上,磁盘臂在表面上移动, 将磁头定位在期望的磁道上。磁道被分为一个一个的扇区。
首先寻道,然后等待转动延迟,最后传输
旋转延迟
磁盘必须等待期望的扇区旋转到磁头下之后才能读取数据,这被称为旋转延迟(rotational delay)。
寻道时间
为了服务一个读取请求,驱动器必须首先将磁盘臂移动到正确的磁道,通过一个所谓的寻道(seek)过程。
传输
当磁头落到正确的位置,就会开始传输(transfer),数据从表面读取或写入表面。
磁道偏斜(track skew)
为了方便跨磁道间的顺序读写,防止寻道时间影响,通常会在磁道间加入磁道偏斜,如下图
磁道缓冲区(track buffer)
驱动器可以使用这些内存来保存从磁盘读取或写入磁盘的数据,这里就又涉及到缓存一致性的问题。
磁盘调度
与CPU任务调度不同,磁盘通常可以猜测当前任务的执行时间。因此,磁盘调度程序将尝试在其操作中遵循 SJF(最短任务优先)的原则(principle of SJF,shortest job first)
SSTF:最短寻道时间优先
SSTF 按磁道对 I/O 请求队列排序,选择在最近磁道上的请求先完成。
问题:饥饿,有可能磁盘对同一磁道有较为稳定的需求,而不会去执行其他磁道的请求。
电梯(SCAN 或 C-SCAN)
简单地以跨越磁道的顺序来服务磁盘请求。
问题:忽略旋转。
SPTF:最短定位时间优先
视情况而定,这里讲的不是特别清楚。
简单文件系统(VSFS)
整体组织
首先将磁盘进行分块(block),然后对于不同的块进行分区处理,对于一个64块,每块为4KB的组织结构,如下图:
在这里将前8块保留系统使用,后面的留给用户使用,称为数据区域(data region)。
使用第3~7
块存放inode,每个inode都存放了文件的metadata。
使用第1~2
块来表示inode和数据区域的使用情况,使用的数据结构是位图(bitmap),一种用于数据区域(数据位图,data bitmap),另一种用于 inode 表(inode 位图,inode bitmap)。
最后,第一块通常称之为超级块(supperblock),里面存放的是系统的一些metadata,幻数,初始化参数之类的。
inode
inode是一种结构,里面存放了文件的关键metadata,对于ext2,字段如下:
计算inode位置
每个 inode 都由一个数字(称为 inumber)隐式引用。在当前的简单文件系统中,计算方式如下:
假设有以下磁盘组织形式:
如果要获取inode的inumber为32,首先要去计算inode的物理地址:
但是,磁盘通常是按照扇区来分块的,假设一个扇区为512KB
,那么:
所以就会像扇区40发出请求。
如何引用数据块
当拿到inode后,怎么找到其对应的数据区域的数据块呢,最简单的办法就是设置一个或多个直接指针,指向对应的数据块。
可当文件大小过于巨大时,直接索引就显得不太够用了,所以又引入了间接指针(indirect pointer),通过一个直接索引指向一个包含更多索引的块。很显然,还会存在双重间接指针,三重间接指针。
这里的思想其实很像虚拟内存中的多重页表
目录组织
一个目录基本上只包含一个二元组(条目名称,inode 号)的列表。对于给定目录中的每个文件或目录,目录的数据块中都有一个字符串和一个数字。对于每个字符串,可能还有一个长度(假定采用可变大小的名称)。
目录的存储和文件相同,只不过在inode中标识了目录这一属性。
访问路径
通过一个例子来解释系统是如何通过路径来寻找文件的。
当发出open("/foo/bar", O_RDONLY)
调用时,我们目前只有一条绝对路径。因此,必须要从根目录/
开始,递归的寻找inode。对于大多数UNIX系统,根目录的inumber是众所周知的,为2
。
当我们拿到2后,就可以寻找对应的数据块,然后再根目录的数据块中,找到foo对应的inumber。重复上面的操作,最后会找到bar对应的inode。系统会进行权限检查,在每个进程的打开文件表中,分配一个文件描述符(fd),返回给用户。
之后,程序可以发出read(fd,buffer,buffer_size)
的系统调用。
很明显,在文件访问中,IO量和路径长度呈正相关。
写入:一个读取数据位图(然后更新以标记新分配的块被使用),一个写入位图(将它的新状态存入磁盘),再是两次读取,然后写入 inode(用新块的位置更新),最后一次写入真正的数据块本身。
创建:一个读取 inode 位图(查找空闲 inode),一个写入 inode 位图(将其标记为已分配),一个写入新的 inode 本身(初始化它),一个写入目录的数据(将文件的高级名称链接到它的 inode 号),以及一个读写目录 inode 以便更新它。如果目录需要增长以容纳新条目,则还需要额外的 I/O(即数据位图和新目录块)。
缓存和缓冲
现代系统采用动态划分(dynamic partitioning),现代操作系统将虚拟内存页面和文件系统页面集成到统一页面缓存中。通过这种方式,可以在虚拟内存和文件系统之间更灵活地分配内存,具体取决于在给定时间哪种内存需要更多的内存。
对于写入,一般会采用延迟批量写入的策略,但是这样就有可能会出现缓存一致性的问题,可以通过fsync(fd)
强行将缓存刷到磁盘中。
linux同步IO函数:sync、fsync与fdatasync
链接
上面的文章讲的很好了,补充一点,当myfile被删除后,再次写入soft,生成的myfile和hard的inode不是同一个inode,可以通过stat hard
查看Links,或者使用ls -i hard
查看。
FFS
FFS–Unix文件系统的鼻祖,这一篇讲了关于FFS对简单文件系统的优化,包括group,增大blockSize等等解决了空间碎片导致的同一文件数据块的分散,进而引发磁盘的不断寻道,降低了系统IO效率的问题,对应ostep是第41章。
崩溃一致性(ext,NTFS)
文件系统面临的一个主要挑战在于,如何在出现断电(power loss)或系统崩溃(system crash)的情况下,更新持久数据结构。称为崩溃一致性问题(crash-consistency problem)。
举一个例子:
现在需要将单个数据块附加到原有文件。通过打开文件,调用 lseek()将文件偏移量移动到文件末尾,然后在关闭文件之前,向文件发出单个4KB 写入来完成追加。在这里还是使用简单文件系统来描述,初始状态如下:
期望的最终状态如下:
在此过程中,涉及到三次IO操作:
- 对于inode的修改。
- 对于data bmap的修改。
- 对于data block的写入。
由于缓冲区的存在,IO操作不会立马落到磁盘上,而是先缓存在缓冲区,等待一段时间后在批量写入,在此过程中发生的问题就称为崩溃一致性问题。
崩溃场景
都没有写入磁盘:很明显用户的操作丢失了,但是对系统没有什么影响。
只有data block写入磁盘:系统中就会出现一块不被任何inode引用的data block,系统会把其当作空闲块处理。
只有inode写入磁盘:inode的指针指向了一块垃圾区域,同时,出现了文件系统不一致(file-system inconsistency),即inode bmap和inode的状态不一致。
只有data bmap写入磁盘:发生了内存泄漏(space leak),文件系统将永远不会使用这一块区域。
inode和data bmap写入磁盘:文件系统一致,但是指向的是垃圾块。
inode和data block写入磁盘:文件系统不一致。
data bmap和data block写入磁盘:文件系统不一致。
fsck
一种办法是事后检查,采用fsck程序进行检查修复。
- 超级块:fsck 首先检查超级块是否合理,主要是进行健全性检查,例如确保文件系 统大小大于分配的块数。通常,这些健全性检查的目的是找到一个可疑的(冲突的)超级块。在这种情况下,系统(或管理员)可以决定使用超级块的备用副本。
- 空闲块:接下来,fsck 扫描 inode、间接块、双重间接块等,以了解当前在文件系 统中分配的块。它利用这些知识生成正确版本的分配位图。因此,如果位图和 inode 之间存在任何不一致,则通过信任 inode 内的信息来解决它。对所有 inode 执行相 同类型的检查,确保所有看起来像在用的 inode,都在 inode 位图中有标记。
- inode 状态:检查每个 inode 是否存在损坏或其他问题。例如,fsck 确保每个分配 的 inode 具有有效的类型字段(即常规文件、目录、符号链接等)。如果 inode 字 段存在问题,不易修复,则 inode 被认为是可疑的,并被 fsck 清除,inode 位图相 应地更新。
- inode 链接:fsck 还会验证每个已分配的 inode 的链接数。你可能还记得,链接计数表示包含此特定文件的引用(即链接)的不同目录的数量。为了验证链接计数, fsck 从根目录开始扫描整个目录树,并为文件系统中的每个文件和目录构建自己的链接计数。如果新计算的计数与 inode 中找到的计数不匹配,则必须采取纠正措施, 通常是修复 inode 中的计数。如果发现已分配的 inode 但没有目录引用它,则会将 其移动到 lost + found 目录。
- 重复:fsck 还检查重复指针,即两个不同的 inode 引用同一个块的情况。如果一个 inode 明显不好,可能会被清除。或者,可以复制指向的块,从而根据需要为每个 inode 提供其自己的副本。
- 坏块:在扫描所有指针列表时,还会检查坏块指针。如果指针显然指向超出其有效范围的某个指针,则该指针被认为是“坏的”,例如,它的地址指向大于分区 大小的块。在这种情况下,fsck 不能做任何太聪明的事情。它只是从 inode 或间接 块中删除(清除)该指针。
- 目录检查:fsck 不了解用户文件的内容。但是,目录包含由文件系统本身创建的特定格式的信息。因此,fsck 对每个目录的内容执行额外的完整性检查,确保“.” 和“..”是前面的条目,目录条目中引用的每个 inode 都已分配,并确保整个层次 结构中没有目录的引用超过一次。
预写日志
更新磁盘时,在覆写结构之前,首先写下一点小注记(在磁盘上的其他地方,在一个众所周知的位置),描述接下来将要做的事情。写下这个注记就是“预写”部分, 把它写入一个结构,并组织成“日志”。因此,就有了预写日志。
数据日志
Linux ext3的一种模式,日志格式如下:
- TxB是事务开启块,存放着本次事务的metadata,包括TID,以及bit map和data block的写入地址等。
- 中间存放的是物理数据,所以也称为物理日志(physical logging),同样还有逻辑日志(logical logging),存放的是一些操作的描述。
- TxE是事务结束的标记,也会存一些matadata。
当日志成功的写入后,就可以对文件系统进行操作,也称为检查点设置(checkpointing),现在日志协议如下:
- 日志写入
- 检查点设置
现在需要考虑在写入日志时,发生崩溃的情况了,我们知道由于缓冲区的存在,日志块也是批量同时写入的,同时,磁盘内部是可以对写入顺序进行调整的,如果在日志写入过程中崩溃了,日志的状态有可能如下:
为了解决上面的问题,将第一步拆分为两步,现在的日志协议如下:
- 日志写入:将事务的内容(包括 TxB、元数据和数据)写入日志,等待这些写入完成。
- 日志提交:将事务提交块(包括 TxE)写入日志,等待写完成,事务被认为已提交 (committed)。
- 检查点设置:将更新内容(元数据和数据)写入其最终的磁盘位置。
强制写入磁盘:
- 禁用写缓冲
- 加入写屏障,能够保证在屏障之前的操作都已经刷到磁盘中了。
优化日志写入(Linux ext4):
将事务写入日志时,在开始和结束块中包含日志内容的校验和。这样做可以使文件系统立即写入整个事务,而不会产生等待。如果在恢复期间,文件系统发现计算的校验和与事务中存储的校验和不匹配,则可以断定在写入事务期间发生了崩溃,从而丢弃了文件系统更新。
恢复
在整个日志阶段,任何时候都有可能崩溃,在第二部提交之前的崩溃,我们无能为力。但是在提交之后的崩溃,可以按照以下的步骤进行恢复:
- 系统引导时,文件系统恢复过程将扫描日志,并查找已提交到磁盘的事务。
- 这些事务被重放(replayed,按顺序),文件系统再次尝试将事务中的块写入它们最终的磁盘位置。
我们也称其为重做日志(redo log)。
批处理
基于日志协议,每次的写入都需要大量的额外IO操作。为了解决这个问题,一些文件系统不会一次一个地向磁盘提交每个更新(例如,Linux ext3)。与此不同,可以将所有更新缓冲到全局事务中。
有限日志
当日志文件不断累加,必定会带来很多问题,所以日志文件系统将日志视为循环结构,所以也被称为循环日志(circular log),所以需要在检查点设置之后,释放日志空间。例如在日志超级块中标记最旧和最新的事务:
那么现在日志基本协议就变为四步:
- 日志写入:将事务的内容(包括 TxB 和更新内容)写入日志,等待这些写入完成。
- 日志提交:将事务提交块(包括 TxE)写入日志,等待写完成,事务被认为已提交 (committed)。
- 加检查点:将更新内容写入其最终的磁盘位置。
- 释放:一段时间后,通过更新日志超级块,在日志中标记该事务为空闲。
元数据日志
和数据日志类似,为了节省磁盘IO,我们把data block拿出,单独写入文件系统。有一点要注意在commit之前data block和元数据日志必须写入完成。
- 数据写入:将数据写入最终位置,等待完成(等待是可选的,详见下文)。
- 日志元数据写入:将开始块和元数据写入日志,等待写入完成。
- 日志提交:将事务提交块(包括 TxE)写入日志,等待写完成,现在认为事务(包 括数据)已提交(committed)。
- 加检查点元数据:将元数据更新的内容写入文件系统中的最终位置。
- 释放:稍后,在日志超级块中将事务标记为空闲。
通过先写数据,保证了inode不会指向垃圾块。如果数据放在日志提交后,发生崩溃后,就有可能使得inode指向垃圾块。
如果在第一步之后系统崩溃,那么分配的数据块就称为了垃圾,系统是如何回收的?
个人理解:不需要回收,默认当作空闲块处理。
日志结构文件系统(LFS)
通过观察发现,已构建的FS有以下特点:
- 内存大小不断增大:其中FS的性能更多的取决于写操作,读操作一般使用cache进行处理。
- 顺序IO的效率远远超于随机IO。
顺序写入
如果要达成顺序写入的目标,那么inode就不能放在一个固定的地方,而是应该和data block放在一起。
这时,还不是一个真正的顺序写入,因为第一次向地址A写入数据后,当第二次向A+1地址写入时,在第二次和第一次之间会存在时间间隔,而这时磁盘有可能已经旋转到其他扇区。
所以,这里还是要用到写缓冲(write buffer),将写入操作存在cache中,批量的将cache的操作刷入磁盘。
在这里将一次写入的大块更新称之为段(segment),下面是两次操作构成的段:
缓冲量
在这里简单的计算一下需要一次写入的量和磁盘写入贷款之间的关系:
设旋转和寻道的时间消耗为,写入数据块的时间是,磁盘峰值带宽是,有效带宽是写入数据,那么有:
所以有效带宽是:
同时有,F是当前带宽达到峰值带宽的比值:
$R_{e} = R_{p} * F $
联立两个式子,得:
假设,,,那么算出 ,也就是说需要一次写入9MB的数据才能满足90%的峰值带宽,99%就需要99MB。
查找inode
现在inode遍布在磁盘各处,怎么去找到它呢,在这里就引入了一个inode 映射(inode map,imap)的数据结构,k存的是inumber,v是inode的地址。一般是将imap放在inode的旁边
检查点区域
那么现在又要如何寻找imap呢?LFS在磁盘有一个固定的位置,称之为检查点区域(checkpoint region, CR)。它始终位于磁盘的开头,地址为 0。
检查点区域包含指向最新的 inode 映射片段的指针(即地址),因此可以通过首先读取 CR 来寻找到 imap。检查点区域仅定期更新(例如每 30s 左右),因此性能不会受到影响。
垃圾收集(GC)
第一种情况:
可以认为对文件进行了覆盖。
第二种:
可以认为对文件进行了追加写。
在LFS种,只对文件保存最新版本,因此LFS必须进行GC。LFS的GC也是以segment为基本单位。比如,对N个段进行GC,对其进行判断,打包出M个有效数据段,追加在磁盘中。很大程度上避免了外部空间碎片。
确认垃圾
为了确认data block是否是垃圾,需要对每一段额外保存一些metadata,LFS会在段头部保存一段段摘要块(segment summary block),保存了每一个数据块的inode号(即表明它属于哪个文件),偏移量(文件的哪一块)。
何时清理
清理策略有很多,定时,当存满时,空闲时间。
还有一种是将segment分为热和冷两类,热段表示经常发生覆盖,不应该去经常清理,冷段表示很少发生更改,更应该去清理。
崩溃恢复:
LFS 在日志(log)中组织这些写入,即指向头部段和尾部段的检查点区域, 并且每个段指向要写入的下一个段。LFS 还定期更新检查点区域(CR)。在这些操作期间都可能发生崩溃(写入段,写入 CR)。那么 LFS 在写入这些结构时如何处理崩溃?
第一种:
由于 LFS 每隔 30s 左右写入一次 CR,因此文件系统的最后一致快照可能很旧。因此,在重新启动时,LFS 可以通过简单地读取检查点区域、它指向的 imap 片段以及后续文件和目录,从而轻松地恢复。但是,最后许多秒的更新将会丢失。 为了改进这一点,LFS 尝试通过数据库社区中称为前滚(roll forward)的技术,重建其 中许多段。基本思想是从最后一个检查点区域开始,很到日志的结尾(包含在 CR 中),然 后使用它来读取下一个段,并查看其中是否有任何有效更新。如果有,LFS 会相应地更新文 件系统,从而恢复自上一个检查点以来写入的大部分数据和元数据。
第二种:
为了确保 CR 更新以原子方式发生,LFS 实际上保留了两个 CR,每个位于磁盘的一端,并交替写入它们。当使用最新的指向 inode 映射和其他信息的 指针更新 CR 时,LFS 还实现了一个谨慎的协议。具体来说,它首先写出一个头(带有时间 戳),然后写出 CR 的主体,然后最后写出最后一部分(也带有时间戳)。如果系统在 CR 更 新期间崩溃,LFS 可以通过查看一对不一致的时间戳来检测到这一点。LFS 将始终选择使用 具有一致时间戳的最新 CR,从而实现 CR 的一致更新。
LFS论文:LFS.pdf
LFS有关文章:Log-structured File System
brfs:
ZFS:
数据的完整性与保护
故障模式
故障模式通常分为两种:
- 潜在扇区错误(Latent-Sector Errors,LSE):由于外部因素导致磁盘物理层面收到损坏,驱动器使用磁盘内纠错码(Error Correcting Code,ECC)来确定块中的磁盘位是否良好。
- 块错误(block corruption):发生了文件系统不一致。
处理LSE
可以采用RAID技术,对数据进行冗余备份,在发生LSE错误时,可以使用镜像进行备份,重建该块。
处理块错误
现代存储系统用于保持数据完整性的主要机制称为校验算法(check algorithm)。常见的校验算法有:MD5,XOR,MOD,奇偶校验,校验和,CRC等。
CRC将数据块D视为一个大的二进制数并将其除以约定的值(k)。该除法的余数部分是 CRC 的值。
但是并没有完美的校验算法,因为所有的校验算法都会有碰撞发生,对于两个文件,有可能发生校验结果一致的情况。
校验信息布局
对于有些驱动制造商,会给每个data block额外保留8个字节用于存放校验信息:
没有预留空间的磁盘,将所有校验信息打包放置在一起:
很明显,第二种需要更多的IO操作,性能不如第一种。在读取数据块时,会计算当前数据块的校验信息,然后和存储的校验信息比对,判断数据是否有误。
错误写入
对于数据写入,如果期望将数据写入地址X,但是实际上写入了地址Y,当前的校验信息无法帮我们纠正,所以需要引入新的额外信息:物理标识符(Physical Identifier,物理 ID)。
丢失写入
当前的校验信息并不能确保对于丢失写入的判断,因为有可能旧块和要写入的块校验信息相同的情况。
较为传统的解决方法是写入验证(write verify),或写入后读取(read-after-write)。
磁盘检查
如果对于每一次的读取都要进行校验,就会占用大量的CPU资源,影响用户体验。许多系统采用了定期的磁盘检测(disk scrubbing),通过校验信息对磁盘进行全方位的扫描。
分布式系统
本节简单的介绍了一下分布式系统的和兴概念。
分布式系统主要关心的几个点:故障(failure),性能(performance),安全(security),以及通信(communication)。
通信基础
现代网络的核心原则是:通信基本是不可靠的,无论是由于信号影响,某些位错误(可以通过校验信息判断),还是丢包(重传机制),都有可能影响到消息的传递。
通信抽象
OS抽象
采用分布式共享内存(Distributed Shared Memory,DSM)使不同机器上的进程能够共享一个大的虚拟地址空间,但是故障处理很难搞。
RPC
使在远程机器上执行代码的过程像调用本地函数一样简单直接,这就是远程过程调用(RPC)。因此,对于客户端来说,进行一个过程调用,并在一段时间后返回结果。 服务器只是定义了一些它希望导出的例程。其余的由 RPC 系统处理,RPC 系统通常有两部 分:存根生成器(stub generator,有时称为协议编译器,protocol compiler)和运行时库(run-time library)。
Stub Generator:通过自动化生成代码,消除将函数参数和结果打包成消息的一些麻烦。
客户端的步骤:
- 创建消息缓冲区。消息缓冲区通常只是某种大小的连续字节数组。
- 将所需信息打包到消息缓冲区中。该信息包括要调用的函数的某种标识符,以及函数所需的所有参数。将所有这些信息放入单个连续缓冲区的过程,有时被称为参数的封送处理(marshaling)或消息的序列化(serialization)。
- 将消息发送到目标 RPC 服务器。与 RPC 服务器的通信,以及使其正常运行所需的所有细节,都由 RPC 运行时库处理。
- 等待回复。由于函数调用通常是同步的(synchronous),因此调用将等待其完成。
- 解包返回代码和其他参数。如果函数只返回一个返回码,那么这个过程很简单。 但是,较复杂的函数可能会返回更复杂的结果(例如,列表),因此存根可能也 需要对它们解包。此步骤也称为解封处理(unmarshaling)或反序列化 (deserialization)。
服务端的步骤:
- 解包消息。通过解封处理或反序列化, 将信息从传入消息中取出。提取函数标识符和参数。
- 调用实际函数。现在到了实际执行远程函数的地方。RPC 运行时调用 ID 指定的函数,并传入所需的参数。
- 打包结果。返回参数被封送处理,放入一个回复缓冲区。
- 发送回复。回复最终被发送给调用者。
在其中,通常会采用线程池(thread pool)这一并发技术,将线程池化,提高工作效率
Runtime library:在这里处理性能和可靠性的问题。
- 如何找到服务地址,也就是如何对服务命名,我们需要将数据包从系统中的任何其他机器路由到特定地址。
- 选择什么协议,不同的RPC框架会选择不同的消息协议,有HTTP,有TCP,UDP,也有基于二进制的协议。
- 其他的考虑:熔断,降级,重试,同步异步,双端的字节序不匹配(lib里面解决)等等...