前言
在生产环境中,通常需要通过配置资源配额(Resource Quota)来限制一个命名空间(namespace)能使用的资源量。在资源紧张的情况下,常常需要调整工作负载(workload)的请求值(requests)和限制值(limits),以确保工作负载能够顺利部署。本文将从Kubernetes源码的角度,简要分析Kubernetes如何计算Pod中的限制值(limits)对资源配额(Resource Quota)限制的影响。
源代码地址
k8s版本 V1.29
kubernetes/pkg/api/v1/resource/helpers.go at master · kubernetes/kubernetes · GitHub
源码解析
container分类
这里以Pod limits为例,计算给定Pod的资源限制(limits),包括普通容器和初始化容器的资源限制。
这里实际上把容器分成了3类
- 普通容器
- 初始化容器
- 可重启初始化容器(RestartPolicy不为空,且RestartPolicy=Always)
- 其他情况初始化容器
在1.29版本以前
initcontainer RestartPolicy只能为空默认为Always,且无法设置
在1.29版本以及之后
集群启用了 SidecarContainers
特性门控 (该特性自 Kubernetes v1.29 起默认启用),你可以为 Pod 的 initContainers
字段中列出的容器指定 restartPolicy
。
启动以上参数之后,RestartPolicy可以在container级别设置,此时initcontainer的RestartPolicy就可能为其他值,如果restartPolicy
设置为always,则此初始化容器为特殊情况的边车容器,在执行完初始化之后不会退出,会随主容器一起运行
参考链接: 边车容器 | Kubernetes
源码
// PodLimits 计算根据提供的PodResourcesOptions选项计算Pod的资源限制(limits)。
// 如果PodResourcesOptions为nil,则返回的限制包含任何非零限制的Pod开销。
// 这个计算是API的一部分,必须作为API更改来审查。
func PodLimits(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList {
// 尝试重用传递的maps,如果没有传递则分配新的
limits := reuseOrClearResourceList(opts.Reuse)
// 遍历Pod中的每个容器
for _, container := range pod.Spec.Containers {
// 如果提供了ContainerFn函数,则调用它处理容器资源限制
if opts.ContainerFn != nil {
opts.ContainerFn(container.Resources.Limits, podutil.Containers)
}
// 将容器的资源限制添加到总的limits中
addResourceList(limits, container.Resources.Limits)
}
restartableInitContainerLimits := v1.ResourceList{}
initContainerLimits := v1.ResourceList{}
// 初始化容器定义了任何资源的最小值
//
// 假设 `InitContainerUse(i)` 是第i个初始化容器初始化时的资源需求,
// 则 `InitContainerUse(i) = sum(重启初始化容器的资源,索引 < i) + 第i个初始化容器的资源`。
//
// 详细信息见 https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/753-sidecar-containers#exposing-pod-resource-requirements
for _, container := range pod.Spec.InitContainers {
containerLimits := container.Resources.Limits
// 检查初始化容器是否标记为可重启的初始化容器,实际上目前initcontainer必须设置为可重启
if container.RestartPolicy != nil && *container.RestartPolicy == v1.ContainerRestartPolicyAlways {
// 将容器限制添加到总的limits中
addResourceList(limits, containerLimits)
// 跟踪累积的重启初始化容器资源
addResourceList(restartableInitContainerLimits, containerLimits)
containerLimits = restartableInitContainerLimits
} else {
// 创建一个临时的资源列表,包含当前初始化容器和累积的重启初始化容器资源
tmp := v1.ResourceList{}
addResourceList(tmp, containerLimits)
addResourceList(tmp, restartableInitContainerLimits)
containerLimits = tmp
}
// 如果提供了ContainerFn函数,则调用它处理初始化容器资源限制
if opts.ContainerFn != nil {
opts.ContainerFn(containerLimits, podutil.InitContainers)
}
// 取initContainerLimits和containerLimits的最大值
maxResourceList(initContainerLimits, containerLimits)
}
// 取limits和initContainerLimits的最大值
maxResourceList(limits, initContainerLimits)
// 如果不排除开销,并且Pod的Spec中有开销,则添加开销到非零限制中
if !opts.ExcludeOverhead && pod.Spec.Overhead != nil {
for name, quantity := range pod.Spec.Overhead {
if value, ok := limits[name]; ok && !value.IsZero() {
value.Add(quantity)
limits[name] = value
}
}
}
// 返回计算的资源限制
return limits
}
重要概念
在上述代码中,有以下几点需要理解
- 需要取一个满足任何情况的最大limits计算
- 如果有普通容器和initcontainer,则先运行完initcontainer(其他情况初始化容器+可重启初始化容器(RestartPolicy不为空,RestartPolicy: Always)),再同时运行普通容器+可重启初始化容器(RestartPolicy不为空,RestartPolicy: Always)
Pod limits计算规则
理解了以上几点,那么Pod limits的计算规则就很容易得出了:
-
普通容器资源需求:计算Pod中所有普通容器的资源限制总和 + 可重启初始化容器(RestartPolicy不为空,RestartPolicy: Always)
-
初始化容器资源需求:其他情况启初始化容器的资源需求总和+可重启初始化容器(RestartPolicy不为空,RestartPolicy: Always)
-
计算公式:Pod资源总需求 = max(普通容器资源需求, 初始化容器资源需求)
典型情况
假设你的Pod包含以下容器:
- 一个可重启的初始化容器,资源限制为:CPU 100m,内存 100Mi
- 一个普通的初始化容器,资源限制为:CPU 200m,内存 200Mi
- 一个主容器,资源限制为:CPU 300m,内存 300Mi
根据代码逻辑:
- 主容器:将CPU 300m和内存300Mi添加到
limits
中。 - 可重启初始化容器:
- 添加到
limits
中:limits
现在为CPU 400m,内存400Mi。 - 累加到
restartableInitContainerLimits
中:restartableInitContainerLimits
为CPU 100m,内存100Mi。
- 添加到
- 普通初始化容器:
- 创建临时资源列表:临时资源为CPU 300m(200m + 100m),内存300Mi(200Mi + 100Mi)。
- 取最大值:
initContainerLimits
现在为CPU 300m,内存300Mi。
- 比较
limits
和initContainerLimits
:limits
取最大值后保持为CPU 400m,内存400Mi。
最终,Pod的资源限制(limits)为:
- CPU:400m
- 内存:400Mi