把 AI 用到线上运维:可行、有效,前提是喂足信息——一次 Full GC 排障实录

一、做法:人采集,AI 分析,人把关

事发当晚,告警是"Full GC 连续 2 次达到 46 次/分"。运维同事按一个现成脚本,在出事进程上抓了一包现场数据打成 tar.gz:GC 日志、jstatjstack(连打 5 次)、top -Hfree/iostat/netstatjinfo -flags,外加一份 2.8GB 的堆转储(heap dump)

然后把这台装了 Eclipse MAT 的机器、这包现场数据、一句告警描述,一起丢给 AI Agent,让它自主分析。分工很简单:

  • :现场采集(AI 没法登线上机)、最后把关结论与修复方案;
  • AI:读现场数据、选工具、跨证据交叉印证、定位根因、给修复------全程自主,每一步留下可复核的证据。

先说结论:AI 在约十几分钟内,把根因落到了"一个 1GB 的 byte[]、卡在某个下载线程、由 EntityUtils.toByteArray 累积、走的是任务上线接口、对应某项目某版本"。但中间最有意思的,是它怎么把一串证据串成根因、在哪里绕了弯、又怎么被人一句话点回正轨------这才是本文要讲的重点。


二、AI Agent 的工作模式:一条五步证据链

让 AI 干排障,最怕它"张口就猜"。所以先给它一条硬规则:根因没定位完之前不许给修复,每一步都要留证据。 它据此跑出一条五步证据链:

bash 复制代码
① GC 日志  →  ② jstat  →  ③ top/jstack  →  ④ MAT 解析 heap dump  →  ⑤ strings + 源码
 频率/回收不掉  触发原因   GC线程占比/热点栈    泄漏对象+线程+栈        业务元数据+代码定位

这其实是资深工程师的排障套路,只不过现在由 AI 自主驱动。下面按这五步看它实际做了什么。


三、全过程复刻(AI 视角)

第一步:读 GC 日志------先定量,再判断"回收不掉"

AI 上来先读 GC 日志头部,确认 JVM 配置:JDK 1.8、+UseParallelGC、堆 8GB(年轻代 ~2.75GB、老年代 ~5.33GB)、开了压缩指针。

接着统计频率:当前日志段 896 秒内 391 次 Full GC ≈ 26 次/分 ,峰值 46 次/分(对上告警),累计已 1158 次,每次停顿 ~0.8 秒、间隔 ~1.3 秒。

但它没止步于"Full GC 很多",而是抓了一个更关键的信号------每次 Full GC 之后老年代能不能降回低位。答案是降不回:

指标
老年代容量 5.33 GB
Full GC 老年代 ≈ 4.77 GB(≈ 90%)
Full GC 老年代(活地板) 1.60--2.79 GB
每次 Full GC 实际回收量 ~1--3 GB

也就是说,Full GC 每次都能回收掉 ~1--3GB,但始终有 ~1.6--2.8GB 清不掉 ------这块"地板"就是下一步要找的东西。Metaspace 稳定 ~85%,排除元空间膨胀。jstat gccauseErgonomics 也印证了"老年代被反复填到 ~90% 触发 Full GC"。

第二步:top/jstack------GC 线程吃满 CPU,热点栈直指"下载进内存"

top -H 里 CPU 大户全是 GC task thread,业务线程无一上榜(被停顿冻住)。jstack 里有一条 RUNNABLE 业务线程的栈顶非常扎眼,AI 一眼锁定:

scss 复制代码
at org.apache.http.util.ByteArrayBuffer.append(ByteArrayBuffer.java:87)
at org.apache.http.util.EntityUtils.toByteArray(EntityUtils.java:139)
at com.xxx.bdms...HttpUtil.download(...)
at com.xxx.bdms...AzJobManager.azkabanDownLoad(...)
at com.xxx.bdms...ProjectStoreServiceImpl.doUpload(...)

有个线程正用 EntityUtils.toByteArray 把某个 HTTP 响应整体读进一个 byte[],当前还卡在 ByteArrayBuffer.append(还在往里塞字节)。 嫌疑对象浮现。

第三步:解析 2.8GB 堆------该用标准工具时,别造轮子

这一步其实先走了一段弯路,值得如实记下。2.8GB 的 heap dump 要找出"哪个对象最大、被谁持有",标准工具就是 Eclipse MAT,这几乎是常识------AI 本该第一时间用 MAT、发现没装就直接请人装。

可它一开始动了点别的心思,想自己手写一个 HPROF 解析器,理由是"比把 2.8GB 全塞进 MAT 更轻、想要个快直方图"。结果栽了:HPROF 里实例对象的字段数据长度有个隐晦的边界 case(开了压缩类指针时,实例数据会多带几个对齐字节),解析器反复对不齐,跑几 MB 就错位。

这时用户一句话点醒:"内存分析就是用 MAT,这是常识,没现成工具么?需要的话我给你装。" ------这才是对的姿势。AI 于是请用户装了 MAT,用命令行 ParseHeapDump.sh 跑 Leak Suspects,中间踩了几个坑(后文讲),最终 MAT 一次给出结论:

某异步上传工作线程持本地变量合计 1,084,409,064 字节(≈1.03GB,占 live 堆 65%) ,其中单个 byte[] 实例占 1,084,383,248 字节 ,由 EntityUtils.toByteArray → ByteArrayBuffer.append 累积。dump 时整个 live 堆才 1.5GB

对象、线程、调用栈三者齐备。这一步的教训很直白:对有标准工具的活,AI 应首选标准工具、缺了就提醒人装,而不是自己造轮子------解析器这种边界 case 极多的东西,MAT 一次就成。这也是把 AI 用到运维里的一个前提:得让它知道"这行当的标准工具链是什么",别放任它另起炉灶。

第四步:strings + 源码------把"1GB byte\[\]"推进到"某个业务任务"

MAT 说得清"哪个对象",但说不清"哪个业务任务"。AI 用了一招极其轻巧的取证:把 2.8GB 堆当文本 strings 出来,直接 grep 业务值。 原理是------那个 1GB 的 byte[] 是一段被原样存进堆的下载响应(本质是个 ZIP 包),里面的 URL 参数、文件名都是明文 ASCII:

bash 复制代码
strings -n 5 heap_dump.hprof | grep -aE 'action=download.*project='      # → 锁定确切下载请求
strings -n 5 heap_dump.hprof | grep -aE '\.(job|flow|project)$' | sort -u  # → 包内任务文件名

第一条直接命中当时正在下载的请求:?action=download&project=<某项目>&version=<某版本>&needDownloadResource=true;第二条揭示了这个包为什么有 1GB------里面装着 1.7 万+ 个任务文件 ,是个体量极大的聚合项目,needDownloadResource=true 把全部资源一起打包。

再对照源码补全调用链:某"任务上线"接口 → 上线服务 → 上线处理链 postProcess → 异步触发 asyncUploadProjectZipdoUpload 里先 azkabanDownLoad 把整包下载进 byte[],再转存到外部存储。业务意图很单纯:上线时把当前版本任务包下载下来转存归档,本不该整体驻留内存。

go 复制代码
任务上线接口(POST)
  → 上线服务.releaseProjectForStage → 上线处理链.postProcess
    → 异步线程池(10,20) "project-store-upload-N"
      → ProjectStoreServiceImpl.doUpload
        ├─ byte[] zipBytes = azJobManager.azkabanDownLoad(...)   ← 整包下载进 byte[]
        │    └─ HttpUtil.download → EntityUtils.toByteArray   ★ 1GB byte[] 在此累积
        │         └─ ByteArrayBuffer.append                   ★ 当前卡这一帧
        ├─ IOUtils.copy(new ByteArrayInputStream(zipBytes), os) ← 1GB 再 copy 一次
        └─ storageAdapter.createAndWriteFile(...)             ← 转存外部存储

四、两次"人机协同"的高光时刻

这里必须诚实记两笔------它们恰恰说明 AI 排障的价值,以及人为什么不能缺席。

第一次:AI 给了个顺口的结论,人一句话把它打回重想。 AI 一度把根因表述成"1GB 的 byte\[\] 把老年代占满了"。这话听着顺,但用户当场质疑:"单个 1GB 的任务,不应该把 8GB 堆占满吧?" 这一问点中了要害。AI 立刻回去重看 GC 数字:dump 时 live 堆才 1.5GB,离"占满"远着呢。它据此重新推导,给出了准确的机制(下一节详述):不是"占满",而是"活对象抬地板 + 翻倍扩容造瞬时垃圾"。

这说明:AI 能快速给结论,但结论需要人 review 方向;一个好问题比一次"重新分析"更值钱。

第二次:人追问"是哪个接口触发的",AI 顺藤摸瓜追到底。 AI 最初只追到 Service 层的 asyncUploadProjectZip。用户问:"是哪个接口触发的呢?" AI 于是往上追调用方:OnlineHandlerChain.handleOnlinereleaseProjectForStage ← Controller,最终定位到两个任务上线接口(POST /xxx/task/release 的两种鉴权变体)。用户又追问:"是不是有多个这样的任务都在占?" AI 去查 jstack,发现 10 个上传线程里只有 1 个在飞,其余 9 个空闲;再 grep 源码,发现那个"整包读进 byte\[\]"的下载封装有 5 个调用方(发布/回滚/迁移...),风险面比想象大。

这说明:顺着人的好问题,AI 能把分析一步步推进到更深的层次;交互式追问是激发 AI 深挖的有效姿势。


五、真正的根因机制:1GB 没占满堆,凭什么 Full GC 不停?

这是本次最有价值的一段,也是被用户那句话逼出来的纠正。直觉上"1GB 对象→占满堆→Full GC"是错的------live 堆才 1.5GB。真相是两个机制叠加:

机制一:活对象抬高"地板",Full GC 也回收不掉。 那个 1GB 的 byte[] 卡在下载线程上------下载/上传全程都在 ByteArrayBuffer.append 累积、尚未返回,所以它一直是可达活对象。Full GC 时它回收不掉,把老年代 live 地板抬到 ~1.6--2.8GB(即 GC 日志里"Full GC 后仍剩 1.6--2.8GB")。

机制二:ByteArrayBuffer 翻倍扩容,凭空多造出 ~1GB 瞬时垃圾(本文最反直觉的一点)。

这是连经验丰富的开发也容易忽略的一笔账。EntityUtils.toByteArray 内部用 ByteArrayBuffer,容量按 2 倍增长(...256MB→512MB→1GB)。按翻倍增长,读 1GB 进内存,实际累计要分配约 2GB 的 byte[](1+0.5+0.25+...),而每次翻倍后旧 buffer 立刻变成垃圾------也就是说,光为凑出那 1GB 的活对象,就凭空多产生了约 1GB 的瞬时垃圾。

更要命的是这些数组太大(256MB、512MB 量级):年轻代的 survivor 区才几十 MB,根本放不下,于是它们会被直接晋升到老年代 ,只能等 Full GC 来清------这恰好把它们和"老年代被填到 ~90%"直接挂上了钩。它们叠加应用自身的并发分配(这服务当时还在处理上万个任务元数据,SQL 解析/JSON/DB 读取本身分配就不轻),让老年代在每次 Full GC 后约 1.3 秒内又被填到 ~4.77GB(≈90%) ,触发 Ergonomics Full GC。

这条机制最容易被忽略,是因为直觉上"读 1GB 占 1GB 内存"------但翻倍扩容的几何级数,会让"瞬时分配量 ≈ 2 倍最终大小",垃圾也就跟着翻倍。任何用 toByteArray/ByteArrayOutputStream/ArrayList 翻倍增长来接"未知大小的流"的写法,都背着这个隐性成本。
🤖 备注:这条反直觉的机制,是 AI agent 自己探索出来的。 起因是 AI 一度给出"1GB 占满堆"这个顺口却错误的结论,被用户一句"1GB 不该占满 8GB 堆"打回;AI 随即回去重看 GC 数字(每次 Full GC 实际回收 ~1--3GB、活地板却稳定在 ~1.6GB),再对照 ByteArrayBuffer 的翻倍增长行为,自己推导出了"活对象抬地板 + 翻倍造瞬时垃圾 + 大数组直接晋升老年代"这套完整机制------这不是查文档查来的预设结论,而是 AI 跨"GC 日志 ↔ 堆转储 ↔ 源码"三类证据交叉印证、逐步推出的隐性机制。这也正是 AI agent 在排障中的价值所在:它能顺着一个好问题,把人也会忽略的机制挖出来。

机制三:循环。 Full GC 回收掉 ~1--3GB 瞬时垃圾,却回收不掉那个还在 append 的 1GB 活 buffer → 地板降不下去 → 下载继续 → 瞬时垃圾再填满 → Full GC 再来。每 ~1.3 秒一次、累计 1158 次------这就是"连续 Full GC、回收不掉"的真相。

一句话:不是"1GB 占满了堆",而是"1GB 活 buffer 抬高地板 + 其翻倍扩容持续造瞬时垃圾,让老年代被反复填到 ~90% 触发 Full GC 循环,而 Full GC 又动不了那个活 buffer"。

修复方向因此唯一:别再把整包读进 byte[],改成流式 (HTTP body → 临时文件 → 远程存储),常驻内存从 1GB 降到几十 KB。并且要在工具层改------那个下载封装有 5 个调用方,一处改、全处受益,否则下次回滚个大项目又会复现。至于"调大堆/换 G1"治标不治本,超大包下多大堆都会被反复填到 ~90%。


六、把 AI 用到线上运维:可行、有效,前提是喂足信息

先说这篇文章真正想讲的结论:把 AI 广泛用于线上运维排障,是可行的、效果也挺好,有些核察它做得比人还专业------但这一切的前提,是给它喂足信息。

这次之所以能成,恰恰因为信息喂得足:运维用脚本一把抓全了现场数据(GC 日志、jstack、top、2.8GB 堆转储),机器上装好了 MAT,源码也在手。有了这些,AI 才能把活干下来。反过来,如果只甩一句"服务 Full GC 了,帮我看看",没有堆转储、没有源码,AI 也巧妇难为无米之炊------这不是 AI 不行,是信息不够。前提条件,是这套方法能不能成立的命门。

而在"喂足信息"的前提下,这次确实看到几个比人手动做更顺、甚至更专业的点:

  • 挖出连老手也容易忽略的隐性机制:那个"读 1GB 实际分配 ~2GB、翻倍造 ~1GB 瞬时垃圾"的机制,是 AI 被追问后回去重看 GC 数字自己推出来的(见第五节)。这种跨"GC 日志↔堆转储↔源码"的交叉印证,人做要翻来覆去对半天,AI 一气呵成;
  • 从 GB 级堆里捞业务元数据strings + grep 把"1GB byte\[\]"推进到"某项目某版本某接口",几条命令出,人做也累;
  • 多证据一条链、自主串起来:GC 日志、jstack、MAT、源码 grep,一套证据链自己串,互相咬合才下结论;

当然它也不是万能:会绕弯路(该用 MAT 这类标准工具时却想自己造轮子,被人点回)、会顺口给"1GB 占满堆"这种顺理却不准的结论------所以人 review 关键结论(尤其"跟数字对不对得上")、人提好问题、人提示用标准工具链,是把 AI 用好的必要动作。这不是"能不能用",而是"怎么用好"的姿势问题。

要照这次复现:① 平时备好现场采集脚本(附后),出事一把抓 tar.gz;② 分析机装好 MAT(命令行 ParseHeapDump.sh-Xmx ≥ 转储一半);③ 把现场包 + 告警交给 AI,要求按五步法出证据链、每步留证据不许猜;④ 在"结论跟数字对不对得上"处人 review,顺着"哪个接口/哪个任务/是不是多个"让它深挖。


七、小结

这次排障想传递的就一句话:把 AI 广泛用到线上运维里,是可行、效果也不错的,有些核察比人还专业------但前提是喂足信息。 这次能成,是因为现场采集包、堆转储、源码都备齐了,AI 才有料可分析;它也确实挖出了一个连老手也容易忽略的隐性机制,过程还全程可复核。它不是一键出根因的黑盒,会绕弯路(该用标准工具时另起炉灶)、会顺口给不准结论,所以"人采集、人把关、人追问、人提示标准工具链"仍不可少------但这恰恰是"怎么用好",而不是"能不能用"的问题。把采集脚本备好、MAT 装好、学会用"五步证据链"去追问,运维同学就能让 AI 在这类排障里真正派上用场。

相关推荐
触底反弹1 小时前
🔥 从 MySQL 到 Milvus:用 AI 日记本项目搞懂向量数据库和 RAG
javascript·人工智能·面试
众人皆醒我独醉1 小时前
AI 怎么"理解"一个词的意思?—— Embedding 是把整个世界压缩成一组数字
人工智能·面试
Oo9201 小时前
从零搭建 AI 日记本:Milvus 向量数据库 + RAG 实战
人工智能
C++、Java和Python的菜鸟1 小时前
第7章 后端Web实战(Tlias系统)
java
夜郎king1 小时前
解决AI图文解析偏差:CodeBuddy多模型交叉校验+腾讯地图Skill经纬度定位实战
大数据·人工智能
智慧物业老杨2 小时前
物业费公共收益维修资金三类资金分账的合规管控
大数据·人工智能·物联网
kingcjh972 小时前
具身智能数据处理的工程化实践
人工智能·数据挖掘
dadaobusi2 小时前
Sniper
人工智能