嘿,先别急着去配置那些复杂的 YAML 文件。在 Kubernetes 的世界里,多租户隔离从来不是“开一个开关”就能搞定的事。很多刚接手公司集群的工程师,看着那些 Namespace 风平浪静,以为万事大吉,结果一到高峰期,CPU 飙红、内存 OOM、网络超时,一堆锅甩过来,才发现之前的“隔离”其实只是心理安慰。
今天咱们不聊教科书式的定义,我就带你把这套东西从里到外拆解一遍。想象一下,你把一栋大楼(集群)租给了好几家公司(租户)。你不能只说“你们各住各的楼层”,还得保证 A 公司搬重物时不能把 B 公司的承重墙震裂,A 公司开会噪音太大不能影响 B 公司开会。Kubernetes 的 Namespace 就像楼层和房间,但真正的“隔音”和“限电”措施,需要一层层去配。
一、 资源配额:别让一家独大,吃撑了还打嗝
首先得解决最直观的问题:资源争抢。在 Kubernetes 里,最基本的隔离手段就是 ResourceQuota 和 LimitRange。这俩玩意儿经常一起用,但分工不同,我习惯把它们比作“房间租金”和“电器功率限制”。
1.1 ResourceQuota:给整个租户定个“总预算”
ResourceQuota 是针对整个 Namespace 的总和限制。如果你的租户 A 有 10 个团队,你不能保证每个团队都不超,但你得保证整个 Namespace 的总 CPU 和总内存不能超过你买的服务器容量,对吧?
假设你给租户 A 分配了 100 核 CPU 和 200Gi 内存的额度。那我们就得创建一个 ResourceQuota 对象。
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-a-quota
namespace: tenant-a
spec:
hard:
requests.cpu: "100"
requests.memory: 200Gi
limits.cpu: "200"
limits.memory: 400Gi
pods: "50"
services: "20"
persistentvolumeclaims: "10"
replicationcontrollers: "10"
configmaps: "50"
secrets: "50"
services.loadbalancers: "2"
services.nodeports: "0"
你看,这里不光限制了 CPU 和内存,我还特意限制了 pods、services 的数量。为什么?因为有些恶意(或者不小心)的用户,可能写一个循环脚本,瞬间创建几千个 Pod,把 API Server 打崩。这叫“拒绝服务”,不一定是故意的,但得防着点。
实战坑点:很多新手只配 limits.cpu,忘了配 requests.cpu。结果是,集群调度过度激进,所有 Pod 都挤在一台机器上,因为 Kubernetes 认为它们只占很少的资源(基于 request),但实际跑起来全满负载,导致邻居租户的 Pod 被挤压到交换分区,甚至 OOM。所以,requests 代表“保底需求”,limits 代表“最大上限”,两个都要设,而且 requests 别设太低,要贴近真实使用量。
1.2 LimitRange:给每个“小房间”定个“单体规格”
有了总预算,还得防止租户 A 里某个不守规矩的团队,单个 Pod 就吃掉所有配额。这时候 LimitRange 就派上用场了。它能限制单个容器的最小和最大资源。
apiVersion: v1
kind: LimitRange
metadata:
name: tenant-a-limits
namespace: tenant-a
spec:
limits:
- default:
cpu: "4"
memory: 8Gi
defaultRequest:
cpu: "2"
memory: 4Gi
max:
cpu: "8"
memory: 16Gi
min:
cpu: "100m"
memory: 128Mi
type: Container
这段配置的意思是:在这个 Namespace 里,每个容器默认申请 2 核 4G,最大不能超过 8 核 16G。如果你想跑个超大数据库,对不起,得找管理员申请特批。
这里有个细节:default 和 defaultRequest 必须配合使用。如果你只设了 default(限制),没设 defaultRequest(请求),Kubernetes 可能会把 default 同时当作 request 和 limit,这会导致集群调度器误判资源可用性,进而引发集群层面的不稳定。
二、 访问控制:谁进谁的门,带什么钥匙
资源隔离好了,接下来是权限隔离。Namespace 本身是个逻辑隔离边界,但默认情况下,Kubernetes 的 RBAC(基于角色的访问控制)是集群范围的。这意味着,租户 A 的管理员可能无意中能看到租户 B 的 Pod 列表,虽然看不到内容,但这种“窥视”感很糟糕,而且一旦配置错误,权限就可能越界。
2.1 精细化的 RBAC 策略
我们要做的,是给每个租户创建独立的 Role 和 RoleBinding,并把范围死死锁在各自的 Namespace 里。
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: tenant-a
name: tenant-a-developer
rules:
- apiGroups: ["", "apps", "batch", "extensions"]
resources: ["pods", "deployments", "jobs", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenant-a-developer-binding
namespace: tenant-a
subjects:
- kind: User
name: developer-alice
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: tenant-a-developer
apiGroup: rbac.authorization.k8s.io
注意看,Role 和 RoleBinding 都绑定在 tenant-a 这个 Namespace 下。developer-alice 只能操作 tenant-a 里的资源。如果你想让她也能看日志,还得额外加 pods/log 的权限。
但是,等等! 这样够吗?不够。因为 Kubernetes 里还有 ClusterRole 和 ClusterRoleBinding。有些内置的 ClusterRole(比如 view、edit、admin)如果是集群级别的,可能会跨 Namespace 生效。所以,强烈建议:不要直接使用集群默认的 ClusterRole 绑定到用户身上,而是复制一份,做成 Namespace 级别的 Role。
2.2 网络策略:物理防火墙
资源配额管的是 CPU 和内存,但不管网络。如果租户 A 的 Pod 想去扫描租户 B 的服务,或者发起 DDOS 攻击,纯靠配额是拦不住的。这时候就需要 NetworkPolicy。
NetworkPolicy 依赖于 CNI 插件。你得确认你的集群用的是 Calico、Cilium 或者 Cilium 这种支持 NetworkPolicy 的网络插件。如果你用的是最简单的 Flannel 默认配置,NetworkPolicy 是不生效的!这点非常重要,很多生产环境踩的坑就在这里。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-internal-only
namespace: tenant-a
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
purpose: tenant-a
podSelector:
matchLabels:
app: frontend
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 8080
这个策略的意思是:在 tenant-a 这个 Namespace 里,标签为 app: backend 的 Pod,只允许接收来自以下来源的流量:
- 同样在
tenant-a里,标签为app: frontend的 Pod。 - 其他也在
tenant-a里的backendPod。 - 端口必须是 8080。
这就实现了一个很关键的安全边界:默认拒绝所有入站流量,只开放必要的。这样,即使租户 B 和租户 A 跑在同一台物理机上,租户 B 的 Pod 也无法直接通过 IP 访问租户 A 的服务,除非你们显式地配置了跨 Namespace 的网络连通性(通常不建议这么做,除非有 Service Mesh 介入)。
三、 调度隔离:别让邻居住隔壁
前面说了资源配额和网络隔离,但还有一个隐形的问题:调度。Kubernetes 的调度器默认是“最佳适配”,它会把 Pod 塞到资源最空闲的节点上。但如果租户 A 的任务突然激增,占满了某些节点,可能会影响跑在同一个节点上的租户 B 的 Pod,尤其是如果它们共享了一些系统级资源(比如 inode、文件描述符、磁盘 IO)。
3.1 Node Affinity 与 Taints/Tolerations
最粗暴但最有效的方法,是给每个租户分配专用的节点池。
比如,租户 A 需要高性能计算,我们给他一群 GPU 节点;租户 B 需要稳定运行 Web 服务,给他一群普通 CPU 节点。
apiVersion: v1
kind: Node
metadata:
name: node-gpu-01
labels:
tenant: alpha
gpu-type: nvidia-a100
spec:
taints:
- key: tenant-alpha-only
effect: NoSchedule
然后,在租户 A 的 Deployment 里配置容忍度:
apiVersion: apps/v1
kind: Deployment
metadata:
name: tenant-a-app
namespace: tenant-a
spec:
template:
metadata:
labels:
app: tenant-a-app
spec:
tolerations:
- key: tenant-alpha-only
effect: NoSchedule
nodeSelector:
tenant: alpha
这样,tenant-a-app 的 Pod 就只能调度到标有 tenant: alpha 的节点上,而且这些节点被 taint 禁止了其他租户的 Pod 进入。这就实现了物理层面的隔离。
但是,物理隔离有成本。如果租户 A 的流量波动大,节点闲置时很浪费。这时候可以考虑用 kube-scheduler 的扩展插件,或者使用 Volcano 这种批处理调度器,根据资源使用率动态调整,但这比较复杂,不是所有公司都有能力维护。
3.2 共享节点下的 QoS 保障
如果必须共享节点,那就要靠 QoS(Quality of Service)等级了。Kubernetes 根据 Pod 的 resource requests 和 limits 是否一致,把 Pod 分成三类:
- Guaranteed:requests == limits。这类 Pod 最可靠,不会被轻易驱逐。
- Burstable:requests < limits。这类 Pod 可以突发使用更多资源,但保障较低。
- BestEffort:没设 requests 也没设 limits。这类 Pod 最便宜,但也最脆弱,节点压力大时第一个被 OOM Kill。
对于多租户场景,我建议:关键业务必须设为 Guaranteed。
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
cpu: "2"
memory: 4Gi
这样,当集群资源紧张时,调度器会优先驱逐 BestEffort 和 Burstable 的 Pod,保护 Guaranteed 的 Pod。如果你希望租户 A 的业务优先于租户 B,可以结合 Pod Priority and Preemption 机制,给租户 A 的 Pod 设置更高的优先级。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: tenant-a-high-priority
value: 1000000
globalDefault: false
description: "High priority for tenant A"
---
apiVersion: v1
kind: Pod
metadata:
name: tenant-a-critical-pod
namespace: tenant-a
spec:
priorityClassName: tenant-a-high-priority
# ...
这样,当资源不足时,低优先级的 Pod 会被抢占,给高优先级的 Pod 腾出空间。
四、 存储隔离:数据不串门
存储隔离往往被忽视。如果租户 A 和租户 B 共享同一个 PVC(PersistentVolumeClaim),或者挂载了同一个 HostPath,那数据泄露的风险就大了。
4.1 StorageClass 与动态供给
最好每个租户使用独立的 StorageClass,或者至少通过 PVC 绑定到不同的 PV。但更高级的做法是使用 StoragePolicy 或者在 CSI(Container Storage Interface)驱动层面做权限隔离。
比如,使用 NetApp ONTAP CSI 驱动,可以为不同的 Namespace 分配不同的卷,并且通过 LUN 映射实现底层存储的隔离。
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: tenant-a-snapshot-class
driver: netapp.appdata
deletionPolicy: Delete
parameters:
subprotocol: "ontap-nas"
storagePodSecurityContextLabel: "true"
snapshotPrefix: "tenant-a"
这样,快照的名字前缀、存储路径都可以带上租户标识,便于管理和审计。
4.2 Secret 和 ConfigMap 的隔离
前面 RBAC 里提到了 Secret 和 ConfigMap 的权限控制。但要注意,Secret 是明文存储在 ETCD 里的(除非你启用了加密)。所以,如果租户 A 能访问 ETCD,他们就能看到租户 B 的 Secret。
解决办法:
- 启用 ETCD 加密:在 Kubernetes API Server 的配置里开启加密提供者(EncryptionConfiguration)。
- 外部密钥管理:使用 HashiCorp Vault 或 AWS Secrets Manager,让 Kubernetes 只存引用,不存实际值。这样,即使 ETCD 被泄露,密钥也不会暴露。
apiVersion: v1
kind: Secret
metadata:
name: my-secret
namespace: tenant-a
type: Opaque
data:
password: bXlwYXNzd29yZA==
stringData:
config.json: |
{
"db_host": "db.internal",
"db_pass": "password123"
}
如果用外部密钥管理,配置可能长这样:
apiVersion: v1
kind: Secret
metadata:
name: vault-secret
namespace: tenant-a
type: Opaque
data:
# 这里只存一个 token 或者路径,实际密钥由 Sidecar 注入
vault-path: "secret/data/tenant-a/db"
五、 审计与监控:看不见的眼睛
最后,也是最容易被忽略的:你怎么知道隔离是否生效?你怎么知道有没有人在偷偷越权?
5.1 审计日志(Audit Log)
Kubernetes 自带审计功能。你可以配置 Audit Policy,记录所有对 API Server 的请求。
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["pods", "secrets", "configmaps"]
namespaces: ["tenant-a", "tenant-b"]
- level: Metadata
resources:
- group: ""
resources: ["pods", "secrets"]
- level: None
resources:
- group: ""
resources: ["nodes", "persistentvolumes"]
这段策略的意思是:记录 tenant-a 和 tenant-b 里对 Pod、Secret、ConfigMap 的完整请求和响应;记录其他 Namespace 对 Pod 和 Secret 的元数据(谁、什么时候、干了什么);对 Node 和 PV 的操作不记录(减少日志量)。
这些日志最好实时同步到 ELK(Elasticsearch, Logstash, Kibana)或者 Loki 这种日志平台,设置告警。比如,如果某个用户在 tenant-a 里突然执行了大量的 delete 操作,或者尝试访问 tenant-b 的 Secret,立刻报警。
5.2 监控告警
用 Prometheus + Grafana 监控集群。除了监控 CPU、内存,还要监控隔离指标:
- 每个 Namespace 的资源使用率 vs 配额使用率。
- 每个 Namespace 的网络流量出入。
- 异常的 Pod 启动/停止频率。
如果一个 Namespace 的 CPU 使用率突然降到 0,但配额还没用完,可能是有人删除了所有 Pod,或者 Pod 被调度到了其他节点(说明节点池隔离失效)。
六、 总结:隔离是一场持续的博弈
写到这里,你可能觉得:天哪,这么多层!又要配配额,又要搞网络策略,又要管 RBAC,还要监控审计……没错,Kubernetes 的多租户隔离确实不轻松。它不像 Docker Swarm 或者早期的虚拟机那样,开箱即用就万事大吉。Kubernetes 给你的是一把锋利的瑞士军刀,但怎么用,全看你自己的功夫。
我见过太多案例,开头信心满满地创建了 Namespace,配了几个 ResourceQuota,就觉得“搞定了”。结果半年后,因为一个配置错误的 ClusterRole,某个普通用户拿到了集群管理员权限,删掉了生产环境的 Deployment。或者,因为网络策略没放开,租户 A 的应用连不上租户 B 的数据库,排查了三天三夜,最后发现是 NetworkPolicy 默认拒绝了所有流量,而开发文档里没写这条规则。
所以,我的建议是:
- 从小事做起,但原则不能妥协:每个 Namespace 必须有 ResourceQuota 和 LimitRange。没有这两个,就别往集群里扔工作负载。
- 网络策略默认拒绝: