第一章 开篇:一个经典的报错

1.1 "Too many open files"

如果你做过高并发服务器的开发或运维,几乎一定遇到过这个报错:

socket: Too many open files

或者更完整的:

java.net.SocketException: Too many open files

这个错误信息翻译过来就是 "打开的文件太多了"。但对于很多开发者来说,第一反应往往是:"我没打开文件啊,我只是建立了一个 TCP 连接而已!"

这就是问题的核心所在 —— 在 Linux 系统中,"一切皆文件"。

每当你建立一个 TCP 连接,内核就会分配一个文件描述符(File Descriptor, FD)。当你打开一个普通文件、创建一个管道、或者建立一条网络连接,在内核看来,它们本质上都是 "文件"。

如果没有限制,一个存在 Bug 的程序(比如忘记关闭连接)或者恶意攻击者,可能会不断创建新的连接,最终耗尽系统内存,导致整个操作系统崩溃。

因此,Linux 设计了一套双重阀门机制来控制文件打开的数量。


第二章 第一道防线:进程级文件描述符限制

2.1 两层限制机制

Linux 对文件描述符的限制分为两层:

  1. 进程级限制:限制单个进程能打开的最大文件数
  2. 系统级限制:限制整个操作系统所有进程能打开的文件总数

如果进程打开的文件数超过了进程级限制,就会报 Too many open files(EMFILE 错误)。

如果系统所有进程打开的文件总数超过了系统级限制,就会报 File table overflow(ENFILE 错误)。

我们先来看进程级限制。

2.2 三个关键参数

进程级限制涉及三个参数:soft nofilehard nofile 和 fs.nr_open

2.2.1 Soft nofile(软限制)

这是当前生效的限制值。当进程打开的文件数达到这个值时,就会触发 EMFILE 错误。

特点:

  • 进程可以在运行时自行修改这个值
  • 但修改的上限不能超过 hard nofile

查看方式:

ulimit -n
2.2.2 Hard nofile(硬限制)

这是 soft nofile 的上限

特点:

  • 普通用户只能降低 hard nofile,不能提高
  • 只有 root 用户才能提高 hard nofile
  • 作用:作为安全防线,防止用户随意将软限制调得过高

查看方式:

ulimit -Hn
2.2.3 fs.nr_open(内核级进程限制)

这是内核允许的单个进程能打开的最大文件数的绝对上限

优先级:即使你把 hard nofile 设置得很大,如果 fs.nr_open 比较小,那么实际生效的上限依然是 fs.nr_open。

这三个参数的层级关系是:

soft nofile ≤ hard nofile ≤ fs.nr_open

2.3 内核源码中的检查逻辑

通过内核源码可以看到这个检查过程。当进程调用 socket() 创建连接时,内核会调用 get_unused_fd_flags() 函数。

在这个函数里,内核会检查当前进程已打开的 FD 数量是否超过了 rlimit(RLIMIT_NOFILE)(也就是 soft nofile):

// fs/file.c 中的关键逻辑(简化描述)
if (current->files->next_fd >= rlimit(RLIMIT_NOFILE))
    return -EMFILE; // Too many open files
  • 如果没超过:继续分配
  • 如果超过了:直接返回 EMFILE 错误

2.4 一个惨痛的教训

这里有一个真实的教训:有人曾修改了 fs.nr_open,但重启后失效了,导致无法登录系统。

原因分析:

  • 直接用 echo 修改 /proc 下的参数是临时的,重启后会丢失
  • 如果设置不当(比如设得太小),重启后系统可能无法启动关键服务
  • 因为登录过程本身也需要打开文件(SSH 需要建立连接、加载库文件等)

这就是为什么修改系统参数要用 sysctl 或者修改配置文件,而不是直接 echo 到 /proc。


第三章 第二道防线:系统级文件描述符限制

3.1 fs.file-max

即使每个进程都很守规矩,没有超过自己的限制,但如果这台机器上跑了成千上万个进程,它们加起来打开的文件总数可能会耗尽内核内存。

这就涉及到了系统级参数:fs.file-max

定义:整个操作系统范围内,所有进程加起来能打开的最大文件句柄总数。

查看方式:

cat /proc/sys/fs/file-max

查看当前已打开的文件数:

cat /proc/sys/fs/file-nr

3.2 内核源码中的检查逻辑

在分配文件对象(sock_alloc_file 调用 alloc_file)时,内核会检查:

// 简化描述
if (get_nr_files() >= files_stat.max_files)
    return -ENFILE; // File table overflow

其中:

  • get_nr_files():当前系统已打开的文件总数
  • files_stat.max_files:即 fs.file-max

3.3 Root 用户的特权

需要特别指出:root 用户不受这个限制(或者说优先级极高)。

这是为了防止系统在极限情况下彻底死锁,留一条后路给管理员。即使系统达到了 fs.file-max 的限制,root 用户仍然可以登录并执行修复操作。


第四章 打破迷思:TCP 连接数真的只有 65535 吗?

4.1 一个常见的误区

很多人认为:"TCP 的端口号是 16 位的,最大值 65535,所以一台服务器最多只能支持 6.5 万个连接。"

这是一个经典的误区。

这个误区混淆了两个概念:

  1. 服务端的监听端口数量(确实受限于 65535)
  2. 服务端能接受的连接总数(远不止 65535)

4.2 四元组决定唯一性

TCP 连接是由一个四元组唯一确定的:

{源IP, 源端口, 目的IP, 目的端口}

对于服务端来说:

  • 目的 IP(服务器 IP)是固定的
  • 目的端口(如 80 端口)是固定的
  • 源 IP源端口是客户端的,可以变化

因此,服务端能够接受的连接数理论上是非常大的:

  • 源 IP 有 2^32 ≈ 42 亿 种可能
  • 源端口有 2^16 = 65536 种可能
  • 理论最大连接数 ≈ 42 亿 × 6.5 万 ≈ 2.8 × 10^14(两百多万亿)

4.3 内核源码如何区分连接

为了证明端口可以复用,我们需要深入到 Linux 内核源码层面。

内核中有一个核心结构体 struct sock_common,它记录了:

struct sock_common {
    // ...
    __be32  skc_rcv_saddr;   // 接收端IP(本地IP)
    __be32  skc_daddr;       // 发送端IP(远端IP)
    __be16  skc_num;         // 本地端口
    __be16  skc_dport;       // 远端端口
    // ...
};

当网络包到达时,内核通过 __inet_lookup() 函数查找对应的 socket。

匹配宏 INET_MATCH 的核心逻辑:

#define INET_MATCH(sk, net, hash, daddr, dport, saddr, sport) \
    (sk->sk_hash == hash &&                    \
     net_eq(sock_net(sk), net) &&              \
     sk->sk_daddr == daddr &&                  \
     sk->sk_rcv_saddr == saddr &&              \
     sk->sk_dport == dport &&                  \
     sk->sk_num == sport)

关键结论:内核并不是只比对端口,而是比对了四元组。只要四元组中有一个元素不同,内核就能准确区分出这是两条不同的连接,绝对不会串线。

第五章 客户端视角:一台机器能发起多少连接?

5.1 客户端的限制

从客户端视角看,问题稍微复杂一些。

对于客户端来说:

  • 目的 IP(服务器 IP)是固定的
  • 目的端口(如 80 端口)是固定的
  • 变化空间只有源 IP源端口

单 IP 情况下,客户端可用的临时端口范围是有限的。

5.2 ip_local_port_range

Linux 系统为了安全和管理,默认只允许使用一部分端口作为临时端口(ephemeral ports)。

查看方式:

cat /proc/sys/net/ipv4/ip_local_port_range

默认值通常是:

32768 60999

这意味着只有约 28,231 个端口可用。

5.3 突破 65535 的方法

方法一:调大端口范围

直接修改 ip_local_port_range

# 临时修改
echo "5000 65000" > /proc/sys/net/ipv4/ip_local_port_range

# 永久修改(/etc/sysctl.conf)
net.ipv4.ip_local_port_range = 5000 65000

这样可用端口就增加到了约 60,000 个。

方法二:多 IP

给客户端配置多个 IP 地址,每个 IP 都可以使用 60,000 个端口。

计算方式:20 个 IP × 60,000 端口 = 120 万连接

这就是最经典的多 IP 突破法。

方法三:连接不同的服务端端口

即使客户端只有一个 IP,只要连接的服务端端口不同,客户端的端口就可以复用。

例如:

  • 连接服务端的 8000 端口:使用源端口 5000
  • 连接服务端的 8001 端口:同样可以使用源端口 5000

因为四元组中的目的端口不同,内核会认为这是两条不同的连接。

5.4 实验验证:单客户端百万连接

我们通过一个实验来验证上述理论。

实验条件

  • 客户端:1 台机器
  • 服务端:监听 20 个端口(8000~8019)
  • 端口范围:5000~65000

实验过程

  1. 客户端连接服务端的 8000 端口,打满 5 万个连接
  2. 然后切换连接 8001 端口,端口可以复用
  3. 依次连接 8002、8003... 直到 8020

实验结果

  • 成功建立了 1,000,013 个 ESTABLISHED 连接
  • 使用 ss -ant | grep ESTAB | wc -l 验证
  • 内存消耗:通过 slabtop 可以看到 TCP 和 sock_inode_cache 对象数量达到 100 万

结论:单台客户端完全有能力发起百万并发连接。


第六章 服务端视角:一台机器能接受多少连接?

6.1 真正的瓶颈不是端口

对于服务端来说,端口号完全不是问题。服务端通常只监听一个端口(如 80 或 443),但可以接受来自任意客户端 IP 和端口的连接。

真正的瓶颈在于资源

  1. 文件描述符(FD):每个连接需要一个 FD
  2. 内存:每个连接需要内核数据结构
  3. CPU:处理网络包需要 CPU 时间

6.2 内存:真正的核心瓶颈

这才是本文最核心的内容 ——一条 TCP 连接到底消耗多少内存?

我们通过一个精心设计的实验,精确测量了每条 TCP 连接的内存开销。

6.3 实验设计

实验目的:建立 50,000 条 TCP 连接,测量内核多消耗了多少内存。

实验环境

  • 两台机器:一台客户端(Client),一台服务端(Server)
  • 工具:slabtop(查看内核缓存)、/proc/meminfo(查看总内存)

关键准备工作

在开始测试前,需要调整以下内核参数:

# 1. 扩大端口范围(客户端需要)
net.ipv4.ip_local_port_range = 5000 65000

# 2. 关闭TIME_WAIT快速回收(为了观测TIME_WAIT的内存开销)
net.ipv4.tcp_tw_reuse = 0
net.ipv4.tcp_tw_recycle = 0

# 3. 扩大TIME_WAIT桶数量,防止连接被丢弃
net.ipv4.tcp_max_tw_buckets = 600000

# 4. 扩大全连接队列
net.core.somaxconn = 1024

测量基准线

在开始测试前,先记录初始内存数值:

# 查看Slab内存
slabtop -o | head -20

# 查看内存总量
cat /proc/meminfo | grep -E "MemTotal|MemFree|Slab"

例如,客户端初始 Slab 内存是 39,848 kB

6.4 核心实测:ESTABLISHED 状态的内存开销

实验过程

  1. 客户端向服务端发起连接
  2. 直到建立 50,000 条连接(状态为 ESTABLISHED)
  3. 对比实验前后的 slabtop 数据

数据分析

对比实验前后的 slabtop 数据,以下几项暴涨:

Slab 对象 单对象大小 说明
TCP 1.94 KB 核心 struct tcp_sock
sock_inode_cache 0.62 KB struct socket + struct inode
kmalloc-256 0.25 KB struct file(文件对象)
dentry 0.19 KB 目录项缓存
kmalloc-64 0.06 KB 等待队列或端口绑定信息
合计 ~3.06 KB

算一笔账

把这些加起来:

1.94 + 0.62 + 0.25 + 0.19 + 0.06 ≈ 3.06 KB

总量验证

(实验后总Slab - 实验前总Slab) / 50000
= (206,896 - 39,848) / 50,000
≈ 3.34 KB

结论:理论计算(3.06 KB)和实际测量(3.34 KB)非常接近。

这意味着在 Linux 下,维持一条 TCP 连接(ESTABLISHED 状态),内核大概要消耗 3.3 KB 左右的内存。

6.5 服务端与客户端的差异

服务端不需要绑定本地端口(因为只用监听端口),所以少了一个 tcp_bind_bucket 的开销。

  • 服务端单条连接开销:约 3.05 KB
  • 客户端单条连接开销:约 3.34 KB(略大)

6.6 FIN_WAIT2 状态的内存开销

当连接关闭时,资源会被逐步释放。

实验:客户端发送 FIN,服务端回复 ACK,客户端处于 FIN_WAIT2 状态。

结果

  • 开销:约 0.396 KB
  • 分析:此时内核释放了大部分资源,只保留了最基本的控制块

6.7 TIME_WAIT 状态的内存开销

TIME_WAIT 是让很多人头疼的状态。主动关闭连接的一方会进入此状态,持续 2MSL(通常 60 秒)。

实验:让连接进入 TIME_WAIT 状态,测量内存开销。

结果

  • 开销:约 0.17 KB
  • 分析:
    • TCP 这个 slab 消失了(被回收)
    • 只剩下 tw_sock_TCP(0.19K)和 dentry
    • 结论:TIME_WAIT 虽然占用连接数名额,但占用的内存非常小

6.8 不同连接状态的内存开销对比

连接状态 核心消耗对象 单连接大约内存 备注
ESTABLISHED tcp_sock, socket, file, dentry ~3.3 KB 资源最全,占用最大
FIN_WAIT2 基础控制块 ~0.4 KB 资源已释放大半
TIME_WAIT tw_sock_TCP, dentry ~0.17 KB 占用极小

6.9 核心启示

  1. 不要怕 TIME_WAIT:很多人担心服务器上有几万个 TIME_WAIT 会把内存撑爆,其实完全不用担心,它们非常轻量级(每个才 0.17KB)。

  2. 真正的瓶颈在 ESTABLISHED:如果你有 10 万个并发长连接,消耗的内存大约是:

    100,000 × 3.3KB ≈ 330MB
    
     

    虽然看起来不多,但如果连接数达到百万级:

    1,000,000 × 3.3KB ≈ 3.3GB
    
     

    这就是几个 GB 的内存了,且全是内核内存(Slab),这是无法通过 swap 交换到磁盘的,必须物理内存扛住。


第七章 百万连接实战:系统调优指南

7.1 实验目标

在一台机器上实现 1,000,000+ 的 TCP 并发连接。

7.2 两种方案

方案一:多 IP 客户端法
  • 架构:服务端只开启一个进程,监听一个端口
  • 客户端:使用 20 个 IP 地址发起连接
  • 原理:利用源 IP 不同,突破单 IP 的端口限制
方案二:多进程服务端法
  • 架构:服务端开启多个进程,每个进程监听不同端口
  • 客户端:使用一个 IP,连接服务端的不同端口
  • 原理:利用目的端口不同来区分连接

7.3 方案一详细步骤(重点推荐)

7.3.1 硬件准备
  • 两台机器:一台客户端,一台服务端
  • 内存:建议大于 4GB(客户端 4GB,服务端 6GB 以上)
  • CPU:单核即可
  • IP 资源:客户端需要配置 20 个虚拟 IP
7.3.2 客户端参数调优

1. 调整可用端口范围

# /etc/sysctl.conf
net.ipv4.ip_local_port_range = 5000 65000

2. 调整系统级最大打开文件数

# /etc/sysctl.conf
fs.file-max = 1100000

3. 调整进程级最大打开文件数

# /etc/sysctl.conf
fs.nr_open = 60000

4. 调整用户级限制

# /etc/security/limits.conf
* soft nofile 55000
* hard nofile 55000

注意hard nofile 必须小于 fs.nr_open,否则可能导致用户无法登录!

5. 配置额外的 20 个 IP

使用 ifconfig 或 ip addr 绑定虚拟 IP:

# 示例:绑定 eth0:0 到 eth0:19
ifconfig eth0:0 192.168.1.100 netmask 255.255.255.0 up
ifconfig eth0:1 192.168.1.101 netmask 255.255.255.0 up
# ... 以此类推

6. 清理缓存

echo "3" > /proc/sys/vm/drop_caches
7.3.3 服务端参数调优

1. 调整文件句柄限制

# /etc/sysctl.conf
fs.file-max = 1100000
fs.nr_open = 1100000
# /etc/security/limits.conf
* soft nofile 1010000
* hard nofile 1010000

2. 调整全连接队列

# /etc/sysctl.conf
net.core.somaxconn = 1024

为什么需要调大 somaxconn?

三次握手完成后,连接会被放入全连接队列(accept 队列),等待应用程序调用 accept() 取出。

默认值 128 意味着,如果应用程序处理不及时,全连接队列满了之后,后续的握手成功包(第三次握手的 ACK)会被丢弃,导致客户端以为连接还未建立,触发超时重传,握手速度急剧变慢

7.3.4 开始实验

1. 启动服务端

# 设置端口(默认8090)
make run-srv
# 验证监听
netstat -nlt | grep 8090

2. 启动客户端

# 设置服务端IP和端口
make run-cli

3. 实时监控连接数

watch "ss -ant | grep ESTAB | wc -l"

4. 预期结果

如果一切顺利,你会看到:

  • 连接数稳步上升
  • 突破 6.5 万(打破 65535 迷思)
  • 突破 10 万、50 万...
  • 最终超过 1,000,000

7.4 实验结果

连接数验证

使用 ss -ant | grep ESTAB | wc -l 命令查看,连接数达到了 1,000,024

内存验证

查看 /proc/meminfo

Slab: 3200000 kB
MemFree: 100000 kB

可以看到 Slab(内核缓存区)占用了 3.2GB 之多,验证了前面的理论 ——100 万连接 × 3.3KB ≈ 3.3GB。

此时系统剩余内存(MemFree)仅剩 100MB 左右。

内核对象验证

通过 slabtop 命令:

slabtop -o | head -10

可以看到 sock_inode_cacheTCP 等内核对象的数量都达到了 100 万左右。

7.5 常见问题与调试

问题 1:连接建立速度慢

  • 原因:全连接队列溢出丢包,导致超时重传
  • 解决:检查 somaxconn 是否生效,调大 backlog

问题 2:端口被占用(Address already in use)

  • 原因:TIME_WAIT 状态的连接还在
  • 解决:稍等片刻再启动,或者调大 tcp_max_tw_buckets

问题 3:实验结束后无法重启

  • 原因:端口范围或文件描述符设置不合理
  • 解决:检查 /etc/security/limits.conf 配置,确保 hard nofile < fs.nr_open

7.6 结局:活跃连接的崩溃

实验中有一个反转。虽然连接建立成功了,但当客户端同时发送数据时,服务器瞬间崩溃

原因分析

  1. OOM(内存溢出):发送数据需要分配接收缓冲区。原本仅剩的 100MB 内存瞬间被填满,导致内核触发 OOM Killer 杀掉进程,或者系统直接卡死。

  2. CPU 过载:处理大量并发数据包会让 CPU 使用率飙升到 100%,无法响应其他请求。

启示

  • 空连接(空闲连接)主要消耗内核 Slab 内存
  • 活跃连接(有数据传输)还要消耗更多的内存(缓冲区)和 CPU 时间
  • 所以在实际生产环境中,单机百万连接是可行的,但百万活跃连接是极为困难的

第八章 Too many open files 的排查与解决

8.1 如何排查

当系统报 Too many open files 时,可以按以下步骤排查:

1. 查看当前进程打开了多少文件

# 查看进程PID的文件描述符使用情况
ls -la /proc/<PID>/fd | wc -l

# 或者使用 lsof
lsof -p <PID> | wc -l

2. 查看系统级限制

cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr

file-nr 的三个值分别表示:已分配文件句柄数、已使用文件句柄数、最大文件句柄数。

3. 查看进程级限制

cat /proc/<PID>/limits | grep "open files"

8.2 快速修复

# 临时修改当前shell的限制
ulimit -n 1000000

# 修改系统级限制
sysctl -w fs.file-max=1100000

# 永久修改
echo "fs.file-max = 1100000" >> /etc/sysctl.conf
sysctl -p

8.3 永久配置

步骤 1:修改 /etc/sysctl.conf

# 系统级总限制
fs.file-max = 1100000

# 单进程内核级上限
fs.nr_open = 1100000

步骤 2:修改 /etc/security/limits.conf

# 所有用户
* soft nofile 1000000
* hard nofile 1000000

步骤 3:重启或重新登录

  • 修改 limits.conf 后,需要重新登录重启服务才能生效
  • 使用 ulimit -n 验证

8.4 避坑指南

坑 1:hard nofile 超过 fs.nr_open

如果 hard nofile 设置得比 fs.nr_open 还大,可能会导致:

  • 用户无法登录
  • SSH 无法建立连接
  • 系统完全不可访问

坑 2:直接用 echo 修改 /proc

# ❌ 危险操作
echo "1000000" > /proc/sys/fs/nr_open

如果设置得比当前已打开的文件数还小,或者设置得不合理,可能导致 SSH 无法登录(因为登录过程本身也需要打开文件),甚至导致系统崩溃。

✅ 正确做法

# ✅ 使用 sysctl 修改
sysctl -w fs.nr_open=1100000

# ✅ 或修改 /etc/sysctl.conf 后
sysctl -p

第九章 TCP 连接内存开销全景

9.1 四大核心内核对象

在深入了解 TCP 连接的内存开销之前,我们需要先搞清楚一条 TCP 连接在内核中到底对应哪些数据结构。

创建一个 TCP socket 时,内核会从不同的 Slab 缓存池里拿出四个核心对象

1. struct socket(来自 socket_alloc 缓存)

  • 这是给用户层(应用程序)看的接口
  • 提供 socket->ops 操作集(bind/connect/accept 等)

2. struct sock(来自 TCP 的 Slab 缓存)

  • 这是内核网络层真正干活的对象
  • 存储 TCP 的状态、窗口大小、接收发送队列等核心信息
  • TCP 场景下实际分配的是 struct tcp_sock(更大)

3. struct dentry(来自 dentry_cache 缓存)

  • 目录项,代表文件系统中的一个路径节点
  • Socket 也会被映射到文件系统,如 /proc/[pid]/fd/

4. struct file(来自 filp_cache 缓存)

  • Linux "一切皆文件" 的实现
  • 对接用户态的文件描述符(fd)

9.2 它们的关联方式

这四个对象通过指针串联成一条完整链路:

用户态 fd
    ↓
struct file (文件对象)
    ↓ file->private_data
struct socket (套接字通用层)
    ↓ socket->sk
struct sock (TCP/IP协议层核心控制块)
    ↓
struct dentry (目录项,挂载到 /proc/[pid]/fd 下)

9.3 创建流程详解

阶段 1:用户调用 socket ()

内核执行:

  1. 从 socket_alloc 缓存池分配 struct socket,初始化协议族、类型
  2. 为 TCP 协议,从 tcp_sock 缓存池分配 struct sock,初始化 TCP 状态(CLOSED)
  3. 把 socket->sk 指向刚创建的 struct sock
  4. 从 filp_cache 分配 struct file,初始化文件操作集
  5. 从 dentry_cache 分配 struct dentry,挂载到进程的 /proc/[pid]/fd/
  6. 把 file->private_data 指向 struct socket
  7. 把 struct file 映射到文件描述符,返回 sockfd

关键结论:创建一个 TCP 连接,内核至少要分配 4 个核心对象,它们分别来自不同的 Slab 缓存池。

9.4 完整的开销明细

通过 /proc/slabinfo 的数据,一张表看清单条 TCP 连接的内存开销:

Slab 对象 类型 大小 说明
TCP tcp_sock 1.94 KB TCP 控制块,占大头
sock_inode_cache socket + inode 0.62 KB BSD Socket 层
kmalloc-256 file 对象 0.25 KB 文件系统对象
dentry dentry 对象 0.19 KB 目录项缓存
kmalloc-64 socket_wq/bind_bucket 0.06 KB 等待队列 / 端口绑定
总计 ~3.06 KB
实际测量 ~3.34 KB

9.5 计算过程拆解

通过 /proc/slabinfo 中 TCP 的数据,可以清晰看到计算过程:

TCP  288  384  1984  16  8

解读:

  • 1984:每个 Slab 的大小(字节)
  • 16:每个 Slab 里能放多少个 TCP 对象
  • 8:每个 Slab 占用了多少个内存页(8 × 4KB = 32KB)

计算

  1. 每个 Slab 总大小:8 页 × 4KB / 页 = 32,768 字节
  2. 实际有效数据:16 个对象 × 1984 字节 / 对象 = 31,744 字节
  3. 浪费(碎片):32,768 - 31,744 = 1,024 字节

结论:碎片率仅为 3%(1KB/32KB),Slab 用微小的空间代价换取了极高的分配速度。

9.6 规模化计算

10 万空连接

100,000 × 3.3KB ≈ 330MB

100 万空连接

1,000,000 × 3.3KB ≈ 3.3GB

1000 万空连接

10,000,000 × 3.3KB ≈ 33GB

注意:这是纯内核 Slab 内存,不包括:

  • 应用层的内存消耗
  • 收发数据的缓冲区
  • 业务逻辑的内存

第十章 常见误区与避坑指南

10.1 误区一:端口 65535 是 TCP 连接上限

❌ 错误:端口最大 65535,所以一台机器最多 6.5 万连接。

✅ 正确

  • 服务端:四元组中源 IP + 源端口可变化,理论上限 ≈ 2^48 ≈ 2.8 × 10^14
  • 客户端:可以通过多 IP 或多端口突破 65535

10.2 误区二:TIME_WAIT 会撑爆内存

❌ 错误:TIME_WAIT 状态连接太多会导致内存不足。

✅ 正确

  • TIME_WAIT 每个连接只消耗约 0.17KB 内存
  • 10 万个 TIME_WAIT 才消耗约 17MB 内存
  • 真正的内存杀手是 ESTABLISHED 状态(~3.3KB / 个)

10.3 误区三:修改 limits.conf 后不用重启

❌ 错误:改了 /etc/security/limits.conf 后,正在运行的程序会自动生效。

✅ 正确

  • 修改 limits.conf 后,需要重新登录重启服务才能生效
  • 正在运行的程序不会自动获取新的限制

10.4 误区四:echo /proc 和 sysctl 一样

❌ 错误:直接 echo 修改 /proc 下的文件和 sysctl -w 效果一样。

✅ 正确

  • echo 直接操作 /proc 非常危险
  • 如果设置不合理(比如设得比当前值还小),可能导致 SSH 无法登录
  • 必须使用 sysctl -w 或修改 /etc/sysctl.conf 后 sysctl -p

10.5 误区五:只要调大 ulimit 就够了

❌ 错误:把 ulimit -n 调大就能支持百万连接。

✅ 正确
必须同时调大以下参数:

  1. fs.file-max(系统级)
  2. fs.nr_open(内核级进程上限)
  3. /etc/security/limits.conf 的 nofile(用户级)
  4. 确保 soft nofile ≤ hard nofile ≤ fs.nr_open ≤ fs.file-max

第十一章 架构实践:1 亿用户推送系统需要多少台机器?

基于本章的知识,我们来做一个经典的架构估算。

11.1 场景假设

  • 长连接推送服务(如微信、IM 即时通讯)
  • 大部分时间连接是空闲的(CPU 开销小,主要看内存)
  • 目标用户数:1 亿

11.2 单机容量估算

参数设置

  • 服务器内存:128GB
  • 保守设定:单机支持 500 万 并发连接

内存消耗验证

500万 × 3.3KB ≈ 16.5GB

这远小于 128GB,剩下的内存可用于:

  • 操作系统自身
  • 业务逻辑
  • 网络缓冲区
  • 留出 Buffer

11.3 最终计算

总用户数 / 单机容量 = 100,000,000 / 5,000,000 = 20

结论:仅仅需要 20 台 128GB 内存的服务器,就可以支撑 1 亿 用户的长连接!

11.4 一些说明

这个计算结果意味着什么?

  • 连接本身的开销非常小:1 亿个空连接,内核 Slab 内存消耗约为:

    100,000,000 × 3.3KB ≈ 330GB
    
     

    分摊到 20 台机器,每台 17GB,完全在 128GB 能力范围内。

  • 真正的成本和挑战

    1. 数据传输:如果有大量用户同时收发数据,CPU 和带宽会成为新的瓶颈
    2. 业务逻辑:推送消息的分发、路由、存储
    3. 高可用:机器故障时的连接迁移
    4. 网络带宽:千万级的消息推送,带宽消耗非常大

第十二章 总结

12.1 核心知识点回顾

1. "Too many open files" 的本质

  • Linux "一切皆文件"
  • TCP 连接也是文件,需要占用文件描述符
  • 两级限制:进程级(soft/hard nofile, fs.nr_open)和系统级(fs.file-max)

2. TCP 连接数的真正上限

  • 不是 65535
  • 服务端:四元组中的源 IP + 源端口可以任意变化,理论上限 ≈ 2^48
  • 客户端:可以通过多 IP 或多端口突破限制

3. 一条 TCP 连接的内存开销

连接状态 内存开销
ESTABLISHED ~3.3 KB
FIN_WAIT2 ~0.4 KB
TIME_WAIT ~0.17 KB

4. 百万连接是可行的

  • 需要调整系统参数:file-max, nr_open, limits.conf, somaxconn
  • 空连接主要消耗 Slab 内存
  • 活跃连接(有数据传输)还需要更多内存(缓冲区)和 CPU

5. 亿级用户架构

  • 20 台 128GB 内存的服务器即可支撑 1 亿长连接
  • 但真正的挑战在于数据传输、业务逻辑和高可用

12.2 一句话总结

TCP 连接数不是端口限制的问题,而是内存容量的问题。

 

一条空连接消耗约 3.3KB 内核内存;
一台 128GB 的服务器理论上可以支撑 4000 万 条空连接;
只要内存足够大,单机百万连接完全可行。


附录 A:常见命令速查

功能 命令
查看进程 FD 限制 ulimit -n
查看硬限制 ulimit -Hn
修改 FD 限制 ulimit -n 1000000
查看系统总 FD cat /proc/sys/fs/file-nr
查看系统 FD 上限 cat /proc/sys/fs/file-max
查看内核 FD 上限 cat /proc/sys/fs/nr_open
查看端口范围 cat /proc/sys/net/ipv4/ip_local_port_range
查看全连接队列 cat /proc/sys/net/core/somaxconn
查看 ESTABLISHED 连接数 ss -ant | grep ESTAB | wc -l
查看 Slab 内存 slabtop -o | head -20
查看内存详情 cat /proc/meminfo | grep -E "Slab|MemFree|MemTotal"
查看进程 FD 明细 ls -la /proc/<PID>/fd
查看进程所有限制 cat /proc/<PID>/limits

附录 B:百万连接配置文件参考

/etc/sysctl.conf

# 系统级文件句柄
fs.file-max = 1100000
fs.nr_open = 1100000

# 网络优化
net.core.somaxconn = 1024
net.ipv4.tcp_max_tw_buckets = 600000
net.ipv4.tcp_tw_reuse = 0
net.ipv4.ip_local_port_range = 5000 65000

0voice · GitHub

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐