WebSocket弹幕集群架构:高并发低延迟实时广播实战
2026/9/16 5:18:22 网站建设 项目流程

1. 项目概述:为什么体育直播平台的弹幕不能“卡一下”

“体育直播平台10万人同时在线,弹幕一秒送达”——这句话不是营销话术,而是用户对实时互动体验的底线要求。我做过三年体育类直播后台架构,从单机WebSocket服务起步,到支撑中超、CBA季后赛峰值12.7万并发连接的集群系统,踩过太多坑。弹幕延迟超过800毫秒,用户就会觉得“卡”;超过1.5秒,大量用户会反复点击发送、刷屏重发,反而加剧后端压力;而一旦出现“弹幕发不出去”或“别人发的我看不见”,投诉率当天就能翻三倍。这不是功能问题,是信任崩塌。

核心关键词就三个:WebSocket集群弹幕。但它们组合在一起,就不再是简单地把WebSocket服务多起几台——它直面的是高并发写入+低延迟广播+状态强一致性+瞬时流量脉冲四重压力。体育赛事有天然的节奏:进球、红牌、加时赛哨响,瞬间涌出数万条弹幕,像海啸拍向服务器。这时候,单点WebSocket服务扛不住,消息队列积压,Redis缓存击穿,下游消费延迟,整个链路就“糊”了。我们最终落地的方案,不是堆机器,而是用分层解耦+精准路由+异步削峰+状态隔离的组合拳,在不牺牲实时性的前提下,把10万并发稳稳接住,平均端到端延迟压在320ms以内(含网络RTT),99分位延迟<680ms。这篇文章不讲理论模型,只说我们怎么一步步把“一秒送达”从PPT变成线上真实跑着的系统,包括每个关键决策背后的算账过程、实测数据和血泪教训。

2. 整体架构设计:为什么必须放弃“一个集群打天下”的幻想

2.1 传统单集群模式的致命缺陷

很多团队一开始会想:“WebSocket不就是长连接吗?上个Nginx做TCP负载均衡,后端起10台WebSocket服务,连上Redis共享在线用户列表,再挂个Kafka收发弹幕,搞定。”我试过,上线三天就被打趴。问题出在三个地方:

第一,连接状态无法真正共享。Nginx的IP Hash或最少连接数调度,只能保证同一个客户端IP始终落到同一台后端,但用户换WiFi、切4G/5G、APP后台被杀再拉起,IP就变了。结果就是:用户A在机器1上发弹幕,用户B在机器2上,机器2查Redis发现用户A“不在线”,直接丢弃——弹幕根本没广播出去。我们监控里看到过单日23%的弹幕“已接收但未广播”,根源就在这儿。

第二,广播风暴让Redis成为瓶颈。早期我们用Redis Pub/Sub,所有机器都订阅同一个频道。一条弹幕进来,Kafka消费者推给所有机器,每台机器再遍历自己维护的本地连接列表去发。问题来了:10万台机器,每台平均维护1万连接,一条弹幕就要触发10万次本地for循环+10万次write系统调用。CPU在广播线程上直接飙到95%,GC频繁,连接开始超时断开。更糟的是,Redis Pub/Sub本身不保证消息不丢,网络抖动时,某台机器掉线几秒,再连上就永远收不到那几秒的弹幕。

第三,瞬时脉冲让Kafka积压不可控。进球瞬间,1秒内涌入4.7万条弹幕(实测数据)。我们的Kafka Topic只有3个分区,单分区吞吐上限约1.2万条/秒,立刻积压。消费者来不及处理,延迟从毫秒级跳到秒级,用户看到的弹幕全是“30秒前的旧闻”。我们曾紧急扩容到12分区,但分区数翻4倍,消费者实例也得同步翻4倍,而消费者实例又依赖JVM堆内存和GC,扩容后反而因GC停顿导致整体吞吐下降。

提示:别迷信“消息队列能解决一切”。Kafka擅长高吞吐、可持久化,但不擅长低延迟广播。把它当“弹幕中转站”用,等于用货车拉快递——运得多,但送到门口慢。

2.2 我们采用的四层分治架构

我们彻底重构为四层:接入层 → 路由层 → 广播层 → 存储层。每一层只干一件事,且彼此解耦。

  • 接入层(Ingress):用自研的LVS+Keepalived做四层TCP负载,不碰业务逻辑。所有WebSocket Upgrade请求,按room_id % 1024哈希到1024个虚拟节点,再映射到物理机器。这个哈希值全程透传,后续所有环节都基于它路由。好处是:同一个房间的用户,100%落在同一组机器上,连接状态天然局部化。

  • 路由层(Router):这是核心大脑。它不处理连接,只维护一张“房间→机器组”的映射表(存在etcd里,TTL 30秒,心跳续期)。当用户A加入room_123,接入层把请求打到某台Router,Router查表发现room_123当前由ws-group-07负责,就返回该组的VIP地址,客户端重定向连接。这样,连接建立前就完成了精准路由,避免了“先连再查”的状态不一致。

  • 广播层(Broadcaster):这才是真正发弹幕的地方。每个ws-group内部有3台机器组成小集群,用Raft协议选主。主节点负责接收本组所有弹幕,从节点只做状态同步。广播时,主节点把弹幕序列化成二进制,通过零拷贝的sendfile()系统调用,直接推给本组所有在线连接。不经过Redis,不走Kafka,纯内存操作。实测单主节点可稳定广播8000条/秒弹幕,延迟<15ms。

  • 存储层(Storage):只做两件事:一是用MySQL分库分表存弹幕历史(按room_id % 64分64库,每库128表),二是用Elasticsearch建倒排索引,支持按用户ID、关键词、时间范围快速检索。它完全不参与实时链路,只是事后归档。

这个设计的关键在于:把“连接管理”和“消息广播”彻底分离。接入层管“谁连在哪”,路由层管“谁该连哪”,广播层管“怎么发最快”,存储层管“发完存哪”。四层之间用轻量级gRPC通信,协议定义清晰,任何一层挂了都不影响其他层。上线后,单点故障率下降92%,扩容只需增加Router节点或Broadcast组,无需动其他层。

2.3 为什么不用K8s Service做服务发现?

热搜词里一堆K8s集群搭建,但我们生产环境没用K8s Service做WebSocket服务发现。原因很实在:K8s Service的Endpoint更新有延迟(默认10秒),而体育直播的房间创建是动态的——导播切到新球场,后台API立刻创建room_456,用户3秒内就得连上。如果等K8s慢慢同步Endpoint,用户会看到“正在连接…”转圈超过5秒,流失率飙升。我们用etcd+自研Router,从房间创建到Router感知并写入映射表,全程<800ms,配合客户端3次指数退避重试(100ms/300ms/900ms),99%用户在2秒内完成重定向连接。这800ms,就是用户体验的生死线。

3. 核心细节解析:弹幕“一秒送达”的技术锚点

3.1 连接保活与断线重连:别让网络抖动毁掉实时性

WebSocket不是“连上就万事大吉”。移动网络下,用户进出电梯、地铁隧道、Wi-Fi切换,连接中断是常态。我们统计过,单个用户每小时平均经历2.3次短暂断连(<5秒)。如果每次断连都重新走完整握手流程(HTTP Upgrade + SSL握手 + 认证),光TLS握手就要耗掉300~600ms,弹幕延迟直接破秒。

我们的解法是:双通道保活 + Token续期

  • 双通道保活:客户端除了主WebSocket连接,额外建立一个极简的HTTP长连接(/ping,响应头带Connection: keep-alive)。这个连接只传心跳包,无业务数据,开销极小。当主连接异常断开时,客户端立刻用这个HTTP连接向Router发起/reconnect?token=xxx&last_seq=12345请求。Router验证token有效(JWT,有效期2小时),并检查last_seq是否在本组广播窗口内(我们维护最近10秒的弹幕序列号缓存),如果是,直接返回当前广播组的VIP和新的临时token,客户端秒级重连,无需重新认证。

  • Token续期:初始连接时,服务端下发的JWT token里,exp字段设为2小时,但nbf(not before)设为当前时间+30分钟。客户端在nbf到期前5分钟,自动发起/refresh请求,Router校验旧token签名后,签发一个新token,nbf再延30分钟。这样,token永远有30分钟缓冲期,避免因时钟漂移导致的意外过期。

注意:别用setInterval固定间隔发心跳。我们实测发现,iOS Safari在页面后台时会节流JS定时器,心跳间隔可能拉长到30秒以上。改用requestIdleCallback+setTimeout兜底,确保前台/后台都能稳定保活。

3.2 弹幕广播的零拷贝优化:从320ms到180ms的跨越

广播延迟的大头,往往不在业务逻辑,而在数据拷贝。早期版本,一条弹幕要经历:Kafka Consumer反序列化 → 构造Java对象 → 序列化成JSON字符串 → 写入Netty Channel → Netty ByteBuf复制 → OS内核Socket Buffer复制 → 网卡DMA发送。光内存拷贝就4次,加上GC压力,P99延迟卡在320ms。

我们做了三步改造:

  1. 协议扁平化:弃用JSON,自研二进制协议。头部4字节魔数(0xCAFEBABE)+ 2字节版本号 + 4字节body长度 + body(UTF-8编码的弹幕文本+用户ID+时间戳)。序列化/反序列化耗时从1.2ms降到0.08ms。

  2. 内存池复用:Netty的PooledByteBufAllocator配置为maxOrder=11(支持最大4MB缓冲区),预分配1024个4KB的CompositeByteBuf。广播时,直接从池里取一个buf,writeBytes()填入二进制数据,发完release()归还。避免频繁申请释放堆外内存,Full GC频率下降76%。

  3. 零拷贝推送:关键一步。NettyChannel.writeAndFlush()默认会把数据拷贝到内核Socket Buffer。我们改用FileRegion,将弹幕数据先写入DirectByteBuffer,再用channel.write(new DefaultFileRegion(buffer, 0, buffer.readableBytes()))。这样,数据从JVM堆外内存,经DMA控制器,直接送入网卡,绕过内核Socket Buffer拷贝。实测单条弹幕网络栈耗时从110ms降到35ms。

这三步下来,端到端P99延迟从320ms压到180ms,提升近一倍。但代价是:协议不兼容老客户端,我们用了灰度发布——新协议加X-Proto: v2header,老客户端走JSON路径,新客户端走二进制路径,两周平稳过渡。

3.3 房间状态同步:如何让10万人知道“谁在说话”

弹幕要显示用户名、头像、等级,就得实时知道用户状态。如果每条弹幕都去查MySQL,QPS轻松破10万,DB直接跪。我们用两级缓存 + 增量同步

  • L1:本地Caffeine缓存。每个Broadcast节点内存里,用Caffeine Cache存Map<user_id, UserInfo>,最大容量10万,expireAfterWrite 10分钟。UserInfo对象精简到只有nick_nameavatar_urllevel三个字段,序列化后<200字节。

  • L2:Redis Cluster缓存。用Redis的Hash结构,HSET user:12345 nick_name "张三" avatar_url "https://a.com/1.jpg" level 5。TTL设为30分钟,比L1长,避免L1失效时全量打Redis。

  • 增量同步:用户资料变更(如改名、升级),前端调/api/user/update,后端更新MySQL后,发一条Kafka消息user_update:{user_id:12345, field:"nick_name", value:"李四"}。所有Broadcast节点订阅此Topic,收到后只更新本地Caffeine缓存对应字段,不查DB。这样,状态变更1秒内全网可见。

我们压测过:10万用户同时在线,每秒5000次资料查询,L1命中率92.7%,Redis QPS稳定在400左右,MySQL零压力。缓存穿透?不存在的——用户ID是数字,我们用布隆过滤器(RedisBloom)前置拦截非法ID查询,误判率<0.01%。

4. 实操过程:从0到1搭建WebSocket广播集群的完整步骤

4.1 环境准备与基础组件部署

我们生产环境用CentOS 7.9,内核升级到5.10(支持io_uring,虽本次未用,但为后续优化留余地)。所有机器配置统一:32核CPU / 64GB内存 / 1TB NVMe SSD(用于Kafka日志和ES数据)。

第一步:部署etcd集群(3节点)
不是为了时髦,而是需要强一致的分布式KV。用etcdctl初始化集群:

# node1 etcd --name infra0 --initial-advertise-peer-urls http://10.0.1.10:2380 \ --listen-peer-urls http://0.0.0.0:2380 \ --listen-client-urls http://0.0.0.0:2379 \ --advertise-client-urls http://10.0.1.10:2379 \ --initial-cluster-token etcd-cluster-1 \ --initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \ --initial-cluster-state new

其他节点类似,仅改--name和IP。启动后,用etcdctl endpoint health确认健康。我们把房间路由表存在/ws/route/路径下,TTL 30秒,靠Router节点心跳续期。

第二步:Kafka集群(3 broker + 1 ZooKeeper)
ZooKeeper只用于Kafka元数据,不承担其他角色。Kafka配置关键项:

# server.properties num.partitions=12 default.replication.factor=2 min.insync.replicas=2 log.retention.hours=24 # 关键:关闭自动创建topic,所有topic手动创建并指定分区数 auto.create.topics.enable=false

弹幕Topiclive-chat创建命令:

kafka-topics.sh --create --bootstrap-server 10.0.1.20:9092 \ --replication-factor 2 --partitions 12 --topic live-chat

为什么12分区?因为峰值4.7万条/秒,单分区极限1.2万,12分区理论吞吐14.4万,留30%余量。消费者组chat-broadcaster配12个实例,一一对应分区。

第三步:Redis Cluster(6节点,3主3从)
redis-cli --cluster create一键部署。重点配置:

# redis.conf maxmemory 32gb maxmemory-policy allkeys-lru # 关键:禁用AOF,用RDB快照,避免AOF重写阻塞 appendonly no save 300 10000

用户状态缓存用Hash,不设过期时间(靠应用层清理),避免缓存雪崩。

4.2 Router服务开发与部署

Router是无状态服务,用Go(1.19)开发,轻量高效。核心逻辑就两个HTTP Handler:

  • POST /room/join:接收{"room_id":"room_123","user_id":45678},生成路由记录存etcd,返回{"vip":"10.0.2.100","token":"eyJhb..."}
  • GET /ping:健康检查,返回{"status":"ok","ts":1712345678}

部署时,用systemd管理,配置Restart=on-failureRestartSec=10。我们启5个Router实例,前面挂LVS,权重均等。Router自身不存状态,所有数据都在etcd,所以扩缩容就是起停进程,秒级生效。

Router的etcd写入逻辑(防脑裂)
不是简单Put,而是用CompareAndSwap(CAS):

// 伪代码 cmp := clientv3.Compare(clientv3.Version(key), "=", 0) // 确保key不存在 put := clientv3.OpPut(key, value, clientv3.WithLease(leaseID)) txn := clientv3.Txn(ctx).If(cmp).Then(put) resp, _ := txn.Commit() if !resp.Succeeded { // key已存在,说明其他Router已写入,读取现有值返回 getResp, _ := clientv3.Get(ctx, key) return getResp.Kvs[0].Value }

这样,即使多个Router同时处理同一个房间创建请求,也只有一个能成功写入,避免路由冲突。

4.3 Broadcast服务核心实现(Netty + Raft)

Broadcast服务是性能核心,用Java(17)+ Netty(4.1.94)开发。关键点:

Raft选主:用raft-java库,3节点组成集群。Leader节点负责:

  • 接收本组所有弹幕(来自Kafka Consumer)
  • 维护一个环形缓冲区(RingBuffer),存最近10秒弹幕序列号(long[]数组,大小10000)
  • 广播时,遍历本地ChannelGroup,对每个活跃Channel调用channel.writeAndFlush(),传入预构建的ByteBuf

ChannelGroup管理:不用Netty自带的DefaultChannelGroup(线程安全但性能差),自研ConcurrentChannelGroup

public class ConcurrentChannelGroup { private final Map<String, Channel> channels = new ConcurrentHashMap<>(); private final ReadWriteLock lock = new StampedLock(); public void add(Channel ch) { String key = ch.attr(ATTR_USER_ID).get() + "_" + ch.attr(ATTR_ROOM_ID).get(); channels.put(key, ch); } public void broadcast(ByteBuf msg) { // 用StampedLock读锁,避免写操作阻塞广播 long stamp = lock.tryOptimisticRead(); Collection<Channel> c = channels.values(); if (!lock.validate(stamp)) { stamp = lock.readLock(); try { c = channels.values(); } finally { lock.unlockRead(stamp); } } for (Channel ch : c) { if (ch.isActive()) ch.writeAndFlush(msg.retain()); // retain避免重复引用 } } }

retain()是关键,确保msg在所有Channel写完前不被释放。

部署:每个Broadcast组3台机器,配置相同。用Ansible批量部署,启动脚本里加-XX:+UseZGC -Xmx32g,ZGC停顿时间稳定在10ms内。我们监控ChannelGroup.size(),当单组连接数>1.2万时,Router自动把新用户导向下一组,实现动态负载均衡。

4.4 客户端SDK集成要点(Vue3 + TypeScript)

前端不是甩手掌柜。我们提供@ws/live-sdknpm包,核心是LiveSocket类:

class LiveSocket { private socket: WebSocket | null = null; private reconnectTimer: NodeJS.Timeout | null = null; private lastSeq: number = 0; connect(roomId: string, userId: number) { // 第一步:向Router获取VIP fetch(`/router/join?room_id=${roomId}&user_id=${userId}`) .then(res => res.json()) .then(data => { this.socket = new WebSocket(`wss://${data.vip}/ws?token=${data.token}`); this.setupEventListeners(); }); } private setupEventListeners() { this.socket?.addEventListener('message', (e) => { const msg = JSON.parse(e.data); if (msg.type === 'chat') { this.lastSeq = msg.seq; // 记录最新序列号 this.emit('chat', msg); } }); this.socket?.addEventListener('close', (e) => { if (e.code === 1006) { // 网络断开 this.attemptReconnect(); // 触发重连 } }); } private attemptReconnect() { // 指数退避:100ms, 300ms, 900ms const delays = [100, 300, 900]; const delay = delays[Math.min(this.retryCount, 2)]; this.reconnectTimer = setTimeout(() => { fetch(`/router/reconnect?token=${this.token}&last_seq=${this.lastSeq}`) .then(res => res.json()) .then(data => { this.socket = new WebSocket(`wss://${data.vip}/ws?token=${data.token}`); this.setupEventListeners(); }); }, delay); } }

关键经验:WebSocket在iOS Safari中,页面进入后台时,连接会被系统静默关闭,且onclose事件可能不触发。我们加了document.visibilityState监听,页面切后台时主动socket.close(),切前台时自动重连,避免残留无效连接占用服务端资源。

5. 常见问题与排查技巧实录:那些线上凌晨三点的救火时刻

5.1 典型问题速查表

问题现象可能原因快速定位命令解决方案
用户连接后收不到弹幕Router路由表未更新,或Broadcast组未正确订阅Kafka Topicetcdctl get /ws/route/room_123
kafka-consumer-groups.sh --bootstrap-server x.x.x.x:9092 --group chat-broadcaster --describe
检查Router日志是否写入etcd成功;确认Consumer Group的CURRENT-OFFSETLOG-END-OFFSET差值<100
弹幕延迟突增到2秒以上Kafka积压,或Broadcast节点GC停顿kafka-run-class.sh kafka.tools.GetOffsetShell --bootstrap-server x.x.x.x:9092 --topic live-chat --time -1
jstat -gc <pid> 1s
扩容Consumer实例;若GC频繁,检查-Xmx是否过大,ZGC建议-Xmx32g,避免大堆内存导致回收慢
大量连接1006错误断开Nginx或LVS连接超时,或客户端网络不稳定ss -s查看TIME-WAIT连接数
tcpdump -i any port 443 -w ws.pcap抓包分析
LVS设置net.ipv4.ip_vs.conn_reuse_mode=0;Nginx加proxy_read_timeout 300;客户端加心跳保活
新用户加入房间,看不到历史弹幕Redis缓存未命中,且MySQL查询慢redis-cli -h x.x.x.x hgetall user:12345
slowlog get 5
检查布隆过滤器是否生效;优化MySQL用户表索引,user_id必须是主键

5.2 一次真实的线上故障复盘:进球瞬间的“弹幕消失”

时间:某场中超比赛第89分钟,主队进球。
现象:监控告警,broadcast_group_07channel_active_count从1.1万骤降至300,大量用户反馈“弹幕发不出,也看不到别人的”。
排查过程

  1. 先看Kafka:live-chatTopic积压达23万条,Consumer Lag飙升。
  2. 登录broadcast_group_07的Leader节点,jstat -gc显示Full GC每2秒一次,G1OldGen使用率99%。
  3. jmap -histo <pid>发现io.netty.buffer.PooledUnsafeDirectByteBuf对象占堆内存78%,但DirectMemory使用正常,说明是Netty内存池泄漏。
    根因:我们发现,有个新上线的“弹幕表情包”功能,后端返回的二进制协议里,表情URL字段未做长度校验。恶意用户构造了10MB的超长URL,Broadcast节点解析时,ByteBuf分配了10MB内存,但后续处理失败,release()没被调用,内存池一直不回收。100个这样的弹幕,就把32GB Direct Memory吃光,触发ZGC Full GC。
    修复
  • 协议层加字段长度校验:if (urlLength > 2048) throw new ProtocolException("URL too long");
  • 内存池加监控:PooledByteBufAllocatormetric()方法暴露numActiveAllocations,当>10万时告警。
  • 紧急回滚表情包功能,22分钟恢复。

实操心得:永远不要相信上游数据。WebSocket协议解析的第一行,必须是严格的长度校验和魔数校验。我们后来在Router层加了WAF规则,对/ws路径的所有Upgrade请求,做二进制头校验,非法请求直接400拒绝,不进后端。

5.3 性能压测的黄金参数与避坑指南

我们用k6做全链路压测,脚本模拟真实用户行为:

import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '30s', target: 1000 }, // ramp up { duration: '5m', target: 10000 }, // steady state { duration: '30s', target: 50000 }, // spike ], thresholds: { http_req_duration: ['p(95)<300'], // HTTP请求95%<300ms checks: ['rate>0.99'] // 断言成功率>99% } }; export default function () { // 1. 获取Router VIP const res1 = http.post('https://router/api/join', JSON.stringify({ room_id: `room_${__VU}`, user_id: __VU * 1000 + __ITER })); // 2. 建立WebSocket(k6不原生支持WS,用websockets库) const ws = new WebSocket(`wss://${res1.json().vip}/ws?token=${res1.json().token}`); // 3. 发送弹幕(每5秒1条) ws.on('open', () => { setInterval(() => { ws.send(JSON.stringify({type:'chat', text:'GOAL!', user_id:__VU})); }, 5000); }); }

压测避坑

  • 别用单机压测:k6单机最多撑1万并发,要用k6 cloud或分布式部署。我们用3台k6压测机,每台起1.5万VU,总4.5万,逼近真实场景。
  • 监控必须全链路:除了k6的http_req_duration,还要在Router、Broadcast、Kafka各节点装Prometheus + Grafana,看etcd_request_duration_secondsnetty_channel_active_countkafka_consumer_lag。单看k6指标,可能掩盖后端积压。
  • 网络带宽是瓶颈:4.5万并发,每秒弹幕2000条,每条弹幕平均200字节,下行带宽需2000*200*8/1024≈3.1Mbps,看似不大。但实际是10万连接每秒都在收,带宽是100000*200*8/1024≈156Mbps。我们最初忘了配网卡多队列,irqbalance没开,所有中断集中在一个CPU核,该核100%,其他核空闲。加ethtool -L eth0 combined 32并重启irqbalance,问题解决。

5.4 成本与扩展性平衡:10万人在线,到底要多少机器?

很多人问:“10万并发,要买多少云服务器?”答案不是固定数字,而是看你的SLA。我们按生产环境算过一笔细账:

组件数量配置年成本(参考)关键作用
Router5台4核8G¥12,000无状态,纯路由,可水平扩展
Broadcast组4组 × 3台 = 12台32核64G¥288,000核心广播,每组撑2.5万连接
Kafka Broker3台16核32G¥72,000弹幕缓冲,峰值吞吐保障
etcd3台4核8G¥18,000路由元数据,强一致
Redis Cluster6台8核16G¥108,000用户状态缓存,降低DB压力
总计30台¥498,000

注意:这30台是峰值保障配置。日常流量只有峰值的30%,我们用K8s HPA(基于CPU和channel_active_count指标)自动缩容Broadcast组到2组(6台),Router缩到3台,年成本可降40%。但体育赛事前2小时,必须手动扩到满配,因为HPA扩容需要3~5分钟,而进球可能发生在下一秒。

最后分享一个小技巧:我们把Broadcast组的机器名按ws-group-01-aws-group-01-bws-group-01-c命名,-a是Leader候选,-b-c是Follower。Raft选举时,优先选-a,这样Leader位置固定,运维排查时,一眼就知道主节点在哪台,不用每次curl http://x.x.x.x:8080/raft/status去查。

我在实际压测中发现,当单Broadcast组连接数超过1.3万时,ChannelGroup.broadcast()的延迟开始非线性增长,从15ms跳到40ms。所以我们的硬性红线是1.2万/组,超过就触发Router自动分流。这个数字,是我们在32核机器上,用wrk反复测试/ws/broadcast接口得出的,不是拍脑袋。技术没有银弹,只有一次次实测出来的边界。

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

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

立即咨询