# 用 Volcano 填补私有集群的空闲时间

## 引子

在私有 Kubernetes 场景下，因为硬件规模是一定的，不太会随着业务高峰低谷进行裁撤，因此缩减下来的服务资源并不能带来成本上的优势，如果在闲时~<s>挖挖矿</s>~跑跑 AI 大数据什么的是不是可以贴补一下家用呢？Volcano 值得一试。

[Volcano（https://volcano.sh/zh/）](https://volcano.sh/zh/) 是“一个用于高性能工作负载场景下基于Kubernetes 的容器批量调度引擎”，项目主页上罗列了为数众多的应用案例和适配的 AI、大数据平台。

> 它提供了Kubernetes目前缺少的一套机制，这些机制通常是许多高性能 工作负载所必需的，包括：
> 
> * 机器学习/深度学习
>     
> * 生物学计算/基因计算
>     
> * 大数据应用 这些类型的应用程序通常运行在像 Tensorflow、Spark、PyTorch、 MPI 等通用领域框架上，Volcano 无缝对接这些框架。
>     

## 场景

假设这样一种场景，忙时会占用集群 60% 的 CPU 资源，而闲时则只需要一半，我们需要这样一个能力：

1. 闲时自动启动任务
    
2. 多个任务排队完成
    
3. 如果主业务启动，则自行停止队列任务
    

使用 Volcano，简单的几个 YAML 就能完成任务。

## 部署

可以用 YAML 直接部署 Volcano，K8s 版本要求在 1.13-1.21 之间（1.22 因为 CRD 资源组升级为 v1，需要进行一些修改才能部署），部署文件如下：

* **x86\_64**：`https://raw.githubusercontent.com/volcano-sh/volcano/master/installer/volcano-development.yaml`
    
* **arm64**：`https://raw.githubusercontent.com/volcano-sh/volcano/master/installer/volcano-development-arm64.yaml`
    

部署之后，会生成若干 CRD，以及一个 `volcano-system` 命名空间，其中包含 Volcano 的工作负载。

## 启动工作负载

我用了一个单节点 Kubernetes 集群，剩余可用资源刚好 1 CPU：

```bash
$ kubectl describe node single | grep -E "^\s+cpu\s+"
  cpu                1 (50%)     0 (0%)
```

创建一个 Deployment，为方便控制，我们使用两个占用 300m CPU 的实例（源码见附录 `workload.yaml`）：

```yaml
$ kubectl apply -f workload.yaml
deployment.apps/sleep created
$ kubectl describe node single | grep -E "^\s+cpu\s+"
  cpu                1600m (80%)  600m (30%)
```

可以看到，CPU Request 已经上升至 80%。

## 创建 Volcano 队列

源码见附录 `queue.yaml`，创建之后查看其状态：

```bash
$ kubectl apply -f queue.yaml
queue.scheduling.volcano.sh/q800 created
$ kubectl describe queue q800
...
Spec:
  Capability:
    Cpu:        800m
  Reclaimable:  false
  Weight:       1
Status:
  State:  Open
```

上面结果表明，创建了一个 CPU 容量为 `800m` 且状态开放的队列。

## 创建 Valcano 任务

在创建任务之前，我们首先要给 Volcano 和工作负载分别创建一个 PriorityClass：

```yaml
apiVersion: scheduling.k8s.io/v1
description: Used for volcano jobs
kind: PriorityClass
metadata:
  name: volcano-task
preemptionPolicy: PreemptLowerPriority
value: -1
---
apiVersion: scheduling.k8s.io/v1
description: Used for workloads
kind: PriorityClass
metadata:
  name: workload-task
preemptionPolicy: PreemptLowerPriority
value: 1
```

没有标明 `PriorityClass` 的 Pod ，其缺省优先级为 0，所以使用 -1 优先级的 Pod 就属于人见人踩的小角色了。而 `workload-task` 声明的 `PreemptLowerPriority` 策略，会在调度过程中抢占低优先级任务的资源。

接下来定义一个 Volcano 任务，源码见 `job1.yaml`，定义其优先级为刚刚创建的 `volcano-task`。这个 Job 配置最小可用副本为 2，和 Deployment 的定义类似，必须有两个 Pod 全部启动。

```yaml
$ kubectl apply -f job.yaml
job.batch.volcano.sh/jobb created
```

查看各种对象：

```yaml
$ vcctl job list
Name   Creation       Phase       JobType     Replicas    Min   Pending   Running   Succeeded   Failed    Unknown     RetryCount
jobb   2021-09-09     Pending     Batch       2           2     2         0         0           0         0           0
$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
jobb-sleep-0             0/1     Pending   0          7s
jobb-sleep-1             0/1     Pending   0          7s
sleep-6458fc8f96-qr6s6   1/1     Running   0          14m
sleep-6458fc8f96-tmf9k   1/1     Running   0          14m
```

看到任务和 Pod 都处于 Pending 状态，查看 Pod 的状态，会发现 Pending 原因是：

```bash
$ kubectl describe po jobb-sleep-0
...
Warning  FailedScheduling  8m22s  volcano  all nodes are unavailable: 1 node(s) resource fit failed.
```

因为资源不足，导致任务被挂起，这是我们期待的效果。

## 工作负载扩缩容

假设闲时工作负载资源需求降低 50%，看看会发生什么：

```yaml
$ kubectl scale deployment sleep --replicas=1
deployment.apps/sleep scaled
$ vcctl job list
Name   Creation       Phase       JobType     Replicas    Min   Pending   Running   Succeeded   Failed    Unknown     RetryCount
jobb   2021-09-09     Running     Batch       2           2     0         2         0           0         0           0
```

此时看到队列中的 Job 已经启动运行。

如果工作负载开始增加，优先级就会发生作用了：

```yaml
dustise:mine/ $ kubectl scale deployment sleep --replicas=2                                                               [15:15:25]
deployment.apps/sleep scaled
dustise:mine/ $ kubectl get pods                                                                                          [15:15:29]
NAME                     READY   STATUS        RESTARTS   AGE
jobb-sleep-0             0/1     Pending   0          4m17s
jobb-sleep-1             0/1     Pending   0          4m17s
sleep-7f67d6c47c-5cp7s   1/1     Running   0          5m12s
sleep-7f67d6c47c-jrsqh   1/1     Running   0          4m29s
```

可以看到 `jobb` 已经退回 Pending 状态，任务回到排队状态。

```bash
$ vcctl queue list
Name                     Weight  State   Inqueue Pending Running
...
q800                     1       Open    1       0       0       0
```

## 后记

也不知道 Volcano 啥时候弄个挖矿调度器。。

## 源码

### workload.yaml

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: sleep
  name: sleep
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      containers:
      - image: dustise/sleep:v0.9.8
        name: sleep
        resources:
          requests:
            cpu: 300m
          limits:
            cpu: 300m
```

### job1.yaml

```yaml
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: jobb
spec:
  minAvailable: 2
  schedulerName: volcano
  queue: q800
  priorityClassName: volcano-task
  policies:
    - event: PodEvicted
      action: RestartJob
  tasks:
    - replicas: 2
      name: sleep
      policies:
      - event: TaskCompleted
        action: CompleteJob
      template:
        spec:
          containers:
          - image: dustise/sleep:v0.9.8
            name: sleep
            resources:
              requests:
                cpu: 300m
              limits:
                cpu: 300m
            restartPolicy: Never
```

### queue.yaml

```yaml
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: q800
spec:
  weight: 1
  reclaimable: false
  capability:
    cpu: 800m
```
