- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
本文分享自华为云社区《【网络编程开发系列】一种网络编程中的另类内存泄漏》,作者:架构师李肯。
最近在排查一个网络通讯的压测问题,最后发现跟“内存泄漏”扯上了关系,但这跟常规理解的内存泄漏有那么一点点不同,本文将带你了解问题的始与末。
面对这样的内存泄漏问题,本文也提供了一些常规的分析方法和解决思路,仅供大家参考,欢迎大家指正问题。
我们直接看下测试提供的issue描述:
简单来说,就是设备再执行【断网掉线-》重新联网在线】若干次之后,发现无法再次成功联网,且一直无法成功,直到设备重启后,恢复正常。
由于测试部有专门的测试环境,但是我又不想整他们那一套,麻烦着,还得整一个测试手机。
他们的测试方法是使用手机热点做AP,然后设备连接这个AP,之后在手机跑脚本动态开关Wi-Fi热点,达到让设备掉网再恢复网络的测试目的。
有了这个思路后,我想着我手上正好有一个 随身移动Wi-Fi,不就恰好可以实现无线热点吗?只要能实现在PC上动态切换这个360Wi-Fi热点开关,不就可以实现一样的测试目的吗?
具备以上物理条件之后,我开始找寻找这样的脚本。
要说在Linux下,写个这样的脚本,真不是啥难事,不过,要是在Windows下写个BAT脚本,还真找找才知道。
费了一会劲,在网上找到了一个还算不错的BAT脚本,经过我修改后,长以下这样,主要的功能就是定时开关网络适配器。
@echo off
:: Config your interval time (seconds)
set disable_interval_time=5
set enable_interval_time=15
:: Config your loop times: enable->disable->enable->disable...
set loop_time=10000
:: Config your network adapter list
SET adapter_num=1
SET adapter[0].name=WLAN
::SET adapter[0].name=屑薪鈺犘も晲协
::SET adapter[1].name=屑薪鈺犘も晲协 2
:::::::::::::::::::::::::::::::::::::::::::::::::::::::
echo Loop to switch network adapter state with interval time %interval_time% seconds
set loop_index=0
:LoopStart
if %loop_index% EQU %loop_time% goto :LoopStop
:: Set enable or disable operation
set /A cnt=%loop_index% + 1
set /A result=cnt%%2
if %result% equ 0 (
set operation=enabled
set interval_time=%enable_interval_time%
) else (
set operation=disable
set interval_time=%disable_interval_time%
)
echo [%date:~0,10% %time:~0,2%:%time:~3,2%:%time:~6,2%] loop time ... %cnt% ... %operation%
set adapter_index=0
:AdapterStart
if %adapter_index% EQU %adapter_num% goto :AdapterStop
set adapter_cur.name=0
for /F "usebackq delims==. tokens=1-3" %%I in (`set adapter[%adapter_index%]`) do (
set adapter_cur.%%J=%%K
)
:: swtich adapter state
call:adapter_switch "%adapter_cur.name%" %operation%
set /A adapter_index=%adapter_index% + 1
goto AdapterStart
:AdapterStop
set /A loop_index=%loop_index% + 1
echo [%date:~0,10% %time:~0,2%:%time:~3,2%:%time:~6,2%] sleep some time (%interval_time% seconds) ...
ping -n %interval_time% 127.0.0.1 > nul
goto LoopStart
:LoopStop
echo End of loop ...
pause
goto:eof
:: function definition
:adapter_switch
set cmd=netsh interface set interface %1 %2
echo %cmd%
%cmd%
goto:eof
注意:这个地方填的是发射AP热点的网络适配器,比如如下的。如果是中文的名称,还必须注意BAT脚本的编码问题,否则会出现识别不到正确的网络适配器名称。
同时,为了精准定位掉网恢复的问题,我在网络掉线重连的地方增加了三个变量,分别记录总的重连次数、重连成功的次数、重连失败的次数。
另一方面,如issue描述所说,这是一个固定次数强相关的问题,也可能跟运行时长联系紧密的一个问题,且重启之后一切恢复正常,这一系列的特征,都把问题导向一个很常见的问题:内存泄漏。
于是,在压测前,我在每次重连之后(不管成功与否)重新打印了系统的内存情况(总剩余内存,历史最低剩余内存),以便于判断问题节点的内存情况。
通过调整压测脚本中的disable_interval_time和enable_interval_time参数,在比较短的时间内就复现了问题,的确如果issue描述那样,在30多次之后,无法重连成功,且重启即可恢复。
大部分的问题,只要有复现路劲,都还比较好查,只不过需要花点时间,专研下。
首先肯定是我们怀疑最大可能的内存泄漏信息,初步一看:
由于在断网重连的操作中,可能对应的时间点下Wi-Fi热点还处于关闭状态,所以肯定是会重连失败的,当出现Wi-Fi热点的时候是可以成功的,所以我们会看到free空闲的内存在一个范围内波动,并没有看到它有稳定下降的趋势。
倒是和这个evmin(最低空闲内存)值,在出现问题之后,它出现了一个固定值,并一直持续下去,从这一点上怀疑,这个内存肯定是有问题的,只不过我在第一次分析这个情况的时候并没有下这个结论,现在回过头来看这是一个警惕信号。
我当时推测的点(想要验证的点)是,出现问题的时候,是不是因为内存泄漏导致系统空闲内存不足了,进而无法完成新的连接热点,连接网络等耗内存操作。
所以,通过上面的内存表,我基本笃定了我的结论:没有明显的内存泄漏迹象,并不是因内存不足而重连不上。
问题分析到这里,肯定不能停下来,但是原厂的SDK,比如连热点那块的逻辑,对我们来说是个黑盒子,只能从原厂那里咨询看能不能取得什么有效的信息。
一圈问下来,拿到的有效信息基本是0,所以自己的问题还得靠自己!
在上面的问题场景中,我们已排除掉了内存不足的可能性,那么接下来我们重点应分析三个方面:
这三个问题是一个递进关系,一环扣一环!
我们先看第一个问题,很明显,当复现问题的时候,我们可以从PC的Wi-Fi热点那里看到所连过来的设备,且看到了分配的子网IP地址。
接下来看第二个问题,这个问题测试也很简单,因为我们的命令行中集成了ping命令,输入ping命令一看,居然发现了一个重要信息:
# ping www.baidu.com
ping_Command
ping IP address:www.baidu.com
ping: create socket failed
正常的ping log长这样:
# ping www.baidu.com
ping_Command
ping IP address:www.baidu.com
60 bytes from 14.215.177.39 icmp_seq=0 ttl=53 time=40 ticks
60 bytes from 14.215.177.39 icmp_seq=1 ttl=53 time=118 ticks
60 bytes from 14.215.177.39 icmp_seq=2 ttl=53 time=68 ticks
60 bytes from 14.215.177.39 icmp_seq=3 ttl=53 time=56 ticks
WC!ping: create socket failed 这还创建socket失败了!!!?
我第一时间怀疑是不是lwip组件出问题了?
第二个怀疑:难道socket句柄不够了?因此创建内存大部分的操作就是在申请socket内存资源,并没有进行其他什么高级操作。
这么一想,第二个可能性就非常大,结合前面的总总迹象,是个需要重点排查的对象。
在准确定位问题之前,我们先帮相关的知识点补充完整,方便后续的知识铺开讲解。
socket函数调用的路劲如下:
socket -> lwip_socket -> alloc_socket
alloc_socket函数的实现:
/**
* Allocate a new socket for a given netconn.
*
* @param newconn the netconn for which to allocate a socket
* @param accepted 1 if socket has been created by accept(),
* 0 if socket has been created by socket()
* @return the index of the new socket; -1 on error
*/
static int
alloc_socket(struct netconn *newconn, int accepted)
{
int i;
SYS_ARCH_DECL_PROTECT(lev);
/* allocate a new socket identifier */
for (i = 0; i < NUM_SOCKETS; ++i) {
/* Protect socket array */
SYS_ARCH_PROTECT(lev);
if (!sockets[i].conn && (sockets[i].select_waiting == 0)) {
sockets[i].conn = newconn;
/* The socket is not yet known to anyone, so no need to protect
after having marked it as used. */
SYS_ARCH_UNPROTECT(lev);
sockets[i].lastdata = NULL;
sockets[i].lastoffset = 0;
sockets[i].rcvevent = 0;
/* TCP sendbuf is empty, but the socket is not yet writable until connected
* (unless it has been created by accept()). */
sockets[i].sendevent = (NETCONNTYPE_GROUP(newconn->type) == NETCONN_TCP ? (accepted != 0) : 1);
sockets[i].errevent = 0;
sockets[i].err = 0;
SOC_INIT_SYNC(&sockets[i]);
return i + LWIP_SOCKET_OFFSET;
}
SYS_ARCH_UNPROTECT(lev);
}
return -1;
}
大家注意到,上述函数中的for循环有一个宏 NUM_SOCKETS,这个宏的具体数值是可适配的,不同的平台可根据自己的实际使用情况和内存情况,选择一个合适的数值。
我们看下这个NUM_SOCKETS宏定义的实现:
宏定义替换
#define NUM_SOCKETS MEMP_NUM_NETCONN
在lwipopts.h中找到了其最终的替换
/**
* MEMP_NUM_NETCONN: the number of struct netconns.
* (only needed if you use the sequential API, like api_lib.c)
*
* This number corresponds to the maximum number of active sockets at any
* given point in time. This number must be sum of max. TCP sockets, max. TCP
* sockets used for listening, and max. number of UDP sockets
*/
#define MEMP_NUM_NETCONN (MAX_SOCKETS_TCP + \
MAX_LISTENING_SOCKETS_TCP + MAX_SOCKETS_UDP)
看着这,有点绕,究竟这个值是多少啊?
具备的销毁,我们都知道使用close接口,它的函数调用路径如下:
close -> lwip_close -> free_socket
lwip_close函数的实现如下:
int
lwip_close(int s)
{
struct lwip_sock *sock;
int is_tcp = 0;
err_t err;
LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_close(%d)\n", s));
sock = get_socket(s);
if (!sock) {
return -1;
}
SOCK_DEINIT_SYNC(1, sock);
if (sock->conn != NULL) {
is_tcp = NETCONNTYPE_GROUP(netconn_type(sock->conn)) == NETCONN_TCP;
} else {
LWIP_ASSERT("sock->lastdata == NULL", sock->lastdata == NULL);
}
#if LWIP_IGMP
/* drop all possibly joined IGMP memberships */
lwip_socket_drop_registered_memberships(s);
#endif /* LWIP_IGMP */
err = netconn_delete(sock->conn);
if (err != ERR_OK) {
sock_set_errno(sock, err_to_errno(err));
return -1;
}
free_socket(sock, is_tcp);
set_errno(0);
return 0;
}
这里调用到了free_socket:
/** Free a socket. The socket's netconn must have been
* delete before!
*
* @param sock the socket to free
* @param is_tcp != 0 for TCP sockets, used to free lastdata
*/
static void
free_socket(struct lwip_sock *sock, int is_tcp)
{
void *lastdata;
lastdata = sock->lastdata;
sock->lastdata = NULL;
sock->lastoffset = 0;
sock->err = 0;
/* Protect socket array */
SYS_ARCH_SET(sock->conn, NULL);
/* don't use 'sock' after this line, as another task might have allocated it */
if (lastdata != NULL) {
if (is_tcp) {
pbuf_free((struct pbuf *)lastdata);
} else {
netbuf_delete((struct netbuf *)lastdata);
}
}
}
这个SYS_ARCH_SET(sock->conn, NULL);就会释放对应的socket句柄,从而保证socket句柄可循环使用。
为何在这里会讨论这个知识点,那是因为这个知识点是解决整个问题的关键。
这里就直接把结论摆出来:
了解了lwip组件中对socket句柄的创建和关闭,我们再回到复现问题的本身。
从最细微的log我们知道问题出在无法分配新的socket具备,我们再看下那个分配socket的逻辑中,有一个判断条件:
if (!sockets[i].conn && (sockets[i].select_waiting == 0)) {
//分配新的句柄编号
sockets[i].conn = newconn;
。。。
}
通过增加log,我们知道select_waiting的值是为0的,那么问题就出在conn不为NULL上面了。
在lwip_close中是有对.conn进行赋值NULL的,于是就猜想难道 lwip_close没调用?进行导致句柄没完全释放?
回答这个问题,又需要回到我们的软件架构上了,在实现架构了,我们不同的芯片平台使用了不同版本的lwip组件,而上层跑的MQTT协议是公用的,也就是如果是上层逻辑中没有正确处理close逻辑,那么这个问题应该在所有的平台都会出现,但为何唯独只有这个平台才出问题呢。
答案只有一个,问题可能出在lwip实现这一层。
由于lwip是原厂去适配,我第一时间找了原生的lwip-2.0.2版本做了下对比,主要想知道原厂适配的时候,做了哪些优化和调整。
结果一对比,果然发现了问题。
我们就以出问题的sockets.c为例,我们重点关注socket的申请和释放:
为了比较好描述原厂所做的优化,我把其添加的代码做了少量修改,大致就加了几个宏定义,这几个宏定义看其注释应该是为了处理多任务下新建、关闭socket的同步问题。
#define SOC_INIT_SYNC(sock) do { something ... } while(0)#define SOC_DEINIT_SYNC(sock) do { SOCK_CHECK_NOT_CLOSING(sock); something ... } while(0)#define SOCK_CHECK_NOT_CLOSING(sock) do { \ if ((sock)->closing) { \ SOCK_DEBUG(1, "SOCK_CHECK_NOT_CLOSING:[%d]\n", (sock)->closing); \ return -1; \ } \ } while (0)
只是跟了一下它的逻辑,上层调用lwip_close的时候会调用到SOC_DEINIT_SYNC,同时它会调用到SOCK_CHECK_NOT_CLOSING,从而结束整一个socket释放的全流程。
但是偏偏我们做的MQTT上层在调用TCP链路挂断的时候,是这么玩的:
/* * Gracefully close the connection */void mbedtls_net_free( mbedtls_net_context *ctx ){ if( ctx->fd == -1 ) return; shutdown( ctx->fd, 2 ); close( ctx->fd ); ctx->fd = -1;}
优雅地关闭TCP链路,这时候你应该要想起4.3.2章节的知识点。
这样调用对那几个宏会有影响?
答案是肯定的。
原来的,原厂适配时lwip_shutdown也同样调用了SOC_DEINIT_SYNC,这就导致了如果上层关闭链路既调用shutdown又调用close的话,它的逻辑就会出问题,会引发close的流程走不完整。
为了能够简化这个问题,我大概写了一下它的逻辑:
1)shutdown函数调过来的时候,开始启动关闭流程SOC_DEINIT_SYNC,进入到那几个宏里面,会有一步:(sock)->closing = 1;然后正常返回0;
2)等到close函数调过来的时候,再次进入关闭流程SOC_DEINIT_SYNC,结果一判断(sock)->closing已经是1了,然后报错返回-1;这样close的返回就不正常了;
3)再看lwip_close函数的逻辑:
于是就出现了之前的问题,socket句柄的index一直在上升,应该旧的scoket句柄一直被占用,知道句柄数被耗尽。
最大句柄数NUM_SOCKETS究竟是多少,可以参考之前我的文章将如何看预编译的代码,我们可以清晰地看到他的值就是38。
所有的疑惑均打开,为了一定是30多次之后才出问题,这里给出了答案!
这里我大胆地猜想了一下,应该原厂在适配这段同步操作逻辑的时候,压根就没考虑上层还可以先shutdown再close,所以引发了这个问题。
上面的分析中,已经初步定位了问题代码,接下来就是要进行问题修复了。
问题根源出在先调shutdown再调close,由于是一个上层代码,其他平台也是共用的,且其他平台使用并没有问题,所以肯定不能把上层优雅关闭TCP链路的操作给去掉,只能底层的lwip组件自行优化解决。所谓是:谁惹的祸,谁来擦屁股!
解决问题的关键是,要保证调完shutdown之后,close那次操作需要走一个完整流程,这样才能把占用的socket句柄给释放掉。
所以在执行shutdown和close的时候,SOC_DEINIT_SYNC需要带个参数告知是不是close操作,如果不是close那么就走一个简易流程,这样就能保证close流程是完整的。
当上层只调用close,也能确保close的流程是完整的。
但是,入股上层先调用close,再调shutdown,这样流程就不通了。
当然,上层也不能这么玩,具体参考4.3.2的知识点。
问题修复之后,需要进行同样的流程复测,以确保这个问题确实被修复了。
问题验证也很简单,修改sockets.c中的NUM_SOCKETS,改成一个很小的值,比如3或5,加快问题复现的速度,同时把alloc_socket中获取的句柄id打出来,观察它有没有上升,正常的测试中,在没有其他网络通讯链路的情况下,它应该稳定值为0。
很快就可以验证,不会再复现这个问题了。
接下来,需要将NUM_SOCKETS的值还原成原理的值,真实测试原本复现的场景,确保真的只有这个地方引发了这个问题,而其他代码并没有干扰到。
幸运的是,还原之后的测试也通过了,这就证明了这个问题完全修复了,且没有带来副作用,是一次成功的bug修复。
这与 Payubiz payment gateway sdk 关系不大一体化。但是,主要问题与构建项目有关。 每当我们尝试在模拟器上运行应用程序时。我们得到以下失败: What went wrong:
我有一个现有的应用程序,其中包含在同一主机上运行的 4 个 docker 容器。它们已使用 link 命令链接在一起。 然而,在 docker 升级后,link 行为已被弃用,并且似乎有所改变。我们现
在 Internet 模型中有四层:链路 -> 网络 -> 传输 -> 应用程序。 我真的不知道网络层和传输层之间的区别。当我读到: Transport layer: include congesti
很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visit the help center . 关闭 1
前言: 生活中,我们在上网时,打开一个网页,就可以看到网址,如下: https😕/xhuahua.blog.csdn.net/ 访问网站使用的协议类型:https(基于 http 实现的,只不过在
网络 避免网络问题降低Hadoop和HBase性能的最重要因素可能是所使用的交换硬件,在项目范围的早期做出的决策可能会导致群集大小增加一倍或三倍(或更多)时出现重大问题。 需要考虑的重要事项:
网络 网络峰值 如果您看到定期的网络峰值,您可能需要检查compactionQueues以查看主要压缩是否正在发生。 有关管理压缩的更多信息,请参阅管理压缩部分的内容。 Loopback IP
Pure Data 有一个 loadbang 组件,它按照它说的做:当图形开始运行时发送一个 bang。 NoFlo 的 core/Kick 在其 IN 输入被击中之前不会发送其数据,并且您无法在 n
我有一台 Linux 构建机器,我也安装了 minikube。在 minikube 实例中,我安装了 artifactory,我将使用它来存储各种构建工件 我现在希望能够在我的开发机器上做一些工作(这
我想知道每个视频需要多少种不同的格式才能支持所有主要设备? 在我考虑的主要设备中:安卓手机 + iPhone + iPad . 对具有不同比特率的视频进行编码也是一种好习惯吗? 那里有太多相互矛盾的信
我有一个使用 firebase 的 Flutter Web 应用程序,我有两个 firebase 项目(dev 和 prod)。 我想为这个项目设置 Flavors(只是网络没有移动)。 在移动端,我
我正在读这篇文章Ars article关于密码安全,它提到有一些网站“在传输之前对密码进行哈希处理”? 现在,假设这不使用 SSL 连接 (HTTPS),a.这真的安全吗? b.如果是的话,你会如何在
我试图了解以下之间的关系: eth0在主机上;和 docker0桥;和 eth0每个容器上的接口(interface) 据我了解,Docker: 创建一个 docker0桥接,然后为其分配一个与主机上
我需要编写一个java程序,通过网络将对象发送到客户端程序。问题是一些需要发送的对象是不可序列化的。如何最好地解决这个问题? 最佳答案 发送在客户端重建对象所需的数据。 关于java - 不可序列化对
所以我最近关注了this有关用 Java 制作基本聊天室的教程。它使用多线程,是一个“面向连接”的服务器。我想知道如何使用相同的 Sockets 和 ServerSockets 来发送对象的 3d 位
我想制作一个系统,其中java客户端程序将图像发送到中央服务器。中央服务器保存它们并运行使用这些图像的网站。 我应该如何发送图像以及如何接收它们?我可以使用同一个网络服务器来接收和显示网站吗? 最佳答
我正在尝试设置我的 rails 4 应用程序,以便它发送电子邮件。有谁知道我为什么会得到: Net::SMTPAuthenticationError 534-5.7.9 Application-spe
我正在尝试编写一个简单的客户端-服务器程序,它将客户端计算机连接到服务器计算机。 到目前为止,我的代码在本地主机上运行良好,但是当我将客户端代码中的 IP 地址替换为服务器计算机的本地 IP 地址时,
我需要在服务器上并行启动多个端口,并且所有服务器套接字都应在 socket.accept() 上阻塞。 同一个线程需要启动客户端套接字(许多)来连接到特定的 ServerSocket。 这能实现吗?
我的工作执行了大约 10000 次以下任务: 1) HTTP 请求(1 秒) 2)数据转换(0.3秒) 3)数据库插入(0.7秒) 每次迭代的总时间约为 2 秒,分布如上所述。 我想做多任务处理,但我
我是一名优秀的程序员,十分优秀!