kubernetes通过vertica pod autoscaler实现动态垂直缩放

今天分享一下k8s的垂直缩放这块,垂直扩容会涉及到request的概念,所以这里我会多啰嗦一下request到底是怎么回事和docker的cpu shares又有什么关系?

垂直容器自动缩放器(VPA)简单说就是使用户无需设置最新的资源限制和对容器中容器的要求。
配置后,它将根据使用情况自动设置请求,从而允许在节点上进行适当的调度,以便为每个Pod提供适当的资源量。 它还将保持限制和初始容器配置中指定的请求之间的比率。

它既可以根据资源的使用情况来缩减对资源过度使用的Pod的规模,也可以对资源需求不足的向上扩展的Pod的规模进行扩展。
自动缩放是使用称为VerticalPodAutoscaler的自定义资源定义对象配置的。 它允许指定哪些吊舱应垂直自动缩放,以及是否/如何应用资源建议。

简单来说是 Kubernetes VPA 可以根据实际负载动态设置 pod resource requests。

说到资源限制前面说一下request这块到底是怎么回事?

在我们使用kubernetes的过程中,我们知道Pod 是最小的原子调度单位。这也就意味着,所有跟调度和资源管理相关的属性都应该是属于 Pod 对象的字段。而这其中最重要的部分,就是 Pod 的 CPU 和内存配置,如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        resources:
          requests:
            memory: "64Mi" 
            cpu: "250m"
          limits:
            memory: "128Mi" 
            cpu: "500m"

在 Kubernetes 中,像 CPU 这样的资源被称作“可压缩资源”(compressible resources)。它的典型特点是,当可压缩资源不足时,Pod 只会“饥饿”,但不会退出。而像内存这样的资源,则被称作“不可压缩资源(incompressible resources)。当不可压缩资源不足时,Pod 就会因为 OOM(Out-Of-Memory)被内核杀掉。

而由于 Pod 可以由多个 Container 组成,所以 CPU 和内存资源的限额,是要配置在每个 Container 的定义上的。这样,Pod 整体的资源配置,就由这些 Container 的配置值累加得到。

其中,Kubernetes 里为 CPU 设置的单位是“CPU 的个数”。比如,cpu=1 指的就是,这个 Pod 的 CPU 限额是 1 个 CPU。当然,具体“1 个 CPU”在宿主机上如何解释,是 1 个 CPU 核心,还是 1 个 vCPU,还是 1 个 CPU 的超线程(Hyperthread),完全取决于宿主机的 CPU 实现方式。Kubernetes 只负责保证 Pod 能够使用到“1 个 CPU”的计算能力。

此外,Kubernetes 允许你将 CPU 限额设置为分数,比如在我们的例子里,CPU limits 的值就是 500m。所谓 500m,指的就是 500 millicpu,也就是 0.5 个 CPU 的意思。这样,这个 Pod 就会被分配到 1 个 CPU 一半的计算能力。

当然,你也可以直接把这个配置写成 cpu=0.5。但在实际使用时,我还是推荐你使用 500m 的写法,毕竟这才是 Kubernetes 内部通用的 CPU 表示方式。

而对于内存资源来说,它的单位自然就是 bytes。Kubernetes 支持你使用 Ei、Pi、Ti、Gi、Mi、Ki(或者 E、P、T、G、M、K)的方式来作为 bytes 的值。比如,在我们的例子里,Memory requests 的值就是 64MiB (2 的 26 次方 bytes) 。这里要注意区分 MiB(mebibyte)和 MB(megabyte)的区别。备注:1Mi=10241024;1M=10001000此外,不难看到,Kubernetes 里 Pod 的 CPU 和内存资源,实际上还要分为 limits 和 requests 两种情况,如下所示:

  • 备注:1Mi=10241024;1M=10001000
  • spec.containers[].resources.limits.cpu
  • spec.containers[].resources.limits.memory
  • spec.containers[].resources.requests.cpu
  • spec.containers[].resources.requests.memory

这两者的区别其实非常简单:在调度的时候,kube-scheduler 只会按照 requests 的值进行计算。而在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置。更确切地说,当你指定了 requests.cpu=250m 之后,相当于将 Cgroups 的 cpu.shares 的值设置为 (250/1000)*1024。
那么我们来验证一下这个cpu-shares具体的值
测试运行一个pod,这里我给的资源cpu限制是250m

#kubectl describe po nginx-846bc8d9d4-lcrzk |grep -A 2  Requests:
    Requests:
      cpu:        250m
      memory:     64Mi

现在已经交给了cgroups的cpu.shares的值进行配置,如何计算(250/1000)*1024=256
这个256份额值我们可以通过下面的命令docker inspect 格式化直接获取到,我们现在在docker所看到的256这个值则是pod我们进行request设置的值

#docker ps |grep nginx |awk '{print $1}'|head -1 |xargs docker inspect --format '{{.Id}}:CpuShare={{.HostConfig.CpuShares}}'
b164dc1c62f7eb16a28dc0a14a26e0ef764a7517487d97e9d87883034380302a:CpuShare=256

而当你没有设置 requests.cpu 的时候,cpu.shares go模版的显示默认则是 2,但是我们要知道实际上使用这里默认为1024,每一个启动的容器份额为1024
只是显示的是这样表示
可以通过定位到pod的启动容器的具体目录查看

#cd  /sys/fs/cgroup/cpu/docker
#cat 709e4aeaea9331d09980d6f041e4fc0c8ff78c5d7477825852c076ffcc4fb3d5/cpu.shares 
1024

这样,Kubernetes 就通过 cpu.shares 完成了对 CPU 时间的按比例分配。
这里所说的时间分配又说到了cpu分配的优先级,也就是cpu-shares其实是对cpu使用的一个优先分配的份额,我们知道cpu是可压缩资源,当分配的时候也决定cpu谁有更快分配CPU的能力,我们可以通过下面的测试来验证这个cpu-shares

计划我这里运行3个容器,为它们提供100、500和1000个cpu共享。

在后面,我们将使用实际的Linux基准测试工具使用自己的工作台容器进行这些测试。我们将特别关注在非常短的运行时运行这些占用CPU的系统,并且仍然可以得到准确的结果。

注意,dd、urandom和md5sum也不是工具,只是用来压测我们的cpu-shares的分配的时间,谁更有优先去分配到cpu的能力

我们的CPU压力应用程序:时间dd if=/dev/urandom bs=1M count=2 | md5sum

指标解释:

时间度量运行时间:显示这3个计时器行
dd if=/dev/urandom bs=1M count=2…复制bs=块大小1 MB 进行100次
md5sum……计算md5安全哈希值(给cpu一个负载)
让我们运行它并调查结果:

docker container run -d --cpu-shares=1024 --name mycpu1024 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
docker container run -d --cpu-shares=500 --name mycpu500 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
docker container run -d --cpu-shares=100 --name mycpu100 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'

查看并获取我们的容器返回的数据日志

#docker logs mycpu1024
100+0 records in
100+0 records out
real    0m 0.96s
user    0m 0.00s
sys     0m 0.60s

#docker logs mycpu500
100+0 records in
100+0 records out
real    0m 0.99s
user    0m 0.00s
sys     0m 0.60s
b06118f07ce61d0e5a1201ad31659137  -

#docker logs mycpu100
100+0 records in
100+0 records out
real    0m 1.00s
user    0m 0.00s
sys     0m 0.60s
0046b35a22a48237cac7a75648e4e056  -

注意,所有容器都使用了相同的sys cpu时间这是可以理解的,因为它们都做了完全相同的工作。
--cpu-share =100显然需要更长的时间,但是--cpu-share =500只比--cpu-share =1024稍微慢一点,这里我测试显示的和1024几乎一样,这跟测试也存在略微差别
问题是cpu-shares=1024运行非常快,然后退出。
那么我们可以得到以下结论:
那么--CPU -shares=500和--CPU -shares=100具有对CPU的完全访问权。

然后--CPU -shares=500个完成,因为它拥有最多的CPU共享。

然后--CPU -shares=1024快速完成,因为它拥有大多数CPU共享

经过这个测试我希望你明白一个道理,pod进行配置的request资源的限制,其实和docker的cgroup做了同样的操作,只不过docker将对应的pod的值进行计算得到另外一个值的形式显示出来,而计算的值则是(250/1000)*1024来计算的,分配的request的值如果没有限制则具有大多数cpu的共享,可以优先共享cpu资源。

下面limit这里我也多啰嗦几句,后面回到主题
而如果你指定了 limits.cpu=500m 之后,则相当于将 Cgroups 的 cpu.cfs_quota_us 的值设置为 (500/1000)*100ms,而 cpu.cfs_period_us 的值始终是 100ms。这样,Kubernetes 就为你设置了这个容器只能用到 CPU 的 50%。

而对于内存来说,当你指定了 limits.memory=128Mi 之后,相当于将 Cgroups 的 memory.limit_in_bytes 设置为 128 1024 1024。
我们可以通过docker 模版查看

#docker ps |grep nginx
45676528fbea        nginx

我们限制了128Mi则在docker的cgroup这么计算,12810241024得到cgroup的limit限制则是134217728字节

#docker ps --quiet --all |xargs docker inspect --format '{{.Id }}:Memory={{.HostConfig.Memory}}'
45676528fbea55a94b80553a8f1c57396c31aecc674b91eddb61024931ac11c9:Memory=134217728

而需要注意的是,在调度的时候,调度器只会使用 requests.memory=64Mi 来进行判断。
Kubernetes 这种对 CPU 和内存资源限额的设计,实际上参考了 Borg 论文中对“动态资源边界”的定义,既:容器化作业在提交时所设置的资源边界,并不一定是调度系统所必须严格遵守的,这是因为在实际场景中,大多数作业使用到的资源其实远小于它所请求的资源限额。

基于这种假设,Borg 在作业被提交后,会主动减小它的资源限额配置,以便容纳更多的作业、提升资源利用率。而当作业资源使用量增加到一定阈值时,Borg 会通过“快速恢复”过程,还原作业原始的资源限额,防止出现异常情况。而 Kubernetes 的 requests+limits 的做法,其实就是上述思路的一个简化版:用户在提交 Pod 时,可以声明一个相对较小的 requests 值供调度器使用,而 Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。不难看到,这跟 Borg 的思路相通的。

下面回到正题,说说vpa
部署vpa可参考我的github
https://github.com/zhaocheng173/vpa

Kubernetes VPA 包含以下组件:
Recommender:用于根据监控指标结合内置机制给出资源建议值
Updater:用于实时更新 pod resource requests
History Storage:用于采集和存储监控数据
Admission Controller: 用于在 pod 创建时修改 resource requests

它的架构是这样的
kubernetes通过vertica pod autoscaler实现动态垂直缩放
vpa的实现的主要流程它是这样的:
Recommender在启动时从History Storage获取历史数据,根据内置机制修改VPA API object资源建议值。Updater监听VPA API object,依据建议值动态修改 pod resource requests。VPA Admission Controller则是用于 pod 创建时修改 pod resource requests。History Storage则是通过Kubernetes Metrics API采集和存储监控数据

现在去检查Vertical Pod Autoscaler是否在您的集群中完全正常运行的一种简单方法是创建示例部署和相应的VPA配置:

---
apiVersion: "autoscaling.k8s.io/v1beta2"
kind: VerticalPodAutoscaler
metadata:
  name: hamster-vpa
  namespace: kube-system
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hamster
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: 100m
          memory: 50Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534 # nobody
      containers:
        - name: hamster
          image: nginx
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"

kubectl create -f examples/hamster.yaml

上面的命令创建了一个包含2个Pod的部署,每个Pod运行一个请求100m的容器,并尝试使用最高不超于500m的容器。该命令还会创建一个指向部署的VPA配置。VPA将观察Pod的行为,大约5分钟后,它们应使用更高的CPU请求进行更新(请注意,VPA不会在部署中修改模板,但Pod的实际请求会被更新)。

要查看VPA配置和当前推荐的资源请求,可以运行以下命令:

#kubectl get vpa -A
NAMESPACE     NAME          AGE
kube-system   hamster-vpa   15m

现在我们的默认值是以下配置
            requests:
              cpu: 100m
              memory: 50Mi

大概等待60s之后pod会重建,来获取推荐的request的值

#kubectl describe vpa nginx-deployment-basic -n kube-system
    resources:
      requests:
        cpu: 587m
        memory: 262144k

这个值获取到这里并分为4个指标建议
1、 Lower Bound: 也就是最低的下限,为cpu 25m,memory为262144k
2、Target: 目标我理解为也就是平均值,为cpu25m,memory为262144k vpa的作者也是建议使用target推荐的值
3、Uncapped Target无上限,为cpu25m,memory为262144k
4、Upper Bound 上限,为cpu2651m,也就是2.651个cpu

 Recommendation:
    Container Recommendations:
      Container Name:  nginx
      Lower Bound:
        Cpu:     25m
        Memory:  262144k
      Target:
        Cpu:     25m
        Memory:  262144k
      Uncapped Target:
        Cpu:     25m
        Memory:  262144k
      Upper Bound:
        Cpu:     2651m
        Memory:  2771500k

那么这种情况下我们操作会导致我们的pod会重启,也会将pod调度到其他的节点,对于不设置任何调度规则的话,这样对于我们的业务肯定会受到影响
需要注意以下的在生产中使用时的注意事项

VPA不会驱逐没有在副本控制器管理下的Pod。目前对于这类Pod,Auto模式等同于Initial模式。
目前VPA不能和监控CPU和内存度量的Horizontal Pod Autoscaler (HPA)同时运行,除非HPA只监控其他定制化的或者外部的资源度量。
VPA使用admission webhook作为其准入控制器。如果集群中有其他的admission webhook,需要确保它们不会与VPA发生冲突。准入控制器的执行顺序定义在API Server的配置参数中。
VPA会处理绝大多数OOM(Out Of Memory)的事件,但不保证所有的场景下都有效。
VPA的性能还没有在大型集群中测试过。
VPA对Pod资源requests的修改值可能超过实际的资源上限,例如节点资源上限、空闲资源或资源配额,从而造成Pod处于Pending状态无法被调度。同时使用集群自动伸缩(ClusterAutoscaler)可以一定程度上解决这个问题。
多个VPA同时匹配同一个Pod会造成未定义的行为。

VPA在三种模式下运行:
"Auto":VPA会在吊舱创建时分配资源请求,并使用首选更新机制在现有吊舱上更新资源请求。当前,这等效于"Recreate"(请参见下文)。一旦有可用的Pod请求免费重启(“就地”)更新,该"Auto"模式就可以将其用作首选更新机制。注意: VPA的此功能是试验性的,可能会导致应用程序停机。
"Recreate":当请求的资源与新建议明显不同(尊重Pod中断预算,如果已定义)时,VPA会在创建广告连播时分配资源请求,并通过驱逐它们来更新现有的广告连播。仅当您需要确保每当资源请求更改时都重新启动Pod时,才应很少使用此模式。否则,请选择一种"Auto"模式,该模式可在可用时重新启动免费更新。注意: VPA的此功能是试验性的,可能会导致您的应用程序停机。
"Initial":VPA仅在吊舱创建时分配资源请求,以后再也不会更改它们。
"Off":VPA不会自动更改容器的资源要求。将计算建议,并可以在VPA对象中对其进行检查。
我们其实要做的就是不采用Auto的形式,只通过推荐参考的形式,将历史给出的target作为我们项目参考的request值

现在运行一个redis的示例,并获取vpa推荐的值

---
apiVersion: autoscaling.k8s.io/v1beta2
kind: VerticalPodAutoscaler
metadata:
  name: redis-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: redis-master
  updatePolicy:
    updateMode: "Off"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-master
  labels:
    app: redis
spec:
  selector:
    matchLabels:
      app: redis
  replicas: 3
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: master
          image: redis  # or just image: redis
          ports:
            - containerPort: 6379

运行完之后,我们可以看到已经帮我们将值获取出来,可以通过describe进行查看

[root@master examples]# kubectl get vpa
NAME          AGE
hamster-vpa   53m
redis-vpa     90s
[root@master examples]# kubectl describe vpa redis-vpa
  Recommendation:
    Container Recommendations:
      Container Name:  master
      Lower Bound:
        Cpu:     25m
        Memory:  262144k
      Target:
        Cpu:     25m
        Memory:  262144k
      Uncapped Target:
        Cpu:     25m
        Memory:  262144k
      Upper Bound:
        Cpu:     15077m
        Memory:  7538772727

最后我们就可以根据推荐值来实际配置Deployment中资源的requests。VPA会持续的监控应用资源的使用情况,并提供优化建议。
通过将target的值可以根据换算进行使用到我们的生产环境当中

上一篇:sql – Hadoop:创建数据库管理器时出错


下一篇:VERTIA常用命令