1. 前言
这篇博客是记录下1月底给群友部署 Palworld 服务器时所经历的一些折腾。需要折腾的原因大概可以归结为如下两项:
- 首先,这个游戏的服务端实在太烂了。作为大型开放世界游戏,本身就比较吃内存;而开发组糟糕的代码又引入了非常严重的内存泄漏,这两个因素叠加的结果是惊人的内存占用。一开始,我认为事情不必搞得很复杂,于是使用闲置的旧笔记本,采用物理机部署的方式搭建服务器,而那台机器只有 16G 内存,很快我就发现运行一定时间后必定 OOM,此时玩家们就会卡成 ppt,唯一的解决办法就是重启。这期间最严重的一次 OOM 直接把系统搞挂了,由于物理部署的原因,在系统挂掉以后无法通过远程手段抢救,只能等晚上下班回家手动重启电脑,那个白天群友们就没法玩了。
- 此外,选择了在 linux 系统上部署服务,而 linux 上的 steam 命令行工具
steamcmd
不仅不太好用,而且没有游戏版本更新的推送机制。每次版本更新时我都无法立刻得知,只能等群友们发现连接服务器时报错“客户端和服务端版本不一致”,我才可以开始运维操作。而且彼时的运维方式是纯手工执行,要登到服务器上终止服务端程序,然后通过(非常难用的)steamcmd
更新游戏,最后再次手动启动之,麻烦至极。
所以后来就开始了刻不容缓的容器化 + 一键运维改造,其中主要解决两个问题:
- 通过某种手段限制客户端的内存占用,容器化即可,这样就出现严重的内存泄漏也不至于把系统打挂
- 提供一种快捷的服务端重启方式,且把游戏的更新检查包含在其中,这样可以一条指令同时用于释放内存和更新客户端。
2. 整体方案
- 内网穿透:用廉价的 ECS 做公网接入,用性能、内存充足的本地机器运行游戏服务端程序。ECS region 与物理机所在城市尽可能接近,从而减少在中转链路上的网络延时
- 容器化部署:让游戏服务端运行在容器内,且对容器的内存和 swap 设置上限,保证系统稳定;设置容器自动拉起
- 脚本化更新:把客户端更新、服务器启动等逻辑做到容器的启动脚本中,使重启容器就能自动更新版本,并开发一个远程命令执行的接口方便运维。
3. 环境
3.1. 公网接入的云服务器
- ECS:阿里云ECS 99元/年的乞丐款
- 系统:Ubuntu 22.04
3.2. 本地游戏服务器
- 机器:机械师创物者Mini2(专门买了个迷你主机,有点败家了hhh后面再部署些自己的小玩具上去吧)
- CPU:R5-6600H
- 系统:Ubuntu 22.04
- docker 25.0.1
- 内网穿透工具:frp 0.51.2
4. 容器化改造
4.1. 问题分析
在创建容器时我们需要注意以下几个问题:
- 容器中运行的端口需要映射到宿主机上。帕鲁的游戏通信协议是基于 UDP 的,默认服务端口是 8211,为了直观起见直接映射到宿主的 8211 UDP 上。
- 既然是游戏服务器的容器,游戏存档是需要持久化的,不能随着容器的正常或意外关闭而丢失;帕鲁刚刚发布,还处于频繁的更新中,故其服务端程序的版本更新也是需要持久化的,不能每次容器重启以后都要重新安装版本补丁。因此,我们应当将游戏的服务端目录(连带其中的存档)存放在宿主机上,并用 Volume 机制使容器能够访问。
- 限制CPU核数、内存、SWAP等,并设置挂了以后自动拉起。不要给这个糟糕的服务端程序丝毫的信任
在这种设计下,容器本身只是作为一个程序在硬件资源上的“牢笼”,重要的文件都存放在宿主机上,容器本身的文件系统只用于存放客户端更新所需要的steamcmd
。
4.2. 操作
接下来就到了镜像构建的步骤,首先下载steamcmd
的镜像作为我们的 base image,官方的是cm2network/steamcmd。没什么鼓捣 docker 经验的我为了方便 debug,又在里面下载了一些常用的 netstat、vim 等工具,重新打包成镜像steambase_with_cmd_tools
。
随后就涉及到了启动脚本的编写。这里为了偷懒,把脚本也放在了 Volume 的路径下,实际上是可以打包在镜像里的。思路很简单,就是记录下启动时间,通过 steamcmd 检查游戏更新,然后启动游戏服务端。
注意 steamcmd
对于游戏安装目录的修改是不会持久化的,每次使用时都必须加上参数 force_install_dir [path]
来修改,否则他会在默认的路径(大概是 /home/steam/ 吧?记不清了)重新下载一份完整的游戏,而且还会随着容器的关闭而丢失。
#!/bin/bash
# log restart time
file="$(dirname "$0")/output.log"
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "$current_time" >> "$file"
# update palworld server app
/home/steam/steamcmd/steamcmd.sh +force_install_dir /games/palworld +login anonymous +app_update 2394010 validate +quit > /games/palworld/output.log 2>&1 &
# launch game server
/games/palworld/PalServer.sh -useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS > /games/palworld/output.log 2>&1
脚本安排好以后写 dockerfile
就很简单了:
FROM steambase_with_cmd_tools
CMD ["/games/palworld/startserver.sh"]
完成镜像的打包以后,我们就可以启动的容器了,这里把容器命名为palworld,并需要设置 CPU 限制、内存 + swap 限制、端口映射、volume 配置和自动拉起,完整的命令是:
docker run -itd -p 8211:8211/udp --cpus=6 --memory=22g --memory-swap=26g --name palworld -v /home/cococat/files/volume/palworld:/games/palworld --restart always [image name]
这样一来,往后每次需要更新服务端版本时,只需要重启一次容器即可,即
sudo docker restart palworld
5. 参考
https://developer.valvesoftware.com/wiki/SteamCMD#Linux
https://tech.palworldgame.com/getting-started/deploy-dedicated-server