
凌晨 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