谁看守队列
它看守想要开战者的队列:保管每一个请求,直到找到对手。它按固定节奏 检查队列,并设有一个最长等待时间,超过之后就会触发一条 备用路径。
Sigildream
Tech Blog
一名玩家按下「进入竞技场」。就在同一刻,几千公里之外,另一个人做出同样的动作。 几秒之后,两人便在同一场实时对决中面对面。就在这几秒里,一场看不见的博弈正在进行:决定 谁对上谁,好让这场较量势均力敌,以及在世界的哪个角落运行这场 对决。
第一幕 · 那四秒
在按下「进入竞技场」这个动作的背后,三个系统像接力赛一样传递接力棒。第一个 看守队列:收下想要开战的请求,并一直保管,直到找到对手。第二个是 配对仲裁者:决定谁对上谁,好让较量势均力敌,并分配一座竞技场。第三个 点亮那座竞技场—一台专用服务器,位于离双方最近的那个角落。把这些角色分开 是有明确理由的:这样每个只做一件事,一旦哪里变慢,立刻就知道该盯这三者中的哪一个。
它看守想要开战者的队列:保管每一个请求,直到找到对手。它按固定节奏 检查队列,并设有一个最长等待时间,超过之后就会触发一条 备用路径。
一个专用服务,每提出一对配对,都通过一次带签名的内部调用 来问询它。它比较两名玩家的实力,准备一次性对局钥匙,并挑选 最近的区域。稳定运行时的占用: 12 MiB 内存与 0.4% CPU ,处理超过 2,800 对配对。
就近部署的微型实例,会在离两名玩家最近的区域点亮一座小竞技场, 托管这场对局,并在结束时立刻关闭。如果已有一座空闲的, 交付不到一秒;否则会在约 5 秒内新建一座。
底层原理。 从前这些只是 Nakama(我们的主服务器)里的
一整块。在压力之下—最坏情况下一万名玩家同时涌入—这一块会吞噬内存,
甚至拖慢其他玩家的登录。我们把它拆成三个独立服务,相同负载下占用内存从
3.08 GiB 降到 1.59 GiB。上方就是它们各自的身份与真实开销。
Nakama 单独一块 → 三个服务 · −1.5 GiB 内存
为什么要拆开? 主服务器擅长那些不赶时间的活儿: 维持会话、保存数据、发送通知。而在重负载下充当配对 仲裁者,并不是它的本行 —在我们的测量里这一点看得很清楚。
促使我们下决心拆分的压力测试揭示了两点好处。第一点是内存:在相同工作负载下,从 3.08 GiB 降到 1.59 GiB。第二点 是让配对规则独立 演进的自由,无需等待主服务器的 更新。
一条新的评分规则、一道针对「注册新号去欺负新手」的 额外防线、另一条通往人机对战的备用路径:只更新那一个服务, 几秒内重启即可。主服务器、数据库和手机上的 应用都原封不动。
第一幕 · 流程,逐秒还原
对玩家来说,这只是几秒钟的等待。在那个转圈背后,三个角色沿着一条精确的接力线 传递接力棒:收下玩家、找到 对手、点亮竞技场、把进场的钥匙交到两人手里。下方这条线 逐秒展示这场接力,并标出每一步可能 花费的时间。
底层原理。 玩家的应用只发出一个请求;其余一切 都在我们的服务器上完成。七个系统沿着一条带有三次 签名交接的路径协同工作—这样没人能冒充别人。
玩家的应用
应用向主服务器建立一个连接。
若出错:会逐次拉长间隔重试;请求进行期间按钮保持锁定,以避免重复点击。
主服务器
请求进入排队。
若出错:超过最长等待时间后,同一请求会转为一场人机对战。
主服务器
一次通往配对服务的、带签名的内部调用。
若出错:若该服务无响应,路径会自动退回一条备用路径,玩家不会看到任何报错。
配对服务
计算评分,并对该对局做排他性分配。
若出错:若存档变慢,就退回备用路径;若该对局已被分配,则重新提出这一对配对。
配对服务
为每名玩家生成一把带签名的钥匙。
若出错:钥匙只在几分钟内有效,并在首次进入竞技场时被消耗;用同一把钥匙的第二次尝试会被拒绝。
就近微型竞技场
在最近的区域点亮一座竞技场—开发环境下则用本地孪生。
若出错:若某个区域吃紧,分配会自动转移到别处,无需我们的代码介入。
玩家应用 → 战斗服务器
应用凭自己的对局钥匙进入竞技场。
若出错:重连协议会处理服务器切换的情况—详见战斗一章。
第二幕 · 势均力敌的对手,而非随机
随手抓来的对手就是一场抛硬币:对强者是无聊, 对新手是折磨。所以配对并不随机。在让两个 人面对面之前,系统会问两个问题:每人有多强,以及有多确信。一名刚注册的玩家和一名歇了 一个月才回归的老手,纸面上可能分数相同,但对 前者我们了解得少得多 — 若把他们当作了解程度 相同来对待,就会产生不平衡的对局。连同 我们自己的不确定度一起衡量,正是这一点把配对 从掷骰子变成了一场真正的 较量。
底层原理。 一个朴素的评分系统只为每名玩家保留一个数字:
他有多强。我们的系统保留三个:我们认为他值多少
(μ)、这个值有多少不确定度
(φ),以及这个值随时间有多不稳定
(σ)。举例来说,当一名歇了一个月的老手,遇上某个
强者用来混在新手里、也许正打着连胜的小号时,
这三个数字都很重要。在这三个数字之上,我们的
系统又加了四项自研修正,每一项都用来堵住回合制
竞技游戏里试图钻评分空子的四种方式
之一。
24 名角色并非人人都赢得一样 轻松:在游戏当前阶段,有些更强、有些更弱。评分会把 这份起步的优势或劣势计入其中,这样在当前阶段 选了较弱角色的玩家,就不会被双重 惩罚。角色之间的对比会不时在后台重新计算; 对局进行时,直接取用已经算好的数值,瞬间完成,不拖慢对局。
如果一名强得多的玩家不停地打远比自己 弱的人,那么他从胜利中获得的分数会少得多, 从失败中失去的分数会多得多。此外,注册 不足一小时的账号会被放进单独的队列,这样 一波这样的账号也不会波及正当游玩的玩家。
接连几场失败会让我们的不确定度
(φ)上升:意思是我们不再那么
确信这名玩家究竟值多少。这会缓冲随后的失败,避免
雪崩式的下滑。这是留住玩家的一道防线:否则,一个糟糕的夜晚就会
变成 200 分的下跌,要花几周才能补回来。
如果一名玩家离开很久,我们对他实力的 确信会松动,但只到某个限度为止 — 这样一名 歇了一个月才回归的老手,在他回来后的第一个 周日,就不会立刻被安排去对上某位顶尖高手。
第二幕 · 让队列动起来的旋钮
找到势均力敌的对手要花时间:搜索 保持得越紧,配对越好 — 但 没人愿意无休止地盯着一个转圈。这部分的全部 调校都在这份张力中反复权衡:等得足够久, 好找到合适的对手,在等待变成烦躁之前放宽搜索, 并且即使成千上万的请求在同一刻涌来, 也让队列继续流动。
底层原理。 掌控这份节奏的, 是我们主服务器上的少数几个数字 — 多久 看一次队列、一个请求保活多久、同时 能撑住多少个。默认设置面对最坏情况 — 数万名 玩家在同一时刻排队 — 保守得过了头。下方这些 数值,才是让队列持续流动、而不是越积越堵的 那一组。
查得越勤,计算负担越重;查得太疏,则会超出向玩家承诺的 3–5 s。
超过这个阈值就转为一场人机对战:这是「继续等待」与「显得卡住」之间的平衡点。
是 10,000 名玩家场景的两倍,为「全员同时」的峰值留出余量。
出厂设置面对 10,000 名玩家,一秒内就会堵住;这些数值扛住了每一次压力测试。
第二幕 · 剖开仲裁者
真正决定谁对上谁的那一块,是一个 自成一体的小服务:仲裁者。每提出一对配对,它就计算 这场对决是否会势均力敌,分配一座竞技场,并把 进场的钥匙交给每一方。它运行起来又快又省, 而这一部分会把它一块块拆开来看。
底层原理。 这是一个专用的小 服务,只有少数几个功能,状态模型精简 到极致。在表象之下,「两个承诺」是 两条技术保证:一局对局的身份只能被 兑现一次;胜者与败者始终恰好绑定在配好对的 那两名玩家身上。下面就来看这个服务对外的样子,以及那两条不变量。
它对外只有少数几个内部功能,并 不公开:用于监控的健康检查、主服务器 请求的配对、人机对战,以及 对局结束时的评分更新。这些全是我们各服务之间的 调用,每一次都经过签名与校验。
两个操作 — 找到配对、为它点亮一座竞技场 — 被合并成了一步,既把耗时减半,也 绝不留下半途而废的对局:要么全做,要么不做。
这一步会一次返回进入竞技场所需的一切:在哪里 开战、为每名玩家准备的一把一次性对局钥匙,以及 若干关于配对质量的数字(两人分数有多接近)。
第三幕 · 杜绝作弊
手机是一块屏幕,不是裁判。当它报出一个 分数、宣称一场胜利、或声明自己是谁时,都不能 仅凭它一面之词就相信。这一部分的整套架构正由此 而来:每条消息都带签名,每局对局都发一次性钥匙,每个 结果都要先在我们的服务器上重新校验,然后 金币或排行榜名次才会变动。就从签名讲起 — 有了它, 即便在我们自己的服务器之间,也能 证明自己确实是本人、而非冒充者,就像一枚谁也 仿不来的火漆封印。
底层原理。 每一次通往 配对服务的内部调用都是一个带签名的请求:签名 随消息一同传送,接收方在采信任何一个字之前,会 先核实它是否真实。讲的是思路,而非 配方。
实际上,每条内部消息都带着一枚加密 签名,同时为发送方与内容作证:哪怕 消息只改动一个字符,签名就对不上,请求 便被拒绝。签名只在极短时间内有效,且无法 重用,因此即便有人设法截获,也 无法把它重放。
第三幕 · 对局的钥匙
配好对之后,两名玩家各拿到一把钥匙 — 不是随便一把,而是恰好只开一扇门、只开一 次、并在几分钟后自行失效的那种。 这跟酒店房卡是同一个道理:只有对的客人才能进 对的房间,住期一过,它就再无用处。这样, 配好对的两名对手 — 别无他人 — 才会落进 同一座竞技场,而那把钥匙也无法被重用去 第二次溜进来。
底层原理。 钥匙是两枚带签名 的令牌,每席一枚,只在几分钟内有效,不可续期。 让它们不仅仅是「存在」、而是「安全」的,是它们 被「消耗」的方式:两人中任一方第一次进入竞技场时, 钥匙会在一个不可分割的操作中被标记为已花费, 之后的任何尝试都会被拒绝。就是这个思路。
每把钥匙只说必要的、绝不多说:它属于 谁、对哪局对局有效、用哪个角色。两个席位之间的 区分,以及对手的身份,都不存在于钥匙之中。 当一名玩家出现在竞技场时,战斗服务器先核实 签名,再在一个不可分割的操作中「花掉」这把 钥匙:从那一刻起它便作废,任何人再拿同一把钥匙都会 被拒绝。这正是杜绝「重玩同一局两次以翻倍 领奖」的关键。
第三幕 · 结果,以及我们堵上的漏洞
对局结束后,不能仅凭玩家一面之词就相信 结果:裁定会先在我们的服务器上被重建并重新校验, 然后才会有哪怕一枚金币易手、或哪怕一个 排行榜名次挪动。也只有到那时,同一个结果才会 同时化作三样东西 — 给胜者的奖励、 排行榜的更新,以及一行数据,我们用它来训练 那个终有一天将以冠军水准对战的人工智能。
底层原理。 一局结束时,战斗服务器并不 自行发放奖励:它从不触碰钱包 或排行榜。它为裁定签名,并用两条各自独立 的带签名消息,发给两个有权让其生效的 服务 — 一个入账金币并更新排行榜,另一个 更新评分。在动任何东西之前,各自都会 重做自己的检查。
只有当所有这些检查都通过 — 再加上另一层 对外保密、用于核验消息流转的安全机制 一并通过 — 结果才会 生效。
对局结束的裁定以一条带签名消息的形式传送,接收方 不会凭一面之词就采信:它会核对签名是否真实、 是否来自一台获授权的战斗服务器、是否 新近产生、以及是否此前未曾见过。只有当所有这些 检查都通过,结果才会生效。
第四幕 · 战斗在玩家身边运行
一场对决并不在地球另一端某个庞大的计算中心里 运行:它运行在一座独立的小竞技场里,这座竞技场 诞生于离两名玩家最近的区域,托管这场对局,并在 结束后立刻消失。这是一套先进的、就近部署的 战斗微型实例系统 — 许许多多独立的小竞技场, 在需要的地方、需要的时候出现。 这样,无论对战双方身在何处,从米兰到东京, 对决都灵敏而公平。
底层原理。 这是一套由我们自己构建和运维的 分布式架构,跑在真正的生产级云基础设施上, 带来四项实实在在的好处。
战斗负载是我们基础设施中最难预测 的部分:在同一个 24 小时里,高峰时段欧洲的 需求可以是亚洲低谷时段的四倍。 一套按区域自行开启与关闭的微型竞技场系统,能 跟上这道波峰波谷,而无需照着峰值养一支 空转的机队。
靠近玩家。 竞技场在离两名玩家最近的 区域点亮,而不是在全球某一个点上:指令走的路更短, 即使跨越不同大洲,对决依然灵敏而 公平。
永不停摆。 若某个区域陷入 困难,对决会自动挪到邻近区域,无人察觉, 也不丢失正在进行的对局。
自行扩缩。 竞技场的数量随全球需求 实时起落:人人都玩时数以千计,空档时段几乎一座 不留,没有一丝闲置容量空转。
系统的一部分,而非附件。 它跑在 真正的生产级云基础设施上,并与平台其余部分 深度交织:谁对上谁的决定、对局的推进、 以及最终的结算,始终掌握在我们自己手里。
每个区域里都有一小批竞技场始终点着、随时 就绪;我们的系统挑出一座,并拿到应把两名玩家 送往何处的地址;竞技场迎入对战双方并托管 对局;对局结束时它为裁定签名,奖励服务 入账金币并更新排行榜,随后竞技场 回到就绪的那一批里 — 多余的 则关闭。
在开发环境里,同一套微型竞技场系统跑在一个本地孪生中: 它说着完全相同的语言,并在主服务器 过载时点亮战斗竞技场。开发与生产走同一份 代码,意味着规模问题会暴露在笔记本上, 而不是周日晚上。这个孪生在投入使用前也 经过了一次安全审计,对外暴露也压到了 最小。
第四幕 · 数字,实地测量
这一部分的每个数字都来自负载下的实测, 而非估算。目标是精确说明:把这套系统往上推时它 能扛住什么,又从哪里开始出现裂纹 — 哪怕答案并不好看:只用挂靠在真实跑过的 负载测试上的数字,绝不在测量结果只是千级时 报出「百万级」的数字。
底层原理。 以下是实测的上限,逐行 列出 — 持续负载、最坏情况、把配对仲裁者 从主服务器分离之后的收益 — 每个数字旁边都附上出处;凡是某个数字属于设计 目标、而非已经拿到的测量值,都会 明说。
本章中关于容量的每一句话,都 挂靠在我们内部文档所记录的一次负载测试上。 下方的数字来自负载下的实测, 而非估算。
战斗服务器约 60 MiB,主服务器约 3 GiB,全部为真人之间的配对。稳定,且已在使用中。
内部基准负载测试
数据库和配对服务立刻堵塞。4 月 23 日的 报告记录了约 2,000 局成功对局,以及约 4,500 次 失败后立即重试的配对尝试;此前草稿中引用的 稳态划分「8,000 在排队 + 2,000 已启动」 是一个设计估算,而非那份报告的 测量值。
内部负载报告,2026-04-23(A 阶段)
主服务器内存 1.59 GiB 对比起步的 3.08 GiB (−48%)。配对服务 12 MiB、0.4% CPU。超过 2,800 对。零签名错误。
内部负载报告,2026-04-24(A′ 阶段)
32 到 64 个就绪进程。出厂设置在 10,000 名玩家峰值期间,会被针对本地实例的 对局连发拖垮。
主服务器配置,审计之后
如今是一次一条的直接写入;能扛住高得多流量的 批量写入方案是下一阶段的目标, 尚未发布。
评分存档
在单台 Mac 上模拟负载的工具,受限于机器本身, 大约到 2,800 个模拟玩家就停住了。为了在不撞上这个 天花板的前提下做到 10,000,我们再用一台机器, 装着项目的一份副本,通过局域网把更多模拟玩家推向那台 Mac。 我们就是这样真正验证了「10,000 名玩家」的 场景:上方表格中每一个 10,000 的数字,都来自由那 第二台机器推动的一次测试。
第四幕 · 当队列停下时
任何系统迟早都会卡住。把成熟的 服务和脆弱的服务区分开来的,不是从不 出故障,而是提前知道它怎样出故障,并且 早已备好让它重新运转的那一手。
底层原理。 真正要紧的不 是可能故障的清单,而是故障出现时会发生什么: 多数情况下系统会自行恢复,而任何情况下,一次 故障都不会留下一个错误的评分。
几乎所有预料之内的卡顿都不需要人工 介入。若某个区域或某个部件陷入困难,系统 会自动退回备用路径、继续运转:对决被 改道到邻近区域,一次找不到任何人的搜索 退而求其次转为人机对战,一对被拒的配对被 重新提出。对玩家来说,结果是一场照常开始的 对决,而不是一个报错。
这是所有保证中最重要的一条。托管 对决的服务器无权触碰评分或金币:它 只能为一份裁定签名并提交。那份裁定只有在 有权的服务各自重新校验之后,才会成为现实。这样, 若某台服务器在对局中途宕机,或某条 消息半路丢失,其结果永远是「什么都 不发生」,而绝不是「写下一个错误的 评分」:穿过重重故障,评分依旧保持一致、 可靠。
这是源自设计的韧性:一次故障最坏的结局, 是一次干净的停摆、可以从中重启,而不是一份 要靠人工修复的损坏数据。