第28章 · 上线与运营:反作弊、CI/CD 与热更新

📑 目录

导语:「远港 Online」上线第九天,凌晨三点,值班群被一条告警炸醒:交易所强化石一小时内成交四万组,物价中位数跌了六成。查到天亮,结论很难看——领奖接口少写了一条幂等约束,被人抓包重放刷穿了。外挂只是表象,真正的缺口在业务逻辑里。那天之后团队立了三条规矩:客户端说的话一句不直接信;任何版本必须能十分钟内滚回去;经济指标和 tick 耗时上同一块监控大盘。这一章,就是把这三条规矩展开成体系。

本章地图

mindmap
  root((第28章
上线与运营)) 反作弊 口供不是证据 纵深防御四层 攻击面三连 交付流水线 CI/CD 五段 按分线灰度 回滚时间公式 热更新 版本协商三档 强制更新 vs 兼容期 可观测性 指标 日志 追踪 告警分级与值班 SLA 补课 毕业清单 28 章能力验收总表

反作弊:客户端交上来的每一条消息都是口供

刑侦剧里有一句行话:口供不能单独定案,必须有物证。客户端发给服务器的每一条 RPC 都是口供——它说"我走了三米""我这一刀暴击了""我捡到了传说材料"。服务器手里的世界状态才是物证。口供和物证对不上,以物证为准。

这就是服务器权威。第 20 章从理论上论证了客户端权威是作弊的温床,第 21 章用复制系统把"服务器有真相,客户端有影子"落成了代码。到了运营阶段,它从架构原则变成一条纪律:任何客户端提交的数据,未经服务器校验,不得改变世界状态。

单机时代不需要这条纪律。「回响峡谷」的玩家改自己的存档,伤害的只有自己的体验。MMO 里有交易所、有排行榜、有他人付出的真金白银和时间,作弊不再是对自己耍赖,是对其他玩家的盗窃。性质变了,投入就得跟着变。

服务器权威只是底线,真正的答案在纵深防御(Defense in Depth)里。四层,每层有明确的定位和明码标价的代价。

flowchart TD
    A[攻击者与外挂作者] --> B{第一层 · 客户端侧}
    B -->|内核反作弊 / 反调试 / 混淆| C[作弊门槛抬高
过滤八成闲人] B -->|被突破:DMA 硬件挂| D{第二层 · 协议层} D -->|加密 + 序列号 + nonce| E[抓包重放失效] D -->|被突破:协议逆向| F{第三层 · 服务器权威校验} F -->|移动 / 技能 / 经济二次结算| G[非法请求被拒] F -->|漏网:阈值内微作弊| H{第四层 · 行为分析与审计} H -->|统计异常 + 举报 + 日志| I[封号梯度 + 经济回滚] style F fill:#b5543a,stroke:#4a4a45,color:#fff

第一层是客户端侧:内核级反作弊(EAC、BattlEye、ACE 这一族)、反调试、代码混淆。它的定位是门卫,作用是过滤掉八成只会下载现成外挂的闲人。UE 5.8 通过 EOS 的 Anti-Cheat Interfaces 接入这类方案,具体词条以官方文档为准。它的天花板要说清楚:DMA 硬件挂从 PCIe 插槽直接读内存,操作系统都看不见,内核驱动自然也看不见。这一层的代价是平台合规审核、Linux 与 Steam Deck 的兼容问题,以及玩家社区对内核驱动的天然反感。

第二层是协议层:加密传输、单调递增序列号、一次性 nonce。定位是让抓包重放变成无用功,下一节详细拆。代价是加解密的 CPU 开销,以及排查线上问题时自己人也读不懂包——调试期内网环境通常要留一条明文通道。

第三层是服务器权威校验,整张图里染红的那一格。移动、技能、经济,一切二次结算。代价是 CPU 预算:一个 200 人的 Zone,移动速率校验大约吃掉 tick 预算的 8%,第 25 章做容量模型时这笔钱就得预留。上线后再补,来不及。

第四层是行为分析与审计:统计分布、玩家举报、安全日志。定位是兜底止损——封号梯度(警告→短期→永久)加上经济回滚(第 26 章的回档预案在这里复用)。代价是误杀和申诉流程,这是一笔客服成本,预算表里要有。

攻击面三连:变速齿轮、协议重放、协议伪造

运营 MMO 一年,这三类攻击会反复出现。外挂天天换皮,但它们利用的东西不变:时钟、协议、客户端的信任边界。吃透这三类,比追每一款新外挂有用。

变速齿轮:篡改时间本身

攻击原理:外挂挂接客户端的时钟 API——Windows 上的 QueryPerformanceCounter 是重灾区——让游戏进程以为时间流速是真实世界的两倍。单位真实时间内,客户端提交两倍的移动包,角色在服务器眼里健步如飞。低阶变种更粗暴,直接改内存里的速度字段。

防御方案:服务器拿权威时钟重新算账。每个移动包带客户端时间戳,服务器为每个连接维护一个滑动窗口,窗口内客户端申报的位移除以窗口真实流逝时间,得到申报速率。申报速率超过角色理论上限的 1.2 倍,记一次违规;违规分累积过线,强制回拉位置并踢下线。UE 的 GameNetworkManager 自带一组移动容差参数(MAXPOSITIONERRORSQUARED 一族),源码在 Engine/Source/Runtime/Engine/Classes/GameFramework/ 目录下,默认值是为宽松网络环境调的,MMO 里建议收紧,再叠加自己的速率统计。

残余风险:5% 的微加速。低于校验阈值,混在网络抖动里,单次抓不出来。但作弊是长期行为——把每个玩家的申报速率分布落库,跑周级统计,均值持续站在 103% 的账号会自己浮上来。

协议重放:合法的包,非法的次数

攻击原理:Wireshark 或自写代理把客户端发出的合法包录下来,原样再发一遍。领奖、签到、开宝箱这类"发一次给一次钱"的接口是重灾区,本章导语里的事故就是这个剧本。

防御方案是两道锁。传输层:会话密钥签名、单调递增序列号、时间戳,关键操作再加服务器下发的一次性 nonce,序列号倒退或 nonce 重用直接拒收。业务层:幂等兜底,奖励发放表建唯一索引(玩家 ID + 奖励 ID),第 26 章讲事务时埋的伏笔,在这里接住。

下面这段伪代码是网关侧防重放校验的骨架,解决"同一个包不能生效两次":

# 伪代码:会话级防重放校验,部署在网关与 Zone 服务器之间
class ReplayGuard:
    def __init__(self, session_key):
        self.key = session_key
        self.last_seq = 0              # 本会话已接受的最大序列号
        self.used_nonce = set()        # 关键操作的一次性 nonce
        self.violations = 0

    def check(self, pkt) -> bool:
        if not hmac_verify(pkt, self.key):     # 签名先行,伪造直接丢
            return self.reject("签名无效")
        if pkt.timestamp < now() - 30:         # 时间窗 30 秒
            return self.reject("时间戳过期")
        if pkt.seq <= self.last_seq:           # 序列号必须单调递增
            return self.reject("重放或乱序")
        if pkt.op in CRITICAL_OPS:             # 领奖/交易类强制 nonce
            if pkt.nonce in self.used_nonce:
                return self.reject("nonce 重用")
            self.used_nonce.add(pkt.nonce)
        self.last_seq = pkt.seq
        return True

    def reject(self, reason) -> bool:
        self.violations += 1
        log.security("replay_reject", reason, self.violations)
        if self.violations > 10:               # 违规分超阈值,踢线
            kick(self.session_id)
        return False

要点就三条:签名验证必须排在序列号记账之前;nonce 集合要随会话销毁;违规分是封号的证据链,别只踢不记。

一次完整的"重放被拒"时序长这样:

sequenceDiagram
    participant ATK as 攻击者
    participant CLI as 正常客户端
    participant GW as 网关
    participant SRV as Zone 服务器
    Note over CLI,SRV: 会话建立:协商密钥 K,seq 从 0 开始
    CLI->>GW: 领奖请求 seq=101, nonce=N7, HMAC
    GW->>SRV: 转发并校验
    SRV->>SRV: 签名有效,seq 101 > 100,nonce 未用
    SRV-->>CLI: 发放奖励,记账 seq=101
    ATK->>GW: 原样重发抓到的包 seq=101
    GW->>SRV: 转发并校验
    SRV->>SRV: seq 101 ≤ 101,拒收
    SRV-->>ATK: 丢弃,违规分 +1,写安全日志

残余风险:同会话重放被堵死了,但攻击者可以开新会话,重发"协议字段全合法、业务语义重复"的请求——新的序列号、新的 nonce,干的还是领第二次奖。所以幂等键必须是业务键(奖励 ID),协议键(nonce)做不到幂等。协议防重放,业务防重复,两层各管各的。

协议伪造:客户端只能提交意图

攻击原理:逆向客户端,摸清 RPC 的序列化格式——UE 的 RPC 结构并不神秘——然后构造服务器从没教过客户端发的包:"我对 BOSS 造成 99999 点伤害""我的背包多了一百个传说材料"。

防御方案一句话:客户端只能提交意图,结果永远由服务器算。战斗伤害走 GAS 的服务器二次结算(第 22、23 章),移动走服务器校验,掉落拾取由服务器查表决定"你能不能捡、捡到几个"。字段级校验做范围裁剪:伤害数值超出理论上限的包,直接判非法并记入安全日志。

残余风险:范围内极值伪造。自瞄挂上报的"我瞄准了头部"在协议层面完全合法,字段校验抓不到。这类只能交给第四层的行为分析:爆头率的统计分布里,人类的方差和机器的方差长得不一样。

三连对比,方便贴在工位上:

攻击篡改对象防御主线兜底手段残余风险
变速齿轮本地时钟与内存服务器速率滑动窗口校验周级速率分布统计阈值内微加速
协议重放合法报文的次数序列号 + nonce + 签名业务幂等唯一索引换会话的业务重放
协议伪造报文内容本身服务器二次结算 + 字段裁剪行为分布分析范围内极值(自瞄类)

这场仗的终点是"作弊的期望收益低于成本"——"没人能作弊"是个虚妄的目标,守住经济平衡才是真防线。技术负责抬高成本,封号梯度和经济回滚负责压低收益,两者相乘才是防御力。只押一边的团队,要么被外挂打穿,要么把正常玩家误伤殆尽。

封号梯度本身也是一门设计。「远港 Online」的梯度是首次警告并回收非法所得、二次封停七天、三次永久——证据链不足时只走第四层的标记观察,不出手。出手即要有日志、截图、违规分记录三件套,因为申诉一定会在二十四小时内到达客服。误封一个付费玩家的代价,远高于放过一个可疑账号多观察一周的代价,这笔账在定封号策略时就要算进去。

CI/CD:把"我电脑上能编过"开除出团队

单机时代,「回响峡谷」打包是个仪式:主程在自己电脑上点 Package,出了安装包发群里,大家装上就玩。MMO 时代这套做法是事故温床。客户端、服务器、配置三份产物,任何一份出自"某台电脑",上线就变成掷骰子——那台电脑的环境变量、本地缓存、昨天没提交的改动,全都成了不可复现的变量。

UE 项目搭流水线有三座大山,先把它们认清楚。一是 cook 慢:全量 cook 以小时计,共享 DDC(Derived Data Cache)是刚需——说它是优化就低估了它的分量,构建机挂不上 DDC,每次构建都是全量重来。二是双 target:客户端和 Dedicated Server 要各编一遍(第 24 章),流水线里是两个并行 job,不是一个。三是自动化测试环境重:Gauntlet 冒烟测试要真的拉起客户端和服务器进程跑登录,不是跑几个单元测试就算完。

整条流水线的长相:

flowchart LR
    A[开发提交 MR] --> B[编译
客户端 + Server 双 target] B --> C[自动化测试
单元 + Functional + Gauntlet 冒烟] C -->|失败| X[打回,通知提交者] C -->|通过| D[Cook 与打包
pak + 服务器镜像] D --> E[制品库归档
构建号 + 符号文件] E --> F[部署测试服
QA 验收] F --> G[灰度发布
单分线先行] G --> H{观察窗口
指标正常?} H -->|异常| R[自动回滚] H -->|正常| I[全量推送] style R fill:#b5543a,stroke:#4a4a45,color:#fff

读者里是 GitLab CI 老手,YAML 基础不赘述,只给 UE 特有的关键决策段:

stages: [build, test, canary, full]

build-server:
  stage: build
  tags: [ue5-buildbox]                 # 高内存 cook 构建机
  script:
    - RunUAT.bat BuildCookRun -project=Farport.uproject -target=FarportServer
      -platform=Win64 -build -cook -pak -buildversion=%CI_PIPELINE_IID%
  artifacts: { paths: [Saved/StagedBuilds/], expire_in: 7 days }

gauntlet-test:
  stage: test
  needs: [build-server]
  script: RunUAT.bat RunGauntlet -project=Farport.uproject -test=SmokeLogin
canary:
  stage: canary
  when: manual                         # 值班手动触发,留下审计记录
  script:
    - ./deploy.sh --channel ch-09 --tag %CI_PIPELINE_IID%
    - ./metric_gate.sh --window 45m --metrics tick_p99,crash_rate,econ

full-rollout:
  stage: full
  needs: [canary]
  script: ./deploy.sh --all-channels --tag %CI_PIPELINE_IID%

三个要点。buildversion 把流水线号注入包内,线上崩溃堆栈才能和具体构建对上号,没这个,崩溃报告就是天书。Gauntlet 冒烟挡在打包之前,登录都过不了的版本不配进制品库。制品不可变:同一个构建号滚完测试服滚灰度、滚全量,任何阶段都不重新编译——重新编译出来的东西,和测过的东西,不是同一个东西。

自动化测试的金字塔在 UE 项目里同样成立:单元测试(Automation Spec)毫秒级,Functional Test 拉起空地图秒级,Gauntlet 冒烟拉起完整客户端与服务器分钟级。塔底越厚,流水线越快,反馈越早;只有塔尖没有塔底的团队,每次合并都在赌命。

构建机规格按 cook 峰值内存配。「远港 Online」工程全量 cook 的内存峰值在 60GB 上下,用 64GB 的机器会被 OOM 反复折磨,直接上 128GB——奢侈?不,这笔账早就算过了。

还有一条容易被跳过的纪律:配置即代码。服务器的 DefaultGame.ini、数值表、分线路由表,全部进 Git 仓库走同一条流水线,禁止任何人在服务器上手工改配置。手工改的那一行不会进版本历史,三个月后它就是一次"怎么都想不起来当初为什么改"的事故。配置变更和代码变更一样要过 MR 审查,diff 里一个多写的零,在数值表里就是一次经济事故。

灰度发布:分线是天然的手术台

第 25 章把世界切成分线(Channel/Shard)时,答应过一笔运营红利,现在兑现:分线是最干净的灰度单元。按机器灰度,一个玩家可能在新旧两个进程之间被切来切去;按分线灰度,玩家只待在自己那条线里,版本边界和世界边界重合,问题被锁死在一条线内。

一次按分线灰度的完整流程:

sequenceDiagram
    participant OP as 值班运维
    participant CI as CI/CD 平台
    participant GW as 网关层
    participant CH9 as 分线 Channel-9
    participant MON as 监控告警
    participant CH as 其余分线
    OP->>CI: 触发灰度发布 v1.4.2
    CI->>CH9: 部署新镜像并拉起
    CI->>GW: 更新路由:新登录导入 Channel-9
    GW->>CH9: 灰度玩家登录(约占 5%)
    CI->>MON: 打开观察窗口(45 分钟)
    MON-->>CI: tick p99 / 崩溃率 / 经济水位正常
    CI->>CH: 滚动扩展到 20% 分线
    MON-->>CI: 指标持续正常
    CI->>CH: 全量推送,旧版本下线
    Note over OP,CH: 任一窗口指标越线 → 自动切回旧镜像,灰度线玩家重连

灰度为什么敢做,因为有公式兜底。爆炸半径(Blast Radius):

Rblast=Ncanary×P¯linePtotalR_{blast} = \frac{N_{canary} \times \bar{P}_{line}}{P_{total}}

NcanaryN_{canary} 是灰度分线数,P¯line\bar{P}_{line} 是单线平均在线,PtotalP_{total} 是全服在线。单线 3000 人、全服 8 万人、先发 1 条线,Rblast3.7%R_{blast} \approx 3.7\%。哪怕新版本上线即崩,受伤面也就这么大,舆论和客服都接得住。

第二个公式是回滚时间,它决定"出事之后疼多久":

Trollback=Tdetect+Tdecide+Tswitch+TrejoinT_{rollback} = T_{detect} + T_{decide} + T_{switch} + T_{rejoin}

「远港 Online」的预算:监控发现异常 2 分钟、值班拍板 3 分钟、镜像切回 3 分钟、玩家重连 2 分钟,合计 10 分钟。这个 10 分钟是从每月测试环境演练里压出来的——愿望清单上不写这个数,演练报告里才有。回滚按钮半年不按,真出事那天没人记得命令在哪——演练本身就是交付物。

观察窗口看什么,顺序有讲究。先看 Zone tick p99 有没有抬头(性能回归),再看服务器崩溃率和登录成功率(稳定性),最后看经济水位(新版本带刷钱 bug 的话,物价三十分钟就开始变形,第 27 章的水表在这派上用场)。三项全绿,才允许扩量。

上面这套流程灰的是服务器,客户端的灰度要另算。PC 端走自己的启动器,可以按账号尾号分批推;移动端卡在应用商店审核手里,iOS phased release 铺到全量要七天,这意味着服务器必须为"商店里还在跑的旧客户端"维持兼容期——客户端灰度慢,反过来决定了服务器的兼容窗口关不掉。两边的时间表要在发版计划里对齐,否则就是服务器全量了、一半玩家还在旧客户端上被强制更新弹窗挡在门外。

热更新与版本协商:兼容期是花钱买的,强制更新是拿留存换的

先把两类变更分开,它们的命运完全不同。资源变更——新地图、新时装、平衡性数值表——走 pak 热更:客户端启动时拉取补丁 chunk,按挂载顺序覆盖旧资源,玩家基本无感。UE 侧补丁打包与 ChunkDownloader 的相关词条以官方文档为准。协议与逻辑变更——RPC 签名变了、状态机改了、结算规则换了——必须客户端与服务器版本对齐,热更救不了,这就是版本协商存在的理由。

热更体感好不好,七分在 chunk 怎么切。原则是把更新频率相近的内容打进同一 chunk:数值表、热修复资源归高频 chunk,地图、时装归低频 chunk。切得不好,改一行数值就让玩家下载 2GB 陪嫁资源,热更的口碑就是这么败掉的。「远港 Online」的规矩是单次热更包超过 200MB 必须发版经理签字,这个数字写进了流水线的检查脚本。

协商发生在登录握手时,客户端上报自己的协议版本与资源版本,服务器三档处理:

stateDiagram-v2
    [*] --> 握手: 客户端连接,上报版本
    握手 --> 完全兼容: 协议版本相同
    握手 --> 兼容期放行: 客户端版本 ≥ 最低兼容版本
    握手 --> 强制更新: 客户端版本 < 最低兼容版本
    兼容期放行 --> 弱提醒: 进入游戏,公告 + 邮件提示
    弱提醒 --> 完全兼容: 玩家自行更新
    强制更新 --> 下载补丁: 拉取最新 pak / 安装包
    下载补丁 --> 握手: 更新完成,重新连接
    完全兼容 --> [*]: 正常游戏

网关侧的协商逻辑骨架:

# 伪代码:登录握手时的版本协商(网关执行)
MIN_PROTOCOL = 1042      # 服务器能接受的最低协议版本
CUR_PROTOCOL = 1050      # 当前协议版本
MIN_CONTENT  = 301       # 最低资源版本

def negotiate(client_hello):
    if client_hello.protocol < MIN_PROTOCOL:
        return deny(reason="FORCE_UPDATE",
                    patch_url=store_url(client_hello.platform))
    if client_hello.protocol < CUR_PROTOCOL:
        # 兼容期:服务器降级说话,新功能对旧客户端裁剪
        session = accept(protocol=client_hello.protocol,
                         degraded=FEATURE_MATRIX[client_hello.protocol])
        session.notify("WEAK_UPDATE", cooldown_days=3)
        return session
    if client_hello.content < MIN_CONTENT:
        # 协议没问题,只补资源差量
        return accept_with_hotfix(paks=diff_paks(client_hello.content))
    return accept(protocol=CUR_PROTOCOL)

def deny(reason, **kw):
    metrics.incr("login_reject", tags=[reason])   # 拒绝量也是监控指标
    return ConnectionReject(reason, **kw)

三档策略的取舍,本质是两种成本的兑换:

维度强制更新兼容期放行
玩家体验当天留存掉一截(量级 5%–15%)无感
服务器成本零兼容代码维护 N 个协议分支 + 功能裁剪矩阵
测试矩阵单版本旧客户端 × 新服务器的笛卡尔积
适用场景协议不兼容、安全补丁资源更新、纯新增内容

兼容期有它的价码:服务器要为每个仍在线的旧协议版本保留一套序列化分支和功能裁剪矩阵,QA 的测试矩阵跟着指数膨胀。经验法则两条。强制更新是战略武器,一个赛季最多动两次,多用玩家就用脚投票;兼容期别超过两周,超了 QA 会先起义。还有一个容易漏的点:被强制更新拒绝的登录量本身要进监控——login_reject 指标突然飙升,往往意味着补丁 CDN 出了问题,而不是玩家集体偷懒。

可观测性三件套:指标说出事了,日志说哪一步,追踪说谁拖的

三件套的分工一句话讲清:指标(Metrics)告诉你"出事了",日志(Logs)告诉你"哪一步出的事",追踪(Traces)告诉你"链条上谁拖的后腿"。

指标的玩法照搬 Prometheus 思路:各服务进程暴露 /metrics 端点,Prometheus 定时拉取成时间序列,label 打上维度(分线、地图、玩法类型),Grafana 出大盘。拉模型对 MMO 有个实在的好处:分线进程动态增减时,服务发现注册一下就被纳入采集,不用改监控配置。

MMO 的核心监控指标和 Web 服务长得不一样。下面这张清单是「远港 Online」的实战配置,红线值按自己项目的容量模型调整:

类别指标红线
在线全服 / 分线 CCU、登录成功率、登录队列长度登录成功率 < 98%
性能Zone tick p50 / p95 / p99、单 tick 超 33ms 次数、单连接带宽tick p99 > 33ms
持久化回存延迟(脏标记→落库)、回存失败率、DB 慢查询数回存延迟 > 5s
经济货币产出 / 回收比、交易所物价中位数、强化材料流通量产出 / 回收 > 1.3
安全速率违规次数、重放拒收次数、伪造拒收次数同比突增 3 倍

经济水位和安全指标进大盘,是 MMO 区别于一般后端服务的地方。第 27 章的水龙头水槽模型到这里变成了日报表;安全指标平时躺平,一旦翘头就是有人在试探防线。

日志的纪律:结构化(JSON)、分级、每条带 trace_id。还有一条反常识的——别拿日志当指标使。日志是出事后翻的,指标是出事前看的,用日志聚合算在线人数,成本是指标的十倍,延迟还救不了场。

日志量本身也要预算。八万人在线的 Zone 集群,DEBUG 全开一天能写出 TB 级日志,存储费比服务器还贵。线上默认 INFO,WARN 以上全量保留,DEBUG 按分线动态开关——排查时只对目标分线临时调高等级,查完调回去。改配置重启进程是反模式。运维平台上的一个按钮——这才是开关的正确形态。

追踪解决的是"慢"的归属问题。网关入口生成 trace_id,贯穿 Zone、缓存、数据库的调用链。一次交易耗时 800ms,拆开看是 Zone 逻辑 30ms、缓存 20ms、数据库 750ms——没有追踪,这三段你只能猜。追踪也不是全量留的:八万人同时在线,全量埋点的开销本身就会拖慢服务,生产环境按 1%–5% 采样,慢请求和错误请求 100% 保留。异常的链路一条不丢,正常的链路留个统计影子就够。

这些东西最终汇成一块大盘,布局长这样:

远港 Online · 运营监控大盘(示意)数据源 Prometheus / Grafana · 2026-07-15 03:12 · 值班:B 组全服在线 CCU82,431较昨日 +6%Zone tick p9928ms阈值 33ms · CH-09 灰度中回存延迟 p951.8s阈值 5s · 失败率 0.02%经济水位 产出/回收1.04阈值 1.3 · 连续 5 日回落分线 tick p95 · 近 6 小时0204033ms 红线CH-09 灰度峰值后回落00:0003:0006:00全服在线人数 · 24h050k100k晚高峰 82,431凌晨低谷 31,2050 时12 时24 时实时告警(SEV2 及以上)● 03:05 [SEV2] CH-09 回存延迟 4.1s,逼近阈值● 02:47 [SEV3] 速率违规 1 小时 12 次,账号已标记每条告警附带 runbook 链接与值班群电话分线状态 · 16 线正常灰度 / 告警(CH-09)

有了大盘,还得有告警分级,否则大盘只是壁纸。三档:SEV1 打电话(全服或整条分线不可用,半夜也得响),SEV2 工单(部分受损,工作时间处理),SEV3 仅记录(体验劣化,周会复盘)。铁律一条:每条告警必须对应一个人能执行的动作,否则删掉。告警噪声是值班制度的第一杀手——一夜两百条告警的团队,第两百零一条真告警注定被淹没。

事故从告警到复盘的完整状态机:

stateDiagram-v2
    [*] --> 告警触发: 监控越线或玩家反馈
    告警触发 --> 值班确认: 5 分钟内响应
    值班确认 --> 定级: SEV1 全服故障 / SEV2 部分受损 / SEV3 体验劣化
    定级 --> 止血: 回滚 / 降级 / 关闭入口
    止血 --> 定位修复: 保留现场,拉群作战
    定位修复 --> 恢复验证: 指标回归基线
    恢复验证 --> 复盘: 48 小时内出无指责 postmortem
    复盘 --> [*]: 行动项进 backlog

复盘文化值得单独说一句:postmortem 无指责,查的是流程和系统为什么没能拦住错误——盯着"谁手滑"查,查不出任何能阻止下一次事故的东西。导语里那场事故的复盘产出物,就是那条幂等唯一索引和本章开头立的三条规矩。

补课:运营补课·SLA 与值班制度的最小实践。做过运营的同学可以跳过。

SLA(服务等级协议)先学会换算再对外承诺:99.9% 意味着每月允许停机 43.2 分钟,99.95% 是 21.6 分钟,99.99% 只剩 4.32 分钟——还不够一次灰度回滚的预算。把数字写进公告之前,拿过去三个月的真实可用率对一下,承诺做不到的数字只是把骂声延期。

值班制度的最小形态:一周一轮换,值班期内响应时间写死(告警 5 分钟内确认),值班补贴给足,升级路径明确(值班→系统负责人→制作人)。十人以内的团队玩不起 follow-the-sun,就接受"只有 SEV1 才打电话"的现实,别假装自己能 7×24。

值班三件套随身带:runbook(每条告警对应的操作步骤)、回滚命令速查、电话联系表。凌晨三点的大脑只配执行清单,不配做推理。

毕业清单:从回响峡谷到远港 Online

第 1 章,你双击 .uproject,看见「回响峡谷」的第一帧。第 28 章,你盯着大盘,看「远港 Online」八万人同时在线。中间二十七章,每章是一块能力拼图。下面这张表是全书的句号——逐条自测,打不了勾的章节回去补,不用不好意思。

表里的验收标志故意写得很具体。"懂了"不算数,"跑通了""算得清""压得进预算"才算数——这本书从第 1 章就立过规矩:测量驱动,毕业标准也一样。三卷对应三种身份:卷一让你成为会用引擎的工程师,卷二让你能和美术、音频、动画坐在一张桌子上对话,卷三让你对一个活着的世界负责。缺任何一卷,MMO 这盘棋都下不完整。

卷一 · 地基(第 1–9 章)

你应该能……验收标志
1装好引擎、建对工程、讲清启动链说清启动器版与源码版怎么选
2用反射与 GC 思考对象生死独立定位一次 GC 误杀崩溃
3把逻辑放进对的 Gameplay 类讲清 GameMode 与 GameState 的分工
4划定 C++ 与蓝图的边界模块拆分后增量编译时间可控
5用 Enhanced Input 表达动作语义长按蓄力、组合键一次配成
6读懂一帧的账单stat unit 的数字说得出门道
7开对渲染三件套的档位目标帧率下画质不塌
8用材质函数组织复杂度材质图换个人接手看得懂
9独立完成一个竖切「回响峡谷」30 秒可玩 demo 落地

卷二 · 表现(第 10–18 章)

你应该能……验收标志
10用光讲故事而不是照亮场景标杆场景布光通过美术评审
11状态机加混合空间让角色脚下生根八向移动不滑步
12判断 Motion Matching 值不值得上说清代价函数与数据库成本
13特效先讲可读性再讲帅Overdraw 不超预算
14搭一座会呼吸的声景战斗音乐分层随强度切换
15用镜头叙事开场 90 秒分镜落地
16搭无缝大世界跑图全程无加载黑屏
17用 PCG 规则批量产内容森林与乱石滩一键生成
18测量驱动地优化4K / 60fps 达标且有数据存档

卷三 · 联机(第 19–28 章)

你应该能……验收标志
19用 MVVM 组织 MMO 量级 UI背包刷新不卡帧
20讲透延迟、同步模型与权威徒手画出服务器校验链
21调优复制系统单连接带宽账单算得清
22落地 GAS 五大件战士三技能跑通全流程
23做预测与回滚100ms 延迟下技能跟手
24构建并部署 Dedicated ServerDocker 拉起 1 服 2 客户端
25设计 MMO 分层架构画出容量模型与分线方案
26做持久化与回档预案交易不丢装备,回存可观测
27建数值与经济模型水龙头水槽平衡表配平
28扛住上线与运营本章三条规矩全部落地

这本书给出的,是"能上线、敢运营"的最低完整路径。天花板不在书里,在你的项目里。引擎会换代,玩法会过时,但"服务器权威、测量驱动、灰度回滚"这三件事,十年内不会变。「回响峡谷」教会你游戏怎么好看,「远港 Online」教会你世界怎么立得住。接下来那个世界,是你的了。

小结

客户端交上来的一切都是口供,服务器校验是唯一算数的证据;客户端侧反作弊只是门卫,定案不归它管,它的职责是过滤闲人。反作弊的终点是作弊期望收益低于成本:技术抬成本,封号梯度与经济回滚压收益,只押一边必输。分线架构最大的运营红利是灰度单元;爆炸半径和回滚时间都有公式,回滚能力靠每月演练维持,不靠运气。兼容期是拿服务器复杂度买留存,强制更新是拿留存买简单——只有协议变更和安全补丁配得上强制更新。监控三件套里指标先行,告警必须对应动作;没有指标盯着的上线,等于闭眼开车。

上手任务

  1. 给「远港 Online」的领奖接口加上序列号 + nonce 校验。验收:用抓包工具原样重发同一请求,第二次必须被拒,安全日志新增一条 replay_reject 记录,且业务层幂等索引挡住换会话重发。
  2. 用 GitLab CI 搭一条最小流水线:push 触发编译 + Gauntlet 登录冒烟,测试失败必须阻断打包。验收:故意改坏一个断言,流水线在 test 阶段红掉,制品库里没有新构建号。
  3. 写一份灰度发布 runbook:单分线→20%→全量,每步标注观察指标(tick p99、崩溃率、经济水位)与回滚命令。验收:在测试环境完整演练一次回滚,从发现到玩家重连全程 ≤ 10 分钟。
  4. 为你的项目配一块监控大盘,至少包含本章清单里在线、性能、持久化、经济四类各两个指标,并设两条告警。验收:压测时 tick p99 > 33ms回存延迟 > 5s 两条告警都能触发,且能定位到具体分线。

下一章

没有下一章了。第 29 章的标题页在你自己的项目里——去把它写出来。

延伸阅读

  • 官方文档:Unreal Engine → Online Services → Anti-Cheat Interfaces(EOS 反作弊接入,5.8 下词条以文档为准)
  • 官方文档:Unreal Engine → Automation → Gauntlet Automation Framework(自动化冒烟测试的脚手架)
  • 源码:Engine/Source/Runtime/Engine/Classes/GameFramework/GameNetworkManager.cpp(服务器移动容差校验的全部参数都在这)
  • GDC 2017:Overwatch Gameplay Architecture and Netcode(服务器权威与延迟补偿的工业级范本)
  • Google SRE 团队《Site Reliability Engineering》第 11 章 Being On-Call(值班制度的源头文献)