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 7600、7001 等都会撞车),所以它们不能共用一个 frps —— 必须各占一个公网 IP / frps 实例。两个 game pod 之间、两个 mysql 之间都配了 podAntiAffinity(topologyKey: kubernetes.io/hostname),强制分到不同节点:摊平负载,并缩小故障域,让一次节点事件只影响一个服。
组件构成
每个服由四部分组成,下面以 dnf-weibian 为例(dnf-chaobian 结构对称)。
- Name
- game server(Deployment,replicas: 1)
- Description
- 镜像
registry.dunaifen.games/dnf:2.1.6,请求/限制均为 memory7Gi+ CPU500m/2(按 14 天 Prometheus 峰值定档,request==limit 以免被驱逐)。挂载dnf-weibian-data/dnf-weibian-log两个 RWX(nfs-client)PVC,外加一个 8Gi 的内存型/dev/shmemptyDir。通过MYSQL_HOST=dnf-weibian-mysql-svc.game、MYSQL_PORT=4000连库,PUBLIC_IP注入对应公网 IP。GM 账号等敏感配置经envFrom从 ExternalSecret 注入。
- Name
- mysql(StatefulSet)
- Description
- 镜像
1995chen/mysql:7-5.0.95,volumeClaimTemplates申请 10Gi(nfs-client)。注意这个镜像在 my.cnf 里监听 4000 端口而非 3306,所以 game pod 连的是:4000。headless Servicednf-weibian-mysql-svc(clusterIP: None)供集群内访问,对外的 NodePort 已为安全移除。一个postStart钩子在每次 mysql 启动时幂等重建game用户(它存在非崩溃安全的 MyISAMmysql.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 IP192.168.88.222(metallb.universe.tf/allow-shared-ip: "dnf-lb")。frpc 的frpc.toml把localIP指向这两个 Service 的集群 DNS 名再转发到 frps。
ExternalSecret 从 Vault ClusterSecretStore vault-backend 的 yldm/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 7600、7001 等),这正是它们不能共用一个 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 pod(kubectl -n game delete pod <game-pod>),supervisord 会重新拉起进程并重连(健康的 pod 几秒内恢复到约 70 条连接)。优先用单 pod delete 而非 rollout restart:PVC 是 RWX(nfs-client),滚动 surge 会短暂跑起两个 game 实例对同一数据目录和 mysql 操作(脑裂),delete 只重建那一个。
04-dnf-server.yaml 都加了 liveness 探针:当到 mysql 端口的已建立连接为 0 时探测失败,kubelet 重启容器,supervisord 随后重连。探针留了宽裕的延迟/阈值(initialDelaySeconds: 120、periodSeconds: 30、failureThreshold: 4,约 120s 宽限期),正常启动需要约 30-60s 才连上,节点重启期间的短暂 mysql 中断也不会让 game pod 反复抖动。所以单次节点重启应在约 2 分钟内自愈,只有探针被移除、或 mysql 宕机超过宽限窗口时才需要手动删 pod。多个 worker 节点短时间内一起重启时,两个服可能都僵死 —— 逐个检查并 bounce。game pod 还配了 dnsConfig(attempts: 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 天。