基于分段确定性模型的Ada 95分区透明复制容错方法
1. 分段确定性执行与线性化
在分布式系统中,为了保证系统的正确性和一致性,我们假设所有全局数据都封装在受保护对象中。由于受保护对象通过互斥机制能轻松实现线性化,且线性化是一种局部属性,即不同对象的线性化执行历史组合起来仍能得到线性化历史,所以整个分区的执行是线性化的。在正确的Ada 95分区中,分段确定性状态区间的任何交错都会产生全局线性化历史。
2. 事件分类
执行历史由事件序列来表征,事件描述了确定性状态区间的顺序。事件可分为外部事件和内部事件:
-
外部事件
:分区与系统其他部分交互时发生,体现了通信支持中的非确定性。具体包括:
- 其他分区发送的RPC请求消息的传递。
- 向调用分区发送RPC应答消息以返回远程调用结果。
- 向其他分区发送RPC请求消息。
- 接收其他分区的RPC应答消息。
-
内部事件
:分区内部的非确定性来源,主要涉及任务系统的非确定性,涵盖所有信号操作。具体如下:
- 选择语句中接受哪个入口的决策。
- 定时和条件入口调用的结果。
- 进入和离开受保护操作,特别是锁定和解锁受保护对象。
- 入口调用的排队和重新排队。
- 任务的创建、终止和中止。
- 异步选择语句中可中止部分的中止。
- 受控对象的初始化、终结和赋值。
此外,还有一类特殊的内部事件,即对结果依赖于应用程序外部状态的子程序的本地调用,例如标准包Ada.Calendar中的Clock函数,它返回“挂钟”时间的近似值,这类调用及其结果也应视为内部事件。
下面用表格总结事件分类:
| 事件类型 | 具体事件 |
| ---- | ---- |
| 外部事件 | 其他分区RPC请求消息传递、向调用分区发送RPC应答消息、向其他分区发送RPC请求消息、接收其他分区RPC应答消息 |
| 内部事件 | 选择语句入口决策、定时和条件入口调用结果、受保护操作进出、入口调用排队和重新排队、任务创建/终止/中止、异步选择语句可中止部分中止、受控对象初始化/终结/赋值、依赖外部状态的子程序本地调用 |
3. 半主动复制
分段确定性模型非常适合用于协调副本的半主动复制。在半主动复制中,副本组织成一个组,所有副本都会执行传入的RPC请求。其中一个副本被指定为领导者,负责做出所有非确定性决策,并将这些决策传播给其他跟随者,跟随者必须做出相同的选择。
通过在领导者上记录事件,可以实现副本的一致性。领导者使用FIFO有序的可靠多播将事件发送给跟随者,跟随者则按照领导者上事件发生的顺序重放这些事件。这样,所有副本将经历相同的执行历史,即相同的确定性状态区间序列,从而确保状态访问在所有副本上按相同顺序进行。
下面是半主动复制的mermaid流程图:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(领导者):::process -->|记录事件| B(事件日志):::process
B -->|多播事件日志| C(跟随者1):::process
B -->|多播事件日志| D(跟随者2):::process
C -->|重放事件| E(执行历史):::process
D -->|重放事件| E
4. 正确性保证
这种方法的正确性依赖于Ada 95编程语言的语义。信号模型确保所有状态访问都由事件分隔,并发生在不同的确定性状态区间,因为语言标准中定义的所有信号操作都包含在内部事件集合中。因此,对状态区间进行排序(通过对事件排序)也能对对象的并发访问进行排序。
在异步选择语句中,中止操作可能会导致副本状态出现分歧。例如,下面的代码片段:
task body T is
X : Integer := 0;
begin
...
select
Trigger.Event;
then abort
loop
X := X + 1;
exit when ...;
end loop;
end select;
-- do something with X
end T;
如果在执行过程中,可中止部分的中止操作不是在精确相同的逻辑时刻发生,可能会导致问题。如果赋值操作被中止,目标对象可能会变得异常,使用异常对象的值在语言标准中被视为错误。因此,可中止部分的关键部分必须实现为中止延迟区域,以确保状态的一致性。语言定义了多种延迟中止的构造,如受保护操作以及受控对象的初始化、终结和赋值不能被中止,中止操作只有在中止延迟构造完成后才会发生。通过在跟随者上重放事件日志,可以保证中止操作在与领导者相同的两个中止延迟区域之间发生,从而维护Ada 95的语义。
5. 副本同步:可观察事件
如果在领导者每次发生事件时都对跟随者进行同步,会带来过高的性能开销,因为内部事件发生得非常频繁。实际上,只要状态区间的影响仅局限于领导者本地,跟随者无需了解这些事件。只有当影响对系统其他部分可观察时,才需要同步跟随者。
可观察事件是指领导者向系统其他分区发送信息(或输出内容)的事件,包括:
- 向客户端发送RPC应答消息。
- 向其他分区发送RPC请求消息。
在可观察事件之间,领导者将外部和内部事件的发生情况记录在事件日志中。在可观察事件即将发生之前,领导者将事件日志多播给跟随者,然后再执行可观察事件。我们将事件日志描述的执行路径称为扩展状态区间,每个可观察事件都会开启一个新的扩展状态区间。
跟随者接收到事件日志后,会重放记录的事件结果,从而重现扩展状态区间,但不会重放初始的可观察事件,因为领导者已经执行过该事件。当跟随者遇到不在日志中的事件时,表明整个日志已重放完毕,它会阻塞直到收到领导者的下一个事件日志,或者由于领导者故障而成为新的领导者。
领导者可以在执行过程中定义额外的点来开启新的扩展状态区间,例如当事件日志缓冲区即将溢出时。这不会改变方法的正确性,但会使领导者和跟随者之间的同步更加紧密。
下面用列表总结副本同步的步骤:
1. 领导者在可观察事件之间记录事件到事件日志。
2. 可观察事件即将发生时,领导者多播事件日志给跟随者。
3. 跟随者重放事件日志,重现扩展状态区间。
4. 跟随者遇到非日志事件时阻塞,等待新的事件日志或成为新领导者。
6. 故障处理
6.1 跟随者故障
跟随者的故障是完全透明的。因为跟随者除了在副本组内,不会与系统的其他部分进行交互,所以其故障不会对分布式应用程序产生任何影响。当跟随者发生故障时,只需将其从组中排除即可。
6.2 领导者故障
当领导者发生故障时,需要从跟随者中选举出新的领导者(可以使用任何选举算法,如恶霸算法)。新领导者将从原领导者最后一次与跟随者通信的点开始恢复执行。具体步骤如下:
1. 重新执行事件日志中未处理的待处理事件。
2. 当事件日志处理完毕后,新领导者继续正常执行,记录事件并在每个可观察事件之前将事件日志发送给剩余的跟随者。
尽管原领导者在发送最后一次事件日志后可能已经有了进一步的执行,但由于同步必须在任何可观察事件之前进行,所以原领导者的任何操作都不会影响系统。这类似于数据库中的预写日志,在更新状态之前,必须将用于撤销或重做操作的所有必要信息记录在日志中。
下面通过一个示例来说明领导者故障的处理:
领导者L开始执行请求reqA,向另一个分区P发起嵌套远程调用reqB,等待结果,然后继续处理reqA,最后发生故障。在执行reqB(可观察事件)之前,它将扩展状态区间S1的事件日志发送给跟随者F1和F2,跟随者重放日志以执行S1。当L发生故障时,视图发生变化,跟随者F1成为新的领导者,并从S1之后继续执行。
需要注意的是,扩展状态区间S3可能与S2不同(除了初始可观察事件reqB是相同的),但由于S2不会影响系统的其他部分,所以应用程序的整体状态仍然是一致的。
如果通信是通过可靠多播进行的,并且回复repB已经到达,那么F1在S3开始时不需要重新执行reqB。但如果在repB到达之前发生了视图变化,新领导者F1必须重新执行任何可观察事件,除非它能推断出该事件已经由原领导者执行过。
此外,分区必须能够处理重复的消息,包括传入的RPC请求和应答消息。消息必须带有唯一的系统范围标识符,重复的应答消息将被忽略,重复的请求消息只会执行第一次,并且需要保留结果以确保后续重复请求返回相同的结果。为了避免内存泄漏,需要对保留的结果进行垃圾回收,可以通过在每个消息中附带发送者已经从接收者处获得的RPC应答信息,让接收者丢弃这些保留的应答。
下面用mermaid流程图展示领导者故障处理流程:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(领导者故障):::process --> B(选举新领导者):::process
B --> C(新领导者从最后通信点开始):::process
C --> D(重放待处理事件日志):::process
D -->|日志处理完毕| E(继续执行并记录事件):::process
E -->|可观察事件前| F(发送事件日志给跟随者):::process
F --> E
7. 实现方案
7.1 RAPIDS概述
RAPIDS是基于分段确定性模型和半主动复制方案的实现,适用于GNAT编译器。它作为运行时支持的一部分集成在PCS中,对应用程序来说大部分是透明的。
7.2 RAPIDS的接口
RAPIDS包含三个接口:
-
内部接口
:供Garlic(GNAT的PCS)使用,用于记录和重放外部事件。
-
回调接口
:由GNAT的任务支持GNARL使用,用于记录和重放内部事件。但需要注意的是,RAPIDS只记录应用程序任务的事件,而不记录PCS内部使用的任务的事件。
-
系统子包接口
:供标准库使用,例如Ada.Calendar.Clock或文件访问等操作可能需要记录和重放事件。
7.3 相关修改
Garlic进行了修改,在所有消息中包含唯一的消息标识符,并实现了重复消息检测功能。同时,添加了新的协议来支持必要的组通信,该协议目前只是与第三方视图同步组通信系统Phoenix的接口。
GNARL也进行了修改,添加了回调到RAPIDS的功能,用于内部事件的记录和重放。
下面用表格总结RAPIDS的接口信息:
| 接口类型 | 使用方 | 功能 |
| ---- | ---- | ---- |
| 内部接口 | Garlic | 记录和重放外部事件 |
| 回调接口 | GNARL | 记录和重放内部事件(仅应用程序任务) |
| 系统子包接口 | 标准库 | 记录和重放相关操作事件 |
8. 总结
通过基于分段确定性计算模型和半主动复制的方法,可以实现Ada 95分区的透明复制,以达到容错的目的。该方法能够为异构分布式系统中的任意分区提供k - 弹性。
目前,RAPIDS实现仍处于原型阶段,需要进行大量的优化工作。此外,它还不能处理通过远程访问子程序或远程访问类宽值进行的动态绑定远程调用,并且尚未处理受控类型赋值产生的事件。未来,随着对这些问题的解决和进一步的优化,该方法有望在分布式系统中得到更广泛的应用。