Sigildream Tech Blog

一场对决如何诞生,逐秒还原

一名玩家按下「进入竞技场」。就在同一刻,几千公里之外,另一个人做出同样的动作。 几秒之后,两人便在同一场实时对决中面对面。就在这几秒里,一场看不见的博弈正在进行:决定 对上谁,好让这场较量势均力敌,以及在世界的哪个角落运行这场 对决。

Sigilwake 是什么。 一款回合制角色扮演对战游戏,黑白漫画风格,移动端优先,多平台 (Web、Android、iOS、macOS、Windows),目前处于验证阶段。一对一对决:两名战斗者,各有四个技能, 电影级动画,对局可玩、可实时观看、可回放。阵容共有 24 名角色,个个各具特色。
#GameAI   #GameBalance   #DevLog
从排队票到实时对局 — 配对枢纽的运转 左侧一列配对排队票被吸入中央的配对枢纽。枢纽向外发出配对好的对局令牌, 连向三台区域战斗服务器,分别标注 EU-WEST、US-EAST 和 APAC。 细小的光点沿着进出路径移动,指示方向以及配对枢纽热路径的低延迟。 排队票 · 队列 配对服务 · 撮合 战斗 · 区域 排队票 · Varro μ 1502 φ 68 tier 3 · EU 排队票 · Nyx μ 1459 φ 72 tier 3 · EU 排队票 · Boreus μ 1611 φ 54 tier 4 · US 配对 12 MiB · 0.4% CPU 签名 欧洲竞技场 warm · 200 ms 欧洲区域 美国竞技场 warm · 210 ms 美国区域 亚洲竞技场 cold · ~5 s 亚洲区域 点击 → 首回合 3–5 s · 7 个系统 · 3 次签名交接
排队票 → 配对服务 → 战斗分配,单一请求路径。 p50 ∼ 3.6 s

第一幕 · 那四秒

三个角色接力,把玩家送进竞技场。

在按下「进入竞技场」这个动作的背后,三个系统像接力赛一样传递接力棒。第一个 看守队列:收下想要开战的请求,并一直保管,直到找到对手。第二个是 配对仲裁者:决定谁对上谁,好让较量势均力敌,并分配一座竞技场。第三个 点亮那座竞技场—一台专用服务器,位于离双方最近的那个角落。把这些角色分开 是有明确理由的:这样每个只做一件事,一旦哪里变慢,立刻就知道该盯这三者中的哪一个。

主服务器

谁看守队列

它看守想要开战者的队列:保管每一个请求,直到找到对手。它按固定节奏 检查队列,并设有一个最长等待时间,超过之后就会触发一条 备用路径。

配对服务

配对仲裁者

一个专用服务,每提出一对配对,都通过一次带签名的内部调用 来问询它。它比较两名玩家的实力,准备一次性对局钥匙,并挑选 最近的区域。稳定运行时的占用: 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。第二点 是让配对规则独立 演进的自由,无需等待主服务器的 更新。

一条新的评分规则、一道针对「注册新号去欺负新手」的 额外防线、另一条通往人机对战的备用路径:只更新那一个服务, 几秒内重启即可。主服务器、数据库和手机上的 应用都原封不动。

第一幕 · 流程,逐秒还原

等待的转圈背后发生了什么。

对玩家来说,这只是几秒钟的等待。在那个转圈背后,三个角色沿着一条精确的接力线 传递接力棒:收下玩家、找到 对手、点亮竞技场、把进场的钥匙交到两人手里。下方这条线 逐秒展示这场接力,并标出每一步可能 花费的时间。

底层原理。 玩家的应用只发出一个请求;其余一切 都在我们的服务器上完成。七个系统沿着一条带有三次 签名交接的路径协同工作—这样没人能冒充别人。

配对流程时间线 — 从客户端点击到首回合 一条七步的水平时间线,展示从玩家客户端点击到第一帧 战斗画面的实际耗时路径。各步:1 客户端点击(~40 ms)、 2 排队(最长等待 15 s)、3 提出配对(~90 ms)、4 评分与身份校验(~30 ms)、5 生成对局钥匙(~2 ms)、6 服务器分配(~200 ms warm)、7 客户端连入战斗(~120 ms)。一个移动的光点 沿时间线行进,指示请求方向;一条阴影 排队等待带突出第 2 步,大部分耗时都 累积在这里。 实际耗时 · p50 目标预算 · 非 SLO 承诺 端到端目标 ≈ 3.6 s 排队等待 1 客户端点击 应用 ~40 ms 入队 2 排队 每 1 s · 15 次 0 – 15 s 直到配对 3 提出配对 签名 → 配对服务 ~90 ms 撮合 4 评分与身份 读取存档 ~30 ms 一次性钥匙 5 对局钥匙 每席一把 ~2 ms 短时有效 6 服务器分配 warm pool ~200 ms cold:~5 s 7 首回合 应用 → 战斗 ~120 ms 校验钥匙 主服务器 配对服务 就近微型竞技场 + 战斗
第 2 步的排队等待带,正是大部分耗时 累积的地方—当队里排着足够多的人时,其余六步 加在一起也稳稳保持在一秒 以内。
1

玩家点击

玩家的应用

应用向主服务器建立一个连接。

~40 ms

若出错:会逐次拉长间隔重试;请求进行期间按钮保持锁定,以避免重复点击。

2

等待队列

主服务器

请求进入排队。

0–15 s 直到配对成功

若出错:超过最长等待时间后,同一请求会转为一场人机对战。

3

提出配对

主服务器

一次通往配对服务的、带签名的内部调用。

~90 ms

若出错:若该服务无响应,路径会自动退回一条备用路径,玩家不会看到任何报错。

4

评分与身份

配对服务

计算评分,并对该对局做排他性分配。

~30 ms

若出错:若存档变慢,就退回备用路径;若该对局已被分配,则重新提出这一对配对。

5

对局钥匙

配对服务

为每名玩家生成一把带签名的钥匙。

~2 ms

若出错:钥匙只在几分钟内有效,并在首次进入竞技场时被消耗;用同一把钥匙的第二次尝试会被拒绝。

6

分配

就近微型竞技场

在最近的区域点亮一座竞技场—开发环境下则用本地孪生。

~200 ms 热启动 · 冷启动约 5 s

若出错:若某个区域吃紧,分配会自动转移到别处,无需我们的代码介入。

7

进入竞技场

玩家应用 → 战斗服务器

应用凭自己的对局钥匙进入竞技场。

~120 ms

若出错:重连协议会处理服务器切换的情况—详见战斗一章。

第二幕 · 势均力敌的对手,而非随机

一名玩家值多少 — 以及我们有多确信。

随手抓来的对手就是一场抛硬币:对强者是无聊, 对新手是折磨。所以配对并不随机。在让两个 人面对面之前,系统会问两个问题:每人有多强,以及有多确信。一名刚注册的玩家和一名歇了 一个月才回归的老手,纸面上可能分数相同,但对 前者我们了解得少得多 — 若把他们当作了解程度 相同来对待,就会产生不平衡的对局。连同 我们自己的不确定度一起衡量,正是这一点把配对 从掷骰子变成了一场真正的 较量。

底层原理。 一个朴素的评分系统只为每名玩家保留一个数字: 他有多强。我们的系统保留三个:我们认为他值多少 (μ)、这个值有多少不确定度 (φ),以及这个值随时间有多不稳定 (σ)。举例来说,当一名歇了一个月的老手,遇上某个 强者用来混在新手里、也许正打着连胜的小号时, 这三个数字都很重要。在这三个数字之上,我们的 系统又加了四项自研修正,每一项都用来堵住回合制 竞技游戏里试图钻评分空子的四种方式 之一。

两名玩家的评分随时间如何变化 一张评分漂移图。x 轴是已玩对局数,从 0 到 40。y 轴是评分(mu),从 1300 到 1700。图中画出两名玩家:玩家 A 从 1500 起步,金色;玩家 B 是回归老手,从 1620 起步,紫色。每条线外包一条评分偏差(phi) 阴影带,表示不确定度。四个编号 拐点标注我们评分系统的四项调整: 1 第 8 局时按角色归一化胜率,2 第 14 局时按 tier 施加反小号 gate,3 约第 22 局时进入连败 阻尼,4 第 32 局时触及因不活跃产生的 RD 衰减 下限。一条位于 mu 1500 的虚线基线标记全局评分中位数。 1700 1600 1500 1400 1300 0 5 10 15 20 25 30 35 40 已玩对局 评分(μ) 1 角色 WR 归一化 2 反小号 gate 3 连败阻尼 4 RD 下限 玩家 A · 新账号 · φ 收窄 玩家 B · 回归老手 · φ 较宽 → 触及下限 基线 μ = 1500
两名玩家,四十局对局,我们评分系统的四项 修正。玩家 A 的不确定度带随着系统对他的实力越来越 有把握而收窄;玩家 B 的那条则一直较宽,直到 不活跃时间把它止步在一个最小值。每一项修正 都是曲线上一个编号的点。

1. 角色不会被算两次

24 名角色并非人人都赢得一样 轻松:在游戏当前阶段,有些更强、有些更弱。评分会把 这份起步的优势或劣势计入其中,这样在当前阶段 选了较弱角色的玩家,就不会被双重 惩罚。角色之间的对比会不时在后台重新计算; 对局进行时,直接取用已经算好的数值,瞬间完成,不拖慢对局。

2. 伪装成新手的强者

如果一名强得多的玩家不停地打远比自己 弱的人,那么他从胜利中获得的分数会少得多, 从失败中失去的分数会多得多。此外,注册 不足一小时的账号会被放进单独的队列,这样 一波这样的账号也不会波及正当游玩的玩家。

3. 连败不会一沉到底

接连几场失败会让我们的不确定度 (φ)上升:意思是我们不再那么 确信这名玩家究竟值多少。这会缓冲随后的失败,避免 雪崩式的下滑。这是留住玩家的一道防线:否则,一个糟糕的夜晚就会 变成 200 分的下跌,要花几周才能补回来。

4. 久别之后的回归

如果一名玩家离开很久,我们对他实力的 确信会松动,但只到某个限度为止 — 这样一名 歇了一个月才回归的老手,在他回来后的第一个 周日,就不会立刻被安排去对上某位顶尖高手。

第二幕 · 让队列动起来的旋钮

在放宽搜索之前该等多久。

找到势均力敌的对手要花时间:搜索 保持得越紧,配对越好 — 但 没人愿意无休止地盯着一个转圈。这部分的全部 调校都在这份张力中反复权衡:等得足够久, 好找到合适的对手,在等待变成烦躁之前放宽搜索, 并且即使成千上万的请求在同一刻涌来, 也让队列继续流动。

底层原理。 掌控这份节奏的, 是我们主服务器上的少数几个数字 — 多久 看一次队列、一个请求保活多久、同时 能撑住多少个。默认设置面对最坏情况 — 数万名 玩家在同一时刻排队 — 保守得过了头。下方这些 数值,才是让队列持续流动、而不是越积越堵的 那一组。

多久看一次队列

1 秒

查得越勤,计算负担越重;查得太疏,则会超出向玩家承诺的 3–5 s。

一个请求保活多久

~15 秒

超过这个阈值就转为一场人机对战:这是「继续等待」与「显得卡住」之间的平衡点。

同时能撑住多少请求

20,000

是 10,000 名玩家场景的两倍,为「全员同时」的峰值留出余量。

预备多少个内部进程

32 到 64

出厂设置面对 10,000 名玩家,一秒内就会堵住;这些数值扛住了每一次压力测试。

第二幕 · 剖开仲裁者

配对仲裁者,以及它绝不违背的两个承诺。

真正决定谁对上谁的那一块,是一个 自成一体的小服务:仲裁者。每提出一对配对,它就计算 这场对决是否会势均力敌,分配一座竞技场,并把 进场的钥匙交给每一方。它运行起来又快又省, 而这一部分会把它一块块拆开来看。

底层原理。 这是一个专用的小 服务,只有少数几个功能,状态模型精简 到极致。在表象之下,「两个承诺」是 两条技术保证:一局对局的身份只能被 兑现一次;胜者与败者始终恰好绑定在配好对的 那两名玩家身上。下面就来看这个服务对外的样子,以及那两条不变量。

配对仲裁者,以及它绝不违背的两个承诺 顶部,一对提出的配对——两名玩家——进入中央的一个 节点,也就是仲裁者,它校验这场对决是否势均力敌, 分配一座竞技场,并给每人一把钥匙。底部,两个方框 展示这两个承诺。第一个:一局对局的身份在一步之内 从「开启」变为「已消费」,任何重复的尝试都会被弹回、 被拒绝。第二个:只有配好对的两名玩家能进入竞技场, 而一个外人会被拒绝。 第二幕 · 仲裁者 两个承诺 · 绝不违背 提出的配对 仲裁者 竞技场 · 钥匙 玩家 A 寻找对手中 玩家 B 寻找对手中 仲裁者 公平 · 竞技场 · 钥匙 已分配竞技场 每名玩家一把钥匙 钥匙 A 钥匙 B 仲裁者绝不违背的两个承诺 1 一局对局的身份 只能兑现一次 开启 身份可用 仅一步 已消费 重新提出 · 重用钥匙 · 崩溃后恢复 弹回 — 被拒绝 不动金币,不写评分 2 胜者与败者 系于配好对的两名玩家 竞技场 同一局对局 获准进入 玩家 A A 玩家 B B 外人 ? 无人能顶替他们的位置
仲裁者校验是否势均力敌、分配竞技场并交出钥匙 — 绝不违背这两个承诺。 一局对局,一个身份,两名玩家

这个服务做什么

它对外只有少数几个内部功能,并 不公开:用于监控的健康检查、主服务器 请求的配对、人机对战,以及 对局结束时的评分更新。这些全是我们各服务之间的 调用,每一次都经过签名与校验。

服务绝不违背的两个承诺

  1. 一局对局的身份,在整个平台上只能 兑现一次。 每局对局都有唯一的身份,它在一个不可分割的 操作中,从「开启」变为「已消耗」。 之后的任何尝试 — 重新提出这一对、重用一把 钥匙、崩溃后的恢复循环 — 都会被立刻拒绝:不动一枚 金币,不写一分评分,不开一个 会话。
  2. 胜者与败者始终系于配好对的 那两名玩家。 出现在竞技场的人,必须是当初 为这局配对的两人之一,同样的校验 也适用于每一次评分更新。没有别人能 顶替他们的位置。

配对与分配,一步完成

两个操作 — 找到配对、为它点亮一座竞技场 — 被合并成了一步,既把耗时减半,也 绝不留下半途而废的对局:要么全做,要么不做。

这一步会一次返回进入竞技场所需的一切:在哪里 开战、为每名玩家准备的一把一次性对局钥匙,以及 若干关于配对质量的数字(两人分数有多接近)。

第三幕 · 杜绝作弊

一个服务如何证明自己确实是它本人。

手机是一块屏幕,不是裁判。当它报出一个 分数、宣称一场胜利、或声明自己是谁时,都不能 仅凭它一面之词就相信。这一部分的整套架构正由此 而来:每条消息都带签名,每局对局都发一次性钥匙,每个 结果都要先在我们的服务器上重新校验,然后 金币或排行榜名次才会变动。就从签名讲起 — 有了它, 即便在我们自己的服务器之间,也能 证明自己确实是本人、而非冒充者,就像一枚谁也 仿不来的火漆封印。

底层原理。 每一次通往 配对服务的内部调用都是一个带签名的请求:签名 随消息一同传送,接收方在采信任何一个字之前,会 先核实它是否真实。讲的是思路,而非 配方。

带签名的握手 — 冒充者无法伪造的消息 两个并排的节点:左侧是战斗服务器,右侧 是我们的服务器。一条消息以信封表示,沿 一条虚线连接从左向右移动;行至 中途,信封上出现一枚封印,也就是 签名。抵达时,接收方显示一个对勾,表示签名 已通过校验。下方有一条冒充消息,以淡色、 不同颜色绘制,试图靠近,但在抵达接收方 之前被一个叉号拒绝。 第三幕 · 签名 冒充者无法伪造 战斗 服务器 生成并签名消息 我们的 服务器 信任前先校验签名 消息 签名 看到的人也无法复制 已校验 冒充者被拒
战斗服务器在消息上盖下一枚签名;我们的 服务器在其抵达时核对,只有相符才信任。一条 没有有效签名的消息 — 一个冒充者 — 会在被 采信之前就被拒绝。

实际上,每条内部消息都带着一枚加密 签名,同时为发送方与内容作证:哪怕 消息只改动一个字符,签名就对不上,请求 便被拒绝。签名只在极短时间内有效,且无法 重用,因此即便有人设法截获,也 无法把它重放。

第三幕 · 对局的钥匙

为对的两名玩家、打开对的那扇门的一次性钥匙。

配好对之后,两名玩家各拿到一把钥匙 — 不是随便一把,而是恰好只开一扇门、只开一 次、并在几分钟后自行失效的那种。 这跟酒店房卡是同一个道理:只有对的客人才能进 对的房间,住期一过,它就再无用处。这样, 配好对的两名对手 — 别无他人 — 才会落进 同一座竞技场,而那把钥匙也无法被重用去 第二次溜进来。

底层原理。 钥匙是两枚带签名 的令牌,每席一枚,只在几分钟内有效,不可续期。 让它们不仅仅是「存在」、而是「安全」的,是它们 被「消耗」的方式:两人中任一方第一次进入竞技场时, 钥匙会在一个不可分割的操作中被标记为已花费, 之后的任何尝试都会被拒绝。就是这个思路。

一次性钥匙 — 一扇门,只开一次 一张示意图,中偏左有一个对局核心,向外发出两把 对局钥匙,每名玩家一把。金色钥匙飞向 玩家 A 的竞技场门,紫色飞向玩家 B 的门。 钥匙进入自己的门时,门亮起绿色并开启。 每把钥匙只能开一次门:第二次尝试时,同一把钥匙显示为 已使用且过期,变灰并被划掉。两个移动光点 显示发出的方向;即使关闭动画,其余部分 依然清晰可读。 第三幕 · 对局的钥匙 一次性 · 一门一次 对局 钥匙 对局为每名玩家发一把钥匙 玩家 A 竞技场 已开启 玩家 B 竞技场 已开启 第 2 次 第 2 次 已使用 钥匙过期 已使用 钥匙过期 玩家 A 钥匙 玩家 B 钥匙 门已开 钥匙已用/过期
每局对局为每名玩家发一把一次性钥匙:只把 自己竞技场的门开一次,随后过期。再拿同一把 钥匙也开不了任何门 — 会显示为已使用。

每把钥匙只说必要的、绝不多说:它属于 谁、对哪局对局有效、用哪个角色。两个席位之间的 区分,以及对手的身份,都不存在于钥匙之中。 当一名玩家出现在竞技场时,战斗服务器先核实 签名,再在一个不可分割的操作中「花掉」这把 钥匙:从那一刻起它便作废,任何人再拿同一把钥匙都会 被拒绝。这正是杜绝「重玩同一局两次以翻倍 领奖」的关键。

第三幕 · 结果,以及我们堵上的漏洞

一局打完的对决如何变成奖励、排行榜与训练数据 — 且无人能作假。

对局结束后,不能仅凭玩家一面之词就相信 结果:裁定会先在我们的服务器上被重建并重新校验, 然后才会有哪怕一枚金币易手、或哪怕一个 排行榜名次挪动。也只有到那时,同一个结果才会 同时化作三样东西 — 给胜者的奖励、 排行榜的更新,以及一行数据,我们用它来训练 那个终有一天将以冠军水准对战的人工智能。

底层原理。 一局结束时,战斗服务器并不 自行发放奖励:它从不触碰钱包 或排行榜。它为裁定签名,并用两条各自独立 的带签名消息,发给两个有权让其生效的 服务 — 一个入账金币并更新排行榜,另一个 更新评分。在动任何东西之前,各自都会 重做自己的检查。

只有当所有这些检查都通过 — 再加上另一层 对外保密、用于核验消息流转的安全机制 一并通过 — 结果才会 生效。

结果核对 — 从战斗到主服务器与配对服务 一张三节点的结果核对流程图。左侧,一个战斗容器结束一局对局,并行发出两条已签名消息:一条走上支,通向我们的主服务器,负责发放奖励并更新排行榜;另一条走下支,通向配对仲裁者,安全地更新玩家评分。两条路径最终汇入一份共享且持久的对局记录。沿每条路径都有一个图标,表示该环节已在我们的服务器上签名并校验。 战斗 · 对局结束 主服务器 · 奖励 + 排行榜 配对服务 · 更新评分 数据库 · 持久记录 战斗服务器 对局结束 · 权威裁定 签名 2 封消息 签名 已签名消息 4 GATE 有效性校验 防重放检查 主服务器 已签名裁定 金币 + 排行榜 每局仅一次 3 GATE 结果检查 一次性钥匙 配对服务 更新评分 更新评分 批量 · B 阶段 数据库 评分 + 钱包 仅追加 批量更新 此路径上的安全检查 结果已校验 一次性钥匙 大小限制 防重放 受保护访问 频率限制
战斗服务器并行签名两条消息 — 一条通向为金币入账的服务,一条通向 更新评分的服务。每条支路都有各自的检查,而 战斗从不直接触碰钱包或排行榜。

从战斗服务器到平台的其余部分

对局结束的裁定以一条带签名消息的形式传送,接收方 不会凭一面之词就采信:它会核对签名是否真实、 是否来自一台获授权的战斗服务器、是否 新近产生、以及是否此前未曾见过。只有当所有这些 检查都通过,结果才会生效。

第四幕 · 战斗在玩家身边运行

在两名玩家身旁诞生的微型竞技场,遍及世界各地。

一场对决并不在地球另一端某个庞大的计算中心里 运行:它运行在一座独立的小竞技场里,这座竞技场 诞生于离两名玩家最近的区域,托管这场对局,并在 结束后立刻消失。这是一套先进的、就近部署的 战斗微型实例系统 — 许许多多独立的小竞技场, 在需要的地方、需要的时候出现。 这样,无论对战双方身在何处,从米兰到东京, 对决都灵敏而公平。

底层原理。 这是一套由我们自己构建和运维的 分布式架构,跑在真正的生产级云基础设施上, 带来四项实实在在的好处。

  • 延迟 竞技场在离双方最近的区域点亮,因此指令走的路很短,对决即刻响应。
  • 韧性 若某个区域出问题,对决会自动改道到邻近区域 — 不丢一局,不停一次。
  • 弹性伸缩 容量实时跟随全球需求,峰值时数千座竞技场,平静时几乎一座不留,毫不浪费。
  • 深度集成 这一切与平台其余部分深度交织,而非搁在一旁 — 是一套扎实的分布式系统,而不是一页幻灯片。

战斗负载是我们基础设施中最难预测 的部分:在同一个 24 小时里,高峰时段欧洲的 需求可以是亚洲低谷时段的四倍。 一套按区域自行开启与关闭的微型竞技场系统,能 跟上这道波峰波谷,而无需照着峰值养一支 空转的机队。

全球布点 — 玩家身边的临时竞技场 一张抽象世界地图,包含三个通用区域:美洲、欧洲、 亚洲。每个区域里,一对玩家旁边有一座 就近点亮的竞技场,用一个跳动的金色节点 表示;不远处一个熄灭的灰色节点,代表一局结束后 已回收的竞技场。底部一条带子展示一小批 已就绪的竞技场。光点指示活跃的竞技场 以及与玩家的连接;开启减少动态偏好时,所有 动画都会关闭。 全球布点 · 竞技场就近诞生于玩家身旁 无自有数据中心 美洲 玩家 就近竞技场 活跃 已回收 欧洲 玩家 就近竞技场 活跃 已回收 亚洲 玩家 就近竞技场 活跃 已回收 热池 始终就绪、从不关闭的竞技场 就绪 就绪 就绪 就近竞技场 已回收 就绪 玩家
每座竞技场都在离两名玩家最近的区域诞生,托管这场 对决,并在对局结束后回收;一小批竞技场始终 保持就绪,以缩短等待。

为什么要分布式运行

靠近玩家。 竞技场在离两名玩家最近的 区域点亮,而不是在全球某一个点上:指令走的路更短, 即使跨越不同大洲,对决依然灵敏而 公平。

永不停摆。 若某个区域陷入 困难,对决会自动挪到邻近区域,无人察觉, 也不丢失正在进行的对局。

自行扩缩。 竞技场的数量随全球需求 实时起落:人人都玩时数以千计,空档时段几乎一座 不留,没有一丝闲置容量空转。

系统的一部分,而非附件。 它跑在 真正的生产级云基础设施上,并与平台其余部分 深度交织:谁对上谁的决定、对局的推进、 以及最终的结算,始终掌握在我们自己手里。

一座竞技场的一生,一口气讲完

每个区域里都有一小批竞技场始终点着、随时 就绪;我们的系统挑出一座,并拿到应把两名玩家 送往何处的地址;竞技场迎入对战双方并托管 对局;对局结束时它为裁定签名,奖励服务 入账金币并更新排行榜,随后竞技场 回到就绪的那一批里 — 多余的 则关闭。

与生产环境完全一致的本地孪生

在开发环境里,同一套微型竞技场系统跑在一个本地孪生中: 它说着完全相同的语言,并在主服务器 过载时点亮战斗竞技场。开发与生产走同一份 代码,意味着规模问题会暴露在笔记本上, 而不是周日晚上。这个孪生在投入使用前也 经过了一次安全审计,对外暴露也压到了 最小。

第四幕 · 数字,实地测量

到底能扛住多少,用真实数据测量。

这一部分的每个数字都来自负载下的实测, 而非估算。目标是精确说明:把这套系统往上推时它 能扛住什么,又从哪里开始出现裂纹 — 哪怕答案并不好看:只用挂靠在真实跑过的 负载测试上的数字,绝不在测量结果只是千级时 报出「百万级」的数字。

底层原理。 以下是实测的上限,逐行 列出 — 持续负载、最坏情况、把配对仲裁者 从主服务器分离之后的收益 — 每个数字旁边都附上出处;凡是某个数字属于设计 目标、而非已经拿到的测量值,都会 明说。

本章中关于容量的每一句话,都 挂靠在我们内部文档所记录的一次负载测试上。 下方的数字来自负载下的实测, 而非估算。

1,000 名模拟玩家同时在线,不间断

战斗服务器约 60 MiB,主服务器约 3 GiB,全部为真人之间的配对。稳定,且已在使用中。

内部基准负载测试

10,000 名模拟玩家,最坏情况(全部在同一刻连入)

数据库和配对服务立刻堵塞。4 月 23 日的 报告记录了约 2,000 局成功对局,以及约 4,500 次 失败后立即重试的配对尝试;此前草稿中引用的 稳态划分「8,000 在排队 + 2,000 已启动」 是一个设计估算,而非那份报告的 测量值。

内部负载报告,2026-04-23(A 阶段)

A′ 阶段(部分),9 分钟,服务分离之后

主服务器内存 1.59 GiB 对比起步的 3.08 GiB−48%)。配对服务 12 MiB、0.4% CPU。超过 2,800 对。零签名错误。

内部负载报告,2026-04-24(A′ 阶段)

主服务器的内部进程

32 到 64 个就绪进程。出厂设置在 10,000 名玩家峰值期间,会被针对本地实例的 对局连发拖垮。

主服务器配置,审计之后

评分写入的节奏

如今是一次一条的直接写入;能扛住高得多流量的 批量写入方案是下一阶段的目标, 尚未发布。

评分存档

在测试里我们如何做到 10,000

在单台 Mac 上模拟负载的工具,受限于机器本身, 大约到 2,800 个模拟玩家就停住了。为了在不撞上这个 天花板的前提下做到 10,000,我们再用一台机器, 装着项目的一份副本,通过局域网把更多模拟玩家推向那台 Mac。 我们就是这样真正验证了「10,000 名玩家」的 场景:上方表格中每一个 10,000 的数字,都来自由那 第二台机器推动的一次测试。

第四幕 · 当队列停下时

当某处卡住时我们怎么做。

任何系统迟早都会卡住。把成熟的 服务和脆弱的服务区分开来的,不是从不 出故障,而是提前知道它怎样出故障,并且 早已备好让它重新运转的那一手。

底层原理。 真正要紧的不 是可能故障的清单,而是故障出现时会发生什么: 多数情况下系统会自行恢复,而任何情况下,一次 故障都不会留下一个错误的评分。

恢复与安全兜底 — 它会自行重新运转,绝不写下一个错误的评分 两条通道。上方是恢复通道:从一个请求出发,通往陷入 困难区域的主路径中断,用一个叉号标出;一条绿色的 备用路径亮起并接续,最终仍让对局照常开始。下方是 裁定通道:战斗服务器只能签名并提出一个结果,只有在 有权的服务复核之后,这个结果才会成真。一旦出现故障, 裁定会被干净地丢弃,什么都不会发生:评分保持不变, 绝不出错。 第四幕 · 当队列停下时 源自设计的韧性 1 · 自行重新运转 对局请求 提出的配对 主路径 区域 A 陷入困难 退回备用路径 备用路径 邻近区域 对局照常开始 而非报错 对决改道 → 邻近区域 搜索落空 → 人机对战 配对被拒 → 重新提出 2 · 故障绝不写下错误评分 战斗服务器 只能签名并提出 已签名裁定 复核 有权的服务 全部校验通过 评分已更新 只生效一次 宕机或消息丢失 → 裁定被干净丢弃 什么都不会发生 评分不变,绝不出错
若某个部件失效,路径会退回一条备用路径,对局照常开始;而裁定只有在复核之后才作数 — 存疑时,什么都不会发生。 最坏的结局是一次干净的停摆

它会自行重新运转

几乎所有预料之内的卡顿都不需要人工 介入。若某个区域或某个部件陷入困难,系统 会自动退回备用路径、继续运转:对决被 改道到邻近区域,一次找不到任何人的搜索 退而求其次转为人机对战,一对被拒的配对被 重新提出。对玩家来说,结果是一场照常开始的 对决,而不是一个报错。

故障绝不会写下一个错误的评分

这是所有保证中最重要的一条。托管 对决的服务器无权触碰评分或金币:它 只能为一份裁定签名并提交。那份裁定只有在 有权的服务各自重新校验之后,才会成为现实。这样, 若某台服务器在对局中途宕机,或某条 消息半路丢失,其结果永远是「什么都 不发生」,而绝不是「写下一个错误的 评分」:穿过重重故障,评分依旧保持一致、 可靠。

这是源自设计的韧性:一次故障最坏的结局, 是一次干净的停摆、可以从中重启,而不是一份 要靠人工修复的损坏数据。

黄金法则。 配对路径的设计是 为了安全地退让:在每一个环节,若某处不 响应,就退回一条备用路径,而绝不卡住玩家。 正因如此,当某处看起来坏了,第一个问题不是 「哪一块倒下了?」,而是「为什么 备用路径没有触发?」。