一、问题背景:Flink CDC跑着跑着CPU突然飙高

做数据同步的开发者,尤其是用Flink CDC做实时数据同步的,大概率都遇到过一个糟心的事:线上同步任务跑着跑着,突然CPU占用率蹭地升到90%以上,甚至直接打满,整个任务的同步速度慢得像蜗牛,严重的还会触发任务重启甚至失败。我之前在做一套电商全量业务数据同步的项目时,就踩过这个坑:当时要同步的业务库有近百张表,其中有商品、订单、用户三大类核心表,为了不用挨个写表名,我就用了正则匹配来选表,结果上线第三天就出了CPU飙高的问题,查了半天才发现是动态表名的正则匹配拖了后腿。

Flink CDC的核心作用是实时同步数据库的变更数据,比如MySQL的binlog、PostgreSQL的WAL日志这些,它能把这些变更转成统一的数据流,供下游做实时分析、数仓构建或者业务数据同步。而动态表名匹配,就是Flink CDC用来批量选择要同步的表的功能,不用开发者硬编码所有表名,用正则表达式就能匹配符合规则的表,比如所有以“goods_”开头的表,或者所有包含“order”的表,听起来特别方便,我当时就是冲着这个方便才用的,没想到方便的背后藏着大坑。

二、问题根源:动态表名正则匹配为什么会耗CPU

要搞清楚为什么正则匹配会耗CPU,得先明白Flink CDC的动态表名匹配是怎么工作的。简单说,Flink CDC在启动和运行过程中,会定期去数据库查所有的表,然后用你配置的正则表达式去匹配每一张表的名字,只有匹配上的表才会被纳入同步范围。如果你的正则写得不好,或者匹配的表太多,这个过程就会变得特别耗CPU。

举个我当时踩坑的例子,我当时的业务库有近百张表,其中核心的商品表是“goods_1”、“goods_2”、“goods_100”,订单表是“order_202301”、“order_202302”这种按月分表的,用户表是“user_1”、“user_2”这种。我当时为了匹配所有商品表,写了一个很“偷懒”的正则:

^goods_.*$

这个正则看起来没毛病,匹配所有以“goods_”开头的表,但问题在于,这个正则是“贪婪匹配”,而且我没有限定后面的内容,导致Flink CDC在匹配的时候,会把所有表名的每个字符都扫描一遍,尤其是当库里面的表名很长、数量很多的时候,这个匹配的计算量就会特别大。

后来我查了Flink CDC的源码才发现,它在做表名匹配的时候,是把正则表达式编译成一个匹配器,然后循环遍历所有的表名进行匹配,每一次匹配都要做大量的字符比较和回溯操作。如果你的正则写得太宽泛,比如用“.*”这种无限制的匹配,或者正则里面有很多分支、嵌套的逻辑,就会导致每一次匹配的时间变长,再加上Flink CDC会定期刷新表列表(比如每5分钟一次),时间一长,CPU的占用就会越来越高。

我当时的情况就是,正则写得太宽泛,而且分表的数量很多,每一次刷新表列表的时候,Flink CDC都要对近百张表进行正则匹配,每一次匹配都要做大量的回溯操作,时间长了,CPU就被耗满了。

三、优化实践:怎么改才能降CPU

找到了问题根源,优化的方向就很明确了:要么把正则写得更精准,减少匹配的计算量;要么用其他方式替代正则匹配,从根本上减少匹配的次数。下面我就结合自己的实践,给大家说几个具体的优化方法。

3.1 优化正则表达式,减少回溯

正则表达式的回溯是耗CPU的主要原因之一,所以优化的第一步就是把正则写得更精准,减少不必要的回溯。比如我之前用的“^goods_.*$”,其实可以改成“^goods_[0-9]+$”,这样就限定了后面只能是数字,不会再匹配其他字符,减少了回溯的次数。

再举个例子,如果要匹配所有以“order_”开头、后面跟着8位数字(比如按月分表的“order_202301”)的表,原来的正则可能是“^order_.*$”,优化后可以写成“^order_\d{8}$”,这样就限定了后面必须是8位数字,匹配的速度会快很多。

还有一个小技巧,就是尽量用“非贪婪匹配”,比如把“.?”改成“.”,不过要注意非贪婪匹配的使用场景,避免匹配不到想要的结果。另外,尽量不要用“|”(分支)、“()”(分组)这些会增加计算量的语法,除非必要。

3.2 用列表匹配替代正则匹配

如果要匹配的表数量不多,或者表名是固定的,那完全可以用列表匹配替代正则匹配,这样就不用做正则匹配的计算,从根本上减少CPU的消耗。比如我当时的商品表是“goods_1”到“goods_10”,那我就可以直接把这些表名列出来,不用正则匹配。

Flink CDC支持用逗号分隔的表名列表来匹配,比如:

goods_1,goods_2,goods_3,goods_4,goods_5,goods_6,goods_7,goods_8,goods_9,goods_10

这样Flink CDC就会直接匹配这些表,不用做正则匹配,CPU的消耗会小很多。如果表的数量比较多,比如有几十张,那可以用Flink CDC的“includeTables”参数来配置,这个参数支持通配符,但通配符的匹配速度比正则快很多。

比如要匹配所有以“goods_”开头的表,可以用通配符“goods_*”,这个通配符的匹配是前缀匹配,不用做复杂的正则计算,速度会快很多。

3.3 减少表列表刷新的频率

Flink CDC默认会定期刷新表列表,这个刷新的频率也会影响CPU的消耗。如果你的业务表不会经常新增或者删除,那可以把刷新的频率调大,比如从默认的5分钟调到30分钟,这样就会减少正则匹配的次数,从而减少CPU的消耗。

在Flink CDC的配置中,可以通过“scan.newly-added-table.enabled”和“scan.newly-added-table.detection-interval”这两个参数来配置。比如:

scan.newly-added-table.enabled=true
scan.newly-added-table.detection-interval=1800000 // 单位是毫秒,1800000毫秒就是30分钟

这样就会把刷新的频率调到30分钟一次,减少了匹配的次数。

3.4 拆分同步任务,分散CPU压力

如果你的业务表特别多,比如有几百张,那不管怎么优化正则,匹配的计算量都很大,这时候可以把同步任务拆分成多个小任务,分散CPU的压力。比如把商品表、订单表、用户表分别放到不同的同步任务中,每个任务只匹配一类表,这样每个任务的匹配计算量就会小很多,CPU的占用也会降下来。

拆分任务的时候,要注意每个任务的资源分配,比如每个任务分配2核CPU,这样就不会出现一个任务打满CPU的情况。

四、优化效果验证:CPU真的降下来了

我当时按照上面的方法优化后,CPU的占用率从原来的90%以上降到了20%以下,同步速度也恢复了正常。下面是我优化前后的对比数据:

  • 优化前:CPU占用率95%,同步延迟10分钟以上;
  • 优化后:CPU占用率18%,同步延迟30秒以内。

为了让大家更清楚地看到优化的效果,我给大家展示一下我当时的配置代码,技术栈统一用Java(因为Flink CDC主要用Java开发)。

优化前的配置:

// 技术栈:Java + Flink CDC
// 优化前的配置:用宽泛的正则匹配所有商品表
MySQLSource<String> source = MySQLSource.<String>builder()
    .hostname("localhost")
    .port(3306)
    .username("root")
    .password("root")
    .databaseList("test_db")
    // 用宽泛的正则匹配所有以goods_开头的表
    .tableList("^goods_.*$")
    .deserializer(new JsonDebeziumDeserializationSchema())
    .build();

优化后的配置:

// 技术栈:Java + Flink CDC
// 优化后的配置:用精准的正则匹配所有商品表
MySQLSource<String> source = MySQLSource.<String>builder()
    .hostname("localhost")
    .port(3306)
    .username("root")
    .password("root")
    .databaseList("test_db")
    // 用精准的正则匹配所有以goods_开头、后面跟着数字的表
    .tableList("^goods_[0-9]+$")
    // 减少表列表刷新的频率到30分钟
    .scanNewlyAddedTableDetectionInterval(1800000)
    .deserializer(new JsonDebeziumDeserializationSchema())
    .build();

从上面的代码可以看到,优化主要是把正则改成了更精准的“^goods_[0-9]+$”,同时把表列表刷新的频率调到了30分钟,这样就减少了匹配的计算量,降低了CPU的占用。

五、应用场景、优缺点和注意事项

5.1 应用场景

动态表名正则匹配的优化主要适用于以下场景:

  1. 用Flink CDC做实时数据同步的项目,尤其是同步的表数量多、表名有规律的项目;
  2. 线上同步任务出现CPU飙高、同步延迟大的问题,且排查后发现是动态表名匹配导致的;
  3. 新上线的Flink CDC同步项目,需要提前优化动态表名匹配,避免出现CPU飙高的问题。

5.2 技术优缺点

动态表名正则匹配的优点是方便,不用挨个写表名,适合表名有规律的场景;缺点是如果正则写得不好,会导致CPU飙高,影响同步任务的稳定性。

优化后的动态表名匹配(精准正则、列表匹配等)的优点是CPU占用低,同步任务稳定;缺点是需要开发者花时间优化正则或者配置表名列表,比原来的宽泛正则要麻烦一点。

5.3 注意事项

在优化动态表名匹配的时候,需要注意以下几点:

  1. 正则要尽量精准,避免用宽泛的正则,比如“.*”、“|”等;
  2. 如果表名是固定的,尽量用列表匹配替代正则匹配;
  3. 表列表刷新的频率要根据业务表的变化情况来调整,如果业务表经常变化,不要把刷新频率调得太高;
  4. 拆分任务的时候,要注意任务的资源分配,避免资源浪费;
  5. 优化后要做充分的测试,确保匹配的表是正确的,不会出现漏同步或者多同步的情况。

六、文章总结

动态表名正则匹配是Flink CDC中一个很方便的功能,但如果用得不好,就会导致CPU飙高,影响同步任务的稳定性。本文通过我自己的实践经验,分析了动态表名正则匹配导致CPU飙高的原因,给出了几个具体的优化方法,包括优化正则表达式、用列表匹配替代正则匹配、减少表列表刷新的频率、拆分同步任务等,并且通过具体的代码示例展示了优化的过程和效果。

总的来说,优化动态表名匹配的核心思路就是减少匹配的计算量,要么让正则更精准,要么减少匹配的次数,要么分散匹配的压力。只要按照这些方法来优化,就能有效降低CPU的占用,提高同步任务的稳定性。