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
2
3
4
5
6
7
8
9
[root@VM-30-197-centos ~]# kubectl api-versions | grep autoscal
autoscaling/v1
autoscaling/v2
autoscaling/v2beta1
autoscaling/v2beta2

autoscaling/v1 只支持基于CPU指标的缩放
autoscaling/v2beta1 支持Resource Metrics(资源指标,如pod的CPU)和Custom Metrics(自定义指标)的缩放;
autoscaling/v2beta2 支持Resource Metrics(资源指标,如pod的CPU)和Custom Metrics(自定义指标)和ExternalMetrics(额外指标)的缩放。

安装metrics-server


metrics-server可以用来收集集群中的资源使用情况,在一些k8s发行版中(例如k3s)默认安装了metrics-server

1
2
[root@release-master ~]# kubectl get pod -n kube-system|grep "metrics-server"
metrics-server-86cbb8457f-lddhk 1/1 Running 0 36d
1
2
3
4
5
6
7
8
9
10
11
12
13
# 安装git
[root@master ~]# yum install git -y
# 获取metrics-server, 注意使用的版本
[root@master ~]# git clone -b v0.3.6 https://github.com/kubernetes-incubator/metrics-server
# 修改deployment, 注意修改的是镜像和初始化参数
[root@master ~]# cd /root/metrics-server/deploy/1.8+/
[root@master 1.8+]# vim metrics-server-deployment.yaml
按图中添加下面选项
hostNetwork: true
image: registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server-amd64:v0.3.6
args:
- --kubelet-insecure-tls
- --kubelet-preferred-address-types=InternalIP,Hostname,InternalDNS,ExternalDNS,ExternalIP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 安装metrics-server
[root@master 1.8+]# kubectl apply -f ./

# 查看pod运行情况
[root@release-master ~]# kubectl get pod -n kube-system|grep "metrics-server"
metrics-server-86cbb8457f-lddhk 1/1 Running 0 36d

# 使用kubectl top node 查看资源使用情况
[root@release-master ~]# kubectl top node
W0413 15:59:09.838958 27048 top_node.go:119] Using json format to get metrics. Next release will switch to protocol-buffers, switch early by passing --use-protocol-buffers flag
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
release-master 457m 11% 5268Mi 67%
release-worker01 361m 9% 5061Mi 64%
vm-36-68-centos 314m 7% 4944Mi 63%
...
# 至此,metrics-server安装完成

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
apiVersion: v1
kind: Namespace
metadata:
name: dev
---
# 官方建议在配置了hpa的情况下使用默认的replicas(1)
# https://kubernetes.io/zh-cn/docs/tasks/run-application/horizontal-pod-autoscale/#migrating-deployments-and-statefulsets-to-horizontal-autoscaling
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: dev
labels:
app: nginx
spec:
template:
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
# 这里将资源配置的小一些,以方便测试
resources:
requests:
cpu: 5m
limits:
cpu: 5m
restartPolicy: Always
selector:
matchLabels:
app: nginx
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: dev
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 31980
type: NodePort
---
# 配置hpa(autoscaling/v2版本在1.23+版本之后GA, 如果低于此版本, 使用v1或者v2beta2)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx
namespace: dev
labels:
app: nginx
spec:
# 指定要控制的deployment信息
scaleTargetRef:
apiVersion: apps/v1
name: nginx
kind: Deployment
# 允许最大副本数
maxReplicas: 10
# 允许最小副本数
minReplicas: 1
metrics:
# # 指的是当前伸缩对象下的container的cpu和memory指标,只支持Utilization和AverageValue类型的阈值
# - type: ContainerResource
# containerResource:
# container: C1
# name: memory
# target:
# type: AverageValue
# averageValue: 300Mi
# # 指的是伸缩对象Pods的指标,数据需要第三方的adapter提供,只允许AverageValue类型的阈值
# - type: Pods
# pods:
# metric:
# name: Pods_second
# selector:
# - matchExpressions:
# - key: zone
# operator: In
# values:
# - foo
# - bar
# target:
# type: AverageValue
# averageValue: 1k # 当指标的平均值或资源的平均利用率超过这个的时候,会进行扩容
# # 指的是k8s外部的指标,数据同样需要第三方的adapter提供。只支持Value和AverageValue类型的阈值
# - type: External
# external:
# metric:
# name: External_second
# selector:
# matchLabels:
# app: nginx
# target:
# type: Value
# value: "20"
# # 指的是指定k8s内部对象的指标,数据需要第三方adapter提供,例如Ingress。只支持Value和AverageValue类型的阈值
# - type: Object
# object:
# describedObject:
# apiVersion: networking.k8s.io/v1beta1
# kind: Ingress
# name: main-route
# metric:
# name: ingress_test
# target:
# type: Value
# value: 2k
# 指的是当前伸缩对象下的pod的指标。target.type只支持Utilization和AverageValue类型的阈值
- type: Resource
resource:
name: cpu
target:
type: Utilization # 利用率
averageUtilization: 70 # 期望的 CPU 平均值,当整体的资源利用率超过这个百分比的时候,会进行扩容;HPA 控制器会维持扩缩目标中的 Pods 的平均资源利用率在 60%
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 默认为 0(当指标显示目标应该扩容时,目标会立即扩容);如果配置为n,意味着需要先等待 n秒 的时间窗口,如果在这段时间内负载降下来了就不再扩容
policies:
- periodSeconds: 180 # 每 3 分钟最多扩容 5% 的 Pods
type: Percent
value: 5
- periodSeconds: 60 # 每分钟最多扩容 1 个 Pod,扩的慢一点主要是为了一个个地预热,避免一次扩容太多未预热的 Pods 导致服务可用率剧烈抖动
type: Pods
value: 1
selectPolicy: Min # Min:选择副本数变化最小的策略。 Disabled:将完全禁用该方向的扩缩。 Max:选择副本数变化最大的策略。
# 以下的一切配置,都是为了更平滑地缩容
scaleDown:
stabilizationWindowSeconds: 600 # 当负载降下来时,会等待 600s (10 分钟) 再缩容,避免过快缩容。
policies:
- type: Percent # 每 3 mins 最多缩容 `ceil[当前副本数 * 5%]` 个 pod(20 个 pod 以内,一次只缩容 1 个 pod)
value: 5
periodSeconds: 180
- type: Pods # 每 1 mins 最多缩容 1 个 pod
value: 1
periodSeconds: 60
selectPolicy: Min # 上面的 policies 列表,只生效其中副本数变化最小作为缩容限制(保证平滑缩容)

压测进行测试

1
2
3
4
#!/usr/bin/env bash
while [ 1==1 ]; do
curl -k xxx:31980
done

副本会增加到10个为止

关于HPA

当前指标值的计算方式

提前总结:每个 Pod 的指标是其中所有容器指标之和,如果计算百分比,就再除以 Pod 的 requests.

HPA 默认使用 Pod 的当前指标进行计算,以 CPU 使用率为例,其计算公式为:

1
「Pod 的 CPU 使用率」= 100% * 「所有 Container 的 CPU 用量之和」/「所有 Container 的 CPU requests 之和」

HPA 的扩缩容算法

HPA 什么时候会扩容,这一点是很好理解的。但是 HPA 的缩容策略,会有些迷惑,下面简单分析下。

  1. HPA 的「目标指标」可以使用两种形式:绝对度量指标和资源利用率。
    • 绝对度量指标:比如 CPU,就是指 CPU 的使用量
    • 资源利用率(资源使用量/资源请求 * 100%):在 Pod 设置了资源请求时,可以使用资源利用率进行 Pod 伸缩
  2. HPA 的「当前指标」是一段时间内所有 Pods 的平均值,不是峰值。

HPA 的扩缩容算法为:

1
期望副本数 = ceil[当前副本数 * ( 当前指标 / 目标指标 )]

从上面的参数可以看到:

  1. 只要「当前指标」超过了目标指标,就一定会发生扩容。
  2. 当前指标 / 目标指标要小到一定的程度,才会触发缩容。
    1. 比如双副本的情况下,上述比值要小于等于 1/2,才会缩容到单副本。
    2. 三副本的情况下,上述比值的临界点是 2/3。
    3. 五副本时临界值是 4/5,100副本时临界值是 99/100,依此类推。
    4. 如果 当前指标 / 目标指标 从 1 降到 0.5,副本的数量将会减半。(虽然说副本数越多,发生这么大变化的可能性就越小。)
  3. 当前副本数 / 目标指标的值越大,「当前指标」的波动对「期望副本数」的影响就越大。

为了防止扩缩容过于敏感,HPA 有几个相关参数:

  1. Hardcoded 参数
    1. HPA Loop 延时:默认 15 秒,每 15 秒钟进行一次 HPA 扫描。
    2. 缩容冷却时间:默认 5 分钟。
  2. 对于 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 的 0.6 - 0.9 倍(仅供参考),对应的服务质量等级为 Burstable
      • 也就是超卖了资源,这样做主要的考量点是,很多非核心服务负载都很低,根本跑不到 limits 这么高,降低 requests 可以提高集群资源利用率,也不会损害服务稳定性。
    • HPA: 因为 requests 降低了,而 HPA 是以 requests 为 100% 计算使用率的,我们可以提高 HPA 的期望值(如果使用百分比为期望值的话),比如 90%,最小副本数建议设为 2(保证可用性,仅供参考)
    • PodDisruptionBudget: 保证最少副本数为 2 (保证可用性)

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 循环。

解决方法

可以在「应用层面」解决:

  1. 在启动探针 API 的后端控制器里面,依次调用所有需要预热的接口或者其他方式,提前初始化好所有资源。
    1. 启动探针的控制器中,可以通过 localhost 回环地址调用它自身的接口。
  2. 使用「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.scaleDownbehavior.scaleUp 两个参数,控制每次扩缩容的最多 pod 数量或者比例
  • 选择使用 QPS 等相对比较平滑,没有 GC 这类干扰的指标来进行扩缩容,这可以参考下面的HPA基于Prometheus自定义指标

HPA基于Prometheus自定义指标

从最初的 v1 版本 HPA 只支持 CPU、内存利用率的伸缩,到后来的自定义指标、聚合层 API 的支持,到了 v1.18 版本又加入了配置伸缩行为的支持,HPA 也越来越好用、可靠。

依靠 CPU 或者内存指标的扩容并非使用所有系统,看起来也没那么可靠。对大部分的 web 后端系统来说,基于 RPS(每秒请求数)的弹性伸缩来处理突发的流量则会更加靠谱。

Prometheus 也是当下流行开源监控系统,通过 Prometheus 可以获取到系统的实时流量负载指标

实现原理

Kubernetes 提供了 Custom Metrics APIExternal Metrics API 来对 HPA 的指标进行扩展,让用户能够根据实际需求进行自定义。

prometheus-adapter 对这两种 API 都有支持,通常使用 Custom Metrics API 就够了,本文也主要针对此 API 来实现使用自定义指标进行弹性伸缩。

前提条件

  • 部署有 Prometheus 并做了相应的自定义指标采集。
  • 已安装 helm

业务暴露监控指标

这里以一个简单的 golang 业务程序为例,暴露 HTTP 请求的监控指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"strconv"
)

var (
HTTPRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "httpserver_requests_total",
Help: "Number of the http requests received since the server started",
},
[]string{"status"},
)
)

func init() {
prometheus.MustRegister(HTTPRequests)
}

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
code := 200
switch path {
case "/test":
w.WriteHeader(200)
w.Write([]byte("OK"))
case "/metrics":
promhttp.Handler().ServeHTTP(w, r)
default:
w.WriteHeader(404)
w.Write([]byte("Not Found"))
}
HTTPRequests.WithLabelValues(strconv.Itoa(code)).Inc()
})
http.ListenAndServe(":80", nil)
}

该示例程序暴露了 httpserver_requests_total 指标,记录 HTTP 的请求,通过这个指标可以计算出该业务程序的 QPS 值。

部署业务程序

将我们的业务程序进行容器化并部署到集群,比如使用 Deployment 部署:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpserver
namespace: httpserver
spec:
replicas: 1
selector:
matchLabels:
app: httpserver
# 如果是基于qps来自动伸缩,则此处可以不配置cpu和内存
template:
metadata:
labels:
app: httpserver
spec:
containers:
- name: httpserver
image: imroc.tencentcloudcr.com/test/httpserver:v1
imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: httpserver
namespace: httpserver
labels:
app: httpserver
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/metrics"
prometheus.io/port: "http"
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app: httpserver

Prometheus 采集业务监控

业务部署好了,我们需要让我们的 Promtheus 去采集业务暴露的监控指标。

方式一: 配置 Promtheus 采集规则

在 Promtheus 的采集规则配置文件添加采集规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- job_name: httpserver
scrape_interval: 5s
kubernetes_sd_configs:
- role: endpoints
namespaces:
names:
- httpserver
relabel_configs:
- action: keep
source_labels:
- __meta_kubernetes_service_label_app
regex: httpserver
- action: keep
source_labels:
- __meta_kubernetes_endpoint_port_name
regex: http

方式二: 配置 ServiceMonitor

若已安装 prometheus-operator,则可通过创建 ServiceMonitor 的 CRD 对象配置 Prometheus。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: httpserver
spec:
endpoints:
- port: http
interval: 5s
namespaceSelector:
matchNames:
- httpserver
selector:
matchLabels:
app: httpserver

安装 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
2
3
4
5
6
7
8
9
10
11
12
13
rules:
default: false
custom:
- seriesQuery: 'httpserver_requests_total'
resources:
template: <<.Resource>>
name:
matches: "httpserver_requests_total"
as: "httpserver_requests_qps" # PromQL 计算出来的 QPS 指标
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>)
prometheus:
url: http://prometheus.monitoring.svc.cluster.local # 替换 Prometheus API 的地址 (不写端口)
port: 9090

执行 helm 命令进行安装:

1
2
3
4
5
6
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# Helm 3
helm install prometheus-adapter prometheus-community/prometheus-adapter -f values.yaml
# Helm 2
# helm install --name prometheus-adapter prometheus-community/prometheus-adapter -f values.yaml

利用rancher配置prometheus-adapter

如果是利用rancher安装的Monitoring,可以直接修改ConfigMap: rancher-monitoring-prometheus-adapter,如下(配置完后,重启Deployment: rancher-monitoring-prometheus-adapter

测试是否安装正确

如果安装正确,是可以看到 Custom Metrics API 返回了我们配置的 QPS 相关指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1
{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "custom.metrics.k8s.io/v1beta1",
"resources": [
{
"name": "jobs.batch/httpserver_requests_qps",
"singularName": "",
"namespaced": true,
"kind": "MetricValueList",
"verbs": [
"get"
]
},
{
"name": "pods/httpserver_requests_qps",
"singularName": "",
"namespaced": true,
"kind": "MetricValueList",
"verbs": [
"get"
]
},
{
"name": "namespaces/httpserver_requests_qps",
"singularName": "",
"namespaced": false,
"kind": "MetricValueList",
"verbs": [
"get"
]
}
]
}

也能看到业务 Pod 的 QPS 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/namespaces/${命名空间,这里是httpserver}/pods/*/httpserver_requests_qps
{
"kind": "MetricValueList",
"apiVersion": "custom.metrics.k8s.io/v1beta1",
"metadata": {
"selfLink": "/apis/custom.metrics.k8s.io/v1beta1/namespaces/httpserver/pods/%2A/httpserver_requests_qps"
},
"items": [
{
"describedObject": {
"kind": "Pod",
"namespace": "httpserver",
"name": "httpserver-6f94475d45-7rln9",
"apiVersion": "/v1"
},
"metricName": "httpserver_requests_qps",
"timestamp": "2020-11-17T09:14:36Z",
"value": "500m",
"selector": null
}
]
}

上面示例 QPS 为 500m,表示 QPS 值为 0.5

测试 HPA

假如我们设置每个业务 Pod 的平均 QPS 达到 50,就触发扩容,最小副本为 1 个,最大副本为1000,HPA 可以这么配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: httpserver
namespace: httpserver
spec:
minReplicas: 1
maxReplicas: 1000
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: httpserver
metrics:
- type: Pods
pods:
metric:
name: httpserver_requests_qps
target:
averageValue: 50
type: AverageValue

然后对业务进行压测,观察是否扩容:

1
2
3
4
5
6
7
8
9
10
$ kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
httpserver Deployment/httpserver 83933m/50 1 1000 2 18h

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
httpserver-6f94475d45-47d5w 1/1 Running 0 3m41s
httpserver-6f94475d45-7rln9 1/1 Running 0 37h
httpserver-6f94475d45-6c5xm 0/1 ContainerCreating 0 1s
httpserver-6f94475d45-wl78d 0/1 ContainerCreating 0 1s

扩容正常则说明已经实现 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
2
3
4
5
6
7
8
9
10
11
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: podinfo-pdb
spec:
# 如果不满足 PDB,Pod 驱逐将会失败!
minAvailable: 1 # 最少也要维持一个 Pod 可用
# maxUnavailable: 1 # 最大不可用的 Pod 数,与 minAvailable 不能同时配置!二选一
selector:
matchLabels:
app: podinfo

如果在进行节点维护时(kubectl drain),Pod 不满足 PDB,drain 将会失败,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> kubectl drain node-205 --ignore-daemonsets --delete-local-data
node/node-205 cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/calico-node-nfhj7, kube-system/kube-proxy-94dz5
evicting pod default/podinfo-7c84d8c94d-h9brq
evicting pod default/podinfo-7c84d8c94d-gw6qf
error when evicting pod "podinfo-7c84d8c94d-h9brq" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/podinfo-7c84d8c94d-h9brq
error when evicting pod "podinfo-7c84d8c94d-h9brq" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/podinfo-7c84d8c94d-h9brq
error when evicting pod "podinfo-7c84d8c94d-h9brq" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/podinfo-7c84d8c94d-h9brq
pod/podinfo-7c84d8c94d-gw6qf evicted
pod/podinfo-7c84d8c94d-h9brq evicted
node/node-205 evicted

上面的示例中,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 是完全的线性关系)