天天看点

#yyds干货盘点#记一次线上崩溃问题的排查过程

关注公众号【高性能架构探索】,后台回复【pdf】,免费获取计算机经典书籍

大家好,我是雨乐!

前几天,突然收到报警,线上服务崩溃,然后自动重启。

由于正值双十一期间,业务以稳定为主,线上服务崩溃,这可不是一件小事,赶紧登陆线上服务器,分析原因,迅速解决。

借助这篇文章,记录下整个崩溃的分析和解决过程。

收到报警

上午上班后,正在划水,突然收到邮件报警,如下:

#yyds干货盘点#记一次线上崩溃问题的排查过程

问题分析

马上登录线上服务器,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<char, std::char_traits<char>, std::allocator<char> > 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<char, std::char_traits<char>, std::allocator<char> > 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对象的时候,因此确定就是因为全局变量多线程访问导致的。一个开源的项目,里面竟然有全局变量,这在规范里面是不被允许的。

然后谷歌搜索了下大家都有过类似的问题,再次吐槽下。

#yyds干货盘点#记一次线上崩溃问题的排查过程

问题解决

解决崩溃问题,首先需要看看是不是使用方式的问题,或者找一个线程安全的接口,再或者用其他库进行替换。

修改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是路人。

以候捷在<<STL 源码剖析>>上的一句话作为本文的结束语:

源码面前,了无秘密。

共勉。