前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记一次openssl使用不当引发的内存泄漏

记一次openssl使用不当引发的内存泄漏

原创
作者头像
鹅厂老五
发布2024-06-16 21:32:13
2480
发布2024-06-16 21:32:13
举报
文章被收录于专栏:LinuxLinux

前言:本文记录一起第三方库使用不当引发的内存泄漏的定位过程。在日常工作中新写服务或者代码引发的内存泄漏还是相对较好定位的,因为这种情况下改动范围相对明确。但有时候也会面临从未动过的服务发生内存泄漏,这意味着这个服务很早就引入了内存泄漏,引发内存泄漏的范围相当不聚焦,这个时候很多同学就不知道如何下手。本文主要展现:①展现面对内存泄漏问题的定位及思考过程 ②综合利用wiresharks、jmeter等工具进行效果验证。

1、问题起因

问题的起因非常简单,部门在抓质量;为此相关服务都配置了健康检查了相应的告警。

告警配置后没过多久就收到了一个服务重启的告警后,观察tke的告警事件如下。

tke监控层面内存走势如下。根据掌握的信息这个服务很久没有进行人为的发布或者重启操作了,但是监控层面又的确出现了周期性的内存上涨→到达某阈值后服务重启并迅速降低的曲线。

显然大概率是内存泄漏了。发生这种情况首先考虑的是最近是不是有什么改动上线导致了内存泄漏,但是经过比对最近一年改动的代码很少而且完全没有引入内存泄漏的可能。这就意味这这个服务很有可能子上线以来就一直存在这个问题。这也为问题定位带来了困难即范围不聚焦,全量服务代码包括引用的基础库、第三方库都有可能是问题的始作俑者。

既然范围太广人肉看代码不现实那么借助工具来聚焦范围就是再显然不过的事情了—— valgrind。

2、内存泄漏工具valgrind聚焦范围

(1)valgrind介绍

Valgrind是一套Linux下,开放源代码的仿真调试工具的集合。Valgrind由内核以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;而其他工具则类似于插件 (plugin),利用内核提供的服务完成各种特定的内存调试任务。Valgrind的体系结构如下图所示。

Valgrind的主要工具包括:

①memcheck memcheck是最常用的工具,所有对内存的读写都都可以被它检测到即malloc()/free()/new/delete的调用都会被捕获。它能检测以下问题:

1.对未初始化内存的使用; 2.读/写释放后的内存块; 3.读/写超出malloc分配的内存块; 4.读/写不适当的栈中内存块; 5.内存泄漏,指向一块内存的指针永远丢失; 6.不正确的malloc/free或new/delete匹配; 7.memcpy()相关函数中的dst和src指针重叠。

这些问题往往是C/C++程序员最头疼的问题;显然我们这里用到的就是memcheck了。

除此之外还有callgrindcachegrindhelgrindmassif等工具,可以用以更细致入微的观察程序运行,例如cache命中情况、多线程下的竞态问题、堆栈使用情况等等这里就不做详细介绍。

(2)valgrind安装

代码语言:javascript
复制
​(1)wget http://www.valgrind.org/downloads/valgrind-3.14.0.tar.bz2  #下载安装包
注:最好安装更新的版本,否则可能出现莫名其妙的问题。
(2)tar xvf valgrind-3.14.0.tar   #解压安装包。
(3)cd valgrind-3.14.0 #进入文件夹。
(4)make               #从makefile中读取指令。
(5)make install       #执行安装。
(6)配置环境变量,便于调用。
1)cd /etc/profile.d 目录下,创建文件valgrind.sh
2)在文件里面填入如下内容:
#!/bin/bash
VALGRIND_ROOT=/home/Lyndon/valgrind-3.14.0
VALGRIND_INCLUDE=/usr/local/include/valgrind
VALGRIND_LIB=/usr/local/lib/valgrind
export VALGRIND_ROOT VALGRIND_INCLUDE VALGRIND_LIB
3)chmod +x valgrind.sh      #再修改一下valgrind的权限即可。

(3)valgrind在spp下的使用

valgrind在spp下的使用稍微有些不同,主要有一下注意事项。

代码语言:javascript
复制
(0)如果是tke机器的话重新部署一个实例,注意不要有健康检查啥的。
(1)通过织云包脚本 admin/stop.sh all ,直接杀掉进程会自动重启
(2)不要启动 ctrl 进程,该进程会自动拉起 proxy 和 worker 进程
(3)手动启动 proxy 进程,bin路径下:(直接root用户操作)
./spp_uc_msg_record_read_svr_online_proxy  ../etc/spp_proxy.xml
(4)手动启动 worker 进程,bin路径下 (注:感觉可以不用启动普通worker!!!)
./spp_uc_xxx_xxxxx_xxxx_svr_online_worker ../etc/spp_worker1.xml
(5) 利用 valgrind 启动其中一个 worker 进程    
valgrind --tool=memcheck --leak-check=full --show-reachable=yes --log-file=memmory.txt ./spp_uc_msg_record_read_svr_online_worker ../etc/spp_worker1.xml
(6)需要停止程序不可以直接kill -9,否则memmory文件中可能看不到最后的信息;应该用kill -10 pid并等待一小会儿。

注:执行期间是memmory.txt是看不到太多东西的;kill -10 pid后缓一会就有了。

(4)结果与分析

观察报告其中明确出现了“definitely lost”,这应该就是内存泄漏的位置了。

分析调用链路,valgrind分析发现存在openssl相关的内存泄漏。路径为cos_helper.cpp→ BuildUrl → ComputeSign → HMAC_Init_ex → EVP_DigestInit_ex → 宏定义OPENSSL_malloc → CRYPTO_malloc()。

最开始看到这儿可能也会有些懵,主要是因为openssl是现成的第三方库难道第三方库存在内存泄漏?如果是这样的话岂不是发现了一个openssl的bug,想想还有些小激动呢。不过经过搜索发现并没有人在使用openssl库的时候遇到类似的问题,再转念一想使用如此广泛的库现在被发现存在内存泄漏的可能性也确实不大。不论如何目前至少可以肯定的一点是我们已经可以把目光聚焦在前人们封装的cos_helper.cpp上了。既然如此就了解下openssl库的使用以及看看前人的cos_helper是如何使用这个库的。

在openssl的官网的摘要处看到有如下函数调用其中的HMAC_CTX_cleanup和HMAC_cleanup吸引了我的注意。

其作用如下:主要用以释放相关资源,是必须要调用的。

在回过头看前人封装的cos_helper果然缺少了这个调用。

注:memcheck输出分析。

  • definitely lost:指确定泄露的内存,应尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存则会报这个错误。
  • indirectly lost:指间接泄露的内存,其总是与 definitely lost 一起出现,只要修复 definitely lost 即可恢复。当使用了含有指针成员的类或结构时可能会报这个错误
  • possibly lost:指可能泄露的内存,大多数情况下应视为与 definitely lost 一样需要尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存的起始地址,但可以访问其中的某一部分数据,则会报这个错误。
  • still reachable:如果程序是正常结束的,那么它可能不会造成程序崩溃,但长时间运行有可能耗尽系统资源,因此笔者建议修复它。如果程序是崩溃(如访问非法的地址而崩溃)而非正常结束的,则应当暂时忽略它,先修复导致程序崩溃的错误,然后重新检测。
  • suppressed:已被解决。出现了内存泄露但系统自动处理了。可以无视这类错误。

3、效果验证

接下来就是要验证修复前后的效果了。验证思路是非常简单的,即部署不同的实例观察内存走势即可。但是在具体实施过程中会发现没有那么简单。主要是因为该内存泄漏出现的频率比较低。对于出现内存泄漏的这个服务只有一条协议会访问cos_helper、而且只有在访问到图片/文件的时候才会走到相关逻辑处。即便部署到线上环境可能也要观察好几周才能看到一点效果,而且更重要的问题是直接在线上环境这样验证是不负责的。当然另一个思路就是放到测试环境验证,但是测试环境的请求量更少即便观察一年可能也不会有明显的对比效果。

当然这也难不倒俺!额外部署服务实例针对性的压测不就好了嘛!

说到压测肯定就会想到jmeter了。但是由于这条协议是一个部门自定义的inner协议,并非http、websocket这类通用协议。这就需要jmeter直接发送udp或者tcp的数据包,当然我们也可以按照协议的规则自己取拼接数据然后转换成二进制数据包,不过这样比较麻烦。还有一个方式就是直接触发一个专门访问图片/文件的请求,然后利用利用tcpdump+wireshark抓取数据包直接利用这个抓取到的现成的请求包作为jmeter的udp请求发至目标实例就好了。

(1)抓包工具wireshark

​对于发送16进制数据的情况,我们通过tcpdump抓包把获得的“Hex Stream”直接贴过来即可。如下。

(2)压测工具jmeter

给jmeter安装好UDP Request插件后按如下方式在jmeter GUI下配置如下执行计划,并保存为.jmx文件。

接下来找一台内网机器部署安装jmeter环境后就可以启动jmeter非GUI模式下的压测任务了。

./jmeter.sh -n -t ../yace/303203udptest.jmx -j ../result/303203udptest.log -l ../result/303203udptest.jtl -e -o ../result/303203udptest

如下分别为修复前后服务的内存压测走势对比。

修复前压测内存走势

修复后压测内存走势

注:针对上述验证环节其实很多人认为在通过valgrind去分析是否依然存在“definitely lost”不就可以了吗。当然这个思路也是可以的,不过效果可能没有本文利用jmeter压测更直观;尤其是对于第三方学习者来说更是如此。而且前面也说了内存泄漏部分的代码本身也不属于主干路径,存在遗漏的可能;这也是为什么这个问题能存在这么久没被发现的原因。其实这里的压测就是针对性的让程序逻辑走到内存泄漏位置处,人为的去放大比对效果。显然,如果你的程序本身就已经很明显了就完全不需要进行这个“放大”操作了。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、问题起因
  • 2、内存泄漏工具valgrind聚焦范围
    • (1)valgrind介绍
      • (2)valgrind安装
        • (3)valgrind在spp下的使用
          • (4)结果与分析
          • 3、效果验证
            • (1)抓包工具wireshark
              • (2)压测工具jmeter
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档