使用 k6 对 Centrifugo 进行大规模连接压测从 JWT 鉴权到 WebSocket 订阅的完整实践【免费下载链接】centrifugoScalable real-time messaging server in a language-agnostic way. Self-hosted alternative to Pubnub, Pusher, Ably, socket.io, Phoenix.PubSub, SignalR. Set up once and forever.项目地址: https://gitcode.com/gh_mirrors/ce/centrifugo导读本文以 Centrifugo 仓库内置的 misc/benchmarking/k6/readme.md 为骨架完整讲解如何使用开源压测工具 k6 对 Centrifugo 建立大规模 WebSocket 连接并进行基准测试包括以「每用户独立 JWT」的方式连接、订阅test频道、维持连接并响应服务器心跳的全过程。读完后你将掌握官方压测脚本 benchmark.js 的逐行原理、配套 Centrifugo 配置的每个参数含义以及如何把它改造成你自己的连接压测方案。一、这份压测脚本做了什么仓库中的misc/benchmarking/k6/目录是一个「起步即用」的基准测试示例由两个脚本组成文件说明benchmark.js标准双向 WebSocket 压测JWT 鉴权连接 订阅test频道uni_ws/benchmark.js单向 WebSocketUnidirectional WS压测仅维持 5000 条连接整个测试的流程是每个虚拟用户VU生成一个专属的 HMAC JWT → 通过 WebSocket 连接到 Centrifugo → 订阅名为test的频道 → 维持连接一段时间后退出。脚本刻意保持极简目的是作为你编写更复杂压测场景的起点。从源码角度看该脚本连接的端点正是 Centrifugo 的标准 WebSocket 入口/connection/websocket。在 internal/app/mux.go 中HandlerWebsocket被注册到该路径前缀下而单方向版本连接的/connection/uni_websocket则对应HandlerUniWebsocket见 internal/app/mux.go由 internal/uniws/handler.go 中的Handler.ServeHTTP处理——你可以对照源码确认脚本里每一个端点都是真实存在的。二、压测前的准备k6 与 Centrifugo 配置1. 安装 k6脚本基于 k6 的 WebSocket 模块k6/ws你需要先在本机安装 k6 命令行工具。k6 是 Grafana 开源的单二进制压测工具安装完成后在终端执行k6 version即可验证。2. 启动 Centrifugo关键配置逐项解析原文档要求使用如下 JSON 配置启动 Centrifugo{ token_hmac_secret_key: secret, admin: true, admin_password: password, admin_secret: 0453661f-8b95-4f75-ab49-cfb2e3b2024f, api_key: 7d8cee29-f5c0-419e-ae70-760767b21f8e, allow_subscribe_for_client: true, allowed_origins: [] }每个参数的作用如下这对理解压测脚本至关重要token_hmac_secret_key: secretHMAC-SHA 签名的 JWT 密钥脚本用它生成连接令牌。在服务端该值被传入 internal/jwtverify/token_verifier_jwt.go 的newAlgorithms会同时启用HS256、HS384、HS512三种 HMAC 校验器并在服务启动日志中打印启用的算法列表。压测脚本使用的是 HS256。admin: trueadmin_passwordadmin_secret开启管理界面与 Admin API。其中admin_password和admin_secret必须同时配置否则 internal/admin/handlers.go 会直接记录错误日志。api_key: ...HTTP API 的访问密钥。压测本身不需要它但如果你希望通过 HTTP API 向test频道推送消息来制造真实流量而不只是干连接就需要带上它。服务端在 internal/middleware/auth.go 中通过Authorization: apikey KEY请求头或?api_key查询参数校验。allow_subscribe_for_client: true允许已认证的客户端直接订阅频道。注意它实际上是一个命名空间级的选项其定义在 internal/configtypes/namespace.goAllows authenticated (non-anonymous) clients to subscribe to channels in this namespace。当在全局配置中直接写allow_subscribe_for_client时它作用于默认命名空间对应test频道所在命名空间。压测脚本中客户端用 JWT 完成鉴权后再发起subscribe命令正依赖此配置放行。allowed_origins: []允许的跨域来源列表。压测脚本从ws://localhost:8000/...发起连接Origin 不构成限制置空数组可避免浏览器类来源校验干扰测试WebSocket 升级时的 Origin 检查见 internal/uniws/handler.go标准 WebSocket 端点同理。配置保存为例如config.json后启动 Centrifugocentrifugo -c config.json默认监听8000端口WebSocket 端点为ws://localhost:8000/connection/websocket脚本中的地址与此一致。三、核心脚本 benchmark.js 逐段拆解1. 依赖与常量import ws from k6/ws; import { check } from k6; import crypto from k6/crypto; import encoding from k6/encoding; const hmacSecret secret;k6/ws提供 WebSocket 连接能力k6/cryptok6/encoding用于在脚本内部手工构造 JWT无需外部依赖 JWT 库hmacSecret必须与 Centrifugo 配置里的token_hmac_secret_key保持一致否则服务端验签失败、连接被拒。2. 手工实现 HS256 JWT 签名const algToHash { HS256: sha256, HS384: sha384, HS512: sha512 }; function sign(data, hashAlg, secret) { let hasher crypto.createHMAC(hashAlg, secret); hasher.update(data); // Some manual base64 encoding as Hasher.digest(encodingType) doesnt support that encoding type yet. return hasher.digest(base64).replace(/\//g, _).replace(/\/g, -).replace(//g, ); } function encode(payload, secret, algorithm) { algorithm algorithm || HS256; let header encoding.b64encode(JSON.stringify({ typ: JWT, alg: algorithm }), rawurl); payload encoding.b64encode(JSON.stringify(payload), rawurl); let sig sign(header . payload, algToHash[algorithm], secret); return [header, payload, sig].join(.); }这是一段完整的HS256 JWT 客户端实现Header{ typ: JWT, alg: HS256 }用rawurl模式做 Base64URL 编码raw表示不补填充url表示使用-、_安全字符Payloadencoding.b64encode(..., rawurl)同样处理签名输入header . payload签名用k6/crypto的 HMAC 计算后再手动把标准 Base64 输出转成 Base64URL——注释明确指出这是因为Hasher.digest(encodingType)尚不支持该编码类型这是 k6 脚本中常见的兼容写法algToHash映射表同时支持 HS256/HS384/HS512方便你切换算法对应服务端token_hmac_secret_key派生的三种 verifier见 internal/jwtverify/token_verifier_jwt.go。3. 为每个用户生成专属令牌function generateToken(userId) { const payload { sub: userId, exp: Math.floor(Date.now() / 1000) 60, // Expires in 60 seconds }; return encode(payload, hmacSecret, HS256) }sub是 Centrifugo 识别用户身份的 claim——服务端在VerifyConnectToken中会把sub作为用户 ID见 internal/jwtverify/token_verifier_jwt.go这也是脚本「per user」的含义每个虚拟用户都拥有独立身份从而模拟真实的多用户在线场景exp设为 60 秒后过期。注意这与连接维持时长相关服务端每次验证都会检查expclaims.IsValidExpiresAt(now)见 internal/jwtverify/token_verifier_jwt.go如果测试时长超过 60 秒就需要相应调大exp。4. 压测负载模型stagesexport let options { stages: [ { duration: 10s, target: 1000 }, { duration: 50s, target: 1000 }, ] };前 10 秒并发从 0 线性爬升到1000 个虚拟用户后 50 秒维持 1000 并发单轮测试总计 60 秒正好与 JWT 的 60 秒有效期匹配。stages是 k6 标准的负载阶段描述你可以按需调整想测更大压力就提高target想测「瞬间冲击」就把 ramp 阶段压缩到 1 秒以内。5. 连接、鉴权与订阅export default function () { const url ws://localhost:8000/connection/websocket; const user user_${__VU}_${__ITER}; const token generateToken(user); const response ws.connect(url, {}, function (socket) { socket.on(open, () { const connectCommand JSON.stringify({ id: 1, connect: { token: token } }); const subscribeCommand JSON.stringify({ id: 2, subscribe: { channel: test } }); socket.send(connectCommand \n subscribeCommand); }); ...__VU是 k6 内置的虚拟用户编号__ITER是迭代次数二者组合保证每个连接的用户名唯一连接建立open后立即通过换行符分隔批量发送两条 Centrifuge 协议命令{ id: 1, connect: { token } }携带 JWT 发起连接鉴权{ id: 2, subscribe: { channel: test } }订阅test频道之所以能用\n拼接是因为 Centrifugo 的 WebSocket 协议支持 Protobuf/JSON 文本帧按换行分隔的批处理模式订阅能否成功依赖配置中的allow_subscribe_for_client: true见上文第二节。6. 响应服务器心跳Ping/Pongsocket.on(message, (message) { // Respond to server pings. const substrings message.split(\n); if (substrings.includes({})) { socket.send({}); } });Centrifugo 会周期性向客户端发送 ping 帧默认 ping 间隔在 internal/uniws/config.go 中定义为 25 秒标准 WebSocket 端点同理。客户端收到代表 ping 的空 JSON 对象{}后回发{}作为 pong避免连接因心跳超时被服务端断开——这是长连接压测脚本里最容易遗漏、却直接影响压测有效性的细节。7. 超时控制与结果校验socket.setTimeout(() { socket.close(); }, 60000); }); check(response, { status is 101: (r) r r.status 101 }); }setTimeout(60000)连接最多维持 60 秒后主动关闭与exp、stages总时长对齐check(response, { status is 101 })校验 WebSocket 升级返回 HTTP 101用于判定握手是否成功。k6 会自动统计checks的通过率压测结束后通过checks指标即可一眼看出连接成功率。四、运行测试在misc/benchmarking/k6/目录下执行k6 run benchmark.js脚本会在约 60 秒内完成10 秒爬升至 1000 连接、50 秒维持、随后 VU 依次退出。运行结束后 k6 会输出汇总报告重点关注http_reqs/ws connecting等连接类指标checks通过率对应握手 101 校验iterations完成数。如果目标是观察服务端在持续消息流量下的表现可以结合 Centrifugo 的 HTTP API 向test频道推送消息使用配置中的api_key让压测连接处于真实收发状态而不是纯空连接。五、变体单方向 WebSocket 压测uni_wsuni_ws/benchmark.js是面向单向 WebSocket 传输服务端 → 客户端推送的压测变体适合评估「服务端向海量连接单向广播」这一典型场景如行情推送、通知订阅export let options { stages: [ // Ramp up to 5000 connections over 60 seconds { duration: 60s, target: 5000 }, // Hold 5000 connections for 4 minutes { duration: 4m, target: 5000 }, // Ramp down to 0 connections { duration: 10s, target: 0 }, ], }; export default function () { const url ws://localhost:8000/connection/uni_websocket?cf_connect{}; ...与主脚本的差异点目标规模更大60 秒爬升至5000 连接并维持 4 分钟最后 10 秒归零总时长约 5 分钟连接地址不同/connection/uni_websocket并通过 URL 查询参数cf_connect{}直接携带空连接请求。cf_connect是单方向 WebSocket 约定的连接参数名常量定义见 internal/uniws/handler.go其中需填入协议ConnectRequest的 JSON置为{}表示匿名空连接不需要 JWT维持连接方式不同由于单向传输下客户端不发送协议命令脚本改用每 30 秒一次socket.ping()的 WebSocket 协议层心跳来保活同时设置 5 分钟超时作为兜底。六、大规模连接前的系统调优原文档特别提醒压测大规模连接前请先确认系统已针对海量连接做过调优涉及文件描述符上限、TCP 端口范围、内核网络参数等。当连接数达到数千乃至数万级别时常见瓶颈包括单进程可打开的文件描述符上限ulimit -n每个 WebSocket 连接至少消耗 1 个 socket fd本地端口耗尽TIME_WAIT 状态的端口复用配置内核 TCP 缓冲与连接队列参数。未做调优时压测可能直接触发EMFILE/EADDRNOTAVAIL等错误导致连接数在某个阈值附近异常抖动压测结果失真。建议在压测前先按上述方向检查运行环境例如把ulimit -n提升到 10 万以上再逐级加大target观察服务端表现。七、把示例改造成自己的压测方案以官方脚本为起点常见的定制方向调整并发规模与时长修改options.stages并将generateToken中的exp、socket.setTimeout的时长一并放大保持三者一致避免压测中途令牌过期切换 JWT 算法把encode(payload, hmacSecret, HS256)改为HS512等algToHash已内置映射同时服务端保持token_hmac_secret_key不变即可三种 HMAC verifier 会同时启用多频道订阅在open回调中追加更多{ id, subscribe: { channel } }命令即可模拟单连接订阅多频道的真实业务叠加发布流量用 Centrifugo HTTP API 定时向频道 publish或在压测机中混入发布者脚本测试「读写混合」负载双向随机消息在message回调中除响应{}心跳外再加入业务消息计数用于校验消息可达率可结合 internal/jwtverify/token_verifier_jwt.go 的完整连接验证流程理解消息链路。结语misc/benchmarking/k6/是理解 Centrifugo 连接能力的最佳起点它用不到 100 行代码覆盖了「JWT 鉴权 → WebSocket 握手 → 频道订阅 → 心跳保活 → 结果校验」的完整闭环所有行为都能在仓库源码internal/jwtverify/token_verifier_jwt.go、internal/app/mux.go、internal/uniws/handler.go、internal/configtypes/namespace.go中找到对应实现。将其改造为自己的压测基线即可在真实业务负载下评估 Centrifugo 的性能表现。【免费下载链接】centrifugoScalable real-time messaging server in a language-agnostic way. Self-hosted alternative to Pubnub, Pusher, Ably, socket.io, Phoenix.PubSub, SignalR. Set up once and forever.项目地址: https://gitcode.com/gh_mirrors/ce/centrifugo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
