上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-27 10:37 阅读量:185


香港机房的监控屏上 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 或策略误配)

香港机房里的“坑”与当场解决

  1. MTU/隧道叠加:Underlay 9000,但 CNI 与 Overlay 叠加后,Pod MTU 需留足裕度。我把 Cilium 调到 8900,配合 tunnel=disabled + autoDirectNodeRoutes。之前未调,跨节点流量偶发重传。
  2. CentOS 7 + eBPF:默认 3.10 内核太老,Cilium 特性受限,务必上 ELRepo 5.x。同时把 SystemdCgroup=true,否则 cgroup v1 下 CPU quota 行为易误判。
  3. metrics-server 拉 kubelet 证书:老环境自签名多,加 --kubelet-insecure-tls 先通路,再逐步正则化证书。
  4. 邻居噪声(CPU 抢占):上线 PriorityClass + Overprovisioning 后,业务突刺时先清空缓冲舱,HPA 起容器前 1~2 秒内就能获得可用核,延迟大幅好转。
  5. 租户未写 requests/limits 直接 0/∞:先以 Kyverno 警告(audit),邮件通知规范上线;一周后切到 enforce。
  6. Ingress QPS 做 HPA 指标抖动:Adapter 里用 rate()[1m],HPA 设 stabilizationWindowSeconds=300;QPS 峰值缩短为“弹性吸收+缓冲舱”,不再脉冲扩缩。
  7. NVMe 热点卷:本地卷开发/测试无所谓,生产数据库仍建议用双机柜/跨 AZ 的高可用方案,把 HA 交给数据库层而非 PVC。

成本与效果(真实区间)

过量预置“缓冲舱”占 每节点 2~4 核 + 2~4Gi,换来扩容前的秒级可用核;峰值场景下 P99 延迟下降 15~30%。

Gold/Silver/Bronze 的配额+默认限制,把邻居噪声降到可观测与可控,租户侧的“卡别人资源”的电话几乎消失。

维护复杂度:租户入驻脚本化,5 分钟内交付可用的 Namespace + 配额 + 策略 + 管理员。

凌晨 4 点的走廊与一条来自租户的消息

缓冲舱生效的那一刻,HPA 没来得及伸,就先把“假人”清场了,核心服务的延迟像被人轻轻抚平。
我靠在走廊的防火门上,看着监控板变绿。过了两分钟,租户在群里发:“OK,流量顶过去了,稳定。”
后来我在周会上说:多租户不是“谁更猛”,而是“谁更有边界感”。ResourceQuota/LimitRange 定义秩序;HPA/VPA/KEDA 给你弹性;缓冲舱让弹性来得更快一点。香港这套,我就这么落地了。

附:我的“省心”检查清单

  1.  所有租户 NS 都有 ResourceQuota/LimitRange 且通过 Kyverno 强制。
  2.  默认 NetworkPolicy 拒绝一切,按需开白。
  3.  PriorityClass 与 PDB 已分级设置。
  4.  metrics-server / Prometheus / Adapter / Alertmanager 正常。
  5.  HPA 至少一个资源指标 + 一个业务指标;VPA 先 Off 再灰度 Auto;KEDA 用于异步。
  6.  节点上 Overprovisioning 开启,比例 5%~15%(按业务形态调)。
  7.  MTU 校准;Cilium 配置核对。
  8.  租户自助 RBAC 与“落地脚本”通关。
  9.  SLO 面板:按租户/服务分层。

如果你也在香港或其他机房跑多租户,别着急堆机器。先把边界、弹性和“缓冲”做好。下一次凌晨电话来时,你可能只需要看一眼面板,点点头,然后继续喝完那杯已经不太烫的咖啡。

目录结构
全文