TT Lab
开始
学习 学习路径 课程

构建 EAI 中间层

变更早已在日志里

在 TT Lab 中继续学习

一句话总结

CDC(Change Data Capture)是这样一种对接方式:不等应用程序发送报文,而是读取数据库中已经发生的变更,并把它们传给其他系统。最可靠的办法是读取数据库的事务日志,而作为代价,会产生日志保留、schema 变更、顺序之类新的运维责任。

为什么需要它

到目前为止,本课程中的对接全都是发送方主动发送的结构。账户系统把处理结果交给枢纽,枢纽再搬运到信息系统和外部系统。然而现场有许多系统无法这样对接。无法修改源码的套装软件、二十年前的账本批处理、几十个画面直接修改同一张表的遗留系统,都是如此。“每当发生变更就发一个报文给我们”这个要求,意味着要找到该系统的所有写入点并加以修改,只要漏掉一处,数据就会悄悄地出现偏差。

第二个理由是双写。应用程序写入数据库之后,又向 MQ 发布,那么在二者之间进程一旦死掉,就会出现数据库里有而 MQ 里没有的变更。把两个资源绑成一个事务的分布式事务很重,而且大多数 Broker 不支持。如果只让写入数据库这一件事作为事务,而发布是读取数据库已经确定的变更之后再进行,这个缝隙就消失了。CDC 就是那个读取的一方。

工作原理

捕获变更的方法大致有三种。

方式 原理 代价
查询(轮询) 用 updated_at > 마지막 시각(韩文,意为“最后时刻”)定期读取 看不到删除。会漏掉相同时刻的变更或较晚提交的事务。对源数据库有查询负载
触发器 表上的触发器向变更历史表逐行写入 每次写入都有触发器成本。必须在源数据库中植入触发器
基于日志 读取数据库的事务日志(PostgreSQL WAL、MySQL binlog) 必须运维日志保留、权限和版本兼容

基于日志之所以最可靠,是因为读取的是数据库已经按提交顺序记录下来的内容。无论应用程序代码通过什么路径写入,无论是删除还是批量修改,全都会留在日志里。

具有代表性的开源实现是 Debezium。PostgreSQL 连接器通过逻辑解码(logical decoding)读取 WAL。输出插件使用 PostgreSQL 10 及以上版本默认自带的 pgoutput,或由 Debezium 维护的 decoderbufs,而要使用逻辑解码,源数据库的 wal_level 必须是 logical(PostgreSQL 文档)。MySQL 连接器读取 binlog,把行级别的 INSERT、UPDATE、DELETE 生成为事件(Debezium MySQL)。

第一次是快照,之后是流式。日志不会永远保留,所以连接器在第一次接入时,会以一致的时间点读取整张表(快照)并作为事件输出,然后从那个时间点的日志位置开始继续传送变更。文档说明,由于从快照期间读取的日志位置开始流式传输,所以不会漏掉这期间的变更。

事件同时携带变更前和变更后。Debezium 变更事件的正文中有 before(变更前的行)、after(变更后的行)、source(来自哪个数据库、表、日志位置)、op(操作)、ts_ms(处理时刻)。op 中,c 是创建,u 是修改,d 是删除,r 是快照读取,t 是清空表。在 PostgreSQL 中,before 里装什么,由表的 REPLICA IDENTITY 决定——默认值(DEFAULT)时,修改、删除事件只包含主键列的旧值,如果是 FULL,则包含所有列的旧值。如果需要“余额从多少变成了多少”,就要先看这个设置。

复制槽会拉住日志。PostgreSQL 连接器通过复制槽在数据库中记录自己读到了哪里。即使连接器停止,数据库也不会删除槽尚未读取的 WAL——所以重启后能从停下的位置接着读。反过来说,如果连接器死掉好几天,源数据库的磁盘就会被 WAL 填满。一旦接入 CDC,源数据库的运维中就多了一个监控项。MySQL 一侧则有相反方向的风险。binlog 超过保留期限就会被删除,所以如果连接器停止的时间比这更长,读取的位置就会消失,文档写道这时需要新的快照。

与发件箱模式一起使用。如果把表变更原样传送,消费者就会被绑定在源表的结构上(改了列名,消费者就会出问题)。所以要在业务事务之内,把要发布的事件也写一行到 outbox 表中,CDC 只读取这张 outbox 表并输出。源 schema 得以隐藏,双写问题也被消除。Debezium 提供把 outbox 表的行转换为事件的 Outbox Event Router 转换。

在现场相遇的样子

第一,用 updated_at 查询实现“只给我变更部分”的批处理,会永远漏掉删除。已经注销的账户,在信息系统里好几个月还活着。第二,CDC 只搬运变更,不搬运含义。账本表的三条行变更(扣款行、入账行、余额行)就是一笔转账,这一事实日志里并没有。如果需要业务事件,就必须通过发件箱把含义写下来。第三,交付是至少一次。连接器一重启,已经发送过的事件可能再次到来,所以消费者必须照搬第 8 模块的幂等处理——source 中的日志位置或事件键就是它的依据。第四,schema 变更。源表增加了一列,事件的形态就会改变。这就是要把消费者契约放在发件箱的事件格式上,而不是源表上的原因。

也整理一下与 EAI 的关系。CDC 不会取代中继层。需要请求和响应的交易(转账审批、限额查询)仍然通过同步中继来做。CDC 适用于别的系统需要知道一件已经完成的事的时候——信息系统入库、搜索索引、缓存失效、通知。

总结与测验

本模块不做实验,而是整理概念。用测验来确认轮询、触发器、基于日志的区别,快照与流式,变更事件的形态,复制槽与日志保留的运维责任,以及发件箱。