
事非经过不知难。当初我在标题里写下“百万并发”四个字时心里想的是那种技术分享会上被掌声包围的高光画面——但实际动手做之后被按在地上摩擦的三个月才是真实记忆。从几千连接起步到真正扛住并稳定跑完一百万条长连接过程中踩过的每一个坑最后几乎都指向同一个方向你对 Linux 内核的理解决定了你调优的上限。这篇文章我把整个过程梳理出来围绕事件驱动模型、eve ntfd 唤醒机制、内核网络栈、调优参数这几个核心展开。内容不求面面俱到只求把“为什么这么做”讲透。不管你是正在做 C10K、C1000K 的开发者还是准备啃 Linux 内核又找不到切入点的新手这篇都值得你花十分钟慢读一遍。1. 百万并发这个目标先翻译成人话1.1 一百万连接在资源层面到底意味着什么先说结论百万并发不是“更难”的编程题它是完全不同的物种。假设每个 TCP 连接在内核态需要消耗约 3KB 内存socket 结构、TCP 控制块、读写缓冲区的基础开销一百万条连接光内核内存就接近 3GB。再加上用户态每个连接维护的应用层状态、缓冲区、定时器结构单进程内存占用轻松冲破 10GB。很多人在第一步就挂了因为连接没上万内存先撑不住了。然后是文件描述符。默认的ulimit -n是 1024这意味着你连一万并发都做不到。要放开到百万量级必须同时调整用户态nofile和内核态fs.file-max。这里有个容易忽略的细节fs.file-max是系统级上限ulimit -n是进程级上限两个都要改缺一不可。而且改完之后要确认/proc/sys/fs/file-nr里的已分配值和最大值别改完就以为万事大吉。最后是端口。作为服务端百万并发监听一个端口就够了只要客户端源端口足够多。但如果你是做压测的客户端单机发不出百万连接——客户端四元组被本地端口数量卡死了。这个问题我见过太多人踩服务端调优半天结果发现瓶颈在压测机器上。1.2 为什么“一连接一线程”活不过一万传统的阻塞 IO 模型每个连接分配一个线程这个方案在百级别并发时没有问题千级别也能靠堆机器解决。但到了万级别问题就是结构性的了。线程不是免费的。默认线程栈大小 8MB一万条线程光是虚拟内存就是 80GB。更致命的是上下文切换Linux 调度器在 CFS 算法下运行队列里的线程越多每个线程分到的 CPU 时间片越薄系统大量 CPU 周期都花在“保存现场、恢复现场”上真正干活的只剩零头。我用perf实测过线程数超过两千后上下文切换开销就开始吃掉 30% 以上的 CPU 周期吞吐量曲线不升反降。所以百万并发的地基必须是事件驱动少量线程处理海量连接通过 IO 多路复用等待事件。这个思路从 select 到 poll 再到 epoll本质没有变过——内核帮你盯着文件描述符有事就叫你。变的只是在内核里实现方式的效率。1.3 两个十万八千里的问题总连接数与活跃连接数在设计系统之前必须要分清“百万连接”和“百万 QPS”的区别。长连接场景下一百万条连接可能只有几百条在活跃收发数据这是物联网设备、消息推送网关的典型特征。而你要是想做到百万 QPS 的短连接处理那是完全另一个维度的挑战瓶颈会在 CPU 的软中断和锁竞争上。我的项目属于前者设备接入网关连接建好后多数时间在休眠偶尔上报心跳。这意味着系统的核心指标是“能稳定挂住一百万条空闲连接且能被快速唤醒”。认识清楚了这一点很多调优方向就清晰了。你不需要一台机器处理百万每秒的报文你需要的是大内存、大文件描述符表、高效的事件分发机制以及一个不会在空闲连接上浪费 CPU 的架构。2. epoll 不是银弹但它是百万并发的唯一现实选择2.1 从 select 到 epoll内核态到底改进了什么理解 epoll 为什么快得先知道 select 为什么慢。select 每次调用都要把全部 fd 集合从用户态拷贝到内核态然后内核暴力遍历所有 fd检查每个 socket 有没有事件再把结果拷回用户态。这个 O(n) 的过程在 fd 数量过万之后就是灾难——每次调用都是全量扫描加两次拷贝CPU 全耗在无意义的轮询上了。epoll 的两个核心改进改变了游戏规则。第一注册机制epoll_ctl把感兴趣的 fd 告诉内核内核为每个 fd 建立回调关系不再每次调用都全量拷贝。第二就绪链表当 fd 上有事件发生时内核通过回调函数把该 fd 加入到 epoll 实例的就绪链表中用户态调用epoll_wait时只需要把就绪链表里的事件拷出来复杂度从 O(n) 降到了 O(就绪事件数)。这个设计的美妙之处在于它和“边缘触发/水平触发”结合后能实现真正的事件驱动。活跃连接只有几百条时每次epoll_wait返回的事件数很少扫描代价几乎为零。内核帮你完成了从“主动轮询”到“被动通知”的转变。2.2 边缘触发和水平触发一个字决定架构走向epoll 的 LT水平触发和 ET边缘触发是面试高频题但实际项目里选错模式的人比比皆是。简单说LT 模式下只要缓冲区有数据epoll_wait就会反复通知你ET 模式下只有状态变化的那一次通知如果你没把数据读完内核不会再告诉你。我强烈建议网络编程新手选 LT原因只有一个ET 模式下你必须把数据读到 EAGAIN 为止通常是循环 read 直到返回 -1 且 errno 为 EAGAIN否则会丢数据。LT 模式不容易漏事件代价是可能出现“同一个 fd 被重复唤醒”的情况导致轻微的性能损耗。但对百万连接系统来说这个损耗是值得的因为你的活跃连接占比低重复唤醒开销摊薄后几乎可以忽略。如果你决定用 ET必须配合非阻塞 IO并且处理好 EAGAIN。这里分享一个我踩过的坑ET 模式 阻塞 socket 会导致灾难性后果——因为阻塞 socket 在最后一次 read 时如果没数据会卡住整个线程而不是返回 EAGAIN你的事件循环直接冻结。2.3 内核里 epoll 的数据结构红黑树与回调机制epoll 在内核里靠两个关键结构工作一棵红黑树和一个就绪链表。红黑树用于管理你注册的所有 fdepoll_ctl的增删改查都在树上完成复杂度 O(log n)就绪链表则由内核在事件发生时通过回调动态维护。epoll_wait返回后用户态拿到的是就绪事件。这里有一个被很多人忽略的细节内核在拷贝就绪事件时会检查该 fd 是否被标记为 LT 模式如果是并且事件尚未处理完毕那么它会被重新放回就绪链表。这个“重新入队”的操作正是 LT 反复通知的本质。理解这套机制对调优很关键。比如有同学问为什么我的epoll_wait返回特别频繁但业务上并没有新消息答案很可能就是 LT 模式下缓冲区里残留了数据内核在反复通知你。这时候要么把数据处理干净要么改成 ET 模式。3. eventfd 与唤醒机制别小看了“叫醒别人”这件事3.1 eventfd 是什么一个可以放进 epoll 的“手动事件源”我项目里的典型场景某个线程负责处理耗时的业务逻辑完成后需要通知 IO 线程去发送响应。如果直接往 socket 写数据会碰到并发问题如果用一个管道做唤醒又涉及两个 fd 和多余的系统调用。eventfd 就是为这个场景而生的它创建的是一个特殊的文件描述符内核维护一个 64 位计数器你往里面 write 一个值这个 fd 就会变成可读状态从而触发 epoll 的唤醒。一句话总结eventfd 让你能自己制造事件并把事件送进 epoll 的调度体系里。它把“跨线程通知”这个需求统一成了“fd 可读”这样一个 epoll 原生就能理解的事件避免了信号、条件变量等方案在事件循环模型里无法兼容的窘境。实际用法很简单调用eventfd(0, EFD_NONBLOCK)创建 fd把它加入 epoll 监听。业务线程完成工作后往这个 fd 里 write 一个 1IO 线程在epoll_wait返回时发现这个 fd 可读就知道有任务完成了。读取时务必用非阻塞模式循环读直到 EAGAIN避免计数器残留导致下次误唤醒。3.2 内核里的 eventfd唤醒路径是怎么回事说“调优 eventfd”之前先搞清楚内核唤醒了什么。调用write往 eventfd 写入时内核会更新计数器然后通过 waitqueue 唤醒正在epoll_wait上睡眠的线程。这个唤醒路径会经过一系列复杂的锁和调度操作开销并不小。所以在百万连接场景下eventfd 不宜“每个连接一个”。如果你的架构是每个连接都有自己的 eventfd 用于唤醒那内存里就有上百万个 eventfd 对象内核每个都要维护 waitqueue 和回调关系光这些结构就能吃掉大量内存和唤醒开销。正确做法是共享 eventfd多个连接共享少数几个事件通道比如按 CPU 核数分配每个核一个 eventfdIO 线程和 Worker 线程之间通过这些少量通道通信。共享后的副作用是惊群唤醒一个 eventfd 时所有等待在它上面的线程都会被叫醒但只有一个能拿到任务。内核 4.5 之后提供了EPOLLEXCLUSIVE标志加了这个标志后epoll 唤醒只唤醒等待队列中的第一个线程而不是广播给所有人实测在共享 eventfd 场景下能显著降低无效唤醒。3.3 唤醒风暴百万连接场景最容易忽视的隐患所谓唤醒风暴是指大量 eventfd 同时变成可读状态导致线程被频繁唤醒但实际任务量很少。这是一种“忙等式的浪费”——CPU 使用率看似很高吞吐量却没有提升。我的场景里出现过一次典型问题设备凌晨定时上报心跳大量 eventfd 在同一秒内被写入。默认情况下每个文件描述符的唤醒都是一次独立的调度事件内核在极短时间内要处理几十万次唤醒软中断和调度器压力瞬间飙升最终表现为epoll_wait返回频繁但单次返回事件数很少。解决思路有两个方向。一是打散峰值在应用层给设备心跳加随机延迟让上报时间均匀分布。二是合并唤醒多个任务完成后合并为一个 eventfd 事件把“每任务一唤醒”变成“每批一唤醒”。在实际压测中第二种方案的 CPU 占用降低了四成效果立竿见影。这也是我在深入看内核唤醒机制后收获最大的一课。3.4 惊群问题与 SO_REUSEPORT、EPOLLEXCLUSIVE 的正确用法惊群在 Linux 网络编程里是个老话题。最常见的形式多个线程/进程同时epoll_wait同一个监听 socket新连接到来时所有线程都被唤醒但只有一个能 accept 成功其他线程白白空转一圈。用我们组同事的话说这就像十几个人等一部电梯电梯门一开全醒了但只有一个人能上得去。传统 Nginx 的做法是accept_mutex用锁保证同一时刻只有一个 worker 在 accept。但内核 3.9 之后有了更干净的方案SO_REUSEPORT。它允许多个 socket 绑定同一个端口内核在收到新连接时通过哈希算法只交给其中一个 socket从根源上避免了 accept 惊群。这个特性在多核机器上效果明显还能均衡负载。但要注意SO_REUSEPORT解决的是监听 socket 的惊群事件处理阶段的惊群依然存在。如果你多个线程epoll_wait的是同一个 epoll 实例比如共享同一个连接集合有条连接来数据时仍可能唤醒多个线程。内核 4.5 加入的EPOLLEXCLUSIVE正是针对这种情况同一个 fd 被多个 epoll 实例监听时事件到达只唤醒其中一个。我在线程池共享连接的场景下实测过命中率提升显著。4. 内核网络栈百万连接下你不能不知道的事4.1 TCP 连接从进入到“可用”经历了什么一个 TCP 连接从客户端 SYN 到服务端 accept 返回在内核里走了一条相当长的路。三个队列是关键SYN 队列半连接队列、accept 队列全连接队列、以及最终建立后挂到 epoll 的 socket。连接数上来之前你可能根本没在意过这些队列的长度直到某天connection reset或connect timeout开始刷屏。SYN 队列存的是还没完成三次握手的连接它的长度由net.ipv4.tcp_max_syn_backlog控制。accept 队列存的是已完成握手、等待用户态 accept 的连接长度由两个参数同时约束net.core.somaxconn和 listen 函数传入的 backlog。Linux 内核实际取两者中较小的值。队列满了之后内核会丢弃 SYN 包或直接发 RST表现就是客户端连不上。百万连接的场景里这两个队列必须调得足够大。我的建议是somaxconn设为 65535tcp_max_syn_backlog设为 65535同时监听 socket 的 backlog 也传 65535。但这些值不是越高越好过大的队列会占用内核内存且一旦真的堆积大量连接内核遍历和清理它们的开销也会上升。合理值应该在压测中动态调整。4.2 软中断与收包路径连接多了CPU 先“累死”当连接数超过十万你可能观察到某个 CPU 核心的使用率打满其他核却很闲。这是网络收包的经典问题网卡的中断和内核的软中断尤其NET_RX_SOFTIRQ默认绑定在某个 CPU 上所有收包都在一个核上处理形成一个天然的瓶颈。内核 2.6 之后的 RPSReceive Packet Steering和 RFSReceive Flow Steering就是解决这个问题的。RPS 让收包软中断可以在多个 CPU 之间分散RFS 更进一步根据流的信息把同一个连接的数据包送到正在处理它的 CPU 上利用 CPU 缓存的局部性。配置方法很简单在/sys/class/net/网卡/queues/rx-n/rps_cpus里设置 CPU 掩码即可。我自己实测过四核机器上启用 RPS 后单核收包软中断从打满降到 60% 左右吞吐量提升近一倍。如果你用多队列网卡还可以考虑把不同队列绑到不同 CPU 上实现更细致的分流。但无论如何收包路径的 CPU 消耗是百万并发系统绕不开的成本建议压测时用mpstat -P ALL盯着软中断占比。4.3 那些救命的 TCP 参数一个表格说清楚把实验环境里的有效参数整理成表按客户端/服务端两个角度分别说明。这不是让你照抄而是帮助你理解每个参数到底影响哪一段收发包路径。参数作用我的设置说明net.ipv4.tcp_max_syn_backlogSYN 半连接队列长度65535防止握手风暴时丢连接net.core.somaxconnaccept 全连接队列上限65535队列短了会出现连接 resetnet.ipv4.ip_local_port_range客户端本地端口范围1024-65535压测客户端不够用时会报错net.ipv4.tcp_tw_reuse允许 TIME_WAIT 复用1注意仅在客户端场景适用net.ipv4.tcp_fin_timeoutFIN_WAIT_2 超时30缩短无效连接回收时间net.core.rmem_max/wmem_max收发缓冲区上限16MB提高大包吞吐能力net.ipv4.tcp_rmem/tcp_wmemTCP 缓冲动态范围4096 87380 16777216内核会根据压力动态调整坦白说tcp_tw_reuse是争议比较大的参数。官方文档说它只能在客户端场景安全使用服务端开启可能引发连接异常。我的习惯是客户端压测机器上开服务端关掉。服务端真正要处理的是 TIME_WAIT 数量的可见性管理而不是掩盖它们——时间一长几十万条 TIME_WAIT 照样占着内核内存和端口表。5. 别让内核替你背锅用户态设计才是主战场5.1 内存分配与锁竞争一百万连接的地基工程内核尽力了但瓶颈往往在用户态代码。百万并发系统里锁竞争是最容易把自己坑死的地方。我见过一个版本用了全局锁保护连接表压测到五万连接时锁等待占比就超过 50%CPU 利用率直线上升吞吐量却纹丝不动。正确的思路是避免共享。连接表按 CPU 分片每个核维护自己负责的连接子集访问时无锁需要全局操作时用原子变量或者 RCU 风格的读写锁。Go 的 goroutine 也是这个思路——不是因为它魔法是因为它把锁竞争和调度成本下放给了运行时让程序员默认就走“每 goroutine 独立”的路线。内存分配是另一个容易被忽视的坑。百万连接意味着每次事件处理都可能分配和释放对象如果依赖默认内存分配器堆碎片化和分配器锁竞争很快会把 CPU 吃干净。解决方案是用对象池、slab 式缓冲区管理并尽量复用连接对象。我在项目里用了一个自研的连接池上线后 GC 停顿时间缩小了一个数量级。5.2 定时器百万连接场景下被忽略的“时间负担”每个连接都要有心跳超时检查这意味着你需要管理上百万个定时器。如果每个连接一个timerfd或者用一个独立的定时线程逐个扫描性能和可扩展性都会出问题。传统方案是时间轮或最小堆把所有超时任务组织成高效的数据结构每次 tick 只处理到期的部分。内核 Linux 的 timer 实现其实就是一个例子——它用基于红黑树的管理结构维护海量内核定时器时间复杂度是 O(log n)。用户态实现同样可以用小根堆维护下一个最早超时时间配合一个低频的 epoll_wait 超时参数例如 50ms 的epoll_wait超时在每次事件循环中检查堆顶是否到期。这样即使在百万连接下每秒也只需要几十次堆操作。更省 CPU 的做法是把超时检查放到需要时才做即“惰性检查”某个连接有数据活动时才判断它上一次活动时间是否超时。这个思路依赖epoll_wait返回事件驱动的特性活跃连接少时几乎零开销但需要保证“一直空闲”的连接能被一个低频全局扫描兜底。这个兜底扫描频率设 5-10 秒一次即可CPU 开销忽略不计。5.3 容量规划算清楚账再动手省得线上“踩雷”把内存、CPU、带宽、端口、文件描述符这五个维度各自算一遍是开局的基本功。我这里给你一套比较可靠的经验公式单连接内核态开销按 3KB 估算用户态结构体按 512B 估算连接建立后的缓冲区按实际流量配置心跳型连接可以调小内核缓冲区能省下可观内存。CPU 的预算要看事件模型和活跃比。实测数据一台 8 核机器每秒处理十万次“读-写”事件约占满 40% CPU百万连接但活跃比 1% 时事件循环本身的 CPU 开销几乎可以忽略。所以你要做的第一件事不是优化代码而是弄清楚自己的活跃比和报文模型。带宽也要算。假如每条连接每 30 秒发 100 字节心跳100 万条连接每小时就是 12GB 流量按 24 小时算接近 288GB峰值带宽需求可能达到几十 Mb/s 量级。这个量级家用级服务器带宽够呛必须提前规划带宽上限不然某个业务高峰网卡先被撑爆。6. 就一百万了然后呢容器、内核裁剪与可靠性瓶颈6.1 容器环境下的百万并发内核虚拟化带来的约束这节说点实操之外的话题。如果你把服务跑在容器里千万要注意内核虚拟化带来的隐藏约束。容器共享宿主机内核fs.file-max是宿主机的全局上限而容器的ulimit默认又受 runc 控制。你改/proc/sys/fs/file-max改了宿主机容器重启后配置可能被重置这是大部分容器网关联调失败的直接原因。更麻烦的是 namespace 和 cgroup 对资源的限制。cgroup 的 pids 和 memory 限制若设置得过小容器内连接数超过阈值进程可能直接卡死或者触发 oom-kill。你调优了半天用户态参数结果被 cgroup 一刀切掉这种感觉真的很崩溃。所以容器环境下的调优思路不一样先确认 cgroup 限制再设置容器内部的进程级参数。遇到问题先cat /sys/fs/cgroup/pids/max和memory.max看一眼而不是一上来就改内核参数。顺带说一句宿主机上多容器共享同一个内核劣化应用能把整个宿主机网络栈拖垮最好用 cgroup 的net_cls或者网卡 QoS 给每个容器限流。6.2 内核裁剪的“八股”和虚拟化到底是不是有用的内核裁剪这件事在网络上被说得很玄乎。我的观点很简单开发阶段别碰裁剪。你需要的不是裁剪过的内核而是一套能跑、能调试、能恢复到出厂状态的发行版内核。裁剪带来的收益是启动时间和内存占用减少但对一个已经跑起来的大型服务来说这些都是边际收益而风险是巨大的——裁掉一个你以为是冗余模块可能恰好是某个硬件驱动依赖的基础组件。更关键的是很多云服务器你根本没有裁剪权限。你以为自己在调优内核参数实际上只能在用户态配置sysctl真正的内核编译选项连碰都碰不到。所以除非你是嵌入式环境内存只有几十 MB完全不建议把时间花在内核裁剪上。省下这些时间把用户态的事件循环和内存管理写漂亮收益高得多。虚拟机场景则另有说法。如果你的服务跑在 KVM 或 VMware 上百万并发意味着虚拟机的中断控制器如 vCPU 的 APIC和虚拟网卡驱动处理高吞吐时会成为瓶颈。遇到网络慢、CPU 软中断高的情况优先检查虚拟网卡的队列数ethtool -l 网卡必要时开启多队列 vhost-net。这和物理机调优没有本质区别只是多了一层虚拟化层要排查。6.3 百万并发是手段可服务的可靠性才是目的实话实说用户不会因为你“百万连接”就给你鼓掌他们在意的是“推送消息能不能准时到达”“设备断线重连是否丝滑”。百万连接只是技术指标的副产品。这提醒我们压测通过不意味着线上扛得住因为压测流量是均匀的而线上流量是有突发和尖峰的。稳定性要做到什么程度我的经验标准连接数达到设计值后CPU 峰值不超过 70%内存占用不超过总内存 80%另有 20% 的缓冲余量应对突发。如果压测一看 CPU 已经 95%说明设计冗余不足要继续优化而不是上线。另外百万连接重建对系统的冲击极大必须验证批量断线重连场景否则一次网络抖动就能引发雪崩式重连风暴。这个项目带给我的最大改变是意识到“压测通过”和“线上可用”是两回事。线上环境的网络状况、设备型号、时间周期都比 压测复杂得多。所以我会建议任何做高并发系统的人压测时除了看平均吞吐量还要看最坏延迟和长尾延迟这两项才是用户真正感知的指标。7. 常见问题与排查技巧实录7.1 连接数到三万就上不去先查文件描述符上限现象客户端不停地connect失败服务端的epoll_wait在增长到一定量级后停滞。排查步骤先执行cat /proc/sys/fs/file-nr看系统总分配fd数量是不是撞到了file-max再执行ulimit -n看进程 nnoflimit。两个都改大之后重启进程问题通常就解决了。但注意一个隐蔽情况除了 nofile服务进程如果用了 systemd 管理LimitNOFILE 也需要改。systemd 会覆盖你用户的 ulimit 设置这坑过无数人。改完 systemd 配置记得systemctl daemon-reload然后重启服务。7.2epoll_wait返回很快但吞吐量不涨是卡在锁上了吗这类问题排查有个标准动作perf top看热点函数在哪里。如果是spin_lock或mutex_lock相关热点恭喜你锁竞争实锤了。解决方案已经在前文说过按 CPU 分片、无锁化、减少跨线程通信。如果是syscall相关热点那就是系统调用本身太频繁——考虑批量处理事件减少epoll_wait调用次数。还有一种常见情况用户态代码在事件处理里做了耗时操作比如 DNS 解析、数据库查询把事件循环线程卡住了。排查方法是给事件处理打上分段日志量化每部分耗时。之前我们有个版本上线后吞吐暴跌定位后是某个日志库在并发写大日志时触发了文件锁事件线程全堵在那了。7.3 系统 CPU 整体不高但软中断占用 100%打开mpstat -P ALL发现某个 CPU 的软中断占比长期 100%其他核闲得聊天。这就是前面说过的收包路径集中问题。先看网卡队列数单队列网卡没法靠 RPS 分散再用/proc/irq/中断号/smp_affinity把网卡中断绑到多核上。多队列网卡配合 RFS 效果更佳确认rps_flow_cnt设置合理。还有一招容易被忽略如果连接都是长连接把网卡的 LRO/GRO 打开让大包先在内核合并再进协议栈能显著降低软中断频率。我在实测中开启 GRO 后软中断 CPU 占用下降了约三分之一。7.4 连接积压大量 TIME_WAIT怎么处理服务端和客户端都有可能出现 TIME_WAIT 堆积。客户端场景可以开启tcp_tw_reuse把可用于新连接的 TIME_WAIT 端口复用好服务端场景要区分两种来源一是主动发起关闭的一端会进入 TIME_WAIT二是被动关闭的一端如果后续没有数据活动可能在 FIN_WAIT_2 状态待很久。如果你看过ss -s的输出大量 TIME_WAIT 并不一定意味着问题。内核可以轻松管理几十万条 TIME_WAIT真正的问题在于它占据的四元组不能用于新连接尤其客户端本地端口紧张时就会触发Cannot assign requested address。方案的唯一朴素解扩大端口范围或让客户端主动复用长连接避免频繁断连。7.5 百万连接压测时压测工具成了瓶颈最后说句扎心的话你千辛万苦把服务端调到百万并发一压测压测客户端先崩了。压测客户端要维护百万并发连接同样要面对文件描述符上限、内存占用、事件循环效率问题。你在服务端踩过的坑在客户端要原样再踩一遍。我的压测方案是用多台机器做分布式压测每台压 20 万连接再用一个聚合器汇总结果。而不是在单台机器上头铁地硬干。另一种思路是模拟海量“哑连接”只建连不发数据检验系统的连接容纳能力再单独准备少量“真实流量”连接验证业务逻辑。7.6 排查速查表现象可能原因排查命令解决方案连接数上不去文件描述符不足cat /proc/sys/fs/file-nr、ulimit -n调大 nofile 与 file-max连接被 resetaccept 队列满ss -lntnetstat -s调大 somaxconn 和 backlogCPU 软中断高收包集中单核mpstat -P ALL开 RPS/RFS绑定中断吞吐量低锁竞争perf top分片无锁化、减少共享大量 TIME_WAIT短连接频繁ss -tan state time-wait调大端口范围、长连接复用模板失败cgroup 受限cat /sys/fs/cgroup/memory.max调整 cgroup 限制8. 内核学习的最佳路径从“调参侠”到“理解者”技术圈有个热词叫“内核裁剪八股”讲的是有些开发者背了一堆内核参数、内核编译选项却不知道参数背后在做什么。我在做这个项目的过程中慢慢意识到真正有用的不是背参数而是理解内核的问题域它在内存管理、进程调度、文件系统、网络栈各个子系统中解决的永远是资源稀缺、并发冲突、延迟与吞吐的权衡。给你一条实用的学习路线。第一步掌握进程与线程的底层差异clone系统调用的 flags、线程组、vfork理解 CFS 调度器的工作方式。第二步学会看/proc和/sys下的文件它们就是内核对你“汇报工作”的窗口。第三步选一个子系统深入比如网络栈从socket到tcp_v4_rcv弄清楚一个报文怎么从网卡走进用户态。第四步带着问题去读内核源码不要从头到尾啃而是“用到哪读到哪”。学内核不需要数学基础需要的是“顺着代码追问题”的耐心。我记得刚查 half connection 队列问题时为了搞明白 SYN 重传机制把tcp_v4_conn_request和tcp_conn_request翻来覆去读了一整天。那是我第一次觉得“原来内核源码没有想象中那么可怕”。很多人推荐先读《深入理解Linux内核》但我个人建议先读《Linux内核设计与实现》这本薄书建立整体框架之后再按需要重读第二遍。复杂的概念先从整体结构上理解它解决什么问题再去抠细节效率高得多。内核之美不在背多少参数而在于你终于能看懂它为什么这样设计从而在自己的系统里做出正确的取舍。回到百万并发这个话题本身。它像一个放大镜把你对操作系统每一层模糊的理解全部放大成刺眼的 bug。调完ulimit还有软中断调完软中断还有锁竞争调完锁竞争还有内存分配器。这个过程很折磨但它逼着我把 Linux 内核从黑盒变成了灰盒再变成了白盒。谁能想到一个“百万并发”的目标最终让我收获最大的是读懂了内核的调度和网络路径。如果你也在做类似的系统我的建议很简单先把 epoll、eventfd、网络收包路径这三大块吃透再动手调参。参数只是验证理解的手段理解才是解决百万并发问题的真正钥匙。