压测这件事,很多人第一次接触都是从 ab 或者 wrk 开始的。HTTP 层的东西好办,一个 URL 扔进去,QPS、延迟、成功率全有了。可一旦业务落到长连接、私有协议、游戏网关、IM 推送、行情推送这类场景,HTTP 压测工具就抓瞎了——要么不支持自定义报文,要么连接数上不去,要么压测机自己先被内存拖死。tcpkali 就是在这个缝隙里被很多人捡起来的工具。它是一个纯 C 写的事件驱动 TCP/WebSocket 负载生成器,编译出来就一个可执行文件,没有运行时依赖,单机几十万并发连接、上百万 QPS 的数据我在不同项目里都见过。
这篇内容把 tcpkali 从安装、参数、压测模型、系统调优到排障完整走一遍。适合三类人看:正在给长连接服务做容量评估的后端同学,手里有 WebSocket 网关要验证的测试同学,以及单纯想搞明白“一台普通服务器到底能顶多少连接”的运维同学。即使你之前只用过 JMeter 或 Locust,也能顺着往下读,关键的地方我都补了背景解释。
1. 先搞清楚 tcpkali 的定位:它能干什么,不能干什么
1.1 压测工具的三条技术路线
市面上的压测工具大致分三派。第一派是 HTTP 协议栈派,代表是 ab、wrk、k6,它们内置 HTTP 解析器,你给 URL 它拼报文,性能不错但协议耦合太深,想发个二进制自定义帧基本没戏。第二派是脚本驱动派,JMeter、Locust、Gatling 属于这一类,用 Java、Python、Scala 写压测逻辑,灵活度极高,代价是每个虚拟用户都要吃一份运行时开销,单机连接数通常卡在几千到一两万。第三派是裸 socket 派,socat 加脚本、iperf3、tcpkali 都在这里,它们不关心上层协议语义,只负责按规则把字节灌进 socket,所以资源占用极低,连接数能拉得很高。
选哪一派取决于你要测什么。你要验证某个 REST 接口的 P99,wrk 更省事;你要验证长连接网关在 50 万连接下会不会内存爆炸、心跳会不会大面积超时,那就得用第三派。tcpkali 的定位非常明确:它就是个“连接工厂 + 报文泵”,业务语义得你自己用参数拼出来。
1.2 tcpkali 的设计取舍
tcpkali 的底层是一套自己封装的 epoll/kqueue 事件循环,单线程处理大量 socket,多线程用--workers开。它没有引入 libevent、libuv 这类通用库,而是针对“我只做压测”这件事做了裁剪,所以二进制体积小、启动快、每连接的内存占用约等于几个缓冲区。
它做了几个很聪明的取舍。第一,报文内容用命令行参数或文件提供,不内置协议解析,你想发什么就发什么,协议适配的成本转移给了使用者,换来的是通用性。第二,统计走采样加汇总,不保存每条消息的明细,所以跑一小时和跑十秒的内存曲线几乎一样平。第三,它自带一个“服务端模式”,也就是-l监听模式,能把一台机器变成假服务端,用来验证客户端重连逻辑,这个能力很多压测工具没有。
代价也很明显:它不会帮你做 JSON 序列化、不会做签名、不会做 cookie 管理。凡是需要“每次请求都不同”的场景,你得靠消息文件加多条会话,或者干脆自己写个中间层。
1.3 什么场景该用它
我总结了一张对照表,遇到需求时先对号入座:
| 压测目标 | 是否适合 tcpkali | 说明 |
|---|---|---|
| 长连接网关的连接容量 | 适合 | 核心场景,几十万连接是它的强项 |
| 私有二进制协议吞吐 | 适合 | 消息内容完全自定义 |
| WebSocket 消息推送 | 适合 | 内置握手支持,见第 4 章 |
| 心跳超时率验证 | 适合 | 配合--message-rate模拟心跳 |
| HTTP 接口 QPS | 不太适合 | 无 HTTP 语义,只是裸发字节 |
| 需要登录态、动态签名的业务 | 不适合 | 缺少脚本能力 |
| 数据库协议压测 | 不适合 | 协议太复杂,建议专用工具 |
提示:tcpkali 不负责验证服务端返回内容的正确性,它只统计收到了多少字节、多少条消息。业务正确性得靠日志或抽样抓包另行确认。
2. 核心机制拆解:它凭什么把网卡打满
2.1 事件循环与连接调度
tcpkali 的每个 worker 线程都跑一个独立的事件循环。主线程负责解析参数、建立监听、分配任务,worker 线程各自持有一批连接的文件描述符,用 epoll 的边沿触发模式或者 kqueue 的 EV_CLEAR 模式来处理可读可写事件。这种“多 reactor 无共享”的结构避免了锁竞争,也避免了连接在核间来回迁移导致的缓存失效。
连接建立阶段它用了非阻塞 connect,把一批 socket 一次性扔进内核,然后靠事件回调收割结果。--connect-rate参数就是在这个环节做限速的,单位是每秒新建连接数。为什么要限速?因为瞬时几万个 SYN 会把服务端的 accept 队列打爆,你测出来的失败率其实是自己造成的,不是服务端真实能力。分批次建连更接近真实用户增长曲线,也更容易观察服务端在不同连接水位下的表现。
2.2 内存模型与缓冲区策略
一个空闲 TCP 连接在内核里大约占 3 到 4 KB(收发缓冲区按最小值算),在 tcpkali 进程里还要再加一份用户态缓冲。十万连接大概就是几百 MB 量级,这个数字决定了压测机的内存下限。--sndbuf和--rcvbuf两个参数可以显式设置 socket 缓冲区大小,把它们调小能显著降低内存占用,代价是吞吐上限下降。
这里有个很多人忽略的点:Linux 的接收缓冲区会自动调优,tcp_rmem的默认最大值可能到 6 MB,如果你不显式限制,十万连接的理论内存上限会非常夸张。实际不会真的分配满,但一旦对端持续快速发送,缓冲区会被撑大。所以做大规模连接压测时,我会把--rcvbuf设成一个固定值,比如 4096 或 8192,让内存曲线可控。
2.3 统计口径:延迟数据是怎么算出来的
tcpkali 提供三种延迟统计开关,口径完全不同,混用会得出错误结论:
--latency-connect:从发起 connect 到连接建立完成的时间,反映的是三次握手加服务端 accept 的速度。--latency-first-byte:从连接建立到收到第一个字节的时间,反映服务端处理首个请求的响应速度。--latency-messages:消息级往返延迟,需要服务端有回包,它靠给每条消息打时间戳来算。
这三个指标对应三种完全不同的瓶颈。连接延迟高,问题多半在服务端的 listen backlog 或者 SYN 队列;首字节延迟高,多半是应用层初始化慢,比如连接建立后要做鉴权查库;消息延迟高,才是真正的处理逻辑或者网络链路问题。我见过有人只开--latency-connect就去评判服务端性能,结论必然是偏的。
注意:
--latency-messages需要服务端把收到的内容原样回传,否则统计不到。压测前先用nc或者一个回显服务验证链路。
3. 安装与第一次跑通:别急着上参数
3.1 源码编译与依赖
tcpkali 没有发布二进制包,主流做法是源码编译。依赖很少:gcc、make、autoconf、automake、libtool,部分发行版还需要 pkg-config。流程就是常规三步:
# 拉取源码后进入目录 ./autogen.sh ./configure --prefix=/usr/local make -j$(nproc) sudo make installautogen.sh那一步是生成 configure 脚本,如果报错说找不到aclocal,说明 automake 没装。编译过程中如果提示某个头文件缺失,多半是开发包没装全,比如libtool-ltdl-devel这类。整个过程通常一两分钟就完事,编出来的二进制在src/tcpkali,可以直接拷到其他同架构机器上用,不需要带任何动态库。
装完敲tcpkali -V看版本,再敲tcpkali -h把帮助翻一遍。帮助信息不长,但每个参数都值得看一眼,尤其是那些平时用不到但关键时刻救命的,比如--channel-latency和--stat-file。
3.2 三条命令验证环境
第一次跑,建议按“服务端 → 客户端 → 双向”的顺序验证,别一上来就压业务服务。
第一步,用 tcpkali 自己当服务端:
tcpkali -l 9000 --duration 60这条命令在本机 9000 端口起一个监听,持续 60 秒,收到什么就统计什么。
第二步,另开一个终端当客户端:
tcpkali -c 10 --duration 30 --message 'ping' 127.0.0.1:900010 个并发连接,每个连接建立后发一条ping,然后持续跑 30 秒。
第三步,加上延迟统计再跑一次:
tcpkali -c 10 -T 30 --latency-connect --latency-first-byte --message 'ping' 127.0.0.1:9000这三条命令能跑通,说明编译、网络、参数都没问题,后面再往业务上套。
3.3 输出指标怎么读
tcpkali 结束时打印的汇总大致分几块:总时长、总带宽、连接相关统计、消息相关统计、延迟分位。看的时候有几个坑要避开。
带宽那一栏是双向合计还是单向,取决于你是否开了-u。开了单向模式就只有发送方向。消息速率是“所有连接合计”,不是单连接,别拿它直接除以连接数去算单连接 QPS,因为不同连接的消息数可能不均匀。
延迟分位里最有价值的是 P99 和最大值。P99 突然飙高但平均值正常,通常意味着服务端有偶发的长尾,比如 GC 停顿、连接排队、锁竞争。最大值异常大,先怀疑压测机自己的调度抖动,尤其是你在压测机上还跑了别的进程。
4. 参数全景:按职责分组理解,别死记
4.1 连接控制类参数
这一类决定“建多少连接、多快建、建不上怎么办”。
-c或者--connections是目标连接总数,这是硬上限。--connect-rate是每秒最多新建多少,不设就是不限速,瞬间打满。--connect-timeout控制单次 connect 的超时秒数,服务端 accept 慢的时候调大它能减少误判。--reconnect让连接断开后自动重连,测客户端重连风暴时特别有用。
我一般的组合是:连接数按目标容量的 1.2 倍设,连接速率按每秒总连接数的 5% 到 10% 设。比如要测 20 万连接,就-c 240000 --connect-rate 20000,建连阶段大约十几秒,这个节奏既不会打爆 SYN 队列,也能在合理时间内进入稳态。如果你要测的就是“瞬间涌入”的极端场景,那才把--connect-rate去掉。
4.2 消息与速率类参数
-m/--message指定连接建立后发送的内容,-f/--message-file从文件读,适合发大报文或者二进制内容。-1/--first-message是连接建立后立刻发的第一条消息,常用于模拟“连上就发鉴权包”的握手流程。--message-rate控制每个连接每秒发多少条消息。
这里有个容易踩的坑:--message-rate是每连接的速率,不是全局。你要模拟 10 万连接、每 30 秒一次心跳,算法是 1/30 约等于 0.033,直接写--message-rate 0.033就行,tcpkali 支持小数。写整数 1 的话,那就是每连接每秒一条,总消息量会大到吓人。
-u/--unidirectional是单向模式,只发不收。压测纯上行链路,或者服务端根本不回包时用它,能省掉接收侧的开销。反过来,如果你只关心服务端推送能力,可以用监听模式配合对端来测。
4.3 监听模式:把压测机变成假服务端
-l加上端口号,tcpkali 就从客户端变成服务端,接受外部连接并统计。这个模式我主要用在两个地方。
一是验证客户端 SDK 的重连逻辑。用 tcpkali 起一个服务端,故意在几秒后关掉,观察客户端多久重连、重连几次、有没有退避。二是做对比基准,先用 tcpkali 服务端跑一遍,拿到“纯净环境”下的连接数和吞吐上限,再用真实业务服务跑一遍,两者的差值就是业务逻辑的开销。
监听模式下可以配合--channel-bandwidth和--channel-latency模拟受限网络。前者限制每个连接的带宽,后者给每个连接加固定延迟。这两个参数不需要 root 权限,实现方式是应用层的令牌桶和延迟发送,不是改内核。做弱网模拟时比 tc netem 更方便,因为它只影响 tcpkali 自己的流量,不会波及机器上其他进程。
4.4 WebSocket 相关参数
--websocket打开 WebSocket 模式,tcpkali 会自己完成 HTTP Upgrade 握手,把后续消息封装成 WebSocket 帧。用法上,较新的版本支持直接给ws://开头的地址:
tcpkali --websocket -c 500 -T 60 --message-rate 1 ws://127.0.0.1:8080/echo如果你的版本对 URI 的支持不完整,就退回手动方式:用-1参数把完整的握手请求写进去,再把期望的响应头用--header之类的方式补齐。手写握手报文的时候要特别注意Sec-WebSocket-Key必须是合法的 base64,Upgrade、Connection、Sec-WebSocket-Version三个头一个都不能少,末尾要有两个连续的 CRLF。
WebSocket 压测最容易出问题的地方是帧类型。tcpkali 默认发文本帧,如果你的服务端只认二进制帧,就得看版本是否支持指定--ws-message-type之类的开关。另外,WebSocket 的心跳 ping/pong 是协议层的,跟业务心跳不是一回事,压测时别把两件事混在一起算。
5. 性能调优:从十万连接往百万冲
5.1 系统层面的六项必调参数
压测机上不去,九成是系统参数卡住了,不是 tcpkali 不行。下面这张表是我每次换机器都要过一遍的:
| 参数 | 建议值 | 作用 |
|---|---|---|
fs.file-max | 2000000 | 系统级文件描述符上限 |
ulimit -n | 1048576 | 进程级打开文件数 |
net.core.somaxconn | 65535 | listen 队列长度 |
net.ipv4.tcp_max_syn_backlog | 65535 | SYN 队列长度 |
net.ipv4.ip_local_port_range | 1024 65535 | 客户端可用端口范围 |
net.ipv4.tcp_tw_reuse | 1 | 快速复用 TIME_WAIT 连接 |
改完记得sysctl -p生效。ulimit -n要写进/etc/security/limits.conf,否则重启就回去了。
端口范围这一项很多人忽略。单机连同一个目标地址,客户端可用端口就六万多个,超过这个数连接就建不起来,报Cannot assign requested address。解决办法有两个:给压测机配多个源 IP,用-s指定源地址轮换;或者拉多台压测机分布式压。前者简单,后者才是正道。
5.2 tcpkali 自身的参数调优
--workers是第一个要调的。默认值通常等于 CPU 核数或者更少,实际压测时我一般设成物理核数,超线程核不算。为什么?因为压测是纯 IO 密集加轻计算,逻辑核之间的资源争抢反而会拉高延迟抖动。8 核机器就开 8 个 worker,16 核开 16 个,跑之前用nproc确认一下。
缓冲区大小是第二个杠杆。--sndbuf和--rcvbuf调小能提连接数、降内存,调大能提吞吐。经验值是:做连接数压测,收发缓冲各设 4096;做吞吐压测,各设 65536 甚至更大。这个没有标准答案,拿自己的场景做两轮对比就出来了。
--no-nagle关掉 Nagle 算法,小报文场景下能明显降低延迟。默认 tcpkali 是开 Nagle 的,因为压测工具更在意吞吐。如果你测的是实时性敏感的业务,比如游戏指令、交易撮合,就把 Nagle 关掉,让每个小包立刻发出去。代价是小包数量变多,头部开销上升。
提示:调优过程中一次只改一个参数,每轮跑完记录结果。同时改三四个参数,最后你根本不知道是哪个起了作用。
5.3 多机分布式压测与结果合并
单机顶不住的时候就得拉多台。思路很简单:每台压测机跑 tcpkali,参数里连接数按总目标平均分,各自连同一个服务端。合并结果时注意两件事。
第一,总连接数是各机之和,但总消息速率不是简单相加,因为各机的实际速率会受自身 CPU 和网络限制,可能达不到设定值。第二,延迟数据不能直接平均,得看各机的 P99 分布,取最差的那台作为整体下限。我通常会让每台机器用--stat-file把中间统计定时落盘,事后画曲线对比,这样能看出是哪台机器先到瓶颈。
还有个小技巧:多机压测时给每台机器错开启动时间,比如间隔 5 秒依次启动。同时启动会在服务端形成瞬时冲击,测出来的失败率不能代表稳态能力。
6. 常见问题与排查实录
6.1 连接建不上,报错五花八门
最常见的是Cannot assign requested address,原因就是客户端端口耗尽,前面说过,加源 IP 或者减少连接数。第二种是Connection refused,服务端没监听或者端口写错,先用ss -lntp确认。第三种是连接数卡在某个值上不去,进程没报错但就是不再增长,这多半是ulimit -n没生效,用cat /proc/$(pidof tcpkali)/limits确认实际值,别信 shell 里ulimit -n的输出。
还有一种隐蔽的情况:连接能建,但大量连接在建立后几秒内被对端 RST。抓包看如果是服务端主动 RST,多半是 accept 队列满了之后内核的兜底行为,把net.core.somaxconn和服务端的 backlog 参数一起调大。
6.2 延迟数据忽高忽低
延迟抖动大,先分清是压测机的问题还是被测服务的问题。判断方法很简单:把目标换成 tcpkali 自己的监听模式,本机回环压一轮。如果回环下延迟依然抖,那就是压测机自己的调度或者 CPU 争抢;如果回环很稳、压业务就抖,问题在被测侧。
本机抖动常见原因有三个:CPU 被其他进程抢占、worker 数和核数不匹配导致的上下文切换、网卡中断集中在一个核上。第三个可以用irqbalance或者手动绑中断解决。被测侧抖动常见原因是应用层的锁竞争、连接池打满、日志同步刷盘,这些得配合服务端的监控一起看。
6.3 统计结果和预期差很远
遇到吞吐数字对不上,先确认统计口径。开了-u单向模式,收方向的数字就是 0,这不是 bug。开了--message-rate,消息条数应该接近“连接数 × 速率 × 时长”,差太多就是有连接提前退出了。
再确认消息大小。--message 'ping'是 4 字节,加上 TCP/IP 头 40 字节,实际线路上跑的是 44 字节,带宽统计如果按有效载荷算,会跟网卡计数差一大截。做带宽评估时以网卡计数为准,做业务评估时以有效载荷为准,两个数都记下来。
6.4 常见问题速查表
| 现象 | 优先怀疑 | 快速验证 |
|---|---|---|
| 连接数上不去 | ulimit / 端口耗尽 | cat /proc/pid/limits,ss -s |
| 建连失败率高 | backlog 太小 / 限速过快 | 调大 somaxconn,降低 connect-rate |
| 延迟 P99 飙高 | 服务端长尾 / 压测机抖动 | 换回环目标对比 |
| 消息数远低于预期 | 连接中途断开 | 看服务端连接数曲线 |
| 带宽数字虚高 | 统计含协议头 | 对比网卡计数 |
| WebSocket 握手失败 | 请求头不全 / 版本不支持 | 抓包看 400/426 响应 |
7. 几个我踩过的坑和实操建议
第一个坑是拿压测机当业务机用。有次图省事,在一台还跑着日志采集和监控 agent 的机器上压测,结果连接数一到 8 万就开始掉,查了半天才发现是 agent 在抢 CPU。压测机就该是干净的,除了 tcpkali 和必要的抓包工具,其他服务全停掉。
第二个坑是忘了看服务端的视角。tcpkali 报 20 万连接成功,服务端却只认到 15 万,差的 5 万卡在半连接状态。这种情况通常是服务端的 accept 循环处理不过来,得看服务端的 accept 队列长度和 worker 数。压测数据一定要和服务端监控对齐,单看一边容易得出错误结论。
第三个坑是消息内容带特殊字符。--message里如果包含空格、引号、反斜杠,shell 会先做一层解析,很容易传错。这种场景一律用--message-file,把内容写进文件,二进制内容也不会有编码问题。
第四个坑是持续时间设太短。跑 10 秒得出的 P99 基本没参考价值,短时间的抖动会被放大。我的习惯是最少跑 3 分钟,前 30 秒当预热,只统计后半段。tcpkali 没有内置的预热丢弃功能,得靠--stat-file分段看,或者干脆跑两轮,第一轮热身。
最后分享一个我常用的组合命令,用来做长连接网关的基线测试:
tcpkali -c 200000 \ --connect-rate 20000 \ -T 300 \ --workers 16 \ --sndbuf 4096 --rcvbuf 4096 \ --no-nagle \ --message-rate 0.033 \ --message-file heartbeat.bin \ --latency-connect --latency-first-byte \ --stat-file result.json \ 10.0.0.10:9000这套参数的含义是:20 万连接分 10 秒建完,跑 5 分钟,16 个 worker,收发缓冲压到 4 KB 控制内存,关 Nagle 降延迟,每连接 30 秒一次心跳,统计连接延迟和首字节延迟,中间结果落盘方便画曲线。实际跑下来,16 核 32G 的机器内存占用在 3 GB 上下,网卡千兆的话带宽会先到瓶颈,得上万兆。
这套数字不是标准答案,每换一个业务、一台机器都得重新标定。但方法论是通用的:先把系统参数铺平,再把连接数和速率压到服务端不报错的临界点,然后逐步加消息速率,观察延迟曲线的拐点。拐点出现的位置,就是这套架构的真实容量上限。