
# 在CCE中实现应用高可用部署
在云原生环境中，容器化应用凭借弹性伸缩与敏捷迭代能力支撑业务高效交付，但底层基础设施、调度逻辑、存储依赖及应用生命周期管理中仍存在多重不确定性风险：单一节点或可用区故障、滚动更新策略不当、Pod调度冲突、云硬盘与节点可用区不匹配等问题，均可能引发服务中断或业务不可用。
实现CCE全链路高可用，不能仅依靠多副本部署，需从基础设施层、应用调度层、有状态存储层协同构建防护体系，覆盖以下六大核心维度：
- Pod拓扑分布：通过拓扑分布约束或反亲和策略，建议将工作负载跨可用区、跨节点均匀分布，避免单点故障。
- Pod中断预算：管控自愿中断场景，保障业务最低可用副本数，防止滚动更新或节点驱逐导致服务不可用。
- Pod健康状态管理：配置就绪、存活、启动探针，实现异常自动检测与自愈，确保流量仅转发至健康Pod。
- 优雅生命周期：通过preStop钩子与终止宽限期，确保流量无损下线。
- 节点高可用：通过物理机反亲和与多可用区节点池，实现底层硬件故障隔离。
- 跨AZ存储适配：采用延迟绑定存储类，解决云硬盘跨AZ挂载限制，避免有状态应用调度失败。
 
本文整合应用部署、节点池、存储调度三大实践，以完整方案实现CCE业务端到端高可用。
#### 应用层高可用实践
#### 应用部署
**Pod拓扑分布**
假设集群有4个节点，分布在3个可用区。
- **方案一：使用拓扑分布约束（推荐）**
  使用拓扑分布约束（topologySpreadConstraints）能够精确控制Pod在不同拓扑域（可用区、节点）之间的分布偏差，支持同时设置多个拓扑维度，且不会因其他Pod的干扰导致分布不均。
  配置示例如下：
  当副本数为4且有三个可用区时，调度器会尝试按2,1,1形式尽可能均匀分布（如果某个可用区无节点可用也可能出现2,2,0的形式），优先避免所有Pod挤在同一可用区。
  ```
  ...
  spec:
    replicas: 4
    template:
      spec:
        topologySpreadConstraints:
        # 约束1：在可用区之间均匀分布
        - maxSkew: 1                     # 允许的最大Pod数量差，表示任意两个可用区的Pod数差 ≤ 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule  # 硬约束，无法满足时禁止调度（也可用ScheduleAnyway）
          labelSelector:
            matchLabels:
              app: nginx-ha
        # 约束2：在节点之间均匀分布（可选，进一步打散）
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: nginx-ha
  ...
  ```
  参数说明：
  - maxSkew: 1：任意两个拓扑域之间的Pod数量差不超过1，实现最均匀的分布。
  
  - whenUnsatisfiable: DoNotSchedule：硬约束，若找不到满足条件的节点则Pod保持Pending。
  
  - whenUnsatisfiable: ScheduleAnyway：软约束，调度器会优先尝试均匀分布，实在不行也会调度。
   
- **方案二：使用Pod反亲和性**
  使用Pod反亲和性更灵活，适合复杂逻辑，通过preferredDuringScheduling规则将Pod优先分散到不同可用区的节点上，需要时也可以结合requiredDuringScheduling规则实现硬约束。
  配置示例如下：
  ```
  ...
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80                      # 高权重：优先跨可用区
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app
              operator: In
              values:
              - nginx-ha
          topologyKey: topology.kubernetes.io/zone
      - weight: 20                      # 低权重：其次跨节点
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app
              operator: In
              values:
              - nginx-ha
          topologyKey: kubernetes.io/hostname
  ...
  ```
  
 
**Pod中断预算**
Pod中断预算（PodDisruptionBudget, PDB）是Kubernetes中用于限制自愿性中断的资源对象。其中自愿性中断是指由系统或管理员主动触发的Pod删除操作，例如Deployment的滚动更新、手动删除Pod等。
PDB通过设置 minAvailable（最小可用Pod数）或 maxUnavailable（最大不可用Pod数），确保在执行这些操作时，不会让服务容量低于安全线。
配置示例如下：
```
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-ha-pdb
spec:
  minAvailable: 2        # 至少保持2个Pod可用
  selector:
    matchLabels:
      app: nginx-ha
```
- minAvailable：表示在任何时候，至少要有多少个Pod保持可用（可以是绝对数字，如 2；也可以是百分比，如 50%）。
- maxUnavailable：表示最多允许多少个Pod同时不可用（可以是绝对数字或百分比）。
- selector：通过标签选择器匹配需要受此PDB约束的Pod集合。
![](https://support.huaweicloud.com/bestpractice-cce/public_sys-resources/notice_3.0-zh-cn.png)
minAvailable和maxUnavailable是互斥的，只能设置其中一个。
**Pod健康状态管理**
探针是Kubelet定期在容器上执行的诊断检查，根据结果判断Pod的健康状态并采取相应动作。Kubernetes提供三种探针：
- 就绪探针（readinessProbe）：判断Pod是否准备好接收流量。失败时，Pod会被从Service的Endpoints中移除，但不会重启容器。常用于滚动更新时控制新版本的上线节奏。
- 存活探针（livenessProbe）：判断Pod是否处于健康状态。失败时，Kubelet会重启容器。用于修复卡死、死锁等应用级故障。
- 启动探针（startupProbe）：判断容器内的应用是否已经启动完成。在启动探针成功之前，存活和就绪探针会被禁用。适用于启动时间较长的应用（如Java Spring Boot），防止它们在启动期间被存活探针误杀。
配置示例如下：
```
...
containers:
- name: nginx
  image: nginx:alpine
  ports:
  - containerPort: 80
  # 就绪探针：准备就绪才接收流量
  readinessProbe:
    httpGet:
      path: /
      port: 80
    initialDelaySeconds: 5 # 延迟多少秒才开始探测
    periodSeconds: 10 # 每隔多少秒执行一次探测
    failureThreshold: 3 # 连续失败多少次才判定为失败
  # 存活探针：异常则重启容器
  livenessProbe:
    httpGet:
      path: /
      port: 80
    initialDelaySeconds: 15
    periodSeconds: 20
    failureThreshold: 3  # 可适当放宽（如3-5次），避免因短暂波动（如高负载、GC停顿）导致容器频繁重启
  # 启动探针：保护启动慢的应用（如Java）
  startupProbe: 
    httpGet:
      path: /
      port: 80
    failureThreshold: 30
    periodSeconds: 10
...
```
**优雅生命周期**
当Pod被删除（如滚动更新、节点排空、缩容）时，Kubernetes会向Pod中的容器发送SIGTERM信号。如果容器立即退出，正在处理中的请求可能会中断，导致客户端收到错误响应（如Connection Refused）。配置preStop与优雅终止，可以等Pod先摘除流量，等容器完成所有清理工作并退出。
配置示例如下：
```
...
containers:
- name: nginx
  lifecycle:
    preStop:
      exec:
        command: ["/bin/sh", "-c", "sleep 30"] # 等待30秒，给ELB摘除后端的时间
terminationGracePeriodSeconds: 45   # 总宽限期45秒，必须大于 preStop sleep 时间
...
```
**完整YAML示例**
此示例副本数为4，整合了拓扑分布约束（推荐方案）、PDB、探针和优雅终止，可应对单节点故障和单可用区中断。
```
# 1. PodDisruptionBudget: 保证自愿中断时至少有2个Pod运行
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-ha-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: nginx-ha
---
# 2. Deployment: 整合了拓扑分布约束、探针、优雅终止
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ha
spec:
  replicas: 4
  selector:
    matchLabels:
      app: nginx-ha
  template:
    metadata:
      labels:
        app: nginx-ha
    spec:
      terminationGracePeriodSeconds: 45
      # 使用拓扑分布约束，均匀分布到可用区和节点
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: nginx-ha
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: nginx-ha
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        # 优雅生命周期
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 30"]
        # 探针配置
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 15
          periodSeconds: 20
        resources:
          requests:
            cpu: 250m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
      imagePullSecrets:
      - name: default-secret
```
#### 验证与测试
1. 验证Pod分布：
   ```
   kubectl get pod -owide -l app=nginx-ha
   ```
   观察输出中的NODE字段，应分布在多个节点和可用区。
   ```
   NAME                        READY   STATUS    RESTARTS   AGE   IP           NODE              ZONE
   nginx-ha-5b9f6c8d7c-2xk9l   1/1     Running   0          2m    10.0.0.132   192.168.5.112     az1
   nginx-ha-5b9f6c8d7c-4m7nq   1/1     Running   0          2m    10.0.1.133   192.168.5.252     az2
   nginx-ha-5b9f6c8d7c-7p3rd   1/1     Running   0          2m    10.0.0.9     192.168.5.179     az1
   nginx-ha-5b9f6c8d7c-9s5tv   1/1     Running   0          2m    10.0.2.45    192.168.5.8       az3
   ```
   
2. 验证PDB：
   ```
   kubectl get pdb nginx-ha-pdb
   ```
   输出示例：
   ```
   NAME            MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
   nginx-ha-pdb    2               N/A               2                     5m
   ```
   当执行kubectl drain或触发滚动更新时，系统会确保可用Pod数不低于2，若尝试中断第3个Pod，操作会被阻塞。
   
3. 模拟节点故障：排空一个节点，观察Pod是否漂移，且总不可用Pod数不超过PDB限制。 例如，封锁并排空一个节点（例如 192.168.5.112，其上运行了2个Pod）。
   ```
   # 1. 封锁节点（禁止新Pod调度）
   kubectl cordon 192.168.5.112
   # 2. 排空节点（驱逐Pod，会受PDB限制）
   kubectl drain 192.168.5.112 --ignore-daemonsets --delete-emptydir-data
   ```
   重新查看Pod分布：
   ```
   kubectl get pod -owide -l app=nginx-ha
   ```
   输出示例：
   ```
   NAME                        READY   STATUS    RESTARTS   AGE   IP           NODE              ZONE
   nginx-ha-5b9f6c8d7c-4m7nq   1/1     Running   0          5m    10.0.1.133   192.168.5.252     az2
   nginx-ha-5b9f6c8d7c-9s5tv   1/1     Running   0          5m    10.0.2.45    192.168.5.8       az3
   nginx-ha-5b9f6c8d7c-b4x8w   1/1     Running   0          30s   10.0.1.142   192.168.5.252     az2  # 新调度
   nginx-ha-5b9f6c8d7c-d6y9z   1/1     Running   0          30s   10.0.2.56    192.168.5.8       az3  # 新调度
   ```
   
4. 模拟滚动更新：触发滚动更新（例如修改镜像版本），观察旧Pod终止前是否等待了preStop定义的时间。
   ```
   kubectl set image deployment/nginx-ha nginx=nginx:1.25.0
   ```
   观察旧Pod终止前是否等待了preStop定义的时间:
   ```
   kubectl get pod -w -l app=nginx-ha
   ```
   预期如下：
   ```
   NAME                         READY   STATUS        RESTARTS   AGE
   nginx-ha-5b9f6c8d7c-2xk9l   1/1     Running       0          10m
   nginx-ha-5b9f6c8d7c-4m7nq   1/1     Running       0          10m
   nginx-ha-5b9f6c8d7c-7p3rd   1/1     Running       0          10m
   nginx-ha-5b9f6c8d7c-9s5tv   1/1     Running       0          10m
   nginx-ha-7d8f9c6b5d-2abc   0/1     Pending       0          1s
   nginx-ha-7d8f9c6b5d-2abc   0/1     Pending       0          1s
   nginx-ha-7d8f9c6b5d-2abc   0/1     ContainerCreating   0          1s
   nginx-ha-7d8f9c6b5d-2abc   1/1     Running       0          5s        # 新Pod就绪
   nginx-ha-5b9f6c8d7c-2xk9l   1/1     Terminating   0          10m30s    # 旧Pod开始终止（此时会执行preStop sleep 30s）
   nginx-ha-5b9f6c8d7c-2xk9l   0/1     Terminating   0          11m       # 30秒后容器退出
   nginx-ha-5b9f6c8d7c-2xk9l   0/1     Terminating   0          11m       # 删除完成
   ```
   新Pod启动并变为Running后，旧Pod才开始进入Terminating状态，且旧Pod在Terminating状态下至少停留了30秒（对应preStop sleep 30s），为负载均衡器摘除后端和完成存量请求提供了缓冲时间。
   
5. 模拟应用故障：例如手动关闭应用内部端口，观察就绪探针是否将Pod摘除，存活探针是否重启容器。
   假设Nginx的默认健康检查路径为/，进入容器并停止Nginx服务来模拟故障：
   ```
   kubectl exec -it nginx-ha-5b9f6c8d7c-2xk9l -- /bin/sh
   # 在容器内执行
   kill -STOP 1   # 暂停主进程，使探针失败
   exit
   ```
   观察Pod状态变化：
   ```
   kubectl get pod -w -l app=nginx-ha
   ```
   预期如下：
   ```
   NAME                        READY   STATUS    RESTARTS   AGE
   nginx-ha-5b9f6c8d7c-2xk9l   1/1     Running   0          15m
   nginx-ha-5b9f6c8d7c-2xk9l   0/1     Running   0          15m30s    # 就绪探针失败，READY变为0/1（摘除流量）
   nginx-ha-5b9f6c8d7c-2xk9l   0/1     Running   1          16m       # 存活探针失败，容器重启（RESTARTS增为1）
   nginx-ha-5b9f6c8d7c-2xk9l   1/1     Running   1          16m30s    # 重启后就绪探针成功，READY恢复为1/1
   ```
   
 
#### 节点池高可用实践
在使用节点池部署业务时，为提高可用性和容灾能力，您可以从以下两个方面进行优化：
- **节点层面**：利用节点池的云服务器组和可用区策略实现物理隔离。
- **Pod层面**：通过亲和性调度和拓扑分布约束确保业务副本均匀分布在不同节点上。
更多信息，请参见[节点池高可用最佳实践](https://support.huaweicloud.com/bestpractice-cce/cce_bestpractice_10054.html)。
#### 节点层面：实现物理机与可用区反亲和
目标是完成底层基础设施隔离，规避单机、单可用区故障风险。
- **物理机反亲和（云服务器组）**
  在ECS控制台创建反亲和性云服务器组，在CCE节点池高级设置中绑定该服务器组。节点池扩容生成的节点会被调度到不同物理主机，规避单物理机故障影响全部节点。
  
- **可用区反亲和（多AZ节点池）**
  创建节点池时勾选多个可用区，节点分布在不同可用区。当某一个可用区整体故障，其余可用区节点仍可以保障业务持续运行，是跨AZ高可用架构的基础。
  
 
#### Pod层面：调度策略实现业务副本精细打散
在确保底层基础设施已实现高可用隔离的基础上，充分利用Kubernetes原生调度能力，通过精细化的调度策略将业务Pod副本均匀分散部署，实现从底层硬件到上层业务的完整高可用。
- **Pod反亲和调度**
  使用podAntiAffinity硬约束，将同一业务的多个副本分散到不同的节点上。确保单节点最多运行一个业务Pod副本，有效避免业务副本全部集中在一个节点上。
  
- **Pod拓扑分布约束**
  相比Pod反亲和，拓扑分布约束能力更强，支持配置多维度的Pod分布均衡度。
  - 关键参数：
    - topologyKey：指定拓扑域，支持节点主机名、可用区。
    
    - maxSkew：允许Pod分布最大差值。
    
    - whenUnsatisfiable：条件不满足时策略，DoNotSchedule拒绝调度，ScheduleAnyway尽力调度。
     
  
  - 通过配置两条约束，让Kubernetes在调度Pod时，同时保障Pod在节点、可用区双维度均匀分布。
   
 
#### 有状态存储实践
在CCE多可用区部署有状态业务时，EVS云硬盘无法跨可用区挂载，普通存储类易因PV与Pod可用区不匹配导致调度失败。可使用csi-disk-topology存储类，依托WaitForFirstConsumer延迟绑定机制，先调度Pod再创建同可用区云硬盘，解决跨AZ调度冲突问题。
更多信息，请参见[使用延迟绑定的云硬盘（csi-disk-topology）实现跨AZ调度](https://support.huaweicloud.com/bestpractice-cce/cce_bestpractice_00284.html)。
#### 业务问题背景
EVS云硬盘不支持跨可用区挂载，即AZ1创建的云盘无法挂载至AZ2节点。
- 普通存储类csi-disk采用Immediate绑定模式，创建PVC时会立刻创建PV并生成底层云硬盘。
- 在多AZ集群部署StatefulSet等有状态业务时，若云硬盘被创建在某一个可用区，但Pod被调度到其他可用区节点，就会触发volume node affinity conflict，导致Pod进入Pending调度失败状态。 例如，一个4副本的StatefulSet 部署在3个节点分属3个AZ的集群中。由于云硬盘与节点可用区绑定，其中1个Pod的存储卷落在了与自身调度目标不一致的AZ，该Pod因存储与节点可用区不匹配而调度失败。
  
 
#### csi‑disk‑topology延迟绑定存储类
- 绑定模式：WaitForFirstConsumer（等待第一个消费 Pod），创建PVC时不会立刻生成PV与云硬盘，PVC保持Pending状态。
- 执行流程：先完成Pod调度，识别Pod运行节点的可用区，再在该可用区创建云硬盘与PV，完成PVC‑PV绑定，保证云硬盘和Pod节点处于同一个 AZ，能够正常挂载。
 
#### csi‑disk与csi‑disk‑topology关键对比
| 存储类               | VolumeBindingMode    | 行为特点            | 使用场景                    |
|:---|:---|:---|:---|
| csi-disk          | Immediate            | 创建PVC即创建PV和云硬盘  | 单AZ集群                   |
| csi-disk-topology | WaitForFirstConsumer | 等待Pod调度完成后才创建存储 | 多AZ集群有状态应用（StatefulSet） |
   
两种存储类对比：
- csi-disk：PVC创建后直接Bound，存储立即分配。
- csi-disk-topology：PVC创建后状态为Pending，事件提示waiting for first consumer to be created before binding。只有当Pod调度到具体节点后，才会根据节点所在可用区创建对应存储，确保存储与Pod在同一AZ。
 
#### 验证案例
问题现象：使用csi-disk的StatefulSet，PVC全部绑定成功，但部分Pod因存储所在AZ与调度节点AZ不匹配，处于Pending状态。
解决方案：将volumeClaimTemplates中的storageClassName修改为csi-disk-topology，重新部署后，所有Pod和PVC正常创建，云硬盘跟随Pod所在可用区生成，调度冲突消除。
多AZ集群中运行使用EVS云硬盘的有状态业务，应优先选用csi-disk-topology，规避云硬盘可用区与Pod节点可用区不匹配的调度故障。
#### 常见问题
#### PDB与滚动更新参数maxSurge和maxUnavailable冲突吗？
不冲突。Deployment的maxSurge和maxUnavailable与PDB共同作用，Kubernetes会取更严格的限制。
#### preStop应该设置多久？
取决于业务处理完现有请求的平均+最大时间。通常15-30秒，再配合terminationGracePeriodSeconds多留出5-10秒余量。
#### 反亲和性使用required还是preferred？
若集群中可供调度的节点数充足，且副本数 ≤ 节点/可用区数，可以使用required强制打散。否则建议用preferred，避免Pod因无法满足硬约束而Pending。
#### 拓扑分布约束中maxSkew应设为多少？
maxSkew设置为1可实现最严格的均匀分布。如果节点数不是副本数的整数倍，设为1仍能保证最大差为1。
#### Pod容忍驱逐时间是什么？如何设置？
Pod容忍驱逐时间（tolerationSeconds）仅对NoExecute污点生效，表示节点被打上故障污点后，Pod还能在该节点继续运行的等待秒数，超时后才会被驱逐重建。CCE集群默认对节点失联、未就绪场景设置为300s，在创建工作负载时可自定义时间。
![](https://support.huaweicloud.com/bestpractice-cce/zh-cn_image_0000002723253939.png "点击放大")
#### 如何通过配置Resource Requests \& Limits来保障核心业务Pod稳定性？
核心业务建议将requests与limits设为一致（Guaranteed QoS），避免在节点内存紧张时被底层OOM Killer优先终止。
核心业务Pod建议采用Guaranteed QoS（服务质量） 模式来保障稳定性。具体做法是将requests.memory与limits.memory设为相等值，同时将requests.cpu与limits.cpu也设为相等值。
