性能调优:Linux Kernel 优化(针对 10k+ 连接)

发布: (2025年12月21日 GMT+8 05:30)
10 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容,我将按照要求保留原始链接、格式和技术术语,仅翻译文本部分。

Introduction

在高并发实时架构中,性能瓶颈不可避免地会从应用层转移到操作系统层面。一个在 GeventEventlet 上运行良好的 Flask‑SocketIO 应用理论上可以处理数万条并发连接。然而,在默认的 Linux 环境下,这类应用往往会在 CPU 或内存资源饱和之前就崩溃或停止接受新连接。

出现这种平台的原因是,Linux 内核默认的调优面向通用计算,而不是作为大量持久 TCP 连接的终结点。对于 WebSocket 服务器——连接是长时间且有状态的——资源耗尽的表现为:

  • 文件描述符限制
  • 短暂端口枯竭
  • TCP 栈拥塞

下面的文章概述了在内核层面上需要进行的调优,以将 Flask‑SocketIO 的规模突破 10 000 连接的瓶颈。

文件描述符

在类 Unix 操作系统中,“一切皆文件”。这也包括 TCP 套接字。当客户端连接到服务器时,内核会为该套接字分配一个 文件描述符 (FD)

  • 默认情况下,大多数 Linux 发行版对每个进程强制 1024 个打开文件描述符 的限制——这是一个遗留约束。
  • 对于 WebSocket 服务器来说,这意味着在大约 1 000 条并发用户(再加上日志文件和共享库占用的少量描述符)之后,应用会崩溃或抛出
OSError: [Errno 24] Too many open files

内核区分两类限制:

  • 软限制 – 用户可配置的上限。
  • 硬限制 – 由 root 设置的绝对上限。

验证

ulimit -n
# → 1024

修复

系统范围/etc/security/limits.conf):

* soft nofile 65535
* hard nofile 65535

systemd 服务/etc/systemd/system/app.service):

systemd 会忽略用户限制;必须在单元文件中显式定义:

[Service]
LimitNOFILE=65535

短暂端口

文件描述符限制 入站 连接,而 短暂端口 限制 出站 连接。这一点对依赖 Redis 等消息中间件的 Flask‑SocketIO 架构尤为关键。

当 Flask 应用连接到 Redis(或 Nginx 连接到你的上游 Flask/Gunicorn 工作进程)时,会打开一个 TCP 套接字。内核会从 短暂端口范围 中分配一个 本地端口

  • 默认范围通常较窄(例如 32768–60999),仅提供约 28 000 个端口。
  • 在高吞吐场景下——例如 Flask 应用对 Redis 进行激进发布,或 Nginx 代理大量流量——服务器可能会耗尽可用本地端口。

症状

  • 日志中出现 EADDRNOTAVAIL (Cannot assign requested address) 错误。
  • Flask 应用突然无法与 Redis 通信,尽管 Redis 本身健康。
  • Nginx 返回 502 Bad Gateway,因为它无法打开到上游的套接字。

调优

# 检查当前范围
sysctl net.ipv4.ip_local_port_range

/etc/sysctl.conf 中添加以下内容以扩大范围:

net.ipv4.ip_local_port_range = 1024 65535

应用更改:

sudo sysctl -p

TIME_WAIT 状态

TCP 扩展中最常被误解的部分是 TIME_WAIT 状态。当 TCP 连接关闭时,主动发起关闭的一方会进入 TIME_WAIT,持续 2 * MSL(Maximum Segment Lifetime),通常为 60 秒。这可以确保延迟的数据包被正确处理,而不会被误认为是同一端口上的新连接。

在高 churn 环境下(例如客户端不断刷新页面或重新连接),服务器可能会累计成千上万的套接字处于 TIME_WAIT。这些套接字:

  • 消耗系统资源。
  • 锁定 4‑元组(源 IP、源端口、目标 IP、目标端口),阻止新的出站连接。

tcp_tw_recycle请勿使用

旧的指南曾建议启用 net.ip…(后文省略)

Source:

v4.tcp_tw_recycle`. 它在 Linux kernel 4.12 中被 移除,因为它会通过激进地丢弃乱序数据包,导致 NAT 后面的用户连接中断。

tcp_tw_reuse – 安全替代方案

net.ipv4.tcp_tw_reuse 允许内核在 新连接的时间戳严格大于旧连接的最后一个数据包的时间戳 时,回收一个 TIME_WAIT 状态的套接字用于新的出站连接。对大多数内部基础设施(例如 Flask ↔ Redis)来说是安全的。

配置/etc/sysctl.conf):

# 允许在 TIME_WAIT 状态下复用套接字用于新连接
net.ipv4.tcp_tw_reuse = 1

应用:

sudo sysctl -p

WebSocket 基准测试

ab(Apache Bench)这样的标准 HTTP 基准测试工具对 WebSocket 毫无用处。它们衡量的是 每秒请求数,而 WebSocket 的主要指标是 并发数(同时打开的连接数)和 消息延迟

推荐工具

  • Artillery – 支持 WebSocket 场景。
  • Locust – 可以脚本化实现持久连接。

测试方法

  1. Ramp‑up – 不要一次性连接 10 k 用户;这会触发 “惊群” 保护或 SYN‑flood 防御。应在数分钟内逐步提升。
  2. Sustain – 长时间保持连接打开。
  3. Broadcast – 在保持连接期间触发一次广播事件,以测量 Redis 后端和 Nginx 代理缓冲的延迟。

结果解读

失败点可能原因
~1024 用户文件描述符限制仍然生效
~28 000 用户临时端口范围耗尽
>30 000 TIME_WAIT 套接字连接 churn 问题或缺少 tcp_tw_reuse

可观测性

可观测性是确认内核调优是否生效的唯一途径。在运行高并发工作负载时,监控特定的 OS 级别指标。

关注指标

  • process_open_fds(Gunicorn/uWSGI 进程)——如果该数值在某个固定值(如 1024 或 4096)处趋于平稳且 CPU 仍然低,则说明已经触及硬性限制。
  • 套接字状态计数ESTABLISHEDTIME_WAIT 等。
    • ESTABLISHED 应该与活跃用户数相匹配。
    • TIME_WAIT 突升至 30 k+ → churn 问题或需要 tcp_tw_reuse
  • 已分配套接字sockstat 输出。

示例命令:

# 查看进程使用的打开文件描述符数量(替换 <pid>)
cat /proc/<pid>/fd | wc -l

# 套接字统计信息
ss -s

# 按状态过滤的详细套接字列表
ss -tan state established | wc -l
ss -tan state time-wait | wc -l

Source:

网络缓冲区的内存消耗

如果您使用 iptablesDockernf_conntrack 表会限制防火墙能够跟踪的连接数量。

# 检查内核日志中是否出现 conntrack 表溢出
dmesg | grep "nf_conntrack: table full, dropping packet"

调优(示例):

sysctl -w net.netfilter.nf_conntrack_max=131072

过度内核调优的风险

区域潜在问题影响
安全性扩大临时端口范围会让端口扫描稍微容易一些(在私有 VPC 内影响可忽略)。
稳定性将文件描述符限制设置得过高(例如,数百万)可能导致应用程序的内存泄漏使整个服务器崩溃,而不仅仅是该进程。
连接跟踪增加 nf_conntrack_max 会消耗内核内存(RAM)。请确保服务器拥有足够的内存来存储 10 万以上的跟踪连接状态。

黄金法则:
切勿盲目应用 sysctl 设置。应通过配置管理工具(Ansible、Terraform)进行部署,记录 为何 需要这些设置,并通过负载测试进行验证。

将 Flask‑SocketIO 扩展至 10 000+ 连接

实现高并发既是系统工程问题,也是软件问题。默认的 Linux 配置较为保守——面向桌面或低流量服务器。通过系统性地处理以下三项:

  1. 文件描述符限制 (ulimit)
  2. 临时端口范围 (net.ipv4.ip_local_port_range)
  3. TCP TIME‑WAIT 重用 (net.ipv4.tcp_tw_reuse)

即可释放操作系统处理大量并发套接字的能力。

生产就绪检查清单

  • 文件描述符限制 – 为 Gunicorn 进程设置 ulimit -n > 65535
  • 临时端口范围net.ipv4.ip_local_port_range = 1024 65535
  • TCP TIME‑WAIT 重用net.ipv4.tcp_tw_reuse = 1
  • TCP TIME‑WAIT 回收net.ipv4.tcp_tw_recycle = 0(或该键不存在)。
  • Conntrack 表 – 如使用有状态防火墙,请增大 nf_conntrack_max

示例 sysctl 配置

# /etc/sysctl.d/99-flask-socketio.conf
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0
net.netfilter.nf_conntrack_max = 131072

应用更改:

sudo sysctl --system

请记住:

  • 提高 nf_conntrack_max 后要监控内存使用情况。
  • 关注打开的文件描述符数量(lsofcat /proc/<pid>/fd)。
  • 进行负载测试(例如使用 locustwrk)以验证系统在预期流量下仍保持稳定。
Back to Blog

相关文章

阅读更多 »