8.2 连接风暴与资源耗尽:应急预案


8.2 连接风暴与资源耗尽:应急预案

本节摘要:突发连接风暴、恶意扫描、慢连接占位是长连接服务的三大极限考验;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 的背压);业务线程池用有界队列加拒绝策略,而不是默认的无界排队——无界队列在洪峰下就是"延迟爆炸 + 内存爆炸"二合一。

三、三类资源耗尽的识别卡片

  • FD 耗尽:日志出现 Too many open files,新连接全部失败,老连接还能跑。对策:查 FD 去向(lsof -p 统计),常见根因是连接泄漏——只 close 逻辑连接没 close 物理通道,或超时连接没被心跳踢掉。
  • 内存耗尽:堆外或堆曲线爬坡后 OOM。对策:按 4.3 的泄漏三板斧排查;同时检查写缓冲水位是否把慢客户端的数据都囤在了内存。
  • 线程耗尽:全部 worker 都 BLOCKED/WAITING,新事件无人处理,队列积压飙升(7.3 的前兆指标在这里兑现)。对策:jstack 或 Arthas 定位卡点,通常是某个同步调用(锁、慢 SQL)被搬进了 EventLoop。

⚠️ 常见坑:限流器的配额归还忘了挂在 closeFuture 上,只扣不加——服务跑几天后"假满",新连接全被误拒。计数器类闸门必须有归还路径,且归还与扣减要成对测试。

💡 关键直觉:应急预案的本质是"提前写好的取舍清单"。崩溃边缘没有时间讨论保谁,清单必须平时就写进代码:老连接优先于新连接、核心链路优先于旁路功能、拒绝要快且便宜。

四、闸门之外:纵深防御与演练

应用层闸门挡得住"量",挡不住"针对性攻击"。SYN 洪水这类半连接攻击要靠内核与网络层:开启 SYN cookie(net.ipv4.tcp_syncookies=1)让握手不占队列资源,更前置的清洗交给机房的高防与防火墙——应用层永远不该独自面对原始攻击流量,那不是它的工位。

最后一步是演练:没被真实流量检验过的预案只是文档。低峰期做一次"连接风暴演习"——用 7.2 的负载端模拟突发新建连接,观察四道闸门是否按设计依次生效、拒绝计数是否如实上报、老连接是否纹丝不动。每季度一次、每次半小时,事故来临时这份肌肉记忆就是全部差别。

应急预案速记

  • 四道闸门:内核握手队列、FD 上限、应用限流、内存线程资源,各挡一种洪峰。
  • 越前越便宜:内核拒握手近零成本,应用收下再踢已是重活,闸门前置。
  • 双层限流:全局上限保容量、单 IP 上限防单点恶意,配额归还挂 closeFuture。
  • 有界原则:线程池队列、写缓冲全部有界,无界=延迟与内存双爆。
  • 三类耗尽卡片:FD 看 lsof、内存看曲线与泄漏检测、线程看 jstack 与积压指标。
  • 取舍清单前置:老连接 > 合法新连接 > 未认证连接,平时写进代码而不是事时拍脑袋。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U