HPA
我们可以实现通过手工执行kubectl scale命令实现Pod扩容或缩容,但是这显然不符合Kubernetes的定位目标–自动化、智能化。 Kubernetes期望可以实现通过监测Pod的使用情况,实现pod数量的自动调整,于是就产生了Horizontal Pod Autoscaler(HPA)这种控制器。
HPA可以获取每个Pod利用率,然后和HPA中定义的指标进行对比,同时计算出需要伸缩的具体值,最后实现Pod的数量的调整。其实HPA与之前的Deployment一样,也属于一种Kubernetes资源对象,它通过追踪分析RC控制的所有目标Pod的负载变化情况,来确定是否需要针对性地调整目标Pod的副本数,这是HPA的实现原理
注: 在 K8S 1.18之前,HPA 扩容是无法调整灵敏度的
- 对于缩容,由
kube-controller-manager的--horizontal-pod-autoscaler-downscale-stabilization-window参数控制缩容时间窗口,默认 5 分钟,即负载减小后至少需要等 5 分钟才会缩容。- 对于扩容,由 hpa controller 固定的算法、硬编码的常量因子来控制扩容速度,无法自定义。
以下介绍 为v1版本之后
先查看支持API版本
1 | [root@VM-30-197-centos ~]# kubectl api-versions | grep autoscal |
安装metrics-server
metrics-server可以用来收集集群中的资源使用情况,在一些k8s发行版中(例如k3s)默认安装了metrics-server
1 | [root@release-master ~]# kubectl get pod -n kube-system|grep "metrics-server" |
1 | # 安装git |

1 | # 安装metrics-server |
例子
1 | apiVersion: v1 |
压测进行测试
1 | !/usr/bin/env bash |
副本会增加到10个为止

关于HPA
当前指标值的计算方式
提前总结:每个 Pod 的指标是其中所有容器指标之和,如果计算百分比,就再除以 Pod 的 requests.
HPA 默认使用 Pod 的当前指标进行计算,以 CPU 使用率为例,其计算公式为:
1 | 「Pod 的 使用率」= 100% * 「所有 Container 的 用量之和」/「所有 Container 的 requests 之和」 |
HPA 的扩缩容算法
HPA 什么时候会扩容,这一点是很好理解的。但是 HPA 的缩容策略,会有些迷惑,下面简单分析下。
- HPA 的「目标指标」可以使用两种形式:绝对度量指标和资源利用率。
- 绝对度量指标:比如 CPU,就是指 CPU 的使用量
- 资源利用率(资源使用量/资源请求 * 100%):在 Pod 设置了资源请求时,可以使用资源利用率进行 Pod 伸缩
- HPA 的「当前指标」是一段时间内所有 Pods 的平均值,不是峰值。
HPA 的扩缩容算法为:
1 | 期望副本数 = ceil[当前副本数 * ( 当前指标 / 目标指标 )] |
从上面的参数可以看到:
- 只要「当前指标」超过了目标指标,就一定会发生扩容。
当前指标 / 目标指标要小到一定的程度,才会触发缩容。- 比如双副本的情况下,上述比值要小于等于 1/2,才会缩容到单副本。
- 三副本的情况下,上述比值的临界点是 2/3。
- 五副本时临界值是 4/5,100副本时临界值是 99/100,依此类推。
- 如果
当前指标 / 目标指标从 1 降到 0.5,副本的数量将会减半。(虽然说副本数越多,发生这么大变化的可能性就越小。)
当前副本数 / 目标指标的值越大,「当前指标」的波动对「期望副本数」的影响就越大。
为了防止扩缩容过于敏感,HPA 有几个相关参数:
- Hardcoded 参数
- HPA Loop 延时:默认 15 秒,每 15 秒钟进行一次 HPA 扫描。
- 缩容冷却时间:默认 5 分钟。
- 对于 K8s 1.18+,HPA 通过
spec.behavior提供了多种控制扩缩容行为的参数
HPA 的期望值设成多少合适
这个需要针对每个服务的具体情况,具体分析。
以最常用的按 CPU 值伸缩为例,
- 核心服务
- requests/limits 值: 建议设成相等的,保证服务质量等级为
Guaranteed- 需要注意 CPU 跟 Memory 的 limits 限制策略是不同的,CPU 是真正地限制了上限,而 Memory 是用超了就干掉容器(OOMKilled)
- k8s 一直使用 cgroups v1 (
cpu_shares/memory.limit_in_bytes)来限制 cpu/memory,但是对于Guaranteed的 Pods 而言,内存并不能完全预留,资源竞争总是有可能发生的。1.22 有 alpha 特性改用 cgroups v2,可以关注下。
- HPA: 一般来说,期望值设为 70% 到 80% 可能是比较合适的,最小副本数建议设为 2 - 5. (仅供参考)
- PodDisruptionBudget: 建议按服务的健壮性与 HPA 期望值,来设置 PDB
- requests/limits 值: 建议设成相等的,保证服务质量等级为
- 非核心服务
- requests/limits 值: 建议 requests 设为 limits 的 0.6 - 0.9 倍(仅供参考),对应的服务质量等级为
Burstable- 也就是超卖了资源,这样做主要的考量点是,很多非核心服务负载都很低,根本跑不到 limits 这么高,降低 requests 可以提高集群资源利用率,也不会损害服务稳定性。
- HPA: 因为 requests 降低了,而 HPA 是以 requests 为 100% 计算使用率的,我们可以提高 HPA 的期望值(如果使用百分比为期望值的话),比如 90%,最小副本数建议设为 2(保证可用性,仅供参考)
- PodDisruptionBudget: 保证最少副本数为 2 (保证可用性)
- requests/limits 值: 建议 requests 设为 limits 的 0.6 - 0.9 倍(仅供参考),对应的服务质量等级为
HPA 的常见问题
Pod 扩容 - 预热陷阱
预热:Java/C# 这类运行在虚拟机上的语言,第一次使用到某些功能时,往往需要初始化一些资源,例如「JIT 即时编译」。
如果代码里还应用了动态类加载之类的功能,就很可能导致微服务某些 API 第一次被调用时,响应特别慢(要动态编译 class)。
因此 Pod 在提供服务前,需要提前「预热(slow_start)」一次这些接口,将需要用到的资源提前初始化好。
在负载很高的情况下,HPA 会自动扩容。
但是如果扩容的 Pod 需要预热,就可能会遇到「预热陷阱」。
在有大量用户访问的时候,不论使用何种负载均衡策略,只要请求被转发到新建的 Pod 上,这个请求就会「卡住」。
如果请求速度太快,Pod 启动的瞬间「卡住」的请求就越多,这将会导致新建 Pod 因为压力过大而垮掉。
然后 Pod 一重启就被压垮,进入 CrashLoopBackoff 循环。
如果是在使用多线程做负载测试时,效果更明显:50 个线程在不间断地请求,
别的 Pod 响应时间是「毫秒级」,而新建的 Pod 的首次响应是「秒级」。几乎是一瞬间,50 个线程就会全部陷在新建的 Pod 这里。
而新建的 Pod 在启动的瞬间可能特别脆弱,瞬间的 50 个并发请求就可以将它压垮。
然后 Pod 一重启就被压垮,进入 CrashLoopBackoff 循环。
解决方法:
可以在「应用层面」解决:
- 在启动探针 API 的后端控制器里面,依次调用所有需要预热的接口或者其他方式,提前初始化好所有资源。
- 启动探针的控制器中,可以通过
localhost回环地址调用它自身的接口。
- 启动探针的控制器中,可以通过
- 使用「AOT 预编译」技术:预热,通常都是因为「JIT 即时编译」导致的问题,在需要用到时它才编译。而 AOT 是预先编译,在使用前完成编译,因此 AOT 能解决预热的问题。
HPA 扩缩容过于敏感,导致 Pod 数量震荡
通常来讲,K8s 上绝大部分负载都应该选择使用 CPU 进行扩缩容。因为 CPU 通常能很好的反映服务的负载情况
但是有些服务会存在其他影响 CPU 使用率的因素,导致使用 CPU 扩缩容变得不那么可靠,比如:
- 有些 Java 服务堆内存设得很大,GC pause 也设得比较长,因此内存 GC 会造成 CPU 间歇性飙升,CPU 监控会有大量的尖峰。
- 有些服务有定时任务,定时任务一运行 CPU 就涨,但是这跟服务的 QPS 是无关的
- 有些服务可能一运行 CPU 就会立即处于一个高位状态,它可能希望使用别的业务侧指标来进行扩容,而不是 CPU.
因为上述问题存在,使用 CPU 扩缩容,就可能会造成服务频繁的扩容然后缩容,或者无限扩容。
而有些服务(如我们的「推荐服务」),对「扩容」和「缩容」都是比较敏感的,每次扩缩都会造成服务可用率抖动。
对这类服务而言,HPA 有这几种调整策略:
- 对 kubernetes 1.18+,可以直接使用 HPA 的
behavior.scaleDown和behavior.scaleUp两个参数,控制每次扩缩容的最多 pod 数量或者比例 - 选择使用 QPS 等相对比较平滑,没有 GC 这类干扰的指标来进行扩缩容,这可以参考下面的
HPA基于Prometheus自定义指标。
存在延迟
由于技术限制,HorizontalPodAutoscaler 控制器在确定是否保留某些 CPU 指标时无法准确确定 Pod 首次就绪的时间。 相反,如果 Pod 未准备好并在其启动后
的一个可配置的短时间窗口内转换为准备好,它会认为 Pod “尚未准备好”。 该值使用 --horizontal-pod-autoscaler-initial-readiness-delay 标志配置,
默认值为 30 秒。 一旦 Pod 准备就绪,如果它发生在自启动后较长的、可配置的时间内,它就会认为任何向准备就绪的转换都是第一个。 该值由 -horizontal-
pod-autoscaler-cpu-initialization-period 标志配置,默认为 5 分钟。
所以,K8S集群无法实时根据负载情况动态扩缩容,存在一定的延时(默认30秒)。
HPA基于Prometheus自定义指标
从最初的 v1 版本 HPA 只支持 CPU、内存利用率的伸缩,到后来的自定义指标、聚合层 API 的支持,到了 v1.18 版本又加入了配置伸缩行为的支持,HPA 也越来越好用、可靠。
依靠 CPU 或者内存指标的扩容并非使用所有系统,看起来也没那么可靠。对大部分的 web 后端系统来说,基于 RPS(每秒请求数)的弹性伸缩来处理突发的流量则会更加靠谱。
Prometheus 也是当下流行开源监控系统,通过 Prometheus 可以获取到系统的实时流量负载指标
实现原理
Kubernetes 提供了 Custom Metrics API 与 External Metrics API 来对 HPA 的指标进行扩展,让用户能够根据实际需求进行自定义。
prometheus-adapter 对这两种 API 都有支持,通常使用 Custom Metrics API 就够了,本文也主要针对此 API 来实现使用自定义指标进行弹性伸缩。
前提条件
- 部署有 Prometheus 并做了相应的自定义指标采集。
- 已安装 helm 。
业务暴露监控指标
这里以一个简单的 golang 业务程序为例,暴露 HTTP 请求的监控指标:
1 | package main |
该示例程序暴露了 httpserver_requests_total 指标,记录 HTTP 的请求,通过这个指标可以计算出该业务程序的 QPS 值。
部署业务程序
将我们的业务程序进行容器化并部署到集群,比如使用 Deployment 部署:
1 | apiVersion: apps/v1 |
Prometheus 采集业务监控
业务部署好了,我们需要让我们的 Promtheus 去采集业务暴露的监控指标。
方式一: 配置 Promtheus 采集规则
在 Promtheus 的采集规则配置文件添加采集规则:
1 | - job_name: httpserver |
方式二: 配置 ServiceMonitor
若已安装 prometheus-operator,则可通过创建 ServiceMonitor 的 CRD 对象配置 Prometheus。示例如下:
1 | apiVersion: monitoring.coreos.com/v1 |
安装 prometheus-adapter
我们使用 helm 安装 prometheus-adapter,安装前最重要的是确定并配置自定义指标,按照前面的示例,我们业务中使用 httpserver_requests_total 这个指标来记录 HTTP 请求,那么我们可以通过类似下面的 PromQL 计算出每个业务 Pod 的 QPS 监控:
1 | sum(rate(http_requests_total[2m])) by (pod) |
我们需要将其转换为 prometheus-adapter 的配置,准备一个 values.yaml:
1 | rules: |
执行 helm 命令进行安装:
1 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts |
利用rancher配置prometheus-adapter
如果是利用rancher安装的Monitoring,可以直接修改ConfigMap: rancher-monitoring-prometheus-adapter,如下(配置完后,重启Deployment: rancher-monitoring-prometheus-adapter)

测试是否安装正确
如果安装正确,是可以看到 Custom Metrics API 返回了我们配置的 QPS 相关指标:
1 | $ kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 |
也能看到业务 Pod 的 QPS 值:
1 | $ kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/namespaces/${命名空间,这里是httpserver}/pods/*/httpserver_requests_qps |
上面示例 QPS 为
500m,表示 QPS 值为 0.5
测试 HPA
假如我们设置每个业务 Pod 的平均 QPS 达到 50,就触发扩容,最小副本为 1 个,最大副本为1000,HPA 可以这么配置:
1 | apiVersion: autoscaling/v2beta2 |
然后对业务进行压测,观察是否扩容:
1 | $ kubectl get hpa |
扩容正常则说明已经实现 HPA 基于业务自定义指标进行弹性伸缩。
参考:
http://t.zoukankan.com/dudu-p-12197646.html
https://atbug.com/kubernetes-pod-autoscale-on-prometheus-metrics/
https://github.com/addozhang/hpa-on-prometheus-metrics
https://www.cnblogs.com/dudu/p/12217354.html
https://imroc.cc/k8s/best-practice/custom-metrics-hpa/
PodDistruptionBuget
在我们通过 kubectl drain 将某个节点上的容器驱逐走的时候,
kubernetes 会依据 Pod 的「PodDistruptionBuget」来进行 Pod 的驱逐。
如果不设置任何明确的 PodDistruptionBuget,Pod 将会被直接杀死,然后在别的节点重新调度,这可能导致服务中断!
PDB 是一个单独的 CR 自定义资源,示例如下:
1 | apiVersion: policy/v1beta1 |
如果在进行节点维护时(kubectl drain),Pod 不满足 PDB,drain 将会失败,示例:
1 | kubectl drain node-205 --ignore-daemonsets --delete-local-data |
上面的示例中,podinfo 一共有两个副本,都运行在 node-205 上面。我给它设置了干扰预算 PDB minAvailable: 1。
然后使用 kubectl drain 驱逐 Pod 时,其中一个 Pod 被立即驱逐走了,而另一个 Pod 大概在 15 秒内一直驱逐失败。
因为第一个 Pod 还没有在新的节点上启动完成,它不满足干扰预算 PDB minAvailable: 1 这个条件。
大约 15 秒后,最先被驱逐走的 Pod 在新节点上启动完成了,另一个 Pod 满足了 PDB 所以终于也被驱逐了。这才完成了一个节点的 drain 操作。
ClusterAutoscaler 等集群节点伸缩组件,在缩容节点时也会考虑 PodDisruptionBudget. 如果你的集群使用了 ClusterAutoscaler 等动态扩缩容节点的组件,强烈建议设置为所有服务设置 PodDisruptionBudget.
在 PDB 中使用百分比的注意事项
在使用百分比时,计算出的实例数都会被向上取整,这会造成两个现象:
- 如果使用
minAvailable,实例数较少的情况下,可能会导致 ALLOWED DISRUPTIONS 为 0,所有实例都无法被驱逐了。 - 如果使用
maxUnavailable,因为是向上取整,ALLOWED DISRUPTIONS 的值一定不会低于 1,至少有 1 个实例可以被驱逐。
因此从「便于驱逐」的角度看,如果你的服务至少有 2-3 个实例,建议在 PDB 中使用百分比配置 maxUnavailable,而不是 minAvailable.
相对的从「确保服务稳定性」的角度看,我们则应该使用 minAvailable,确保至少有 1 个实例可用。
最佳实践 Deployment + HPA + PodDisruptionBudget
一般而言,一个无状态服务的每个版本,都应该包含如下三个资源:
- Deployment: 管理服务自身的 Pods 嘛
- HPA: 负责 Pods 的扩缩容,通常使用 CPU 指标进行扩缩容
- PodDisruptionBudget(PDB): 建议按照 HPA 的目标值,来设置 PDB.
比如 HPA CPU 目标值为 60%,就可以考虑设置 PDB
minAvailable=65%,保证至少有 65% 的 Pod 可用。这样理论上极限情况下 QPS 均摊到剩下 65% 的 Pods 上也不会造成雪崩(这里假设 QPS 和 CPU 是完全的线性关系)