WalShadow 选择了一条少见而大胆的复制路径:不使用 PostgreSQL 逻辑解码,而是直接读取流复制副本接收的物理 WAL 字节,自行解析 heap 记录,再写成 ClickHouse 原生数据块。
这不只是换了一个数据接口。它意味着复制工具需要理解 PostgreSQL 存储层的语言,同时还要把面向事务恢复的日志,转换成适合列式分析的批量数据。潜在收益很直接,但工程边界也比普通 CDC 更锋利。
物理 WAL 与逻辑变更流不是同一种抽象
典型的 PostgreSQL CDC 链路依赖逻辑解码:数据库先把 WAL 解释为较稳定的行级事件,例如某张表插入了一行、更新了哪些列,或者删除了哪个键。下游消费的是接近业务语义的变更流。
WalShadow 则站在更低的一层。物理 WAL 的首要目的,是让 PostgreSQL 能够恢复页面并维护物理副本,而不是给外部系统提供结构稳定的事件 API。因此,读取这些字节的程序不能只认识 INSERT、UPDATE 和 DELETE,还需要面对存储层细节,例如:
- relation、文件节点与表对象之间的映射;
- heap tuple 的物理布局和可见性信息;
- HOT update、TOAST 值和 full-page image;
- 事务提交、回滚以及跨记录重组;
- checkpoint、崩溃续传和 WAL 回收;
- DDL 或 PostgreSQL 版本变化带来的格式兼容问题。
这也是这条路线“勇敢”的地方:逻辑解码替调用方吸收了许多内部复杂度,而直接解析物理 WAL,相当于主动接管这部分责任。
为什么还要走这条路
低层接口并非只有成本。绕过逻辑解码后,复制程序可以更直接地消费与物理副本相同的字节流,并按照 ClickHouse 的写入方式组织数据。
ClickHouse 更喜欢成批写入的列式数据,而不是逐行执行事务型更新。WalShadow 将解析结果写成 ClickHouse-native blocks,意味着它可以在转换阶段积累一批记录,按列组织并一次提交,从而减少大量小请求和逐行序列化的开销。
不过,“能解析 WAL”不等于“已经获得正确的分析表”。实际系统仍需明确以下语义:
- 更新如何表示:在 ClickHouse 中追加新版本,还是转换成某种可合并结构?
- 删除如何传播:记录 tombstone、维护符号列,还是执行异步 mutation?
- 事务边界是否保留:同一事务中的多表变更何时对分析端可见?
- 重复投递如何处理:进程在写入成功但尚未记录进度时崩溃,会不会重复写入?
- DDL 如何同步:源表新增列、修改类型或重建表后,下游映射是否仍然有效?
这些决策通常比“每秒能解析多少 WAL”更影响生产可用性。
动手观察 WalShadow 所面对的字节流
下面的实验不安装或模拟 WalShadow,也不代表它的真实配置格式;它只使用 PostgreSQL 自带的 pg_receivewal,展示物理 WAL 消费者看到的入口。假设本机已经安装 Docker,可直接复制运行:
# 启动一个临时 PostgreSQL 16 实例
docker rm -f pg-wal-lab 2>/dev/null || true
docker run -d \
--name pg-wal-lab \
-e POSTGRES_PASSWORD=postgres \
-p 55432:5432 \
postgres:16
# 等待数据库就绪
until docker exec pg-wal-lab pg_isready -U postgres >/dev/null 2>&1; do
sleep 1
done
# 创建测试表和一个物理复制槽
docker exec -i pg-wal-lab psql -U postgres <<'SQL'
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
event_type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
SELECT * FROM pg_create_physical_replication_slot('walshadow_lab');
SQL
# 在容器内启动物理 WAL 接收器;它会像流复制客户端一样接收 WAL 段
docker exec -d -u postgres pg-wal-lab bash -lc '
rm -rf /tmp/wal-copy && mkdir -p /tmp/wal-copy
exec pg_receivewal \
--directory=/tmp/wal-copy \
--slot=walshadow_lab \
--verbose
'
# 生成插入、更新和删除记录
docker exec -i pg-wal-lab psql -U postgres <<'SQL'
INSERT INTO events (event_type, payload)
VALUES
('signup', '{"user_id": 101}'),
('purchase', '{"user_id": 101, "amount": 42.50}');
UPDATE events
SET payload = payload || '{"source": "newsletter"}'::jsonb
WHERE event_type = 'signup';
DELETE FROM events WHERE event_type = 'purchase';
SELECT pg_switch_wal();
SQL
sleep 2
echo 'Received WAL files:'
docker exec pg-wal-lab bash -lc 'ls -lh /tmp/wal-copy'
实验输出只是 WAL 段文件,不会出现可读的 JSON 变更事件。这正是物理 WAL 解析器与普通逻辑 CDC 消费者的根本区别:前者必须从恢复日志中重建行级含义。
测试结束后可清理环境:
docker rm -f pg-wal-lab
上线前应重点验证什么
如果要评估 WalShadow,建议不要只做一次全量导入和吞吐测试。准备一张包含普通列、宽字段、JSON、可空字段和主键的测试表,然后覆盖以下场景:
- 单事务内连续多次更新同一行;
- 插入后回滚,以及保存点回滚;
- 大字段触发 TOAST 后再更新;
- 删除后使用同一个业务键重新插入;
ALTER TABLE新增列或修改默认值;- PostgreSQL 重启、网络中断和消费者重启;
- ClickHouse 写入成功后,在复制进度落盘前强制终止进程;
- 长时间暂停消费者,观察复制槽造成的 WAL 保留。
可以用下面的 SQL 监控物理复制槽是否活跃,以及它保留了多少 WAL。将槽名改成实际部署使用的名称:
SELECT
slot_name,
slot_type,
active,
restart_lsn,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)::bigint
) AS retained_wal
FROM pg_replication_slots
WHERE slot_name = 'walshadow_lab'
AND restart_lsn IS NOT NULL;
复制槽能保护尚未消费的 WAL,但也可能在消费者停机时持续占用磁盘。生产环境应同时设置磁盘告警、复制延迟告警和故障处置流程,不能把槽本身当成无限容量的消息队列。
采用建议:把兼容性和正确性放在吞吐量前面
WalShadow 的吸引力在于它把 PostgreSQL 的底层变更流直接连接到 ClickHouse 的原生批量写入模型。但越靠近存储层,接口越可能受到数据库内部实现与版本变化影响。
评估时至少确认四件事:
- 当前使用的 PostgreSQL 主版本是否被明确支持;
- UPDATE、DELETE、回滚和 DDL 的映射语义是否符合业务需求;
- 复制进度与 ClickHouse 写入之间如何实现幂等或故障恢复;
- 消费者长时间不可用时,如何限制 WAL 保留并安全重建同步。
对追求高吞吐分析复制、并有能力进行严格版本验证的团队,这是一条值得测试的路径。对更重视稳定逻辑协议、跨版本兼容和成熟生态的系统,逻辑解码仍然是更保守的默认选择。真正的判断标准不是方案够不够新,而是故障发生后,团队能否证明 ClickHouse 中的数据仍然完整、可解释并且可以恢复。