跳过正文
Kubernetes 多租户方案深度对比:vCluster vs Capsule vs HNC

Kubernetes 多租户方案深度对比:vCluster vs Capsule vs HNC

·1374 字·7 分钟·
目录

多租户的本质问题
#

很多团队以为给每个团队创建一个 Namespace 就实现了多租户,这是对 K8s 隔离模型最大的误解。

Namespace 本质上只是一个命名空间,不是安全边界。来看几个具体问题:

1. 集群级资源无隔离

ClusterRoleStorageClassPriorityClassIngressClassCRD 全是集群范围的资源。一个租户的管理员如果拿到了 ClusterRole 的创建权限,整个集群就暴露了。即便你用 RoleBinding 把权限锁在 Namespace 内,共享的 ClusterRole 仍然可能被利用。

2. 网络默认互通

不加 NetworkPolicy 的情况下,任意 Pod 都能访问其他 Namespace 的 Service。kube-dns 全局解析,Pod 直接 curl http://payment-service.finance.svc.cluster.local 就能跨租户访问。

3. 资源抢占

没有 ResourceQuotaLimitRange 的 Namespace,里面的 Pod 可以把节点内存吃满,影响所有邻居。但配置这些还需要有人维护,一旦漏掉就是生产故障。

4. 审计和计费困难

多个团队共用集群,谁消耗了多少 CPU/Memory?按 Namespace 汇总很粗粒度,跨 Namespace 的项目更难统计。

5. 自助申请困难

开发团队想新建一个 Namespace,要找平台团队手动操作,还要配齐 NetworkPolicy、ResourceQuota、LimitRange、ServiceAccount、RoleBinding……每次都是重复劳动。

这五个问题是真实的生产痛点。接下来的三个方案从不同维度解决它们。


方案一:vCluster
#

架构原理
#

vCluster 的思路最激进:在宿主集群的 Namespace 里运行一个完整的虚拟 K8s 集群

Host Cluster
└── Namespace: tenant-a
    ├── Pod: vcluster-0 (StatefulSet)
    │   ├── k3s / k8s API Server
    │   ├── etcd (可选独立)
    │   └── syncer (核心组件)
    └── Service: vcluster (LoadBalancer/NodePort)

Syncer 是 vCluster 的关键:它把虚拟集群里的 Pod、Service、PVC 等资源"同步"到宿主集群的 Namespace 里真正调度。虚拟集群的 API Server 完全独立,租户拿到的 kubeconfig 指向这个虚拟 API Server,对宿主集群一无所知。

同步策略分两层:

  • 向下同步:Pod、ConfigMap、Secret(部分)、PVC 从虚拟集群同步到宿主
  • 向上同步:Pod 状态、Node 信息从宿主同步回虚拟集群

Node 默认以伪节点形式出现在虚拟集群里,租户看到的是"完整的集群",但底层调度还是宿主 Scheduler。

安装
#

# 安装 vCluster CLI
curl -L -o /usr/local/bin/vcluster \
  "https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-linux-amd64"
chmod +x /usr/local/bin/vcluster

# 创建租户 A 的虚拟集群
vcluster create tenant-a \
  --namespace vcluster-tenant-a \
  --values values-tenant-a.yaml

values-tenant-a.yaml 的关键配置:

# values-tenant-a.yaml
controlPlane:
  distro:
    k3s:
      enabled: true
      version: "v1.29.3-k3s1"
  statefulSet:
    resources:
      requests:
        cpu: 200m
        memory: 256Mi
      limits:
        cpu: 2
        memory: 2Gi

# 同步策略
sync:
  toHost:
    pods:
      enabled: true
      rewriteHosts:
        enabled: true
    services:
      enabled: true
    persistentVolumeClaims:
      enabled: true
    ingresses:
      enabled: true
    networkPolicies:
      enabled: true  # 允许租户管理自己的 NetworkPolicy
  fromHost:
    nodes:
      enabled: true
      selector:
        all: false
        labels:
          tenant: "a"  # 可以绑定特定节点池

# 资源隔离:映射宿主 StorageClass
mapServices:
  fromHost:
    - from: fast-ssd
      to: default

# 把宿主的某个 Secret 注入虚拟集群(如镜像仓库凭据)
referencedCoreV1Resources: "secrets,configmaps"

# 隔离模式:禁止访问宿主 API
experimental:
  isolatedControlPlane:
    enabled: false

# 给宿主 Namespace 加 ResourceQuota
isolation:
  enabled: true
  resourceQuota:
    enabled: true
    quota:
      requests.cpu: "10"
      requests.memory: 20Gi
      limits.cpu: "20"
      limits.memory: 40Gi
      count/pods: "200"
  limitRange:
    enabled: true
    default:
      cpu: 500m
      memory: 512Mi
    defaultRequest:
      cpu: 100m
      memory: 128Mi

获取租户 kubeconfig
#

# 获取虚拟集群的 kubeconfig
vcluster connect tenant-a --namespace vcluster-tenant-a \
  --server https://tenant-a.k8s.example.com \
  --update-current=false \
  -n vcluster-tenant-a \
  > tenant-a-kubeconfig.yaml

# 租户管理员拿到这个 kubeconfig 后,有完整的集群管理权
KUBECONFIG=tenant-a-kubeconfig.yaml kubectl get nodes
# NAME          STATUS   ROLES    AGE
# fake-node-0   Ready    <none>   5m

网络隔离补充
#

虚拟集群的 Pod 在宿主层共享节点网络,需要在宿主层加 NetworkPolicy 隔离不同虚拟集群的流量:

# 宿主集群:禁止不同 vcluster namespace 之间的流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: isolate-vcluster
  namespace: vcluster-tenant-a
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector: {}          # 同 namespace 内允许
  egress:
    - to:
        - podSelector: {}          # 同 namespace 内允许
    - ports:
        - port: 53                 # DNS
          protocol: UDP
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system

方案二:Capsule
#

架构原理
#

Capsule 的思路是不引入新的控制平面,而是在现有集群上叠加多租户语义

核心概念:Tenant CRD 聚合一组 Namespace,通过 Webhook 和控制器在这些 Namespace 上统一执行策略。

Capsule Controller
├── TenantController → 管理 Namespace 创建/策略下推
├── Admission Webhook → 拦截请求,执行跨 Namespace 策略
└── Capsule Proxy (可选) → 代理 kubectl,实现跨 Namespace 资源聚合视图

Tenant: team-frontend
├── Namespace: frontend-dev
├── Namespace: frontend-staging
└── Namespace: frontend-prod
    (统一 ResourceQuota, NetworkPolicy, RBAC, ImagePolicy)

TenantUser 通过普通 kubeconfig 访问集群,Webhook 识别他的身份,限制他只能操作自己 Tenant 下的 Namespace。

安装
#

helm repo add projectcapsule https://projectcapsule.github.io/charts
helm repo update

helm install capsule projectcapsule/capsule \
  --namespace capsule-system \
  --create-namespace \
  --set manager.options.forceTenantPrefix=true \
  --set manager.options.userGroups[0]=capsule.clastix.io \
  --set manager.options.capsuleUserGroups[0]=capsule.clastix.io

创建 Tenant
#

# tenant-frontend.yaml
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: team-frontend
spec:
  owners:
    - name: alice
      kind: User
    - name: frontend-leads
      kind: Group

  # Namespace 命名限制(强制前缀)
  namespaceOptions:
    quota: 10                    # 最多创建 10 个 Namespace
    forbiddenLabels:
      denied:
        - environment: production  # 禁止自行打 production 标签
    additionalMetadata:
      labels:
        team: frontend
        cost-center: cc-001
      annotations:
        monitoring.example.com/team: frontend

  # 统一 ResourceQuota
  resourceQuotas:
    scope: Tenant                # Tenant 级别总量
    items:
      - hard:
          requests.cpu: "20"
          requests.memory: 40Gi
          limits.cpu: "40"
          limits.memory: 80Gi
          count/pods: "500"
          count/services: "50"
          count/persistentvolumeclaims: "20"

  # 每个 Namespace 的 LimitRange
  limitRanges:
    items:
      - limits:
          - type: Container
            default:
              cpu: 500m
              memory: 512Mi
            defaultRequest:
              cpu: 100m
              memory: 128Mi
            max:
              cpu: "8"
              memory: 16Gi

  # NetworkPolicy:自动注入到每个 Namespace
  networkPolicies:
    items:
      - podSelector: {}
        policyTypes:
          - Ingress
          - Egress
        ingress:
          - from:
              - namespaceSelector:
                  matchLabels:
                    capsule.clastix.io/tenant: team-frontend
        egress:
          - to:
              - namespaceSelector:
                  matchLabels:
                    capsule.clastix.io/tenant: team-frontend
          - ports:
              - port: 53
                protocol: UDP
              - port: 443  # 允许出公网 HTTPS

  # 允许使用哪些 StorageClass
  storageClasses:
    matchLabels:
      capsule.clastix.io/storage-class: allowed
    allowed:
      - fast-ssd
      - standard

  # 允许使用哪些 IngressClass
  ingressOptions:
    allowedClasses:
      allowed:
        - nginx
    allowedHostnames:
      allowed:
        - "*.frontend.example.com"
    hostnameCollisionScope: Tenant  # 防止同租户内域名冲突

  # 节点选择器(可选)
  nodeSelector:
    node-pool: frontend

  # 镜像仓库限制
  containerRegistries:
    allowed:
      - registry.example.com
      - "*.dkr.ecr.*.amazonaws.com"
kubectl apply -f tenant-frontend.yaml

# 创建绑定关系:alice 加入 capsule.clastix.io 组
# (通常通过 OIDC 的 group claim 实现)
kubectl create clusterrolebinding alice-capsule \
  --clusterrole=capsule:tenant:team-frontend \
  --user=alice

租户自助创建 Namespace
#

租户管理员(alice)创建 Namespace 时,Capsule Webhook 自动验证前缀和配额:

# alice 的 kubeconfig 指向同一个 API Server,但 Webhook 限制了她的操作范围
kubectl create namespace frontend-dev
# namespace/frontend-dev created  (Capsule 自动打上 tenant label,注入策略)

kubectl create namespace production-db
# Error: namespace name must have prefix "team-frontend-"
# (forceTenantPrefix=true 时自动验证)

Capsule Proxy
#

Capsule Proxy 让租户用 kubectl get namespaces 只看到自己的 Namespace,解决 ClusterScoped 资源的"幻觉隔离":

helm install capsule-proxy projectcapsule/capsule-proxy \
  --namespace capsule-system \
  --set options.generateCertificates=true \
  --set options.oidcUsernameClaim=email

# 租户 kubeconfig 的 server 改为 capsule-proxy 地址
# kubectl get namespaces 只返回 team-frontend 下的 namespace

方案三:HNC(Hierarchical Namespace Controller)
#

架构原理
#

HNC 来自 Google,解决的是Namespace 策略继承问题,而非完整的多租户隔离。

org-root (anchor)
├── team-platform
│   ├── platform-dev
│   └── platform-staging
└── team-frontend
    ├── frontend-dev
    └── frontend-prod
        └── frontend-prod-canary  (子 Namespace)

核心机制:

  • SubnamespaceAnchor:在父 Namespace 创建一个特殊对象,触发子 Namespace 的创建
  • 传播规则:父 Namespace 的 RBAC、NetworkPolicy、LimitRange 自动传播到所有子 Namespace
  • 异常覆盖:子 Namespace 可以声明 propagate.hnc.x-k8s.io/nonCascading 阻止传播

安装
#

# 使用官方 manifest
kubectl apply -f https://github.com/kubernetes-sigs/hierarchical-namespaces/releases/latest/download/default.yaml

# 安装 kubectl 插件
curl -L https://github.com/kubernetes-sigs/hierarchical-namespaces/releases/latest/download/kubectl-hns_linux_amd64 \
  -o /usr/local/bin/kubectl-hns
chmod +x /usr/local/bin/kubectl-hns

创建层级 Namespace
#

# 创建根 Namespace
kubectl create namespace team-frontend

# 平台团队在 team-frontend 下创建子 Namespace
kubectl hns create frontend-dev -n team-frontend
kubectl hns create frontend-staging -n team-frontend
kubectl hns create frontend-prod -n team-frontend

# 开发团队可以在 frontend-dev 下自助创建子 Namespace
kubectl hns create frontend-dev-feature-x -n frontend-dev

对应的 SubnamespaceAnchor 对象(自动创建):

apiVersion: hnc.x-k8s.io/v1alpha2
kind: SubnamespaceAnchor
metadata:
  name: frontend-dev
  namespace: team-frontend

传播 RBAC
#

# 在父 Namespace 创建 RoleBinding,自动传播到所有子 Namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: frontend-dev-access
  namespace: team-frontend
  annotations:
    # 不加这个 annotation 默认全部传播
    # hnc.x-k8s.io/propagated: "true"
subjects:
  - kind: Group
    name: frontend-engineers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: edit
  apiGroup: rbac.authorization.k8s.io

HNC 配置策略
#

apiVersion: hnc.x-k8s.io/v1alpha2
kind: HNCConfiguration
metadata:
  name: config
spec:
  resources:
    - resource: roles
      group: rbac.authorization.k8s.io
      mode: Propagate          # 传播
    - resource: rolebindings
      group: rbac.authorization.k8s.io
      mode: Propagate
    - resource: networkpolicies
      group: networking.k8s.io
      mode: Propagate
    - resource: limitranges
      group: ""
      mode: Propagate
    - resource: resourcequotas
      group: ""
      mode: Ignore             # ResourceQuota 不传播,各子 Namespace 独立配置
    - resource: configmaps
      group: ""
      mode: Propagate
    - resource: secrets
      group: ""
      mode: AllowPropagate     # 仅传播带有特定 annotation 的 Secret

隔离能力横向对比
#

维度vClusterCapsuleHNC
API Server 隔离完全独立共享共享
etcd 隔离独立(虚拟集群内)共享共享
CRD 隔离完全隔离,租户可自定义 CRD共享 CRD,不能冲突共享 CRD
RBAC 隔离虚拟集群内完全独立Webhook 强制,ClusterRole 共享传播继承,ClusterRole 共享
网络隔离宿主层 NetworkPolicy 手动配置自动注入 NetworkPolicy传播 NetworkPolicy
节点隔离可绑定节点池(node selector)可指定 nodeSelector不涉及
资源配额宿主 Namespace 层 ResourceQuotaTenant 级聚合 + Namespace 级各自独立配置
自助 Namespace租户内完全自助租户内受控自助子 Namespace 自助
K8s 版本差异可以和宿主不同版本必须一致必须一致
运营开销每租户一个虚拟集群(资源开销约 200m CPU/256Mi)轻量,Webhook + Controller极轻量
成熟度CNCF Sandbox,生产可用CNCF Sandbox,生产可用k8s-sigs,Google 内部大量使用

租户自助 Namespace 申请工作流
#

以 Capsule 为例,设计一个 GitOps 驱动的自助申请流程:

开发团队 → PR 到 tenant-config 仓库
         ↓
         提交 SubnamespaceRequest(自定义 CRD 或 YAML)
         ↓
Reviewer 审批 → ArgoCD 同步 → Capsule 创建 Namespace
         ↓
         自动触发:注入 NetworkPolicy、ResourceQuota、LimitRange、ServiceAccount
         ↓
         Slack/钉钉通知申请人

SubnamespaceRequest 示例(简化版 CRD):

apiVersion: platform.example.com/v1alpha1
kind: NamespaceRequest
metadata:
  name: feature-payment-refactor
  namespace: team-backend    # 提交到所在 Tenant 的父 Namespace
spec:
  requestedBy: bob@example.com
  purpose: "重构支付模块,需要独立测试环境"
  ttl: "30d"                 # 30 天后自动回收
  resourceProfile: small     # small/medium/large 对应预设的 ResourceQuota
  environments:
    - dev
    - staging

选型指南
#

SaaS 平台(强隔离)→ vCluster
#

  • 客户之间完全隔离,不能互相感知
  • 客户需要 CRD 自定义能力(安装自己的 Operator)
  • 不同客户可能需要不同 K8s 版本(版本销售)
  • 代价:每客户至少 200m CPU + 256Mi,1000 个租户就是 200 核 + 256Gi 的控制平面开销
# 自动化创建:每个新客户注册时触发
vcluster create customer-${CUSTOMER_ID} \
  --namespace vcluster-${CUSTOMER_ID} \
  --values /etc/vcluster/customer-template.yaml

企业内部平台(受控共享)→ Capsule
#

  • 多个业务团队共用集群,平台团队统一治理
  • 需要集中管控镜像仓库、IngressClass、StorageClass 的使用权限
  • 团队需要跨 Namespace 的聚合视图(多个 env Namespace 属于同一团队)
  • 不需要 CRD 隔离,共享 Operator 生态

开发环境/项目隔离(轻量策略继承)→ HNC
#

  • 项目树状管理:org → team → project → feature-branch
  • 主要诉求是 RBAC 和 NetworkPolicy 的层级继承,减少手工配置
  • 不需要强隔离,信任内部用户
  • 已有大量 Namespace,想在不迁移的情况下增加层级管理

费用计量与 Chargeback(OpenCost)
#

部署 OpenCost 后,按 Namespace 汇总费用,再结合 Capsule 的 Tenant 标签做 Chargeback:

helm install opencost opencost/opencost \
  --namespace opencost \
  --create-namespace \
  --set opencost.exporter.cloudProviderApiKey="" \
  --set opencost.prometheus.internal.enabled=true

查询 team-frontend 的月度费用:

# OpenCost API
curl "http://opencost.opencost.svc:9003/allocation/compute?\
  window=month&\
  aggregate=namespace&\
  filter=namespace:frontend-dev+frontend-staging+frontend-prod" \
  | jq '.data[0] | to_entries[] | {ns: .key, cost: .value.totalCost}'

结合 Capsule 的 cost-center annotation,自动生成 Chargeback 报表:

import requests

def get_tenant_cost(tenant_namespaces: list[str], window: str = "month") -> float:
    ns_filter = "+".join(tenant_namespaces)
    resp = requests.get(
        f"http://opencost.opencost.svc:9003/allocation/compute",
        params={"window": window, "aggregate": "namespace", "filter": f"namespace:{ns_filter}"}
    )
    data = resp.json()["data"][0]
    return sum(v["totalCost"] for v in data.values())

# Capsule Tenant 的 cost-center label → 汇总到对应部门
tenants = {
    "cc-001": ["frontend-dev", "frontend-staging", "frontend-prod"],
    "cc-002": ["backend-dev", "backend-staging"],
}
for cc, namespaces in tenants.items():
    cost = get_tenant_cost(namespaces)
    print(f"Cost Center {cc}: ${cost:.2f}/month")

总结
#

三种方案不是竞争关系,甚至可以组合使用——用 HNC 管理 Namespace 树,在 HNC 管理的 Namespace 里运行 Capsule Tenant,或者用 vCluster 给强隔离需求的外部客户,用 Capsule 管理内部团队。

关键决策因素只有三个:隔离强度(外部客户 vs 内部团队)、CRD 自主性(租户是否需要安装自己的 Operator)、规模(租户数量决定控制平面开销是否可接受)。把这三个问题回答清楚,选型就不会错。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。

相关文章

如何设计一个好的告警体系

·570 字·3 分钟
从真实的告警噪音泛滥经历出发,分享如何用 SLI/SLO 重新设计告警体系,包括告警分级、规则设计原则、路由策略和复盘机制。

Kubernetes GPU 调度实战:AI 训练与推理基础设施

·1926 字·10 分钟
GPU 是 AI 基础设施的核心资源,如何在 Kubernetes 上高效调度和管理 GPU 直接影响训练效率和推理成本。本文从底层驱动安装到上层调度策略,完整覆盖 K8s GPU 基础设施的搭建、监控和优化实践。