本节摘要:突发连接风暴、恶意扫描、慢连接占位是长连接服务的三大极限考验;FD、内存、线程三类资源各有耗尽路径。本节给出从内核队列到应用限流的多道闸门配置,以及一道"在崩溃边缘先保谁"的应急决策流程。
一次十万级并发连接的推送时刻,请求按顺序撞上四道墙:内核握手队列(somaxconn 与 SO_BACKLOG)→ 文件描述符上限(ulimit)→ 应用连接数限流 → 内存与线程资源。四道墙任何一道先满,表现完全不同,排查时必须能对号入座:

# 系统层(重启生效需写进配置文件,容器则调容器运行时参数) ulimit -n 1000000 # 第二道:FD 上限,预估连接数加足余量 sysctl -w net.core.somaxconn=65535 # 第一道:与 SO_BACKLOG 取小者生效 sysctl -w net.ipv4.tcp_max_syn_backlog=65535 # 半连接队列,抗 SYN 洪水
// 应用层:第三道闸门——双层限流器 public class ConnectionThrottle extends ChannelInboundHandlerAdapter { private static final int MAX_TOTAL = 200_000; // 全局上限 private static final int MAX_PER_IP = 100; // 单 IP 上限 private static final AtomicInteger TOTAL = new AtomicInteger(); private static final ConcurrentHashMap<String, AtomicInteger> PER_IP = new ConcurrentHashMap<>(); @Override public void channelActive(ChannelHandlerContext ctx) { String ip = ((InetSocketAddress) ctx.channel().remoteAddress()) .getAddress().getHostAddress(); int now = TOTAL.incrementAndGet(); int ipNow = PER_IP.computeIfAbsent(ip, k -> new AtomicInteger()) .incrementAndGet(); if (now > MAX_TOTAL || ipNow > MAX_PER_IP) { reject(ctx); // 快速拒绝:回错误包后关闭 return; } ctx.channel().closeFuture().addListener(f -> { // 断开时归还配额 TOTAL.decrementAndGet(); PER_IP.get(ip).decrementAndGet(); }); ctx.fireChannelActive(); } private void reject(ChannelHandlerContext ctx) { ctx.writeAndFlush(ProtoErr.TOO_MANY_CONN.toPacket()) .addListener(ChannelFutureListener.CLOSE); } }
第四道闸门的核心是别让任何资源无界:每连接写缓冲配水位(7.3 的背压);业务线程池用有界队列加拒绝策略,而不是默认的无界排队——无界队列在洪峰下就是"延迟爆炸 + 内存爆炸"二合一。
Too many open files,新连接全部失败,老连接还能跑。对策:查 FD 去向(lsof -p 统计),常见根因是连接泄漏——只 close 逻辑连接没 close 物理通道,或超时连接没被心跳踢掉。⚠️ 常见坑:限流器的配额归还忘了挂在 closeFuture 上,只扣不加——服务跑几天后"假满",新连接全被误拒。计数器类闸门必须有归还路径,且归还与扣减要成对测试。
💡 关键直觉:应急预案的本质是"提前写好的取舍清单"。崩溃边缘没有时间讨论保谁,清单必须平时就写进代码:老连接优先于新连接、核心链路优先于旁路功能、拒绝要快且便宜。
应用层闸门挡得住"量",挡不住"针对性攻击"。SYN 洪水这类半连接攻击要靠内核与网络层:开启 SYN cookie(net.ipv4.tcp_syncookies=1)让握手不占队列资源,更前置的清洗交给机房的高防与防火墙——应用层永远不该独自面对原始攻击流量,那不是它的工位。
最后一步是演练:没被真实流量检验过的预案只是文档。低峰期做一次"连接风暴演习"——用 7.2 的负载端模拟突发新建连接,观察四道闸门是否按设计依次生效、拒绝计数是否如实上报、老连接是否纹丝不动。每季度一次、每次半小时,事故来临时这份肌肉记忆就是全部差别。