☰
从零搭建幻兽帕鲁专用服务器:Shell 脚本部署、启停、备份与排障
2026/10/28 6:48:03 网站建设 项目流程

上周把角落里那台吃灰的小主机重新点亮,装好系统,专门拿来跑《幻兽帕鲁》的专用服务器。几个朋友约好周末开荒,公共房不是人多卡成幻灯片,就是房主一关服存档全没,玩了两天实在受不了,索性自己搭。搭的过程比想象中顺,真正花时间的不是游戏本身,而是那套部署用的 Shell 脚本——安装、启动、停止、备份、更新、定时重启,一个都不能少,少一个后面就得半夜爬起来救火。这篇就把我这一整套从零到跑通的流程摊开讲:机器怎么选、目录怎么划、配置里那行长得离谱的OptionSettings怎么改、脚本每一段为什么这么写、踩了哪些坑。适合两类人看:一类是想给自己和朋友开个稳定帕鲁服的玩家,另一类是像我一样,想借一个真实场景把 Shell 脚本和 Linux 服务托管练熟的同学。不需要你已经是运维老手,会基本的cd、ls、vim就够跟上,我不会跳过任何一步。

1. 整体方案拆解:为什么值得自己搭一台

1.1 自建服和挤公共房的真实差别

先把结论摆出来:如果你只是想进去玩两个小时看看画风,公共房完全够用,别折腾。但只要你们是固定几个人、打算长期玩、想调倍率,那自建几乎是唯一解。差别主要在四个地方。第一是存档归属,公共房的存档在别人机器上,房主不玩了、机器欠费了、硬盘挂了,你几十个小时的基地瞬间归零,这种事儿在玩家群里几乎每周都有人哭。第二是规则自由度,经验倍率、捕捉率、死亡惩罚、公会人数上限,这些都在配置文件里,公共房你只能接受房主的口味。第三是稳定性,公共房通常是家宽 + 一台普通电脑,上行带宽就那么点,人多必然卡,而且没法做定时重启、没法做自动备份。第四是管理权限,踢人、发物品、改时间、看在线列表,自建服才有。

我这套方案的核心目标很朴素:一台机器、一个普通用户、一套脚本、一键起停、每天自动备份、崩了自动拉起。不追求高可用集群,也不搞容器编排,就是最朴素的"能稳定跑、出事能救回来"。这个定位很重要,因为它决定了后面所有的技术选型——不引入额外复杂度,能用 shell + systemd 解决的,就不上别的东西。

1.2 机器规格怎么定:从人数倒推资源

这一步是新手最容易拍脑袋的地方。我看到过有人拿 1 核 2G 的入门机器去开八人服,结果进游戏走两步卡一下,还以为是网络问题。帕鲁的专用服务端是单进程的,主要吃单核性能和内存,对多核的利用率不高,所以你买 8 核不如买 4 个高主频的核。

内存是重灾区。服务端存在比较明显的内存增长问题,跑十几个小时之后占用会往上飘,所以规划的时候要留足冗余,而不是卡着线买。我按实际体感整理了一张表,多人同时在线、有人在建大基地的情况下:

同时在线人数建议内存建议 CPU磁盘备注
2 到 4 人8 GB2 核高主频30 GB SSD最低配,建议每天重启一次
4 到 8 人16 GB4 核50 GB SSD我目前用的档位,比较稳
8 到 16 人32 GB4 到 6 核80 GB SSD需要配合定时重启和内存监控
16 人以上32 GB 起6 核以上100 GB SSD建议拆分玩法区域或限制基地数量

磁盘一定要用 SSD,别用机械盘。服务端启动和存档写入都是密集的随机 IO,机械盘不只是慢,存档写入期间还容易出现卡顿甚至写入超时。另外要预留出备份空间,一份完整存档在玩了几十小时后可能有几百 MB,按保留 7 份算,至少留出 5 GB 的余量。

带宽这块容易被忽略。每人同步的数据量不大,但同时移动、打 Boss 的时候会有瞬时峰值。我的经验是按每个在线玩家 100 KB/s 的上行去估算,8 个人就要准备接近 1 MB/s(约 8 Mbps)的稳定上行。家用宽带的上行往往只有 20 到 30 Mbps,理论够用,但如果是共享网络、晚上有人看视频,抖动就会很明显——这也是我最后选择把服务放在独立机器上、和家里网络隔离的原因。

1.3 为什么坚持用 Shell 脚本而不是手工敲命令

有人会问:装一次不就行了,写脚本是不是过度设计?我一开始也是手工敲的,结果第二次换机器重装,光是回忆"当时那个参数是啥、那个目录放哪"就花了半小时,中间还漏了一步导致启动直接失败。从那之后我改了思路:凡是会做第二遍的事,就写成脚本。

脚本在这个场景里的价值有三层。最浅的一层是省事,一条命令起服、一条命令停服、一条命令备份。中间一层是可复现,换机器、换系统、帮朋友搭,把脚本拷过去改两个变量就能跑,不会出现"我这能跑你那不能跑"的扯皮。最深的一层是知识固化,脚本里每一行都是你当时踩坑之后定下来的规矩,比如"停止前必须先让服务端自己保存再退出",这种事写在文档里没人看,写在脚本里就永远生效。

所以这篇的重点其实是一半玩、一半学。游戏是引子,脚本是主线。等你把这一套写完,你会发现它和部署任何一个后台服务没有本质区别:准备环境、拉取程序、写配置、拉起进程、做守护、做备份、做日志轮转。这套骨架换个程序照样能用。

2. 环境准备:把地基打平再动手

2.1 系统选择与基础依赖的取舍

系统我选的是 Ubuntu Server 的 LTS 版本,理由很实际:社区资料多,出问题搜得到;包管理成熟,依赖不缺;长期支持周期长,不用半年重装一次。Debian 系也完全没问题,命令基本一致。不建议用桌面版系统,多出来的图形环境白占内存,而且默认开了不少无关服务。

装完系统第一件事不是急着下游戏,而是建一个专用用户。游戏服务端是来路可疑的第三方程序(相对系统而言),绝对不要用 root 跑。我建的用户就叫pal,家目录/home/pal,所有东西都放它下面。这样即使程序被爆,损失也被限制在这个用户能碰到的范围内。

# 建用户并设置家目录 sudo adduser --disabled-password --gecos "" pal sudo usermod -aG sudo pal # 需要时再提权,平时不用 # 切过去 sudo -iu pal

然后是依赖。帕鲁的服务端依赖 32 位的运行库,因为下载工具本身是 32 位程序。这一步漏了会报一个非常典型的错误,后面排查章节会讲:

sudo dpkg --add-architecture i386 sudo apt update sudo apt install -y lib32gcc-s1 lib32stdc++6 curl tar ca-certificates

lib32gcc-s1是必须的,lib32stdc++6是保险起见。curl和tar后面脚本里要用。ca-certificates是证书链,缺了会导致下载工具握手失败,报一堆看不懂的 TLS 错误。

提示:如果你用的是精简版系统镜像,add-architecture这一步可能还会要求你安装gcc-multilib,按提示装就行,别硬扛。

2.2 目录结构与端口规划:先画图再动手

我见过最乱的一种情况是:下载目录、服务端目录、存档目录、备份目录全堆在家目录里,时间一长自己都不知道哪个能删。所以动手之前先把目录树画出来,这一步花五分钟,能省后面无数麻烦。

/home/pal/ ├── steamcmd/ # 官方命令行下载工具,独立目录 │ └── steamcmd.sh ├── server/ # 服务端本体,可整目录删掉重下 │ ├── PalServer.sh │ └── Pal/ │ └── Saved/ # 存档和配置,绝对不能随便删 │ ├── Config/ │ │ └── LinuxServer/ │ │ └── PalWorldSettings.ini │ ├── SaveGames/ │ └── Logs/ ├── scripts/ # 我写的所有脚本 │ ├── install.sh │ ├── start.sh │ ├── stop.sh │ ├── backup.sh │ └── update.sh ├── backups/ # 备份落盘位置 └── logs/ # 服务端标准输出日志

这个划分的关键是把"可重建的"和"不可重建的"分开。server/整个目录都是可以从网上下回来的,删了重下就行;Pal/Saved/里的存档是唯一不可再生的东西,backups/是它的副本。有了这个前提,你后面做更新、做回滚、做清理,心里就有数了——可以放心大胆删的只有server/,别的一律先备份。

端口规划同样重要。帕鲁服务端默认涉及三个端口:

端口协议用途是否必须放行
8211UDP游戏主通信端口必须
27015UDP服务器查询/列表上报端口上公网列表时需要
25575TCPRCON 远程管理端口按需,建议只对内网开放

我踩过的第一个大坑就在这:主端口是 UDP 不是 TCP。很多教程只写"放行 8211",你按习惯放行了 TCP,结果服务起来了、日志看着也正常,就是死活连不上。所以防火墙规则一定要写清楚协议:

sudo ufw allow 8211/udp sudo ufw allow 27015/udp # RCON 只允许内网访问,不要开公网 sudo ufw allow from 192.168.1.0/24 to any port 25575 proto tcp sudo ufw reload

注意:如果你在云主机上,除了系统防火墙,还要看控制台里的安全组规则。两层防火墙任意一层没放通,表现都是"连不上",排查时两层都要确认。

2.3 配置文件里那行长得离谱的 OptionSettings

帕鲁的配置方式很特别,所有游戏规则全挤在PalWorldSettings.ini里的一行OptionSettings=(...)里,用逗号分隔一堆Key=Value。第一次打开文件的人基本都会愣住,因为默认文件里那一行是空的或者被注释掉的,你需要在server/Pal/Saved/Config/LinuxServer/下自己创建这个文件。

正确做法是先找模板。服务端首次启动后会在同级目录生成一个DefaultPalWorldSettings.ini,里面有一份完整的默认值。把它拷成正式的配置文件名,然后改你需要的那几项:

cd ~/server/Pal/Saved/Config/LinuxServer/ cp DefaultPalWorldSettings.ini PalWorldSettings.ini

打开之后你会看到类似这样一坨东西(我只截了关键部分):

[/Script/Pal.PalGameWorldSettings] OptionSettings=(Difficulty=None,DayTimeSpeedRate=1.000000,NightTimeSpeedRate=1.000000,ExpRate=1.000000,PalCaptureRate=1.000000,PalSpawnNumRate=1.000000,DeathPenalty=All,bEnablePlayerToPlayerDamage=False,bEnableFriendlyFire=False,bIsPvP=False,CoopPlayerMaxNum=4,ServerPlayerMaxNum=32,ServerName="MyPalServer",ServerDescription="",AdminPassword="change_me",ServerPassword="",PublicPort=8211,PublicIP="",RCONEnabled=False,RCONPort=25575,bUseAuth=True,bIsUseBackupSaveData=True)

这里有三条铁律,违反任何一条都会让服务器启动失败或者配置不生效:

  1. 整行不能断开换行。这一行必须完整地写在一行里,中间插入回车会导致解析失败。
  2. 不能加注释。INI 里别的地方可以;注释,但这行括号内部不行,加了解析器会把它当成值的一部分。
  3. 字符串值必须用双引号包住,数字和布尔值不加引号。ServerName="我的世界"对,ServerName=我的世界错。

针对我们五六个朋友一起玩的场景,我做的调整主要是这几项,列出来给你参考:

配置项默认值我的值为什么这么改
ExpRate1.01.5上班族时间有限,不想刷级太肝
PalCaptureRate1.01.2稍微提高,减少空手球的挫败感
DeathPenaltyAllItem只掉物品不掉装备,保住开荒体验
ServerPlayerMaxNum328按实际人数设,避免资源被空位浪费
bIsPvPFalseFalse合作服,关掉玩家互伤
RCONEnabledFalseTrue我要用 RCON 做优雅备份和公告
AdminPassword空强密码有管理密码才能用管理命令
bIsUseBackupSaveDataTrueTrue服务端自带的备份,多一层保险

改完记得重启服务端才生效,改文件热加载是不存在的。另外AdminPassword和ServerPassword一定不要用弱口令,公网暴露的服务端会被人扫到,我见过被恶意进服破坏基地的情况。

3. Shell 脚本拆解:安装、启停、备份三板斧

3.1 安装脚本:把下载器包起来

安装这一步的本质就是"用官方命令行下载工具拉取指定应用的最新版",但直接敲命令会有几个问题:路径写死、失败不重试、装完不校验。所以我把它们包进一个install.sh里。

#!/usr/bin/env bash # install.sh - 首次安装 / 修复安装 set -euo pipefail STEAMCMD_DIR="${HOME}/steamcmd" SERVER_DIR="${HOME}/server" APP_ID="2394010" # 服务端应用 ID,以官方文档为准 log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*"; } mkdir -p "${STEAMCMD_DIR}" "${SERVER_DIR}" # 1. 下载工具不存在则拉取 if [[ ! -x "${STEAMCMD_DIR}/steamcmd.sh" ]]; then log "下载命令行下载工具" curl -fsSL https://example.invalid/steamcmd_linux.tar.gz \ | tar -xz -C "${STEAMCMD_DIR}" fi # 2. 安装 / 更新服务端 log "开始安装服务端,首次约需 5-10 分钟" "${STEAMCMD_DIR}/steamcmd.sh" \ +force_install_dir "${SERVER_DIR}" \ +login anonymous \ +app_update "${APP_ID}" validate \ +quit log "安装完成,服务端位于 ${SERVER_DIR}"

逐段说一下为什么这么写。

set -euo pipefail是 shell 脚本的保命符。-e让任何一条命令失败就立刻退出,避免在错误状态下继续往下跑;-u让引用未定义变量直接报错,防止rm -rf "${DIR}/"里DIR没赋值变成rm -rf /这种事故;pipefail让管道里任意一环失败都能被捕获,因为默认情况下管道的退出码只看最后一个命令。

日志函数用printf而不是echo,因为echo在不同 shell 里对转义字符的处理不一致,printf行为稳定。加上时间戳,是为了后面把输出重定向进文件时能对得上时间线。

+login anonymous表示匿名登录。这一步不需要你有任何账号,服务端的下载是公开的。validate参数会校验文件完整性,多花一点时间,但能修好那些"文件下了一半断了"的诡异问题——我第一次装的时候就遇到过,服务端启动报缺文件,加上validate重跑一遍就好了。

脚本设计上我遵循一个原则:可重复执行。你在已经装好的机器上再跑一遍install.sh,它的行为是"更新到最新并校验",不会把存档删掉。这就是幂等,运维脚本的基本素养。

3.2 启停脚本:优雅退出比启动更重要

启动很简单,难的是停止。我最初的stop.sh就一行pkill -f PalServer,结果有一次在朋友刚存完物资的时候停服,再开服发现存档回档了十几分钟。原因很直白:pkill默认发的是 SIGTERM,服务端收到后不一定会走完整的"保存并退出"流程,而游戏存档是定时写盘的,你打断它,最后一段就没落盘。

所以正确的停止顺序是:先通过 RCON 让服务端保存一次,等几秒,再发 SIGTERM,最后超时才 SIGKILL。先上启动脚本:

#!/usr/bin/env bash # start.sh set -euo pipefail SERVER_DIR="${HOME}/server" LOG_DIR="${HOME}/logs" PID_FILE="${HOME}/server/.pal.pid" mkdir -p "${LOG_DIR}" # 已在运行就不重复启动 if [[ -f "${PID_FILE}" ]] && kill -0 "$(cat "${PID_FILE}")" 2>/dev/null; then echo "服务端已在运行,PID=$(cat "${PID_FILE}")" exit 0 fi cd "${SERVER_DIR}" nohup ./PalServer.sh \ -port=8211 \ -players=8 \ -useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS \ >> "${LOG_DIR}/server-$(date +%F).log" 2>&1 & echo $! > "${PID_FILE}" echo "已启动,PID=$(cat "${PID_FILE}")"

几个启动参数值得单独说。-port和-players会覆盖配置文件里的同名项,命令行优先级更高,所以改人数的时候这两处要一致,否则你会疑惑"我明明在 ini 里改了 8 个人,怎么还能进 20 个"。后面三个是社区里广泛使用的一组合集:-useperfthreads让服务端用多线程处理部分工作,-NoAsyncLoadingThread关掉异步加载线程以减少加载阶段的内存尖峰,-UseMultithreadForDS针对专用服务端开启多线程。这三个参数在不同版本上的实际收益不太一样,但整体上对降低卡顿有帮助,我实测八人服开与不开,基地里跑动的帧率确实有差异。

nohup ... &加$!存 PID,是最朴素的后台托管方式。它的缺点很明显:机器重启不会自动拉起,进程崩了不会自动重启。所以后面还有 systemd 那一层兜底。脚本版适合临时调试,systemd 才是正式方案。

停止脚本:

#!/usr/bin/env bash # stop.sh - 优雅停止 set -euo pipefail PID_FILE="${HOME}/server/.pal.pid" RCON_PORT=25575 pid_of_server() { if [[ -f "${PID_FILE}" ]] && kill -0 "$(cat "${PID_FILE}")" 2>/dev/null; then cat "${PID_FILE}" else pgrep -f 'PalServer-Linux-Test' || true fi } PID="$(pid_of_server)" [[ -z "${PID}" ]] && { echo "没有在运行的服务端"; exit 0; } # 1. 先让服务端自己保存(需要开启 RCON) if command -v rcon >/dev/null 2>&1; then echo "请求服务端保存存档..." rcon -a 127.0.0.1:${RCON_PORT} -p "${RCON_PASSWORD:-}" "Save" || true sleep 10 fi # 2. 正常信号 echo "发送 SIGTERM 到 PID ${PID}" kill -TERM "${PID}" # 3. 最多等 30 秒 for i in $(seq 1 30); do kill -0 "${PID}" 2>/dev/null || { echo "已停止"; rm -f "${PID_FILE}"; exit 0; } sleep 1 done # 4. 兜底强杀 echo "超时未退出,强制结束" kill -KILL "${PID}" 2>/dev/null || true rm -f "${PID_FILE}"

这段代码里最值得学的是那个for循环等待。很多脚本写完kill就直接往下跑了,可进程退出是异步的,你后面的备份操作可能在进程还没完全退出时就开始打包,结果打到一半的文件。加上轮询等待,才算是真正把"停"这个动作做完了。30 秒这个值是拍脑袋定的,但对帕鲁这种规模的进程够用;如果你的服务端存档特别大,可以适当加长。

心得:Save命令执行后到存档真正落盘之间还有一点点延迟,所以我留了sleep 10。这个等待时间宁可长一点,也别省。我曾经把这段去掉试过一次,十次里有两次出现轻微回档。

3.3 备份脚本:备份不做校验等于没备份

备份是我认为比启动更重要的脚本,因为它决定了你出事之后能不能救回来。我的备份策略分三层:服务端自带备份(bIsUseBackupSaveData=True)作为第一层,脚本定时打包作为第二层,异地拷贝作为第三层。

#!/usr/bin/env bash # backup.sh set -euo pipefail SAVE_DIR="${HOME}/server/Pal/Saved" BACKUP_DIR="${HOME}/backups" KEEP_DAYS=7 STAMP="$(date +%Y%m%d-%H%M%S)" TARGET="${BACKUP_DIR}/pal-save-${STAMP}.tar.gz" mkdir -p "${BACKUP_DIR}" # 先做一致性处理:优先停服备份,做不到就至少触发保存 if [[ -f "${HOME}/server/.pal.pid" ]]; then echo "检测到服务端运行中,先触发保存" rcon -a 127.0.0.1:25575 -p "${RCON_PASSWORD:-}" "Save" || true sleep 10 fi tar -czf "${TARGET}" -C "$(dirname "${SAVE_DIR}")" "$(basename "${SAVE_DIR}")" # 校验:能列出内容且大小合理才算成功 if ! tar -tzf "${TARGET}" > /dev/null 2>&1; then echo "备份文件损坏:${TARGET}" >&2 exit 1 fi SIZE_MB=$(( $(stat -c%s "${TARGET}") / 1024 / 1024 )) if (( SIZE_MB < 1 )); then echo "备份文件异常偏小(${SIZE_MB}MB),请检查" >&2 exit 1 fi echo "备份完成:${TARGET}(${SIZE_MB}MB)" # 清理超过保留期的旧备份 find "${BACKUP_DIR}" -name 'pal-save-*.tar.gz' -type f -mtime +${KEEP_DAYS} -delete echo "已清理 ${KEEP_DAYS} 天前的备份"

这里有三处是我踩坑之后才加的,你可以直接抄。

第一是打包后立刻校验。tar -tzf会把归档内容列一遍,能列出来说明 gzip 头和文件结构没坏。这一步的意义在于把"备份失败"提前暴露,而不是等你要恢复的时候才发现包是空的。第二是大小下限检查。有一次磁盘满了,tar只写进去一个头就报错退出,但因为脚本里没有检查,set -e也没拦住(tar在某些情况下返回 0),结果那个 20 KB 的"备份"在目录里躺了一个月。第三是清理用-mtime而不是自己算文件名,简单可靠,跨月跨年都不会出问题。

定时任务这么写:

# crontab -e # 每天凌晨 4:30 备份,日志单独留存 30 4 * * * /home/pal/scripts/backup.sh >> /home/pal/logs/backup.log 2>&1 # 每天凌晨 5:00 重启一次,缓解长时间运行的内存增长 0 5 * * * /home/pal/scripts/stop.sh && sleep 5 && /home/pal/scripts/start.sh

重启放在备份之后,是有意的顺序:先备份再重启,万一重启出问题,你手上至少有一份新鲜存档。这个顺序反过来的话,重启失败你就只能拿昨天的备份了。

异地那份我做得比较土:备份完成后用rsync同步到另一台常开的机器上,走内网,不带任何敏感配置。它不需要多快,只要"另一块物理硬盘上也有一份"就够了。同一个盘上放十份备份,盘挂了还是全没。

3.4 用 systemd 把脚本变成真正的系统服务

脚本版的问题是显而易见的:机器重启后服务不会自己起来,进程意外退出也没人管。正规做法是交给系统的服务管理器托管。写一个单元文件:

# /etc/systemd/system/palserver.service [Unit] Description=Palworld Dedicated Server After=network-online.target Wants=network-online.target [Service] Type=simple User=pal Group=pal WorkingDirectory=/home/pal/server ExecStart=/home/pal/server/PalServer.sh -port=8211 -players=8 -useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS ExecStop=/home/pal/scripts/stop.sh Restart=always RestartSec=15 TimeoutStopSec=120 KillSignal=SIGINT StandardOutput=append:/home/pal/logs/server.log StandardError=append:/home/pal/logs/server.err.log [Install] WantedBy=multi-user.target

几个参数的选择理由。Restart=always配合RestartSec=15是崩溃自愈的关键,15 秒的间隔是为了避免程序启动即崩导致的疯狂重启把机器拖垮。TimeoutStopSec=120给停止留足时间,因为ExecStop里我们还有保存和轮询等待。KillSignal=SIGINT这个要特别注意——帕鲁的服务端对 SIGINT 的处理比 SIGTERM 更接近"正常退出",我对比过好几次,用 SIGINT 停止时存档完整性明显更靠谱。

启用和查看:

sudo systemctl daemon-reload sudo systemctl enable --now palserver sudo systemctl status palserver journalctl -u palserver -f # 实时看日志

用了 systemd 之后,前面那个start.sh就可以退居二线了,只在你需要临时调试、不想动系统服务的时候用。日常运维统一走systemctl start/stop/restart,这样日志、开机自启、崩溃重启全都自动处理,不用自己维护 PID 文件。这是我认为整套方案里性价比最高的一步改造。

4. 排查实录:那些把我按在地上摩擦的坑

4.1 起不来、连不上:按顺序排查才不会乱

这类问题看着五花八门,其实排查是有固定顺序的,从内到外一层层收敛最快。我的习惯是:

第一步,看进程还在不在。systemctl status palserver显示active (running)才算起来了。如果显示activating然后不停重启,说明启动即崩,直接去看journalctl -u palserver -n 200。

启动即崩最常见的三个原因:32 位运行库没装(报错里会出现libstdc++.so.6相关的 32 位找不到)、端口被占用(bind: address already in use)、配置文件格式错误(那一行OptionSettings换行了或者引号不配对)。第三种最难看出来,因为报错往往非常含糊,我建议改完配置之后先用一个干净的最小配置启动一次,确认能起来,再逐项加参数。

第二步,看端口有没有真的在监听。

ss -lunp | grep 8211 # UDP 用 -u ss -ltnp | grep 25575 # TCP 用 -t

这一步能直接分辨是"服务没起来"还是"起来了但没监听对端口"。曾经有一次我改了-port参数但忘了改配置文件里的PublicPort,结果进程监听在 8211,上报给列表的却是另一个端口,表现就是"能连但列表里看不到"。

第三步,从外面探测。在同一内网的另一台机器上试连,能连说明服务本身没问题,问题在网络边界。这时再回头检查两层防火墙——系统防火墙和安全组,UDP 有没有放对。我遇到过的真实情况是:系统防火墙放行了 TCP,安全组放行了 UDP,两边各对一半,合起来就是不通。

第四步,确认公网 IP 的填法。如果你的机器在 NAT 后面,PublicIP要么留空让服务端自动判断,要么明确填对外的地址。填错了的表现是社区列表里能看到服务器,但点进去一直转圈。

4.2 卡顿、掉帧、存档回滚

跑起来之后的问题主要分两类:性能类和数据类。

性能类的典型症状是"人一多就卡,基地里走动像慢动作"。除了前面说的定时重启,还有几个可以立刻做的调整:把PalSpawnNumRate调回 1.0(很多人为了热闹调高,帕鲁数量翻倍对 CPU 压力是线性上升的);限制单个公会的基地数量;把DropItemMaxNum从默认 3000 往下调,地上掉落物太多会持续消耗资源。我用top观察过,卡顿的时候 CPU 单核基本跑满,内存也在高位,属于典型的资源打满,除了加配置没太多办法。

数据类问题更吓人,就是"回档"。除了前面说的优雅停止,还有一个隐蔽的原因:多实例写同一个存档目录。我有一段时间用start.sh手动起了一个,又用 systemd 起了一个,两个进程同时读写SaveGames,存档直接错乱。排查方法很简单,ps aux | grep PalServer数一下进程数量,超过一个就先全杀掉再重新起一个。

另一个是磁盘满导致的写入失败。建议加一条简单的监控:

# 磁盘低于 10% 时记录告警到日志 USE=$(df --output=pcent /home/pal | tail -1 | tr -dc '0-9') if (( USE > 90 )); then echo "[$(date '+%F %T')] 警告:磁盘使用率 ${USE}%" >> /home/pal/logs/alert.log fi

把它挂在每小时执行的定时任务里,比事后发现存档写不进去强得多。

4.3 常见问题速查表

把上面这些整理成一张表,出问题的时候按图索骥比翻聊天记录快得多:

现象最可能的原因快速验证方式处理动作
服务启动后立即退出缺 32 位运行库看日志里libstdc++.so.6报错安装lib32gcc-s1等依赖
进程在跑但连不上端口协议放错或未放行ss -lunp看监听 + 两层防火墙放行 UDP 8211
列表能看到但进不去PublicIP填错或 NAT 问题内网直连测试清空PublicIP或填对外地址
配置改了不生效OptionSettings格式错用最小配置启动验证检查换行、引号、注释
玩一段时间开始卡内存增长 / CPU 打满top看单核与内存定时重启、降低刷新密度
存档回档十几分钟强杀进程未保存查停止脚本与日志改优雅停止流程
存档错乱或丢失多实例同时写ps aux数进程杀多余进程,统一用 systemd
备份文件只有几十 KB磁盘满或打包中断tar -tzf校验清理磁盘,加校验逻辑
夜间无故掉线家宽上行抖动观察掉线时间规律换独立网络或限制同时在线

5. 脚本之外的收获:把它当成一次小型运维练习

5.1 这套骨架其实和游戏无关

写完之后我最大的感受是,这套东西换个程序照样能用。安装脚本的本质是"从某个源拉取程序并校验",启停脚本的本质是"管理一个长期运行的进程",备份脚本的本质是"处理不可再生的数据并验证",systemd 单元的本质是"把进程托管给系统"。这四件事是所有后台服务的公共部分,游戏只是把它变得有意思了一点。

如果你也在学 Shell,我建议就用这种有真实反馈的场景练。写一个脚本,跑一下,服务起没起来、游戏能不能进,反馈是立刻的。比做那些"输出 1 到 100 的偶数"的练习题强太多,因为你必须处理真实的失败:路径不对、权限不够、进程不退、磁盘满了、日志太大。这些是练习题里不会出现的。

5.2 版本更新与回滚的准备动作

服务端会随游戏版本更新,更新脚本我写得很保守:

#!/usr/bin/env bash # update.sh set -euo pipefail "${HOME}/scripts/stop.sh" "${HOME}/scripts/backup.sh" # 更新前必须有一份新鲜备份 "${HOME}/steamcmd/steamcmd.sh" \ +force_install_dir "${HOME}/server" \ +login anonymous \ +app_update 2394010 validate \ +quit "${HOME}/scripts/start.sh"

顺序是先停、再备份、再更新、再起。中间任何一步失败就中断,停在"有备份、服务已停"的安全状态,你可以手工介入。最忌讳的是不停服直接更新,文件被进程占用着,更新完可能出现版本混杂,表现为启动报一堆莫名其妙的错误。

回滚我没做自动化,因为它的路径依赖太强——你得先知道上一个版本号,再指定下载。我的土办法是:每次更新前把整个server/目录cp -a到server.bak,确认新版跑通一天之后再删。占点磁盘,但比出事之后手忙脚乱强。

5.3 几个写运维脚本的习惯

最后分享几条我在这个项目里反复验证过的习惯,跟具体技术无关,但很影响长期体验。

变量全部用双引号包住。rm -rf "${DIR}"和rm -rf ${DIR}在变量为空时是两个世界。这个习惯能挡掉绝大部分"手滑删库"。

脚本开头加set -euo pipefail,除非你明确知道自己在做什么。它会让你在错误发生的第一时间停下来,而不是带着错误状态继续跑出更大的问题。

所有输出带时间戳。日志是事后唯一可信的证据,没有时间戳的日志在排查时序问题时几乎没法用。

不要相信"应该没问题"。备份完校验、启动后看端口、更新后进游戏跑一圈。每一步都花不了十几秒,但它把"不确定"变成了"确定"。

这套服务器我已经连续跑了几周,中途只因为一次磁盘告警手动干预过。真正让我踏实的不是它有多稳定,而是我知道它出问题的时候我能多快恢复——脚本里的每一行,都是在回答这个问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询