# 无需重启，使用 Shell Operator 对 Pod 进行垂直扩缩容

通常情况下，要修改 Pod 的资源定义，是需要重启 Pod 的。在 Kubernetes 1.27 中，有一个 Alpha 状态的 `InPlacePodVerticalScaling` 开关，开启这一特性，就能在不重启 Pod 的情况下，修改 Pod 的资源定义。

要使用这个功能，需要在 `kube-apiserver` 的 `featureGates` 中显式地设置启用，启用这一特性之后，就可以进行测试了。

> 例如 Kind 集群，需要在配置中加入：

```yaml
featureGates:
  "InPlacePodVerticalScaling": true
```

## 测试一下

假设下面的 Pod 定义：

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: stress
spec:
  containers:
  - name: stress
    image: colinianking/stress-ng:latest
    resizePolicy:
    - resourceName: cpu
      restartPolicy: NotRequired
    - resourceName: memory
      restartPolicy: RestartContainer    
    command: ["sleep", "3600"]
    resources:
      limits:
        cpu: 200m
        memory: 200M
      requests:
        cpu: 200m
        memory: 200M
```

可以看到，spec 中加入了 resizePolicy 字段，用来指定对 CPU 和内存的扩缩容策略。内容很直白：

* CPU 的扩缩容策略是 `NotRequired`，即不重启 Pod；
    
* 内存的扩缩容策略是 `RestartContainer`，即重启 Pod。
    

将上述内容提交到 Kubernetes 中运行。启动之后，如果运行 `kubectl get po stress -o yaml`，会发现状态字段中加入了如下内容：

```yaml
- allocatedResources:
    cpu: 200m
    memory: 200Mi
```

说明此时分配给容器的资源。如果这时候对 CPU 进行修改，例如修改为：

```yaml
resources:
  limits:
    cpu: 800m
    memory: 200Mi
  requests:
    cpu: 100m
    memory: 100Mi
```

修改后查看 Pod 列表，会发现 Pod 没有重启：

```bash
$ kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
stress   1/1     Running   0          4m14s
```

重新获取 YAML，会看到状态字段的一些变化：

1. `resize: InProgress`：表示正在扩缩容；
    
2. 当前分配的资源也发生了变化：
    
    ```yaml
    - allocatedResources:
      cpu: 100m
      memory: 100Mi
    ```
    

## 自动纵向扩缩容

到目前为止，VPA 还没有支持这一特性。我们可以简地使用 Prometheus 对 Pod 资源压力进行监控，然后使用 Shell Operator 来实现自动扩缩容。总体思路就是，定期读取 Prometheus，获取指定 Pod 的 CPU 和使用情况，如果 CPU 使用率超过 80%，则将其 CPU 上限扩容一倍。

### Prometheus 监控指标

[Awesome Prometheus alerts](https://samber.github.io/awesome-prometheus-alerts/rules#kubernetes) 提供了如下的告警定义，用于表达 CPU 用量和其 Limit 的关系：

```yaml
  - alert: ContainerHighCpuUtilization
    expr: (sum(rate(container_cpu_usage_seconds_total{container!=""}[5m])) by (pod, container) / sum(container_spec_cpu_quota{container!=""}/container_spec_cpu_period{container!=""}) by (pod, container) * 100) > 80
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Container High CPU utilization (instance {{ $labels.instance }})
      description: "Container CPU utilization is above 80%\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}"
```

我们把它写入 Python 代码：

```python
CPU_USAGE_QUERY = '''(sum(rate(container_cpu_usage_seconds_total{{namespace="{0}", pod="{1}", container="{2}"}}[5m])) by (pod, container) 
/ sum(container_spec_cpu_quota{{namespace="{0}", pod="{1}",container="{2}"}}
/container_spec_cpu_period{{namespace="{0}", pod="{1}", container="{2}"}}) by (pod, container) * 100)'''
```

### 定期运行

要设置 Shell Operator 的定期运行，需要使用 Schedule 类型的配置，下面的 Configmap 设置每两分钟运行一次：

```yaml
apiVersion: v1
data:
  config.yaml: |+
    configVersion: v1
    schedule:
    - crontab: "*/2 * * * *"
      allowFailure: true
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: so-config
```

我们会将这个 Configmap 加载到 Pod 定义中，

```yaml
...
volumeMounts:
- mountPath: /conf/
  name: operator-config
...
volumes:
- configMap:
  name: so-config
name: operator-config
...
```

在 Hook 代码执行参数中带有 `--config` 参数时，读取该配置进行返回：

```python
if len(sys.argv) > 1 and sys.argv[1] == "--config":
    with open("/conf/config.yaml", "r") as f:
        print(f.read())
    exit(0)
```

### RBAC

Shell Operator 需要对 Pod 资源进行扩容，所以需要如下授权：

```yaml
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch", "patch"]
```

### 构建 Docker 镜像

```dockerfile
FROM flant/shell-operator
RUN apk update && \
    apk add --no-cache py3-requests
COPY main.py /hooks
```

### 测试

Workload 中，我们设置的资源是 100M 内存+100m CPU 的配置，使用 `kubectl exec -it stress -- sh` 进入 Pod 之后，执行 `stress-ng --cpu 1 --fork 2` 制造一点压力，触发 Shell Operator 中的脚本对 Pod 进行纵向扩容，在 Prometheus 会看到如下曲线：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745947395636/885c1bd7-6a48-4bec-9125-030fce3c4ae1.png align="center")

随着每次运行和扩容，CPU 水位不断下降，直到稳定。打开 Pod 定义，会看到扩容的痕迹：

```yaml
  containerStatuses:
  - allocatedResources:
      cpu: 6400m
      memory: 100M
    containerID: containerd://10c55739a6a63f3464184f5384a2f2b091a235b7b6689bcdb58526e3eb8bdb19
    image: docker.io/colinianking/stress-ng:latest
    imageID: docker.io/colinianking/stress-ng@sha256:1b10c09968ea3460196596398f7811c7a604489a8311b3dbf477f552ac5ea972
    lastState:
      terminated:
        containerID: containerd://e3f1fe628086e291830b47247c88403d3ce0f4fd5db38b18afcca444659011d3
        exitCode: 0
        finishedAt: "2024-09-08T08:07:49Z"
        reason: Completed
        startedAt: "2024-09-08T07:07:49Z"
    name: stress
    ready: true
    resources:
      limits:
        cpu: 6400m
        memory: 100M
```

## 结论

全部代码被上传到了 `https://github.com/fleeto/vscale-by-shelloperator`。内容当然还是非常简陋，例如缺乏缩容手段、没有对上限进行限制，防抖动措施也是缺乏的。另外该特性还处在 Alpha 阶段，因此不推荐在生产环境中使用。
