线上服务频繁崩溃,每次栈帧都不一样,看起来像玄学问题?其实很多时候都是同一类低级错误堆出来的。
问题现象
IoTHub进程半个月内连续崩溃了五次,每次崩溃的coredump栈帧都不一样:
- 一次崩在
getTopicPrx - 一次崩在
getPublisher - 一次崩在IceStorm内部的rethrow
- 一次崩在LOG_RECORD宏里
- 还有一次栈帧直接乱了,根本看不出来在干嘛
每次重启完好好的,跑个一两天就崩,特别玄学。
第一反应:是不是Ice中间件的bug?
一开始以为是Ice 3.7的bug,毕竟每次崩溃栈里都有Ice的函数。 后来把所有coredump翻出来对比,发现一个共同点:所有崩溃的栈帧,最上面一层都是 __cxa_rethrow。
也就是异常被重新抛出之后,没人接住,直接把进程干崩了。
找问题:五处裸rethrow
把整个IoTHub代码搜了一遍,找出来五处写得非常"潇洒"的catch块:
cpp
catch(...) {
throw; // 直接裸rethrow,啥也不干
}
还有更离谱的:
cpp
catch(const Ice::Exception& e) {
throw e; // 重新拷贝抛出
}
这种代码写的时候爽,觉得"我不在这儿处理,交给上层处理",但问题是------上层根本就没有处理。 异常一路往上抛,最后跑到框架的事件循环里,框架看到没接住的异常,直接abort。
第二个问题:getTopicPrx里的catch不完整
还有一个坑:getTopicPrx 函数内部虽然写了try-catch,但是catch不全:
cpp
try {
tpcPrx = topicManager->retrieve(topic);
} catch(const Ice::NoTopic&) {
// 处理NoTopic异常
}
// 没 catch Ice::ConnectionLost、SocketException 这些连接类异常
当IceStorm服务端重启、网络闪断的时候,抛出的连接异常根本没被catch住,直接就漏出去了,然后一路裸rethrow到进程退出。
修复:把所有漏出去的异常都接住
修复其实特别简单,就是把所有裸rethrow的地方,要么catch住打日志,要么在外面加一层兜底catch:
1. 五处裸rethrow,全部改成catch住打日志
原来的代码:
cpp
try {
// 一堆Ice操作
} catch(...) {
throw; // 直接抛出去
}
改成:
cpp
try {
// 一堆Ice操作
} catch(const Ice::Exception& e) {
LOG_ERROR("操作失败: " << e.what());
} catch(...) {
LOG_ERROR("未知操作失败");
}
业务逻辑上不需要处理这个异常,那就接住打个日志就行,别让它把进程干崩了。 分布式系统里,网络闪断、服务端重启都是常态,不能因为一次连接失败就把整个进程搞挂。
2. getTopicPrx外面包一层兜底
cpp
Ice::ObjectPrxPtr getTopicPrx(const string& topic) {
try {
// 原来的获取逻辑
} catch(...) {
LOG_ERROR("getTopicPrx failed: " << topic);
return nullptr; // 返回空指针,调用处自己判断
}
}
3. LOG_RECORD宏里的异常也接住
之前还有个坑:打日志的LOG_RECORD宏内部,打日志的时候抛了异常,结果日志没打成,反而把进程崩了。 这种地方更要兜底------日志系统是最后一道防线,不能因为打日志把进程搞挂。直接在宏内部catch住所有异常,啥也不做就行。
效果
改完之后部署上去,到现在跑了快一周,一次都没崩过。 之前平均两三天崩一次,现在稳得很。
总结
这种问题真的很常见:
- 写代码的时候觉得"这个异常我不在这处理,抛给上层",结果上层忘了处理
- try-catch写一半,只catch了想得到的异常,没想到的连接类、超时类异常直接漏出去
- 核心组件的初始化函数里没兜底,一次网络抖动就整个进程崩了
线上服务的稳定性,很多时候不是靠什么高深的架构,就是靠这些不起眼的细节:
- 所有对外调用的地方,异常都接住,别让它炸穿进程
- 核心组件失败了返回空指针,让业务层自己判断要不要重试
- 宁可多打两行日志,也别图省事写个裸rethrow
毕竟,线上崩一次,半夜爬起来排障的痛苦,谁经历谁知道。