2 分•作者: bardhyliis•4 个月前
各位 HN 的朋友们,我从零开始构建了一个游戏服务器编排器。作为一名独立开发者,我花了三年多的时间,每天近十个小时才最终完成它,我从 2023 年初开始着手。我现在 26 岁!
完成这个项目所需要研究的复杂性和技术细节,是我做梦也想不到的,但它确实完成了,这是我迄今为止最伟大的职业成就。
下面我将尝试介绍我的游戏服务器编排器的一些核心和最重要的功能。
1. **核心固定与 CCD 缓存对齐**
我研究并理解了 CPU 缓存的布局。我发现,如果我的游戏容器(使用 `docker run`)跨越不同的核心复杂芯片(CCD)或者与繁忙的邻居共享 SMT 同步线程,L3 缓存的颠簸会严重影响单核滴答效率。
然后,我使用 GRUB 将所有非游戏服务器进程严格固定在核心 0 和其 SMT 同步线程核心 12 上:
我禁用了 1000Hz 的定时器中断,以防止上下文切换,从而避免污染 L3 缓存。
我还将 RCU 卸载到核心 0 和 12,以避免对游戏容器产生任何微中断,将 100% 的性能留给游戏容器。
```
GRUB_CMDLINE_LINUX_DEFAULT="nomodeset isolcpus=1-11,13-23 nohz_full=1-11,13-23 rcu_nocbs=1-11,13-23"
```
至于游戏容器,正如我所提到的,我直接使用 `docker run`,因为 Swarm 是不必要的,而且实际上是糟糕的设计。我有一个编排器服务,它使用一个算法来计算哪个 CCD 核心最适合固定游戏服务器容器:
```
// Zen 4 核心复杂芯片 (CCD) 映射 (C#)
int siblingOffset = totalHardwareThreads / 2;
int coresPerCcd = siblingOffset / 2;
int getCcdId(int i) => ((i % siblingOffset) < coresPerCcd) ? 0 : 1;
int getSibling(int i) => (i < siblingOffset) ? (i + siblingOffset) : (i - siblingOffset);
```
我还设置内存限制和内存预留相等(`--memory == --memory-reservation`),以便内核将该 RAM 内存锁定为物理 RAM 并阻止交换使用,以避免“吵闹的邻居”问题。
正如您所见,编排器试图为游戏服务器找到性能最佳的线程,这意味着宿主机 CPU 会被碎片化。特别是对于这种情况,我有一个算法,可以在宿主机上模拟每个正在运行的游戏容器的最佳位置,然后动态地、实时地重新定位部分或全部容器,而无需重启容器或断开任何活动玩家的连接,使用:
```
docker update --cpuset-cpus="{cpuSet}" {containerName}
```
2. **eBPF/XDP + NFTABLES 利用以防止 DDoS 攻击**
由于游戏服务器经常受到 DDoS 攻击、机器人或其他攻击的轰炸,特别是因为各种原因,可能是所谓的“脚本小子”或者有时甚至是“输不起的玩家”,XD。
起初我尝试使用 UFW,但最终放弃了,因为它与 Docker 冲突,这一点我花了不少时间才意识到,因为我当时还在研究网络层的工作原理。
为了获得最佳保护,我决定为每个端口设置特定的连接速率限制。如果达到限制,我将使用黑名单,将攻击者的 IP 注册到其中,并设置一个特定的计时器,然后立即将这些被列入黑名单的 IP 注册到 eBPF 映射中。这些 IP 在禁令到期时会动态地从每个列表/映射中添加和删除。
不过,有一个 `AnonymousPipeClientStream` 的边缘情况,许多游戏有许多不同的模组和插件,这会增加数据包的速率。尽管我已尽力在默认速率限制规则中考虑到了这一点,但也允许游戏服务器所有者根据需要调整这些限制,类似于 Cloudflare 的方式,提供 4 种配置文件:标准、宽松、严格、攻击中。
我已经根据实际数据尽可能地优化了标准配置文件,它应该足以应对 99% 的服务器。其他配置文件可以在其他罕见情况下使用,例如用于大量模组的游戏服务器。
因此,DDoS 缓解的最佳方法是使用 nftables,并为每个游戏服务器端口设置限制,每个游戏端口都有 nftables 限制。
我还将 `rmem_max`/`wmem_max` 缓冲区增加到 16MB,这样特定的游戏容器线程在将映射数据直接注册到 RAM 时就不会阻塞。默认情况下,写入缓冲区非常小,大约 200KB。通过这样做,玩家滴答处理得更快。
由于用户需要管理游戏文件(上传/下载/编辑/删除等),我使用 fireqos 来优先处理游戏流量,这意味着游戏流量走“快车道”,并且永远不会因为客户端使用文件管理器进行操作而被限速,从而确保游戏保持无 ping 尖峰。
我还使用 TCP BBR 拥塞控制,而不是默认的 Linux CUBIC,后者未经优化,会导致“回弹”(rubber-banding),因为它假设游戏服务器和玩家之间存在丢包就一定是网络拥塞,结果降低了传输速度,从而导致延迟尖峰。
BBR 拥塞控制的作用是测量游戏服务器和玩家之间的实际带宽,并以玩家能够消耗的速度发送数据包,从而避免回弹。
我还使用 fq(公平队列),以避免单个游戏服务器所有者耗尽所有带宽,例如,当有人决定上传或下载大文件时。
```
# BBR 拥塞控制
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# UDP/TCP 缓冲区扩展
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 16777216
net.core.wmem_default = 16777216
```
3. **SSR 缓存投毒解决方案**
为了避免 Angular SSR 缓存投毒,我有两个端点:`/graphql` - 公共的、只读的数据,直接缓存在 Cloudflare 上。此端点会立即拒绝任何认证头,通过拒绝整个请求,以防止缓存投毒并防止请求之间的任何状态共享。
第二个端点是 `/secure`,处理任何经过身份验证的数据,并且不缓存任何内容。此外,我所有的 Web 服务,如前端、API、数据库调用都使用我的私有 WireGuard 网格,增加了安全性。
此外,在 Node.js 的 SSR 过程中,我完全跳过了 TLS 握手,通过使用本地 Docker Swarm 网络直接访问我的 API,这增加了一点延迟。
-----
正如我所提到的,我是一名独立开发者,完全自费启动这个项目。我拥有两个裸金属节点,一个在欧洲,一个在美国中部。
今天,我的目标是在扩展之前,观察我的编排器在真实世界中的表现。因此,我邀请任何人通过使用我的免费试用版来启动一个游戏服务器,并尝试破坏我的系统。
如果有人愿意,可以直接访问 <a href="https://ray-hosting.com/en-US/free-trial" rel="nofollow">https://ray-hosting.com/en-US/free-trial</a> 注册以自动获得免费试用。它需要信用卡,仅用于防止滥用。或者,如果您不想提供信用卡(这是可以理解的),在我从我的管理面板直接为您启动试用后,您可以在注册后进行测试。只需在此处留下评论,我今天会关注此帖子。我非常希望听到关于架构、部署速度或您想讨论的任何其他方面的诚实想法和意见。
附注:我不是以英语为母语的人,所以写这个花了我很多时间,哈哈。另外,我的平台还有很多东西可以讲,但现在我有点累了。哈哈,非常感谢您的阅读。