2026西湖龙井茶官网DTC发售:茶农直供,政府溯源防伪到农户家
我们实际要解决的问题
我们不仅仅是在追求 p99 延迟;我们是在解决事件模型与寻宝逻辑之间的根本性不匹配。每一轮寻宝都会产生数千个微事件:玩家加入、物品拾取、时间更新、排行榜重新计算以及实时通知。Node.js 的事件循环在背压下不堪重负。BullMQ 工作器阻塞在 Redis 发布订阅上,这并不是因为网络延迟,而是因为 Node.js 的单线程事件循环无法跟上传入事件的速率。Redis 服务器本身运行良好——CPU 使用率为 12%,内存使用率为 68%,没有发生键驱逐。瓶颈不在于队列或数据存储。而在于运行时。
我使用 0x 添加了调试追踪,发现 78% 的 CPU 时间花费在 uv__io_poll 上,即 epoll/select 的包装器。Node.js 进程花费在等待事件上的时间比处理事件的时间还要多。而且由于 BullMQ 使用 Redis 流,每次发布和消费都是一次网络往返。当我们每秒发布 47,000 个事件时,从美国东部一区到 Redis 集群的 250 微秒往返时间累积起来造成了显著影响。p99 延迟随着并发玩家数量的平方根增长。在 5,000 名玩家时,延迟为 80 毫秒。在 10,000 名玩家时,延迟达到 2.3 秒。系统并非线性扩展。而是急剧恶化。
我们最初的尝试(以及失败原因)
我们尝试对 BullMQ 工作器进行水平扩展。我们在简单队列服务队列后面启动了 8 个工作器。简单队列服务的吞吐量表现良好——持续保持每秒 50,000 个事件——但 BullMQ 的 Redis 背压变成了一个分布式锁的噩梦。工作器争抢相同的 Redis 键范围,而 Redis 发布订阅的扇出效应在 Node.js 事件循环上造成了惊群效应。我们在 XREADGROUP 中观察到锁竞争,超时时间为 200 毫秒。我们尝试将 Redis 流分片为 16 个 shard。分片不平衡的问题非常严重——某些分片承受的负载是其他的三倍。我们尝试将 Node.js 升级到 20 版本。行为依旧相同。我们甚至尝试使用 ioredis 进行连接池管理,但根本性的不匹配仍然存在:Node.js 是一个伪装成事件驱动运行时的流处理器。
随后,我们尝试对事件进行去噪——过滤掉重复的玩家操作、压缩负载、批量处理事件。这帮助我们将数据量减少了 38%,但 p99 延迟仍然随着玩家数量的增加而上升。问题不在于事件数量。而在于运行时无法处理我们所需的并发模型。
架构决策
我们必须接受 Node.js 是制约因素。不是 Redis。不是 BullMQ。而是运行时本身。我们用 Go 语言启动了一个原型项目。使用 go-redis 配合流式消费者组,我们在相同的 c5.4xlarge 实例上实现了每秒 320,000 个事件的处理能力,且 p99 延迟低于 100 毫秒。pprof 的内存分配概况显示垃圾回收压力为每秒 1.2 MiB,与 Node.js 的每秒 47 MiB 相比微不足道。但 Go 并不是唯一的选择。我们也测试了 Rust。
我们使用 Tokio、通过 redis-rs 访问 Redis 流以及一个手工编写的事件路由器,构建了一个最小的 Rust 原型。第一个版本使用 std::thread 进行并发处理,但这导致在高负载下出现线程饥饿。我们切换到 Tokio,配置了 8 个工作任务和单个支持多路复用的 Redis 连接。空闲时的常驻内存占用为 8.7 MiB,在每秒 100,000 个事件的负载下峰值达到 42 MiB。Tokio 运行时的窃取工作调度器意味着没有空闲线程,也没有因等待事件而浪费的 CPU 资源。p95 事件延迟为 18 毫秒,p99 为 47 毫秒。我们进行了长达 12 小时的负载测试,模拟 20,000 名并发玩家,每分钟产生 210 万个事件。零垃圾回收暂停,零内存泄漏,零崩溃。
架构决策不仅仅关乎编程语言。更关乎并发模型。我们从集中式事件总线(Redis 流)迁移到了
免责声明:本文内容来自互联网,该文观点不代表本站观点。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请到页面底部单击反馈,一经查实,本站将立刻删除。