一、疑难杂症
从工作到现在,近几年在写C++大规模分布式系统相关,会遇到最头疼的几类问题,特别是新写大模块,或者重构功能基本都会遇到,如下:
- 内存泄漏
- 内存写坏
- 死锁
- 并发导致状态不对或者结果错误
…
上述几类问题,还可能互相夹杂影响,比如因为并发场景考虑不周全,任务引用计数获取和变化不原子,进而导致内存无法释放,结果不正确。
面对大规模C++工程,总会遇到上述问题,记录一些总结,方便查阅以及后续补充。
二、问题
2.1 内存泄漏
内存泄露有很多排查手段:
1. 检测工具类:
比如valgrind, valgrind是一个动态分析程序的工具框架。Valgrind工具可以自动检测许多内存管理(比如memcheck 模块)和线程错误。具体原理,这里不再赘述。从个人经验而言,valgrind对于中小规模程序检测特别有效,但是对于大规模系统,从曾经几次经验而来,并没有很好的效果,比如一些内存泄露场景,可能是需要高并发跑很久才能出现,而使用valgrind会很大影响程序的运行效率
2. 内存模块打标
这是个人觉得最为有效的使用方法,简而言之,对于每个内存占用的任务或者模块,需要有标记标识,同时后台支持打印这些MOD的内存占用。实现方式有很多,比如改造所有的内存分配器类似如下:
Allocator allocator;
// 初始化传入
allocator.init(int MOD_ID...)
// 分配内存传入
allocator.alloc(int MOD_ID...)
该方式在持续有内存泄露的时候,通过查看每个MOD的内存占用,很多情况下可以迅速定位具体的模块和任务,从而缩小排查的范围。
但是仍然也有例外,曾经查过一个内存泄露问题,MOD是数组类型,然后各处模块都用了数组类型,基本很难定位,该问题最终花了好几天查出来,也是个很有意思的问题。
3. eBPF & bcc
借助于内核提供的分析工具
BPF Compiler Collection (BCC)是基于eBPF的Linux内核分析、跟踪工具。
参考链接:BCC
BCC工具集如下:
其中通过memleak工具可以有效排查很多问题。但是也有一些限制:比如需要特定的内核版本,同时对应用程序有一定的影响。
4. 编码技巧
比如使用智能指针;比如设计任务或者类的析构函数,一定要尝试去释放所有内存
2.2 内存写坏
1. 工具类
(1) 比如valgrind;
(2) 比如Address Sanitizer(一个快速的内存错误检测工具)
(3) 比如借助一些工具,进行代码的静态检查,是否存在越界,是否存在线程局部变量过大等等
2. 程序运行检查
(1) 比如使用mprotect,参考之前一次排查文档:C+±如何排查内存写坏
(2) 程序关键数据结构,设计majic number, 特别是分配内存和释放内存,每次都检查是是否一致。设计校验和checksum等等
(3) vmalloc区域中广泛采用的guard page
3. 一些分析手段
类似之前的c++ 排查一次内存错乱问题
2.3 死锁
死锁问题很常见:一般通过pstack观察程序的堆栈,即可分析到现场。
死锁原因本质上是单个或多个模块因为加锁,导致形成资源的依赖。
场景比如:
- 单个线程重复加读锁,一个模块已经加锁了,但是因为逻辑复杂,调用一些函数,继续加该读锁,导致死锁
- 两个线程,因为加锁顺序,导致互相持有对方想获取的锁,类似如下场景:
存在锁lockA, lockB
线程A 获取lockA成功 尝试获取lockB
线程B 获取lockB成功 尝试获取lockA
导致死锁
- 调用层次不清楚
A模块获取lockA并持有
调用B模块或者B是回调,又尝试获取lockA
解决方式:
- pstack确认现场
- 大规模程序设计,一定有层次感,比如如果要获取一批同样的锁,按照统一的顺序来加锁