问题
线上有个用C++写的b服务,随着运行时间的增减,占用内存也会不断增加,最终kernel会触发OOM,导致服务不可用。
所使用的物理机内存为160GB,以上图中一个broker进程就占了大约6.6GB的内存,运行了10天后内存还在持续增长,存在内存泄漏的问题。
排查过程
代码review
主要以下几个方面:
- malloc/free、new/delete、new []/delete []是否都配套使用了
- 构造函数里申请的内存,在析构函数里都有相应的释放
- 指针的错误使用,没有释放内存。
但没有发现有问题的地方。
内存泄漏检查工具
valgrind
valgrind介绍
Valgrind
是一个开源工具包,提供了许多调试和分析工具。
- Memcheck detects memory-management problems
- Cachegrind a cache profiler
- Callgrind Cachegrind + callgraphs(调用关系图)
- Massif a heap profiler
- Helgrind a thread debugger which finds data races in multithreaded programs
- ……
代码编译
set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -std=c++11 -g -O0 -mavx2 -Wall -DDEBUG_RING”)
- -g include debugging information so that Memcheck’s error messages include exact line numbers
- -O0 禁止编译器的优化,确保valgrind的输出信息的准确性
Memcheck的使用
valgrind --leak-check=yes --track-origins=yes /root/ufile/XsqUFileBroker-set7/UFileBroker -c /root/ufile/XsqUFileBroker-set7/config-set7.ini
线上不建议开启,能使性能下降10~30倍,线上实测下来最多有下降200倍。
在正常压测下,并不会出现内存泄漏的错误信息。但当把broker依赖的底层服务osd重启后,立刻出现以下的错误信息。
错误信息1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ==31907== 773,314 (9,216 direct, 764,098 indirect) bytes in 288 blocks are definitely lost in loss record 1,997 of 2,023 ==31907== at 0x4C2A5B3: operator new(unsigned long) (vg_replace_malloc.c:342) ==31907== by 0x63FD3B: uevent::ConnectionLibevent::Init() (connection_libevent.cc:67) ==31907== by 0x63AECA: uevent::ConnectorLibevent::Connect() (connector_libevent.cc:30) ==31907== by 0x5FBAAE: BrokerServerHandle::GetConnection(std::shared_ptr<uevent::ConnectionUevent> const&, std::string const&, unsigned int) (broker_server.cc:587) ==31907== by 0x600644: BrokerManagerHandle::GetConnection(std::shared_ptr<uevent::ConnectionUevent> const&, std::string const&, unsigned int) (broker_manager.cc:87) ==31907== by 0x5E0194: BrokerGetHandle::GetECOnePieceData(int) (broker_get.cc:312) ==31907== by 0x5E1EBF: BrokerGetHandle::GetECObjectRequest() (broker_get.cc:371) ==31907== by 0x5E3913: BrokerGetHandle::EntryInit(std::shared_ptr<uevent::ConnectionUevent> const&, std::shared_ptr<MessageHeader> const&) (broker_get.cc:110) ==31907== by 0x5F7AC1: BrokerServerHandle::MessageDispatchHandle(std::shared_ptr<uevent::ConnectionUevent> const&, std::shared_ptr<MessageHeader> const&) (broker_server.cc:173) ==31907== by 0x5FAAA3: BrokerServerHandle::MessageReadHandle(std::shared_ptr<uevent::ConnectionUevent> const&) (broker_server.cc:108) ==31907== by 0x5CE779: std::_Function_handler<void (std::shared_ptr<uevent::ConnectionUevent> const&), void (*)(std::shared_ptr<uevent::ConnectionUevent> const&)>::_M_invoke(std::_Any_data const&, std::shared_ptr<uevent::ConnectionUevent> const&) (functional:2071) ==31907== by 0x640D94: operator() (functional:2471) ==31907== by 0x640D94: uevent::ConnectionLibevent::HandleReadEvent(int, std::shared_ptr<uevent::ConnectionUevent> const&) (connection_libevent.cc:114)
|
问题:某个osd进程关闭后,broker对保存长连接而占用的内存,没有释放。
修复方法:调用delete释放。
注意:
这里的connectors类型是std::unordered_map<ConnectorKey, uevent::ConnectorUevent*, ConnectorKeyHash> connectors_
obj_handle→connectors_.erase(it)只会删除指针本身,并不会销毁指针指向的内容,所以导致内存泄漏。
参考https://stackoverflow.com/questions/39605945/remove-element-from-unordered-map-without-calling-destructor-on-it
错误信息2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ==31907== 4,405,094 (507,936 direct, 3,897,158 indirect) bytes in 1,716 blocks are definitely lost in loss record 2,018 of 2,023 ==31907== at 0x4C2A5B3: operator new(unsigned long) (vg_replace_malloc.c:342) ==31907== by 0x5FB78C: BrokerServerHandle::GetConnection(std::shared_ptr<uevent::ConnectionUevent> const&, std::string const&, unsigned int) (broker_server.cc:578) ==31907== by 0x600644: BrokerManagerHandle::GetConnection(std::shared_ptr<uevent::ConnectionUevent> const&, std::string const&, unsigned int) (broker_manager.cc:87) ==31907== by 0x5E0194: BrokerGetHandle::GetECOnePieceData(int) (broker_get.cc:312) ==31907== by 0x5E1EBF: BrokerGetHandle::GetECObjectRequest() (broker_get.cc:371) ==31907== by 0x5E3913: BrokerGetHandle::EntryInit(std::shared_ptr<uevent::ConnectionUevent> const&, std::shared_ptr<MessageHeader> const&) (broker_get.cc:110) ==31907== by 0x5F7AC1: BrokerServerHandle::MessageDispatchHandle(std::shared_ptr<uevent::ConnectionUevent> const&, std::shared_ptr<MessageHeader> const&) (broker_server.cc:173) ==31907== by 0x5FAAA3: BrokerServerHandle::MessageReadHandle(std::shared_ptr<uevent::ConnectionUevent> const&) (broker_server.cc:108) ==31907== by 0x5CE779: std::_Function_handler<void (std::shared_ptr<uevent::ConnectionUevent> const&), void (*)(std::shared_ptr<uevent::ConnectionUevent> const&)>::_M_invoke(std::_Any_data const&, std::shared_ptr<uevent::ConnectionUevent> const&) (functional:2071) ==31907== by 0x640D94: operator() (functional:2471) ==31907== by 0x640D94: uevent::ConnectionLibevent::HandleReadEvent(int, std::shared_ptr<uevent::ConnectionUevent> const&) (connection_libevent.cc:114) ==31907== by 0x4E47A13: event_base_loop (in /usr/lib64/libevent-2.0.so.5.1.9) ==31907== by 0x630C14: uevent::PosixWorker::Running(std::string const&) (posix_stack.cc:192)
|
问题:某个osd进程关闭后,但数据库里还没有标记成BAD。此时broker还会尝试连接该osd,因连接失败不会发送请求,这部分逻辑正确。但是,失败的连接占据的内存没有释放。
修复方法:调用delete释放。
修复以上2个错误后,用valgrind已经没有其他错误信息了。但是线上broker的内存,在osd没有重启的情况下,依然还会不断增长。这说明还有其他内存泄漏的地方。
Dump内存并查找问题
观察线上broker服务内存,在上次版本更新一周后,占用内存分布大致在1~5GB之间。
查看broker的虚拟内存分布,有80%的内存都是同一块内存空间,且是不同broker的共同现象。
自己通过压测环境,也能查到类似现象。
压测环境:6块盘;线上环境:72块盘。
dump内存
所以这块内存是什么呢?可以dump下来查看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| gdb attach 3096
(gdb) dump memory /tmp/3096_broker.dump 0x7fdf33400000 0x7fdf33410000
strings /tmp/3096_broker.dump ZZZZ7_0:4096_broker-32576-25146--7-64115 ZZZ$ ZZZZ7_0:4096_broker-32576-25146--7-64280 ZZZ$ ZZZZ7_0:4096_broker-32576-25146--7-65085 ZZZZ7_0:4096_broker-32576-25146--7-64116 7_0:4096_broker-32576-25146--7-64355 7_0:4096_broker-32576-25146--7-64605 7_0:4096_broker-32576-25146--7-64117 ZZZZ7_0:4096_broker-32576-25146--7-64227 ZZZZ7_0:4096_broker-32576-25146--7-64235 ZZZZ7_0:4096_broker-32576-25146--7-64118
|
因为压测时上传的数据都是ZZZZ…格式的,可以判断跟上传文件的内容有关。
valgrind massif的使用
再用valgrind massif的工具去剖析heap的占用情况。
1 2
| valgrind --tool=massif /root/ufile/XsqUFileBroker-set7/UFileBroker -c /root/ufile/XsqUFileBroker-set7/config-set7.ini ms_print massif.out.907
|
发现有lru_cache的部分占用,但为了分析内存占用,lru_cache已经禁止使用了,但为什么还会有呢?
lru_cache的问题
再去查看跟lru_cache相关的代码,
7_0:4096_broker-32576-25146–7-64605的格式正好是定义的BROKER_CACHE_KEY的格式,
1 2
| #define BROKER_CACHE_KEY \ to_string(SET_ID) + "_" + to_string(offset_) + ":" + to_string(data_size_) + "_" + objectid_
|
并进一步分析lru_cache相关的代码,最后发现又是unordered_map的错误使用。
每次get数据的时候,代码里都会判断下在lru_cache里是否已经存在;如果不存在,则插入该数据。
1 2 3 4 5 6 7 8 9 10 11 12
| lru_cache.h
private: std::unordered_map<K, Node<K, T> *> umap; void Put(K key, T data) { ...... Node<K, T> *node = umap[key]; if (node){ ...... } }
|
但是std::unordered_map<>::[key]在查不到时,会自动插入该数据。
Returns a reference to the value that is mapped to a key equivalent to key
, performing an insertion if such key does not already exist.
到这里就真相大白了,罪魁祸首就是std::unordered_map<>::[key]的使用不当,每次get一个没有在cache里的key时,就会插入一条数据,导致内存不断增大。
TCP Connection的buffer问题
再压测时,pmap看时,还是有一块几个GB的内存空间,dump出来也看不出来是什么。
Buffer 问题
那么到底是哪里占用的呢?
想到golang里面的heap profile,搜到C++里也能用类似的工具gperftools。
具体使用过程参考c++ profile的大杀器-gperftools的使用。
发现压测后占用3GB的内存中,95%都是每条TCP连接的buffer。
网络框架里有一个buffe模块r,维护tcp conncetion占用的Buffer。
Buffer底层由std::vector实现,是一块连续的内存;可以自适应增长;保存数据并提供相应的访问函数。
上层代码,只要对Buffer发送数据或者接收数据,而不用管理具体的存放和容量等。
然后查看Buffer的代码,发现retrive(回收内存的函数)的实现逻辑有问题。
修改后再次压测,对比结果如下:
6磁盘的测试环境,跑压测脚本,证明修改的逻辑有效。
|
改进前 |
改进后 |
retrieve后,>=8Mb的buffer出现的次数 |
7219 |
449 |
72磁盘的大数据测试环境,连续压测2h:
|
改进前 |
改进后 |
占用内存RSS |
4.5GB |
3.0GB |
Broker服务的内存模型
进一步验证:既然是tcp conncetion占用的Buffer,那么把连接断开后,是否会释放buffer呢?
查看跟brorker服务的连接分为两类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| Broker服务的内存模型 进一步验证:既然是tcp conncetion占用的Buffer,那么把连接断开后,是否会释放buffer呢?
查看跟brorker服务的连接分为两类:
// 总共的连接数 [root@bj-ufile-storage-set-osd ~]# netstat -antp | grep 8219 | wc -l 986 // broker --> osd的连接 [root@bj-ufile-storage-set-osd ~]# netstat -antp | grep 8219 | grep :200 | wc -l 711 [root@bj-ufile-storage-set-osd ~]# netstat -antp | grep 8219 | grep 172.22.18.9:20011 | wc -l 10 [root@bj-ufile-storage-set-osd ~]# netstat -antp | grep 8219 | grep 172.22.18.9:20011 tcp 0 0 172.22.18.9:39996 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:37220 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:11742 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:30720 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:58094 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:10776 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:45502 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:40626 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:11806 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s tcp 0 0 172.22.18.9:37098 172.22.18.9:20011 ESTABLISHED 8219/ufile-broker-s // goproxy --> broker的连接 [root@bj-ufile-storage-set-osd ~]# netstat -antp | grep 8219 | egrep "172.22.14.33|172.22.14.35|172.22.14.32|172.22.14.34|172.22.14.36|172.22.14.37|172.22.18.23|172.22.18.18" | wc -l 271
|
重启大数据set的osd和goproxy后,内存释放。
所以衡量broker服务的占用内存可以用公式:
broker_rss = 3MB * (osd连接数+goproxy连接数) ≈ 4MB * osd连接数
再经过反复压测,内存不再增长,问题得到解决。
一般排查步骤的总结
命令
lsof 列举出正在使用的文件
lsof,这个工具用于排查是否存在很在很多超出预料的文件的情况,比如打开某文件未关闭,建立很多的socket连接等等。当然,发现问题只能靠眼力劲了。
lsof -p #查看进程打开的文件情况。
pmap 查看进程内存概要
pmap,用于查看进程的内存映像信息, 发现内存中大块的占用所在,以及分析内存可能存在的异常。
pmap {pid}
pmap -x {pid}
pmap -X {pid}
或者
cat /proc/{pid}/maps, 以及cat /proc/{pid}/smaps,都可以查看内存段的具体起始位置
perf
可以先捕获数据,然后进行性能分析,然后得到可疑的点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| perf record -g -e cpu-clock -p 5545 # 记录进程 5545 的相关性能信息 perf report -i perf.data # 读取刚刚记录的数据,可以显示出种操作的占用情况,如下 Samples: 908 of event 'cpu-clock', Event count (approx.): 227000000 Children Self Command Shared Object Symbol + 32.27% 0.00% java libpthread-2.17.so [.] start_thread + 32.27% 0.00% java libjvm.so [.] java_start + 26.54% 0.00% java libjvm.so [.] ConcurrentG1RefineThread::run + 26.54% 0.11% java libjvm.so [.] ConcurrentG1RefineThread::run_young_rs_sampling + 25.77% 5.62% java libjvm.so [.] YoungList::rs_length_sampling_next + 22.58% 0.55% java [kernel.kallsyms] [k] tracesys + 11.01% 0.00% java perf-5545.map [.] 0x00007f553ec1e981 + 10.79% 0.44% java libjvm.so [.] JVM_Sleep + 9.36% 0.55% java libjvm.so [.] G1CollectorPolicy::update_incremental_cset_info + 8.70% 0.55% java libjvm.so [.] os::sleep + 8.26% 0.00% java [unknown] [k] 0xee83b0ac00709650 + 8.15% 0.99% java libpthread-2.17.so [.] pthread_cond_timedwait@@GLIBC_2.3.2 + 7.93% 0.00% java perf-5545.map [.] 0x00007f553f7f7d30
|
gdb 调试工具dump出可疑内存
1 2 3 4 5 6 7 8 9
| gdb attach <pid> # 先连接到进程中 dump memory /path/dump.bin 0x0011 0x0021 # dump 出内存段的信息,具体要 dump 的内存段地址,可以借助之前pmap 或 cat /proc/<pid>/smaps 或 cat /proc/<pid>/maps 中指示的地址段得出
更好的方式 gdb --batch --pid {PID} -ex "dump memory native_memory.dump 0x7f7588000000 0x7f7588001A40"
strings /path/dump.bin | less # 查看内存内容, 相信你能从中发现一些不一样的东西 strings -n 10 /path/dump.bin | less # 只查看>=10B字符串的行
|
以上这些命令,在不需要改变线上服务的二进制或服务启动方式下,就可以使用,比较方便。如果以上手段都没有效果,再用更复杂的工具。
工具
valgrind
以上已经介绍过,这里就不再说明了。
需要注意的是10~30倍的性能损耗,在线上一般无法接受使用。
gcc
gcc 命令行参数 -fsanitize=address -fno-omit-frame-pointer
新版本的gcc(gcc49)提供了很好的内存访问检查机制,实践中发现对性能的影响居然比Valgrind小很多。在实践中 Electric Fence 和 Valgrind 严重影响了程序的性能,难以触发内存访问问题,而gcc的-fsanitize=address编译参数解决了大问题,唯一的缺点是gcc高版本才支持,而实践中,生成环境的代码都是老版本编译器编译的。
这里有gcc版本问题的讨论。
https://github.com/Raymo111/i3lock-color/issues/79
bcc
可以输出内存火焰图.
具体参考
http://www.brendangregg.com/FlameGraphs/memoryflamegraphs.html
http://www.brendangregg.com/ebpf.html#bcc%22
对os有要求,centos仅限于7.6。
https://github.com/gperftools/gperftools
对性能影响小,可以直接压测,但建议有问题时开启,不要一直运行。
参考链接
- Valgrind’s Tool Suite
- The Valgrind Quick Start Guide
- The Memory Layout of a 64-bit Linux Process
- 内存泄漏排查攻略之:Show me your Memory
- Linux 环境下多线程 C/C++ 程序的内存问题调试