背景
前段时间运维报告过来说线上pod重启了,原因是OOM了,然后mentor把这个活交给我了,让我排查一下,当时心想坏了没做过,呜呜,最后我还是一个人解决了,过程中有一些坑,踩了一些狗屎文章的误导,最后还是突发奇想给解决了,下面说一下我解决这个问题的思路,以及过程中踩的一些坑。
狗屎方案:想办法打node快照,获取堆栈文件来分析
当时想法是我写一个接口,在内存达到50% 或者70%的时候运维写一个脚本访问我的接口,接口生成内存的快照
const v8 = require("v8");
const fs = require("fs");
function POST() {
const fileName = `heap-${Date.now()}.heapsnapshot`;
const stream = v8.getHeapSnapshot();
const file = fs.createWriteStream(fileName);
stream.pipe(file);
file.on("finish", () => {
console.log("Heap snapshot saved:", fileName);
});
}
但是这个方案,会有几个现实的问题
- 即使我生成了堆栈文件 甚至上g 我也是第一次,我也看不懂啊,市面上的ai 分析 他也不可能分析好几个g的文件啊,而且生成的堆栈文件肯定是两三个一起扔给ai呀,这也不行呀
- 当我上到测试环境的时候,卧槽我访问这个接口 测试环境直接502了,当时我以为不是我的问题 ,我把接口下下来,测试环境又好了,卧槽勒。
后面我去看了一下node文档,这里附带原文:
Generating a snapshot is a synchronous operation which blocks the event loop for a duration depending on the heap size.
这v8的这个
getHeapSnapshot函数是同步的,并且会阻塞事件循环,并且获得内存快照是一个高密集cpu任务,当时想着天塌了,得亏没上线,这上线了不把线上给搞崩溃了啊。
狗屎方案二、三:Core Dump + llnode 或者开子进程
- 方案二:Core Dump 这个方案我没实现,因为要依靠运维在崩溃的时候拿到dump文件,并且我用llnode给他转一下,运维没啥时间,搞鸡毛,并且就算拿到了我也未必能分析出来
- 方案三:开子进程,这个方案不行的,因为你开了子进程,通过V8拿的内存快照是子进程的
最后我解决的方案:压测 + ai分析
我们既然是SSR页面,那我完全可以写一个脚本,去压测我们服务器,看是哪个页面内存泄漏了,找到了页面,最后我让ai给我分析这个页面为什么内存泄漏了,这里压测也有限制:我们的SSR 服务是node启动的,他接收不了很大的并发,因此我采用串行的压测方式,压测的方式我选择的autocannon比较友好 并且一个页面压测2000次,每个页面压测间隔5分钟。
const autocannon = require('autocannon')
const DOMAIN = '测试域名'
// const DOMAIN = 'http://localhost:3001'
const PATH = [
// 根页面
// '/',
// ai-search-redirect
// '/ai-search-redirect',
// apps
// '/apps',
// '/apps/uni-fold',
// chat
// '/chat',
// '/chat/[sessionId]',
// '/chat/1b548632-6674-4248-ac2d-dafaece01cec',
// journal-library
// '/journal-library/[[...home]]',
// '/journal-library/chinese/technology',
// '/journal-library/area/Geoscience/地球科学/1?cover=https%3A%2F%2Ficon.bohrium.com%2Farea%2F1.png&tab=International',
// '/journal-library/curated/Top%20Picks%20in%20Medicine/医学精选/46?tab=International',
// '/journal-library/journal-home?journalId=5997&tab=International',
// '/journal-library/mine/subject',
// '/journal-library/search/journal?keyword=JCR&tab=International',
// '/journal-library/subject/chemistry,%20physical/物理化学/119?tab=International',
// paper-details
// '/paper-details/changes-in-elbow-flexion-emg-morphology-during-adjustment-of-deep-brain-stimulator-in-advanced-parkinson-s-disease/811877395857408001-10889',
// patent-details
// 专利仅内部使用 不用测试
// scholar
// '/scholar',
// '/scholar/874a90de/Claudia_Felser',
// '/scholar/search?searchKey=xin%20wang',
// sciencepedia
'/sciencepedia',
// '/sciencepedia/feynman/special_functions-gamma_function_definition_as_an_integral',
// '/sciencepedia/feynman/keyword/ultraproducts',
// '/sciencepedia/agent-tools',
// '/sciencepedia/agent-tools/rxn4chemistry_rxn-reaction-preprocessing',
// '/sciencepedia/agent-tools/c/subdomain-chemistry_%26_computational_chemistry-reaction_prediction_%26_retrosynthesis_ecosystem',
// '/sciencepedia/field/feynman/special_functions',
]
// token 拿测试环境的
// 自己测试 替换/chat/[sessionId] sessionId
const token = 'eyJhbGciOiJSUzI1NiIsIn'
function runTest(url) {
return new Promise((resolve, reject) => {
console.log(`\n🚀 Testing: ${url}\n`)
const instance = autocannon({
url,
connections: 1,
pipelining: 1,
amount: 2000,
headers: {
Authorization: `Bearer ${token}`
}
})
autocannon.track(instance)
instance.on('done', (result) => {
console.log(`✅ Finished: ${url}\n`)
resolve(result)
})
instance.on('error', reject)
})
}
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function main() {
for (const path of PATH) {
const url = `${DOMAIN}${path}`
// 打印 URL 和开始时间
const now = new Date()
console.log(`\n Starting test for: ${url}`)
console.log(` Start time: ${now.toLocaleString()}\n`)
// 执行压测
await runTest(url)
// 等待 5 分钟(300000 ms)再执行下一个 URL
console.log(`⏳ Waiting 5 minutes before next URL...\n`)
await wait(5 * 60 * 1000)
}
console.log('🎉 All tests done')
}
main()
当然这个DOMAIN替换成自己项目的域名,最后我用node跑这个脚本,然后我去美美的睡觉了
等第二天起早看一下这个图,一看就知道发生内存泄漏了,但是我跑去美美睡觉了,怎么知道是哪个页面呢?没事的运维有
openresty ,我去openresty 去看我知道是哪个页面发生了内存泄漏了
最后知道了哪个页面,把页面甩给cursor了,最后cursor找到是BroadcastChannel没有关闭
后续措施
一说到前端内存泄漏,大家想到的就是网上说的 要么没close,要么闭包,要么监听器没有移除,这些乱七八糟的谁不知道啊,要么就吆喝着拿内存快照,拿来分析,简直狗屎没水平。
最后我怎么预防后面的小伙伴写出这样的代码呢?总不可能后面内存又炸锅了让我去分析吧
我在cursor里面加了skills,避免类似事件再发生了
最后
最后聊一下时间和心态线吧:这个是年前的事情,年前一周再探索方案一,觉得可行,年当周生病了就没去上班,年后请了5天假期,想着回来美美把方案一上线,结果出幺蛾子,后面回来的一周,夹杂着其他需求和这个并行着,差不多一个月时间了,网上给的方案是真的狗屎啊,搞得谁不知道内存泄漏咋发生的,问题是要怎么解决啊,后来我解决了,我倒着分析,假如我看node内存泄露是否能通过堆栈文件看出来?答案是否定的,我后面去在本地压测和添加--inspect参数,发现BroadcastChannel确实不断在创建和监听,没有回收,但是泄漏的很小,压根找不到是他,更别说什么函数了,崩溃。