如何在香港服务器的Kubernetes集群中部署游戏微服务,支持全球用户弹性扩容?
技术教程 2025-09-17 10:54 227


凌晨 03:07,美东的玩家刚打完一场跨区战,东南亚的玩家已经陆续上线,等着清晨活动。告警面板上,room-gateway 的 UDP 会话曲线突然拉高,香港集群的 Pod 已经顶到 HPA 的上限。我得在 20 分钟内把新的节点拉起来、把端口广播到公网、把 BGP 路由打出去,不然就会有人在社区里骂延迟。我深呼吸,打开跳板机:今天这篇文章,就按我当时的“硬核现场流程”完整复刻一遍。

  • 目标:在香港的物理/租用服务器上,从零到一部署一个 Kubernetes 集群,承载游戏微服务(涵盖 HTTP/HTTPS、TCP、UDP),具备全球用户接入与弹性扩容能力。
  • 风格:纯实操 + 现场坑位复盘。适合想要“自己动手搭一套能打仗的集群”的架构师/运维,也照顾到新手能跟着做完。
  • 基础假设:你有一排香港机房的服务器(或裸金属/独立服务器),可以控制交换机/网关,手里有公网段或可申请到可路由的地址。

注:用户之前要求 CentOS 7。确实很多老业务和运维体系还在 7 上。我这套方案默认 CentOS 7.9,并给出 稳定路线(Calico + 3.10 内核) 与 高阶路线(升级 5.x LTS 内核 + eBPF/Cilium) 两条分轨做法。CentOS 7 已经 EOL,若能切换到 Rocky/Alma 8/9 会更长治久安,但不强行改变你的现状。

硬件与网络拓扑(我当时的清单)

机房与节点规划

角色 数量 CPU 内存 系统盘 数据盘 网卡 OS/内核 备注
控制面 (master) 3 Xeon Silver 4214 / EPYC 7302P 128 GB SATA SSD 480G NVMe 1.92T ×1 2×10GbE CentOS 7.9 / kernel 3.10(或 5.4 LTS) kubeadm HA、etcd 本地盘
工作节点(通用) 8 EPYC 7313P 256 GB SATA SSD 480G NVMe 1.92T ×2 2×25GbE 同上 无状态微服务/网关
工作节点(状态型) 4 EPYC 7443P 256–512 GB SATA SSD 480G NVMe 3.84T ×4 2×25GbE 同上 Redis/Postgres/消息队列/Longhorn
路由器/ToR 2 机房自备 - - - 25/40GbE 上联 FRR/BGP 与 MetalLB 建邻

NIC 打开 RSS/RPS,确保中断绑核(irqbalance 调整或手动 smp_affinity),UDP 场景收益明显。

IP 与 VLAN 规划

VLAN CIDR 用途 备注
VLAN 10 10.10.0.0/16 Pod 网络 CNI 网段(Calico/Cilium)
VLAN 20 10.20.0.0/16 节点管理 SSH/监控/容器镜像
VLAN 30 10.30.0.0/24 存储后端 Longhorn/复制流量
公网池 A x.x.120.0/24 对外服务 MetalLB 地址池
公网池 B x.x.121.0/24 预留 高峰扩容/灰度

角色与服务组件(微服务切分)

  • account-api(HTTP/gRPC):鉴权、用户资料
  • matchmaking(HTTP/gRPC):匹配服务
  • room-gateway(UDP/TCP):房间网关(维持会话,转发至后端 room-server)
  • room-server(UDP/TCP,StatefulSet):真正的房间逻辑/帧同步
  • state-service(Redis Cluster):房间状态/会话/排行榜
  • db(PostgreSQL):持久化
  • asset-cdn(HTTP):静态资源(可前置 CDN)
  • observability(Prometheus/Grafana/Loki/Alertmanager)
  • ingress(Nginx Ingress) + L4/UDP 入口(MetalLB+BGP)

系统与内核调优(CentOS 7 现场脚本)

  • 稳定路线:不升级内核(3.10),选 Calico,功能即刻可用
  • 高阶路线:升级到 5.4/5.10(ELRepo),可选 Cilium eBPF,UDP/高并发收益更好
# 基础准备(所有节点)
sudo timedatectl set-timezone Asia/Hong_Kong
sudo swapoff -a && sudo sed -ri 's/.*swap.*/#&/' /etc/fstab
sudo setenforce 0 && sudo sed -ri 's/SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
sudo yum install -y yum-utils device-mapper-persistent-data lvm2 ipvsadm jq htop conntrack-tools

# 内核网络参数 - /etc/sysctl.d/99-game-net.conf
cat <<'EOF' | sudo tee /etc/sysctl.d/99-game-net.conf
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.ipv4.ip_forward = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_fin_timeout = 15
net.ipv4.udp_mem = 3145728 4194304 6291456
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.udp_rmem_min = 131072
net.ipv4.udp_wmem_min = 131072
net.ipv4.ip_local_port_range = 10000 65000
fs.file-max = 10485760
EOF
sudo sysctl --system

# IPVS(kube-proxy 走 ipvs 模式更稳更快)
cat <<'EOF' | sudo tee /etc/modules-load.d/ipvs.conf
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack
EOF
sudo modprobe ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack

# 可选:升级 5.x LTS 内核(高阶路线)
# sudo yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
# sudo yum --enablerepo=elrepo-kernel install -y kernel-ml
# sudo grub2-set-default 0 && sudo reboot

安装容器运行时 + kubeadm(containerd)
# containerd
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y containerd.io
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -ri 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl enable --now containerd

# kubeadm/kubelet/kubectl(建议 1.27 ~ 1.29 之间选稳定版本;CentOS7 建议 1.27/1.28)
cat <<'EOF' | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-$basearch
enabled=1
gpgcheck=0
repo_gpgcheck=0
EOF
sudo yum install -y kubelet-1.28.7 kubeadm-1.28.7 kubectl-1.28.7
sudo systemctl enable kubelet

初始化控制面(第 1 台 master):

sudo kubeadm init \
  --pod-network-cidr=10.10.0.0/16 \
  --kubernetes-version=v1.28.7 \
  --control-plane-endpoint "k8s-lb.example.hk:6443" \
  --apiserver-advertise-address=<MASTER1_IP> \
  --upload-certs \
  --v=5

初始化完成后保存输出的 kubeadm join 命令,随后把另外两台 master 和所有 worker 加入。

CNI:两条路线

路线 A(稳定):Calico(不升内核即可用)

# 在 master 上
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.25.0/manifests/calico.yaml
# 调整 MTU(常见坑:VLAN/隧道造成 MTU 不匹配)
kubectl -n kube-system set env daemonset/calico-node FELIX_IPINIPMTU=1450

路线 B(高阶):Cilium eBPF(建议 5.x 内核)

# helm 安装(示例)
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --version 1.14.5 \
  --namespace kube-system \
  --set kubeProxyReplacement=strict \
  --set k8sServiceHost=k8s-lb.example.hk \
  --set bpf.masquerade=true \
  --set tunnel=disabled \
  --set autoDirectNodeRoutes=true

坑 1: 内核 <5.4 会限制 eBPF 功能,观测到 UDP 丢包上升;所以要么升内核、要么回到 Calico。

负载入口:MetalLB + BGP(对外暴露 TCP/UDP)

我们在 ToR 路由器/边界网关 上用 FRR 开了 BGP,与 MetalLB 建邻,MetalLB 从公网地址池分配 LoadBalancer,把 VIP 通过 BGP announce 出去。关键点:

  • externalTrafficPolicy: Local,保留源 IP,方便限流/回源逻辑
  • UDP 服务建议独立 Service,避免和 HTTP/TCP 混在一起
  • BGP MED/local-pref 与上游协商,防止回程绕路

MetalLB 部署与地址池:

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml

cat <<'EOF' | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: pub-a
  namespace: metallb-system
spec:
  addresses:
  - x.x.120.10-x.x.120.200
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
  name: pub-a-adv
  namespace: metallb-system
spec:
  ipAddressPools:
  - pub-a
  localPref: 200
EOF

与 ToR 建邻:

apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
  name: tor-1
  namespace: metallb-system
spec:
  myASN: 65001
  peerASN: 65000
  peerAddress: 10.20.0.1
---
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
  name: tor-2
  namespace: metallb-system
spec:
  myASN: 65001
  peerASN: 65000
  peerAddress: 10.20.0.2

坑 2(实战):ToR 默认启用了 BGP max-prefix,高峰期我们临时增加了 LoadBalancer 数量,触发邻居 reset。解决:与网工调高 max-prefix 并把地址池做聚合。

存储:Longhorn(NVMe 复制,简单够用)

我选 Longhorn 做状态服务本地复制,热点用 NVMe,副本数 2–3。它对 CentOS7 也友好。

kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.6.2/deploy/longhorn.yaml
# 设置存储类
kubectl patch storageclass longhorn -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

坑 3: Longhorn 默认副本跨节点,但不要跨拥塞的 ToR。我们给“状态节点”设了 nodeSelector + topology spread,保证副本在带宽充足的区间。

Ingress(HTTP/HTTPS)与证书

# Nginx Ingress
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.service.annotations."metallb\.universe\.tf/address-pool"=pub-a

# cert-manager(自动签发)
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace \
  --set installCRDs=true

容器镜像与传输(香港本地 Harbor,减少跨境抖动)

  • 在管理 VLAN 部署 Harbor(或使用云上的镜像仓库但配边缘缓存)
  • 节点 /etc/containerd/config.toml 配置 镜像加速 与私有仓库的 TLS/认证

CI/CD 例(GitHub Actions,构建+推送 Harbor):

name: build-and-push
on: [push]
jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: harbor.example.hk
          username: ${{ secrets.HARBOR_USER }}
          password: ${{ secrets.HARBOR_PASS }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: harbor.example.hk/game/room-server:${{ github.sha }}

微服务部署(关键 YAML)

1) 房间网关(UDP 入口)

Service(UDP,保留源 IP):

apiVersion: v1
kind: Service
metadata:
  name: room-gateway-svc
  annotations:
    metallb.universe.tf/address-pool: pub-a
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  ipFamilies: ["IPv4"]
  selector:
    app: room-gateway
  ports:
    - name: udp-room
      port: 30000
      protocol: UDP
      targetPort: 30000

Deployment(亲和与亲核,减少跨 NUMA 抖动):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: room-gateway
spec:
  replicas: 6
  selector:
    matchLabels: { app: room-gateway }
  template:
    metadata:
      labels: { app: room-gateway }
    spec:
      nodeSelector:
        node-role.kubernetes.io/udp-gw: "true"
      containers:
      - name: gw
        image: harbor.example.hk/game/room-gateway:1.2.3
        ports:
          - containerPort: 30000
            protocol: UDP
        resources:
          requests: { cpu: "2", memory: "2Gi" }
          limits: { cpu: "4", memory: "4Gi" }
        env:
          - name: UDP_READ_BUFFER
            value: "8388608"
      tolerations:
      - key: "udp-gw" 
        operator: "Exists" 
        effect: "NoSchedule"
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: "kubernetes.io/hostname"
        whenUnsatisfiable: ScheduleAnyway
        labelSelector: { matchLabels: { app: room-gateway } }

坑 4:externalTrafficPolicy: Local 会导致只有有 Pod 的节点才对外接流;请确保副本分散并监控 VIP 的后端数。

2) 房间服务(StatefulSet)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: room-server
spec:
  serviceName: "room-server"
  replicas: 12
  selector:
    matchLabels: { app: room-server }
  template:
    metadata:
      labels: { app: room-server }
    spec:
      nodeSelector:
        node-role.kubernetes.io/stateful: "true"
      containers:
      - name: server
        image: harbor.example.hk/game/room-server:1.2.3
        ports:
          - name: game-udp
            containerPort: 30100
            protocol: UDP
        volumeMounts:
          - name: data
            mountPath: /var/lib/room
        resources:
          requests: { cpu: "3", memory: "6Gi" }
          limits: { cpu: "6", memory: "10Gi" }
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "longhorn"
      resources:
        requests:
          storage: 50Gi

3) HTTP 入口(账号、匹配、资产)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: game-ing
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt"
spec:
  tls:
  - hosts: [api.game.example.com, cdn.game.example.com]
    secretName: tls-game
  rules:
  - host: api.game.example.com
    http:
      paths:
      - path: /v1
        pathType: Prefix
        backend:
          service:
            name: account-api
            port: { number: 80 }

弹性扩容:HPA + 自定义指标(Prometheus Adapter)与 KEDA

HPA(基于 CPU/自定义 QPS)

  • 安装 Prometheus 与 prometheus-adapter
  • 暴露 requests_per_second 指标映射为 k8s 自定义指标

HPA 示例(按 QPS 扩容 room-gateway):

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: room-gateway-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: room-gateway
  minReplicas: 6
  maxReplicas: 60
  metrics:
  - type: Pods
    pods:
      metric:
        name: requests_per_second
      target:
        type: AverageValue
        averageValue: "1200"

KEDA(按队列/并发事件)

如果匹配/房间分配走消息流(如 Redis Streams / Kafka),KEDA 很顺手:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: matchmaking-so
spec:
  scaleTargetRef:
    kind: Deployment
    name: matchmaking
  minReplicaCount: 4
  maxReplicaCount: 80
  triggers:
  - type: redis-streams
    metadata:
      address: redis://redis-svc:6379
      stream: mm-events
      consumerGroup: mm-consumer
      pendingEntriesCount: "5000"

坑 5:自定义指标缺失会让 HPA“卡住不动”。务必为目标命名空间授权 custom.metrics.k8s.io 的访问,观察 adapter 的日志。

“全球用户可用”:接入策略与回程优化

静态资源:前置 全球 CDN,回源香港(带缓存与预取)

HTTP API:CDN 边缘回源 + 智能路由/Anycast(提升跨洋 RTT 的稳定性)

TCP/UDP 游戏流量:

  • 方案 1(落地快):香港单点 + 全球加速(如运营商加速/GA/Spectrum 类)的 L4/UDP 代理,最少改造即可把入口就近接入,再回到香港
  • 方案 2(更优雅):多地区(HK/SIN/JP/LA)小集群 + GeoDNS/LBR,按延迟/就近调度;本文聚焦 香港单点,但架构天然可横向复制

DNS/GSLB 最小做法:

  • api.game.example.com、udp.game.example.com 做地域/延迟解析;
  • 维护健康检查,异常时移除香港 VIP;
  • 配合 BGP local-pref 控制出入方向。

观测与 SLO(我们当晚看的就是这几条)

  • UDP RTT p50/p95/p99(从客户端 SDK 与网关两端打点)
  • 房间人数/活跃房间数
  • 丢包率(网关侧估算 + 客户端重传)
  • HPA 事件(扩/缩容频率、失败次数)
  • 节点网卡中断/队列拥塞(ethtool -S, nstat)
  • Longhorn 副本健康/重建速率
指标 目标 高峰实测(例)
UDP RTT p95 ≤ 180ms(全球) 155ms
丢包率 ≤ 1% 0.7%
网关 CPU ≤ 70% 62%
HPA 扩容延迟 ≤ 45s 28s
房间失败创建率 ≤ 0.5% 0.2%

最简单的金丝雀用 Nginx Ingress + Header/Weight,或上 Argo Rollouts。UDP 侧可以在 room-gateway 做按 UID Hash 的流量拨权,逐步拉新版本的 room-server。

安全基线

  • NetworkPolicy:网关仅能访问特定后端与 Redis;DB 白名单
  • PodSecurity:默认 restricted,只有网关/监控有 CAP 例外
  • Secrets:sealed-secrets 或外部 KMS
  • 镜像签名与准入:cosign + kyverno/OPA Gatekeeper
  • 审计:启用 k8s 审计日志,集中到 Loki

常见坑位与现场解法

UDP “负载不均”:默认四元组哈希可能导致单核热点。

解法:开启 NIC RSS,ethtool -L 调整队列数;Pod 多副本 + 拓扑均衡;必要时在网关层做一致性哈希,避免抖动。

MTU 不匹配:Overlay/VLAN 叠加后 MTU 变小,出现零星超时。

解法:统一设定 MTU(Calico/Cilium/节点下发),抓包验证 DF。

externalTrafficPolicy: Local 导致半数节点“无后端”:流量不均或黑洞。

解法:强制分散副本、配 podAntiAffinity 与 topologySpread,并做 Liveness/Readiness,停服即摘除。

BGP 邻居因 prefix 数量 reset:扩容瞬间分配太多 LoadBalancer。

解法:地址池聚合、增大 max-prefix、对 UDP 入口做端口复用(单 VIP 多端口)。

Longhorn 重建占满带宽:高峰期副本重建影响时延。

解法:限速重建、在离峰做维护;热点卷单独节点池。

HPA 指标“抖动”:扩缩容来回震荡。

解法:设置 behavior 冷却期、滑动窗口;对突发场景给固定预热副本。

kube-proxy iptables 模式效率低:连接数上万时延迟上升。

解法:切 ipvs 模式;或 Cilium 严格替代 kube-proxy。

验收与压测(我们是这样打的)

HTTP:k6 跑 account-api/matchmaking(RPS、p95)

UDP:自写 go 小工具(并发连接、包大小、心跳间隔),从海外云主机多点发起

端到端:用测试客户端跑 5 分钟“房间创建 → 加入 → 对战 → 退出”

简单的 UDP 工具(示意):

// go run udpbench.go -host udp.game.example.com -port 30000 -c 2000 -dur 300s

重点看:p95 RTT 曲线是否稳、扩容期间是否出现瞬时丢包尖峰。

运维日常脚本(扩容节点 10 分钟搞定)

当晚我就是用这套 Playbook 拉起 4 台新 worker:

# 1) PXE/装机完成后,一键节点初始化
ansible -i hosts new-workers -m script -a bootstrap_centos7.sh

# 2) 加入集群
ansible -i hosts new-workers -m shell -a \
  "kubeadm join k8s-lb.example.hk:6443 --token $TOKEN --discovery-token-ca-cert-hash $HASH"

# 3) 打上角色标签
kubectl label node worker-13 node-role.kubernetes.io/udp-gw=true
kubectl label node worker-14 node-role.kubernetes.io/stateful=true

成本与性能小结(我们调优的抓手)

抓手 成本影响 效果
eBPF(Cilium,需 5.x 内核) 降低内核路径开销,UDP 时延更稳
RSS/中断绑核 热点核消除,尾延迟下降
externalTrafficPolicy: Local 保留源 IP,限流/风控好做
Longhorn NVMe 状态服务抖动小,重建快
HPA + 预热副本 高峰前不被打爆
Harbor 本地仓库 发版速度稳定,少跨境波动

我愿意反复复用的“最小闭环”

  • 稳定网络栈(Calico 或升内核配 Cilium)
  • MetalLB + BGP 暴露 UDP/TCP VIP
  • 房间网关/房间服务 解耦(网关“轻”、房间“重”)
  • HPA + KEDA 按业务指标伸缩
  • 观测拉满(RTT/丢包/副本健康)
  • 脚本化扩容(10 分钟一批)

05:20,天色泛白,Grafana 的 p95 RTT 线回落到熟悉的水平。新加的 4 台节点很安静,UDP 会话均衡地铺开,BGP 邻居稳定,Longhorn 也没在高峰时瞎重建。那一刻我把咖啡放下,给美东的同事发了一个 ✅。

这就是我在香港服务器上搭的 Kubernetes 游戏微服务栈:不追求花哨,但要在凌晨三点顶得住。你可以照搬,也可以按你的资源和团队习惯去换件;关键是把路径打通——入口、网络、存储、扩缩容、观测,每个环节都能落地、可复用。

如果你正准备把“全球用户弹性扩容”的活扛起来,希望这篇实操能让你少踩几个坑,也希望在某个凌晨,你的面板同样安静。

附:关键配置清单(可直接落地)

kube-proxy 切换 ipvs

kubectl -n kube-system edit configmap kube-proxy
# 修改
# mode: "ipvs"
# ipvs:
#   scheduler: "rr"
# 然后滚动重启 DaemonSet
kubectl -n kube-system rollout restart ds/kube-proxy

Prometheus Adapter(自定义指标映射示意)

rules:
  default: false
  custom:
  - seriesQuery: 'gateway_requests_per_second{namespace!="",pod!=""}'
    resources:
      overrides:
        namespace: {resource: "namespace"}
        pod: {resource: "pod"}
    name:
      matches: "gateway_requests_per_second"
      as: "requests_per_second"
    metricsQuery: 'sum(rate(gateway_requests_total[1m])) by (namespace,pod)'

NetworkPolicy(网关最小可用)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: room-gateway-min
  namespace: default
spec:
  podSelector: { matchLabels: { app: room-gateway } }
  policyTypes: [Ingress, Egress]
  ingress:
  - from:
    - ipBlock: { cidr: 0.0.0.0/0 }  # 公网入口
    ports:
    - protocol: UDP
      port: 30000
  egress:
  - to:
    - podSelector: { matchLabels: { app: room-server } }
    ports:
    - protocol: UDP
      port: 30100

HPA 行为(防抖)

spec:
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 100
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 180
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60