DNF 私服

applications/game/dnf-server/ 下是两个相互独立、各自自包含的 DNF(地下城与勇士)私服,都部署在共享的 game namespace 里,由 ArgoCD 从 yldm-tech/k8s-config 持续同步。两个服各带一套自己的 mysql StatefulSet、frpc 隧道和 PVC,互不共享数据。

两个私服

两个变种用的是同一个游戏镜像 registry.dunaifen.games/dnf(tag 由 argocd-image-updater 按 semver 回写到根 kustomization.yaml,一处更新同时作用于两个服),但绑定不同的公网 IP 和不同的 frps 实例。

  • Name
    dnf-weibian(微变)
    Description
    公网 IP 129.150.55.22,经 frps1 穿透。game pod / mysql / frpc / PVC 全部以 dnf-weibian- 前缀命名。
  • Name
    dnf-chaobian(超变)
    Description
    公网 IP 138.2.110.59(VPS 第二 VNIC 的公网 IP),经 frps2 穿透。资源以 dnf-chaobian- 前缀命名。

两个服对外暴露的端口完全相同(gate 881、launcher 76007001 等都会撞车),所以它们不能共用一个 frps —— 必须各占一个公网 IP / frps 实例。两个 game pod 之间、两个 mysql 之间都配了 podAntiAffinitytopologyKey: kubernetes.io/hostname),强制分到不同节点:摊平负载,并缩小故障域,让一次节点事件只影响一个服。

组件构成

每个服由四部分组成,下面以 dnf-weibian 为例(dnf-chaobian 结构对称)。

  • Name
    game server(Deployment,replicas: 1)
    Description
    镜像 registry.dunaifen.games/dnf:2.1.6,请求/限制均为 memory 7Gi + CPU 500m/2(按 14 天 Prometheus 峰值定档,request==limit 以免被驱逐)。挂载 dnf-weibian-data / dnf-weibian-log 两个 RWX(nfs-client)PVC,外加一个 8Gi 的内存型 /dev/shm emptyDir。通过 MYSQL_HOST=dnf-weibian-mysql-svc.gameMYSQL_PORT=4000 连库,PUBLIC_IP 注入对应公网 IP。GM 账号等敏感配置经 envFrom 从 ExternalSecret 注入。
  • Name
    mysql(StatefulSet)
    Description
    镜像 1995chen/mysql:7-5.0.95volumeClaimTemplates 申请 10Gi(nfs-client)。注意这个镜像在 my.cnf 里监听 4000 端口而非 3306,所以 game pod 连的是 :4000。headless Service dnf-weibian-mysql-svcclusterIP: None)供集群内访问,对外的 NodePort 已为安全移除。一个 postStart 钩子在每次 mysql 启动时幂等重建 game 用户(它存在非崩溃安全的 MyISAM mysql.user 表里,异常崩溃可能丢失)。
  • Name
    frpc(Deployment,replicas: 1)
    Description
    镜像 snowdreamtech/frpc:latest,挂载由 ExternalSecret 渲染的 frpc.toml(仅 frp token 来自 Vault,其余静态)。把 game 的 TCP/UDP 端口经 frps 隧道出去;DB 端口 3306 故意不暴露。
  • Name
    LoadBalancer Service(TCP + UDP 各一)
    Description
    dnf-weibian-tcp / dnf-weibian-udp 通过 MetalLB 共享同一个内网 LB IP 192.168.88.222metallb.universe.tf/allow-shared-ip: "dnf-lb")。frpc 的 frpc.tomllocalIP 指向这两个 Service 的集群 DNS 名再转发到 frps。

ExternalSecret 从 Vault ClusterSecretStore vault-backendyldm/production/dnf-weibian(chaobian 对应 yldm/production/dnf-chaobian)拉取 DB root / game 密码、GM 账号、frp token 等。

此外还有一个独立的 dnf-backend(FastAPI Web 工具,Deployment replicas: 1,镜像 ghcr.io/houko/dnf-classic/backend,ingress host api.dunaifen.games),它跨 service DNS 同时连两个变种的 mysql 与登录网关。

玩家流量路径

公网入口不经集群 ingress,而是 client → 公网 IP → Oracle VPS 上的 frps → frpc pod → 集群内 LB Service → game pod:

client → <public-ip>:881(gate) / :7600(launcher)
       → frps (Oracle VPS)
       → frpc pod
       → dnf-<srv>-tcp / dnf-<srv>-udp Service (LB IP 192.168.88.222)
       → game pod (dnf-<srv>-server)

两个服在不同公网 IP / frps 上暴露相同的端口集(gate 881、launcher 76007001 等),这正是它们不能共用一个 frps 的原因。

端到端连通性确认(服务端):nc -z <public-ip> 881 7600 应为 OPEN;外部探测 :881 会在 game pod 的 /data/log/tongyi_gate.log 里出现 EchoSession <frpc-pod-ip> 连接成功。如果这些都通但玩家仍连不上,问题就在玩家到 Oracle VPS 的网络路径上(Oracle 的 IP 常被国内运营商限速/封锁),不在集群。

头号故障:节点重启后卡登录器

这是 DNF 私服的 #1 故障模式。DNF 的 DB 中间件(mysql_proxy / df_dbmw_r)在它的 mysql pod 发生位移时(节点重启、驱逐、手动删除 → 新的 pod IP)不会自动重连。表现极具迷惑性:game pod 仍是 Running,每个端口都还 LISTEN,公网路径仍可达 —— 但到 mysql :4000 的已建立连接掉为 0,玩家卡在登录器进不去。kubectl get pod 看不出任何异常,只能靠数 game pod 的活跃 DB 连接发现:

MYSQL_IP=$(kubectl -n game get pod -l app=dnf-<srv>-mysql -o jsonpath='{.items[0].status.podIP}')
SVR=$(kubectl -n game get pod -l app=dnf-<srv>-server --field-selector=status.phase=Running -o jsonpath='{.items[-1:].metadata.name}')
kubectl -n game exec "$SVR" -- sh -c "ss -tn | grep -c ':4000'"   # 0 = 中间件已僵死

修法:在 mysql 稳定后删掉 game podkubectl -n game delete pod <game-pod>),supervisord 会重新拉起进程并重连(健康的 pod 几秒内恢复到约 70 条连接)。优先用单 pod delete 而非 rollout restart:PVC 是 RWX(nfs-client),滚动 surge 会短暂跑起两个 game 实例对同一数据目录和 mysql 操作(脑裂),delete 只重建那一个。

game pod 还配了 dnsConfigattempts: 3 / timeout: 2)让解析器扛过 CoreDNS 短暂抖动;否则一次 mysql 主机名解析失败就会让 entrypoint 崩溃。

备份

backup/cronjob-mysql-backup.yaml 是一个 CronJob(每天 04:00 低峰时段,concurrencyPolicy: Forbid),用与 DB 完全匹配的 1995chen/mysql:7-5.0.95 镜像做 dump(避免新 client 的 secure_auth / 老密码 hash 兼容坑),通过 :4000 对两个服各做一次 mysqldump --lock-tables --routines --triggers --all-databases,gzip 后用 minio/mc 上传到 MinIO 的 database-backups/dnf-mysql/ 桶(该桶在 nas-backup-sync 列表里会被 rclone 推到异地 NAS),保留最近 14 天。

评论