这是一篇开发日志,也夹杂着一些碎碎念。我会聊聊,我们是如何在 Windows 平台上运行 CSGO 服务器,并把 var 最低稳定压到 0.000 的。不过,涉及具体实现的代码细节不会在这里展开
背景
在 Linux 系列服务器上开服时,var 通常都会比较低,甚至可以稳定到 0.000
但随着我们社区硬件不断升级,我发现只要是在 Windows 上开服,var 依旧会稳定在 0.3 左右
这不禁让我思考:为什么 Windows 开服会让 var 长期卡在 0.3 左右?为什么即便更换更强的硬件,结果依然没有变化?
首先可以排除硬件性能瓶颈。毕竟 CSGO 已经是一款十多年前的游戏了,按理说不至于对硬件有如此夸张的要求;而实际测试结果也表明,即使硬件配置继续提升,var 仍然会卡在 0.3 左右
研究
我查阅了一些相关资料,可惜几乎没有找到关于 var 的有效讨论
不过,一篇 12 年前关于 CSGO 更新的讨论,引起了我的注意:
[Csgo_servers] removal of sleep_when_meeting_framerate_headroom_ms
机翻:
最近移除了 `sleep_when_meeting_framerate_headroom_ms` 控制台变量,我想解释一下原因。之前在 Linux 系统上,我们会以毫秒为单位进行睡眠,然后忙等待剩余时间。`sleep_when_meeting_framerate_headroom_ms` 控制台变量允许服务器运维人员通过指定忙等待的时间来控制帧时间的精确性和 CPU 时间的浪费之间的平衡。
我们更改了行为,现在使用 `nanosleep` 来睡眠,直到帧应该开始的确切时间。这样就避免了循环等待。现在无需进行任何权衡。我们会睡眠到帧时间,然后准时唤醒,无需循环等待。在我们的服务器上,这在不影响帧率波动的情况下降低了几个百分点的 CPU 消耗。
此更改还提高了效率,因为服务器进程不再需要每毫秒都唤醒一次来确认是否还有工作时间。降低 CPU 使用率意味着您可以在每台机器上运行更多服务器,或者只是略微降低电费。
`sleep_when_meeting_framerate` 应该始终设置为 true(非零值),因为持续循环会浪费大量的 CPU 时间和电力,而且实际上可能会导致帧率波动更大。
我们有时会移除服务器运维人员多年来一直在使用的控制台变量,却没有解释原因,因为我们没有意识到这些变量的使用频率很高。我们尽量只在真正不需要的时候才移除控制台变量,希望这个解释能让您明白。如果还有不明白的地方,请告诉我。
进一步定位
翻完那段讨论后,我的思路一下子清晰了不少
Valve 在公开的 csgo-demoinfo 协议定义中,给 host_framestarttime_std_deviation 这个字段写了一句非常关键的注释:Host frame start time stddev in usec (1/1,000,000th sec) - measures how precisely we can wake up from sleep when meeting server framerate。[1] 翻译:它衡量的是服务器在满足目标帧率时,从 sleep 中唤醒的精确程度
这句话其实已经把方向点明了:这里的 var,看的就是苏醒精度。如果顺着这个思路去看 Windows,很多现象就都能对上了
放到 Windows 版 CSGO 里,这段“等下一帧”的休眠路径在引擎接口层通常会先经过 ThreadSleep 这一层封装。但封装并不会改变本质:只要底层仍然走的是 Windows 的休眠与调度路径,那么它最终就还是要吃 Windows 这套苏醒机制的精度上限。
微软官方对 Sleep 的描述也很直接:线程休眠建立在系统时钟分辨率之上;如果请求的休眠时间小于系统时钟分辨率,那么实际休眠时间就可能偏离目标值。即便休眠时间已经过去,线程也只是“变为可运行”,并不保证立刻获得调度。换句话说,Sleep 只能让线程先睡下去,但不能保证它在目标时刻准确醒来 [2]
问题到这里,其实就已经很明白了
64 tick 服务器每帧预算大约是 15.625 ms,128 tick 则只有 7.8125 ms。对于这种固定频率的主循环来说,只要服务器没法在“该醒来的那个时刻”准确醒来,var 就一定会高。这里的关键根本不在于这一帧算得快不快,而在于下一帧是不是准时开始
这也就解释了为什么继续堆硬件往往没有效果。(只要“醒不准”这个问题还在,var 就还是下不去)
反过来看 Linux,当年的讨论中讲得很直白:他们把等待逻辑改成了直接睡到“帧应该开始的准确时间点”,并且使用 nanosleep。[3] 而 Linux 的 nanosleep 本身就提供更高精度的休眠语义,再结合 Linux 内核长期提供的高精度定时器机制,这条路径天然更适合拿来做“准点醒来”这件事 [4][5]
所以,这里的结论其实可以说得很直接:Windows 版 CSGO 服务器 `var` 偏高,核心底层原因就是默认休眠路径的苏醒精度不够;Linux 之所以更容易把 `var` 压得很低,本质上就是它更容易在目标时刻准确醒来
后面我们要处理的重点,不再是继续堆硬件,也不是继续压榨帧内逻辑,而是想办法把“等待下一帧”的这段过程变得更可控、更接近 Linux 下的行为。而我们后续所有优化,基本也都是围绕这个目标展开的。在我们彻底修补了 CSGO 的底层调度后,我们成功达到了目的
