linux内核之一台机器到底能够支持多少条tcp连接?
TCP 连接数不是端口限制的问题,而是内存容量的问题。一条空连接消耗约3.3KB内核内存;一台 128GB 的服务器理论上可以支撑4000 万条空连接;只要内存足够大,单机百万连接完全可行。附录 A:常见命令速查功能命令查看进程 FD 限制ulimit -n查看硬限制ulimit -Hn修改 FD 限制查看系统总 FD查看系统 FD 上限查看内核 FD 上限查看端口范围查看全连接队列查看 ESTA
第一章 开篇:一个经典的报错
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 对文件描述符的限制分为两层:
- 进程级限制:限制单个进程能打开的最大文件数
- 系统级限制:限制整个操作系统所有进程能打开的文件总数
如果进程打开的文件数超过了进程级限制,就会报 Too many open files(EMFILE 错误)。
如果系统所有进程打开的文件总数超过了系统级限制,就会报 File table overflow(ENFILE 错误)。
我们先来看进程级限制。
2.2 三个关键参数
进程级限制涉及三个参数:soft nofile、hard 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 万个连接。"
这是一个经典的误区。
这个误区混淆了两个概念:
- 服务端的监听端口数量(确实受限于 65535)
- 服务端能接受的连接总数(远不止 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
实验过程:
- 客户端连接服务端的 8000 端口,打满 5 万个连接
- 然后切换连接 8001 端口,端口可以复用
- 依次连接 8002、8003... 直到 8020
实验结果:
- 成功建立了 1,000,013 个 ESTABLISHED 连接
- 使用
ss -ant | grep ESTAB | wc -l验证 - 内存消耗:通过
slabtop可以看到 TCP 和 sock_inode_cache 对象数量达到 100 万
结论:单台客户端完全有能力发起百万并发连接。
第六章 服务端视角:一台机器能接受多少连接?
6.1 真正的瓶颈不是端口
对于服务端来说,端口号完全不是问题。服务端通常只监听一个端口(如 80 或 443),但可以接受来自任意客户端 IP 和端口的连接。
真正的瓶颈在于资源:
- 文件描述符(FD):每个连接需要一个 FD
- 内存:每个连接需要内核数据结构
- 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 状态的内存开销
实验过程:
- 客户端向服务端发起连接
- 直到建立 50,000 条连接(状态为 ESTABLISHED)
- 对比实验前后的
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 核心启示
-
不要怕 TIME_WAIT:很多人担心服务器上有几万个 TIME_WAIT 会把内存撑爆,其实完全不用担心,它们非常轻量级(每个才 0.17KB)。
-
真正的瓶颈在 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_cache、TCP 等内核对象的数量都达到了 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 结局:活跃连接的崩溃
实验中有一个反转。虽然连接建立成功了,但当客户端同时发送数据时,服务器瞬间崩溃。
原因分析:
-
OOM(内存溢出):发送数据需要分配接收缓冲区。原本仅剩的 100MB 内存瞬间被填满,导致内核触发 OOM Killer 杀掉进程,或者系统直接卡死。
-
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 ()
内核执行:
- 从
socket_alloc缓存池分配struct socket,初始化协议族、类型 - 为 TCP 协议,从
tcp_sock缓存池分配struct sock,初始化 TCP 状态(CLOSED) - 把
socket->sk指向刚创建的struct sock - 从
filp_cache分配struct file,初始化文件操作集 - 从
dentry_cache分配struct dentry,挂载到进程的/proc/[pid]/fd/ - 把
file->private_data指向struct socket - 把
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)
计算:
- 每个 Slab 总大小:8 页 × 4KB / 页 = 32,768 字节
- 实际有效数据:16 个对象 × 1984 字节 / 对象 = 31,744 字节
- 浪费(碎片):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 调大就能支持百万连接。
✅ 正确:
必须同时调大以下参数:
fs.file-max(系统级)fs.nr_open(内核级进程上限)/etc/security/limits.conf的 nofile(用户级)- 确保
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 能力范围内。
-
真正的成本和挑战:
- 数据传输:如果有大量用户同时收发数据,CPU 和带宽会成为新的瓶颈
- 业务逻辑:推送消息的分发、路由、存储
- 高可用:机器故障时的连接迁移
- 网络带宽:千万级的消息推送,带宽消耗非常大
第十二章 总结
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
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)