
决赛前晚 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 分钟)
- 切换入口到 金丝雀,预热静态与 Top N 接口
- 检查:conntrack 使用率 < 40%,Ingress 活跃连接 < 60%
- 提前放大:api、realtime 副本 +50%,HPA 上限翻倍
- Redis 热 Key 预热,Kafka 分区 leader 均衡
- 压测 5 分钟(低强度)验证
- 比赛开始 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
比赛结束时,观众还在弹幕里刷“太刺激了”,我把当晚的三条变更写进了值班记录:
- 把 conntrack 上限从 2M 提到 6M;
- Ingress 改成 DaemonSet 全节点铺开,并开启 reuseport;
- 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