记一次难忘的Doris排障:FECPU飙升背后的"元凶"竟不是配置
做技术的朋友大概都有过这样的经历:系统出了性能问题,周围的人第一反应就是"是不是配置写错了"或者"内存是不是不够用了"。说实话,这种直觉挺正常的,毕竟配置参数和资源大小是最容易被想到的因素。但最近帮团队排查的一个Doris写入性能问题,却让大伙儿彻底改变了这个认知——原来问题可以藏在完全意想不到的地方。
事情是这样的:线上Doris的FE节点CPU突然飙得很高,大家一开始都觉得肯定是fe.conf哪里配错了,或者FE的堆内存8G不够用,要不要直接加到12G。结果查了一圈下来,发现配置本身没有任何硬伤,真正的问题出在JDBC参数、MyBatis生成SQL的方式,以及批次大小这三个因素的"化学反应"上。这个发现过程其实挺有意思的,值得跟做技术的朋友们好好聊聊。
让人头疼的CPU飙升
问题的起点是业务反馈Doris写入变慢了,FE的CPU占用率明显偏高。团队小伙伴赶紧上去看:top命令显示FEJava进程CPU确实很高,内存方面大概9G左右驻留。初步判断可能是内存不足导致频繁GC,从而把CPU打满了。
但仔细一看jstat-gcutil的输出,大家都愣住了:FGC等于0,Old区占用也不高,YoungGC倒是有的,但没有出现FullGC风暴。这个数据说明什么?说明FE节点根本不是因为GC问题导致的CPU高,而是真实的业务负载打到了FE上。这个发现让我们不得不重新思考排查方向。
业务侧是通过jdbc:mysql://FE:16003连接Doris的,用MyBatis执行批量INSERTINTO...VALUES语句写入一张UniqueKey表,并且配置了function_column.sequence_col=version来实现UPSERT语义。代码里除了批量写入,还有预查版本、状态轮询、单条更新状态这些操作——简单来说,Doris在这里不仅承担了OLAP批量写入的职责,还承担了一部分OLTP任务状态表的工作。
配置排查走不通
按照常规思路,我们先拉出了fe.conf开始逐行检查。端口配置正常,JAVA_OPTS_FOR_JDK_17参数合理,Xmx8192m/Xms8192m设置一致,lower_case_table_names=2虽然是比较特殊的设置,但跟这次的CPU飙升问题没有直接关系。
查完一遍之后,团队成员面面相觑:配置本身没有明显的错误。那问题到底在哪里?这时有经验的老同事提了一个思路:配置没问题,不代表使用方式没问题。FE热起来不一定是因为"配错了",也可能是"用法把FE打热了"。这句话点醒了不少人。
写入方式的深层问题
把代码翻出来仔细看之后,发现了几个之前没太注意的问题。首先,批量写入之前会先查一遍版本——每次真正写入前都要执行类似SELECTbiz_key,versionFROMxxxWHEREbiz_keyIN(...)的查询。也就是说,每个批次实际上对应至少2条SQL:先查一遍,再插一遍。其次,代码里还有listPendingRows(200)轮询加单条markSuccess/markFail更新的逻辑。
到这一步,大家开始意识到一个中期判断可能是对的:FECPU高,未必是2000条批次本身太大,更可能是Doris同时承担了大量"小查询+小更新+批量插入"的混合流量导致的。
为了进一步定位问题,我们开始调整JDBC参数。原来连接的URL大概是这样的:useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8。这组参数能正常连接,但对Doris这种场景不太友好,缺少了一些关键的性能优化参数。
意外出现的新问题
加上useServerPrepStmts=true、useLocalSessionState=true、rewriteBatchedStatements=true、cachePrepStmts=true以及sessionVariables=group_commit=async_mode这些参数后,本来以为问题应该能缓解了,结果冒出了一个新异常:Parameterindexoutofbounds.44930isnotbetweenvalidvaluesof1and44928。
这个异常栈里有几个关键信号:ServerPreparedStatement.checkBounds、ClientPreparedStatement.setString、Parameterindexoutofbounds。从这些信息来看,问题已经不是Doris表结构或者UniqueKey+sequence_col配置的问题了,而是JDBC驱动在绑定PreparedStatement参数的时候直接崩了。
这时候排查方向不得不再次修正。我们开始深入分析MyBatis的batchUpsert到底是怎么工作的。结果发现,这根本不是标准JDBC的addBatch/executeBatch模式,而是通过
找到真正的根因
当useServerPrepStmts=true打开之后,这种超长的多VALUESSQL会走服务端预编译。一旦批次太大,参数个数会急剧膨胀。举个例子,如果一行有24个字段,批次2000行就是48000个参数,批次8000行就是192000个参数级别——这已经远远超出普通批量场景的合理范畴了。
所以问题的本质终于清晰了:不是useServerPrepStmts=true这个参数本身有问题,而是开启之后,批次大小需要重新评估。超长SQL加上服务端预编译再加上过多参数,三者叠加才是真正的"元凶"。
后来的解决思路其实很简单:保留useServerPrepStmts=true,因为它对PreparedStatement复用有实际价值,有助于降低FE的SQL解析和执行计划生成压力;同时把批量大小从8000降到2000。这个调整看似简单,但精准命中了根因——MyBatis拼接超长VALUESSQL时,批次过大会导致参数规模失控,配合serverpreparedstatement后驱动层先行崩溃。
经验萃取与实践建议
回过头来看这次排障过程,有几点经验特别想跟做技术的朋友们分享。
第一点,看问题要讲究顺序。看到CPU高先别急着怀疑配置,先用jstat看看有没有FullGC,再决定要不要动FE堆。CPU高不等于JVM有问题,这个先后顺序很重要。
第二点,DorisFE热不一定是你配错了。有时候是SQL太碎,有时候是批次不合理,有时候是JDBC参数没配对,还有时候是把Doris当成了队列表和状态表来用——这些才是真正应该关注的地方。
第三点,MyBatis的"批量"跟真正的JDBCbatch不是一回事。看起来你在做batch,实际上MyBatis只是拼了一条超长的SQL,这对Doris来说压力是完全不同的。
第四点,useServerPrepStmts=true不是万能加速开关。它有它的适用场景,如果SQL本身已经长到离谱,再开它反而容易把问题提前暴露出来。
第五点,批次大小真的要结合实际情况来定。不是越大越好,也不是越小越好,而是要结合字段数量、SQL形态、驱动行为、服务端处理方式一起来评估。
这次排障最有价值的地方,不是最终改对了哪个参数,而是把问题从"怀疑FE配置"一步步收敛到了"JDBC+MyBatis+批次策略"的真实根因上。这种思维方式上的转变,可能比解决这次具体问题更有意义。
