云原生网络扫雷笔记:探究一条活跃连接却有TCP OOM的奇怪问题

简介: 本文联合作者 @牧原问题的背景某个名字很喜气的大客户的前线同学在一个傍晚找到了我们团队,反馈网络出现了严重的卡顿现象:“这个节点上所有的服务都很卡,扩容之后没几天还会出现!本来以为是AMD的问题,现在换了机型还是一样。”从客户的表述中我们已经了解到,在此之前他们做了很多的尝试,但是现象很明显:新节点调度业务Pod上去后,过一段时间就会出现。和机型没啥关系。随后客户反馈了一个比较关键的信息,他们有注

本文联合作者 @牧原

问题的背景

某个名字很喜气的大客户的前线同学在一个傍晚找到了我们团队,反馈网络出现了严重的卡顿现象:

“这个节点上所有的服务都很卡,扩容之后没几天还会出现!本来以为是AMD的问题,现在换了机型还是一样。”

从客户的表述中我们已经了解到,在此之前他们做了很多的尝试,但是现象很明显:

  1. 新节点调度业务Pod上去后,过一段时间就会出现。
  2. 和机型没啥关系。

随后客户反馈了一个比较关键的信息,他们有注意到TCP OOM出现,但是因为流量很小,觉得和这个没关系。

问题的排查过程

排查客户问题的第一步,就是不要轻易相信客户说的话,于是我们开始验证客户所说的现象。

首先是速率很卡,这个非常好验证,在VNC的操作过程中,我们能很明显的感觉到有卡顿。

随后是客户反馈的TCP OOM,查看了客户异常节点的dmesg信息,确实,客户的节点在问题出现的时候持续有这个现象:

TCP OOM指的是TCP协议能够使用的内存触发了配置的阈值,在Linux内核中,作为socket子系统的一个实现,TCP协议能够使用的内存通过net.ipv4.tcp_mem这个sysctl设置三个值,分别代表着min,pressure,max。

TCP OOM的出现,意味着TCP协议使用的内存已经到达了max,除了fin相关的报文之外,TCP协议只能释放一点内存才能使用一点,所以出现卡顿的原因也就不难理解了。

然后我们开始检查客户的连接数量,对于容器网络来说,不同的Pod通过Linux的netns进行网络上的隔离,因此,我们重点针对不同的Pod开始统计netstat中的TCP连接数量,然而结果显示,确实如客户所说,所有的Pod的连接数都不多,其中能看到有明显的流量出入连接仅仅是个位数:

那么,有没有可能是这些连接本身有巨大的吞吐量呢?我们针对上面有较大流量的Pod进行了观测,结果显示,流量也很小。

到了这一步,能确定的现象是:

  1. 客户确实存在TCP协议消耗了大量的内存。
  2. 客户的流量不高。

两个现象结合在一起,就不得不怀疑客户的节点上出现了socket的泄漏,随后我们查看了节点级别的socket状态:

果然,客户的节点上出现了明显的socket泄漏,而TCP协议的socket占用的内存总量也只比tcp_mem默认的配置上限。sockstat信息和tcp_mem的配置都是节点级别生效的,从上图中的信息不难得出:

  1. TCP协议正常使用的socket(通常就是ESTABLISHED状态的连接)是38,tw状态(TIMEWAIT)也只有49,但是alloc状态(分配成功的socket)的socket却有7385个。
  2. orphan状态的socket是0个,很少,orphan状态比较特殊,他指的是“没有被用户进程持有的socket”,也就是说,已经释放但是还没有被内核回收的socket都归为此类,通常在连接建立频繁的业务上会出现较多,显然客户这个节点不属于这种情况。

那么,alloc但是却不是tw,inuse和orphan的socket,到底去哪儿了呢?

我们回顾以上上面几个socket状态的语义:

  1. inUse,这个很好理解,在内核中通过sock_prot_inuse_get进行统计,属于实时的状态。
  2. tw,这个也不难理解,tw_count也是内核直接维护的变量。
  3. orphans,如上所述,除了inUse和tw,其他没有用户程序持有fd的socket都归类在这里,在内核的tcp_close中增长,在inet_csk_destroy_sock中减少。

那么在这三者之外,最有可能的去处是哪里呢?那就是,已经被关闭了连接,但是仍然被用户程序持有的socket。

理解这个概念其实需要明白socket与TCP连接之间的关系。在Linux中,一切皆文件,socket本身也是sockfs这个文件系统实现的一类文件:

  1. 用户程序可以通过操作文件的方式,例如open/close等操作一个socket文件,相应的也会持有socket的fd。
  2. TCP作为socket的一种类型,将响应的操作实现为TCP协议的动作,例如open会打开一个TCP的socket,write可以实现向TCP流写入数据等。

如果存在socket文件还在用户程序的持有状态,但是TCP会话已经正常关闭,那么我们将能够看到还存在大量的socket类型的fd在某一个进程的活跃句柄中,随机我们对节点上所有进程进行了统计:

for i in $(ps aux |awk '{print $2}');do echo $i;ls -l /proc/$i/fd |awk '/socket/' |wc -l;done

果然我们找到了这样的进程:

可以看到53129这个进程持有了5336而socket类型的fd,看起来socket泄漏的主要元凶就这这个进程。

随后我们和客户确认了这个进程,是一个直播业务,在这个Pod中,只有一条ESTABLISHED状态的连接,出现如此巨大的socket泄漏,显然是客户代码写的有问题,问题排查到这里,客户已经了解了问题的前因后果,但是拒绝提供代码让我们进行分析,于是我们复现了这个场景,以下代码实现了和客户一样的bug:

import socket

def main():
    # 创建socket对象
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 绑定地址和端口
    sock.bind(('0.0.0.0', 8888))
    # 开始监听
    sock.listen(5)
    count = 0
    connections = {}
    while True:
        # 接受连接请求
        conn, addr = sock.accept()
        # 发送连接总数
        count += 1
        connections[addr] = conn
        conn.send(f'当前连接总数:{count}'.encode())
        # 关闭连接
        #conn.close()

if __name__ == '__main__':
    main()

如代码中所示,我们提供了一个监听8888端口的TCP服务,在每次接受请求后,将返回的socket文件描述符宝存在一个dict中防止被自动释放,随后我们再提供一个client脚本用于模拟客户端:

import socket
import time

def client():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('10.1.18.154', 8888))
    print(sock.recv(1024).decode())
    sock.close()

while True:
    client()
    time.sleep(2)

客户端也很简单:

  1. 发起对服务端的连接,然后接受并打印服务端的信息也就是服务端当前已经保存的socket数量,随后主动关闭连接。
  2. 每间隔2s重复一次上述的行为,用来产生泄漏的socket。

我们将server部署到kubernetes中,然后开始进行测试,果不其然,很快就能看到socket数量在上升:

看一下服务端的状态,我们通过kubeskoop-exporter的Pod进行更加高效的检查:

# 选择为与上面的server的Pod同节点kubeskoop-exporter实例即可
kubectl exec -it -n kubeskoop kubeskoop-exporter-ns9kz -- sh

可以发现,此时我们的server的Pod没有任何处于ESTABLISHED状态的连接,而查看进程打开的socket,则会发现有很多:

分析到这里,其实客户此次出现socket泄漏的原因已经非常清晰了,客户还有一个疑问:明明内核已经关闭了TCP连接,所有的数据都被用户程序读取了,为什么泄漏的socket还会占据大量内存呢?

其实这个问题在上面的文章中已经提到过,对于TCP协议而言,他所统计的内存并不仅仅指的是内核占据的部分,对于socket这个文件在用户态申请的内存,也就是用户视角的socket这个文件中存在的内容,也会计算在其中,所以当socket泄漏时,很大一部分的用户态的内存占据了TCP协议使用内存的额度,产生了这个问题。

问题的背后

尽管这个问题是一个客户代码引入的问题,我们还是能够看到,从ECS时代走向云原生时代,容器的抽象带来了排查问题的挑战,对于不了解容器底层原理的同学来说,排查容器的网络问题,往往会有这样的痛点:

  1. 那些参数是节点级别,那些参数又只针对容器生效?
  2. 如何更加高效的收集数据,尤其是在netstat这些传统工具无法应对容器场景的情况下。

为了解决这样的痛苦,我们在kubeskoop项目中提供了大量的功能来帮助不熟悉网络底层原理的同学更加高效的进行排查,例如,在这个案例中的socket类型的fd的打开数量,我们已经在kubeskoop新版本中做了支持:

现在你可以通过一个简单的命令就可以清晰地查看每个Pod打开的fd数量和socket类型的fd数量,结合上面已经就绪的针对TCP和socket的支持,排查这一类问题,将会事半功倍。欢迎大家就kubeskoop项目与我们交流,更多的信息,可以直接查看项目的主页 https://github.com/alibaba/kubeskoop

目录
相关文章
|
1天前
|
传感器 运维 物联网
蓝牙Mesh网络:连接未来的智能解决方案
蓝牙Mesh网络:连接未来的智能解决方案
28 12
|
8天前
|
机器学习/深度学习
小土堆-pytorch-神经网络-损失函数与反向传播_笔记
在使用损失函数时,关键在于匹配输入和输出形状。例如,在L1Loss中,输入形状中的N代表批量大小。以下是具体示例:对于相同形状的输入和目标张量,L1Loss默认计算差值并求平均;此外,均方误差(MSE)也是常用损失函数。实战中,损失函数用于计算模型输出与真实标签间的差距,并通过反向传播更新模型参数。
|
17天前
|
网络协议 C语言
C语言 网络编程(十三)并发的TCP服务端-以进程完成功能
这段代码实现了一个基于TCP协议的多进程并发服务端和客户端程序。服务端通过创建子进程来处理多个客户端连接,解决了粘包问题,并支持不定长数据传输。客户端则循环发送数据并接收服务端回传的信息,同样处理了粘包问题。程序通过自定义的数据长度前缀确保了数据的完整性和准确性。
|
17天前
|
网络协议 C语言
C语言 网络编程(十一)TCP通信创建流程---服务端
在服务器流程中,新增了绑定IP地址与端口号、建立监听队列及接受连接并创建新文件描述符等步骤。`bind`函数用于绑定IP地址与端口,`listen`函数建立监听队列并设置监听状态,`accept`函数则接受连接请求并创建新的文件描述符用于数据传输。套接字状态包括关闭(CLOSED)、同步发送(SYN-SENT)、同步接收(SYN-RECEIVE)和已建立连接(ESTABLISHED)。示例代码展示了TCP服务端程序如何初始化socket、绑定地址、监听连接请求以及接收和发送数据。
|
17天前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
17天前
|
网络协议 C语言
C语言 网络编程(十二)TCP通信创建-粘包
TCP通信中的“粘包”现象指的是由于协议特性,发送方的数据包被拆分并在接收方按序组装,导致多个数据包粘连或单个数据包分割。为避免粘包,可采用定长数据包或先传送数据长度再传送数据的方式。示例代码展示了通过在发送前添加数据长度信息,并在接收时先读取长度后读取数据的具体实现方法。此方案适用于长度不固定的数据传输场景。
|
17天前
|
缓存 网络协议 网络性能优化
C语言 网络编程(二)TCP 协议
TCP(传输控制协议)是一种面向连接、可靠的传输层协议,通过校验和、序列号、确认应答等机制确保数据完整性和可靠性。通信双方需先建立连接,再进行通信,采用三次握手建立连接,四次挥手断开连接。TCP支持任意字节长度的数据传输,具备超时重传、流量控制及拥塞控制机制。三次握手用于同步序列号和确认双方通信能力,四次挥手则确保双方均能完成连接关闭操作,保证数据传输的可靠性。
|
17天前
|
网络协议 C语言
C语言 网络编程(十)TCP通信创建流程---客户端
在TCP通信中,客户端需通过一系列步骤与服务器建立连接并进行数据传输。首先使用 `socket()` 函数创建一个流式套接字,然后通过 `connect()` 函数连接服务器。连接成功后,可以使用 `send()` 和 `recv()` 函数进行数据发送和接收。最后展示了一个完整的客户端示例代码,实现了与服务器的通信过程。
|
24天前
|
安全 网络安全 数据安全/隐私保护
云原生技术探索:容器化与微服务架构的实践之路网络安全与信息安全:保护数据的关键策略
【8月更文挑战第28天】本文将深入探讨云原生技术的核心概念,包括容器化和微服务架构。我们将通过实际案例和代码示例,展示如何在云平台上实现高效的应用部署和管理。文章不仅提供理论知识,还包含实操指南,帮助开发者理解并应用这些前沿技术。 【8月更文挑战第28天】在数字化时代,网络安全和信息安全是保护个人和企业数据的前线防御。本文将探讨网络安全漏洞的成因、加密技术的应用以及提升安全意识的重要性。文章旨在通过分析网络安全的薄弱环节,介绍如何利用加密技术和提高用户警觉性来构建更为坚固的数据保护屏障。
|
14天前
|
网络协议
网络协议概览:HTTP、UDP、TCP与IP
理解这些基本的网络协议对于任何网络专业人员都是至关重要的,它们不仅是网络通信的基础,也是构建更复杂网络服务和应用的基石。网络技术的不断发展可能会带来新的协议和标准,但这些基本协议的核心概念和原理将继续是理解和创新网络技术的关键。
31 0