解决国标28181传输视频流导致设备内存消耗的问题
项目场景
在项目中,我们设备通过国标28181协议接入到服务器上,设备上线注册成功后,在服务端使用tcp拉流,具体的拉流流程这里就不多讲了,如果不懂的话,可以去找个标准协议看看,里面讲解的很清楚,当实时预览画面出来之后,稳定运行一段时间,这个使用场景和实际用户的场景差不多,设备上线,拉流,查看实时视频,作为监控设备这是基本的功能。
问题描述
稳定运行一段时间后,发现设备内存减少的很厉害,导致影响了其他业务的正常运行,设备出现了死机情况。
初步判定可能存在内存泄漏,内存泄漏主要是对动态分配的内存没有管理好导致的,项目中用到了new、malloc等动态分配内存的接口,用的时候需要注意及时释放资源。查看内存泄漏可以使用开源软件 Valgrind 工具,使用起来也比较方便;还有就是使用内存钩子,有时间单独讲一期内存钩子的使用方法。但是断开国标28181协议后,内存又能回来,带着这个问题一块往下看👇。
原因分析
刚开始的时候查找问题的方向集中到了内存泄漏方面,然后对new、malloc等分配内存的地方进行了排除,分析结果就是动态申请内存这块没有问题,接着国标28181模块中使用到的sip库也进行了摸排,也不是这个地方的问题。一时间对这个问题有点摸不着头脑。
然后我们把定位问题的方向放到了通信方面,是这样的,在反复测试复现该问题的过程中,我们发现断开国标连接后,减少掉的内存大部分能够回来,这个现象基本上排除了是内存泄漏导致的这个问题。紧接着把注意力转移到了缓存方面,那是什么缓存导致的问题呢?
国标协议除了基本的sip通信外,还有就是通过rtp发送的音视频流了。上面已经验证了sip库是没有问题的,基本上就锁定到了tcp发送音视频数据的通信上了。创建和使用socket大家肯定都会,下面先讲解一下对socket的基本配置,然后内存消耗的问题也就弄清楚了,接着往下看,说的不对的地方希望大家指出。
知识点
* tcp客户端基本配置选项
* 对接国标28181平台处理rtcp
* 对tcp window full的理解
1、tcp客户端基本配置选项
(1)socket 配置 TCP_NODELAY
首先简单介绍一下在网络拥塞控制领域,非常著名的算法:Nagle算法。
这是使用它的发明人John Nagle的名字来命名的,John Nagle在1984年首次用这个算法来尝试解决福特汽车公司的网络拥塞问题(RFC 896),该问题的具体描述是:如果我们的应用程序一次产生1个字节的数据,而这个1个字节数据又以网络数据包的形式发送到远端服务器,那么就很容易导致网络由于太多的数据包而过载。比如,当用户使用Telnet连接到远程服务器时,每一次击键操作就会产生1个字节数据,进而发送出去一个数据包,所以,在典型情况下,传送一个只拥有1个字节有效数据的数据包,却要发费40个字节长包头(即ip头20字节+tcp头20字节)的额外开销,这种有效载荷(payload)利用率极其低下的情况被统称之为愚蠢窗口症候群(Silly Window Syndrome)。可以看到,这种情况对于轻负载的网络来说,可能还可以接受,但是对于重负载的网络而言,就极有可能承载不了而轻易的发生拥塞瘫痪。(网络上摘抄的)
简单通俗的讲,就是每次发送一个字节数据,但数据的外面要包上一层40个个自己的头,这种效率其实很低,Nagle算法,就是如果要发送的数据大小满足MSS,就会立即发送,否则数据会被放到缓冲区,等到已经发送的包被确认了之后才能继续发送。降低了网络中小包的数量,从而提高了网络性能。
//参数设置方法,关闭该算法
#include <netinet/tcp.h>//需要引用的头文件
int on = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
Delay算法和40ms:
从下图抓包中看到,设备发送视频流后,服务器端过了大约40ms才回复的ack,这个跟Delay Ack有关。
简单的说,Delay Ack就是延时发送ACK,在收到数据包的时候,会检查是否需要发送ACK,如果需要的话,进行快速ACK还是延时ACK,在无法使用快速确认的条件下,就会使用Delay Ack。
TCP在何时发送ACK的时候有如下规定:
1.当有响应数据发送的时候,ACK会随着数据一块发送。
2.如果没有响应数据,ACK就会有一个延迟,以等待是否有响应数据一块发送,但是这个延迟一般在40ms~500ms之间,一般情况下在40ms左右,如果在40ms内有数据发送,那么ACK会随着数据一块发送,对于这个延迟的需要注意一下,这个延迟并不是指的是收到数据到发送ACK的时间延迟,而是内核会启动一个定时器,每隔200ms就会检查一次,比如定时器在0ms启动,200ms到期,180ms的时候data来到,那么200ms的时候没有响应数据,ACK仍然会被发送,这个时候延迟了20ms。
总结一下: TCP_NODELAY配置,就是不让tcp把每个小包先缓存起来,等到一定的长度后再进行发送的问题,减少本地缓存,来了数据之后就往外发送,降低缓存大小。还有一个算法是DELAY ACK,从抓包中看一看到服务端是开启这个算法的,发送数据后,有时并不会立即回复ack报文,如果这个时候也有往客户端发送的数据时,ack和数据会一块发送。
(2)socket 配置端口回收 SO_REUSEADDR
一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR可以让端口释放后立即就可以被再次使用。
根据实际的业务场景,合理的使用这个配置:
(1)作为客户端时,创建sokcet并绑定端口后,会使用这个端口进行数据传输,当连接断开之后,该端口会被立刻回收,下次再创建socket的时候还可以继续使用该端口,降低对端口数量的消耗。
(2)作为服务端时,有客户端连接并已建立,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此时再次启动服务器,就会bind不成功,报:Address already in use。使用SO_REUSEADDR配置后,服务端主动关闭后,使用的端口也会被立刻释放回收,服务器重启之后就可以再次使用该端口。
int on = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on);
(3) socket 配置接收或发送超时时间 SO_RCVTIMEO SO_SNDTIMEO
(1)SO_RCVTIMEO ,设置sokect接收数据超时时间
(2)SO_SNDTIMEO ,设置socket发送数据超时时间。
这两个选项会对下面的接口有影响:send, sendmsg, recv, recvmsg, accept, connect。特别注意对 accept 和 connect同样有效,这两个选项设置后,若超时, 返回-1,并设置errno为EAGAIN或EWOULDBLOCK。其中connect超时的话,也是返回-1, 但errno设置为EINPROGRESS。
struct timeval tv = {1, 0};
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv);
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv);
(4) socket 配置缓存区大小 SO_RCVBUF SO_SNDBUF
从配置选项名称就可以看出来作用,负责设置接收缓存大小和发送缓存大小的。
注意:缓冲区的上限不能无限大,如果超过内核设置的上限值,则以内核设置值为准(sysctl -a命令查看)。
net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_wmem = 8192 65536 16777216
net.ipv4.tcp_mem = 8388608 12582912 16777216
int size = RCV_BUF_LEN;
if (setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size);
size = SND_BUF_LEN;
if (setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size);
总结一下: 如果socket不配置的话,这块是有一个默认配置的,但默认配置不一定符合当前业务的要求,所以最好根据业务需求设置一个合理的大小。
2、 对接国标28181平台处理rtcp
首先国标协议中使用rtp传输音视频流,并没有规定要发送rtcp报文。但有一些厂家平台还是会给设备发送rtcp报文,如果设备端不对rtcp报文处理的话,会出现一些问题。
服务端会定时发送rtcp报文给设备,设备端如果没有进行接收的话,会导致设备端接收缓存被占满,缓存数据无法清除,这个也是内存减少的一部分原因。
接收缓存满了之后,就会出现下面👇这个问题(tcp window full)。所以在对接国标协议时,还是要注意,看看有没有发送rtcp报文,有的话最好处理一下。
3、 对tcp window full的理解
TCP/IP协议为流控制协议,TCP窗口是其中一个重要的概念。在TCP接受和发送端都有缓存区,用户缓存数据,当缓存区满的时候就不能在向缓存区中写入数据了。发送缓存区满表现为send的返回值不再是指定的字节数,而小于该值的一个值;而接收缓存区满表现为对端发送收到影响。
[TCP Window Full] :服务端向客户端发送的一种窗口警告,表示已经发送到数据接收端的极限了。
[TCP Window Update]:缓冲区已释放为所示的大小,因此请恢复传输。
[Zero Window] :客户端向服务端发送的一种窗口警告,告诉发送者你的接收窗口已满,暂时停止发送。
总结一下: 出现这个问题的原因就是设备端的rtcp没有处理,一直缓存在底层导致不能释放。
总结
看到这里,内存消耗问题也基本上弄清楚了,就是因为客户端socket和服务端socket配置没有匹配起来导致设备缓存了音视频数据,缓存的数据多大,是可以计算出来的,视频帧率为25的话,一秒就是25帧,分辨率1920*1080,一帧的大小大约在300000左右,通过这些数据是可以算出缓存大小的。
最终这个问题得到了解决,主要还是对tcp通信不了解吧,还要继续学习网络通信这块,这里也分享给大家一本书TCP详解,一块学习,谢谢。