降本的第一个拐点
《裂隙战场》的房间服容量按对局数扩容。运维给出一张示例表:从 8 个房间进程加到 12 个,每千次对局成本下降;加到 16 个,成本基本持平;再加到 24 个,单位成本反而上升。原因是匹配服务、数据库连接和跨进程广播的开销开始超过新增房间带来的收益。
这就是成本曲线的拐点。扩容不是线性的,降本也不是。 在拐点左侧加容量是划算的,在拐点右侧加容量只是把账单做大。拐点的位置必须用固定流量压测测出来,不能从实例单价推算;实例单价只告诉你一台机器多少钱,不告诉你系统在什么规模下开始互相拖累。
测量方法不复杂:固定一份回放流量,从当前容量开始逐档增加房间进程,每档跑够稳定窗口,记录每千次对局成本、P99 tick 抖动和匹配排队时间。成本最低的那一档就是经济拐点,但还要看体验指标是否仍在预算内。若经济拐点的 P99 已经超预算,真正可用的容量是体验拐点,不是成本拐点。延迟预算的口径见3.1 全球延迟预算与路由 。
跨区部署还有第二个台阶:从单区到双区,成本不是乘以二,而是乘以二再加跨区复制、专线和一致性开销。示例中双区的单位成本比单区高 35%,但延迟覆盖范围扩大了一倍。这个台阶值不值,不能只看成本,要看新增覆盖地区的付费转化。
四个常见手段的边界
降本手段很多,但每个都有体验边界。下面四个是北辰工作室真实讨论过的:
| 手段 | 做法 | 体验影响 | 节省来源 | 可逆性 |
|---|---|---|---|---|
| tick 降频 | 20Hz 降到 15Hz | 射击手感、判定精度 | 房间服 CPU | 高,改配置 |
| 区域收敛 | 低峰区合并到邻近区 | 延迟增加 20–40ms | 区域固定成本 | 中,需迁移 |
| 日志采样 | 全量改为 10% 采样 | 排障粒度下降 | 存储与写入 | 高,改采样率 |
| 回放保留期 | 30 天缩短到 7 天 | 老回放不可查 | 对象存储 | 中,删除不可逆 |
tick 降频对《裂隙战场》这类竞技产品不是纯技术决策。20Hz 降到 15Hz 节省的 CPU 示例为 18%,但命中判定窗口变宽,高水平玩家会感知到“打中了却没伤害”。可以先在休闲模式灰度,观察命中率、投诉率和退款率,再决定是否推广到排位。对《泡泡工坊》这种休闲产品,tick 从 30Hz 降到 10Hz 几乎没有感知,因为它不是实时对抗。
区域收敛对《群岛纪元》更合适。SLG 的离线推进和异步结算对延迟不敏感,把低峰区合并到邻近区,代价是部分玩家延迟从 60ms 升到 90ms。要先确认这个区间仍在可接受范围,再检查数据驻留和跨区合规——区域收敛不能违反数据落地要求。
日志采样要区分“审计日志”和“调试日志”。支付、封禁、道具发放属于审计链路,不能采样;房间帧日志、路径寻路日志可以按 10% 采样,但采样必须保留错误全量。回放保留期缩短最容易做,也最容易被忽略:删除是不可逆的,玩家申诉和赛事复核都依赖回放,保留期变更要提前公告。
用一把尺子给手段打分
三个维度:体验影响 0–10、成本节省比例 0–1、可逆性 0–1。评分公式不直接把三者相乘,而是:
score = saving × reversibility / (1 + experience)
分母用 1 + experience 而不是 experience,是为了让零影响的手段保持有限分值,也避免“体验影响为 0 就得到无穷大”的除零问题。这个公式表达的是:节省和可逆性加分,体验影响减分,但减分是渐进的,不是一票否决。一票否决交给阈值。
阈值示例:体验影响不超过 3,节省比例不低于 10%,可逆性不低于 0.5,组合后的体验影响总和不超过 3。这四个数字不是普适真理,是工作室当前阶段的政策;政策要写进配置,随版本评审调整,而不是散落在会议纪要里。
为什么可逆性要单独一维?因为降本提案的最大风险不是“省得少”,而是“改错了回不去”。tick 降频、日志采样改配置就能回滚,适合先做;删除历史回放、合并区域、迁移数据存储,回滚成本极高,必须要求更高的节省和更低的体验影响。可逆性低的提案,即使分数高,也应该走更长的灰度周期。
组合估算:节省不能简单相加
单个手段省 10%,另一个也省 10%,合计不是 20%。原因有两个:作用范围可能重叠,比如日志采样和回放保留期都在动存储;体验影响会累加,两个“轻微”改动叠加可能让玩家明显感知。
组合估算按作用范围计算绝对节省:节省 = 基线 × 作用范围占比 × 范围内节省比例。作用范围重叠时,宁可低估也不要高估;最终节省 capped 在基线以内,不允许出现负账单。体验影响按算术和累计,再和组合预算比较。
这一步的意义不是算得有多准,而是暴露“单看都合格、合起来超预算”的组合。三个体验影响为 2 的手段,单独都过 3 的阈值,合起来是 6,已经超过组合预算 3。这时要砍掉分数最低的那个,而不是等玩家反馈。
数据模型与可运行片段
internal/costexperience 实现三件事:Score 计算三维评分,Evaluate 按政策阈值筛选并保留拒绝原因,Combine 估算组合后的账单与体验总和。拒绝原因要保留,因为评审需要看到“为什么没选”,而不只是“选了什么”。
// file: internal/costexperience/costexperience.go
// tier: L1
// verify: go test ./internal/costexperience/
package costexperience
import (
"errors"
"sort"
)
// Lever is one cost-reduction measure with the two numbers that decide
// whether it is allowed: how much it hurts and how easily it is undone.
type Lever struct {
ID string
Experience float64 // 0..10, expected player-visible impact
SavingPct float64 // 0..1, share of the addressed scope saved
Reversibility float64 // 0..1, 1 means a config flag flips it back
ScopePct float64 // 0..1, share of the total bill the lever addresses
}
// Policy is the review gate. A lever must pass every threshold on its own.
type Policy struct {
MaxExperience float64
MinSavingPct float64
MinReversibility float64
MaxCombinedImpact float64 // sum of admitted experience scores
}
// Decision records one lever after the gate, including the reason it lost.
type Decision struct {
Lever Lever
Score float64
Admitted bool
Reason string
}
// Validate rejects values outside the documented ranges. A bad number in a
// cost review is worse than a missing one because it silently wins.
func Validate(l Lever) error {
switch {
case l.Experience < 0 || l.Experience > 10:
return errors.New("costexperience: experience must be within [0,10]")
case l.SavingPct < 0 || l.SavingPct > 1:
return errors.New("costexperience: saving must be within [0,1]")
case l.Reversibility < 0 || l.Reversibility > 1:
return errors.New("costexperience: reversibility must be within [0,1]")
case l.ScopePct < 0 || l.ScopePct > 1:
return errors.New("costexperience: scope must be within [0,1]")
}
return nil
}
// Score rewards savings and reversibility, and discounts experience damage.
// The denominator keeps a zero-impact lever finite and a ten-impact lever
// from being selected by a large saving alone.
func Score(l Lever) float64 {
if err := Validate(l); err != nil {
return 0
}
return l.SavingPct * l.Reversibility / (1 + l.Experience)
}
// Evaluate applies the per-lever thresholds and returns a deterministic,
// score-descending list. Rejected levers stay in the result so the review
// can show what was considered and why it lost.
func Evaluate(levers []Lever, p Policy) []Decision {
out := make([]Decision, 0, len(levers))
for _, l := range levers {
d := Decision{Lever: l, Score: Score(l)}
switch {
case Validate(l) != nil:
d.Reason = "invalid range"
case l.Experience > p.MaxExperience:
d.Reason = "experience impact above policy"
case l.SavingPct < p.MinSavingPct:
d.Reason = "saving below policy"
case l.Reversibility < p.MinReversibility:
d.Reason = "not reversible enough"
default:
d.Admitted = true
}
out = append(out, d)
}
sort.SliceStable(out, func(i, j int) bool {
if out[i].Admitted != out[j].Admitted {
return out[i].Admitted
}
if out[i].Score != out[j].Score {
return out[i].Score > out[j].Score
}
return out[i].Lever.ID < out[j].Lever.ID
})
return out
}
// Selected returns only admitted levers, preserving score order.
func Selected(decisions []Decision) []Lever {
var out []Lever
for _, d := range decisions {
if d.Admitted {
out = append(out, d.Lever)
}
}
return out
}
// Estimate is the combined result of admitted levers. Savings are additive
// against disjoint scopes and capped at the baseline; experience is summed
// because several small degradations can add up to one bad session.
type Estimate struct {
BaselineMicros int64
AfterMicros int64
SavingMicros int64
ExperienceSum float64
WithinBudget bool
}
// Combine estimates the bill after all admitted levers. A zero
// MaxCombinedImpact means the combined gate is not configured.
func Combine(baselineMicros int64, levers []Lever, p Policy) Estimate {
if baselineMicros < 0 {
baselineMicros = 0
}
est := Estimate{BaselineMicros: baselineMicros, AfterMicros: baselineMicros}
for _, d := range Evaluate(levers, p) {
if !d.Admitted {
continue
}
saving := int64(float64(baselineMicros) * d.Lever.ScopePct * d.Lever.SavingPct)
est.SavingMicros += saving
est.ExperienceSum += d.Lever.Experience
}
if est.SavingMicros > est.BaselineMicros {
est.SavingMicros = est.BaselineMicros
}
est.AfterMicros = est.BaselineMicros - est.SavingMicros
est.WithinBudget = p.MaxCombinedImpact <= 0 || est.ExperienceSum <= p.MaxCombinedImpact
return est
}
本机验证命令与结果:
cd .local/checks/gsarch-lab
go test -count=1 ./internal/costexperience/
ok gsarchlab/internal/costexperience
该包共有 5 个表驱动测试,覆盖取值校验、评分、阈值筛选与排序、组合估算、节省上限与拒绝隔离;gofmt -l 无输出,go vet 退出码为 0。
一个示例决策矩阵
把三款产品的候选手段放进同一张表,评分和阈值的作用就清楚了。下表的体验影响、节省比例、可逆性、作用范围都是示例值,只用于演示判定过程:
| 手段 | 产品 | 体验 | 节省 | 可逆 | 范围 | 结论 |
|---|---|---|---|---|---|---|
| tick 30→10Hz | 《泡泡工坊》 | 1 | 0.22 | 1.0 | 0.35 | 通过,先灰度 |
| 日志采样 10% | 三款通用 | 0.5 | 0.45 | 1.0 | 0.10 | 通过,优先做 |
| 回放保留 30→7 天 | 《裂隙战场》 | 2 | 0.30 | 0.4 | 0.08 | 可逆性不足,评审 |
| 区域合并 | 《群岛纪元》 | 4 | 0.25 | 0.3 | 0.15 | 体验与可逆性双超限 |
| tick 20→15Hz | 《裂隙战场》 | 4 | 0.18 | 1.0 | 0.30 | 体验超限,仅休闲模式试点 |
这张表最有价值的一列是“结论”,因为它把争论变成了可复查的判定。认为《裂隙战场》降频可以做的同学,需要先解释为什么体验影响低于 3;认为回放可以缩短的同学,需要给出可逆性高于 0.5 的方案,例如先缩短到 14 天并保留赛事回放白名单。
矩阵还要有产品维度。同一个手段在不同品类的体验影响不同:tick 降频对休闲产品几乎无感,对竞技产品是核心手感;区域合并对 SLG 可接受,对 5v5 竞技会直接破坏跨区匹配的公平性。不要把“手段”当成全局开关,它是产品级的决策。
组合时先做高可逆、低体验的手段,再用节省出来的预算评估低可逆手段。示例顺序是日志采样、休闲 tick 降频、回放保留评审、区域合并评审。每一步都保留对照组,后一步的基线是前一步生效后的真实账单,而不是最初的理论值。
灰度与回滚门槛
评分决定“能不能做”,灰度决定“做完是否保留”。每个手段上线前要写清三件事:
- 观察指标:tick 降频看命中率、P99 输入延迟、投诉率;日志采样看排障平均时长;区域收敛看目标区延迟与留存。
- 观察窗口:至少覆盖一个完整高峰和一个低峰,竞技产品还要覆盖一个排位周期。
- 回滚门槛:指标恶化超过示例 5% 或出现阻断性投诉,自动回滚,不等待评审。
灰度比例从 5% 开始,逐步放大到 25%、50%、100%。每一步都要对比对照组,而不是和上周的自己比;版本、活动、买量都会影响指标,只有同周期对照才能分离出手段本身的影响。
三种错误决策
只算节省,不算体验。 账单当月下降,三个月后留存下降,省下的钱不够买回用户。节省必须和体验指标一起看,且体验指标的权重不能低于成本。
把不可逆手段当试点。 删除回放、合并大区、迁移存储,一旦执行就很难回退。可逆性低于 0.5 的手段不应进入快速试点,应该走完整评审和更长灰度。
用平均值掩盖长尾。 平均帧时间没变,P99 却恶化了;平均延迟可接受,特定地区玩家已经掉线。降本手段的体验评估要看 P95、P99 和分地区分品类,不能只看大盘均值。
小结
扩容有拐点,降本有边界。先用固定流量测出成本拐点和体验拐点,再讨论具体手段。tick 降频、区域收敛、日志采样、回放保留期各有适用品类和不可逆程度。评分用 节省 × 可逆性 / (1 + 体验影响),阈值负责一票否决,组合估算负责暴露叠加风险。最后用灰度、对照和回滚门槛把决策变成可撤回的实验。
常见错误有四个:把节省比例直接相加、忽略作用范围重叠、可逆性低的改动快速试点、用平均值代替长尾指标。它们的共同点是把“账面省钱”误当成“系统变好”。
本章从归因、配额走到体验取舍,回答的是“钱花在哪、最多能花多少、省得值不值”。下一章进入合规:版号、实名与防沉迷如何落到架构上,以及它们带来的固定成本。
阅读导航:上一节:9.2 资源配额与混部 · 下一节:10.1 版号、实名、防沉迷的架构落点 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。