Linux 中每个 TCP 连接最少占用多少内存?

Linux 中每个 TCP 连接最少占用多少内存?

陈硕·2017/02/18·不得转载

关于“Linux 中每个 TCP 连接占用多少内存”这一问题,中文网络上流传一种说法:TCP 连接建立的时候会分配接收缓冲区和发送缓冲区,各 4KB,一共是 8KB。如果加上 TCP 协议控制块(protocol control block)的 2KB,一共是 10KB,并且用 tcp_rmem 和 tcp_wmem 的值来佐证这个观点。这种说法是错误的。


$ sysctl -A |grep tcp_.mem
net.ipv4.tcp_rmem = 4096        87380   6291456
net.ipv4.tcp_wmem = 4096        16384   4194304

以上说法错误的原因在于 TCP 连接在建立时并不会真的去分配接收缓冲区和发送缓冲区,后文我们会谈到这点。

下面我们通过分析内核源码并借助实验来找出每个 TCP 连接最少占用多少内存。这里的“TCP 连接”准确地说是一个 TCP socket,一条 TCP 连接实际上对应两个 TCP socket。如果服务器和客户端都在本机,那么解读测试结果的时候不要忘了这一点。

原理与源码分析

调用 socket(2) 会返回一个文件描述符,socket(AF_INET, SOCK_STREAM, 0) 影响的内核数据结构示意图如下。

原图:chenshuo.github.io/note

在内核中,TCP 协议控制块是 struct tcp_sock,每个 TCP socket 会对应一个 tcp_sock 对象。除此之外,为了能从文件描述符映射到 tcp_sock,还需要一些其他 struct,包括:

  • struct file,对应每个打开的文件
  • struct dentry,文件所在的目录
  • struct socket_alloc,包含 struct socket 和 struct inode 两个成员,是连接 VFS 和 tcp_sock 的桥梁
  • struct socket_wq,用于 wait queue(例如阻塞IO时挂起当前线程)

从逻辑上说,通过文件描述符 sockfd 找到对应的 tcp_sock 的途径是:current->files->fdt->fd[sockfd]->private_data->sk。

这里每个 struct 的大小可以通过 /proc/slabinfo 找到,本文以陈硕家某台机器上运行的 Debian 8 x86-64 为例(Linux 内核版本 3.16):

| struct           | size | slab cache name    |
| ---------------- | ---- | ------------------ |
| file             |  256 | "filp"             |
| dentry           |  192 | "dentry"           |
| socket_alloc     |  640 | "sock_inode_cache" |
| tcp_sock         | 1792 | "TCP"              |
| socket_wq        |   64 | "kmalloc-64"       |
| ---------------- | ---- | ------------------ |
| inet_bind_bucket |   64 | "tcp_bind_bucket"  |
| epitem           |  128 | "eventpoll_epi"    |
| tcp_request_sock |  256 | "request_sock_TCP" |

因此,每个 TCP socket 占用的内存最少是 256 + 192 + 640 + 1792 + 64 = 2944 字节。后面的实验表明,实际占用的字节数会比这个略大,原因有三点。

  • SLAB 的额外开销(overhead)。以 tcp_sock 为例,sizeof(struct tcp_sock) == 1792,对于 4KB 的 page,每个 page 只能放 2 个 tcp_sock,因此每个 tcp_sock 的实际开销是 2048 字节。/proc/slabinfo 会告诉我们每个 slab cache 的一个 slab 有多少 page (绝大多数是 1,少数是 2),以及每个 slab 可以放多少个对象,这样我们能算出额外开销。例如 dentry 对象大小是 192 字节,每个 page 可以放 21 个对象,那么存放 10000 个 dentry 实际需要 ⌈10000/21⌉ * 4 = 1908 KB,而不是 10000 * 192 / 1024 = 1875 KB。读者可自行分析 socket_alloc 的额外开销是多少字节,提示:每个 4KB page 可以放 6 个对象。
  • 每建立一个客户端连接会使用一个 ephemeral port,每个被占用的 TCP port 会对应一个 inet_bind_bucket 对象,占用 64 字节。不过,服务端连接会共享 listening socket 的 inet_bind_bucket,所以不会为每个 accept() 得到的连接新建 inet_bind_bucket 对象,就是说服务端连接占用的内存比客户端连接略小,这恐怕与很多人的想象相反。
    以下是客户端调用 connect() 之后的内核数据结构示意图,其中 tcp_sock 伴随的 file/dentry/socket_alloc 等从略,下一篇文章会介绍图中 ehash/ehash/listening_hash 这三个 hash 表的具体含义。从中可以看出为 ephemeral port 44404 新建了一个 inet_bind_bucket 对象。原图:chenshuo.github.io/note
  • 通常的服务端网络编程会使用 epoll 来处理多个并发连接,用 EPOLL_CTL_ADD 往 epollfd 每添加一个 sockfd 会创建一个 epitem 对象,大小为 128 字节。

对于接收缓冲区和发送缓冲区,如果没有数据,是不占内存的。具体来说,对于接收缓冲区,只有当有数据可读但应用程序尚未读取的时候才占内存(就是 epoll_wait 返回 EPOLL_IN之后,程序调用 read() 之前的那一小段时间)。换句话说,只要服务器总是及时读取数据,接收缓冲区基本不占内存。对于发送缓冲区,只有等待发送的数据和发送之后尚未收到 ACK 的数据才占用内存,在稳态下,发送缓冲区占用的内存等于 BDP。比如考虑发送方每秒钟发 1MB 数据,rtt 是 50ms 的情况,那么发送缓冲区平均占用 51.2 KB (不计 skbuff 的额外开销)。对于千兆网环境,一台单网口的机器最多支持 112 个这样的连接,即便考虑 skbuff 的额外开销,所有这些连接的发送缓冲区一共占用不到 10MB 内存。(即便 rtt 高达 1s,那么千兆网的 BDP 是 125MB,比起购买带宽的成本,这点内存开销可以忽略不计)。

PS. 服务端在收到 SYN 之后不会立刻创建 tcp_sock,而是会创建 tcp_request_sock 来处理三路握手,后者要小得多(256 字节),等到收到三路握手最后的 ACK 才创建 tcp_sock。主动关闭 TCP socket 会进入 TIME_WAIT 状态,tcp_sock 会被释放,取而代之的是小得多的 inet_timewait_sock 对象(192 字节),因此 TIME_WAIT 并不占用多少资源。总之,Linux 协议栈尽可能缩小 tcp_sock 的生命期,以节约内存。

实际测试结果

创建 10000 个 TCP socket 会使用 31552 KB 内存(通过比较 /proc/meminfo 得出),即每个 TCP socket 占用 3.155 KB,这个数字很接近上面考虑 SLAB overhead 之后的计算结果。

代码:github.com/chenshuo/rec

小结

中文网络上这种似是而非、以讹传讹的说法很多,常见的还包括“epoll 快是因为用了共享内存来避免拷贝数据”等等,不可轻信。

PS. 评论中可以找到错误说法的几个链接 。

「真诚赞赏,手留余香」
11 人赞赏
waterd
许万金
November
ziv
张春雨
libxv
于鑫
alex chen
文章被以下专栏收录
37 条评论
推荐阅读