
Engineering Case Study · RCA · Postmortem 文中 IP、账号、库名、表名与人员信息均已脱敏系统页面显示推送开关已开启业务应用也打印了“已连接 Canal”但当天没有任何打卡通知。最终发现应用确实连上了 Canal 的 TCP 端口Canal 却因源库账号缺少复制权限而无法读取 Binlog。断电并不会正常删除 MySQL 授权却迫使整条链路重新连接从而暴露了长期隐藏的权限、位点持久化与监控缺口。结论摘要直接原因是同步账号缺少REPLICATION CLIENT、REPLICATION SLAVE和目标打卡表的SELECT权限。补齐授权后Canal 自动重试并恢复 Binlog Dump新打卡在约 7 秒内完成企业微信推送接口返回errcode0。历史事件不会因为授权恢复自动全部补发需要单独设计回放或补偿。MySQL 5.7Canal 1.1.xSpring Boot企业微信应用消息Binlog CDC故障复盘1. 背景与问题系统通过 Canal 订阅考勤数据库的checkinout与userinfo变更。收到打卡 INSERT 事件后业务应用先把事件写入本地通知日志表再由独立调度任务解析员工与企业微信 UserID 映射最后调用企业微信应用消息接口。故障当天出现三个容易误导判断的现象后台“打卡自动推送”开关已经打开业务应用进程、HTTP 端口、数据库连接均正常应用日志出现“已连接 Canal”。然而通知队列当天为 0最后一条成功记录停留在断电发生之前。这说明问题不在消息模板或企业微信接口而在事件进入业务系统之前。2. 目标与约束排查目标定位数据在哪一层停止流动确认断电与授权异常的因果关系恢复未来实时推送给历史补偿保留安全边界。操作约束线上先只读排查不直接重启或改位点不在日志和文档中输出密码、Token、手机号避免一次性回放千余条历史消息造成骚扰和限流验证必须覆盖源库、Canal、业务队列和企业微信四层。3. 系统架构图 1应用连接 Canal 只证明 TCP 层可用不能证明 Canal 已经成功订阅源库 Binlog。系统采用“Binlog 消费与消息发送解耦”的方式Canal 线程只负责把事件可靠落库消息发送任务从数据库队列中领取记录。这个设计避免企业微信接口延迟阻塞 Binlog 消费也让失败重试、人工重发和幂等去重成为可能。4. 核心设计与数据模型图 2源库与业务库之间没有物理外键依靠业务键和唯一事件键建立关联。4.1 表职责逻辑表职责关键字段一致性策略attendance_checkin源系统打卡事实表user_id、check_time由考勤设备写入产生 ROW Binlogattendance_user考勤用户与员工编号映射user_id、badge_no按用户变更清理应用缓存notification_log通知日志兼持久化队列event_key、status唯一索引防重复状态机控制重试user_wecom_mapping员工编号到企微 UserID 的缓存employee_code、wecom_userid映射状态决定发送或跳过notification_control总开关和全局熔断notify_enabled、circuit_until多实例共享控制状态4.2 写入顺序与幂等Canal 根据 Binlog 文件名、偏移量和行序号计算event_key使用INSERT IGNORE写入通知队列唯一索引阻止重复入队补充员工编号和企业微信映射失败时只记录错误不回滚已经落库的 Binlog 事件发送任务通过原子状态更新领取记录避免并发重复发送企业微信成功后更新为SUCCESS可重试错误进入FAILED不可推送记录进入SKIPPED。5. 正常执行流程图 3正常时序与本次故障点。TCP 连接建立后Canal 仍需成功定位主库位点并发起 Binlog Dump。关键边界是“Canal 客户端已连接”与“Canal EventParser 正在推进位点”是两个不同健康指标。前者只覆盖业务应用到 Canal Server后者才覆盖 Canal Server 到源 MySQL。6. 故障现象与时间线图 4断电、源库恢复、Canal 重启、授权修复和最终验证的关键节点。Symptoms推送开关为开启状态但当天没有通知日志企业微信接口没有失败记录因为发送阶段根本没有被触发Canal 端口可访问业务应用保持 ESTABLISHED 连接源库中存在大量当天打卡业务通知队列却没有对应记录。断电的作用断电不会按正常机制删除 MySQL GRANT。它会终止既有长连接迫使 MySQL、Canal 和业务应用全部重新建立会话。重新连接时权限、Host 匹配、位点持久化和启动配置问题才会被重新校验因此断电是触发器而不是授权消失的正常机制。7. Investigation分层排查过程图 5先确定事件消失在哪一层再针对该层检查输入、状态和输出。7.1 第一层业务应用与端口确认 Java 进程和业务端口均正常并检查应用与 Canal 的 TCP 连接。应用日志确实出现“已连接 Canal”因此排除了应用未启动、环境变量关闭订阅线程以及网络完全不通等问题。7.2 第二层通知队列查询业务库发现推送总开关为开启状态、熔断时间早已过期但当天队列记录为 0。最后记录停留在断电前。这一步非常关键它把排查范围从“企业微信发送失败”前移到“Binlog 事件没有进入应用”。7.3 第三层Canal 实例与容器服务器上同时存在两个 Canal Server 容器一个挂载了持久化目录但源库地址仍是默认的127.0.0.1:3306另一个实际承接业务流量源库地址和过滤表达式正确但没有挂载持久化的配置与位点目录。通过容器网络命名空间确认业务应用连接的是第二个实例。这个实例与源库网络可达但日志反复出现command: SHOW MASTER STATUS has an error ERROR 1227: Access denied; you need SUPER or REPLICATION CLIENT privilege7.4 第四层账号匹配与权限探针从 Canal 所在网络位置使用同一账号连接源库验证得到USER()表示客户端从 Canal 主机接入CURRENT_USER()实际匹配到canal_sync%账号只有部分业务表 SELECT没有复制权限也没有目标打卡表 SELECTSHOW MASTER STATUS返回 1227读取目标打卡表返回 1142。这一步把“可能是网络、过滤器或企业微信”的猜测收敛成可复现的权限故障。7.5 第五层断电证据源 MySQL 的Uptime显示它刚恢复运行Canal 日志从断电日开始持续记录源库连接失败。源库仍使用原有server_uuid和数据目录说明不是全新安装。更合理的解释是断电导致源库和 Canal 既有会话中断恢复后 Canal 必须重新执行位点定位与复制握手当前实际匹配账号的授权不足重连失败系统之前没有针对权限和位点做启动自检导致故障只表现为“没有消息”。8. Root Cause Analysis图 6直接原因是复制权限不足系统性原因是健康检查只覆盖端口而没有覆盖位点推进。Root Cause根因Canal 使用的数据库账号缺少读取 Binlog 所需的全局复制权限并缺少订阅表的 SELECT 权限。断电后的首次重连需要执行SHOW MASTER STATUS因此 EventParser 无法建立起始位点整条事件链路停在源库与 Canal 之间。Contributing Factors把“应用连接 Canal”误当作整条 CDC 链路健康存在重复 Canal 实例持久化实例配置错误正确实例却没有持久化位点没有监控 Binlog 位点更新时间和队列最后入库时间没有在启动时主动验证CURRENT_USER()、SHOW MASTER STATUS和目标表访问断电恢复手册只关注进程和端口没有覆盖账号权限与消费进度。9. Resolution修复方案9.1 补齐最小权限在源 MySQL 使用管理账号执行。公开文章中的账号、Host 和库名均为示例-- 示例账号与库名均已脱敏请按实际环境收敛 Host 范围 GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO canal_sync10.20.30.%; GRANT SELECT ON attendance_db.checkinout TO canal_sync10.20.30.%; GRANT SELECT ON attendance_db.userinfo TO canal_sync10.20.30.%; FLUSH PRIVILEGES; SHOW GRANTS FOR canal_sync10.20.30.%;MySQL 5.7 中GRANT会持久化到权限表正常重启不会丢失。若再次出现“授权消失”应检查是否恢复了旧数据目录、重建了账号、改变了 Host 匹配规则或由设备软件覆盖了系统库。9.2 等待 Canal 自动重试本次不需要立即重启 Canal。EventParser 原本就在循环重试授权生效后自动完成SHOW MASTER STATUS成功找到新的起始位点与源库建立复制连接位点开始推进新 INSERT 事件进入业务应用。9.3 历史数据恢复策略不要直接大范围回放。授权恢复时活跃 Canal 实例没有有效历史游标因此默认从恢复时的最新位点开始。当天此前的千余条打卡不会自动补发。若直接回退到断电前位点虽然event_key可以防止数据库重复入队但仍要评估企业微信限流、员工消息骚扰、映射变化和历史 Binlog 是否仍保留。推荐按以下顺序处理历史记录10. Verification验证过程修复后没有停留在“错误日志消失”而是进行了端到端验证层级验证方式结果源 MySQLSHOW MASTER STATUS成功返回当前 Binlog 文件与 PositionCanal → 源库检查 ESTABLISHED 复制连接连接建立EventParser 找到起始位点Canal → 应用检查客户端连接和 meta cursor游标持续更新业务队列产生一条新打卡通知日志新增进入发送状态企业微信检查接口返回与最终状态errcode0约 7 秒后发送成功验证结论未来新增打卡已恢复实时推送。历史漏发记录未自动补发符合“先恢复实时链路、再受控补偿历史”的风险控制策略。常用验证命令-- 源库确认 Binlog 与账号实际匹配关系 SELECT USER(), CURRENT_USER(), server_uuid, log_bin, binlog_format; SHOW MASTER STATUS; -- 业务库确认事件是否进入队列 SELECT status, COUNT(*) FROM notification_log WHERE check_time CURRENT_DATE GROUP BY status;# Canal确认源库连接、位点推进与权限错误 grep -E find start position|binlog dump|Access denied|1227 logs/example/example.log | tail -100 # 应用确认 Canal 连接和企业微信返回码 grep -E 已连接Canal|通知发送成功|errcode application.log | tail -100 # 网络端口连通不是最终结论还要看到源库复制会话 ss -ntp | grep -E 11111|330611. 方案取舍方案优势风险/代价结论每 30 秒轮询源表实现直观容易补查历史持续压源库、延迟高、分页和去重复杂不作为实时主链路可保留为受控补偿工具Canal Binlog CDC低延迟、源库侵入小、事件顺序明确依赖复制权限、Binlog 保留和位点治理继续作为实时主链路考勤系统主动调用接口业务语义清晰不依赖 Binlog需要改造第三方系统重试和签名由双方治理若源系统可扩展是长期更理想方案业务应用直接同步发送企微链路短消息 API 会阻塞消费故障放大拒绝继续采用持久化队列解耦12. Troubleshooting容易踩坑的判断坑一看到“已连接 Canal”就认为 CDC 正常该日志只证明客户端到 Canal Server 的 TCP 连接正常。必须同时验证 Canal 到 MySQL 的复制会话、当前位点时间和业务队列最后入库时间。坑二第一时间重启所有服务盲目重启会覆盖现场、改变位点甚至让临时正常的长连接也断开。正确做法是先采集进程、连接、启动时间、游标、权限和最后成功记录再决定是否重启。坑三把断电理解为“GRANT 会丢”正常 MySQL 授权是持久化的。断电后权限变化通常意味着使用了不同账号 Host、恢复了旧系统表、账号被重建或者之前依赖的长连接一直没有重新鉴权。坑四修好实时链路后立即回放全部历史数据库幂等不等于消息侧绝对安全。历史回放还要考虑企业微信调用频率、接收体验、模板变化和人员映射变化。13. Lessons Learned14. 后续优化计划优先级改进项验收标准P0合并重复 Canal 实例修正持久化实例配置线上只有一个明确服务实例配置与 meta 均挂载持久卷P0增加 Canal 启动自检启动时验证CURRENT_USER()、SHOW MASTER STATUS和订阅表 SELECTP0监控位点与通知队列新鲜度位点超过阈值不更新或队列长时间无新增时主动告警P1同步账号权限基线脚本化可重复执行并定期对比实际 GRANT 与期望 GRANTP1提供受控历史补偿工具支持人员、时间窗口、批次限速、预览和审计P1增加链路状态页展示源库、Canal、应用、队列、企微五层状态而不是单一开关P2演练断电与服务重建每季度验证配置、权限、位点与告警均可恢复15. 总结这次故障表面上是“企业微信没有消息”真正的问题却发生在链路最前端Canal 的数据库账号无法读取 Binlog。断电让所有连接重新建立也让此前未被健康检查覆盖的权限和位点问题一次性暴露。本次最重要的改进不是记住一条 GRANT SQL而是建立一套可迁移的方法确认历史 Binlog 文件仍存在先选择测试员工和小时间窗口回放对已经成功发送的event_key做幂等校验按批次限速补偿并记录补偿批次业务确认后再逐步扩大范围。