使用 k6 对 Centrifugo 进行大规模连接压测:从 JWT 鉴权到 WebSocket 订阅的完整实践
2026/9/23 15:23:12 网站建设 项目流程

使用 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单向 WebSocket(Unidirectional 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: "secret":HMAC-SHA 签名的 JWT 密钥,脚本用它生成连接令牌。在服务端,该值被传入 internal/jwtverify/token_verifier_jwt.go 的newAlgorithms,会同时启用HS256HS384HS512三种 HMAC 校验器,并在服务启动日志中打印启用的算法列表。压测脚本使用的是 HS256。
  • admin: true+admin_password+admin_secret:开启管理界面与 Admin API。其中admin_passwordadmin_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.go:Allows 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后,启动 Centrifugo:

centrifugo -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/crypto+k6/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)` doesn't 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. 压测负载模型(stages)

export 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/Pong)

socket.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 秒后主动关闭(与expstages总时长对齐);
  • 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_ws)

uni_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观察服务端表现。

七、把示例改造成自己的压测方案

以官方脚本为起点,常见的定制方向:

  1. 调整并发规模与时长:修改options.stages,并将generateToken中的expsocket.setTimeout的时长一并放大,保持三者一致,避免压测中途令牌过期;
  2. 切换 JWT 算法:把encode(payload, hmacSecret, "HS256")改为HS512等,algToHash已内置映射;同时服务端保持token_hmac_secret_key不变即可(三种 HMAC verifier 会同时启用);
  3. 多频道订阅:在open回调中追加更多{ id, subscribe: { channel } }命令,即可模拟单连接订阅多频道的真实业务;
  4. 叠加发布流量:用 Centrifugo HTTP API 定时向频道 publish,或在压测机中混入发布者脚本,测试「读写混合」负载;
  5. 双向随机消息:在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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询