Kubernetes / Linux Note / 探知未来 / 运维笔记

Kubernetes 集群上基于 GPU 负载的HPA自动缩放

Einic Yeo · 7月30日 · 2021年 · · ·

首先我们有一个 Kubernetes 集群。它可以识别节点上的 GPU 并使用它们。甚至 GPU 指标也是通过 dcgm-exporter 和 prometheus 堆栈公开的。接下来是什么?我们知道我们可以Horizontal Pod Autoscaler根据 CPU 和内存使用情况在 Kubernetes 集群上自动扩展我们的程序。我们可以对 GPU 指标做同样的事情吗?答案是肯定的,Prometheus Adapter可以解决问题。

本文假设您有一个Kubernetes 集群可以使用 GPU,并且您正在使用 dcgm 和 prometheus 堆栈来获取 GPU 指标。如果没有,您可以按照我之前关于如何到达那里的文章进行操作。我的 Kubernetes 版本是1.20.2,而且我有Tesla V100GPU。这些已经在centos7运行

我们让 Prometheus 充当指标提供者——我们将其配置为更早地为我们提供 GPU 指标。Prometheus Adapter 为我们提供了自定义指标 API,提取由外部指标提供者(在本例中为 Prometheus)提供的指标。然后,Horizo​​ntal Pod Autoscalers 可以使用这些自定义指标来扩展工作负载。

在我们安装 Prometheus Adapter 之前,让我们检查连接到 Prometheus 的 url。稍后在配置我们的适配器时它会派上用场。

# kubectl get svc -n prometheus
NAME                                                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
alertmanager-operated                                       ClusterIP   None             <none>        9093/TCP,9094/TCP,9094/UDP   1d
kube-prometheus-stack-1614-alertmanager                     ClusterIP   10.97.150.200    <none>        9093/TCP                     1d
kube-prometheus-stack-1614-operator                         ClusterIP   10.107.120.24    <none>        443/TCP                      1d
kube-prometheus-stack-1614-prometheus                       NodePort    10.106.243.247   <none>        9090:30090/TCP               1d
kube-prometheus-stack-1614100923-grafana                    NodePort    10.100.30.205    <none>        80:32197/TCP                 1d
kube-prometheus-stack-1614100923-kube-state-metrics         ClusterIP   10.111.133.205   <none>        8080/TCP                     1d
kube-prometheus-stack-1614100923-prometheus-node-exporter   ClusterIP   10.110.160.60    <none>        9100/TCP                     1d
prometheus-operated                                         ClusterIP   None             <none>        9090/TCP                     1d

请注意,我们的服务在 port 可用的名称空间中prometheus-operated运行。因此,适配器使用的 prometheus url 将是http://prometheus-operated.prometheus.svc,端口为 9090。prometheus9090

一、安装 Prometheus adapter

使用 helm 安装 Prometheus Adapter 非常容易,但是需要一些配置来设置 Prometheus 指标和 Kubernetes 资源发现以及通过自定义指标 API 呈现这些指标。从技术上讲,这也可以通过https://github.com/kubernetes-sigs/prometheus-adapter 中记录的 helm install 版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!命令来完成。但是,这对我不起作用。我最终拉了helm chart,用自定义配置修改了它的值,然后安装。以下是步骤。

将helm chart拉到一个临时文件夹中。

helm pull prometheus-community/prometheus-adapter

这将为您提供 zip 文件中最新版本的adapter helm charts — prometheus-adapter-2.15.2.tgz. 解压缩此文件并转到prometheus-adapter文件夹。

tar -xvzf ../prometheus-adapter-2.15.2.tgz
cd prometheus-adapter/

在您喜欢的编辑器中编辑values.yaml文件。找到 prometheus 的 url 并将其从默认更改为我们拥有的。它应该是这样的。

现在找到rules并复制以下配置的部分。要更深入地了解这些规则,您可以查看https://github.com/kubernetes-sigs/prometheus-adapter/blob/master/docs/config-walkthrough.md

# Url to access prometheus
prometheus:
  # Value is templated
  url: http://prometheus-operated.prometheus.svc
  port: 9090
  path: ""

这里我们使用DCGM导出的 GPU 指标(有关详细信息,请参阅我关于 GPU 指标的另一篇文章)。在此版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!规则配置中,我们要求提供新的指标系列 – ‘ _current‘ 后缀将为我们提供当前值,而 ‘ _over_time‘ 前缀为我们提供超过三分钟的平均 pod 指标。您当然可以使用此配置并想出适合您目的的东西。请注意,我们注释掉了default规则。

rules:
  #default: true
  custom: 
    - seriesQuery: '{UUID!=""}'
      resources:
        overrides:
          node: {resource: "node"}
          exported_pod: {resource: "pod"}
          exported_namespace: {resource: "namespace"}
      name:
        matches: ^DCGM_FI_(.*)$
        as: "${1}_over_time"
      metricsQuery: ceil(avg_over_time(<<.Series>>{<<.LabelMatchers>>}[3m]))
    - seriesQuery: '{UUID!=""}'
      resources:
        overrides:
          node: {resource: "node"}
          exported_pod: {resource: "pod"}
          exported_namespace: {resource: "namespace"}
      name:
        matches: ^DCGM_FI_(.*)$
        as: "${1}_current"
      metricsQuery: <<.Series>>{<<.LabelMatchers>>}
  existing:
  external: []

保存此文件,返回一个文件夹级别并安装 adapter。

cd ..
helm install prometheus-adapter prometheus-adapter/ -n kube-system

查看adapter pod.

$ kubectl get pods -n kube-system|grep prometheus
prometheus-adapter-fd8ccb5b7-5lhsm                        1/1     Running   0          2m

现在是实际运行一个强调 GPU 的程序并查看它是否自动缩放的时候了。

二、扩展 HPA

创建一个 yaml 文件,例如,gpu-ray-autoscaler.yaml包含以下内容。

---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-gpu
  namespace: rayexperiment
spec:
  scaleTargetRef:
    apiVersion: apps/v1beta1
    kind: Deployment
    name: ray-worker
  minReplicas: 1
  maxReplicas: 4
  metrics:
   - type: Pods
     pods:
       # use the metric that you used above: pods/http_requests
       metricName: DEV_GPU_UTIL_current
       targetAverageValue: 40

我们的目标ray-worker是稍后要创建的 Deployment 。记住我们创建的指标——我们正在使用其中之一DEV_GPU_UTIL_current。如果 GPU 使用率超过 40%,则会创建一个新的 ray-worker pod,最多有四个副本。如果需要,我们还可以添加其他 GPU 指标。现在让我们部署这个自动缩放器。请注意,我们正在使用rayexperiment命名空间。

kubectl create -f gpu-ray-autoscaler.yaml -n rayexperiment

三、GPU 工作负载

当然,我们可以创建一个实际的 ML/深度学习工作负载来练习 GPU 和自动缩放器,但现在让我们坚持一个简单的。让我们创建一个 ray python 程序,用模拟负载对 GPU 施加压力。然后我们将用它创建一个 docker 容器并在一个 ray 集群中使用它。

这是一个强调 GPU 的简单 Ray 程序。一个简单的函数,模拟五分钟的高 GPU 负载,循环调用十次。启用光线就像将方法装饰为远程并版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!适当地调用它一样简单。我还在ray.wait分布式执行上添加了一个。

import torch
@ray.remote(num_gpus=1)
def load_gpu(x):
    print("starting iteration ", x)
    t_end = time.time() + 60 * 5
    my_tensor = torch.linspace(0, 10, 1100**2).cuda()
    while time.time() < t_end:
        my_tensor*my_tensor
    return x

if __name__ == '__main__':

    ray.init(address='auto', _redis_password='5241590000000000')
    results = []
    
    for i in range(0, 10):
        # call the ray remote method, and save the returned identifiers in a list
        results.append(load_gpu.remote(i))
    while len(results):
        # now wait on the results
        done, results = ray.wait(results)
        r = ray.get(done[0])
        print('done with result', r)

这是requirement版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!s.txt一个单行文件,只有 – torch==1.6.0。我们将使用一个包含所有其他必要包的 ray 基础镜像。

我们的Dockerfile, ray 的 gpu 基础映像将包含必要的cuda工具包。

FROM rayproject/ray:nightly-gpu
# Set up the dependencies 
WORKDIR /app
COPY requirements.txt .
RUN pip install -U pip && \
    pip install -r requirements.txt
COPY . ./

我们的.dockerignore文件的完整性。注意ray集群部署yaml文件,我们接下来要创建。

Dockerfile
.git
.gitignore
ray-cluster-gpuload.yaml

现在让我们构建该镜像。

docker build -t ray-gpu-load .

将映像推送到 Kubernetes 集群中当前使用的映像存储库。

docker tag ray-gpu-load:latest <your-docker-repo/your-namespace>/ray-gpu-load:latest
docker push <your-docker-repo/your-namespace>/ray-gpu-load:latest

四、部署 Ray 集群

我们有一个 ray 集群部署 yaml,可以从 ray 项目中轻松获得。让我们下载它。

https://github.com/ray-project/ray/blob/master/doc/kubernetes/ray-cluster.yaml

我们需要对该文件进行一些更改。我们将需要使用我们刚刚构建的镜像并请求 gpu 资源。让我们ray-cluster-gpuload.yaml在这些更改之后保存这个文件。

对于ray-head部分:

containers:
        - name: ray-head
          image: <your-docker-repo/your-namespace>/ray-gpu-load:latest

版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!及 ray-worker 部分:


containers:
      - name: ray-worker
        image: <your-docker-repo/your-namespace>/ray-gpu-load:latest

在 ray-worker 部分请求 gpu 资源。


resources:
          requests:
            cpu: 1000m
            memory: 512Mi
          limits:
            cpu: 1000
            nvidia.com/gpu: 1

注意:您可能需要添加,imagePullSecrets具体取决于您如何配置集群以从存储库中提取 docker 镜像。

还要将此 yaml 文件中的命名空间更改rayexperiment为与我们迄今为止所做的相匹配。

apiVersion: v1
kind: Service
metadata:
  namespace: rayexperiment

让我们用我们的镜像部署到 ray 集群中。


kubectl create -f ray-cluster-gpuload.yaml

查看并查询 rayexperiment 的pod


$ kubectl get pods -n rayexperiment
NAME                          READY   STATUS    RESTARTS   AGE
ray-head-74c449cb64-l9gfc     1/1     Running   0          15s
ray-worker-85789d96d4-zvb6t   1/1     Running   0          15s

五、Load rayexperiment

进入 ray worker pod

$ kubectl -n rayexperiment exec --stdin --tty ray-worker-85789d96d4-zvb6t -- /bin/bash
(base) [email protected]:/app$

运行我们的程序


(base) [email protected]:/app$ python3 ray_gpu_load.py 
2021-07-30 07:38:45,374 INFO worker.py:655 -- Connecting to existing Ray cluster at address: 10.99.14.184:6379
(pid=91) starting iteration  0
(pid=104, ip=10.244.0.201) starting iteration  1

六、查看自动伸缩

很快您就可以看到我们的自动缩放器在运行中,由于 GPU 使用率高而导致 pod 扩展。

# kubectl get pods -n rayexperiment
NAME                          READY   STATUS    RESTARTS   AGE
ray-head-74c449cb64-l9gfc     1/1     Running   0          5m12s
ray-worker-85789d96d4-6bjwc   1/1     Running   0          113s
ray-worker-85789d96d4-7tn68   1/1     Running   0          83s
ray-worker-85789d96d4-t8n77   1/1     Running   0          113s
ray-worker-85789d96d4-zvb6t   1/1     Running   0          5m12s

如果您已配置Grafana,则可以使用标准 NVIDIA DCGM 仪表板可视化指标。(你可以参考我之前关于如何配置它的文章,如果你还没有的话)。

几分钟后,程序完成,您可以看到 pod 数量随着 GPU 的使用而下降。

而已!我们现在可以根据 GPU 利用率指标来扩展我们的光线程序。您可以用实际的深度学习工作负载替换这个玩具程序。

参考文献

https://medium.com/@rajupavuluri/how-to-autoscale-your-ray-programs-based-on-gpu-load-on-kubernetes-clusters-9ba75d46b71b

1 条回应
  1. 远乡2022-4-28 · 21:47

    有个疑问, 这里的hpa配置中,数据来源于DCGM的pod,目标pod是另一种deployment; 这种能用Pod类型的metrics吗?
    据了解,pods类型应当描述的是当前扩容目标中每个pod的指标

    apiVersion: autoscaling/v2beta1
    kind: HorizontalPodAutoscaler
    metadata:
    name: hpa-gpu
    namespace: rayexperiment
    spec:
    scaleTargetRef:
    apiVersion: apps/v1beta1
    kind: Deployment
    name: ray-worker
    minReplicas: 1
    maxReplicas: 4
    metrics:
    – type: Pods
    pods:
    # use the metric that you used above: pods/http_requests
    metricName: DEV_GPU_UTIL_current
    targetAverageValue: 40