decoderbufs 是 PostgreSQL 逻辑复制生态中的一个输出插件,它负责把逻辑解码得到的行级变更序列化为 Protocol Buffers 格式。这意味着下游消费者拿到的不是 pgoutput 那种私有二进制流,也不是 wal2json 那样的文本 JSON,而是带字段描述、类型信息并且体积更紧凑的 Protobuf 消息。对于正在构建 CDC 管道、事件驱动架构或者需要把数据库变更推送到 Kafka 的团队来说,decoderbufs 提供了一种类型安全且跨语言的传输中间层。

使用 decoderbufs 之前,需要弄清楚它和 pgoutput 最本质的区别。pgoutput 是 PostgreSQL 内置的输出插件,虽然也是二进制,但它的消息格式没有公开的 schema,解析必须依赖 PostgreSQL 服务端提供的头文件;而 decoderbufs 的 proto 文件是公开的,无论下游用 Go、Java 还是 Python,只要拿到对应的 proto 生成代码,就能直接反序列化,不用关心 PostgreSQL 版本的内部细节。这个特性让它特别适合多语言消费端。
一、安装与编译
decoderbufs 需要从源码编译,因为它依赖 PostgreSQL 的服务端头文件以及 protobuf-c 运行时。先确认服务器上已经安装了 postgresql-server-dev 对应版本的包和 protobuf-c 开发库。以 Ubuntu 为例,如果使用 PostgreSQL 16,需要安装 postgresql-server-dev-16 和 libprotobuf-c-dev。如果缺少这些依赖,make 阶段会报找不到头文件的错误,这时不要急着改 Makefile,先补齐开发包即可。
编译步骤并不复杂,执行 git clone 获取源码后进入目录执行 make 和 make install。源码仓库地址可以在 Debezium 的 GitHub 组织下找到。编译完成后,decoderbufs.so 会被复制到 PostgreSQL 的 pkglibdir 目录,使用 pg_config 可以确认这个目录的位置。之后修改 postgresql.conf,把 wal_level 设置为 logical,同时适当调大 max_replication_slots 和 max_wal_senders,因为每个逻辑复制槽需要一个 WAL sender 进程。
git clone https://github.com/debezium/postgres-decoderbufs.git cd postgres-decoderbufs make make install
完成安装后,重启 PostgreSQL 让参数生效,然后通过 SQL 创建一个使用 decoderbufs 的复制槽。如果一切正常,创建语句不会报错;如果提示找不到输出插件,说明 decoderbufs.so 没有安装到正确位置,或者共享库路径没有被 PostgreSQL 识别。此时可以检查 pg_config 输出的 pkglibdir 和实际文件是否匹配,通常问题都出在开发包版本与 PostgreSQL 主版本不一致。
二、核心消息结构与字段映射
decoderbufs 输出的 Protobuf 消息不是一条一条孤立发送,而是以事务为单位进行组织。每个事务开始时先发送一条 TransactionMessage,里面包含事务 ID 和提交时间,随后是若干条 RowMessage,最后在事务提交时再给出事务结束标记。这种设计可以让订阅端轻松实现按事务聚合,避免在消费过程中把未提交的数据提前对外暴露。
RowMessage 是核心载体,它至少包含表名、操作类型以及新旧两个元组。操作类型通常有 INSERT、UPDATE、DELETE 和 TRUNCATE 几种。对于 UPDATE 操作,old_tuple 和 new_tuple 会同时存在,分别保存修改前后的字段值;对于 DELETE 只存在 old_tuple;INSERT 只存在 new_tuple。字段值不是字符串,而是 bytes 类型,同时每个字段会携带列名、PostgreSQL 类型 OID 和是否为 NULL 的标志。这个设计对下游解析非常重要,因为仅靠二进制内容无法判断一个空字节数组到底是空字符串还是 NULL。
message RowMessage {
optional uint32 transaction_id = 1;
optional string table = 2;
optional string op = 3;
optional Tuple old_tuple = 4;
optional Tuple new_tuple = 5;
}
message Tuple {
repeated Column columns = 1;
}
message Column {
optional string name = 1;
optional string type = 2;
optional bytes value = 3;
optional bool is_null = 4;
}
上面的结构是简化后的示意,实际 proto 文件中还包含事务级别和表过滤等辅助消息。需要注意的是,decoderbufs 并不会把 PostgreSQL 的 numeric、timestamp 等类型转换为人类可读文本,而是保留原始二进制或字符串表示,具体行为取决于列类型。例如 integer 会编码为 4 字节大端整数,text 则直接使用 UTF-8 字节序列。因此消费端如果不能正确理解类型信息,就会把数值解析成乱码。这也是为什么 decoderbufs 的商业价值之一就在于它保留了列类型字段。
三、通过 pg_recvlogical 消费验证
要快速观察 decoderbufs 的输出内容,最方便的方式是使用 PostgreSQL 自带的 pg_recvlogical 工具。它会从复制槽读取逻辑变更并写入文件,不需要额外编写客户端程序。先创建一个复制槽,然后在一个新的终端中启动 pg_recvlogical 监听并指定输出文件,接着对测试表进行插入和更新操作,最后停止工具并用十六进制查看器或 Protobuf 解析工具检查内容。
SELECT * FROM pg_create_logical_replication_slot('dec_slot', 'decoderbufs');
创建复制槽后,执行如下命令开始抓取变更。参数中的 -d 指定数据库,-S 指定复制槽名称,-f 指定输出文件,-v 表示输出详细信息。pg_recvlogical 会持续运行,直到收到停止信号或数据库连接断开。为了获得完整事务,测试时可以手动执行 BEGIN 和 COMMIT 包裹多条 DML 语句,这样在输出文件中可以看到完整的事务边界。
pg_recvlogical -d testdb -S dec_slot -f changes.bin -v
抓取到的 changes.bin 是二进制 Protobuf 数据,直接 cat 会显示乱码。我们可以使用 protoc 配合 decoderbufs 的 proto 文件生成例如 Go 或 Python 的结构体,然后编写一个极简的反序列化程序读取该文件。由于 decoderbufs 输出的是连续的 Protobuf 消息,且每条消息之间没有明确的长度前缀,因此直接一次性反序列化会失败。正确做法是使用 Decoder 接口逐条读取,或者根据消息类型做流式切分。实际生产环境中,Kafka Connect 的 Debezium connector 已经处理了这些细节,我们只需关注消息体本身。
四、性能、NULL 与 TOAST 列注意事项
decoderbufs 的二进制特性在大事务、宽表场景下比 JSON 输出插件有明显优势。JSON 会因为字段名重复和文本数字表达而膨胀,而 Protobuf 通过字段编号和 varint 编码减小体积。不过体积优势并不是免费的,decoderbufs 依赖 protobuf-c 在服务端进行编码,相比 pgoutput 的内置实现会多一些 CPU 开销。对于写入并不极端频繁的数据库,这个开销通常可以忽略;但如果单库 TPS 很高,建议先在测试环境用实际负载对比 pgoutput 和 decoderbufs 的 CPU 与磁盘占用,再决定是否在生产启用。
NULL 值处理方面,decoderbufs 为每个列都提供了 is_null 标志,消费端不能因为 value 为空就跳过该列,因为空字节数组和 NULL 在 PostgreSQL 中是两个概念。另一个容易忽略的问题是 TOAST 列。PostgreSQL 对大字段会启用 TOAST 机制,逻辑解码时默认不会把完整的 TOAST 数据包含在变更流中,除非在创建复制槽时调整选项或者下游 Connector 配置了 include-toast 之类的能力。decoderbufs 本身并没有偏离 PostgreSQL 的逻辑解码规则,遇到 large text 或 bytea 列时,如果发现 value 缺失,应该检查 TOAST 设置。
复制槽管理同样不能忽视。逻辑复制槽会阻止 WAL 过早清理,如果下游消费者长时间停止,槽对应的 WAL 会不断累积,最终填满磁盘。因此在使用 decoderbufs 的系统中,务必配置监控告警,定期清理不再需要的复制槽。删除槽时也要注意先在消费端停止任务,否则会看到连接异常或数据丢失。
decoderbufsProtobuf格式PostgreSQL逻辑解码修改时间:2026-09-25 23:14:15