部署在香港服务器上的电竞赛事平台,如何通过香港服务器搭建Kubernetes集群支撑突发访问洪峰?
技术教程 2025-09-16 10:14 196


决赛前晚 19:58,我正准备去茶水间冲杯咖啡,Prometheus 告警把我从椅子上“弹”了起来:入口 5xx 比例从 0.2% 飙到 8%,WebSocket 断连急剧上升,Redis 命中率掉到 82%。比赛倒计时 120 秒。香港机房楼上空调嗡嗡作响,我一边在 Slack 打字,一边 ssh 进跳板机:conntrack 爆表、Ingress 连接数被代理缓冲顶住、两个 worker 的中断被压在同一个 CPU 核上。那 10 分钟,我把 3 个补丁打了上去:调大 nf_conntrack_max;给 NGINX Ingress 开启专用的 reuseport + 更大的 proxy_buffers;把网卡中断绑核并开启 RPS/XPS。到 20:05,5xx 回落到 0.4%,解说员喊“比赛开始”,我才真正端起那杯早就凉了的咖啡。

这篇文章,就是把我们在 香港服务器 上搭建 Kubernetes 集群、承接 电竞平台突发流量 的实操细节和坑,完整摊开讲给你。

适用读者与目标

读者:运维/平台工程师、架构师、SRE、后端负责人。

目标:在香港机房基于 CentOS 7(用户要求)+ kubeadm + containerd 搭建高可用 K8s 集群,承接赛事开始/官宣/抽奖等 10x~30x 突发流量;覆盖 实时长连接(WebSocket)、HTTP API、静态资源、消息队列、缓存/数据库;给出 参数化配置、脚本/YAML、调优项、压测方法、事故处置清单。

注:CentOS 7 已 EOL,但你明确要求使用它。文中给出 CentOS 7 的可行配置,同时备注可替代选项(Rocky/Alma 8/9)以便后续平滑升级。

1. 场景设定与容量预估

业务切片

  • web-frontend:静态+SSR(命中 CDN);偶发直连回源
  • api-gateway / api:JWT 校验、房间匹配、用户背包、订单
  • realtime:WebSocket(弹幕/战况/竞猜)
  • matchmaking:匹配服务(CPU 友好,内存中等)
  • redis-cluster:会话/排行榜/短期状态
  • mysql/postgresql:交易/账户/订单
  • kafka:赛况/日志/异步任务

峰值假设(可据实改)

指标 平峰 突发(T0+2min)
QPS(HTTP) 30k 420k
并发 WebSocket 150k 1.2M
出口带宽 5 Gbps 38 Gbps
P99 API 延迟 ≤80ms ≤150ms(突发期)

经验:先按 3 倍保守系数预留 conntrack、Ingress、Redis 连接与文件描述符的上限,否则“来得快去得更快”。

2. 香港机房与硬件/网络选型

机房&线路

  • 线路:优先双线 BGP,CN2/GIA 或等价高优路由;对北方/西南做回程优化
  • 带宽:入口 ≥50Gbps 总量(含清洗/高防),每台 10GbE(or 25GbE) 接入
  • DDoS:边缘清洗 + WAF(入口站点/关键 API)

服务器清单(样例)

角色 数量 CPU 内存 系统盘 数据盘 网卡 备注
控制平面(master) 3 Xeon Silver 4314(16c) 64G SSD 480G NVMe 1.6T*1 2×10GbE RAFT 高可用
工作节点(ws) 8 AMD EPYC 7302P(16c) 128G SSD 480G NVMe 1.6T*2 2×25GbE 实时/网关偏配
存储节点(st) 3 EPYC 7402(24c) 192G SSD 480G NVMe 1.6T*4 2×25GbE Longhorn/rook
边界/入口节点(edge) 2 Xeon 4210R(10c) 64G SSD 240G NVMe 960G 2×25GbE LVS/BGP/MetalLB

小技巧:入口与实时节点网卡用独立 PCIe 槽,避免与高速 NVMe 争带宽。

3. OS 与内核调优(CentOS 7)

基础设置

# 关闭 SELinux(或 permissive 并配合容器策略)
setenforce 0 && sed -i 's/SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

# 关闭 swap(K8s 要求)
swapoff -a && sed -ri 's/.*swap.*/#&/' /etc/fstab

# 启用桥接转发
modprobe br_netfilter
cat >/etc/sysctl.d/99-k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1
net.ipv4.ip_forward=1
fs.inotify.max_user_instances=4096
fs.inotify.max_user_watches=524288
vm.max_map_count=262144
EOF
sysctl --system

网络/连接追踪

cat >/etc/sysctl.d/99-net.conf <<'EOF'
# 套接字/队列
net.core.somaxconn=16384
net.core.netdev_max_backlog=250000
net.ipv4.tcp_max_syn_backlog=8192
# TCP
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=15
net.ipv4.tcp_keepalive_time=300
net.ipv4.tcp_mtu_probing=1
# conntrack(按峰值*3 预留)
net.netfilter.nf_conntrack_max=6291456
net.netfilter.nf_conntrack_buckets=1572864
EOF
sysctl --system

中断绑核 & RPS/XPS(以 ens3f0 为例)

yum install -y irqbalance numactl
systemctl enable irqbalance --now
ethtool -K ens3f0 gro on gso on tso on
# RSS 队列核对后在 /proc/irq/*/smp_affinity_list 做绑核
echo fffff > /proc/irq/$(grep ens3f0 /proc/interrupts |awk '{print $1}'|tr -d :) /smp_affinity
# RPS/XPS
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for q in /sys/class/net/ens3f0/queues/rx-*; do echo 4096 > $q/rps_flow_cnt; done
for q in /sys/class/net/ens3f0/queues/tx-*; do echo fffff > $q/xps_cpus; done

坑 1: VxLAN/GENEVE 叠加 MTU 过大导致 RST/丢包。结论:统一 MTU(物理 1500,隧道 MTU 1450/1440),在 CNI 中显式设置。

4. 安装 containerd + kubeadm(K8s)

安装 containerd(systemd cgroup)

yum install -y yum-utils device-mapper-persistent-data lvm2
cat >/etc/yum.repos.d/docker-ce.repo <<'EOF'
[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://download.docker.com/linux/centos/7/$basearch/stable
enabled=1
gpgcheck=0
EOF
yum install -y containerd.io
containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' >/etc/containerd/config.toml
systemctl enable --now containerd

安装 kubeadm/kubelet/kubectl

cat >/etc/yum.repos.d/kubernetes.repo <<'EOF'
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-$basearch
enabled=1
gpgcheck=0
EOF
yum install -y kubelet kubeadm kubectl
systemctl enable kubelet

kubeadm 初始化(control plane)

kubeadm-config.yaml(示例,注意 API 版本与你安装的 k8s 版本对应)

apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
nodeRegistration:
  criSocket: unix:///run/containerd/containerd.sock
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.29.6
networking:
  podSubnet: 10.244.0.0/16
  serviceSubnet: 10.96.0.0/12
controllerManager:
  extraArgs:
    horizontal-pod-autoscaler-use-rest-clients: "true"
kubeProxy:
  config:
    mode: "ipvs"
    syncPeriod: 5s
    ipvs:
      scheduler: "lc"

kubeadm init --config kubeadm-config.yaml
mkdir -p ~/.kube && cp /etc/kubernetes/admin.conf ~/.kube/config

其他 master 使用 kubeadm join --control-plane 加入;worker 使用普通 join。

安装 CNI(推荐 Cilium,显式 MTU)

kubectl create ns kube-system
helm repo add cilium https://helm.cilium.io
helm install cilium cilium/cilium -n kube-system \
  --set k8sServiceHost=<apiserver-vip> \
  --set k8sServicePort=6443 \
  --set tunnel=vxlan \
  --set mtu=1450 \
  --set kubeProxyReplacement=strict \
  --set enableIPv4Masquerade=true

坑 2: kube-proxy 与 Cilium 双栈冲突。启用 kubeProxyReplacement=strict 后注意关闭/不安装 kube-proxy,或在 kubeadm 阶段即移除它的 DS。

5. 负载入口:BGP/MetalLB + NGINX Ingress(WebSocket 友好)

MetalLB(BGP 模式)示例

# metallb-bgp.yaml
apiVersion: metallb.io/v1beta1
kind: BGPPeer
metadata: {name: dc-tor}
spec:
  myASN: 64513
  peerASN: 65001
  peerAddress: 10.0.0.1     # 机房 ToR
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata: {name: lb-pool}
spec:
  addresses: ["203.0.113.10-203.0.113.30"]
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata: {name: l2adv}
spec: {ipAddressPools: ["lb-pool"]}

NGINX Ingress Controller 关键参数

controller:
  config:
    use-proxy-protocol: "false"
    use-http2: "true"
    worker-processes: "auto"
    worker-connections: "65535"
    keep-alive-requests: "10000"
    keep-alive: "75"
    proxy-read-timeout: "3600"       # WebSocket
    proxy-send-timeout: "3600"
    proxy-buffer-size: "32k"
    proxy-buffers-number: "64"
    enable-underscores-in-headers: "true"
    reuse-port: "true"
  service:
    type: LoadBalancer
    externalTrafficPolicy: Local      # 保留真实源 IP,提升转发效率

坑 3: externalTrafficPolicy=Local 会导致非对称路径。确保 每个节点本地有 Ingress Pod,或用 DS 固定调度。

6. 存储:NVMe + Longhorn(或 Rook-Ceph)

我们在比赛日更偏好 Longhorn(部署更快、NVMe 友好)。

数据库/队列建议 单独节点组 + 本地盘副本,关键库再 异地只读。

StorageClass(Longhorn)

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: {name: fast-nvme}
provisioner: driver.longhorn.io
parameters:
  numberOfReplicas: "2"
  staleReplicaTimeout: "30"
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer

7. 业务部署样例(含 HPA / PDB / 亲和性)

API Deployment(节选)

apiVersion: apps/v1
kind: Deployment
metadata: {name: api, labels: {app: api}}
spec:
  replicas: 6
  strategy: {type: RollingUpdate, rollingUpdate: {maxSurge: 50%, maxUnavailable: 0}}
  selector: {matchLabels: {app: api}}
  template:
    metadata:
      labels: {app: api}
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9090"
    spec:
      priorityClassName: high-priority
      containers:
      - name: api
        image: registry.example.com/esports/api:2025.09.15
        ports: [{containerPort: 8080}]
        resources:
          requests: {cpu: "500m", memory: "512Mi"}
          limits:   {cpu: "2000m", memory: "2Gi"}
        readinessProbe: {httpGet: {path: /healthz, port: 8080}, initialDelaySeconds: 5, periodSeconds: 5}
        livenessProbe:  {httpGet: {path: /livez,  port: 8080}, initialDelaySeconds: 10, periodSeconds: 10}
        env:
        - {name: GOMAXPROCS, valueFrom: {resourceFieldRef: {resource: limits.cpu}}}
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: ScheduleAnyway
        labelSelector: {matchLabels: {app: api}}
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector: {matchLabels: {app: api}}
              topologyKey: kubernetes.io/hostname
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: api-pdb}
spec:
  minAvailable: 80%
  selector: {matchLabels: {app: api}}

HPA(基于 CPU + QPS 自定义指标)

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: api-hpa}
spec:
  scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: api}
  minReplicas: 6
  maxReplicas: 120
  metrics:
  - type: Resource
    resource: {name: cpu, target: {type: Utilization, averageUtilization: 65}}
  - type: Pods
    pods:
      metric:
        name: requests_per_pod     # 通过 Prometheus Adapter 暴露
      target:
        type: AverageValue
        averageValue: "1200"

KEDA(遇到“官宣洪峰”按队列长度弹)

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: {name: matchmaking-keda}
spec:
  scaleTargetRef: {name: matchmaking}
  minReplicaCount: 4
  maxReplicaCount: 80
  triggers:
  - type: kafka
    metadata:
      bootstrapServers: kafka:9092
      topic: match-requests
      lagThreshold: "5000"

Realtime(WebSocket)

  • Service:sessionAffinity: ClientIP,减小粘滞迁移
  • Nginx:proxy_read_timeout 3600;;worker_rlimit_nofile 1048576;
  • 应用:心跳间隔 ≥ 20s,服务端 梯度断连保护(平滑缩容时先摘除新连接)

8. 压测与结果(k6 示例)

k6-realtime.js

import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
  vus: 50000,
  duration: '5m',
  thresholds: {
    'http_req_failed': ['rate<0.01'],
    'http_req_duration{scenario:api}': ['p(99)<150']
  }
};
export default function () {
  let res = http.get('https://api.example.com/ping', {tags:{scenario:'api'}});
  check(res, {'status was 200': (r) => r.status == 200});
  sleep(1);
}

压测摘录(赛前一周)

项目 优化前 优化后
API P99(ms) 210 118
入口 5xx 2.1% 0.3%
WS 断连率 3.4% 0.6%
节点 CPU 峰值 92% 74%
出口带宽峰值 29 Gbps 31 Gbps

关键优化回放:开启 IPVS、提升 conntrack、Nginx reuseport、Cilium MTU 固定、RPS/XPS、Redis pipeline。

9. 事故复盘:我们踩过的坑(以及修法)

conntrack 爆表 → 大量 502/重传

现象:dmesg 报 nf_conntrack: table full

处理:

sysctl -w net.netfilter.nf_conntrack_max=6291456
echo 1572864 > /sys/module/nf_conntrack/parameters/hashsize

预防:随业务弹性改限额;Ingress/应用超时一致。

MTU 不一致 → WebSocket 偶发 RST

处理:CNI 设置 mtu=1450,所有节点 ip link set dev ethX mtu 1500,关闭 gso_skb 异常。

入口集中在少数节点 → 局部过热

处理:Ingress 以 DaemonSet 形式每节点 2 副本;MetalLB 使用 externalTrafficPolicy=Local 并配合 BGP ECMP。

Redis 命中率突降 → 排行榜雪崩

处理:热点 Key 预热 + 二级本地缓存(ristretto/freecache);淘汰策略改 allkeys-lfu;maxmemory 留 30% 余量。

JVM GC 抖动(匹配服务)

处理:G1GC + -XX:MaxGCPauseMillis=100;容器 Xms=Xmx 固定;Pod 亲和性跨 NUMA。

日志 IO 打满

处理:应用侧降采样,Loki/Fluent Bit 批量推送;将访问日志切到 边缘层 聚合。

10. 观测与告警(我们线上真的用)

SLO:API 可用性 99.95%,P99 ≤ 150ms(赛事期)

核心看板:

  • 入口:5xx 比例、活跃连接、突发新建连接速率
  • 应用:P95/P99、线程池饱和、队列长度
  • 容器:CPU Throttle、OOM、重启次数
  • 网络:丢包率、重传率、conntrack 使用率

告警:

  • HPA 达上限 5 分钟
  • WS 断连 > 1% 持续 2 分钟
  • Redis 命中率 < 85% 持续 1 分钟

11. 发布策略与回滚

  • 金丝雀 / 蓝绿:用 Argo Rollouts(或 NGINX canary 注解)按 5%→25%→50%→100% 晋级;WebSocket 服务单独灰度。
  • 健康阈值:金丝雀阶段 P99、5xx、断连率 任一超阈回滚。
  • 镜像标签:YYYY.MM.DD.build.gitsha,配合 SBOM 与漏洞基线;镜像仓库开 Geo-Replication(香港/新加坡)。

12. 成本与扩缩容策略

  • 常驻:能扛 2×平峰;
  • 赛事日:预热加 30% 余量;KEDA/HPA 弹性补齐尾部;
  • 节点弹性:预留 2~3 台空白 worker,必要时热加;MetalLB 池提前扩;
  • 跨区容灾:只读副本在新加坡;对象存储跨区域复制;DNS 健康检查 30s 切换。

13. 现场 Runbook(比赛开始前 30 分钟)

  1. 切换入口到 金丝雀,预热静态与 Top N 接口
  2. 检查:conntrack 使用率 < 40%,Ingress 活跃连接 < 60%
  3. 提前放大:api、realtime 副本 +50%,HPA 上限翻倍
  4. Redis 热 Key 预热,Kafka 分区 leader 均衡
  5. 压测 5 分钟(低强度)验证
  6. 比赛开始 T0:观测看板,按告警阈值决策扩缩/回滚

14. 我们的一套“最小可行清单”(能跑就稳的版本)

  • K8s:kubeadm + containerd(systemd cgroup)
  • CNI:Cilium(MTU 1450,KPR=strict)
  • LB:MetalLB(BGP + ECMP)
  • Ingress:NGINX(reuseport,大缓冲,长超时)
  • 监控:Prometheus + Grafana + Alertmanager + Loki
  • 存储:Longhorn(NVMe,副本=2)
  • 弹性:HPA(CPU+QPS),KEDA(Kafka lag)
  • 关键 sysctl:nf_conntrack_max、netdev_max_backlog、somaxconn
  • 网络优化:中断绑核 + RPS/XPS + IPVS

比赛结束时,观众还在弹幕里刷“太刺激了”,我把当晚的三条变更写进了值班记录:

  1. 把 conntrack 上限从 2M 提到 6M;
  2. Ingress 改成 DaemonSet 全节点铺开,并开启 reuseport;
  3. Cilium 统一 MTU 1450 并复查所有节点物理 MTU。

第二天早上,产品同学说:“昨晚峰值在线比预估还高 18%,但用户没怎么反馈卡顿。”我点点头,把昨晚的补丁固化成了 git 里的默认配置。这就是运维的日常:不惊艳,不骄傲,但每一次洪峰都更稳一点。

附:关键配置片段集合

1)kube-proxy(若保留)IPVS

apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
  scheduler: "lc"

2)Nginx Ingress(WebSocket 路由示例)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: realtime
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  rules:
  - host: ws.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: realtime-svc
            port: {number: 8081}

3)Realtime Service(粘性会话)

apiVersion: v1
kind: Service
metadata: {name: realtime-svc}
spec:
  type: ClusterIP
  sessionAffinity: ClientIP
  selector: {app: realtime}
  ports: [{port: 8081, targetPort: 8081}]

4)PriorityClass

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata: {name: high-priority}
value: 1000000
globalDefault: false
description: "Critical for tournament traffic"

5)Redis(命中率与淘汰)

maxmemory 64gb
maxmemory-policy allkeys-lfu
tcp-backlog 16384
timeout 0