63,000台设备同时在线时,系统突然拒绝生长。CPU只用了15%,内存还剩56GB,新连接却像撞上一堵透明墙——连接、拒绝、重试、再拒绝。
作者花了三天时间怀疑人生。重写消息处理器,检查数据库连接池,排查负载均衡器,甚至开始质疑自己是否真懂计算机。最后发现凶手是个六位数:65536。
ulimit -n 65536——这个他自己半年前随手设置的参数,成了整件事的隐形天花板。
Linux把每个网络连接都当作文件描述符(file descriptor)来追踪。触顶之后,内核直接在握手阶段就拒绝新连接,连错误日志都懒得往应用层送。服务器觉得一切正常,因为故障发生在代码能看见之前。
「64K?绝对够用了」——Past Me的自信有多贵
这套系统跑的是工业物联网:仓库温度传感器、数据中心湿度追踪器、工厂设备监控仪。每台设备保持一个WebSocket连接,几周不发一言,偶尔丢个JSON过来汇报数据。
这和常规互联网服务完全相反。HTTP请求来了就走,数据库查询瞬间完成,短促爆发是常态。但「实时监测」卖的是存在感——客户要的是「永远在线」,不是「30秒后再来看看坏没坏」。
横向扩展的方案被摆上台面:100台服务器各扛5万连接,负载均衡一挂,完事。
作者算了笔账。单台AWS c6i.8xlarge月租约800美元,100台就是8000美元。就算估算偏差30%,也是10倍成本差距。
更隐蔽的账单在后头:每台服务器要跑监控代理、日志管道、DNS配置,TLS握手次数乘以100,负载均衡自己还得做冗余。客户端代码要处理100个不同端点,连接按什么分片?设备ID?地理位置?设备移动了怎么办?
优化应用代码是本能,但本能在这件事上撒谎了
作者做了开发者该做的事:优化应用层。重写处理器,JSON换MessagePack,调优根本不热的连接池。每个性能分析器都说「你没问题」,每张监控图都说「你没问题」。
唯一崩坏的是他对「问题在哪」的理解。
真相藏在操作系统层面。Linux内核为每个连接维护大量元数据:TCP控制块、socket缓冲区、定时器、文件描述符表项。当连接数逼近百万级,这些结构的内存占用和CPU遍历成本开始显性化。
作者最终把单机推到了500万并发。路径不是堆硬件,而是逐层拆解内核假设:
文件描述符限制只是第一道门。接下来是TCP内存预算、epoll实例的fd上限、进程可分配内存的软限制。每解开一个,系统就向前蠕动一截,直到撞上新的天花板。
某次调整tcp_mem参数后,连接数从80万跳到120万,内存占用反而下降——内核的自动调谐算法在特定负载下会过度保守,手动干预后回收了大量闲置缓冲区。
「正常」是操作系统写给批量短连接的情书
这套系统的诡异之处在于反向压力分布。大多数服务是计算或IO密集型,CPU或磁盘先挂。但百万级空闲连接吃的是「元数据密集型」——内核数据结构的空间和遍历时间。
作者打了个比方:传统优化是疏通高速公路,让车流更快通过。他的问题是停车场管理——车辆进场后不动,但系统得记住每辆车的位置、车主、预计离开时间,且随时能叫醒任何一辆。
Linux的默认配置为「快进快出」优化。TCP keepalive间隔、文件描述符回收策略、内存分页行为,全部假设连接生命周期以分钟计。当生命周期变成周,这些假设集体失效。
一个典型陷阱是TCP滑动窗口的自动调谐。空闲连接的广告窗口会收缩,唤醒时需要重新协商,大量设备同时上报时引发ACK风暴。作者关闭了自动调谐,固定窗口大小,CPU占用率从40%降到12%。
另一个隐蔽消耗是内核的定时器轮(timer wheel)。每个TCP连接挂多个定时器:重传、保活、延迟ACK。百万连接意味着百万级定时器扫描,作者改用noHZ模式,让CPU在空闲时真正休眠。
500万不是终点,是理解边界的起点
最终架构保留了单台核心服务器,前端用四层负载均衡做故障转移而非分片。成本从8000美元压到800美元加冗余实例的边际开销。
作者留下一组数字:500万连接时,系统内存约占用180GB,CPU峰值35%,平均12%。这些数字对特定硬件和内核版本有效,换个场景可能完全失效。
他反复强调的教训是:性能瓶颈的地理位置,往往和直觉相反。应用层优化是舒适区,但真正的天花板在操作系统与硬件的交界处——那些你「以为理解」的抽象层。
文末有个细节。作者提到某次凌晨调试时,发现连接数曲线出现周期性毛刺,每两小时一次。追查三小时后,发现是日志轮转脚本触发的fsync阻塞了事件循环。和65536的陷阱如出一辙:问题不在业务逻辑,在基础设施的副作用。
当你下次看到「CPU空闲、内存充足、系统就是不干活」时,会先从哪个数字开始查?
热门跟贴