一次 Doris FE CPU 飙高的排障实录从怀疑fe.conf到定位 MyBatis 超长批量 UPSERT最近排查了一个 Doris 写入性能问题过程挺典型一开始我怀疑是FE 配置有问题后来怀疑是单 FE 扛不住再后来怀疑是JVM GC最后才发现真正的问题并不在fe.conf本身而在JDBC 参数、MyBatis 批量 SQL 形态、以及批次大小的组合上。最终问题解决了结果也很朴素加上useServerPrepStmtstrue并把单次批量从 8000 降到 2000任务恢复正常。这篇就把整个排查过程完整复盘一下。一、问题背景业务侧通过jdbc:mysql://FE:16003/...连接 Doris使用 MyBatis 执行批量INSERT INTO ... VALUES (...)目标表是•Unique Key• 配置了function_column.sequence_col version也就是说逻辑上走的是UPSERT version 控新。当时线上现象是• FE CPU 明显偏高• 正在走 Doris MySQL 协议做大批量插入• 用户直觉怀疑是不是fe.conf配置有问题或者 FE 堆太小了我拿到的信息主要有三类1FE 进程资源占用top看到 FE Java 进程 CPU 一度很高内存大概 9G 左右驻留。2JVM GC 情况jstat -gcutil显示•FGC 0• Old 区占用不高• Young GC 有但没有 Full GC 风暴这说明一个很关键的事实FE 并不是先死在 GC 上。也就是说CPU 高更像是“业务负载真的打到了 FE”而不是“GC 把 CPU 吃满了”。3代码形态代码里核心是这种模式• 批量查源数据• 组装 DTO• 批量batchUpsert• 写入前还会先按bizKey查一遍已存在版本• 后面还有listPendingRows(200)轮询 单条markSuccess/markFail也就是说Doris 实际承担的不只是“批量 UPSERT”还承担了• 预查版本• 状态轮询• 单条更新状态这在排查里非常重要。二、第一阶段先怀疑fe.conf最开始看到 FE CPU 高最自然的怀疑就是• FE 堆是不是太小了•fe.conf有没有明显错误• 单 FE 会不会天然扛不住但把fe.conf过一遍后结论其实很快出来了这份fe.conf没有明显硬伤比如• 端口配置正常•JAVA_OPTS_FOR_JDK_17也正常•Xmx8192m/Xms8192m•lower_case_table_names 2虽然是特殊设置但和这次 CPU 飙高无直接关系如果只是从配置文件文本本身看看不出“配错了导致 FE 异常”的证据。这一步很重要因为它让我意识到这次大概率不是“配错了”而是“使用方式把 FE 打热了”。三、第二阶段开始怀疑写入路径而不是 FE 参数继续往下看代码和 SQL 形态后我的注意力逐渐从fe.conf转移到了写入方式上。因为这套逻辑并不是简单的“2000 条一批插进去就完了”而是1. 批量写入前先查版本类似这种逻辑queryExistingSalesVersionMapByBatch(bizKeys)queryExistingShipVersionMapByBatch(bizKeys)也就是每次真正写入前还要先打一遍SELECTbiz_key,versionFROM xxxWHERE biz_keyIN(...)这意味着每一个批次不是 1 条 SQL而是至少 2 条• 先查一遍• 再插一遍2. Doris 被当成“待处理队列表”后面还有这种逻辑listPendingRows(200)markSuccess(id)markFail(id,msg)也就是• 先查 200 条待处理• 再对每一条做单独状态更新这其实已经不是典型的 OLAP 写法了而是在用 Doris 扛一部分OLTP/任务状态表的职责。到这里我基本形成了一个中期判断FE CPU 高未必是 2000 条批次本身太大而更可能是 Doris 同时承担了大量“小查询 小更新 批量插入”的混合流量。四、第三阶段开始优化 JDBC 参数既然怀疑问题不全在 FE而在 JDBC 层和 SQL 形态那下一步自然就是看连接参数。当时原始 JDBC URL 大概是这样:16003/wlm_data_prd?useUnicodetruecharacterEncodingutf8allowMultiQueriestruezeroDateTimeBehaviorconvertToNulluseSSLtrueserverTimezoneGMT%2B8这串参数能连但对 Doris 这种场景不够友好尤其少了几个关键项•useServerPrepStmtstrue•useLocalSessionStatetrue•rewriteBatchedStatementstrue•cachePrepStmtstrue•sessionVariablesgroup_commitasync_mode于是开始往这个方向调。五、第四阶段新问题出现了——参数越调越报错参数一改问题没立刻结束反而冒出了一个新异常Parameter index out of bounds. 44930 is not between valid values of 1 and 44928异常栈里几个关键信号特别醒目•ServerPreparedStatement.checkBounds•ClientPreparedStatement.setString•Parameter index out of bounds这说明问题已经不是 Doris 表结构也不是Unique Key sequence_col配置而是JDBC 驱动在绑定 PreparedStatement 参数时炸了。这一刻排查方向又得修正。六、第五阶段定位真正根因——超长 SQL Server Prepared Statement继续分析后核心原因逐渐清楚了。我的 MyBatisbatchUpsert不是标准 JDBCaddBatch/executeBatch而是这种写法insertidbatchUpsertINSERT INTO xxx (...) VALUESforeachcollectionlistitemitemseparator,(...)/foreach/insert这意味着• MyBatis 会拼出一条非常长的INSERT ... VALUES (...),(...),...• 当useServerPrepStmtstrue打开后• 这条“超长多 values SQL”会走服务端预编译• 一旦批次太大参数个数会急剧膨胀举个最直观的估算• 假设 1 行 24 个字段• 批次 2000 行就是 48000 个参数• 批次 8000 行就是 192000 个参数量级这已经不是“普通批量”了而是超长预编译 SQL。所以当时虽然我一度怀疑是不是useServerPrepStmtstrue本身有问题但继续往下看后真正的结论应该更精确不是useServerPrepStmtstrue不能开而是开了以后批次不能再像以前那样无脑放大。问题本质是超长 SQL 服务端预编译 参数过多。这个判断后来被实际验证了。七、最终解法保留useServerPrepStmtstrue把批次从 8000 降到 2000最后的处理方式并不复杂1保留useServerPrepStmtstrue因为它本身对 Doris FE 来说是有价值的尤其在 PreparedStatement 复用上对降低 FE 解析和计划压力是有帮助的。2把批量从 8000 降到 2000这一刀非常关键。因为真正的问题不是“Doris 不能批量”而是• 你这不是标准 JDBC batch• 而是 MyBatis 拼接超长多 values SQL• 批次太大时参数数量会非常夸张• 配合 server prepared statement驱动层先撑不住了当批次从 8000 改成 2000 后问题直接消失。这说明根因判断是对的。八、最后回头看哪些怀疑是对的哪些是不对的对的怀疑1. 怀疑 FE CPU 高不只是配置问题这是对的。最后证明fe.conf不是主因。2. 怀疑 SQL 形态本身不够友好也是对的。尤其是 MyBatisforeach拼长 SQL 这一点非常关键。3. 怀疑批次过大这个也对但要说完整不是“2000 太大”而是“8000 对当前这种写法太大”。不完全对的怀疑1. 一开始怀疑 FE 8G 不够要不要上 12G这个怀疑不算离谱但不是主因。因为当时• 没有 Full GC• Old 区也不高• 真正先炸的是 JDBC 参数绑定也就是说哪怕 FE 改成 12G这次的Parameter index out of bounds也不会自己消失。2. 一度想直接否掉useServerPrepStmtstrue这个判断后来被修正了。更准确的说法应该是useServerPrepStmtstrue可以保留但必须和合理批次搭配使用。对 MyBatis 超长VALUESSQL 来说批次不能太大。九、这次排障后我总结出的几个经验1. 先看现象再决定怀疑方向CPU 高不等于 JVM 有问题。先看jstat先看有没有 Full GC再决定是否动 FE 堆。2. Doris FE 热不一定是 FE 配置错很多时候不是fe.conf写错而是• SQL 太碎• 批次不合理• JDBC 参数不匹配• 把 Doris 当成了队列表/状态表3. MyBatis 的“批量”不等于真正 JDBC batch这个坑很典型。看起来你在做 batch实际上只是拼了一条超长 SQL。这和PreparedStatement.addBatch()/executeBatch()不是一回事。4.useServerPrepStmtstrue不是万能加速按钮它有前提。如果 SQL 本身已经长到离谱再开它反而容易把问题提前暴露出来。5. 批次不是越大越好这次最终结果其实很能说明问题• 8000报错• 2000恢复正常批次大小要结合• 字段数量• SQL 形态• 驱动行为• 服务端处理方式一起看而不是只看“总行数”。十、最终结论这次问题最终证明不是 Dorisfe.conf明显配置错误也不是Unique Key sequence_col有问题。真正的核心在于• 业务使用 MyBatis 拼接超长INSERT ... VALUES• 开启useServerPrepStmtstrue后走服务端预编译• 当单批放大到 8000 时参数规模过大• 最终在 JDBC 驱动参数绑定阶段报错最终解决方案是•保留useServerPrepStmtstrue•将单批从 8000 调整为 2000• 必要时继续结合group_commit、批量策略、SQL 形态做后续优化排障到最后最有价值的一点不是“改对了哪个参数”而是把问题从“怀疑 FE 配置”一步步收敛到了“JDBC MyBatis 批次策略”的真实根因上。这比单纯解决一次报错更重要。