在实现了 CURP 共识协议之后,一个关键问题是:如何验证实现的正确性? 分布式系统的测试比单机系统复杂得多,需要考虑网络延迟、节点崩溃、消息丢失等各种故障场景。本节将详细介绍 CURP 的测试框架设计。
1 为什么分布式系统测试这么难?
在讨论测试框架之前,让我们先理解分布式系统测试的挑战。
1.1 非确定性性行为
单机程序的执行通常是确定性的:给定相同的输入,输出也是相同的。但分布式系统不同:
1 | 场景:两个客户端同时写入相同 key |
挑战: 测试必须接受所有正确的执行顺序,不能只验证一种预期结果。
1.2 故障注入的复杂性
分布式系统必须正确处理各种故障:
| 故障类型 | 表现 | 测试难点 |
|---|---|---|
| 网络延迟 | 消息延迟到达 | 可能打乱消息顺序 |
| 网络分区 | 节点间无法通信 | 可能导致脑裂 |
| 消息丢失 | 消息永远不会到达 | 需要重传机制 |
| 节点崩溃 | 进程突然终止 | 需要恢复机制 |
| 崩溃恢复 | 节点重启后恢复 | 状态可能不一致 |
挑战: 这些故障可能以任意组合、任意顺序发生,穷举测试是不可能的。
1.3 并发问题难以复现
分布式系统的 bug 通常与特定的执行顺序相关:
1 | // 一个典型的竞态条件 |
挑战: 这类 bug 在正常测试中可能永远不会触发,只有在特定的并发场景下才会出现。
2 测试策略总览
针对上述挑战,我们采用多层测试策略:
1 | 测试金字塔(从下到上): |
2.1 单元测试
目标: 验证单个组件的正确性。
测试内容:
- Witness 的可交换性检查逻辑
- CurpMaster 的推测执行条件判断
- CurpClient 的并行发送逻辑
特点:
- 快速执行
- 隔离环境
- 易于定位问题
2.2 集成测试
目标: 验证多个组件的交互正确性。
测试内容:
- Client → Master → Witness 的完整写路径
- Master → Backup 的异步同步
- Master 崩溃后的恢复流程
特点:
- 使用模拟网络
- 可控的故障注入
- 验证组件协作
2.3 端到端测试
目标: 验证真实环境下的正确性。
测试内容:
- 长时间运行下的稳定性
- 真实网络延迟下的性能
- 各种故障场景的恢复
特点:
- 使用真实网络
- 长时间运行
- 接近生产环境
3 测试框架设计
为了支持上述测试策略,我们需要一个完整的测试框架。
3.1 框架架构
1 | ┌─────────────────────────────────────────────────────────────┐ |
3.2 核心组件
Network Simulator(网络模拟器)
职责: 模拟网络行为,包括延迟、丢包、分区。
1 | class NetworkSimulator { |
使用示例:
1 | TEST(CurpTest, NetworkLatency) { |
Node Controller(节点控制器)
职责: 控制节点的生命周期,模拟崩溃和恢复。
1 | class NodeController { |
关键区别:
stopNode:优雅关闭,数据完整保存crashNode:模拟崩溃,内存数据丢失,只有持久化的数据能恢复
1 | TEST(CurpTest, CrashRecovery) { |
Consistency Checker(一致性检查器)
职责: 验证操作的线性一致性。
1 | class ConsistencyChecker { |
3.3 线性一致性检查原理
线性一致性(Linearizability)是最强的一致性模型,要求:
所有操作看起来都在某个时间点原子执行,且操作的顺序与实时序一致。
检查方法
我们使用 Wing & Gong 算法 的简化版本:
- 记录所有操作:每个操作记录开始时间和结束时间
- 构建实时序图:
- 如果操作 A 的结束时间 < 操作 B 的开始时间,则 A → B
- 如果操作 A 和 B 作用于相同 key,且至少一个是写操作,则它们之间有顺序约束
- 检测环:如果图中存在环,则违反线性一致性
1 | bool ConsistencyChecker::checkLinearizability() { |
4 测试场景设计
4.1 单元测试
Witness 单元测试
1 | // test/dtest_witness.cpp |
CurpMaster 单元测试
1 | // test/dtest_curp_master.cpp |
4.2 集成测试
完整写路径测试
1 | // test/dtest_curp_integration.cpp |
4.3 故障注入测试
网络分区测试
1 | TEST(CurpFaultTest, NetworkPartition) { |
并发写入测试
1 | TEST(CurpConcurrencyTest, ConcurrentWrites) { |
5 性能测试
5.1 快速路径成功率
1 | TEST(CurpPerformanceTest, FastPathSuccessRate) { |
5.2 延迟测量
1 | TEST(CurpPerformanceTest, WriteLatency) { |
6 测试指标
| 指标 | 说明 | 目标值 |
|---|---|---|
| 快速路径成功率 | 1 RTT 完成的比例 | > 90% |
| 写延迟 P50 | 中位数写延迟 | < 5ms(同数据中心) |
| 写延迟 P99 | 99% 写延迟 | < 20ms |
| 恢复时间 | 崩溃后恢复时间 | < 1s |
| 数据丢失率 | 故障后数据丢失比例 | 0% |
| 线性一致性违反 | 检测到的不一致次数 | 0 |
7 持续集成
建议使用 GitHub Actions 进行持续集成测试:
1 | # .github/workflows/test.yml |
8 总结
分布式系统测试是一个复杂但必要的任务。通过本文介绍的测试框架:
- Network Simulator:模拟网络行为,验证系统在各种网络条件下的正确性
- Node Controller:控制节点生命周期,测试崩溃恢复
- Consistency Checker:验证线性一致性
我们可以系统性地验证 CURP 实现的正确性和性能。
测试不是一次性的工作,而是持续的过程。 每次修改代码都应该运行完整的测试套件,确保没有引入新的 bug。
参考资料
- Linearizability: A Correctness Condition for Concurrent Objects, Herlihy & Wing, 1990
- Testing Distributed Systems for Linearizability, aphyr.com
- Jepsen: Calls Out Distributed Systems on Their Claims, jepsen.io