
一、资源超卖的本质和存在的风险界限
在Kubernetes集群中,节点资源超卖(Overcommit)是指节点上所有的Pod请求之和大于节点可以分配给它们的资源的情况。超卖的主要目的是为了提高硬件利用率——大多数业务Pod所设定的请求值都高于其实际使用的资源量,因此存在很多未被利用的资源。
但是超卖有一个明确的物理界限。requests是调度的基础,limits是运行时的限制条件。如果只有requests被超卖了,那么调度器会允许更多的Pod进入节点,但是每个Pod的limits仍然可以单独对其资源使用进行限制;而如果both requests和limits都被突破了的话,则会出现CPU Throttling(CPU节流)或者内存OOM(Out Of Memory)的情况。
超卖策略的第一条就是:内存超卖的比例要小于CPU超卖的比例,并且都要有一个硬性限制。
二、CPU超卖和限制:CFS配额以及优先级划分
CPU可以被视为一种可以被压缩的资源,在超卖之后所面临的主要挑战就是如何公平地分配这些有限的资源,而不是简单地杀掉某个进程。
2.1 CFS配额和限速
Kubernetes使用Linux CFS(完全公平调度器)来进行带宽控制来限制CPU的使用量,在cgroup中把每个容器的CPU配额写成cpu.cfs_quota_us的形式。当整个节点上的所有容器总的CPU使用量大于物理核心数的时候,CFS会根据权重(即cpu.shares,它是由请求值转换而来的)来分配时间片。
存在的问题是:CFS中cpu.cfs_period_us默认为100ms,较短的时间间隔使得突发性负载经常引起节流。解决办法有:
- 增大CFS周期:通过kubelet的
--cpu-cfs-period参数调整,减少Throttling频率,代价是调度延迟略有上升。 - 启用CPU Burst:Kubernetes 1.22+支持
cpu.cfs_burst_us,允许Pod在空闲时积累配额,应对短时突发。 - 调整CPU Manager策略:对延迟敏感型Pod使用
Static策略绑定独占核,其余Pod使用None策略共享余量。
2.2 压制策略:由QoS至cgroup v2
Kubernetes的QoS等级(Guaranteed > Burstable > BestEffort)会决定在内存溢出时被驱逐的顺序,但是它对CPU限制的作用很小。起作用的是cgroup v2中的CPU权重机制:
cgroup v2使用cpu.weight来代替v1中的cpu.shares,取值范围是1到10000之间,更加线性化。当节点上的CPU资源被占满的时候,内核会根据权重比例来分配时间片,高优先级的Pod会得到更多的CPU时间。另外还可以用cpu.max来限制总的CPU使用量,分为软限制(通过调整权重实现)和硬限制(设置上限)。
三、内存超卖和压制:不可压缩资源最后的防线
内存超卖的风险比CPU大得多,Kubelet用eviction-hard、eviction-soft两个参数来设置驱逐阈值,在节点内存占用达到该值的时候就会把Pod踢出去。
3.1 驱逐机制和信号的选择
重要的驱逐信号是:
| 信号 | 含义 | 建议阈值 |
|---|---|---|
memory.available | 节点可用内存 | hard: 100Mi, soft: 200Mi |
nodefs.available | 节点文件系统可用空间 | hard: 10% |
imagefs.available | 容器镜像存储可用空间 | hard: 15% |
驱逐的顺序是根据QoS等级以及Pod实际占用的内存大小来确定的,BestEffort类型的Pod会先被驱逐出去,而Guaranteed类型的Pod则最后才会被驱逐掉。但是驱逐是一个延迟的过程——当内存压力出现的时候才开始进行驱逐,在这个过程中可能会发生OOMKill的情况。
3.2 主动抑制:Memory QoS(cgroup v2)
Kubernetes 1.22新增了内存QoS功能(需要cgroup v2),使用memory.high和memory.max来主动限制:
memory.high:设置内存使用上限(小于memory.max),当超出这个值时,内核会自动收回该cgroup所占用的内存页面,而不是等到整个系统出现内存溢出才采取行动。这就是所谓的“提前抑制”的主要方式之一。memory.max:硬性限制,超出该值就会发生OOMKill。
开启方法:在kubelet配置文件中加入--feature-gates=MemoryQoS=true参数,给Pod加上注释kubernetes.io/enforce-memory-qos: "true"。
四、调度层超卖控制:ResourceQuota和LimitRange
超卖策略不能只依靠运行时的抑制来实施,在调度的时候也要有相应的限制。
4.1 LimitRange强制限制
LimitRange用来给命名空间中的Pod设定默认的请求和限制值来避免一个Pod没有声明任何资源的情况发生:
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
spec:
limits:
- default:
cpu: "2"
memory: "2Gi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "4"
memory: "8Gi"
min:
cpu: "50m"
memory: "64Mi"
type: Container
4.2 ResourceQuota总限额
ResourceQuota用来限制整个命名空间中的总资源量,避免一个租户用光所有的资源:
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
用limits/requests的比例来限制超售的程度——上面的设置可以允许最大的2倍CPU超售以及2倍内存超售。
五、动态超卖:Descheduler和VPA形成的闭环
静态超卖比例不能应对业务的起伏变化,而动态超卖则依靠以下三部分构成一个完整的系统:
- VPA(Vertical Pod Autoscaler):根据历史数据来调整请求和限制的值,避免人工计算带来的误差。通过设置
updateMode: Recreate可以自动重启Pod以应用新的参数。 - Descheduler:定期扫描集群,驱逐低优先级Pod或资源分布不均的Pod,为调度器腾出重新平衡的机会。关键策略包括
LowNodeUtilization(将Pod从低利用率节点迁移到高利用率节点)和RemoveDuplicates(驱逐重复Pod)。 - Cluster Autoscaler:当超卖导致节点资源真正不足时,自动扩容节点;当节点长期低利用率时,自动缩容。
六、保证稳定性的最佳参数搭配
生产环境推荐参数基线:
| 参数 | 推荐值 | 说明 |
|---|---|---|
kubelet --cpu-cfs-period | 200ms | 降低Throttling频率 |
kubelet --eviction-hard | memory.available<100Mi,nodefs.available<10% | 硬驱逐阈值 |
kubelet --eviction-soft | memory.available<200Mi | 软驱逐阈值 |
kubelet --eviction-minimum-reclaim | memory.available=100Mi | 每次驱逐至少回收量 |
kubelet --system-reserved | cpu=500m,memory=1Gi | 系统进程预留 |
kubelet --kube-reserved | cpu=250m,memory=512Mi | Kubernetes组件预留 |
| 内存超卖比例上限 | 1.3x | 超过此值OOM风险陡增 |
| CPU超卖比例上限 | 2.5x | 超过此值延迟不可控 |
七、多租户环境中的超卖隔离
在多租户集群中,超卖不可以跨越不同的租户进行统一处理,需要引入:
- Pod Anti-Affinity:防止同一租户的Pod集中到同一节点。
- Topology Spread Constraints:均匀分散Pod到不同拓扑域。
- ResourceQuota per Namespace:租户级资源总量隔离。
- PriorityClass:为关键业务设置高优先级,确保超卖竞争时优先获得资源。
总结
Kubernetes集群资源超卖可以提高硬件的使用效率,但是要以稳定为前提条件。主要的做法是:用CFS配额和cgroup v2权重来限制CPU使用量,用Memory QoS以及驱逐机制来保护内存不被占用,在调度层设置LimitRange和ResourceQuota来设定超卖的上限,在动态层利用VPA和Descheduler来进行资源的分配循环控制。在生产环境中应该把内存超卖的比例控制在1.3以内、CPU超卖的比例控制在2.5以内,留出一定的系统资源作为缓冲。超卖并不是无限制地压榨硬件,而是要在提高资源利用率的同时保证应用系统的稳定性之间找到一个可以量化的平衡点。