谁都知道,真正的千万级在线场景从来不是上线那天才来的——是在机房报障群里来的。项目上线前,某团队负责的推送网关要扛住一批规模不小的终端并发连接,当时压测目标直接定到了百万级WebSocket长连接。这个数字一开始听起来挺吓人,但真正动手做下来之后,收获最大的反而是整个压测体系的设计思路,而不只是那个“百万”数字本身。
这篇文章就把整个压测架构的完整设计过程、环境参数、踩过的坑和验证方法完整记录下来,给后面要做类似长连接压测的同行当一个可参考的底稿。这里不会只给结论,会把每个决策背后的计算过程、每个参数为什么这么调、每个问题是怎么定位的都讲清楚,适合正在自研压测平台、或者准备对网关类服务做大规模长连接压测的架构师和运维同学。
1. 压测目标与总体架构设计思路
1.1 压测目标是怎么定出来的
很多人一上来就问“百万级连接需要多少台机器”,这是个好问题,但顺序反了。第一步应该先想清楚:为什么是百万,而不是五十万,也不是三百万。
当时我们的推算逻辑是这样的:线上真实终端设备数约80万台,峰值活跃率按70%估算,就是56万左右的长连接。再考虑未来半年设备增量、以及突发活动带来的集中唤醒场景,把压测目标定为100万连接,相当于在线峰值的1.7倍左右,留出了足够的冗余空间。这个数字不是拍脑袋拍的,而是从业务DAU、设备在线率、单用户连接数这几个可量化的指标倒推出来的。
- 先统计真实的业务指标:设备总量、日活、平均在线时长。
- 估算单用户连接数:通常一个客户端会建立1到2条长连接(主备链路或业务隔离)。
- 设置冗余系数:核心链路建议1.5到2倍冗余,非核心链路1.2倍就够了。
1.2 总体压测架构的组成
百万级WebSocket压测不是一台机器就能完成的,它本质上是一个小型的分布式压测系统。整个架构分成三块:
- 压测客户端集群:负责建立连接、维持连接、发送业务消息、采集客户端视角的指标。
- 被测服务端集群:这里被测的是某推送网关系统,也就是真实环境中要承载百万连接的服务端。
- 监控与数据采集端:负责采集服务端视角的连接数、CPU、内存、网络、文件描述符等指标,同时汇总压测客户端的报告数据。
三者的关系是:压测客户端模拟海量终端设备,向网关发起WebSocket连接,连接建立完成后保持在线,周期性地发送心跳和少量业务消息;网关侧把连接调度到后端逻辑节点上;监控端则在压测过程中持续采样两边的指标。
这里要特别强调,压测架构规划中最容易忽略的是监控端。很多团队做压测就是起两个脚本拼命建连接,最后只知道“连上了”还是“没连上”,连服务端CPU是不是已经跑满了、文件描述符是不是耗尽都不知道。这种压测结果没有任何参考价值。所以我们这次从压测最开始就搭了一套独立的监控面板,把客户端和服务端的核心指标全部实时汇总到一个看板上,压测过程中每一个拐点都能对上时间线。
1.3 为什么不用现成的压测工具
团队里一开始也讨论过要不要直接用现成的开源压测工具,毕竟省事。调研了一圈之后放弃了,原因是:
- 大部分开源压测工具面向HTTP短连接的场景设计,对WebSocket长连接的支持要么是后加的扩展,要么根本不适合百万并发级别。
- 长连接压测真正的难点在现场调度:如何分布式协调各压测机、如何控制建连速率、如何长时间保持连接状态、如何精确统计在线连接数。这些现成工具很少能直接覆盖。
- 被测的是私有协议,现成工具往往需要在脚本层二次开发,调试成本不低。
所以最终决定自研一个轻量级压测客户端,只做一件事:最大效率地建立和维持海量WebSocket连接,同时把建连结果和消息延迟准确上报。
这个决策在后续执行中被验证是对的。压测过程中我们遇到了很多非常“偏门”的问题,比如单机源端口耗尽、连接建立速率过快导致服务端accept队列溢出、本地连接表过大导致软中断严重等。这些问题如果用现成工具,光定位是哪一层的瓶颈就要花掉半天时间,自研工具则可以随时按需增加探针和指标。
2. 基础设施准备与操作系统级调优
2.1 硬件配置怎么选
做百万级连接压测之前,先要算清楚资源账。以WebSocket长连接为例,每条连接在服务端占据的资源大概包括:
- 一个文件描述符:内核里对应一个struct file实例,加上socket相关的数据结构,总计约3-5KB内核内存。
- 读写缓冲区:默认的socket收发缓冲区通常会按16KB到64KB分配,但如果实际业务流量很小,可以通过setsockopt调小。
- 业务层连接对象:服务端每个连接通常在堆上保存上下文对象,包含状态、远端地址、加密上下文等,大约占用2-8KB。
- 事件循环数据结构:每个连接在事件驱动框架中注册的句柄和回调,大约1-2KB。
粗略估算,服务端维持一条空闲长连接的开销在8-20KB之间。100万连接意味着至少10GB级别的内存占用,这还只是保守估计。所以被测服务端的机器配置定位为多台16核32GB内存的云主机,压测客户端则因为要维护更多的socket状态,内存消耗反而更大。
压测机上的内存估算容易被忽略。我打个比方:服务端连接是被动的,来了连接就登记,而压测客户端需要主动管理每条连接的状态机、维护待发送的队列、记录每条连接的消息延迟,这些业务层状态每条可能要占到几十KB。所以压测客户端的内存往往是服务端的2到3倍。我们压测机上跑着的实例内存占用普遍到20GB以上,这一点谁做谁有体会。
2.2 文件描述符上限调整
所有Linux环境默认的进程级文件描述符上限是1024,这是给常规应用用的。对于要承载百万连接的进程,这个值必须调整。
调整分为三个层面:
- 系统全局限制
/proc/sys/fs/file-max:这个值一般默认较大,但压测前还是要确认一下,建议设置到3000000以上。 - 用户级限制
/etc/security/limits.conf:需要同时修改软限制和硬限制,这两项容易踩坑。只改软限制不报错,但进程运行中一旦超过软限制就会被杀掉。建议直接设置成相同的值。
* soft nofile 1048576 * hard nofile 1048576- systemd unit 文件中如果有
LimitNOFILE字段也要同步改,否则limits.conf配置会被覆盖。
这个坑我们真实踩过:最开始只改了limits.conf,用普通用户拉起服务端进程,连接数刚到几万就被系统杀掉,排查了大半天才发现是systemd服务文件里默认的LimitNOFILE=1024把进程限制住了。所以现在凡是部署服务端或者压测客户端,一律把服务配置文件的nofile也显式改掉。
2.3 必调的内核网络参数
百万级长连接场景下,默认内核参数几乎全部不适用。下面这几项是我们实际调过的,每一项都经过线上对比确认:
| 参数 | 默认值 | 压测建议值 | 作用 |
|---|---|---|---|
| net.core.somaxconn | 128 | 65536 | 全连接队列长度,影响accept吞吐 |
| net.ipv4.ip_local_port_range | 32768-60999 | 1024-65535 | 本地端口范围,影响压测机建连能力 |
| net.ipv4.tcp_max_syn_backlog | 128 | 65536 | 半连接队列长度 |
| net.ipv4.tcp_tw_reuse | 0 | 1 | 允许TIME_WAIT连接复用 |
| net.ipv4.tcp_max_tw_buckets | 180000 | 2000000 | TIME_WAIT连接上限 |
| net.ipv4.tcp_keepalive_time | 7200 | 600 | 服务端KeepAlive探测周期 |
| net.core.netdev_max_backlog | 1000 | 300000 | 网卡接收队列长度 |
| net.ipv4.tcp_rmem / tcp_wmem | 默认 | 调小或按需 | 控制每连接缓冲区内存占用 |
很多人会问tcp_tw_reuse不是不建议开启吗?这里有些争议。在我的经验里,如果是主动发起连接的一方(压测客户端),开启tw_reuse对端口复用有明显帮助,能有效延后源端口耗尽的发生时间。而服务端因为不主动发起连接,这个参数基本不影响。要注意的是,tw_reuse能不能生效还依赖时间戳选项,两台机器之间如果时间戳不一致,可能有副作用。压力机上对这个参数关闭,靠多IP和分布式拆分摊端口压力。
2.4 连接内存与TCP缓冲区调优
百万条连接最容易被忽略的内存大头是TCP socket缓冲区。Linux默认的读缓冲区可以增长到6MB,写缓冲区也能到6MB左右,但这是“按需分配”的,不代表每条连接就占这么多。问题在于高并发压测场景下,内核的自动调优机制可能让每条连接的实际缓冲区涨到不合理的程度,100万乘下来就是灾难。
所以压测前建议通过sysctl把收发缓冲区的上限调小:
net.ipv4.tcp_rmem = 4096 8192 262144 net.ipv4.tcp_wmem = 4096 8192 262144 net.core.rmem_max = 262144 net.core.wmem_max = 262144这三个数字分别表示最小值、默认值和最大值,单位是字节。把最大值限制到256KB,对于心跳为主的长连接场景已经足够宽裕。这么做之后,整体内存占用能下降30%到40%。
另外一个隐藏的内存消耗是连接跟踪表。Linux的nf_conntrack模块会为每个TCP连接建立一条跟踪记录,默认的哈希表大小可能不够支持百万级连接。如果压测场景不需要防火墙/NAT功能,建议直接关掉这个模块,或者把表的最大值调大:
net.netfilter.nf_conntrack_max = 2000000 net.netfilter.nf_conntrack_buckets = 524288关不掉的情况下一定要调大,否则连接数到了一定级别之后,内核会疯狂打印“nf_conntrack: table full, dropping packet”的日志,新连接开始随机丢包,压测数据会变得非常难看。
注意:关闭nf_conntrack会影响NAT和防火墙规则,只适用于纯内网压测环境。生产环境的机器不要随便关。
3. 压测客户端设计:从1万到100万连接的关键跨越
3.1 压测客户端的整体结构
自研压测客户端选择Go语言实现,核心原因有两个:goroutine的并发模型天然适合“每个连接一个协程”的编程模式,代码写起来非常直观;另外Go的net库在事件驱动方面封装得很好,底层的epoll由运行时统一管理,省去了手工处理大规模I/O的复杂度。
压测客户端的结构可以简化为三层:
- 连接调度层:负责维护待建连的队列,按指定的速率发起连接。
- 连接会话层:每条WebSocket连接对应一个goroutine,负责心跳发送、消息接收、数据统计。
- 指标汇总层:定时把各连接的延迟、成功率、状态上报到监控系统。
每个压测实例启动时指定三个参数:本机要建立的连接总数、建连速率上限(每秒多少条)、服务端地址列表。比如单机分配10万连接,建连速率控制在每秒5000条,那么完成一轮建连需要20秒。
3.2 单机源端口限制的破解
做长连接压测时第一个绕不开的限制就是源端口数量。一个TCP连接由四元组唯一确定:源IP、源端口、目标IP、目标端口。如果压测机只有一个IP,它能建立的连接数上限就是约65535个,还要减去系统占用的端口,实际可用也就六万左右。
要突破这个限制,方法有几个:
- 给压测机配置多个IP地址。一个网卡可以绑定多个IP,每个IP都能提供6万多个源端口。配置四到五个IP,单机能力就上到30万连接了。
- 如果是物理机,可以直接用多网卡多IP组合。
- 修改
ip_local_port_range扩大小端口范围,但上限仍然受限于65535。
我们的做法是给每台压测机绑定5个内网IP,单机压测能力配置在20万到25万连接。目标100万连接只需要四五台压测机,这在机房环境里完全可控。
实际操作给网卡绑IP的命令类似这样:
ip addr add 192.168.1.21/24 dev eth0 ip addr add 192.168.1.22/24 dev eth0 ip addr add 192.168.1.23/24 dev eth0在Go代码里,可以通过Dialer的LocalAddr字段显式指定从哪个IP发起连接,多IP轮询分配。
3.3 建连速率控制与浪涌问题
百万级连接如果一股脑在几十秒内全部建立,服务端基本会直接崩掉。问题是新连接建立涉及TCP三次握手、WebSocket握手、服务端的连接初始化和内存分配,瞬时压力非常集中。
所以压测客户端必须支持建连速率控制。实现上其实就是做一个令牌桶:每秒钟允许建立N条连接,多余的请求排队等待。我们的压测策略是分梯度加载:
- 第一阶段:每秒5000条,持续50秒,完成25万连接。
- 第二阶段:观察服务端的CPU、内存、负载情况,稳定后再提速。
- 第三阶段:每秒8000到10000条,逐步爬到100万。
这种渐变加载的好处非常明显:一旦某个环节出现瓶颈,可以在小规模时就能发现,不至于在临近百万时整条链路雪崩导致无法定位问题。
3.4 连接建立后的保活与状态管理
长连接压测不是连接建立完就完事了,还要模拟真实的客户端行为。我们的压测客户端建立连接后,会做下面这些事情:
- 周期性发送心跳包:每隔90秒发一次WebSocket Ping帧,服务端返回Pong。
- 随机发送业务消息:按预设比例模拟真实业务的指令下发和上行数据。
- 记录消息往返延迟:收到服务端主动推送的消息时,记录消息ID和时间差。
这三个动作分别对应真实场景中的保活、业务通信、端到端延迟三大类核心指标。心跳能真实反映连接的健康状态,因为如果一个连接是半开状态(一端已断开但另一端不知道),心跳就能暴露出来。随机业务消息则能验证服务端不是只“挂住”连接不做事,而是在真实地转发业务流量。
4. 服务端压测实战与瓶颈定位
4.1 服务端架构与承载模型
被测的推送网关服务的核心是一个基于事件循环和异步I/O的接入层,客户端连接通过负载均衡器分发到多台网关节点上,每台节点负责维护一部分长连接。
长连接服务端和普通HTTP服务最大的不同在于:没有“一个请求一个响应”的天然循环约束。每条连接建立后,如果没有事件发生,它就是纯粹的闲置状态。但内核要持续维护这条连接,应用层的事件循环也要保存连接上下文。所以服务端最关键的指标不是QPS,而是:
- 当前活跃连接数
- 每连接的内存开销
- 事件循环的调度延迟
- CPU软中断占比
- 文件描述符使用率
这些指标在压测过程中我们全部做了实时采集。
4.2 压测过程中观察到的三个典型瓶颈
整个压测过程中,我们碰到了三个非常典型的瓶颈,这里逐一拆开讲。
第一个是accept队列溢出。当建连速率突然拉高时,服务端应用层来不及从accept队列里取出新连接,内核的全连接队列就满了,新连接被直接丢弃。现象是客户端显示大量TCP连接失败,错误码为超时或connection reset,而服务端的CPU和内存看起来都还健康。这个问题的本质是事件循环线程数不够,或者每个连接初始化的耗时太长。解决方法是增加事件循环线程数,同时把初始化逻辑中不必要的同步操作改成异步。
第二个是软中断集中在单个CPU上。高连接数场景中,网卡收包触发的软中断如果全部集中到某一个CPU核心上,会导致那个核心跑满100%,而其他核心处于空闲状态。这就是典型的RPS(Receive Packet Steering)没有开启。通过开启RPS并把哈希表设置到多队列,可以把软中断分散到多个核心上,整体吞吐立刻提升。
开启RPS的参考写法是在每个网卡队列的rps_cpus文件里写CPU掩码,比如:
echo "ff" > /sys/class/net/eth0/queues/rx-0/rps_cpus echo "ff" > /sys/class/net/eth0/queues/rx-1/rps_cpus这里“ff”表示前8个CPU核心参与分担软中断。如果有更多核心,按需要把掩码扩展到位。
第三个是TCP连接出现大量半开状态。压测过程中发现服务端记录的连接数和客户端报告的成功连接数之间有偏差,查下来发现是部分客户端IP的网络路径上有中间设备把空闲连接清理掉了,但两端都不知道。这种问题不能只靠调整KeepAlive参数来解决,需要从压测网络环境的稳定性入手,尽量选择二层互通的机房网络。
4.3 服务端连接数从哪一秒开始飙升
这里分享一个观察经验。压测刚开始的前十秒,服务端的连接数增长非常平稳,几乎和客户端报告的建立速率一致。但到十几万连接之后,增长曲线开始出现锯齿状波动,一会上去了,一会又掉下来一点。刚开始以为是调度问题,后来抓包才发现是负载均衡层在连接数超过阈值后主动断开了一些空闲连接,导致连接数一直上不去。
定位到这个问题后,把负载均衡与网关之间的空闲连接超时时间同步调大,并修改了网关侧对连接断开的处理逻辑——断开后不立即重连,而是按指数退避策略等待。很快锯齿就消失了,连接数顺畅地冲过了50万。
这个经验说明:压测时不光要看最终数据,更要观察中间过程的曲线形态。任何异常的抖动,背后都隐藏着一个值得追查的机制问题。
5. 结果验证、数据采集与常见问题速查
5.1 百万连接是否“真连接”的验证方法
压测结束后,连接数报表显示“100万”的时候,先别急着庆祝。我这里说的验证方法是多视角交叉确认,不要只信压测客户端的一面之词。
- 客户端视角:统计成功建立且心跳正常的连接数。
- 服务端视角:通过ss命令统计服务端处于ESTABLISHED状态的连接数,或者直接读应用的连接数指标。
- 网络设备视角:在交换机或负载均衡器上查看并发连接数统计。
三个视角的数据如果基本吻合,才能认定百万连接是真实存在的。如果客户端说100万、服务端只有80万,那其中一定有连接没有建立成功或者已经断开,需要重新核对。
某个环节我们当时就发现客户端统计到100万成功连接,但网关侧通过ss统计只有96万左右,差了4万。排查后发现,压测客户端里有一部分连接因心跳超时被判为失效,后台自动重连补满,网关侧则把超时未心跳的连接清掉了,所以两边口径不一致。后来统一了统计口径,以服务端ss输出为基准,客户端报告作为参考,数据就对上了。
5.2 需要采集的核心压测指标
压测过程中的数据采集是分层的,每一层的重点不一样。
- 系统层:CPU使用率、内存占用、负载、软中断占比、网络吞吐、连接表大小、文件描述符数量。
- 中间件层:负载均衡的后端连接数、健康检查结果、转发延迟。
- 应用层:服务端当前连接数、建立连接速率、心跳成功率、消息推送成功率、端到端延迟百分位。
- 客户端层:成功连接数、失败连接数、重连次数、消息延迟P99。
这些指标全部打点记录到时序数据库里,压测结束后导出图表,用于后续的分析和报告输出。没有时序打点的压测,等于只看了一部没有进度条的电影,回头只能靠记忆补剧情。
5.3 常见问题速查表
这里把压测过程中遇到的典型问题整理成一张表格,后续谁再遇到可以直接对照排查。
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 连接数爬到几万就崩溃 | 进程fd上限未调 | 检查ulimit和systemd配置 |
| 建连速率上不去,大量超时 | accept队列溢出 | 调大somaxconn,检查事件循环处理速度 |
| 客户端报本地端口不足 | 单IP源端口耗尽 | 压测机绑定多IP |
| 服务端CPU不高但连接建立慢 | 软中断集中在单核 | 开启RPS |
| 连接数曲线反复锯齿 | 中间设备清理空闲连接 | 调整KeepAlive和设备空闲超时 |
| 内存占用增长异常 | socket缓冲区自动调优 | 调小tcp_rmem和tcp_wmem上限 |
| 连接全部建立后CPU高居不下 | 心跳处理逻辑低效 | 检查心跳是否触发不必要的业务逻辑 |
| 时间戳:服务端连接数和客户端不一致 | 半开连接 | 用ss核对,统一统计口径 |
5.4 压测过程中总结的实操技巧
最后分享几个压测之后沉淀下来的经验,都是文档里查不到但实战特别有用的东西。
- 压测客户端和服务端的系统时间一定要用NTP同步精确,否则消息延迟的P99数据完全失真,连“哪一端慢”都说不清。
- 一轮压测做完不要立刻清场,保持连接继续跑15到30分钟,让长时间在线状态暴露隐藏的内存泄漏和句柄泄漏问题。我们就在这个阶段抓到过一个连接对象未释放的问题,连续增长了两小时才开始回落。
- 做百万级压测最好准备一套“降级预案”,比如压测过程中如果服务端负载异常升高,可以立即让客户端按比例退出,把连接数压回去。没这个预案,真发现问题时只能眼巴巴看着系统被打死。
- 压测过程中保留实时抓包的权限和工具,遇到协议层问题可以立刻定位,不用事后补抓。
- 所有压测参数(建连速率、连接总数、IP配置、心跳间隔)都要在启动前落盘记录,压测结束后回看数据时才能准确还原现场。没有参数的压测数据就是一堆没有单位的数字,解释权完全靠猜。
这套压测架构做完之后,后续又复用了很多次。不管是新版本网关的回归测试,还是对连接上限的再评估,都直接拿这套压测客户端和监控看板跑一轮,效率比第一次搭的时候高了很多。压测体系这东西,第一次搭起来最费劲,但一旦跑顺了,就成了整个链路可靠性建设里最值钱的一笔投入。