# K8s 1.37 新特性：原生 Gang Scheduling 与 HPA Scale to Zero


![K8s 1.37 新特性：原生 Gang Scheduling 与 HPA Scale to Zero](https://img.lixueduan.com/kubernetes/cover/k8s-1.37-new-features.jpg)

Kubernetes 1.37 已于 2026 年 8 月 26 日发布，今天分享其中两项值得关注的新特性。
**原生 Gang Scheduling 和 HPA Scale to Zero 都进入 Beta 了**。前者让调度器能够按一组 Pod 来判断资源是否够用，后者让空闲的 Pod 自动缩到零，新任务来了再启动。

这篇主要分享这两个特性，顺便搭一个集群实际跑一下，看看用起来是什么效果。

<!--more-->

## 1.K8s 1.37 主要新特性

Kubernetes 1.37 更新主要在资源管理和调度方面：
- 原生 Gang Scheduling、HPA Scale to Zero 进入 Beta
- DRA 的扩展资源支持和设备污点等能力正式 GA
- Memory QoS 也进入 Beta
- 同时 Metrics API 也正式稳定
- PVC 新增闲置状态追踪，Pod 证书与信任包进入 GA。

更多新特性可以参考 [Kubernetes v1.37 官方发布公告](https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release)。

这篇主要挑 Gang Scheduling 和 HPA Scale to Zero 展开，看看实际怎么用。

## 2.重点特性介绍

### 2.1 Gang Scheduling

Gang Scheduling 并不是什么新概念，在 k8s 生态中，早在 2018 年左右就受到关注了，大致发展分为以下几个阶段：
- 2018-2020：早期探索，主要解决 TensorFlow、MPI、Spark 等分布式任务的调度问题，代表项目 kube-batch 和 Volcano。
- 2021-2024：逐步普及，随着越来越多的大数据、AI 训练等分布式任务切换到 K8s 底座，Gang Scheduling 逐渐成为了重要调度能力，Volcano 等项目持续发展。
- 2025-2026：走向原生，Kubernetes 将 Gang Scheduling 纳入原生调度体系，先后在 v1.35、v1.36、v1.37 中实现和完善。

1.37 除了将核心 API 提升至 Beta，还让 PodGroup 直接作为调度队列中的单元，允许动态修改 `minCount`，并将 Workload-Aware Preemption 合并到 `GenericWorkload` 开关中。

#### 核心对象

原生 Gang Scheduling 引入了两个核心对象：`Workload` 和 `PodGroup`。

**Workload 保存调度策略模板。** 比如一个任务需要两个 Worker 一起调度，可以这样定义：

```yaml
apiVersion: scheduling.k8s.io/v1beta1
kind: Workload
metadata:
  name: training
spec:
  podGroupTemplates:
    - name: workers
      schedulingPolicy:
        gang:
          minCount: 2
```

这里定义了一个名为 `workers` 的模板，要求至少两个成员满足调度条件。Workload 本身不会创建 Pod，也不会因为创建了模板就自动生成 PodGroup；这些工作需要相应的控制器完成。

**PodGroup 表示实际参与调度的一组 Pod，保存这组的调度要求和状态。** 根据上面的模板创建的 PodGroup 可以写成：

```yaml
apiVersion: scheduling.k8s.io/v1beta1
kind: PodGroup
metadata:
  name: training-workers
spec:
  workloadRef:
    workloadName: training
    templateName: workers
  schedulingPolicy:
    gang:
      minCount: 2
```

`workloadRef` 标明使用了哪个模板，实际调度规则则保存在 PodGroup 自己的 `schedulingPolicy` 中。Pod 通过下面的字段加入这个组：

```yaml
spec:
  schedulingGroup:
    podGroupName: training-workers
```

这样，同一个 Workload 模板可以用于创建多个 PodGroup，每个组分别记录自己的调度状态。Pod 仍由 Job 等控制器或用户创建，PodGroup 负责表达组调度要求。

**PodGroup 也可以独立使用，不需要先创建 Workload。** 后面的 Demo 就采用这种方式，直接创建 PodGroup 和 Pod，观察资源不足时的调度行为。

#### 原生支持之后，Volcano 还有必要吗

*K8s 原生支持 Gang Scheduling 之后还有必要使用 Volcano 吗？*

提到 Gang Scheduling 大家就会想到 Volcano，但是 Volcano 的功能却不止于此。
Volcano 还负责批处理作业管理，以及多个团队、多个作业之间的资源分配。这些需求并没有因为原生支持 Gang 而消失。

- 如果只需要简单的 Gang Scheduling，那可以优先考虑 K8s 原生能力，没必要专门引入 Volcano 之类的项目
- 需要多租户 GPU 队列、资源借用与回收、公平调度，那么 Volcano 仍然有明显价值。

关键在于：K8s 原生 Gang Scheduling 解决的是“一组 Pod 如何一起调度”，Volcano 解决的则是“多组作业如何公平、高效地共享整个集群的资源”。


### 2.2 HPA Scale to Zero

早在 Serverless 兴起时期，K8s 就在探索工作负载的零副本弹性伸缩。同样可以大致分为三个阶段：
- 2018-2020：随着 Serverless 和事件驱动架构的发展，Knative、KEDA 等项目相继出现，实现了工作负载从 0 到 N 的弹性伸缩。K8s 也早在 v1.16 就引入了 HPA Scale to Zero 的 Alpha 实现。
- 2021-2025：生态发展。 KEDA、Knative 等项目持续完善零副本伸缩能力，逐渐应用于事件驱动服务、消息队列消费者和 Serverless 工作负载，但 Kubernetes 原生 HPA 的相关能力仍长期停留在 Alpha 阶段。
- 2026：原生能力完善，K8s v1.36 改进了 HPA Scale to Zero 的状态管理与扩缩容逻辑，v1.37 则正式将其提升至 Beta 并默认启用，使基于 Object、External Metrics 的工作负载可以通过原生 HPA 实现从 0 到 N 的自动伸缩。


#### 零副本靠什么唤醒？

Scale to Zero 并不难，最大的问题其实是零副本靠什么唤醒。

传统 HPA 常用 CPU、内存指标扩缩容，但副本缩到零之后，Pod 指标也没了，拿什么发现新任务？

所以零副本伸缩需要独立存在的 Object 或 External 指标。一般是通过额外的队列长度来解决这个问题，零副本之后新任务仍可以入队。HPA 持续读取这个独立存在的指标，就能知道什么时候该启动消费者。

> **`minReplicas: 0` 至少需要一个 Object 或 External 指标**，只有 CPU、内存指标的配置会被 API Server 拒绝。

1.37 中 `HPAScaleToZero` 在 kube-apiserver 和 kube-controller-manager 上默认开启，这里直接配置 HPA。后面会用 `ScaledToZero` Condition 区分自动归零和手动暂停。

#### 与 KEDA、Knative 的分工

KEDA、Knative 早就能缩到零，原生 HPA 补上这项能力后，它们仍各有用途：

| 方案 | 主要解决什么 | 零副本时如何恢复 |
| --- | --- | --- |
| 原生 HPA | 根据指标调整工作负载副本数 | 持续读取 Object、External 指标，恢复副本；指标适配和请求缓冲需要另外提供 |
| KEDA | 对接消息队列、数据库等事件源，提供 Scaler、认证和触发器配置 | 在这里参考的 KEDA 2.18 架构中，由 Operator 处理 0↔1，生成的 HPA 处理 1↔N |
| Knative Serving（KPA） | 提供面向 HTTP 服务的自动伸缩和流量接入 | Activator 暂存请求并配合 Autoscaler 唤醒服务，就绪后转发请求 |

如果已经有可靠的 External Metrics 链路，只需要根据队列长度伸缩消费者，可以直接使用原生 HPA。需要方便地接入多种事件源时，KEDA 的 Scaler 和配置能力仍然有价值；需要 HTTP 请求触发唤醒、等待冷启动和转发流量时，Knative Serving 提供的是更完整的服务链路。

## 3. Demo

### 环境准备

这里使用 KubeClipper v1.7.0 部署 Kubernetes v1.37.0 单节点集群，安装和创建集群的步骤参考之前的 [KubeClipper 1.7.0 发布：Operation 优化与 Kubernetes 1.37 支持](https://www.lixueduan.com/posts/kubernetes/66-kubeclipper-release-1.7.0/)。

```bash
kubectl get node

NAME     STATUS   ROLES           AGE     VERSION
node-1   Ready    control-plane   9m20s   v1.37.0
```

本次节点 Allocatable CPU 为 8 核，未设置控制平面 `NoSchedule` 污点。其他规格的环境需要相应调整示例中的 CPU requests。

#### 开启 Gang Scheduling

Gang Scheduling 在 1.37 中进入 Beta，但默认没有开启。需要在控制平面组件中打开 `GenericWorkload`，同时启用对应的 API。

先备份静态 Pod 配置:

```bash
mkdir -p /root/k8s-137-manifests-backup
cp -a /etc/kubernetes/manifests/*.yaml /root/k8s-137-manifests-backup/
```

编辑 `/etc/kubernetes/manifests/` 下的三个文件，在各自的 `spec.containers[].command` 参数列表中增加：

| 文件 | 增加的参数 |
| --- | --- |
| `kube-apiserver.yaml` | `--feature-gates=GenericWorkload=true`、`--runtime-config=scheduling.k8s.io/v1beta1=true` |
| `kube-controller-manager.yaml` | `--feature-gates=GenericWorkload=true` |
| `kube-scheduler.yaml` | `--feature-gates=GenericWorkload=true` |

例如，kube-apiserver 中的配置如下，其他已有参数保持原样：

```yaml
spec:
  containers:
  - command:
    - kube-apiserver
    - --feature-gates=GenericWorkload=true
    - --runtime-config=scheduling.k8s.io/v1beta1=true
    # 其他已有参数省略
```
保存后 kubelet 会自动重建对应的静态 Pod。等待控制平面组件恢复为 `1/1 Running`，再次确认实际运行进程的启动参数：

```bash
kubectl get pods -n kube-system -l tier=control-plane
ps -ef | grep -E '[k]ube-(apiserver|controller-manager|scheduler)'
```

下面只保留进程名和相关参数，其余输出省略：

```text
kube-apiserver --feature-gates=GenericWorkload=true --runtime-config=scheduling.k8s.io/v1beta1=true ...
kube-controller-manager --feature-gates=GenericWorkload=true ...
kube-scheduler --feature-gates=GenericWorkload=true ...
```

三个组件的实际运行进程都带有 `--feature-gates=GenericWorkload=true`，说明开关已在运行的组件中启用。kube-apiserver 还应带有 `--runtime-config=scheduling.k8s.io/v1beta1=true`。

然后确认对应的 API 也已开放：

```bash
kubectl api-resources --api-group=scheduling.k8s.io
```

```text
NAME              SHORTNAMES   APIVERSION                  NAMESPACED   KIND
podgroups                      scheduling.k8s.io/v1beta1   true         PodGroup
priorityclasses   pc           scheduling.k8s.io/v1        false        PriorityClass
workloads                      scheduling.k8s.io/v1beta1   true         Workload
```

资源列表中应能看到 `podgroups` 和 `workloads`，API 版本为 `scheduling.k8s.io/v1beta1`。至此，组件开关和 API 均已确认启用，接下来通过 Demo 验证实际调度效果。

> HPA Scale to Zero 在 1.37 中默认开启，无需额外修改 `HPAScaleToZero`。


### Gang Scheduling

下面通过一个简单示例，对比资源不足时普通调度和 Gang 调度的表现。

![普通调度与 Gang Scheduling 在资源不足时的行为对比](https://img.lixueduan.com/kubernetes/k8s-1.37/gang-scheduling-comparison.jpg)

#### 普通调度：先启动一部分

为了方便测试调度 Pending 的情况，这里先创建一个占位 Pod，这样剩余资源只能创建一个 Pod 了，便于测试：

```bash
kubectl create namespace gang-demo

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: blocker
  namespace: gang-demo
spec:
  containers:
    - name: blocker
      image: busybox:1.37.0
      command: ["sh", "-c", "sleep 3600"]
      resources:
        requests:
          cpu: "2"
EOF

kubectl get pods -n gang-demo
```

确认 blocker 为 `Running` 后，再创建两个普通 Pod：

```bash
for i in 0 1; do
  kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: plain-${i}
  namespace: gang-demo
spec:
  containers:
    - name: worker
      image: busybox:1.37.0
      command: ["sh", "-c", "sleep 3600"]
      resources:
        requests:
          cpu: "3"
EOF
done
```

稍等后再次查看，可以看到一个 Worker 运行，另一个 Pending：
```bash
kubectl get pods -n gang-demo

NAME      READY   STATUS    RESTARTS   AGE
blocker   1/1     Running   0          ...
plain-0   1/1     Running   0          ...
plain-1   0/1     Pending   0          ...
```

普通调度会逐个处理 Pod，能放下的先运行，不会等待其他成员。

#### Gang 调度：资源不足，整组等待

删除刚才的普通 Pod，保留占位 Pod，保证两次实验的资源条件一致：

```bash
kubectl delete pod -n gang-demo plain-0 plain-1
```
创建独立的 PodGroup，`minCount` 要求至少两个成员满足调度条件：

```bash
kubectl apply -f - <<'EOF'
apiVersion: scheduling.k8s.io/v1beta1
kind: PodGroup
metadata:
  name: workers
  namespace: gang-demo
spec:
  schedulingPolicy:
    gang:
      minCount: 2
EOF
```

再创建两个 Pod，通过 `schedulingGroup.podGroupName` 加入这个组。其他配置与普通 Pod 一致：

```bash
for i in 0 1; do
  kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: gang-${i}
  namespace: gang-demo
spec:
  schedulingGroup:
    podGroupName: workers
  containers:
    - name: worker
      image: busybox:1.37.0
      command: ["sh", "-c", "sleep 3600"]
      resources:
        requests:
          cpu: "3"
EOF
done
```

稍等一会儿再次查看，两个成员都保持 Pending，且都没有绑定节点：

```bash
kubectl get pods -n gang-demo \
  -o custom-columns=NAME:.metadata.name,PHASE:.status.phase,NODE:.spec.nodeName

NAME      PHASE     NODE
blocker   Running   node-1
gang-0    Pending   <none>
gang-1    Pending   <none>
```

这次成员已经齐全，等待的原因是资源不足。查看 PodGroup 的状态和事件：

```bash
kubectl describe podgroup workers -n gang-demo

Type:    PodGroupInitiallyScheduled
Status:  False
Reason:  Unschedulable
```

PodGroup 明确报告无法调度。即使节点还能容纳一个 Worker，调度器也不会先调度，因为无法满足 `minCount: 2`，这也正是 Gang 调度的语义。

#### 释放资源，整组调度

删除占位 Pod，再查看两个成员的状态：

```bash
kubectl delete pod blocker -n gang-demo

kubectl get pods -n gang-demo
```

稍等后，两个成员都变为 Running：

```text
NAME     READY   STATUS    RESTARTS   AGE
gang-0   1/1     Running   0          ...
gang-1   1/1     Running   0          ...
```

两次实验的区别就很直观了：**普通调度允许部分 Pod 先运行；Gang 调度在无法满足最小成员数时整组等待，资源满足后才推进调度。** 这里保证的是初始调度门槛，容器启动仍有先后；成员后续被删除或驱逐时，也不保证运行数量始终满足 `minCount`。

实验结束后清理：

```bash
kubectl delete namespace gang-demo
```

### HPA Scale to Zero

本次 HPA 实验已在 `hpa-zero` 命名空间预先部署 Redis、Redis Exporter、Prometheus 和 Prometheus Adapter。下面展示关键配置和运行结果，省略基础组件的安装过程。

#### 获取队列指标

简单起见，我们用 Redis List 来保存任务，重点看队列长度怎样传给 HPA。

![HPA Scale to Zero 指标链路与零副本唤醒过程](https://img.lixueduan.com/kubernetes/k8s-1.37/hpa-scale-to-zero-flow.jpg)

**先把队列长度暴露为 Prometheus 指标。** Redis Exporter 通过 `--script` 加载下面的 Lua 脚本，读取 `jobs` 队列长度：

```lua
return {"queue_depth", tostring(redis.call("LLEN", "jobs"))}
```

这里用 `LLEN` 直接读取长度，空队列返回 0，避免 List 清空、key 消失后指标也跟着消失。

Prometheus 采集 Exporter，并通过采集配置和 relabel 补上命名空间、队列标签。采集后的指标类似这样，其他标签省略：

```text
redis_script_values{key="queue_depth",namespace="hpa-zero",queue="jobs"} 40
```

**再把指标提供给 HPA。** Prometheus Adapter 将这条时间序列映射为 External Metric `queue_depth`，对应的 Helm Chart values 配置片段如下：

```yaml
rules:
  default: false
  external:
    - seriesQuery: 'redis_script_values{key="queue_depth",namespace!=""}'
      resources:
        overrides:
          namespace: {resource: namespace}
      name:
        matches: ^redis_script_values$
        as: queue_depth
      metricsQuery: >-
        sum(<<.Series>>{key="queue_depth",<<.LabelMatchers>>}
          and on(job,instance) (up{job="redis"} == 1)
          and on(job,instance) (redis_up == 1)) by (namespace,queue)
```

其中 `namespace` 对应 Kubernetes 命名空间，`queue` 用来选择队列。查询还检查采集状态和 `redis_up`，上游失败时不返回有效负载值，不能把“指标读不到”补成零。

最后确认 Kubernetes 能读到指标：

```bash
kubectl get --raw \
  '/apis/external.metrics.k8s.io/v1beta1/namespaces/hpa-zero/queue_depth?labelSelector=queue%3Djobs'
```

空队列时应返回 `value: "0"`。如果这条指标不可用，HPA 可能报告 `ScalingActive=False / FailedGetExternalMetric`，已经归零的消费者也无法自动唤醒。

#### 配置消费者和 HPA

消费者从 Redis 的 `jobs` 列表取任务，处理后写入 `completed` 列表，方便核对完成数量。直接创建 Deployment 和 HPA，Deployment 初始为 1 个副本：

```bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: queue-worker
  namespace: hpa-zero
spec:
  replicas: 1
  selector:
    matchLabels: {app: queue-worker}
  template:
    metadata:
      labels: {app: queue-worker}
    spec:
      terminationGracePeriodSeconds: 5
      containers:
        - name: worker
          image: redis:7.4.2-alpine
          command: ["sh", "-ec"]
          args:
            - |
              while true; do
                task=$(redis-cli -h redis --raw LPOP jobs)
                if [ -n "$task" ]; then
                  echo "$(date -u +%FT%TZ) processing $task"
                  sleep 2
                  redis-cli -h redis RPUSH completed "$task" >/dev/null
                  echo "$(date -u +%FT%TZ) completed $task"
                else
                  sleep 1
                fi
              done
          resources:
            requests: {cpu: 50m, memory: 32Mi}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: queue-worker
  namespace: hpa-zero
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: queue-worker
  minReplicas: 0
  maxReplicas: 4
  metrics:
    - type: External
      external:
        metric:
          name: queue_depth
          selector:
            matchLabels: {queue: jobs}
        target:
          type: Value
          value: "10"
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 30
EOF
```

这里用 `LPOP` 模拟消费，仅验证伸缩效果。任务出队后，若 Pod 在处理完成前退出，任务可能丢失；实际业务需要消息确认、失败重投和优雅退出，队列长度也不包含正在执行的任务。

本例使用 `Value: 10` 验证从零恢复：指标为 40 时，计算出的期望副本数为 4。这不表示始终按“每 10 个任务一个 Pod”伸缩，非零副本时还会考虑 Ready Pod 数量。按每副本平均积压量设置目标，应使用 `AverageValue`。

为了少等一会儿，把缩容稳定窗口改成了 30 秒，默认是 5 分钟。实际缩容还受指标刷新和 HPA 同步周期影响，不会刚好卡着队列清空后的第 30 秒发生。

#### 从零副本到自动唤醒

先不放任务，看下 HPA：

```bash
kubectl get hpa -n hpa-zero -w
```

另一个终端观察消费者：

```bash
kubectl get deploy,pods -n hpa-zero
```

HPA 会把初始的 1 个消费者缩成 0，等 Pod 全部退出后，查看 Condition：

```bash
kubectl describe hpa queue-worker -n hpa-zero
```

关键状态如下：

```text
Type:    ScaledToZero
Status:  True
Reason:  ScaledToZero
```

`ScaledToZero=True` 表示 HPA 自己将副本缩到了零，后续会继续评估指标。手动将 Deployment 缩到零则保留暂停语义，不等同于这次自动归零。

接下来直接放 40 个任务，不用手动启动消费者：

```bash
kubectl exec -n hpa-zero deploy/redis -- redis-cli EVAL \
  'for i=1,40 do redis.call("RPUSH","jobs","task-"..i) end return redis.call("LLEN","jobs")' 0
```

这次 HPA 把期望副本数从 0 调成了 4，消费者启动后开始处理任务。唤醒之后，`ScaledToZero` 变成 `False`，Reason 是 `NotScaledToZero`。

等消费完成，核对一下队列和完成数量：

```bash
$ kubectl exec -n hpa-zero deploy/redis -- redis-cli LLEN jobs
0
$ kubectl exec -n hpa-zero deploy/redis -- redis-cli LLEN completed
40
```

40 个任务都处理完了。再等缩容稳定窗口过去，消费者重新缩成 0，Pod 全部退出，`ScaledToZero` 也重新变成 `True`。

完整过程就是 **1 → 0 → 4 → 0**：

| 阶段 | 消费者 | 任务状态 |
| --- | --- | --- |
| 空闲 | 自动缩到 0 | 队列为空 |
| 放入 40 个任务 | 起初为 0 | 40 个任务等待处理 |
| HPA 读到新指标 | 扩到 4，随后全部就绪 | 开始消费 |
| 消费完成 | 暂时保留副本 | 队列为 0，完成数为 40 |
| 稳定窗口过后 | 再次自动归零 | 消费者 Pod 全部退出 |

## 4. 使用上的取舍

Gang Scheduling 和 HPA Scale to Zero 这次两个特性，都是把生态里使用较多的能力进一步补进 Kubernetes 原生体系。简单场景有了更直接的选择，而 Volcano 的队列和资源管理、KEDA 的事件源适配、Knative 的请求接入仍各有用途。是否替换现有方案，还是要看实际需要哪些能力。


---

> 作者: [意琦行](https://github.com/lixd)  
> URL: https://www.lixueduan.com/posts/kubernetes/70-k8s-1.37-new-features/  

