在香港服务器上交付“省心”的多租户 Kubernetes:ResourceQuota/LimitRange 配置标准、HPA/VPA/KEDA 自动扩容实践、优先级与过量预置

香港机房的监控屏上 CPU 信用条像心电图乱跳——一个新租户把线上 API 打成了满堂红,邻居的延迟被带崩,电话一路打到我这儿。
“要不你们给我单独集群?”对面语气不善。
“给我一点时间。”我回。
我知道,真正“省心”的方案不是撒手不管的“全托管”,而是给多租户一个明确的边界、可预期的弹性,以及在物理边界(机柜/网段/磁盘)内的秩序。这篇就是我之后一周在香港节点把集群从“能跑”打磨到“好用、省心”的全流程笔记。
目标与思路
目标:在香港机房的物理服务器上,交付一个多租户的 Kubernetes 集群;每个租户有明确的资源配额与默认/最大限制,支持自动扩容(HPA/VPA/KEDA),并保证邻居噪声可控、可观测、可自助。
思路(三板斧):
- 硬边界:Namespace + NetworkPolicy + PodSecurity + RBAC,把租户隔离清楚;用 ResourceQuota/LimitRange 给每个租户“量杯”和“天花板”。
- 软弹性:HPA(短期流量摇摆)+ VPA(长期配额建议)+ 事件驱动的 KEDA(异步峰值),并配合低优先级过量预置(Overprovisioning)作为“缓冲舱”,极快回收可用核。
- 省心运维:可观测与告警(Prometheus/Alertmanager + Adapter)、统一的租户落地脚本、事后复盘用的 SLO 面板。
现场硬件与基础参数(香港·荃湾/葵涌两机柜互备)
| 角色 | 数量 | 机型/CPU | 内存 | 系统盘 | 数据盘 | 网卡 | 系统 |
|---|---|---|---|---|---|---|---|
| 控制平面/etcd | 3 | AMD EPYC 7302(16C) | 128GB | 2×480G SSD(RAID1) | 2×1.92TB NVMe(etcd 独占、RAID1) | 2×25GbE(Mellanox CX4-Lx,LACP) | CentOS 7(内核升级) |
| 工作节点 | 6 | AMD EPYC 7543(32C) | 256GB | 2×480G SSD(RAID1) | 4×3.84TB NVMe(LVM thin) | 2×25GbE(LACP) | CentOS 7(内核升级) |
| TOR 交换机 | 2 | 25G × 48口 + 100G 上联 | - | - | - | - | - |
- 网络:机房 Underlay MTU 9000,K8s CNI 侧我设 MTU 8900(预留封装),多租户走同一 CNI(Cilium),Namespace+NetPol 做东西向隔离。
- 为什么 CentOS 7:历史原因;我升级内核到 5.x(ELRepo kernel-ml),否则 eBPF 能力与 Cilium 的一些特性不舒服。
- 容器运行时:containerd。
- Kubernetes 版本:v1.28(生产稳定+特性妥当)。
系统准备(所有节点)
注:以下命令在 CentOS 7 上执行,先升级内核到 5.x 再装 CNI 更稳。
# 0) 关闭 swap + 基本内核参数
swapoff -a
sed -ri 's/^\s*([^#].*\sswap\s)/#\1/g' /etc/fstab
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
vm.max_map_count = 262144
EOF
modprobe br_netfilter
sysctl --system
# 1) 升级内核(必须重启)
yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel install -y kernel-ml
grub2-set-default 0
reboot
重启后:
uname -r # 确认 5.x 内核
安装 containerd / kubeadm / kubelet / kubectl:
# 2) containerd
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y containerd.io
mkdir -p /etc/containerd
containerd config default >/etc/containerd/config.toml
# SystemdCgroup = true
sed -ri 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
systemctl enable --now containerd
# 3) kube*(锁定一个稳定小版本,例如 1.28.x)
cat >/etc/yum.repos.d/kubernetes.repo <<'EOF'
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.28/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.28/rpm/repodata/repomd.xml.key
EOF
setenforce 0 || true
yum install -y kubelet kubeadm kubectl
systemctl enable kubelet
初始化控制平面(首个控制节点)
# 注意:pod-network-cidr 给 Cilium/Calico 预留,示例使用 10.244.0.0/16
kubeadm init \
--kubernetes-version=v1.28.9 \
--pod-network-cidr=10.244.0.0/16 \
--upload-certs
按输出配置 ~/.kube/config 并记录 join 命令。其余控制节点与工作节点用 kubeadm join 加入。
安装 CNI:Cilium(eBPF,内核 5.x)
# 安装 helm(如未装)
curl -fsSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
helm repo add cilium https://helm.cilium.io
helm repo update
# kube-proxy 先保留,Cilium 部分替代
helm install cilium cilium/cilium --version 1.15.7 \
--namespace kube-system \
--set kubeProxyReplacement=partial \
--set bpf.hostLegacyRouting=false \
--set autoDirectNodeRoutes=true \
--set tunnel=disabled \
--set mtu=8900
如果你坚持不升级内核,也可以上 Calico(iptables 模式),但在高 PPS 下开销更大,网络策略要小心优化。
基础观测与扩缩容前置
1)metrics-server(HPA 所需)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# 在老内核环境/自签名证书时,可以为 metrics-server Deployment 追加:
# args:
# - --kubelet-insecure-tls
# - --kubelet-preferred-address-types=InternalIP,Hostname,InternalDNS
2)Prometheus + kube-state-metrics + Adapter(自定义指标)
用社区 helm chart 部署 Prometheus/Alertmanager 与 kube-state-metrics。
Prometheus Adapter 将 PromQL 暴露为 Custom/External Metrics,供 HPA 使用(例如 ingress QPS、队列长度)。
Adapter 映射示例(ConfigMap 片段):
apiVersion: v1
kind: ConfigMap
metadata:
name: adapter-config
namespace: custom-metrics
data:
config.yaml: |
rules:
- seriesQuery: 'nginx_ingress_controller_requests:rate1m'
resources:
overrides:
namespace:
resource: namespace
ingress:
resource: ingress
name:
matches: "^(.*)_sum"
as: "${1}"
metricsQuery: 'sum(rate(nginx_ingress_controller_requests{ingress!="",namespace="<<.Namespace>>"}[1m])) by (namespace)'
多租户落地:Namespace + RBAC + 安全基线
A. 网络与安全默认拒绝
# 默认拒绝一切入站/出站,按需再开白名单
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: tenant-a
spec:
podSelector: {}
policyTypes: ["Ingress","Egress"]
# PodSecurity 标准:限制特权/hostPath 等
apiVersion: policy/v1
kind: PodSecurityPolicy # 若你已用 Pod Security Admission,则用 Namespace label: pod-security.kubernetes.io/*
# 如用 PSA,给 tenant 命名空间打:
# labels:
# pod-security.kubernetes.io/enforce: "baseline"
# pod-security.kubernetes.io/audit: "restricted"
# pod-security.kubernetes.io/warn: "restricted"
生产上我更推荐 Pod Security Admission + Kyverno:PSA 提供底座,Kyverno 强制“必须写 requests/limits”。
Kyverno 策略示例(强制资源声明):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-requests-limits
spec:
validationFailureAction: enforce
rules:
- name: require-resources
match:
resources:
kinds: ["Pod"]
validate:
message: "Containers must have CPU/memory requests and limits"
pattern:
spec:
containers:
- resources:
requests:
cpu: "?*"
memory: "?*"
limits:
cpu: "?*"
memory: "?*"
B. RBAC:租户自助但仅可见本命名空间
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tenant-admin
namespace: tenant-a
rules:
- apiGroups: ["", "apps", "batch", "extensions", "networking.k8s.io"]
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenant-admin-binding
namespace: tenant-a
subjects:
- kind: User
name: alice@example.com
roleRef:
kind: Role
name: tenant-admin
apiGroup: rbac.authorization.k8s.io
资源配额:ResourceQuota + LimitRange(“量杯 + 天花板”)
我们定义三档:Silver(默认)/ Gold / Bronze。配额表(示例,可按机房容量调整):
| 档位 | CPU requests | CPU limits | Mem requests | Mem limits | Pods | PVC | 存储 | LB/NP | 备注 |
|---|---|---|---|---|---|---|---|---|---|
| Bronze | 4 | 8 | 8Gi | 16Gi | 150 | 20 | 200Gi | LB:1 / NP:5 | 适合开发/测试 |
| Silver | 8 | 16 | 16Gi | 32Gi | 300 | 40 | 500Gi | LB:2 / NP:10 | 默认 |
| Gold | 16 | 32 | 32Gi | 64Gi | 600 | 80 | 1Ti | LB:4 / NP:20 | 生产核心 |
ResourceQuota 示例(Silver):
apiVersion: v1
kind: ResourceQuota
metadata:
name: rq-silver
namespace: tenant-a
spec:
hard:
requests.cpu: "8"
limits.cpu: "16"
requests.memory: 16Gi
limits.memory: 32Gi
pods: "300"
persistentvolumeclaims: "40"
requests.storage: 500Gi
services.loadbalancers: "2"
services.nodeports: "10"
count/ingresses.networking.k8s.io: "10"
configmaps: "200"
secrets: "400"
LimitRange(默认/最大/最小):
apiVersion: v1
kind: LimitRange
metadata:
name: lr-defaults
namespace: tenant-a
spec:
limits:
- type: Container
defaultRequest:
cpu: "100m"
memory: "256Mi"
default:
cpu: "500m"
memory: "512Mi"
max:
cpu: "2"
memory: "4Gi"
min:
cpu: "50m"
memory: "128Mi"
经验:LimitRange 的 defaultRequest 和 default 是“兜底安全带”。很多租户没写 requests/limits,一旦配额上线,会直接拒绝;先用 Kyverno 软提醒,再逐步 enforce。
优先级与中断预算(邻居噪声抑制)
PriorityClass:业务分级(gold/silver/bronze),缓冲舱(overprovisioning)设最低优先级。
PodDisruptionBudget (PDB):核心服务设 minAvailable,避免驱逐/升级时集群抖动。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: prio-gold
value: 100000
globalDefault: false
description: "Gold workloads"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: prio-overprovision
value: -10
globalDefault: false
description: "Spare capacity"
“缓冲舱”:低优先级过量预置(超快回收可用核)
思路:在每个节点跑一组 低优先级、可随时驱逐 的 pause 容器(或 sleep),占住部分 CPU/Mem。业务流量来了,kube-scheduler 会先驱逐它们,瞬间释放资源,避免 HPA 等待拉起准备期的真容器。
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovision
namespace: kube-system
spec:
replicas: 6 # 或按节点数 / 目标缓冲核设置
selector:
matchLabels:
app: overprovision
template:
metadata:
labels:
app: overprovision
spec:
priorityClassName: prio-overprovision
tolerations:
- operator: "Exists"
containers:
- name: reserve
image: gcr.io/google-containers/pause:3.6
resources:
requests:
cpu: "2000m" # 每副本占 2 核
memory: "2Gi"
limits:
cpu: "2000m"
memory: "2Gi"
这招在物理节点固定(暂不做自动加/减节点)的机房里特别好用,配合 HPA/VPA 非常“省心”。
自动扩容:HPA / VPA / KEDA 组合拳
1)HPA(基于 CPU/内存 + 自定义指标)
示例:某租户的 api-deployment 以 CPU 为主触发,且参考 ingress QPS(来自 Prometheus Adapter 的 external metric)。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: tenant-a
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-deployment
minReplicas: 3
maxReplicas: 30
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: External
external:
metric:
name: nginx_ingress_controller_requests
target:
type: AverageValue
averageValue: "200" # 每 Pod 目标 200 RPS(需与 Adapter 配置吻合)
2)VPA(建议优先“推荐模式”,逐步灰度“自动”)
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-vpa
namespace: tenant-a
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: api-deployment
updatePolicy:
updateMode: "Off" # 先只给建议,避免生产频繁驱逐
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
cpu: "100m"
memory: "256Mi"
maxAllowed:
cpu: "4"
memory: "8Gi"
我每周例会把 VPA 建议与实际 QPS/延迟对齐,必要时手动更新 Deployment 的 requests/limits。成熟后,部分无状态服务可以灰度打开 Auto。
3)KEDA(事件驱动,例如 Kafka/队列长度)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-scaledobject
namespace: tenant-a
spec:
scaleTargetRef:
kind: Deployment
name: worker
minReplicaCount: 0
maxReplicaCount: 50
cooldownPeriod: 120
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-0.kafka:9092
topic: orders
consumerGroup: worker
lagThreshold: "1000"
存储:本地 NVMe + LVM Thin + 本地存储类
香港节点我选 本地 NVMe 做性能优先,避免跨机柜复制带来的延迟。使用 LVM Thin 切卷,结合 local-path-provisioner 提供简单的 StorageClass;对真正需要高可用的状态服务,我用双机柜的数据库层(MySQL/Galera 或 Kafka 集群)而不是把 HA 压到 PVC 上。
StorageClass(local-path)示例:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-nvme
provisioner: rancher.io/local-path
parameters:
path: /var/local-path-provisioner
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
租户落地一把梭:我在现场用的脚本(创建 NS/配额/默认限制/安全策略/管理员)
TENANT=tenant-a
TIER=silver # bronze/silver/gold
kubectl create ns $TENANT
# 默认拒绝流量
kubectl -n $TENANT apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes: ["Ingress","Egress"]
EOF
# LimitRange
kubectl -n $TENANT apply -f - <<'EOF'
apiVersion: v1
kind: LimitRange
metadata:
name: lr-defaults
spec:
limits:
- type: Container
defaultRequest: { cpu: "100m", memory: "256Mi" }
default: { cpu: "500m", memory: "512Mi" }
max: { cpu: "2", memory: "4Gi" }
min: { cpu: "50m", memory: "128Mi" }
EOF
# ResourceQuota(按档位注入)
case $TIER in
bronze) CPU_REQ=4; CPU_LIM=8; MEM_REQ=8Gi; MEM_LIM=16Gi; PODS=150; PVC=20; STOR=200Gi; LB=1; NP=5;;
silver) CPU_REQ=8; CPU_LIM=16; MEM_REQ=16Gi; MEM_LIM=32Gi; PODS=300; PVC=40; STOR=500Gi; LB=2; NP=10;;
gold) CPU_REQ=16; CPU_LIM=32; MEM_REQ=32Gi; MEM_LIM=64Gi; PODS=600; PVC=80; STOR=1Ti; LB=4; NP=20;;
esac
cat <<EOF | kubectl -n $TENANT apply -f -
apiVersion: v1
kind: ResourceQuota
metadata:
name: rq-$TIER
spec:
hard:
requests.cpu: "${CPU_REQ}"
limits.cpu: "${CPU_LIM}"
requests.memory: ${MEM_REQ}
limits.memory: ${MEM_LIM}
pods: "${PODS}"
persistentvolumeclaims: "${PVC}"
requests.storage: ${STOR}
services.loadbalancers: "${LB}"
services.nodeports: "${NP}"
count/ingresses.networking.k8s.io: "${NP}"
configmaps: "200"
secrets: "400"
EOF
# 绑定管理员(示例)
kubectl -n $TENANT create role tenant-admin --verb='*' --resource='*' || true
kubectl -n $TENANT create rolebinding tenant-admin-binding \
--role=tenant-admin --user=alice@example.com
观测与 SLO:我在线上盯的几个面板/告警
按租户维度的资源用量(CPU/内存/Pods/PVC/存储空间)对齐 ResourceQuota。
HPA 行为:1 分钟/5 分钟内的扩容次数、冷却期命中、是否打到了 maxReplicas。
队列积压(KEDA 触发指标)与消费速度。
PodEviction/驱逐原因(是否来自 VPA/资源压力/节点压力)。
告警阈值:
- 某租户 requests.cpu 使用率 > 85% 持续 10 分钟
- HPA 撞 maxReplicas 且 P99 延迟恶化 5 分钟
- 节点磁盘可用 < 15%(NVMe)
- Cilium Drop/CT 错误突增(通常是 MTU 或策略误配)
香港机房里的“坑”与当场解决
- MTU/隧道叠加:Underlay 9000,但 CNI 与 Overlay 叠加后,Pod MTU 需留足裕度。我把 Cilium 调到 8900,配合 tunnel=disabled + autoDirectNodeRoutes。之前未调,跨节点流量偶发重传。
- CentOS 7 + eBPF:默认 3.10 内核太老,Cilium 特性受限,务必上 ELRepo 5.x。同时把 SystemdCgroup=true,否则 cgroup v1 下 CPU quota 行为易误判。
- metrics-server 拉 kubelet 证书:老环境自签名多,加 --kubelet-insecure-tls 先通路,再逐步正则化证书。
- 邻居噪声(CPU 抢占):上线 PriorityClass + Overprovisioning 后,业务突刺时先清空缓冲舱,HPA 起容器前 1~2 秒内就能获得可用核,延迟大幅好转。
- 租户未写 requests/limits 直接 0/∞:先以 Kyverno 警告(audit),邮件通知规范上线;一周后切到 enforce。
- Ingress QPS 做 HPA 指标抖动:Adapter 里用 rate()[1m],HPA 设 stabilizationWindowSeconds=300;QPS 峰值缩短为“弹性吸收+缓冲舱”,不再脉冲扩缩。
- NVMe 热点卷:本地卷开发/测试无所谓,生产数据库仍建议用双机柜/跨 AZ 的高可用方案,把 HA 交给数据库层而非 PVC。
成本与效果(真实区间)
过量预置“缓冲舱”占 每节点 2~4 核 + 2~4Gi,换来扩容前的秒级可用核;峰值场景下 P99 延迟下降 15~30%。
Gold/Silver/Bronze 的配额+默认限制,把邻居噪声降到可观测与可控,租户侧的“卡别人资源”的电话几乎消失。
维护复杂度:租户入驻脚本化,5 分钟内交付可用的 Namespace + 配额 + 策略 + 管理员。
凌晨 4 点的走廊与一条来自租户的消息
缓冲舱生效的那一刻,HPA 没来得及伸,就先把“假人”清场了,核心服务的延迟像被人轻轻抚平。
我靠在走廊的防火门上,看着监控板变绿。过了两分钟,租户在群里发:“OK,流量顶过去了,稳定。”
后来我在周会上说:多租户不是“谁更猛”,而是“谁更有边界感”。ResourceQuota/LimitRange 定义秩序;HPA/VPA/KEDA 给你弹性;缓冲舱让弹性来得更快一点。香港这套,我就这么落地了。
附:我的“省心”检查清单
- 所有租户 NS 都有 ResourceQuota/LimitRange 且通过 Kyverno 强制。
- 默认 NetworkPolicy 拒绝一切,按需开白。
- PriorityClass 与 PDB 已分级设置。
- metrics-server / Prometheus / Adapter / Alertmanager 正常。
- HPA 至少一个资源指标 + 一个业务指标;VPA 先 Off 再灰度 Auto;KEDA 用于异步。
- 节点上 Overprovisioning 开启,比例 5%~15%(按业务形态调)。
- MTU 校准;Cilium 配置核对。
- 租户自助 RBAC 与“落地脚本”通关。
- SLO 面板:按租户/服务分层。
如果你也在香港或其他机房跑多租户,别着急堆机器。先把边界、弹性和“缓冲”做好。下一次凌晨电话来时,你可能只需要看一眼面板,点点头,然后继续喝完那杯已经不太烫的咖啡。