记一次线上崩溃问题的排查过程
共 3553字,需浏览 8分钟
·
2022-01-01 01:29
前几天,突然收到报警,线上服务崩溃,然后自动重启。
由于正值双十一期间,业务以稳定为主,线上服务崩溃,这可不是一件小事,赶紧登陆线上服务器,分析原因,迅速解决。
借助这篇文章,记录下整个崩溃的分析和解决过程。
收到报警
上午上班后,正在划水,突然收到邮件报警,如下:
问题分析
马上登录线上服务器,gdb调试堆栈信息。
堆栈信息如下:
#0 0x0000003ab9a324f5 in raise () from /lib64/libc.so.6
#1 0x0000003ab9a33cd5 in abort () from /lib64/libc.so.6
#2 0x0000003abcebea8d in __gnu_cxx::__verbose_terminate_handler() () from /usr/lib64/libstdc++.so.6
#3 0x0000003abcebcbe6 in ?? () from /usr/lib64/libstdc++.so.6
#4 0x0000003abcebcc13 in std::terminate() () from /usr/lib64/libstdc++.so.6
#5 0x0000003abcebcd32 in __cxa_throw () from /usr/lib64/libstdc++.so.6
#6 0x00000000006966bf in Json::throwRuntimeError(std::basic_string, std::allocator > const&) ()
#7 0x0000000000681019 in Json::Reader::readValue() ()
#8 0x000000000068277c in Json::Reader::readArray(Json::Reader::Token&) ()
#9 0x0000000000681152 in Json::Reader::readValue() ()
#10 0x00000000006823a6 in Json::Reader::readObject(Json::Reader::Token&) ()
#11 0x00000000006810f5 in Json::Reader::readValue() ()
#12 0x0000000000680e6e in Json::Reader::parse(char const*, char const*, Json::Value&, bool) ()
#13 0x0000000000680c52 in Json::Reader::parse(std::basic_string, std::allocator > const&, Json::Value&, bool) ()
......
在上面堆栈信息中可以看到在调用Json::Reader::parse后经过Json::Reader::readValue等调用,最后再调用Json::Reader::readValue时调用Json::throwRuntimeError抛出异常。
查看调用Json::throwRuntimeError函数的地方:
src/lib_json/json_writer.cpp: throwRuntimeError("commentStyle must be 'All' or 'None'");
src/lib_json/json_reader.cpp: if (stackDepth_g >= stackLimit_g) throwRuntimeError("Exceeded stackLimit in readValue().");
src/lib_json/json_reader.cpp: if (stackDepth_ >= features_.stackLimit_) throwRuntimeError("Exceeded stackLimit in readValue().");
src/lib_json/json_reader.cpp: if (name.length() >= (1U<<30)) throwRuntimeError("keylength >= 2^30");
src/lib_json/json_reader.cpp: throwRuntimeError(errs);
src/lib_json/json_value.cpp: throwRuntimeError(
src/lib_json/json_value.cpp: throwRuntimeError(
src/lib_json/json_value.cpp:JSONCPP_NORETURN void throwRuntimeError(JSONCPP_STRING const& msg)
src/lib_json/json_valueiterator.inl: throwRuntimeError("ConstIterator to Iterator should never be allowed.");
进入对应的函数
bool Reader::readValue() {
if (stackDepth_g >= stackLimit_g) throwRuntimeError("Exceeded stackLimit in readValue().");
++stackDepth_g;
... ...
--stackDepth_g;
return successful;
}
发现,在满足条件
stackDepth_g >= stackLimit_g
的时候,会调用throwRuntimeError,那么分析下stackDepth_g和stackLimit_g的声明定义:
static int const stackLimit_g = 1000;
static int stackDepth_g = 0;
问题基本明了:
❝stackDepth_g是个静态全局变量,线程不安全,而出问题的服务是多线程的
❞
在此准备吐槽下,笔者使用jsoncpp对象的时候,都是在线程内部一个局部变量,因此不会存在多线程访问同一个局部jsoncpp对象的时候,因此确定就是因为全局变量多线程访问导致的。一个开源的项目,里面竟然有全局变量,这在规范里面是不被允许的。
然后谷歌搜索了下大家都有过类似的问题,再次吐槽下。
![](https://filescdn.proginn.com/94fa63e5c096c74b30a38936fa7872d9/11efaccac699cf43aad9652b7a827ee8.webp)
问题解决
解决崩溃问题,首先需要看看是不是使用方式的问题,或者找一个线程安全的接口,再或者用其他库进行替换。
修改jsoncpp源码
为了解决线程安全的问题,有两种方案:1、在操作全局变量的时候,加上mutex,这个无非对性能要求很高的业务一个致命打击,为了提高业务性能,所以内部锁都使用其他方式进行了优化,比如mutex使用双buffer方式进行了替换,虽然mutex的一个加锁解锁过程也就100ns。
2、将上述全局变量放入Json对象中,这样局部变量就不会存在崩溃现象,但是这种方案存在一个问题,就是改动点很大,且需要大量严格的测试,放弃。
所以综合考虑上述两点,决定采用其他更安全可靠的方式来解决线上崩溃问题。
使用rapidjson
之所以采用rapidjson,是因为线上几十个服务,大部分都使用rapidjson,只有线上崩溃的这个服务等少数几个服务,因为历史原因,用的jsoncpp。
先介绍下rapidjson,下述内容来自于rapidjson官网:
RapidJSON 是一个 C++ 的 JSON 解析器及生成器。它的灵感来自 RapidXml。
RapidJSON 小而全。它同时支持 SAX 和 DOM 风格的 API。SAX 解析器只有约 500 行代码。
RapidJSON 快。它的性能可与 strlen() 相比。可支持 SSE2/SSE4.2 加速。
RapidJSON 独立。它不依赖于 BOOST 等外部库。它甚至不依赖于 STL。
RapidJSON 对内存友好。在大部分 32/64 位机器上,每个 JSON 值只占 16 字节(除字符串外)。它预设使用一个快速的内存分配器,令分析器可以紧凑地分配内存。
RapidJSON 对 Unicode 友好。它支持 UTF-8、UTF-16、UTF-32 (大端序/小端序),并内部支持这些编码的检测、校验及转码。例如,RapidJSON 可以在分析一个 UTF-8 文件至 DOM 时,把当中的 JSON 字符串转码至 UTF-16。它也支持代理对(surrogate pair)及 "\u0000"(空字符)。
不过rapidjson为了性能,在使用上面需要极其小心。
❝笔者之前踩过类似坑,局部字符串赋值给rapidjson对象,结果rapidjson并没有马上使用该局部字符串,而是在最后才会访问局部字符串里面的内容,而此时,局部字符串早已出了作用域,导致rapidjson获取的内容是乱码。
❞
结语
在使用开源项目的时候,一定要做好调研,必要的时候,能过一下源码实现(这个有点难😁),否则很容易入坑。
笔者在使用libcurl作为httpclient的时候,也因为触发了libcurl的一个bug,导致线上崩溃,当时连续通宵了两个晚上,才解决。
一入C++深似海,从此XX是路人。
以候捷在<
❝源码之前,了无秘密。
❞
共勉。