多节点下嵌入式Binlog框架引发的线上故障

0 阅读5分钟

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

上一节介绍Binlog的两类框架,我们公司项目中两种都使用了,聊一聊其中嵌入式框架Binlog4j在多节点下引发的故障。该故障被被认定为A1级故障,必须当天要解决。

02 框架回顾

Binlog4j这个框架在之前的文章中介绍过,就不细聊了。想要深入了解的可以直接去Gitee上了解。

Gitee地址:gitee.com/dromara/bin…

虽然该项目已经停更2年了,但是易用性值得大家去瞅瞅。

2.1 依赖

<dependency>
   <groupId>com.gitee.Jmysy</groupId>
   <artifactId>binlog4j-core</artifactId>
   <version>latest.version</version>
</dependency>

2.2 客户端创建

public static void main(String[] args) {
        
    BinlogClientConfig clientConfig = new BinlogClientConfig();
    clientConfig.setHost("127.0.0.1");
    clientConfig.setPort(3306);
    clientConfig.setUsername("root");
    clientConfig.setPassword("taoren@123");
    
    // 保证客户端唯一的关键属性
    clientConfig.setServerId(1990);

    IBinlogClient binlogClient = new BinlogClient(clientConfig);

    binlogClient.registerEventHandler(new IBinlogEventHandler() {

        @Override
        public void onInsert(BinlogEvent event) {
            System.out.println("插入数据:{}", event.getData());
        }

        @Override
        public void onUpdate(BinlogEvent event) {
            System.out.println("修改数据:{}", event.getData());
        }

        @Override
        public void onDelete(BinlogEvent event) {
            System.out.println("删除数据:{}", event.getData());
        }

        @Override
        public boolean isHandle(String database, String table) {
            return database.equals("pear-admin") && table.equals("sys_user");
        }
    });

    binlogClient.connect();
}

2.3 故障出现的关键代码

 clientConfig.setServerId(xxx);

serverId表示当前客户端的唯一标识,如果serverId不同,将是不同的客户端,当Binlog产生时,不同的客户端都触发IBinlogEventHandler

为了监听的消息不被重复消费,保持幂等性。我们需要限制同一套在不同的节点只要有一个客户端能够处理消息即可。

03 故障重现

我们先来看看我们项目的写法:

3.1 代码解释

看到这块代码大家可能会比较疑惑,为什么会这么设计。所谓存在即合理,必然是解决一些问题,后面会单独来说。

Rediskey是常量+环境,如:binlog:listen:serverId:prod

  • 常量:binlog:listen:serverId:
  • 环境:包括开发(dev)、测试(int)、预发布(stage)、生产(prod)等

如果不存在,则设置serverId为一个随机数,然后设置有效期为10分钟,并建立连接;如果存在直接重置有效期为10分钟。

通过简易的分布式锁来设置serverId

3.2 解决的问题

因为Binlog端是嵌在业务代码中的,所以有多个节点,就会实例化多次。我们实际生产中至少应该是双节点或者更多节点,已达到负载均衡的目的。

因为我们每台节点的启动时间大致在3分钟左右,两个节点的顺序启动时间大致6分钟左右。所以10分钟之内,可以保证只有一个节点可以正常设置serverId,来作为专门解析Binlog的客户端。

简单来说就是保证一个节点作为客户端使用。

3.3 故障复现

上面的代码也是运行了一年多没有出现问题,但是特殊的场景,悄然的触发这个故障。

某个夜黑风高的晚上,我们发布了带有Binlog客户端代码的业务代码。结果发布速度很快,5分钟就发布好了。而我们的运维团队,为了验证一键回滚的脚本,10分钟之内完成回滚的测试。业务代码也是没有问题的。

风平浪静,必有杀招。大家都快快乐乐的回家了,直到第二天中午,有客户反馈说看不到推送的内容。经过排查代码得知,推送的内容是通过Binlog监控的节点触发的。原本以为只是个别没有触发,才发现凡是涉及监听Binlog的都没有数据,那就是Binlog出问题了,瞬间觉得天塌了。Binlog监听的可是实时数据呀。

3.4 故障分析

想必大家也猜到了,就是分布式锁导致的问题。因为有效期是10分钟,凡是10分钟的启动都不会触发Binlog客户端的连接。

而一键回滚的脚本就是在10分钟之内操作的,RedisKey并没有过期,所以不会触发binlogClient.connect()Binlog客户端也就失效了。这才导致问题的发生。

04 设计溯源

Binlog4j这个框架是我引入的,当初的设计也比较简单直接使用的是数据库名称的hashCode,相当于一个常量。

clientConfig.setServerId(database.hashCode());

即便的多节点,因为ServerId相同,最终也会只有一个客户端有效。这种方式在生产环境没有问题,但是在我们测试环境又会存在问题。

我们测试环境有5套,共享一个数据库。按照我设计的模式,5套环境最终只会有一个环境的一个节点的客户端有效。但是,我们很多时候都是并行开发,可能需要两个或多个环境同时需要可用的Binlog客户端。我的这种设计就无法满足了。

基于上面的缺陷,最终才会演变成Redis分布式锁的结果。

为了应对突发状况,我们还设计了手动建立连接以及手动关闭的开关。

05 小结

为什么之前会说,独立部署的Binlog客户端可能会更好些呢?原因就在这里。因为是独立部署的,不会收到发布的影响,还可以复用。铁子们,有没有遇到类似的问题呢?