云原生网络扫雷笔记:探究一条活跃连接却有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

目录
相关文章
|
2月前
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
213 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
7天前
|
负载均衡 网络协议 算法
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
这网络层就像搭积木一样,上层协议都是基于下层协议搭出来的。不管是ping(用了ICMP协议)还是tcp本质上都是基于网络层IP协议的数据包,而到了物理层,都是二进制01串,都走网卡发出去了。 如果网络环境没发生变化,目的地又一样,那按道理说他们走的网络路径应该是一样的,什么情况下会不同呢? 我们就从路由这个话题聊起吧。
29 4
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
|
3天前
|
网络协议
TCP报文格式全解析:网络小白变高手的必读指南
本文深入解析TCP报文格式,涵盖源端口、目的端口、序号、确认序号、首部长度、标志字段、窗口大小、检验和、紧急指针及选项字段。每个字段的作用和意义详尽说明,帮助理解TCP协议如何确保可靠的数据传输,是互联网通信的基石。通过学习这些内容,读者可以更好地掌握TCP的工作原理及其在网络中的应用。
|
28天前
|
监控 网络协议 网络性能优化
网络通信的核心选择:TCP与UDP协议深度解析
在网络通信领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种基础且截然不同的传输层协议。它们各自的特点和适用场景对于网络工程师和开发者来说至关重要。本文将深入探讨TCP和UDP的核心区别,并分析它们在实际应用中的选择依据。
56 3
|
2月前
|
Web App开发 缓存 网络协议
不为人知的网络编程(十八):UDP比TCP高效?还真不一定!
熟悉网络编程的(尤其搞实时音视频聊天技术的)同学们都有个约定俗成的主观论调,一提起UDP和TCP,马上想到的是UDP没有TCP可靠,但UDP肯定比TCP高效。说到UDP比TCP高效,理由是什么呢?事实真是这样吗?跟着本文咱们一探究竟!
72 10
|
1月前
|
网络协议 算法 网络性能优化
计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议
计算机网络常见面试题(一):TCP/IP五层模型、应用层常见的协议、TCP与UDP的区别,TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议、ARP协议
|
1月前
|
物联网 5G 数据中心
|
2月前
|
Docker 容器
docker swarm启动服务并连接到网络
【10月更文挑战第16天】
45 5
|
2月前
|
安全 网络架构
无线网络:连接未来的无形纽带
【10月更文挑战第13天】
80 8
|
2月前
|
机器学习/深度学习 数据采集 算法
目标分类笔记(一): 利用包含多个网络多种训练策略的框架来完成多目标分类任务(从数据准备到训练测试部署的完整流程)
这篇博客文章介绍了如何使用包含多个网络和多种训练策略的框架来完成多目标分类任务,涵盖了从数据准备到训练、测试和部署的完整流程,并提供了相关代码和配置文件。
69 0
目标分类笔记(一): 利用包含多个网络多种训练策略的框架来完成多目标分类任务(从数据准备到训练测试部署的完整流程)