Skip to content

在现代互联网应用中,尤其是物联网(IoT)、即时通讯(IM)、直播等场景下,服务器需要维护海量的并发长连接。实现单机支持百万级别的长连接(C1M问题)是一项极具挑战性的系统工程,它不仅仅是写好应用代码,更涉及到对底层操作系统,特别是Linux内核的深度理解和参数优化。

本文将从基本概念入手,深入探讨需要优化的各项关键参数,并结合业界实践,为您提供一份详尽的指南。


一、基本概念

1. 文件描述符限制

在Linux系统中,一切皆文件。每一个TCP连接都会占用一个文件描述符(File Descriptor, fd)。因此,文件描述符的数量是限制连接数的第一个也是最常见的瓶颈。这个限制分为三个层级:

  • 系统级别限制 (fs.file-max):整个操作系统所有进程能打开的文件描述符总数。
  • 用户级别限制 (ulimit -u):单个用户能够打开的文件描述符总数。
  • 进程级别限制 (ulimit -n):单个进程能够打开的文件描述符总数。

要支持大量连接,必须首先放宽这些限制。

2. 服务器TCP连接数量上限

一个服务端应用理论上可以支持的最大TCP连接数是多少?这由TCP的四元组唯一确定一个连接:

TCP Connection = {客户端IP, 客户端端口, 服务端IP, 服务端端口}

对于一个监听特定端口的服务(例如0.0.0.0:8080),服务端IP和服务端端口是固定的。因此,理论上的连接上限取决于客户端IP和客户端端口的组合数量。

连接上限 = 客户端IP数量 × 客户端Port数量 = 2^32 × 2^16 ≈ 2^48

这是一个天文数字。然而,实际情况中,由于多种因素,我们远不能达到这个理论值。

实际限制因素:

  1. 公网IP地址限制:IPv4地址资源有限,且内网地址、保留地址等不能用于公网通信。
  2. 端口限制:客户端的端口号(ephemeral ports)虽然有约64K个,但并非全部可用。
  3. 服务器资源限制:这是最主要的瓶颈。每一个TCP连接都需要消耗服务器的内存(如TCP缓冲区、应用层缓冲区)、CPU(上下文切换、数据处理)和文件描述符等资源。因此,一台服务器能支持的最大连接数,在内核参数调优到位的前提下,主要取决于其内存大小。

二、解决方案:参数调优

1. 调整文件描述符限制

这是突破连接数限制的第一步。

  • 临时性调整 (当前会话生效)

通过 ulimit 命令可以立即调整当前终端会话的限制,但退出或重启后会失效。这对于临时测试非常有用。

bash
# -n: number of open files
# -H: hard limit
# -S: soft limit
# 调整为1048576 (即100万)
ulimit -HSn 1048576
  • 永久性设置

要使配置在系统重启后依然生效,需要修改配置文件 /etc/security/limits.conf

bash
sudo vim /etc/security/limits.conf

在文件末尾追加以下内容,表示为所有用户(*)和root用户设置文件描述符的软限制和硬限制。

# <domain>      <type>  <item>         <value>
# 软限制(soft limit)不能超过硬限制(hard limit)
# 建议将两者设置为相同的值以避免不必要的问题
*    soft  nofile  1048576
*    hard  nofile  1048576
root soft  nofile  1048576
root hard  nofile  1048576

修改后需要重新登录用户或重启服务器才能生效。

  • 修改内核全局限制

单个进程的文件描述符数量不能超过内核中所有进程的文件描述符总数限制。因此,还需要修改 /proc/sys/fs/file-max/proc/sys/fs/nr_open

通过修改 /etc/sysctl.conf 文件来进行永久化配置:

bash
sudo vim /etc/sysctl.conf

增加或修改以下内容:

# 设置系统所有进程一共可以打开的文件数量
fs.file-max = 2097152

# 设置单个进程可以打开的文件描述符数量 (建议设置为比 nofile 略大的值)
fs.nr_open = 1048576

运行 sudo sysctl -p 命令使配置立即生效。

2. Linux内核网络参数调优

除了文件描述符,还需要对Linux内核中TCP/IP协议栈的诸多参数进行精细调整。这些参数同样通过修改 /etc/sysctl.conf 并执行 sudo sysctl -p 来生效。

以下是核心参数及其说明:

# ======================== Socket 和连接队列 ========================

# 1. 增加监听队列(SYN队列)的长度
# 系统级别的最大连接数,防止在高并发时因为队列满了而丢弃新连接请求
# The maximum number of connections that can be queued for acceptance by the kernel.
net.core.somaxconn = 65535

# 2. 增加半连接队列的容量
# The maximum number of SYN requests that can be queued by the kernel.
net.ipv4.tcp_max_syn_backlog = 65535


# ======================== TIME_WAIT 状态优化 ========================
# TIME_WAIT是TCP连接主动关闭方会进入的状态,会占用资源一段时间(2*MSL)。
# 在高并发短连接场景下,大量TIME_WAIT状态的连接会迅速耗尽资源。

# 1. 开启TIME_WAIT状态连接的复用
# 允许将处于TIME_WAIT状态的socket用于新的TCP连接
net.ipv4.tcp_tw_reuse = 1

# 2. 缩短TIME_WAIT状态的超时时间 (FIN-WAIT-2)
net.ipv4.tcp_fin_timeout = 15

# 3. 设置系统中TIME_WAIT状态连接的最大数量
# 超过此数值时,系统会立即清理并打印警告
net.ipv4.tcp_max_tw_buckets = 1048576

# 注意:`net.ipv4.tcp_tw_recycle` 参数在Linux 4.12及更高版本的内核中已被移除,
# 因为它在NAT环境下可能导致严重问题。因此不建议再使用此参数。
# net.ipv4.tcp_tw_recycle = 0


# ======================== TCP Keepalive 参数 ========================
# 对于长连接应用,Keepalive机制用于探测对端是否存活。

# 1. 缩短TCP连接的保活时间 (单位: 秒)
# 连接在闲置多久后开始发送保活探测包,默认为7200s (2小时)
net.ipv4.tcp_keepalive_time = 600

# 2. 缩短探测包的发送间隔 (单位: 秒)
net.ipv4.tcp_keepalive_intvl = 15

# 3. 减少探测失败的重试次数
# 在判定连接失效前,发送多少个保活探测包
net.ipv4.tcp_keepalive_probes = 3


# ======================== TCP 内存和缓冲区 ========================
# 这是影响性能和连接容量的关键。需要根据服务器物理内存进行调整。

# 1. TCP总内存使用 (单位: page, 1 page = 4KB)
# [low, pressure, high]
# low: 低于此值,TCP不进行内存压力控制
# pressure: 进入内存压力模式的阈值
# high: TCP全局内存用量的硬限制
net.ipv4.tcp_mem = 8388608 12582912 16777216

# 2. TCP接收/发送缓冲区大小 (单位: byte)
# [min, default, max]
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# 3. 核心Socket缓冲区大小
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216


# ======================== 连接跟踪 (Firewall/NAT) ========================
# 如果服务器启用了防火墙(netfilter),连接跟踪表也是一个常见瓶颈。

# 1. 增大连接跟踪表的大小
# 注意: 如果防火墙未开启,此参数可能不存在。
net.netfilter.nf_conntrack_max = 1048576

# 2. 缩短连接跟踪表中TIME_WAIT状态的超时时间
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30


# ======================== 客户端参数 (当服务器作为客户端时) ========================
# 当你的服务器需要作为客户端去连接其他服务时(例如代理服务器),本地端口范围也可能成为瓶颈。

# 1. 扩大本地端口范围
net.ipv4.ip_local_port_range = 10000 65535

三、超越内核调优:当内核成为瓶颈

值得注意的是,当连接数达到百万甚至千万级别(C10M)时,即使经过深度优化,Linux内核本身处理网络数据包的开销也会成为新的瓶颈。此时,业界会采用更极致的方案:

  • 内核旁路(Kernel Bypass):让网络数据包绕过操作系统的内核协议栈,直接由用户态的应用程序处理。
  • 代表技术
    • DPDK (Data Plane Development Kit):Intel提供的开源套件,允许用户态程序直接操作网卡。
    • PF_RING / Netmap:同样是实现高速数据包捕获和传输的机制。

这种方式将数据平面(Data Plane)的处理从内核移到应用中,而内核只负责控制平面(Control Plane),从而获得极高的网络吞吐量和极低的时延,但这需要应用自身实现TCP/IP协议栈,开发复杂度也更高。


四、总结

实现百万长连接是一个系统性的优化过程,从文件描述符到TCP内核参数,每一环都至关重要。本文提供的参数可以作为一个坚实的起点,但最佳配置永远取决于您的具体应用场景、硬件配置和业务负载。

建议的优化步骤:

  1. 评估硬件:确保有足够的内存,这是支持大量连接的基础。
  2. 放宽文件描述符限制:通过 limits.confsysctl.conf 进行永久化配置。
  3. 精细化内核参数:参考本文列表,逐步调整 /etc/sysctl.conf 中的网络参数。
  4. 压力测试:使用专业的压测工具(如 emqtt-bench 等)模拟真实负载,监控系统各项指标,找到瓶颈并反复调优。
  5. 应用层面优化:选择高效的I/O模型(如 epoll / kqueue),并优化应用内存使用。

五、扩展阅读

  1. Tuning EMQX to Scale to One Million Concurrent Connection on Kubernetes
  2. The Secret to 10 Million Concurrent Connections -The Kernel is the Problem, Not the Solution
  3. A Million-user Comet Application with Mochiweb (Part 3)
  4. 有赞 TCP 网络编程最佳实践
  5. 海量长连接消息推送系统实践