SeaTunnel 2.3.12生产实践:从配置即代码到稳定CDC的工程化落地
1. 为什么是 SeaTunnel 2.3.12?——不是版本号,而是生产可用性的分水岭
你点开这篇万字长文,大概率不是为了凑热闹看个“新版本发布”,而是正卡在某个数据管道的十字路口:Flink CDC 启动慢、Kafka 消费延迟高、Spark 批处理任务改一次 SQL 就要重跑全量、或者更糟——业务方催着要“昨天的订单实时同步到 BI 看板”,而你还在手动写 Shell 脚本拼接mysqlbinlog和jq。这时候,一个名字反复出现在团队 Slack 频道里:“Seatunnel”“Seatunnel”“Seatunnel……它真能替代我们那套七拼八凑的 ETL 流水线吗?”
我去年在三个不同行业(电商中台、金融风控、IoT 设备平台)落地过 SeaTunnel,从 2.1.x 一路踩坑到 2.3.12。坦白说,2.3.12 不是功能堆砌最猛的版本,但它是我第一个敢在核心支付对账链路里上线、且连续稳定运行 147 天的版本。它的价值不在“新增了几个 connector”,而在于把过去被大家诟病的“配置像写 Java 代码”“报错信息像天书”“CDC 断点续传靠玄学”这些痛点,用工程化的方式钉死在了地面上。
先说一个反直觉的事实:SeaTunnel 2.3.12 的核心突破,是把“数据同步”这件事,从“开发任务”降维成了“运维配置”。
什么意思?以前你写 Flink CDC Job,得定义 SourceFunction、定义 Watermark、定义 Checkpoint 间隔、定义 State Backend、定义重启策略……每改一个字段类型,都得重新编译打包部署。而 SeaTunnel 2.3.12 的 YAML 配置里,你只需要声明:“我要从 MySQL 的orders表,按update_time字段增量同步,目标是 StarRocks 的dwd_orders_inc表,断点存在 MySQL 自己的seatunnel_checkpoint表里”。剩下的——连接池管理、Binlog 位点解析、Schema 变更兼容、失败重试退避、Exactly-Once 语义保障——全由引擎内部闭环处理。你看到的是一行配置,背后是 2000+ 行经过生产验证的状态机代码。
这直接改变了团队协作模式。DBA 不再需要等开发排期改 SQL;数据工程师不用再给每个新表手写一套 Flink 作业;运维同学第一次能在 Grafana 里看到“SeaTunnel Pipeline 运行时长 P95 < 800ms”这种可量化的 SLO 指标。而这一切的起点,就是 2.3.12 引入的Unified Engine Runtime架构——它把 Spark、Flink、Trino 的执行层抽象成统一的 DAG 编排器,让 connector 开发者只关心“怎么读、怎么写”,不再操心“在哪跑”。
所以,如果你正在评估是否引入 SeaTunnel,别只看官网文档里列的 50+ connector 支持列表。真正该问的是:你的团队有没有人愿意花三天时间,把所有离线报表的 Hive Insert 语句,替换成一份 20 行的 YAML?有没有人敢把 Kafka 消费端的 Python 脚本,换成 SeaTunnel 的kafka→elasticsearch单步 pipeline?2.3.12 的意义,就是让这个“敢”字,有了技术底气。
提示:很多团队卡在第一步——“连不上 MySQL”。这不是权限问题,而是 2.3.12 默认启用了
debezium模式的 Binlog 解析,要求 MySQL 必须开启binlog_format=ROW且binlog_row_image=FULL。这个配置项藏在my.cnf的[mysqld]段落里,重启 MySQL 服务才生效。我见过太多人对着Failed to connect to MySQL binlog的报错,在账号密码上折腾两小时,最后发现是这个底层参数没开。
2. 部署不是复制粘贴——从零构建可监控、可回滚的生产级环境
网上搜“SeaTunnel 部署”,90% 的教程停在wget + tar -xzf + ./bin/start-seatunnel.sh。这在你本地 Mac 上跑 Demo 没问题,但一旦放到 K8s 集群里跑金融级对账任务,这套流程会把你拖进深渊。我亲眼见过一个团队因为没做资源隔离,SeaTunnel Worker 把整个 YARN 队列的内存吃光,导致下游 Spark 任务集体 OOM。所以,2.3.12 的部署,必须按“生产中间件”的标准来设计。
2.1 环境准备:三类依赖,缺一不可
SeaTunnel 2.3.12 的依赖关系比前代更清晰,但也更严格。它不再容忍“差不多就行”的环境:
| 依赖类型 | 最低要求 | 关键原因 | 实测踩坑点 |
|---|---|---|---|
| JDK | OpenJDK 11.0.16+ 或 JDK 17 | 2.3.12 使用了 JEP 359(Records)和 JEP 405(Pattern Matching for instanceof),JDK 8/11.0.15 会直接启动失败 | 某客户用阿里云 ACK 自带的 OpenJDK 11.0.12,启动时报java.lang.UnsupportedClassVersionError: Record class not supported,换 JDK 11.0.18 后解决 |
| Python | Python 3.8+(仅限 PyFlink 模式) | 当启用flink-sqlconnector 时,PyFlink 运行时需调用 Python 解释器解析 UDF | 服务器默认 Python 2.7,pip3 install apache-flink失败,需显式指定python3 -m pip install apache-flink==1.17.1 |
| Native Lib | glibc 2.28+(CentOS 8+/Ubuntu 20.04+) | 2.3.12 内置的 RocksDB StateBackend 依赖较新的libstdc++.so.6.0.28,CentOS 7 的 glibc 2.17 会报undefined symbol: _ZTVNSt7__cxx1119basic_ostringstreamIcSt11char_traitsIcESaIcEEE | 在 CentOS 7 上部署必须手动编译 RocksDB 或降级使用 MemoryStateBackend |
特别强调:不要用./bin/start-seatunnel.sh直接启动生产服务。这个脚本本质是开发调试工具,它会把所有日志打到控制台,不支持优雅关闭,且无法设置 JVM 参数。真正的生产启动,必须用systemd或supervisord管理。
2.2 核心配置:seatunnel.yaml的 7 个生死开关
conf/seatunnel.yaml是 SeaTunnel 的心脏,2.3.12 新增了 12 个关键配置项,但其中 7 个直接影响稳定性。我把它拆解成“必配”和“慎配”两类:
必配项(不设即崩)
engine.checkpoint.interval
默认值30000(30 秒),但这是陷阱。在 CDC 场景下,如果 MySQL Binlog 刷盘间隔是 1 秒,而 checkpoint 间隔是 30 秒,意味着最多丢失 30 秒数据。生产环境必须设为5000(5 秒),并确保state.backend.rocksdb.predefined-options设为SPINNING_DISK_OPTIMIZED_HIGH_MEM(针对 HDD 优化)。plugin.kerberos.enable
如果你的 HDFS/Hive 启用了 Kerberos 认证,此项必须设为true,且kerberos.principal和kerberos.keytab路径必须指向 Worker 节点上的绝对路径(如/etc/seatunnel/seatunnel.keytab)。切记:Keytab 文件权限必须是600,且属主为运行 SeaTunnel 的用户,否则报LoginException: Unable to obtain password from user。metrics.reporters
默认只启用了consolereporter。生产必须加prometheus:metrics: reporters: - console - prometheus: host: "0.0.0.0" port: 9201这样 Prometheus 才能抓取
seatunnel_job_status{job_name="mysql_to_starrocks"}这类指标。我见过团队因漏配此项,故障时只能翻日志查“Last checkpoint completed at”,耗时 40 分钟。
慎配项(设错即瘫)
engine.parallelism.default
默认1,看似安全,实则致命。当 pipeline 包含filter或map算子时,单并发会成为瓶颈。正确做法是:根据 Worker 节点 CPU 核数 × 0.8 设置(如 16 核机器设为12),并在env中显式声明:env: parallelism.default: 12plugin.mysql.jdbc.driver
2.3.12 默认使用com.mysql.cj.jdbc.Driver,但某些老 MySQL 5.6 实例需强制降级为com.mysql.jdbc.Driver。若出现Unknown system variable 'tx_isolation'错误,立即在plugins/mysql/lib/下替换mysql-connector-java-5.1.49.jar,并在配置中指定:source: - mysql: driver: "com.mysql.jdbc.Driver"state.backend.type
默认rocksdb,但 RocksDB 对磁盘 IOPS 敏感。在云服务器(如 AWS i3.2xlarge)上,若磁盘是 gp3 类型(默认 3000 IOPS),必须调大state.backend.rocksdb.options:state.backend.rocksdb.options: max_background_compactions: "4" max_open_files: "1000"否则 Compaction 线程会抢占主线程 CPU,导致 pipeline 延迟飙升。
job.restart-strategy
默认fixed-delay,但生产必须设为failure-rate:job: restart-strategy: type: "failure-rate" failure-rate: 3 failure-interval: 60000 delay: 60000这表示“1 分钟内最多失败 3 次,每次重试间隔 1 分钟”。比固定延迟更智能——避免网络抖动时无限重试,也防止真正故障时错过告警。
2.3 部署拓扑:为什么推荐“Master-Worker”而非“All-in-One”
很多教程教你在一台机器上start-seatunnel.sh启动,这适合 Demo,但生产必须拆分:
- Master 节点(1 台):只运行
seatunnel-server,负责接收 REST API 请求、调度任务、管理元数据(存于 MySQL)、暴露 Prometheus Metrics。CPU 4 核 / 内存 8G 足够。 - Worker 节点(N 台):运行
seatunnel-worker,只干活不调度。每台配置取决于负载:CDC 场景建议 8 核 / 16G;批处理场景可 4 核 / 8G。
拆分的核心收益是故障隔离。去年某次线上事故:Worker 节点因 RocksDB Compaction 导致 CPU 100%,但 Master 依然健康,我们通过curl -X POST http://master:5005/api/v1/jobs/stop/{job_id}一键停止故障 pipeline,30 秒内恢复其他任务,全程无感知。
部署命令示例(Worker 节点):
# 创建专用用户,避免 root 权限 sudo useradd -m -s /bin/bash seatunnel sudo chown -R seatunnel:seatunnel /opt/seatunnel # 切换用户,启动 Worker sudo -u seatunnel bash -c ' export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export SEATUNNEL_HOME=/opt/seatunnel cd $SEATUNNEL_HOME nohup bin/seatunnel-worker.sh \ --config conf/seatunnel.yaml \ --master http://master-ip:5005 \ --worker-id worker-01 \ > logs/worker.out 2>&1 & '注意:
--master参数必须是 Master 节点的内网 IP,不能写localhost或域名。DNS 解析失败会导致 Worker 注册超时,日志里只显示Register to master failed,没有具体原因。我为此排查了 3 小时,最后用tcpdump -i eth0 port 5005抓包才发现 DNS 请求根本没发出。
3. 7 个 Demo 的真实价值——不是“Hello World”,而是生产场景的最小闭环
标题说“7 个 Demo”,但别被数字迷惑。这 7 个不是教你“怎么打印一行字”,而是覆盖了数据工程师日常 80% 的高频场景。每个 Demo 我都附上了生产环境已验证的 YAML 片段、关键参数解释、以及我踩过的坑。你可以直接复制,但请务必理解每一行背后的意图。
3.1 Demo 1:MySQL 全量 + 增量同步到 StarRocks(CDC 核心场景)
这是 SeaTunnel 2.3.12 最受关注的能力。很多人以为 CDC 就是“监听 Binlog”,其实难点在全量与增量的无缝衔接。2.3.12 用checkpoint表实现了原子切换。
env: execution.parallelism: 4 checkpoint.interval: 5000 source: - mysql-cdc: plugin-name: mysql-cdc hostname: "mysql-prod" port: 3306 username: "seatunnel_reader" password: "xxx" database-name: "ecommerce" table-name: "orders" server-time-zone: "Asia/Shanghai" # 关键:全量阶段扫描的起始位点 startup.mode: "initial" # 关键:断点续传的存储位置(必须是 MySQL 自己的库) checkpoint.table: "seatunnel_checkpoints" transform: - sql: query: "SELECT order_id, user_id, amount, status, update_time FROM orders WHERE status != 'deleted'" sink: - starrocks: load-url: "starrocks-prod:9030" username: "seatunnel_writer" password: "xxx" database: "dwd" table: "orders_inc" # 关键:StarRocks 的唯一键模型,必须指定 unique-key: "order_id" # 关键:批量写入大小,太小吞吐低,太大内存溢出 batch-size: 10000为什么这样写?
startup.mode: "initial"表示先扫全量,再接增量。SeaTunnel 会自动在seatunnel_checkpoints表里记录全量扫描完成的 Binlog 位点,然后无缝切到增量模式。checkpoint.table必须是 MySQL 的表,不能是 HDFS 或 Redis。因为只有 MySQL 能保证位点写入的事务性。batch-size: 10000是经验值:StarRocks 官方测试,10000 行/批时 Load 性能最优;超过 50000 行,Worker JVM 堆内存容易 OOM。
我踩的坑:
某次同步orders表时,StarRocks 报错Load failed: duplicate key order_id=12345。排查发现是 MySQL 表有逻辑删除(status='deleted'),但transform.sql里WHERE status != 'deleted'没生效——因为 SeaTunnel 的 SQL Transform 是在 Flink Runtime 里执行的,而status字段在 MySQL Binlog 里是TINYINT类型(0/1),SQL 里写字符串'deleted'会导致类型隐式转换失败。解决方案:改用WHERE status = 0。
3.2 Demo 2:Kafka JSON 消息解析并写入 Elasticsearch(日志分析场景)
Kafka 里存的往往是嵌套 JSON,比如 Nginx 日志:{"host":"192.168.1.1","status":200,"body_bytes_sent":1234,"@timestamp":"2024-01-01T12:00:00Z"}。直接写 ES 会变成扁平字段,但业务需要按host聚合 PV。
source: - kafka: bootstrap.servers: "kafka-prod:9092" group.id: "seatunnel-es-log" topic: "nginx_access_log" format: "json" # 关键:JSON Schema 必须显式声明,否则嵌套字段解析失败 schema: | { "type": "object", "properties": { "host": {"type": "string"}, "status": {"type": "integer"}, "body_bytes_sent": {"type": "integer"}, "@timestamp": {"type": "string"} } } transform: - json: # 关键:把 @timestamp 字符串转成 ES 能识别的 date 类型 field: "@timestamp" target-field: "event_time" format: "strict_date_optional_time_nanos" sink: - elasticsearch: hosts: ["http://es-prod:9200"] index: "nginx-access-log-{date}" username: "seatunnel_es" password: "xxx" # 关键:ES 的动态模板,让 event_time 自动映射为 date dynamic-template: | { "dates": { "match_mapping_type": "string", "match": "event_time", "mapping": { "type": "date", "format": "strict_date_optional_time_nanos" } } }为什么这样写?
schema不是可选的!2.3.12 的 Kafka JSON Source 默认用org.apache.flink.api.common.serialization.SimpleStringSchema,不解析结构。必须用jsonformat 并提供 schema,才能提取嵌套字段。json.transform的format必须和 ES 的dynamic-template里的format严格一致,否则 ES 会把event_time当成字符串索引,聚合失效。
我踩的坑:
ES 索引里event_time字段全是null。日志显示Failed to parse field [@timestamp] of type [text]。原因是 Kafka 消息里的@timestamp是"2024-01-01T12:00:00Z",而strict_date_optional_time_nanos格式要求纳秒精度(2024-01-01T12:00:00.000000000Z)。解决方案:在transform里加一步sql:
- sql: query: "SELECT *, TO_TIMESTAMP(`@timestamp`, 'yyyy-MM-dd''T''HH:mm:ss.SSSSSSSSS''Z''') AS event_time FROM default"3.3 Demo 3:Hive 分区表增量同步到 Doris(数仓分层场景)
Hive 里常按dt='20240101'分区,Doris 要求按dt字段自动创建分区。2.3.12 的 Doris Sink 支持动态分区。
source: - hive: metastore.uris: "thrift://hive-metastore:9083" database: "ods" table: "user_behavior" # 关键:只读取最近 3 天的分区,避免全表扫描 partition: "dt>=20240101" transform: - sql: # 关键:Hive 分区字段 dt 是 string,Doris 分区要求 date 类型 query: "SELECT user_id, item_id, behavior, TO_DATE(dt, 'yyyyMMdd') AS dt FROM user_behavior" sink: - doris: fenodes: "doris-fe-prod:9030" username: "seatunnel_doris" password: "xxx" database: "dwd" table: "user_behavior_dwd" # 关键:Doris 动态分区配置 dynamic-partition: enable: true time-unit: "DAY" time-zone: "Asia/Shanghai" history-partition-number: 3 create-history-partition: true为什么这样写?
partition: "dt>=20240101"是 Hive Source 的原生过滤,直接下推到 Metastore,比在transform.sql里WHERE dt >= '20240101'快 10 倍。dynamic-partition让 Doris Sink 自动创建p20240101、p20240102等分区,无需 DBA 提前建表。
我踩的坑:
Doris 报错Table does not exist。检查发现dwd.user_behavior_dwd表确实存在,但dynamic-partition的time-unit: "DAY"要求 Doris 表的dt字段必须是DATE类型,而我们建表时用了VARCHAR(10)。解决方案:重建 Doris 表,dt DATE NOT NULL。
3.4 Demo 4:HTTP API 数据拉取并写入 PostgreSQL(外部系统对接场景)
很多 SaaS 系统只提供 REST API,没有数据库直连。SeaTunnel 2.3.12 的 HTTP Source 支持分页和 Token 刷新。
source: - http: url: "https://api.saaS-platform.com/v1/users" method: "GET" # 关键:分页参数,page 和 per_page 会自动递增 params: page: "${page}" per_page: "100" # 关键:Token 刷新逻辑,当响应 code=401 时触发 auth: type: "bearer" token: "${access_token}" refresh-url: "https://api.saaS-platform.com/v1/auth/refresh" refresh-method: "POST" refresh-params: refresh_token: "${refresh_token}" transform: - json: # 关键:API 返回是 { "data": [...], "meta": {...} },只取 data 数组 field: "data" sink: - jdbc: url: "jdbc:postgresql://pg-prod:5432/analytics" driver: "org.postgresql.Driver" user: "seatunnel_pg" password: "xxx" table: "users_saaas" # 关键:PostgreSQL 的 upsert 语法 upsert-key: ["id"] upsert-statement: | INSERT INTO users_saaas (id, name, email, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email, updated_at = EXCLUDED.updated_at为什么这样写?
params: page: "${page}"是 SeaTunnel 的变量语法,会自动从 1 开始递增,直到 API 返回空数组。auth.refresh-*配置让 Seatunnel 在 Token 过期时自动调用刷新接口,拿到新 Token 后重试原请求,无需人工干预。
我踩的坑:
API 返回的updated_at是"2024-01-01T12:00:00+08:00",PostgreSQL 报错Can't convert string to timestamp。因为 JDBC Driver 默认不支持带时区的 ISO8601 格式。解决方案:在transform加sql转换:
- sql: query: "SELECT id, name, email, PARSE_DATETIME(updated_at, 'yyyy-MM-dd''T''HH:mm:ss.SSSSSSXXX') AS updated_at FROM default"3.5 Demo 5:CSV 文件批量导入 HDFS(传统数仓迁移场景)
别笑,这是最刚需的场景。很多银行还在用 Oracle,导出 CSV 给数据团队,要求“今天下班前入库”。
source: - filesystem: path: "file:///data/incoming/users_*.csv" format: "csv" # 关键:CSV 头部字段名,必须和文件实际一致 schema: "id:int,name:string,email:string,created_at:string" # 关键:跳过第一行(header) csv.read-header: true transform: - sql: # 关键:CSV 的 created_at 是 '2024-01-01 12:00:00',转成 Hive 支持的 timestamp query: "SELECT id, name, email, TO_TIMESTAMP(created_at, 'yyyy-MM-dd HH:mm:ss') AS created_at FROM default" sink: - hive: metastore.uris: "thrift://hive-metastore:9083" database: "ods" table: "users_csv" # 关键:Hive 分区字段,按日期自动创建 partition: "dt=${date}" # 关键:写入 HDFS 的路径,${date} 会被替换为当前日期 hdfs.path: "hdfs://namenode:8020/user/hive/warehouse/ods.db/users_csv/dt=${date}"为什么这样写?
filesystem.path支持通配符*,可以一次读取多个 CSV 文件。partition: "dt=${date}"是 SeaTunnel 的内置变量,${date}默认格式是yyyyMMdd,和hdfs.path里的${date}保持一致。
我踩的坑:
Hive 表里created_at字段全是NULL。日志显示Cannot cast string to timestamp。原因是 CSV 文件里created_at有空值,TO_TIMESTAMP函数遇到空字符串会返回NULL,但 Hive 的timestamp类型不允许NULL。解决方案:在sql里加COALESCE:
SELECT id, name, email, COALESCE(TO_TIMESTAMP(created_at, 'yyyy-MM-dd HH:mm:ss'), CURRENT_TIMESTAMP) AS created_at FROM default3.6 Demo 6:Redis Hash 数据同步到 MySQL(缓存一致性场景)
电商商品详情页用 Redis Hash 存item:123,字段包括price、stock、version。MySQL 里要保持最终一致。
source: - redis: host: "redis-prod" port: 6379 password: "xxx" # 关键:Redis Key 模式,支持通配符 pattern: "item:*" # 关键:Redis 数据类型,Hash 需要指定 field >INSERT INTO items_cache (id, price, stock, version) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE price = ?, stock = ?, version = ?3.7 Demo 7:Flink SQL 实时计算并写入 Kafka(复杂事件处理场景)
这是唯一用到 Flink SQL Engine 的 Demo。比如实时计算“用户 5 分钟内下单金额 > 10000 元”的预警。
env: execution.engine: "flink" execution.parallelism: 2 source: - kafka: bootstrap.servers: "kafka-prod:9092" group.id: "seatunnel-fraud-detect" topic: "orders_raw" format: "json" schema: | {"type":"object","properties":{"user_id":{"type":"string"},"amount":{"type":"number"},"order_time":{"type":"string"}}} transform: - flink-sql: # 关键:Flink SQL 的 TUMBLING WINDOW query: | SELECT user_id, SUM(amount) AS total_amount, MAX(order_time) AS last_order_time, COUNT(*) AS order_count FROM ( SELECT user_id, amount, TO_TIMESTAMP(order_time, 'yyyy-MM-dd HH:mm:ss') AS order_time FROM default ) GROUP BY user_id, TUMBLING(order_time, INTERVAL '5' MINUTE) sink: - kafka: bootstrap.servers: "kafka-prod:9092" topic: "fraud_alerts" format: "json" # 关键:Kafka 的 key 用 user_id,便于下游按用户聚合 key.field: "user_id"为什么这样写?
execution.engine: "flink"显式指定引擎,避免和 Spark Engine 混淆。TUMBLING WINDOW是固定窗口,比 HOPPING 更适合告警场景,避免重复计算。
我踩的坑:
Flink Web UI 显示WindowOperator的numRecordsInPerSecond为 0。日志里有Could not find a suitable time characteristic。原因是order_time字段没设为 Event Time。解决方案:在flink-sql里加WATERMARK:
SELECT user_id, SUM(amount) AS total_amount, MAX(order_time) AS last_order_time, COUNT(*) AS order_count FROM ( SELECT user_id, amount, TO_TIMESTAMP(order_time, 'yyyy-MM-dd HH:mm:ss') AS order_time, WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND FROM default ) GROUP BY user_id, TUMBLING(order_time, INTERVAL '5' MINUTE)4. CDC 实时采集的终极挑战——不是“能不能跑”,而是“怎么稳住”
标题里“CDC 实时采集”是最大亮点,也是最易翻车的环节。很多团队跑通 Demo 后,一上生产就遇到:延迟从 100ms 涨到 10s、Binlog 位点卡住不动、Schema 变更后 pipeline 直接崩溃。2.3.12 提供了工具,但稳住它需要体系化思维。
4.1 延迟诊断:三层定位法(网络 → MySQL → SeaTunnel)
当 Grafana 显示seatunnel_cdc_latency_ms{job="mysql_orders"} > 5000,别急着重启。按顺序查:
网络层:
telnet mysql-prod 3306看连通性;ping mysql-prod看延迟;tcpdump -i any port 3306抓包看是否有大量TCP Retransmission。我遇到过一次,是云厂商安全组规则限制了 MySQL 的wait_timeout,导致 SeaTunnel 的 Binlog 连接被强制断开,重连耗时 3 秒。MySQL 层:登录 MySQL,查
SHOW PROCESSLIST,看是否有Binlog Dump线程卡在Sending binlog event to slave。再查SHOW MASTER STATUS,对比File和Position是否在增长。如果不增长,说明 Binlog 没产生新事件,问题在上游业务。SeaTunnel 层:看
logs/seatunnel-worker.out,搜索binlog position。正常应每秒打印一次,如:INFO c.a.s.c.m.b.BinlogSplitReader - Current binlog position: mysql-bin.000001:123456789
如果 10 秒没更新,说明 Reader 线程卡死。此时jstack <pid>看线程栈,90% 是RocksDB的writeBatch阻塞,需调大state.backend.rocksdb.write-buffer-size。
