在移动云大云海山数据库的存算分离架构中,计算节点与共享存储的解耦带来了显著的扩展性和成本优势,但也引入了数据一致性的新挑战。作为数据库内核开发者,我们经常遇到这样的场景:当计算节点的本地缓存未命中时,需要从共享存储获取数据页,但此时无法确保获取的页版本与计算节点当前事务视图的一致性。
这种架构下主要存在两类数据一致性问题:
传统单机数据库通过缓冲区管理器和WAL日志保证一致性,但在存算分离架构中,这些机制需要重新设计。我们的解决方案是通过在Redis中维护页面校验和(checksum)的全局视图,建立跨节点的数据一致性基准。
关键设计原则:校验操作必须满足ACID特性中的隔离性——校验过程本身不能影响正常查询性能,校验失败必须立即终止可能引发数据错误的事务。
整个校验框架由三个关键组件构成环形验证链:
code复制计算节点 → 共享存储 → Redis校验服务 → 计算节点
| 组件 | 职责 | 关键指标 |
|---|---|---|
| 计算节点 | 生成/校验checksum | 校验延迟<5ms |
| 共享存储(DS) | 提供数据页持久化 | 99.9%可用性 |
| Redis集群 | 维护checksum全局视图 | 读写吞吐>10万QPS |
写路径校验:
读路径校验:
mermaid复制sequenceDiagram
计算节点->>共享存储: 1. 读取数据页
计算节点->>Redis: 2. 获取预期checksum
alt 校验通过
计算节点->>应用: 3. 返回合规数据
else 校验失败
计算节点->>系统: 4. 触发panic保护
end
针对checksum存储的特殊性,我们做了以下优化:
dbId_spcId_relId_forkNum_blockNum的复合键结构,确保全局唯一性c复制// Key生成算法示例
char* GeneratePageKey(Oid dbNode, Oid spcNode, Oid relNode, ForkNumber forknum, BlockNumber blocknum) {
char *key = palloc0(MAX_KEY_LENGTH);
snprintf(key, MAX_KEY_LENGTH, "%u_%u_%u_%d_%u",
dbNode, spcNode, relNode, forknum, blocknum);
return key;
}
采用改进的Fletcher-32算法,相比PostgreSQL原生实现有以下增强:
c复制uint32 pg_checksum_block(const PGChecksummablePage *page) {
uint32 sums[N_SUMS] = {0};
uint32 result = 0;
// 元数据校验区
CHECKSUM_COMP(sums[0], page->pd_lsn);
CHECKSUM_COMP(sums[1], page->pd_checksum);
// 数据内容校验区
for (int i = 0; i < BLCKSZ/sizeof(uint32); i++) {
CHECKSUM_COMP(sums[i%N_SUMS], page->data[i]);
}
// 时间指纹校验
uint32 timestamp = (uint32)time(NULL);
CHECKSUM_COMP(sums[N_SUMS-1], timestamp);
for (int i = 0; i < N_SUMS; i++)
result ^= sums[i];
return result;
}
针对不同索引类型的页结构差异,设计动态掩码机制:
| 索引类型 | 掩码策略 | 特殊处理 |
|---|---|---|
| B-Tree | 固定头部16字节 | 忽略分裂状态标记 |
| Hash | 屏蔽桶指针 | 校验溢出页链 |
| GiST | 保留路径标记 | 过滤删除标记 |
| GIN | 跳过posting list | 校验pending list |
c复制// B-Tree页校验示例
void btree_mask(Page page) {
BTPageOpaque opaque = (BTPageOpaque) PageGetSpecialPointer(page);
mask_page_lsn_and_checksum(page);
if (P_ISDELETED(opaque)) {
mask_page_content(page); // 已删除页只校验元数据
} else {
mask_unused_space(page);
}
}
批量校验:对连续块号实施批量checksum校验,减少Redis往返次数
sql复制-- 批量获取checksum的Redis命令
MGET page_1_1663_24575_0_0 page_1_1663_24575_0_1 ... page_1_1663_24575_0_63
热点页缓存:在计算节点本地维护高频访问页的checksum缓存
异步校验:对只读查询启用延迟校验模式
c复制if (IsReadOnlyTransaction()) {
EnableLazyChecksum();
}
我们建立了分级响应策略:
| 错误类型 | 响应措施 | 恢复方案 |
|---|---|---|
| 校验失败 | 立即panic | 重启后从备节点恢复 |
| Redis超时 | 重试3次 | 切换Redis副本 |
| 版本冲突 | 触发页修复 | 从WAL重建数据页 |
典型的页修复流程:
pg_checksum_error系统表在TPC-C基准测试中,不同负载下的性能表现:
| 负载等级 | 吞吐量下降 | 平均延迟增加 |
|---|---|---|
| 1000 TPS | <2% | 0.5ms |
| 5000 TPS | 5-8% | 1.2ms |
| 10000 TPS | 10-15% | 2.8ms |
共享存储缓存污染:
tenant_id维度增强校验key网络分区引发的版本分裂:
python复制# 模拟网络分区的测试用例
def test_network_partition():
with db.transaction(): # 事务T1
update_page(block_id=42)
network.drop_packets() # 模拟网络中断
assert read_page(block_id=42).checksum == get_redis_checksum(42) # 触发panic
索引页逻辑损坏:
在实际运维中我们积累了几个关键改进点:
一个正在试验中的优化是选择性校验:
sql复制-- 通过页面访问模式决定校验强度
CREATE POLICY checksum_policy ON ALL TABLES
USING (access_mode IN ('critical', 'normal'))
WITH (check_frequency = CASE
WHEN access_mode = 'critical' THEN 'always'
ELSE 'lazy'
END);
这套框架上线后,将数据一致性事故的平均修复时间(MTTR)从小时级降低到分钟级。对于数据库内核开发者而言,最重要的经验是:在存算分离架构中,必须建立贯穿整个IO路径的验证机制,才能确保分布式环境下的数据正确性。