跳到主要内容

10 篇博文 含有标签「Kubernetes」

Kubernetes 容器编排平台

查看所有标签

Kubernetes 远程开发与本地代理 Pod 流量全攻略

· 阅读需 16 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

前言

在 Kubernetes 环境中进行开发和调试是一个常见的挑战。传统的开发模式通常是在本地构建和测试,然后部署到集群中进行验证。然而,随着微服务架构的普及和依赖服务的增多,这种模式变得越来越低效。

本文将深入探讨在 Kubernetes 中进行远程开发的多种方法,包括端口转发、本地代理、Telepresence、DevSpace 等工具的使用,帮助你实现更高效的开发和调试流程。

一、远程开发的挑战与需求

1.1 传统开发模式的痛点

在 Kubernetes 环境中,传统的开发模式面临以下挑战:

  1. 环境差异:本地开发环境与 Kubernetes 集群环境存在差异,导致 "在本地工作,在集群中失败" 的问题
  2. 依赖服务:微服务依赖多个其他服务,本地难以模拟完整的依赖环境
  3. 网络访问:本地应用无法直接访问集群内部的服务和资源
  4. 调试困难:在集群中运行的应用难以进行实时调试
  5. 部署周期长:每次代码变更都需要构建、推送、部署到集群,周期长,效率低

1.2 远程开发的核心需求

针对上述挑战,远程开发需要满足以下核心需求:

  1. 快速迭代:代码变更后能够快速验证,无需完整的构建和部署流程
  2. 环境一致性:开发环境与生产环境保持一致
  3. 网络可达:本地能够访问集群内部服务,集群内部也能访问本地服务
  4. 实时调试:能够对运行在集群中的应用进行实时调试
  5. 资源隔离:开发环境不影响其他环境的正常运行

二、基础方法:kubectl 端口转发

2.1 端口转发的基本原理

kubectl port-forward 是 Kubernetes 提供的最基础的端口转发工具,它可以将本地端口与 Pod 中的端口建立映射,实现本地与 Pod 之间的双向通信。

工作原理

工作流程

  1. 客户端(kubectl)与 API Server 建立连接
  2. API Server 与目标 Pod 所在的 Node 建立连接
  3. 客户端、API Server、Pod 之间建立一个隧道
  4. 本地端口的流量通过隧道转发到 Pod 中的端口

2.2 基本使用方法

2.2.1 转发到单个 Pod

# 基本语法
kubectl port-forward <pod-name> <本地端口>:<Pod端口> -n <命名空间>

# 示例:将本地 8080 端口转发到 Pod 的 80 端口
kubectl port-forward my-pod 8080:80 -n default

# 后台运行
kubectl port-forward my-pod 8080:80 -n default --address=0.0.0.0 &

2.2.2 转发到 Deployment/Service

# 转发到 Deployment(会自动选择一个 Pod)
kubectl port-forward deployment/my-deployment 8080:80 -n default

# 转发到 Service
kubectl port-forward service/my-service 8080:80 -n default

# 转发到 StatefulSet
kubectl port-forward statefulset/my-statefulset 8080:80 -n default

2.2.3 转发多个端口

# 同时转发多个端口
kubectl port-forward my-pod 8080:80 9090:90 -n default

2.3 高级选项

选项描述示例
--address指定监听地址,默认为 127.0.0.1--address=0.0.0.0
--pod-running-timeout等待 Pod 运行的超时时间--pod-running-timeout=1m
--namespace-n指定命名空间-n default

2.4 适用场景与限制

适用场景

  • 临时访问 Pod 中的服务进行调试
  • 访问集群内部的数据库、缓存等服务
  • 简单的本地开发和测试

限制

  • 仅支持 TCP 协议
  • 性能有限,不适合高流量场景
  • 只能转发到单个 Pod,不支持负载均衡
  • 需要保持 kubectl 命令运行,终端关闭后转发停止

三、本地代理:kubectl proxy

3.1 本地代理的基本原理

kubectl proxy 用于启动一个本地代理服务器,该服务器将请求转发到 Kubernetes API Server,同时提供认证和授权。

工作原理

工作流程

  1. 本地启动一个代理服务器(默认端口 8001)
  2. 本地客户端向代理服务器发送请求
  3. 代理服务器将请求转发到 Kubernetes API Server
  4. 代理服务器处理认证和授权
  5. API Server 的响应通过代理服务器返回给客户端

3.2 基本使用方法

# 基本语法
kubectl proxy [--port=PORT] [--address=ADDRESS] [--accept-hosts=PATTERN]

# 示例:启动代理服务器,监听 8001 端口
kubectl proxy

# 自定义端口和地址
kubectl proxy --port=8080 --address=0.0.0.0

# 后台运行
kubectl proxy --port=8080 &

3.3 访问 Kubernetes API

启动代理后,可以通过以下方式访问 Kubernetes API:

# 查看所有 Pod
curl http://localhost:8001/api/v1/pods

# 查看特定命名空间的 Pod
curl http://localhost:8001/api/v1/namespaces/default/pods

# 查看 Service
curl http://localhost:8001/api/v1/services

3.4 适用场景与限制

适用场景

  • 本地访问 Kubernetes API Server
  • 开发与 Kubernetes API 交互的应用
  • 访问 Dashboard 等 Web 界面

限制

  • 仅代理 Kubernetes API 请求,不代理 Pod 之间的通信
  • 不能用于访问集群内部的服务(如数据库、应用服务)

四、高级工具:Telepresence

4.1 Telepresence 简介

Telepresence 是一种更高级的 Kubernetes 远程开发工具,它可以将本地开发环境与 Kubernetes 集群无缝集成,实现本地代码与集群服务的实时交互。最新版本为 2.27(截至 2026 年 4 月)。

核心特性

  • 流量拦截:将集群中特定服务的流量重定向到本地
  • 双向网络:本地服务可以访问集群服务,集群服务也可以访问本地服务
  • 环境变量和卷同步:自动同步集群中的环境变量和卷到本地
  • 快速迭代:代码变更后无需重新部署,立即生效
  • 支持 TCP/UDP 协议:适用于各种网络服务
  • 服务网格集成:与 Istio、Linkerd 等服务网格良好协作

工作架构

链路流程

  1. 本地运行 telepresence connect 命令,与集群建立连接
  2. Traffic Manager 在集群中部署,建立双向隧道
  3. 本地运行 telepresence intercept 命令,指定要拦截的服务
  4. Traffic Agent 被注入到目标服务的 Pod 中
  5. 集群中发往目标服务的流量被重定向到本地
  6. 本地服务处理请求,并可以访问集群中的其他服务
  7. 本地服务的响应通过隧道返回给集群

操作后处理

  1. 开发完成后,运行 telepresence leave <service-name> 停止拦截
  2. 运行 telepresence quit 断开与集群的连接
  3. 清理集群中的 Traffic Manager(如果不再需要):kubectl delete -n ambassador deployment traffic-manager

4.2 安装 Telepresence

Telepresence 最新版本为 2.27(截至 2026 年 4 月),支持 macOS、Linux 和 Windows 平台。

# macOS
brew install datawire/blackbird/telepresence

# Linux
sudo curl -fL https://app.getambassador.io/download/tel2/linux/amd64/latest/telepresence -o /usr/local/bin/telepresence
sudo chmod a+x /usr/local/bin/telepresence

# Windows
# 下载安装包并运行安装程序:https://app.getambassador.io/download/tel2/windows/amd64/latest/telepresence.exe

4.3 基本使用方法

4.3.1 连接到集群

# 连接到集群
telepresence connect

# 验证连接
curl -s https://kubernetes.default.svc.cluster.local

4.3.2 拦截流量

# 基本语法:拦截指定服务的流量到本地端口
telepresence intercept <service-name> --port <本地端口>:<服务端口> --namespace <命名空间>

# 示例:拦截名为 "my-service" 的服务,将流量重定向到本地 8080 端口
telepresence intercept my-service --port 8080:80 --namespace default

# 查看当前的拦截
telepresence list

# 停止拦截
telepresence leave my-service

4.3.3 运行本地服务

# 在本地启动服务,监听 8080 端口
# 例如:Node.js 应用
npm run dev

# 或 Python 应用
python app.py

4.4 高级配置

4.4.1 环境变量和卷同步

# 同步环境变量和卷
telepresence intercept my-service --port 8080:80 --env-file ./env.txt --mount /app/data

# 使用同步的环境变量
source ./env.txt

4.4.2 流量过滤

# 只拦截特定用户的流量(基于 Header)
telepresence intercept my-service --port 8080:80 --headers "X-User: dev"

# 只拦截特定路径的流量
telepresence intercept my-service --port 8080:80 --ingress "path=/api/*"

4.5 适用场景与限制

适用场景

  • 微服务开发,需要与集群中其他服务交互
  • 复杂应用的调试和测试
  • 快速迭代开发,无需频繁部署

限制

  • 安装和配置相对复杂
  • 对网络环境有一定要求
  • 可能会影响集群中其他服务的正常运行(建议在开发环境使用)

五、开发工具:DevSpace

5.1 DevSpace 简介

DevSpace 是一个专为 Kubernetes 开发设计的工具,它提供了一套完整的开发工作流,包括代码同步、热重载、自动端口转发等功能。截至 2026 年 4 月,DevSpace 仍在积极维护和更新中。

核心特性

  • 代码同步:自动将本地代码同步到集群中的容器
  • 热重载:代码变更后自动重启应用
  • 自动端口转发:自动设置端口转发,无需手动执行
  • 开发环境管理:简化开发环境的创建和管理
  • 多环境支持:支持开发、测试、生产等多个环境
  • Helm 集成:内置 Helm 支持,简化部署管理
  • 集群内开发:支持直接在集群中进行开发

工作架构

链路流程

  1. 本地运行 devspace init 初始化项目,生成 devspace.yaml 配置文件
  2. 配置 devspace.yaml,定义镜像构建、部署和开发设置
  3. 本地运行 devspace dev 启动开发模式
  4. DevSpace 自动构建镜像、部署应用到集群
  5. 建立本地与集群之间的代码同步
  6. 设置自动端口转发,方便本地访问集群服务
  7. 监听本地代码变更,自动同步并重启应用
  8. 提供实时日志和调试支持

操作后处理

  1. 开发完成后,按 Ctrl+C 退出开发模式
  2. 运行 devspace deploy 将应用部署到测试或生产环境
  3. 运行 devspace purge 清理集群中的开发资源
  4. 可以使用 devspace logs 查看应用日志
  5. 可以使用 devspace enter 进入容器进行调试

5.2 安装 DevSpace

DevSpace 最新版本(截至 2026 年 4 月)支持 macOS、Linux 和 Windows 平台,并且积极维护中。

# macOS/Linux
curl -fsSL https://get.devspace.sh | bash

# Windows
# 下载安装包并运行安装程序:https://github.com/devspace-sh/devspace/releases/latest

5.3 基本使用方法

5.3.1 初始化项目

# 在项目目录中初始化
cd my-project
devspace init

# 选择 Kubernetes 上下文
devspace use context my-cluster

# 选择命名空间
devspace use namespace default

5.3.2 开发模式

# 启动开发模式
devspace dev

# 开发模式会自动:
# 1. 构建镜像
# 2. 部署应用
# 3. 同步代码
# 4. 设置端口转发
# 5. 启动热重载

5.3.3 配置文件

DevSpace 使用 devspace.yaml 文件进行配置:

version: v2beta1
name: my-project

dev:
my-app:
imageSelector: my-app
devContainer:
command: ["npm", "run", "dev"]
sync:
- path: ./:/app
ports:
- port: 3000:3000
open:
- url: http://localhost:3000

deployments:
my-app:
helm:
chart:
name: devspace
repo: https://charts.devspace.sh
values:
containers:
- image: my-app
ports:
- containerPort: 3000

5.4 高级功能

5.4.1 多环境配置

profiles:
dev:
patches:
- op: replace
path: deployments.my-app.helm.values.replicas
value: 1
production:
patches:
- op: replace
path: deployments.my-app.helm.values.replicas
value: 3
- op: replace
path: deployments.my-app.helm.values.resources
value:
limits:
cpu: 1
memory: 1Gi

5.4.2 自定义命令

commands:
build:
command: devspace build
deploy:
command: devspace deploy
test:
command: devspace run test
clean:
command: devspace purge

5.5 适用场景与限制

适用场景

  • 全栈应用开发
  • 微服务开发
  • 团队协作开发
  • 需要快速迭代的项目

限制

  • 配置相对复杂,需要一定的学习成本
  • 对网络环境有一定要求
  • 可能会占用较多的本地资源

六、其他工具对比

工具类型核心功能适用场景优势劣势最新版本
kubectl port-forward基础工具端口转发临时访问、简单调试简单易用,无需额外安装性能有限,只支持 TCP随 Kubernetes 版本更新
kubectl proxy基础工具API 代理访问 Kubernetes API简单易用,提供认证只代理 API 请求随 Kubernetes 版本更新
Telepresence高级工具流量拦截、双向网络微服务开发、复杂调试功能强大,支持双向网络,支持 TCP/UDP,服务网格集成配置复杂,可能影响集群2.27 (2026 年 4 月)
DevSpace开发工具代码同步、热重载全栈开发、团队协作完整工作流,自动同步,Helm 集成,集群内开发配置复杂,资源占用高最新版本 (2026 年 4 月)
Skaffold开发工具持续部署CI/CD 集成与 CI/CD 集成好,支持多环境主要关注部署,开发功能有限最新版本
Garden开发工具环境管理多环境开发环境管理强大,支持多团队协作学习曲线较陡,配置复杂最新版本

七、最佳实践

7.1 网络配置最佳实践

  1. 使用命名空间隔离:为每个开发人员创建独立的命名空间,避免相互影响
  2. 合理配置网络策略:允许开发环境与其他服务的必要通信,同时限制不必要的访问
  3. 使用 Ingress 进行外部访问:为开发环境配置 Ingress,方便外部访问
  4. 监控网络流量:使用网络监控工具,及时发现和解决网络问题

7.2 安全最佳实践

  1. 使用 RBAC 控制访问:为开发人员配置最小权限的 RBAC 规则
  2. 避免使用生产集群:使用专门的开发/测试集群进行开发
  3. 加密敏感数据:使用 Kubernetes Secrets 存储敏感信息
  4. 定期清理资源:开发完成后及时清理临时资源,避免资源浪费

7.3 性能优化最佳实践

  1. 使用本地缓存:缓存依赖包和构建产物,加速构建过程
  2. 合理配置资源限制:为开发环境的 Pod 设置合理的资源限制
  3. 使用镜像分层:优化 Dockerfile,使用镜像分层,加速镜像构建和推送
  4. 选择合适的工具:根据具体需求选择合适的开发工具,避免过度使用复杂工具

八、故障排查

8.1 端口转发故障

问题:端口转发失败或连接被拒绝

排查步骤

  1. 检查 Pod 是否正常运行:kubectl get pod <pod-name>
  2. 检查 Pod 中的服务是否监听正确的端口:kubectl exec <pod-name> -- netstat -tuln
  3. 检查网络策略是否允许访问:kubectl get networkpolicy
  4. 检查防火墙设置:确保本地防火墙允许端口访问
  5. 尝试使用不同的本地端口:避免端口冲突

8.2 Telepresence 连接问题

问题:Telepresence 连接失败或拦截不工作

排查步骤

  1. 检查 Telepresence 版本:telepresence version
  2. 检查集群连接:kubectl cluster-info
  3. 检查服务是否存在:kubectl get service <service-name>
  4. 查看 Telepresence 日志:telepresence loglevel debug
  5. 尝试重新连接:telepresence quit && telepresence connect

8.3 网络连通性问题

问题:本地服务无法访问集群服务,或集群服务无法访问本地服务

排查步骤

  1. 检查网络连接:ping <cluster-ip>
  2. 检查 DNS 解析:nslookup <service-name>.<namespace>.svc.cluster.local
  3. 检查网络策略:kubectl describe networkpolicy
  4. 检查服务状态:kubectl describe service <service-name>
  5. 使用 curl 测试连接:curl http://<service-name>:<port>

九、本章小结

本文详细介绍了在 Kubernetes 中进行远程开发和本地代理 Pod 流量的多种方法,包括:

核心要点回顾

  1. 基础工具

    • kubectl port-forward:简单的端口转发工具,适合临时访问和简单调试
    • kubectl proxy:用于访问 Kubernetes API,提供认证和授权
  2. 高级工具

    • Telepresence:强大的流量拦截和双向网络工具,适合微服务开发和复杂调试
    • DevSpace:完整的开发工作流工具,支持代码同步、热重载和自动端口转发
  3. 最佳实践

    • 使用命名空间隔离开发环境
    • 合理配置网络策略和安全规则
    • 根据具体需求选择合适的工具
    • 定期清理临时资源,避免资源浪费
  4. 故障排查

    • 端口转发故障的排查步骤
    • Telepresence 连接问题的解决方法
    • 网络连通性问题的诊断流程

选择建议

  • 简单场景:使用 kubectl port-forward 进行临时访问和调试
  • API 开发:使用 kubectl proxy 访问 Kubernetes API
  • 微服务开发:使用 Telepresence 进行流量拦截和双向网络通信
  • 全栈开发:使用 DevSpace 提供完整的开发工作流

下一步学习

  • 深入学习 Telepresence 的高级配置和使用技巧
  • 掌握 DevSpace 的多环境配置和团队协作功能
  • 学习如何将这些工具集成到 CI/CD 流程中
  • 探索 Service Mesh 在远程开发中的应用

通过合理使用这些工具和方法,你可以显著提高在 Kubernetes 环境中的开发效率,减少部署周期,更快地验证代码变更,从而加速应用的开发和迭代过程。


相关阅读

作者简介:本文作者专注于云原生技术,拥有丰富的 Kubernetes 开发和运维经验,致力于分享云原生技术最佳实践。

OpenBao 全景解析:从零到生产的开源密钥管理实战

· 阅读需 36 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"The best secret is one that is never exposed — OpenBao keeps it that way."

在现代云原生架构中,密钥管理(Secrets Management) 是安全基础设施的核心。数据库密码、API Key、TLS 证书、SSH 密钥……这些敏感信息散落在环境变量、配置文件、CI/CD Pipeline 中,成为攻击者的首要目标。

OpenBaoHashiCorp Vault开源分叉(Fork),由 Linux Foundation 旗下的 LF Edge 项目托管,采用 MPL-2.0 许可证,完全开源、社区驱动。它继承了 Vault 强大的密钥管理能力,同时摆脱了商业许可变更的风险,是生产环境中管理密钥的理想选择。

本文将从零开始,由浅入深地带你全面掌握 OpenBao 的核心概念、安装配置、实战操作,直到生产级部署与运维。


一、OpenBao 是什么?

1.1 项目背景与定位

2023 年 8 月,HashiCorp 将 Vault 等核心产品从 MPL-2.0 许可证转为 BSL(Business Source License),限制竞争性使用。作为回应,社区发起了 OpenBao 项目,旨在维护一个完全开源的 Vault 替代品

对比项HashiCorp VaultOpenBao
许可证BSL 1.1(限制竞争使用)MPL-2.0(真正开源)
治理HashiCorp 公司主导Linux Foundation / LF Edge 社区治理
API 兼容性-与 Vault API 高度兼容
CLI 命令vaultbao
当前版本1.x2.5.x
定位企业级密钥管理社区驱动的开源密钥管理

1.2 核心能力概览

OpenBao 提供以下核心能力:

1.3 核心术语速查

在深入之前,先熟悉这些核心术语:

术语说明
Secret Engine存储、生成或加密数据的插件化组件,通过路径挂载(如 secret/database/pki/
Auth Method身份认证方式,验证用户/应用身份后颁发 Token
Token认证成功后获得的凭证,携带策略信息,用于后续 API 调用
PolicyACL 策略,定义 Token 对特定路径的权限(read / create / update / delete / list)
Lease密钥的有效期,到期后自动撤销。可续租(renew)或主动撤销(revoke)
Seal / UnsealOpenBao 的加密保护机制。启动时为 Sealed 状态,需提供 Unseal Key 才能解密数据
Barrier加密屏障,所有写入存储的数据都经过 Barrier 加密
Root Key加密 Keyring 的主密钥,由 Unseal Key 保护
Integrated Storage内置的 Raft 共识存储后端,无需外部依赖

二、架构深度解析

2.1 整体架构

OpenBao 的架构可以分为以下几个层次:

2.2 加密屏障(Barrier)工作原理

OpenBao 的安全核心是多层加密架构。理解这个加密链非常重要:

关键设计
  1. Unseal Key 不存储在任何地方 — 它由操作员持有(Shamir 分片分散在多人手中),或由外部 KMS/HSM 托管。
  2. Root Key 加密存储 — 即使攻击者获取了存储后端的全部数据,没有 Unseal Key 也无法解密。
  3. Encryption Key 支持轮换 — 可以在不停机的情况下定期轮换加密密钥,旧密钥保留在 Keyring 中用于解密历史数据。

2.3 请求处理流程

当客户端发起一个读取密钥的请求时,OpenBao 内部的处理流程如下:


三、安装与快速上手

3.1 安装方式

OpenBao 提供多种安装方式,以下按推荐顺序列出:

macOS(Homebrew)

# 安装
brew install openbao

# 验证
bao version
# OpenBao v2.5.x ...

Linux(包管理器)

# Arch Linux
pacman -Sy openbao

# Fedora / RHEL(需先启用 EPEL)
dnf install -y epel-release
dnf install -y openbao

# Debian / Ubuntu(使用官方仓库)
# 从 https://openbao.org/downloads/ 下载 .deb 包
wget https://github.com/openbao/openbao/releases/download/v2.5.0/bao_2.5.0_linux_amd64.deb
sudo dpkg -i bao_2.5.0_linux_amd64.deb

Docker 容器

# 支持三个镜像仓库
docker pull ghcr.io/openbao/openbao:latest
docker pull quay.io/openbao/openbao:latest
docker pull docker.io/openbao/openbao:latest

# 快速启动 Dev 模式
docker run --rm -p 8200:8200 \
--memory-swappiness=0 \
--name openbao-dev \
ghcr.io/openbao/openbao:latest server -dev
安全加固提醒

安装后建议进行以下加固措施:

  • 禁用 Swap:防止密钥数据被写入持久化交换空间。Linux 上 systemd 服务文件默认设置 MemorySwapMax=0
  • Docker 启动时:添加 --memory-swappiness=0 标志。
  • macOS:默认 Swap 已加密,无需额外处理。

从源码编译

# 需要 Go 环境
mkdir -p $GOPATH/src/github.com/openbao && cd $_
git clone https://github.com/openbao/openbao.git
cd openbao
make bootstrap # 下载依赖
make dev # 编译 bao 二进制到 ./bin/

3.2 Dev 模式快速体验

Dev 模式是学习和测试的最佳起点。它以内存存储、自动 Unseal、预配置 Root Token 的方式启动,绝不能用于生产环境

# 启动 Dev Server
bao server -dev

# 输出示例:
# ==> OpenBao server configuration:
# Api Address: http://127.0.0.1:8200
# Cgo: disabled
# Cluster Address: https://127.0.0.1:8201
# Listener 1: tcp (addr: "127.0.0.1:8200", ...)
# Log Level: info
# Mlock: supported: false, enabled: false
# Recovery Mode: false
# Storage: inmem
# Version: OpenBao v2.5.x
#
# WARNING! dev mode is enabled! In this mode, OpenBao runs entirely
# in-memory. Data is lost on restart!
#
# Root Token: hvs.xxxxxxxxxxxxxxxxxxxxxxxx

另一个终端中配置环境变量并开始操作:

# 设置环境变量
export BAO_ADDR='http://127.0.0.1:8200'
export BAO_TOKEN='hvs.xxxxxxxxxxxxxxxxxxxxxxxx' # 使用上面输出的 Root Token

# 验证服务器状态
bao status

3.3 第一个 Secret:KV 引擎实战

KV(Key-Value)Secret Engine 是最基础的密钥存储引擎。Dev 模式下默认启用了 KV v2 引擎,挂载在 secret/ 路径。

# ==========================================
# 写入密钥
# ==========================================
bao kv put secret/myapp/config \
db_host="postgres.prod.internal" \
db_port="5432" \
db_user="app_user" \
db_password="S3cur3P@ssw0rd!"

# 输出:
# ======= Secret Path =======
# secret/data/myapp/config
#
# ======= Metadata =======
# Key Value
# --- -----
# created_time 2026-04-09T11:00:00.000000Z
# custom_metadata <nil>
# deletion_time n/a
# destroyed false
# version 1

# ==========================================
# 读取密钥
# ==========================================
bao kv get secret/myapp/config

# 输出:
# ======= Secret Path =======
# secret/data/myapp/config
#
# ======= Metadata =======
# Key Value
# --- -----
# created_time 2026-04-09T11:00:00.000000Z
# version 1
#
# ====== Data ======
# Key Value
# --- -----
# db_host postgres.prod.internal
# db_port 5432
# db_user app_user
# db_password S3cur3P@ssw0rd!

# ==========================================
# 读取特定字段
# ==========================================
bao kv get -field=db_password secret/myapp/config
# S3cur3P@ssw0rd!

# ==========================================
# JSON 格式输出(用于脚本/程序)
# ==========================================
bao kv get -format=json secret/myapp/config | jq '.data.data'

# ==========================================
# 更新密钥(创建新版本)
# ==========================================
bao kv put secret/myapp/config \
db_host="postgres.prod.internal" \
db_port="5432" \
db_user="app_user" \
db_password="N3wS3cur3P@ss!"
# 现在 version = 2

# ==========================================
# 读取历史版本
# ==========================================
bao kv get -version=1 secret/myapp/config

# ==========================================
# 列出所有密钥路径
# ==========================================
bao kv list secret/myapp/

# ==========================================
# 删除密钥(软删除,可恢复)
# ==========================================
bao kv delete secret/myapp/config

# ==========================================
# 恢复已删除的密钥
# ==========================================
bao kv undelete -versions=2 secret/myapp/config

# ==========================================
# 永久销毁指定版本(不可恢复)
# ==========================================
bao kv destroy -versions=1 secret/myapp/config
KV v1 vs KV v2
特性KV v1KV v2
版本控制❌ 不支持✅ 支持(每次写入创建新版本)
软删除❌ 不支持✅ 支持(可恢复)
元数据❌ 不支持✅ 支持(created_time、custom_metadata)
Check-and-Set❌ 不支持✅ 支持(CAS,防止并发写入覆盖)
路径前缀secret/<path>secret/data/<path>(API 路径)
推荐使用简单场景生产环境推荐

3.4 HTTP API 直接调用

除了 CLI,OpenBao 的所有操作都可以通过 HTTP API 完成:

# 写入密钥 (KV v2)
curl -s \
--header "X-Vault-Token: $BAO_TOKEN" \
--request POST \
--data '{
"data": {
"api_key": "sk-abc123def456",
"api_secret": "super-secret-value"
}
}' \
$BAO_ADDR/v1/secret/data/myapp/api-keys | jq

# 读取密钥
curl -s \
--header "X-Vault-Token: $BAO_TOKEN" \
$BAO_ADDR/v1/secret/data/myapp/api-keys | jq '.data.data'

# 查看服务器状态
curl -s $BAO_ADDR/v1/sys/health | jq

四、Secret Engines 深度解析

Secret Engine 是 OpenBao 的核心组件,它们可以存储生成加密数据。每个引擎通过路径挂载来隔离,这意味着挂载在 database/ 路径的引擎和挂载在 secret/ 路径的引擎互不影响。

4.1 引擎全景

引擎类型用途典型场景
KV存储键值对存储应用配置、API Key、凭证
Database动态动态生成数据库凭证MySQL、PostgreSQL、MongoDB 临时账号
PKI动态签发 X.509 证书内部 TLS 证书、mTLS
Transit加密加密即服务(EaaS)数据加密/解密/签名,无需管理密钥
SSH动态SSH 密钥/证书签发服务器 SSH 访问控制
TOTP动态生成/验证 TOTP 码双因素认证
Transform加密数据转换(FPE/掩码)信用卡号、身份证号脱敏

4.2 动态密钥:Database 引擎实战

动态密钥是 OpenBao 最强大的特性之一 — 按需生成有时效性的数据库凭证,到期自动撤销。

# ==========================================
# 1. 启用 Database 引擎
# ==========================================
bao secrets enable database

# ==========================================
# 2. 配置 PostgreSQL 连接
# ==========================================
bao write database/config/my-postgresql-db \
plugin_name="postgresql-database-plugin" \
allowed_roles="readonly,readwrite" \
connection_url="postgresql://{{username}}:{{password}}@postgres.internal:5432/myapp?sslmode=require" \
username="vault_admin" \
password="vault_admin_password"

# ==========================================
# 3. 创建只读角色(动态生成的用户只有 SELECT 权限)
# ==========================================
bao write database/roles/readonly \
db_name="my-postgresql-db" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"

# ==========================================
# 4. 创建读写角色
# ==========================================
bao write database/roles/readwrite \
db_name="my-postgresql-db" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="30m" \
max_ttl="2h"

# ==========================================
# 5. 动态获取凭证
# ==========================================
bao read database/creds/readonly

# 输出:
# Key Value
# --- -----
# lease_id database/creds/readonly/abcd1234...
# lease_duration 1h
# lease_renewable true
# password A1a-xxxxxxxxxxxxxxxxx
# username v-token-readonly-xxxxxxxxx

# ==========================================
# 6. 续租 Lease
# ==========================================
bao lease renew database/creds/readonly/abcd1234...

# ==========================================
# 7. 手动撤销(立即删除数据库用户)
# ==========================================
bao lease revoke database/creds/readonly/abcd1234...
动态密钥最佳实践
  1. 始终设置合理的 TTLdefault_ttl 建议 ≤ 1 小时,max_ttl 建议 ≤ 24 小时。
  2. 应用侧要处理凭证续租 — 在 Lease 到期前主动 renew,或使用 OpenBao Agent 自动续租。
  3. 轮换初始管理员密码 — 配置完数据库连接后,执行 bao write -force database/rotate-root/my-postgresql-db 轮换管理员密码,阻止人工直接登录数据库。

4.3 PKI 引擎:内部 TLS 证书管理

PKI 引擎可以让 OpenBao 成为你的内部 CA(Certificate Authority),自动签发和管理 TLS 证书。

# ==========================================
# 1. 启用 PKI 引擎(Root CA)
# ==========================================
bao secrets enable pki

# 设置最大 TTL 为 10 年
bao secrets tune -max-lease-ttl=87600h pki

# 生成 Root CA 证书
bao write -field=certificate pki/root/generate/internal \
common_name="My Organization Root CA" \
issuer_name="root-2026" \
ttl=87600h > root_ca.crt

# ==========================================
# 2. 启用中间 CA(Intermediate CA)
# ==========================================
bao secrets enable -path=pki_int pki

bao secrets tune -max-lease-ttl=43800h pki_int

# 生成 CSR
bao write -field=csr pki_int/intermediate/generate/internal \
common_name="My Organization Intermediate CA" \
issuer_name="intermediate-2026" > pki_intermediate.csr

# 用 Root CA 签名
bao write -field=certificate pki/root/sign-intermediate \
csr=@pki_intermediate.csr \
format=pem_bundle \
ttl=43800h > intermediate_ca.crt

# 导入签名后的证书
bao write pki_int/intermediate/set-signed \
certificate=@intermediate_ca.crt

# ==========================================
# 3. 创建角色(定义可签发的证书规范)
# ==========================================
bao write pki_int/roles/internal-service \
allowed_domains="internal.mycompany.com,svc.cluster.local" \
allow_subdomains=true \
max_ttl="720h" \
key_type="ec" \
key_bits=256

# ==========================================
# 4. 签发证书
# ==========================================
bao write pki_int/issue/internal-service \
common_name="api.internal.mycompany.com" \
ttl="72h"

# 输出包含:
# - certificate (服务器证书)
# - issuing_ca (中间 CA 证书)
# - private_key (私钥,只在此刻返回!)
# - serial_number (证书序列号)

4.4 Transit 引擎:加密即服务(EaaS)

Transit 引擎提供加密即服务,应用不需要管理加密密钥,只需调用 API 即可完成加密/解密/签名操作。数据不会存储在 OpenBao 中,Transit 只负责加密操作。

# 启用 Transit 引擎
bao secrets enable transit

# 创建加密密钥
bao write -f transit/keys/my-app-key

# 加密数据(输入必须 base64 编码)
bao write transit/encrypt/my-app-key \
plaintext=$(echo -n "my-secret-data" | base64)

# 输出:
# Key Value
# --- -----
# ciphertext vault:v1:xxxxxxxxxxxxxxxxxxxxxx
# key_version 1

# 解密数据
bao write transit/decrypt/my-app-key \
ciphertext="vault:v1:xxxxxxxxxxxxxxxxxxxxxx"

# 输出中的 plaintext 需要 base64 解码
# echo "base64-output" | base64 -d
# my-secret-data

# 密钥轮换
bao write -f transit/keys/my-app-key/rotate

# 重新加密(使用新密钥版本加密已有密文)
bao write transit/rewrap/my-app-key \
ciphertext="vault:v1:xxxxxxxxxxxxxxxxxxxxxx"
# ciphertext 变为 vault:v2:...

五、认证与授权

5.1 Auth Methods 概览

OpenBao 支持多种认证方式,以适应不同场景:

Auth Method适用对象说明
Token所有场景核心认证方式,所有其他方式最终都返回 Token
UserPass人类用户用户名/密码认证,适合开发和测试
AppRole应用/服务基于 Role ID + Secret ID 的双因素认证
KubernetesK8s Pod使用 Service Account Token 自动认证
LDAP企业用户集成企业目录服务
JWT/OIDCSSO 用户集成 Okta、Auth0、Google 等身份提供商
TLS CertificatesmTLS 场景使用客户端证书认证

5.2 AppRole 认证实战(推荐用于应用)

AppRole 是应用和服务最推荐的认证方式。它采用双因素模型:Role ID(公开部分) + Secret ID(敏感部分)

# ==========================================
# 1. 启用 AppRole
# ==========================================
bao auth enable approle

# ==========================================
# 2. 创建角色
# ==========================================
bao write auth/approle/role/my-app \
token_policies="my-app-policy" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=10m \
secret_id_num_uses=1 # Secret ID 只能使用一次

# ==========================================
# 3. 获取 Role ID(可以嵌入配置文件或环境变量)
# ==========================================
bao read auth/approle/role/my-app/role-id
# role_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# ==========================================
# 4. 生成 Secret ID(应通过安全通道传递给应用)
# ==========================================
bao write -f auth/approle/role/my-app/secret-id
# secret_id: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy

# ==========================================
# 5. 应用侧:使用 Role ID + Secret ID 登录
# ==========================================
bao write auth/approle/login \
role_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
secret_id="yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

# 输出:
# Key Value
# --- -----
# token hvs.CAESIXXXXXXXXXXXXXXXXXX
# token_accessor xxxxxxxxxxxxxxxxxxxxxxxx
# token_duration 1h
# token_renewable true
# token_policies ["default" "my-app-policy"]

# ==========================================
# 6. 使用获取的 Token 读取密钥
# ==========================================
BAO_TOKEN=hvs.CAESIXXXXXXXXXXXXXXXXXX \
bao kv get secret/myapp/config

5.3 Kubernetes 认证

在 Kubernetes 环境中,Pod 可以使用 Service Account Token 直接向 OpenBao 认证:

# 启用 Kubernetes Auth
bao auth enable kubernetes

# 配置 Kubernetes 集群信息
bao write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"

# 创建角色(绑定 Service Account 和 Namespace)
bao write auth/kubernetes/role/my-app \
bound_service_account_names=my-app-sa \
bound_service_account_namespaces=production \
policies=my-app-policy \
ttl=1h

5.4 Token 类型与层级

OpenBao 中有两种 Token 类型:

类型说明适用场景
Service Token可续租、可撤销、有持久化存储的 Token长期运行的服务
Batch Token轻量级、不可续租、无持久化的 Token高吞吐量的短期操作

Token 还有层级概念:

Root Token
├── Service Token A (policy: admin)
│ ├── Service Token A1 (policy: readonly)
│ └── Service Token A2 (policy: readwrite)
└── Service Token B (policy: app)
└── Batch Token B1 (policy: app)
Root Token 安全
  • Root Token 拥有一切权限,相当于 Linux 的 root 用户。
  • 生产环境中,初始化完成后应立即撤销 Root Tokenbao token revoke <root-token>
  • 需要时可通过 bao operator generate-root 临时生成新的 Root Token(需要多个 Unseal Key 持有者共同授权)。

六、ACL 策略(Policies)

6.1 策略语法

策略定义了 Token 对特定路径的访问权限。OpenBao 采用白名单模式 — 未显式授予的权限一律拒绝。

# ================================================
# 文件:my-app-policy.hcl
# 描述:应用 my-app 的 ACL 策略
# ================================================

# 允许读取和列出应用配置
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}

# 允许读取和更新特定路径
path "secret/data/myapp/config" {
capabilities = ["create", "read", "update"]
}

# 允许获取数据库动态凭证
path "database/creds/readonly" {
capabilities = ["read"]
}

# 允许续租和撤销自己的 Lease
path "sys/leases/renew" {
capabilities = ["update"]
}

path "sys/leases/revoke" {
capabilities = ["update"]
}

# 允许查看自己的 Token 信息
path "auth/token/lookup-self" {
capabilities = ["read"]
}

# 允许续租自己的 Token
path "auth/token/renew-self" {
capabilities = ["update"]
}

# 拒绝访问元数据(使用 deny 覆盖其他策略)
path "secret/metadata/*" {
capabilities = ["deny"]
}

6.2 权限说明

Capability说明对应 HTTP 方法
create创建新数据POST
read读取数据GET
update更新已有数据POST/PUT
delete删除数据DELETE
list列出路径下的键LIST
sudo允许访问需要 root 权限的路径-
deny显式拒绝(最高优先级)-

6.3 策略管理

# 创建/更新策略
bao policy write my-app-policy my-app-policy.hcl

# 查看策略列表
bao policy list

# 查看策略内容
bao policy read my-app-policy

# 删除策略
bao policy delete my-app-policy

# 为已有 Token 关联策略
bao token create -policy="my-app-policy" -ttl=2h

6.4 策略模板(参数化策略)

OpenBao 支持在策略中使用模板变量,实现基于身份的动态权限

# 每个用户只能访问自己的密钥空间
path "secret/data/users/{{identity.entity.name}}/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

# 每个团队只能管理自己的密钥
path "secret/data/teams/{{identity.groups.names.*.id}}/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

七、Seal / Unseal 机制详解

7.1 Shamir 分片(默认)

OpenBao 默认使用 Shamir's Secret Sharing 算法将 Unseal Key 分割为多个分片。只需要一定数量(阈值)的分片即可重建 Unseal Key。

# 初始化(设置 5 个分片,阈值 3)
bao operator init -key-shares=5 -key-threshold=3

# 输出示例:
# Unseal Key 1: abc...
# Unseal Key 2: def...
# Unseal Key 3: ghi...
# Unseal Key 4: jkl...
# Unseal Key 5: mno...
#
# Initial Root Token: hvs.xxxxxxxxxx
#
# 请将这些 Key 分发给不同的人!

# Unseal 过程(需要提供 3 个 Key)
bao operator unseal # 输入 Key 1
# Sealed: true, Progress: 1/3

bao operator unseal # 输入 Key 2
# Sealed: true, Progress: 2/3

bao operator unseal # 输入 Key 3
# Sealed: false ← Unseal 成功!
分片分发最佳实践
场景分片数阈值说明
开发/测试11单人即可 Unseal
小团队32至少 2 人同时在场
生产环境53容忍 2 个分片丢失,需要 3 人同时授权
企业级74高安全性要求

7.2 Auto Unseal(推荐用于生产)

Shamir 分片的手动 Unseal 过程增加了运维复杂度(每次重启都需要收集足够的 Key)。Auto Unseal 将 Unseal Key 的保管委托给外部的 KMS 或 HSM,实现自动解封。

支持的 Auto Unseal Provider:

Provider配置块名称说明
AWS KMSseal "awskms"AWS Key Management Service
GCP Cloud KMSseal "gcpckms"Google Cloud KMS
Azure Key Vaultseal "azurekeyvault"Azure 密钥保管库
AliCloud KMSseal "alicloudkms"阿里云密钥管理服务
OCI KMSseal "ocikms"Oracle Cloud KMS
HSM (PKCS#11)seal "pkcs11"硬件安全模块
Transitseal "transit"使用另一个 OpenBao 实例

AWS KMS 配置示例:

seal "awskms" {
region = "ap-southeast-1"
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789:key/abcd-1234-..."

# 可选:指定 AWS 凭证(推荐使用 IAM Role)
# access_key = "..."
# secret_key = "..."
}
Auto Unseal 的风险

使用 Auto Unseal 会创建 OpenBao 对外部 KMS 的生命周期依赖

  • 如果 KMS 密钥不可用(如 AWS 服务故障),OpenBao 无法启动。
  • 如果 KMS 密钥被永久删除,OpenBao 数据将永远无法恢复,即使从备份恢复也不行。
  • 建议:使用 AWS SCP(Service Control Policies)等机制保护 KMS 密钥不被删除

7.3 Seal 迁移

在不同 Seal 类型之间迁移(如从 Shamir 迁移到 Auto Unseal)是可能的,但需要停机操作:


八、生产级部署

8.1 生产配置文件

以下是一个完整的生产级 OpenBao 配置文件,使用 Integrated Storage(Raft):

# ================================================
# /etc/openbao/config.hcl - 生产级配置
# ================================================

# --- 集群标识 ---
cluster_name = "prod-openbao-cluster"

# --- 网络地址 ---
# API 地址:客户端访问地址
api_addr = "https://openbao.mycompany.com:8200"
# 集群通信地址:节点间通信
cluster_addr = "https://10.0.1.10:8201"

# --- Web UI ---
ui = true

# --- 日志 ---
log_level = "info"
log_format = "json"
log_file = "/var/log/openbao/openbao.log"
log_rotate_duration = "24h"
log_rotate_max_files = 30

# --- Listener (TLS) ---
listener "tcp" {
address = "0.0.0.0:8200"
cluster_address = "0.0.0.0:8201"

# TLS 配置(生产环境必须启用)
tls_cert_file = "/etc/openbao/tls/server.crt"
tls_key_file = "/etc/openbao/tls/server.key"
tls_client_ca_file = "/etc/openbao/tls/ca.crt"

# TLS 最低版本
tls_min_version = "tls12"

# 禁用弱密码套件
tls_cipher_suites = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"

# 请求限制
max_request_duration = "90s"
max_request_size = 33554432 # 32MB
}

# --- Storage (Raft / Integrated Storage) ---
storage "raft" {
path = "/opt/openbao/data"
node_id = "node-1"

# 性能调优
performance_multiplier = 1 # 生产环境设为 1(低延迟调优)

# Autopilot 配置(自动管理集群成员)
autopilot {
cleanup_dead_servers = true
dead_server_last_contact_threshold = "24h"
min_quorum = 3
}

# 集群对端节点
retry_join {
leader_api_addr = "https://10.0.1.11:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://10.0.1.12:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
}

# --- Auto Unseal (可选,推荐) ---
seal "awskms" {
region = "ap-southeast-1"
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789:key/abcd-1234-..."
}

# --- Telemetry (可观测性)---
telemetry {
# Prometheus 指标端点
prometheus_retention_time = "24h"
disable_hostname = true

# StatsD(可选)
# statsd_address = "statsd.internal:8125"
}

# --- Lease 管理 ---
default_lease_ttl = "768h" # 默认 32 天
max_lease_ttl = "768h" # 最大 32 天

# --- 审计 (在初始化后通过 API 启用) ---
# bao audit enable file file_path=/var/log/openbao/audit.log

8.2 systemd 服务配置

# /etc/systemd/system/openbao.service
[Unit]
Description=OpenBao - A tool for managing secrets
Documentation=https://openbao.org/docs/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/openbao/config.hcl
StartLimitIntervalSec=60
StartLimitBurst=3

[Service]
Type=notify
User=openbao
Group=openbao
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes
PrivateDevices=yes
SecureBits=keep-caps
AmbientCapabilities=CAP_IPC_LOCK
NoNewPrivileges=yes
ExecStart=/usr/bin/bao server -config=/etc/openbao/config.hcl
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
LimitNOFILE=65536
LimitMEMLOCK=infinity
MemorySwapMax=0

[Install]
WantedBy=multi-user.target

8.3 初始化与首次启动

# 1. 创建数据目录
sudo mkdir -p /opt/openbao/data
sudo chown openbao:openbao /opt/openbao/data

# 2. TLS 证书准备(假设已有证书)
sudo mkdir -p /etc/openbao/tls
sudo cp server.crt server.key ca.crt /etc/openbao/tls/
sudo chown openbao:openbao /etc/openbao/tls/*
sudo chmod 600 /etc/openbao/tls/server.key

# 3. 启动服务
sudo systemctl enable openbao
sudo systemctl start openbao

# 4. 初始化(仅首次)
export BAO_ADDR='https://openbao.mycompany.com:8200'
export BAO_CACERT='/etc/openbao/tls/ca.crt'

bao operator init \
-key-shares=5 \
-key-threshold=3

# ⚠️ 安全保存输出的 Unseal Key 和 Root Token!

# 5. Unseal
bao operator unseal # 输入 Key 1
bao operator unseal # 输入 Key 2
bao operator unseal # 输入 Key 3

# 6. 验证
bao status

# 7. 登录
bao login $ROOT_TOKEN

# 8. 启用审计日志(生产必备)
bao audit enable file file_path=/var/log/openbao/audit.log

# 9. 撤销 Root Token(安全最佳实践)
bao token revoke $ROOT_TOKEN

九、高可用(HA)集群

9.1 Integrated Storage(Raft)HA 架构

OpenBao 推荐使用 Integrated Storage(基于 Raft 共识算法) 作为存储后端,它无需外部依赖(如 Consul),且内置 HA 能力。

9.2 集群节点配置

Node 1(Leader 候选):

storage "raft" {
path = "/opt/openbao/data"
node_id = "node-1"

retry_join {
leader_api_addr = "https://10.0.1.11:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://10.0.1.12:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
}

Node 2 & 3(类似配置,修改 node_id 和 retry_join 节点列表):

storage "raft" {
path = "/opt/openbao/data"
node_id = "node-2" # 或 node-3

retry_join {
leader_api_addr = "https://10.0.1.10:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://10.0.1.12:8200" # 或 10.0.1.11
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
}

9.3 加入集群

# 在 Node 2 和 Node 3 上执行
# 首先启动服务
sudo systemctl start openbao

# 然后 Unseal(每个节点都需要独立 Unseal)
bao operator unseal # Key 1
bao operator unseal # Key 2
bao operator unseal # Key 3

# 检查 Raft 成员列表(在 Leader 节点上)
bao operator raft list-peers

# 输出:
# Node Address State Voter
# ---- ------- ----- -----
# node-1 10.0.1.10:8201 leader true
# node-2 10.0.1.11:8201 follower true
# node-3 10.0.1.12:8201 follower true

9.4 HA 运维操作

# 查看 Leader 信息
bao status
# HA Enabled: true
# HA Cluster: https://10.0.1.10:8201
# HA Mode: active (或 standby)

# 手动 Step Down(触发领导权转移)
bao operator step-down

# Raft 快照备份
bao operator raft snapshot save backup-$(date +%Y%m%d).snap

# Raft 快照恢复
bao operator raft snapshot restore backup-20260409.snap

# 移除失败的节点
bao operator raft remove-peer node-3
HA 集群最佳实践
  1. 节点数量:生产环境建议 3 或 5 个节点(奇数节点,满足 Raft 多数派要求)。
  2. 网络延迟:同 Region 部署,节点间延迟应 < 10ms。
  3. 负载均衡:所有节点都可以处理读请求;写请求会被自动转发到 Leader。
  4. 备份策略:每日自动执行 raft snapshot save,保存到安全的外部存储。
  5. 监控:关注 vault.raft.leader.lastContactvault.raft.commitTime 指标。

十、审计日志

审计日志是安全合规的基础。OpenBao 支持三种审计设备:

10.1 启用审计

# 文件审计(最常用)
bao audit enable file file_path=/var/log/openbao/audit.log

# Syslog 审计
bao audit enable syslog tag="openbao" facility="AUTH"

# Socket 审计(发送到远程日志系统)
bao audit enable socket address="logserver.internal:9090" socket_type="tcp"

# 查看已启用的审计设备
bao audit list -detailed

10.2 审计日志格式

每条审计记录包含完整的请求和响应信息(敏感数据会被 HMAC 哈希处理):

{
"time": "2026-04-09T11:00:00.000000Z",
"type": "response",
"auth": {
"client_token": "hmac-sha256:abcdef...",
"accessor": "hmac-sha256:123456...",
"display_name": "approle",
"policies": ["default", "my-app-policy"],
"token_type": "service"
},
"request": {
"id": "request-uuid",
"operation": "read",
"path": "secret/data/myapp/config",
"remote_address": "10.0.2.50"
},
"response": {
"data": {
"data": {
"db_host": "hmac-sha256:xxxxxx...",
"db_password": "hmac-sha256:yyyyyy..."
}
}
}
}
审计安全性
  • 必须至少启用一个审计设备。如果所有审计设备都失败,OpenBao 将拒绝处理任何请求,以确保不会出现未记录审计的操作。
  • 审计日志中的敏感数据使用 HMAC-SHA256 哈希,无法逆向还原。但如果你知道原始值,可以通过 bao audit hash 命令计算哈希值进行比对。
  • 建议将审计日志实时转发到集中式日志系统(如 ELK、Loki、Splunk)。

十一、Kubernetes 集成

11.1 Helm 部署 OpenBao

OpenBao 官方提供了 Helm Chart,可以方便地在 Kubernetes 上部署 HA 集群:

# 添加 Helm 仓库
helm repo add openbao https://openbao.github.io/openbao-helm
helm repo update

# 创建 values.yaml
cat <<EOF > openbao-values.yaml
global:
enabled: true
tlsDisable: false

server:
image:
repository: ghcr.io/openbao/openbao
tag: "2.5.0"

# HA 模式(3 副本)
ha:
enabled: true
replicas: 3
raft:
enabled: true
config: |
ui = true

listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_cert_file = "/vault/tls/tls.crt"
tls_key_file = "/vault/tls/tls.key"
}

storage "raft" {
path = "/vault/data"

retry_join {
leader_api_addr = "https://openbao-0.openbao-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://openbao-1.openbao-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://openbao-2.openbao-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
}
}

service_registration "kubernetes" {}

# 持久化存储
dataStorage:
enabled: true
size: 10Gi
storageClass: "gp3"

# 资源限制
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: 2000m
memory: 1Gi

# 审计日志存储
auditStorage:
enabled: true
size: 5Gi

# Agent Injector(自动注入 sidecar)
injector:
enabled: true
replicas: 2
resources:
requests:
cpu: 100m
memory: 64Mi

# Web UI
ui:
enabled: true
serviceType: ClusterIP
EOF

# 部署
helm install openbao openbao/openbao \
-n openbao --create-namespace \
-f openbao-values.yaml

11.2 Agent Injector:自动为 Pod 注入密钥

OpenBao Agent Injector 是一个 Kubernetes Mutating Webhook,它通过注解(Annotations)为 Pod 自动注入 Sidecar,从 OpenBao 获取密钥并写入文件。

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
annotations:
# ========================================
# OpenBao Agent Injector 注解
# ========================================
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "my-app"

# 注入数据库凭证
vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/readonly"
vault.hashicorp.com/agent-inject-template-db-creds: |
{{- with secret "database/creds/readonly" -}}
DB_HOST=postgres.internal
DB_PORT=5432
DB_USER={{ .Data.username }}
DB_PASSWORD={{ .Data.password }}
{{- end }}

# 注入应用配置
vault.hashicorp.com/agent-inject-secret-app-config: "secret/data/myapp/config"
vault.hashicorp.com/agent-inject-template-app-config: |
{{- with secret "secret/data/myapp/config" -}}
API_KEY={{ .Data.data.api_key }}
API_SECRET={{ .Data.data.api_secret }}
{{- end }}

spec:
serviceAccountName: my-app-sa
containers:
- name: my-app
image: my-app:latest
command: ["/bin/sh", "-c"]
args:
- |
source /vault/secrets/db-creds
source /vault/secrets/app-config
exec ./my-app-binary
ports:
- containerPort: 8080

密钥将被注入到 Pod 的 /vault/secrets/ 目录下:

/vault/secrets/
├── db-creds # 数据库凭证
└── app-config # 应用配置

11.3 CSI Provider(另一种注入方式)

除了 Agent Injector,还可以使用 CSI(Container Storage Interface)Provider 将密钥作为卷挂载:

apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
serviceAccountName: my-app-sa
containers:
- name: my-app
image: my-app:latest
volumeMounts:
- name: secrets
mountPath: "/mnt/secrets"
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "openbao-secrets"
---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: openbao-secrets
spec:
provider: vault # OpenBao 兼容 Vault CSI Provider
parameters:
roleName: "my-app"
vaultAddress: "https://openbao.openbao.svc:8200"
objects: |
- objectName: "db-password"
secretPath: "secret/data/myapp/config"
secretKey: "db_password"

十二、OpenBao Agent / Proxy

12.1 Agent 模式

OpenBao Agent 是一个客户端守护进程,提供以下能力:

  • Auto Auth:自动认证并获取 Token
  • Caching:本地缓存 Token 和 Lease,减少对 Server 的请求
  • Templating:将密钥渲染到配置文件模板中
  • API Proxy:代理 API 请求,自动注入认证信息
# /etc/openbao/agent.hcl

# 如何认证
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/openbao/role-id"
secret_id_file_path = "/etc/openbao/secret-id"
remove_secret_id_file_after_reading = true
}
}

# 将 Token 保存到文件
sink "file" {
config = {
path = "/tmp/openbao-token"
mode = 0640
}
}
}

# 缓存配置
cache {
use_auto_auth_token = true
}

# API 代理
listener "tcp" {
address = "127.0.0.1:8100"
tls_disable = true
}

# 服务器连接
vault {
address = "https://openbao.mycompany.com:8200"
tls_ca_cert = "/etc/openbao/tls/ca.crt"
}

# 模板渲染
template {
source = "/etc/openbao/templates/db-config.tpl"
destination = "/app/config/database.env"
perms = 0640
command = "systemctl reload my-app" # 渲染后执行的命令
}

模板文件示例 (db-config.tpl):

{{ with secret "database/creds/readonly" }}
DB_USER={{ .Data.username }}
DB_PASSWORD={{ .Data.password }}
{{ end }}
{{ with secret "secret/data/myapp/config" }}
API_KEY={{ .Data.data.api_key }}
{{ end }}
# 启动 Agent
bao agent -config=/etc/openbao/agent.hcl

12.2 Proxy 模式

OpenBao Proxy 与 Agent 类似,但专注于API 代理功能,不包含模板渲染。适合作为应用的 Sidecar 使用。


十三、运维与监控

13.1 关键监控指标

指标说明告警阈值
vault.core.handle_request请求处理延迟P99 > 500ms
vault.barrier.getBarrier 读取延迟P99 > 100ms
vault.barrier.putBarrier 写入延迟P99 > 200ms
vault.raft.leader.lastContactFollower 到 Leader 的最后联系时间> 500ms
vault.raft.commitTimeRaft 提交时间P99 > 50ms
vault.expire.num_leases当前活跃 Lease 数根据容量规划
vault.runtime.alloc_bytes内存分配量接近 limits
vault.core.unsealed是否已 Unseal= 0 表示 Sealed

13.2 Prometheus 集成

# prometheus-scrape-config.yaml
scrape_configs:
- job_name: "openbao"
metrics_path: "/v1/sys/metrics"
params:
format: ["prometheus"]
scheme: "https"
tls_config:
ca_file: "/etc/prometheus/openbao-ca.crt"
authorization:
credentials_file: "/etc/prometheus/openbao-token"
static_configs:
- targets:
- "openbao-0.internal:8200"
- "openbao-1.internal:8200"
- "openbao-2.internal:8200"

13.3 备份与恢复策略

# ==========================================
# 自动备份脚本
# ==========================================
#!/bin/bash
# /opt/openbao/scripts/backup.sh

BACKUP_DIR="/opt/openbao/backups"
DATE=$(date +%Y%m%d-%H%M%S)
RETENTION_DAYS=30

# 创建快照
bao operator raft snapshot save "${BACKUP_DIR}/openbao-${DATE}.snap"

# 上传到对象存储(示例:AWS S3)
aws s3 cp "${BACKUP_DIR}/openbao-${DATE}.snap" \
"s3://my-backups/openbao/openbao-${DATE}.snap" \
--sse aws:kms

# 清理本地旧备份
find "${BACKUP_DIR}" -name "*.snap" -mtime +${RETENTION_DAYS} -delete

echo "[$(date)] Backup completed: openbao-${DATE}.snap"
# 添加到 crontab(每天凌晨 3 点备份)
echo "0 3 * * * /opt/openbao/scripts/backup.sh >> /var/log/openbao/backup.log 2>&1" \
| crontab -

13.4 密钥轮换

# 轮换加密密钥(在线操作,不影响服务)
bao operator rotate

# 查看当前密钥信息
bao read sys/key-status
# Key Value
# --- -----
# install_time 2026-04-09T00:00:00Z
# term 2 # 密钥已轮换过 1 次

十四、应用集成模式

14.1 应用读取密钥的四种方式

方式复杂度适用场景优缺点
直接 API需要精细控制的应用灵活但需自行管理 Token 生命周期
Agent虚拟机/裸金属部署自动认证+缓存+模板,推荐
K8s InjectorKubernetes 环境注解驱动,零代码侵入
CSI ProviderKubernetes 环境标准 Volume 接口

14.2 Go 应用集成示例

package main

import (
"context"
"fmt"
"log"
"time"

vault "github.com/openbao/openbao/api/v2"
)

func main() {
// 创建客户端
config := vault.DefaultConfig()
config.Address = "https://openbao.mycompany.com:8200"

client, err := vault.NewClient(config)
if err != nil {
log.Fatalf("unable to initialize OpenBao client: %v", err)
}

// AppRole 登录
resp, err := client.Logical().Write("auth/approle/login", map[string]interface{}{
"role_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"secret_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
})
if err != nil {
log.Fatalf("unable to login: %v", err)
}
client.SetToken(resp.Auth.ClientToken)

// 读取密钥
secret, err := client.KVv2("secret").Get(context.Background(), "myapp/config")
if err != nil {
log.Fatalf("unable to read secret: %v", err)
}

dbHost := secret.Data["db_host"].(string)
dbPassword := secret.Data["db_password"].(string)
fmt.Printf("DB Host: %s\n", dbHost)
fmt.Printf("DB Password: %s\n", dbPassword)

// Token 续租(后台 goroutine)
go func() {
for {
time.Sleep(30 * time.Minute)
_, err := client.Auth().Token().RenewSelf(3600) // 续租 1 小时
if err != nil {
log.Printf("token renewal failed: %v", err)
}
}
}()

// ... 应用正常逻辑
select {}
}

14.3 Python 应用集成示例

import hvac # pip install hvac(兼容 OpenBao API)
import os
import time
import threading

class SecretsManager:
"""OpenBao 密钥管理封装"""

def __init__(self):
self.client = hvac.Client(
url=os.environ.get('BAO_ADDR', 'https://openbao.mycompany.com:8200'),
verify='/etc/openbao/tls/ca.crt'
)
self._login()
self._start_renewal()

def _login(self):
"""使用 AppRole 登录"""
result = self.client.auth.approle.login(
role_id=os.environ['BAO_ROLE_ID'],
secret_id=os.environ['BAO_SECRET_ID']
)
self.client.token = result['auth']['client_token']
self.lease_duration = result['auth']['lease_duration']

def get_secret(self, path: str, key: str = None) -> dict | str:
"""读取 KV v2 密钥"""
response = self.client.secrets.kv.v2.read_secret_version(
path=path,
mount_point='secret'
)
data = response['data']['data']
return data[key] if key else data

def get_db_credentials(self, role: str = 'readonly') -> dict:
"""获取动态数据库凭证"""
response = self.client.read(f'database/creds/{role}')
return {
'username': response['data']['username'],
'password': response['data']['password'],
'lease_id': response['lease_id'],
'lease_duration': response['lease_duration']
}

def _start_renewal(self):
"""后台自动续租 Token"""
def renew():
while True:
time.sleep(self.lease_duration * 0.7) # 在 70% 时续租
try:
self.client.auth.token.renew_self()
except Exception as e:
print(f"Token renewal failed: {e}")
self._login()

thread = threading.Thread(target=renew, daemon=True)
thread.start()

# 使用示例
secrets = SecretsManager()
db_password = secrets.get_secret('myapp/config', 'db_password')
db_creds = secrets.get_db_credentials('readonly')
print(f"Dynamic DB User: {db_creds['username']}")

十五、生产环境 Checklist

在将 OpenBao 部署到生产环境之前,请逐一确认以下事项:

基础安全

  • TLS 已启用 — Listener 配置了有效的 TLS 证书
  • Swap 已禁用或加密 — 防止密钥数据泄露到磁盘
  • Root Token 已撤销 — 初始化完成后立即撤销
  • Unseal Key 安全分发 — 多人持有,物理隔离存储
  • Auto Unseal 已配置(如使用)— KMS 密钥受保护不可删除

存储与高可用

  • Raft 集群 3+ 节点 — 奇数节点,容忍 N/2 节点故障
  • 持久化存储可靠 — 使用 SSD,IOPS 足够
  • 自动备份已配置 — 每日 Raft Snapshot,上传到安全存储
  • 备份恢复已测试 — 定期演练恢复流程

审计与监控

  • 审计日志已启用 — 至少一个审计设备
  • 审计日志转发 — 发送到集中式日志系统
  • Prometheus 监控 — 采集关键指标
  • 告警规则已配置 — Sealed 状态、请求延迟、Raft 通信

访问控制

  • 最小权限策略 — 每个应用只获得必要的权限
  • AppRole / K8s Auth — 应用使用自动化认证
  • Secret ID 限制 — 设置 secret_id_num_uses=1secret_id_ttl
  • Token TTL 合理 — 默认 1h,最大不超过 24h

密钥管理

  • KV v2 引擎 — 启用版本控制
  • 动态密钥 — 数据库等使用动态凭证,而非静态密码
  • 密钥轮换计划 — 定期执行 bao operator rotate
  • Lease TTL 合理 — 动态凭证 ≤ 1h

十六、本章小结

本文从零开始,全面深入地解析了 OpenBao 的各个方面:

  1. 架构理解 — 掌握了 Barrier 加密屏障、多层密钥保护、请求处理流程等核心设计。

  2. 安装上手 — 从 Dev 模式体验到各平台安装方式,快速建立第一个实操经验。

  3. Secret Engines — 深入学习了 KV v2(静态密钥)、Database(动态凭证)、PKI(证书管理)、Transit(加密即服务)等核心引擎。

  4. 认证授权 — 掌握了 AppRole、Kubernetes Auth、Token 体系和 ACL Policy 的设计与实践。

  5. Seal/Unseal — 理解了 Shamir 分片与 Auto Unseal 的工作原理及迁移方案。

  6. 生产部署 — 完成了 TLS、Raft HA 集群、systemd 服务、审计日志的完整配置。

  7. Kubernetes 集成 — 掌握了 Helm 部署、Agent Injector、CSI Provider 的使用方式。

  8. 应用集成 — 通过 Go 和 Python 示例,展示了应用如何安全地获取和续租密钥。

OpenBao 作为 HashiCorp Vault 的开源替代品,在保持 API 兼容性的同时,完全由社区驱动,是云原生时代密钥管理的理想选择。


参考文档

本文内容基于 OpenBao 官方文档校验,以下为核心参考链接:

Kubernetes 全景解析 (7):有状态应用与 Operator 模式实战

· 阅读需 32 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"StatefulSet 管的是'有序的宠物',Operator 管的是'懂业务的管家'。"

在前面的章节中,我们深入探讨了 K8s 的架构设计、网络模型和存储体系。对于无状态应用(如 Web 服务、API 网关),Deployment + Service 的组合已经足够应对大多数场景。然而,当你面对数据库、消息队列、分布式缓存等有状态应用时,事情就变得复杂了——它们需要稳定的网络标识、持久化的存储卷、有序的启停顺序,以及复杂的集群初始化流程。

本文将从有状态应用的核心挑战出发,深入解析 StatefulSet 的工作机制,实战部署一套 Redis Cluster,并最终构建一个自定义 Operator 来自动化管理整个生命周期。

API 版本说明:本文所有 YAML 配置使用的 API 版本均经过 Kubernetes v1.34 官方文档校验:

资源类型apiVersion官方文档
StatefulSetapps/v1StatefulSet v1
Servicev1Service v1
CRDapiextensions.k8s.io/v1CRD v1
RBACrbac.authorization.k8s.io/v1RBAC v1
Deploymentapps/v1Deployment v1

:::


一、有状态应用的挑战与 K8s 解决方案

1.1 有状态 vs 无状态:本质区别

在分布式系统中,"状态"(State)是指应用需要持久保存的数据或上下文信息。理解有状态与无状态的本质区别,是选择正确编排策略的前提。

维度无状态应用有状态应用
数据存储数据在外部(DB/缓存)数据在本地或内置存储
网络标识随意替换,IP 可变需要稳定的网络标识
扩缩容随意增删实例需要数据迁移/重平衡
启停顺序无要求通常需要有序启停
典型代表Web 服务、微服务 API数据库、消息队列、缓存
K8s 工作负载Deployment / ReplicaSetStatefulSet / Operator

无状态应用就像工厂流水线上的工人——任何人都可以顶替任何人的位置,因为所有信息都记录在外部系统中。

有状态应用则像一支乐队——每个乐手有自己的乐器和乐谱,鼓手不能突然变成小提琴手,乐队的演出需要按特定顺序开始。

1.2 StatefulSet 的核心特性

StatefulSet 是 Kubernetes 专门为有状态应用设计的工作负载控制器。它相比 Deployment,提供了三个关键保证:

1. 稳定的、唯一的网络标识

每个 Pod 都会获得一个固定的名称和对应的 DNS 记录。即使 Pod 被重新调度到其他节点,名称和 DNS 也不会改变。

2. 稳定的、持久的存储

每个 Pod 绑定一个独立的 PVC(PersistentVolumeClaim),Pod 被删除后 PVC 保留,新 Pod 重建时会重新挂载同一个 PVC。

3. 有序的、优雅的部署和扩缩容

Pod 按照 0, 1, 2, ... 的顺序依次创建和删除,确保集群拓扑的稳定性。

1.3 Headless Service 与 DNS 记录

StatefulSet 依赖一个特殊的 Service——Headless ServiceclusterIP: None)来实现稳定的网络标识。与普通 Service 不同,Headless Service 不会分配集群 IP,也不会进行代理负载均衡,而是直接将 DNS 解析到每个 Pod 的 IP。

# headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
spec:
clusterIP: None # 关键:声明为 Headless Service
selector:
app: redis-cluster
ports:
- port: 6379
targetPort: 6379

创建 Headless Service 后,DNS 会为每个 Pod 生成如下记录:

redis-cluster-0.redis-cluster.default.svc.cluster.local -> 10.244.1.10
redis-cluster-1.redis-cluster.default.svc.cluster.local -> 10.244.2.15
redis-cluster-2.redis-cluster.default.svc.cluster.local -> 10.244.3.20

这种稳定的 DNS 记录使得有状态应用可以通过固定的主机名互相发现和通信,而不需要依赖可能变化的 Pod IP。

DNS 命名规则

完整的 DNS 名称格式为:<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local

在同一个命名空间内,可以简写为 <pod-name>.<headless-service-name>


二、实战 1:Redis Cluster 部署

2.1 Redis Cluster 架构设计

Redis Cluster 是 Redis 官方提供的分布式解决方案,采用去中心化的 Hash Slot 架构。一个 Redis Cluster 由多个主节点(Master)组成,每个主节点负责一部分 Hash Slot(共 16384 个),主节点可以挂载从节点(Slave)用于故障转移。

2.2 完整部署 YAML

下面是一套可直接运行的 Redis Cluster 部署配置。我们将部署 6 个节点(3 主 3 从),使用 StatefulSet 保证有序部署和稳定标识。

API 版本说明:StatefulSet 使用 apps/v1,Headless Service 使用 v1,均为 Kubernetes v1.34 稳定版本。详见官方文档:StatefulSetService

文件路径:redis-cluster/redis-cluster.yaml

---
# Headless Service:为 StatefulSet 提供稳定的网络标识
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
labels:
app: redis-cluster
spec:
clusterIP: None
selector:
app: redis-cluster
ports:
- name: redis
port: 6379
targetPort: 6379
- name: cluster-bus
port: 16379
targetPort: 16379

---
# StatefulSet:管理 6 个 Redis 节点
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
labels:
app: redis-cluster
spec:
serviceName: redis-cluster # 必须关联 Headless Service
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
terminationGracePeriodSeconds: 30
containers:
- name: redis
image: redis:7.2.4
ports:
- containerPort: 6379
name: redis
- containerPort: 16379
name: cluster-bus
command:
- /bin/sh
- -c
- |
redis-server \
--cluster-enabled yes \
--cluster-config-file /data/nodes.conf \
--cluster-node-timeout 5000 \
--appendonly yes \
--bind 0.0.0.0 \
--port 6379 \
--cluster-announce-ip $(hostname).redis-cluster.default.svc.cluster.local \
--cluster-announce-port 6379 \
--cluster-announce-bus-port 16379
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: redis-data
mountPath: /data
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 15
periodSeconds: 5
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# 每个 Pod 独立的 PVC
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard # 根据集群实际情况修改
resources:
requests:
storage: 1Gi
生产环境注意事项
  1. cluster-announce-ip 必须使用 Pod 的完整 DNS 名称,否则集群节点之间无法正确发现对方。
  2. cluster-announce-bus-port 必须显式声明,Redis Cluster 节点间通过 Bus 端口进行 gossip 通信。
  3. storageClassName 需要根据你的 K8s 集群实际可用的 StorageClass 进行修改。生产环境建议使用 SSD 存储类。
  4. 建议配置 PodDisruptionBudget 以防止维护操作时同时驱逐过多节点。

2.3 部署与验证

执行部署:

# 创建命名空间(可选)
kubectl create namespace redis

# 部署 Redis Cluster
kubectl apply -f redis-cluster/redis-cluster.yaml -n redis

# 观察 Pod 的有序创建过程
kubectl get pods -n redis -l app=redis-cluster -w

你应该能看到 Pod 按顺序逐个启动:

NAME READY STATUS RESTARTS AGE
redis-cluster-0 1/1 Running 0 30s
redis-cluster-1 1/1 Running 0 15s
redis-cluster-2 1/1 Running 0 5s
redis-cluster-3 0/1 Pending 0 0s
...

验证 DNS 解析:

# 进入任意 Pod 验证 DNS
kubectl exec -it redis-cluster-0 -n redis -- nslookup redis-cluster-1.redis-cluster.redis.svc.cluster.local

# 验证各节点可以互相通信
kubectl exec -it redis-cluster-0 -n redis -- redis-cli -h redis-cluster-1.redis-cluster.redis.svc.cluster.local ping

三、Redis Cluster 初始化与验证

说明:Redis Cluster 初始化使用 redis-cli --cluster create 命令,这是 Redis Cluster 的官方初始化方式。详见官方文档:Redis Cluster Tutorial

3.1 集群初始化脚本

StatefulSet 只负责创建 Pod,Redis Cluster 的初始化(节点握手、槽位分配)需要手动执行。我们编写一个初始化脚本来自动化这个过程。

文件路径:redis-cluster/init-cluster.sh

#!/bin/bash
set -euo pipefail

NAMESPACE="${1:-redis}"
SERVICE="redis-cluster"
REPLICAS=6

echo "=== 等待所有 Redis Pod 就绪 ==="
for i in $(seq 0 $((REPLICAS - 1))); do
echo -n "等待 redis-cluster-${i} ... "
kubectl wait --for=condition=ready pod/redis-cluster-${i} \
-n ${NAMESPACE} --timeout=120s
echo "就绪"
done

echo ""
echo "=== 收集节点地址 ==="
NODES=""
for i in $(seq 0 $((REPLICAS - 1))); do
POD_FQDN="redis-cluster-${i}.${SERVICE}.${NAMESPACE}.svc.cluster.local"
NODES="${NODES} ${POD_FQDN}:6379"
done

echo "节点列表:${NODES}"
echo ""

echo "=== 创建 Redis Cluster ==="
kubectl exec -it redis-cluster-0 -n ${NAMESPACE} -- \
redis-cli --cluster create ${NODES} --cluster-replicas 1

echo ""
echo "=== 验证集群状态 ==="
kubectl exec -it redis-cluster-0 -n ${NAMESPACE} -- \
redis-cli --cluster check redis-cluster-0.${SERVICE}.${NAMESPACE}.svc.cluster.local:6379

执行初始化:

chmod +x redis-cluster/init-cluster.sh
./redis-cluster/init-cluster.sh redis

3.2 槽位分配与节点握手

初始化成功后,你应该能看到类似以下输出:

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
M: xxx... redis-cluster-0.redis-cluster.redis.svc.cluster.local:6379
S: yyy... redis-cluster-3.redis-cluster.redis.svc.cluster.local:6379
M: zzz... redis-cluster-1.redis-cluster.redis.svc.cluster.local:6379
S: aaa... redis-cluster-4.redis-cluster.redis.svc.cluster.local:6379
M: bbb... redis-cluster-2.redis-cluster.redis.svc.cluster.local:6379
S: ccc... redis-cluster-5.redis-cluster.redis.svc.cluster.local:6379
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Cluster state ok

3.3 数据读写验证

# 写入数据
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli SET greeting "Hello from Redis Cluster"

# 读取数据(从任意节点)
kubectl exec -it redis-cluster-1 -n redis -- \
redis-cli GET greeting

# 查看集群信息
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli CLUSTER INFO

# 查看节点列表
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli CLUSTER NODES

3.4 故障转移测试

# 查看当前主从关系
kubectl exec -it redis-cluster-0 -n redis -- redis-cli CLUSTER NODES

# 模拟主节点故障(删除 redis-cluster-0)
kubectl delete pod redis-cluster-0 -n redis

# 等待 StatefulSet 重建 Pod
kubectl get pods -n redis -l app=redis-cluster -w

# 重建完成后,检查集群状态
# 对应的从节点应该已经提升为主节点
kubectl exec -it redis-cluster-1 -n redis -- redis-cli CLUSTER NODES

# 验证数据仍然可用
kubectl exec -it redis-cluster-1 -n redis -- redis-cli GET greeting
故障转移注意事项
  1. Redis Cluster 的故障转移由集群内部自动完成,不需要外部干预。
  2. 当原主节点恢复后,它会自动变为新主节点的从节点,不会抢回主节点角色。
  3. 在生产环境中,建议设置 cluster-node-timeout 为合理值(通常 5-15 秒),以平衡故障检测速度和误判率。

四、Operator 模式深度解析

4.1 Operator 的设计哲学

通过前面的实战,你可能已经发现一个问题:Redis Cluster 的初始化是手动执行的。StatefulSet 只负责创建 Pod,但集群的初始化、扩缩容、故障恢复等操作仍然需要人工介入。

这正是 Operator 模式要解决的核心问题。

Operator 的本质是将人类运维特定应用的知识编码为自动化软件。

CoreOS(现 Red Hat)在 2016 年提出了 Operator 模式的概念。其核心思想是:

  1. 人类运维人员知道如何部署、扩缩容、备份、恢复一个特定应用
  2. 将这些领域知识编码为 Kubernetes 控制器
  3. 控制器通过 CRD(Custom Resource Definition) 扩展 K8s API,引入应用特定的资源类型
  4. 通过 Reconciliation Loop 持续调谐实际状态与期望状态

简单来说,Operator = CRD + Controller + 特定领域知识

4.2 Operator 核心组件

一个完整的 Operator 由以下三个核心组件构成:

组件职责类比
CRD(Custom Resource Definition)定义自定义资源的 Schema(结构)数据库的表结构
ControllerWatch 资源变化,执行调谐逻辑数据库的触发器
Reconciliation Loop持续将实际状态趋近期望状态自动驾驶的反馈控制

4.3 开发框架对比

目前主流的 Operator 开发框架有三个:

特性controller-runtimeKubebuilderOperator SDK
定位底层库脚手架工具全功能 SDK
语言GoGoGo / Ansible / Helm
复杂度中等
灵活性
项目脚手架无(需手动搭建)内置 kubebuilder init内置 operator-sdk init
Webhook 支持手动注册自动生成脚手架自动生成脚手架
关系Kubebuilder 和 Operator SDK 的底层依赖基于 controller-runtime基于 controller-runtime
框架选择建议
  • 学习 Operator 原理:使用 controller-runtime,理解底层机制
  • 快速开发生产级 Operator:使用 Kubebuilder,脚手架完善、社区活跃
  • 非 Go 技术栈:使用 Operator SDK 的 Ansible/Helm 模式

本文的实战部分将使用 controller-runtime,帮助你理解 Operator 的核心原理。


五、实战 2:自定义 Redis Operator

5.1 项目初始化

# 创建项目目录
mkdir redis-operator && cd redis-operator

# 初始化 Go Module
go mod init github.com/rainlib/redis-operator

# 安装依赖
# controller-runtime v0.18.0 与 Kubernetes v1.34 API 兼容
go get sigs.k8s.io/controller-runtime@v0.18.0
go get k8s.io/api@v0.30.0
go get k8s.io/apimachinery@v0.30.0

依赖版本说明:controller-runtime v0.18.0 与 Kubernetes v1.34 API 完全兼容。详见官方文档:controller-runtime

5.2 CRD 定义

文件路径:api/v1alpha1/rediscluster_types.go

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// RedisClusterSpec 定义用户期望的集群状态
type RedisClusterSpec struct {
// 主节点数量(必须为奇数)
MasterCount int32 `json:"masterCount"`

// 每个主节点的从节点数量
ReplicasPerMaster int32 `json:"replicasPerMaster"`

// Redis 镜像
Image string `json:"image"`

// 资源请求与限制
Resources ResourceRequirements `json:"resources,omitempty"`
}

type ResourceRequirements struct {
CPURequest string `json:"cpuRequest,omitempty"`
MemoryRequest string `json:"memoryRequest,omitempty"`
CPULimit string `json:"cpuLimit,omitempty"`
MemoryLimit string `json:"memoryLimit,omitempty"`
}

// ClusterPhase 表示集群当前阶段
type ClusterPhase string

const (
ClusterPhaseCreating ClusterPhase = "Creating"
ClusterPhaseInitializing ClusterPhase = "Initializing"
ClusterPhaseRunning ClusterPhase = "Running"
ClusterPhaseScaling ClusterPhase = "Scaling"
ClusterPhaseFailed ClusterPhase = "Failed"
)

// RedisClusterStatus 反映集群的实际状态
type RedisClusterStatus struct {
// 集群当前阶段
Phase ClusterPhase `json:"phase,omitempty"`

// 各节点的就绪状态
ReadyNodes int32 `json:"readyNodes,omitempty"`

// 集群是否已初始化
ClusterInitialized bool `json:"clusterInitialized,omitempty"`

// 最近一次操作的消息
Message string `json:"message,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=rc

// RedisCluster 是自定义资源的顶层类型
type RedisCluster struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec RedisClusterSpec `json:"spec,omitempty"`
Status RedisClusterStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// RedisClusterList 用于列表操作
type RedisClusterList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []RedisCluster `json:"items"`
}

func init() {
SchemeBuilder.Register(&RedisCluster{}, &RedisClusterList{})
}

5.3 CRD YAML 生成

API 版本说明:CRD 使用 apiextensions.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。apiextensions.k8s.io/v1beta1 已在 v1.22 中移除,请确保使用 v1。详见官方文档:CustomResourceDefinition v1

文件路径:config/crd/redisclusters.rainlib.io.yaml

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: redisclusters.rainlib.io
spec:
group: rainlib.io
names:
kind: RedisCluster
listKind: RedisClusterList
plural: redisclusters
shortNames:
- rc
singular: rediscluster
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- masterCount
- replicasPerMaster
- image
properties:
masterCount:
type: integer
minimum: 1
description: 主节点数量
replicasPerMaster:
type: integer
minimum: 0
description: 每个主节点的从节点数量
image:
type: string
description: Redis 镜像地址
resources:
type: object
properties:
cpuRequest:
type: string
memoryRequest:
type: string
cpuLimit:
type: string
memoryLimit:
type: string
status:
type: object
properties:
phase:
type: string
enum:
- Creating
- Initializing
- Running
- Scaling
- Failed
readyNodes:
type: integer
clusterInitialized:
type: boolean
message:
type: string
subresources:
status: {}

5.4 Controller 实现

文件路径:controllers/rediscluster_controller.go

package controllers

import (
"context"
"fmt"
"time"

redisv1alpha1 "github.com/rainlib/redis-operator/api/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
finalizerName = "rainlib.io/rediscluster-finalizer"
)

// RedisClusterReconciler 调谐 RedisCluster 资源
type RedisClusterReconciler struct {
client.Client
Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch

func (r *RedisClusterReconciler) Reconcile(
ctx context.Context,
req ctrl.Request,
) (ctrl.Result, error) {
logger := log.FromContext(ctx)

// 1. 获取 RedisCluster 实例
var rc redisv1alpha1.RedisCluster
if err := r.Get(ctx, req.NamespacedName, &rc); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}

// 2. 处理 Finalizer(删除逻辑)
if !rc.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&rc, finalizerName) {
logger.Info("执行清理逻辑")
// 这里可以添加资源清理逻辑(如外部存储清理)
controllerutil.RemoveFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

// 3. 添加 Finalizer
if !controllerutil.ContainsFinalizer(&rc, finalizerName) {
controllerutil.AddFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}

// 4. 创建/更新 Headless Service
if err := r.reconcileService(ctx, &rc); err != nil {
logger.Error(err, "调和 Service 失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, 0, false, err.Error())
return ctrl.Result{}, err
}

// 5. 创建/更新 StatefulSet
if err := r.reconcileStatefulSet(ctx, &rc); err != nil {
logger.Error(err, "调和 StatefulSet 失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, 0, false, err.Error())
return ctrl.Result{}, err
}

// 6. 检查 Pod 就绪状态
readyNodes, err := r.getReadyNodeCount(ctx, &rc)
if err != nil {
return ctrl.Result{}, err
}

totalNodes := rc.Spec.MasterCount * (1 + rc.Spec.ReplicasPerMaster)

if readyNodes < totalNodes {
logger.Info("等待所有节点就绪",
"ready", readyNodes, "total", totalNodes)
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseCreating, readyNodes, false,
fmt.Sprintf("等待节点就绪 %d/%d", readyNodes, totalNodes))
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}

// 7. 初始化集群(如果尚未初始化)
if !rc.Status.ClusterInitialized {
logger.Info("开始初始化 Redis Cluster")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseInitializing, readyNodes, false,
"正在初始化集群")

if err := r.initializeCluster(ctx, &rc); err != nil {
logger.Error(err, "初始化集群失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, readyNodes, false, err.Error())
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}

r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseRunning, readyNodes, true,
"集群已就绪")
}

return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

// reconcileService 创建或更新 Headless Service
func (r *RedisClusterReconciler) reconcileService(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

desired := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: rc.Name,
Namespace: rc.Namespace,
},
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Selector: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
Ports: []corev1.ServicePort{
{Name: "redis", Port: 6379, TargetPort: intstr.FromInt(6379)},
{Name: "bus", Port: 16379, TargetPort: intstr.FromInt(16379)},
},
},
}

// 设置 OwnerReference
if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

var existing corev1.Service
err := r.Get(ctx, types.NamespacedName{Name: rc.Name, Namespace: rc.Namespace}, &existing)

if errors.IsNotFound(err) {
logger.Info("创建 Headless Service")
return r.Create(ctx, desired)
} else if err != nil {
return err
}

// Service 已存在,更新
existing.Spec.Selector = desired.Spec.Selector
existing.Spec.Ports = desired.Spec.Ports
return r.Update(ctx, &existing)
}

// reconcileStatefulSet 创建或更新 StatefulSet
func (r *RedisClusterReconciler) reconcileStatefulSet(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

totalReplicas := rc.Spec.MasterCount * (1 + rc.Spec.ReplicasPerMaster)

cpuReq := rc.Spec.Resources.CPURequest
memReq := rc.Spec.Resources.MemoryRequest
cpuLim := rc.Spec.Resources.CPULimit
memLim := rc.Spec.Resources.MemoryLimit

if cpuReq == "" {
cpuReq = "100m"
}
if memReq == "" {
memReq = "256Mi"
}
if cpuLim == "" {
cpuLim = "500m"
}
if memLim == "" {
memLim = "512Mi"
}

desired := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: rc.Name,
Namespace: rc.Namespace,
},
Spec: appsv1.StatefulSetSpec{
ServiceName: rc.Name,
Replicas: &totalReplicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "redis",
Image: rc.Spec.Image,
Command: []string{
"/bin/sh", "-c",
fmt.Sprintf(`
redis-server \
--cluster-enabled yes \
--cluster-config-file /data/nodes.conf \
--cluster-node-timeout 5000 \
--appendonly yes \
--bind 0.0.0.0 \
--port 6379 \
--cluster-announce-ip $(hostname).%s.%s.svc.cluster.local \
--cluster-announce-port 6379 \
--cluster-announce-bus-port 16379
`, rc.Name, rc.Namespace),
},
Ports: []corev1.ContainerPort{
{ContainerPort: 6379, Name: "redis"},
{ContainerPort: 16379, Name: "bus"},
},
VolumeMounts: []corev1.VolumeMount{
{Name: "data", MountPath: "/data"},
},
LivenessProbe: &corev1.Probe{
Exec: &corev1.ExecAction{
Command: []string{"redis-cli", "ping"},
},
InitialDelaySeconds: 15,
PeriodSeconds: 5,
},
ReadinessProbe: &corev1.Probe{
Exec: &corev1.ExecAction{
Command: []string{"redis-cli", "ping"},
},
InitialDelaySeconds: 5,
PeriodSeconds: 3,
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpuReq),
corev1.ResourceMemory: resource.MustParse(memReq),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpuLim),
corev1.ResourceMemory: resource.MustParse(memLim),
},
},
},
},
},
},
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "data",
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
},
},
},
}

if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

var existing appsv1.StatefulSet
err := r.Get(ctx, types.NamespacedName{Name: rc.Name, Namespace: rc.Namespace}, &existing)

if errors.IsNotFound(err) {
logger.Info("创建 StatefulSet", "replicas", totalReplicas)
return r.Create(ctx, desired)
} else if err != nil {
return err
}

// StatefulSet 已存在,检查是否需要更新
if *existing.Spec.Replicas != totalReplicas || existing.Spec.Template.Spec.Containers[0].Image != rc.Spec.Image {
logger.Info("更新 StatefulSet")
existing.Spec.Replicas = &totalReplicas
existing.Spec.Template.Spec.Containers[0].Image = rc.Spec.Image
return r.Update(ctx, &existing)
}

return nil
}

// getReadyNodeCount 统计就绪的 Pod 数量
func (r *RedisClusterReconciler) getReadyNodeCount(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) (int32, error) {
var podList corev1.PodList
if err := r.List(ctx, &podList,
client.InNamespace(rc.Namespace),
client.MatchingLabels{
"app": rc.Name,
"redis-cluster": rc.Name,
},
); err != nil {
return 0, err
}

var ready int32
for _, pod := range podList.Items {
for _, cond := range pod.Status.Conditions {
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
ready++
break
}
}
}
return ready, nil
}

// initializeCluster 执行 Redis Cluster 初始化
func (r *RedisClusterReconciler) initializeCluster(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

// 在实际生产中,这里会通过 exec 进入 Pod 执行 redis-cli --cluster create
// 为简化示例,这里记录初始化事件
logger.Info("Redis Cluster 初始化完成",
"masters", rc.Spec.MasterCount,
"replicas", rc.Spec.ReplicasPerMaster)

// 生产环境实现示例(伪代码):
// 1. 收集所有 Pod 的 FQDN
// 2. exec 进入第一个 Pod 执行 redis-cli --cluster create
// 3. 验证集群状态

return nil
}

// updateStatus 更新 RedisCluster 的状态子资源
func (r *RedisClusterReconciler) updateStatus(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
phase redisv1alpha1.ClusterPhase,
readyNodes int32,
initialized bool,
message string,
) {
rc.Status.Phase = phase
rc.Status.ReadyNodes = readyNodes
rc.Status.ClusterInitialized = initialized
rc.Status.Message = message

if err := r.Status().Update(ctx, rc); err != nil {
log.FromContext(ctx).Error(err, "更新状态失败")
}
}

// SetupWithManager 注册 Controller
func (r *RedisClusterReconciler) SetupWithManager(
mgr ctrl.Manager,
) error {
return ctrl.NewControllerManagedBy(mgr).
For(&redisv1alpha1.RedisCluster{}).
Owns(&appsv1.StatefulSet{}).
Owns(&corev1.Service{}).
Complete(r)
}
注意

上面的 Controller 代码中使用了 intstr.FromInt,需要在 import 中添加 "k8s.io/apimachinery/pkg/util/intstr"

5.5 Reconciliation Loop 工作流程

下图展示了 Operator 的 Reconciliation Loop 在不同场景下的工作流程:

5.6 主入口

文件路径:main.go

package main

import (
"os"

redisv1alpha1 "github.com/rainlib/redis-operator/api/v1alpha1"
"github.com/rainlib/redis-operator/controllers"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

var (
scheme = runtime.NewScheme()
)

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(redisv1alpha1.AddToScheme(scheme))
}

func main() {
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: ":8080",
},
HealthProbeBindAddress: ":8081",
LeaderElection: true,
LeaderElectionID: "redis-operator.rainlib.io",
})
if err != nil {
setupLog.Error(err, "无法创建 Manager")
os.Exit(1)
}

if err := (&controllers.RedisClusterReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "无法创建 Controller")
os.Exit(1)
}

if err := mgr.AddHealthzCheck("healthz", healthz.PingHealthz); err != nil {
setupLog.Error(err, "无法设置健康检查")
os.Exit(1)
}

setupLog.Info("启动 Manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "Manager 运行错误")
os.Exit(1)
}
}

六、Operator 部署与测试

6.1 RBAC 配置

API 版本说明:RBAC 资源使用 rbac.authorization.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。详见官方文档:RBAC v1

文件路径:config/rbac/rbac.yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
name: redis-operator
namespace: redis-operator

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: redis-operator-role
rules:
# 管理 RedisCluster CRD
- apiGroups: ["rainlib.io"]
resources: ["redisclusters"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["rainlib.io"]
resources: ["redisclusters/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["rainlib.io"]
resources: ["redisclusters/finalizers"]
verbs: ["update"]
# 管理 StatefulSet
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# 管理 Service
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# 读取 Pod 状态
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# 创建 Event
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: redis-operator-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: redis-operator-role
subjects:
- kind: ServiceAccount
name: redis-operator
namespace: redis-operator

6.2 部署 Operator

API 版本说明:Deployment 使用 apps/v1,ServiceAccount 使用 v1,均为 Kubernetes v1.34 稳定版本。详见官方文档:Deployment v1ServiceAccount v1

文件路径:config/deployment/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-operator
namespace: redis-operator
labels:
app: redis-operator
spec:
replicas: 1
selector:
matchLabels:
app: redis-operator
template:
metadata:
labels:
app: redis-operator
spec:
serviceAccountName: redis-operator
containers:
- name: redis-operator
image: rainlib/redis-operator:latest
args:
- --leader-elect
ports:
- containerPort: 8080
name: metrics
- containerPort: 9443
name: webhook
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true

部署步骤:

# 1. 创建命名空间
kubectl create namespace redis-operator

# 2. 安装 CRD
kubectl apply -f config/crd/redisclusters.rainlib.io.yaml

# 3. 安装 RBAC
kubectl apply -f config/rbac/rbac.yaml

# 4. 部署 Operator
kubectl apply -f config/deployment/deployment.yaml

# 5. 验证 Operator 运行状态
kubectl get pods -n redis-operator
kubectl logs -n redis-operator -l app=redis-operator

6.3 创建 RedisCluster 实例

文件路径:config/samples/rediscluster-sample.yaml

apiVersion: rainlib.io/v1alpha1
kind: RedisCluster
metadata:
name: my-redis
namespace: redis
spec:
masterCount: 3
replicasPerMaster: 1
image: redis:7.2.4
resources:
cpuRequest: "200m"
memoryRequest: "512Mi"
cpuLimit: "1000m"
memoryLimit: "1Gi"
# 创建命名空间
kubectl create namespace redis

# 创建 RedisCluster 实例
kubectl apply -f config/samples/rediscluster-sample.yaml

# 观察 Operator 行为
kubectl get rediscluster -n redis -w

# 查看 Operator 日志
kubectl logs -n redis-operator -l app=redis-operator -f

# 查看创建的资源
kubectl get statefulset,service,pod -n redis

6.4 验证 Operator 行为

自动扩缩容测试:

# 修改 replicasPerMaster 从 1 到 2
kubectl patch rediscluster my-redis -n redis --type=merge \
-p '{"spec":{"replicasPerMaster":2}}'

# 观察 Operator 自动更新 StatefulSet
kubectl get pods -n redis -w
kubectl logs -n redis-operator -l app=redis-operator -f

故障恢复测试:

# 模拟 Pod 故障
kubectl delete pod my-redis-0 -n redis

# 观察 StatefulSet 自动重建 Pod
kubectl get pods -n redis -w

# 验证 RedisCluster 状态
kubectl get rediscluster my-redis -n redis -o yaml

七、高级 Operator 模式

7.1 Finalizer 机制

Finalizer 是 Kubernetes 中用于防止资源被意外删除的机制。当资源带有 Finalizer 时,K8s 不会立即删除该资源,而是先设置 deletionTimestamp,等待 Controller 执行清理逻辑后再移除 Finalizer,最终完成删除。

// 处理删除逻辑
if !rc.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&rc, finalizerName) {
// 1. 执行自定义清理逻辑
if err := r.cleanupExternalResources(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
// 2. 移除 Finalizer,允许 K8s 完成删除
controllerutil.RemoveFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
生产环境注意事项

Finalizer 中的清理逻辑必须保证幂等性(Idempotent),因为 Reconcile 可能会被多次调用。如果清理逻辑失败且不移除 Finalizer,资源将一直处于"正在删除"状态,成为僵尸资源。

7.2 OwnerReference 与级联删除

通过 controllerutil.SetControllerReference 设置的 OwnerReference 实现了级联删除:当 RedisCluster 被删除时,其创建的所有 StatefulSet、Service 等子资源会自动被 K8s 回收。

// 设置 OwnerReference
if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

OwnerReference 的传播策略有两种:

策略行为适用场景
Background(默认)立即删除父资源,后台异步删除子资源大多数场景
Foreground先删除所有子资源,再删除父资源需要确保子资源被正确清理的场景

7.3 Webhook(Admission / Mutating)

Webhook 允许 Operator 在资源创建或更新时进行拦截和修改,实现更精细的控制。

API 版本说明:ValidatingWebhookConfiguration 和 MutatingWebhookConfiguration 使用 admissionregistration.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。详见官方文档:ValidatingWebhookConfigurationMutatingWebhookConfiguration

Validating Webhook(验证 webhook):拒绝不合法的请求。

// 验证 masterCount 必须为奇数
func (r *RedisCluster) ValidateCreate() (admission.Warnings, error) {
if r.Spec.MasterCount%2 == 0 {
return nil, fmt.Errorf("masterCount must be an odd number, got %d", r.Spec.MasterCount)
}
return nil, nil
}

func (r *RedisCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
oldRC := old.(*RedisCluster)
if oldRC.Spec.MasterCount != r.Spec.MasterCount {
return nil, fmt.Errorf("masterCount is immutable after creation")
}
return nil, nil
}

Mutating Webhook(变更 webhook):自动修改请求内容。

// 自动设置默认值
func (r *RedisCluster) Default() {
if r.Spec.Image == "" {
r.Spec.Image = "redis:7.2.4"
}
if r.Spec.ReplicasPerMaster == 0 {
r.Spec.ReplicasPerMaster = 1
}
}

7.4 Leader Election(高可用)

当 Operator 部署多个副本时,Leader Election 确保只有一个实例在执行 Reconcile,其余实例处于 Standby 状态。

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
LeaderElection: true, // 启用 Leader Election
LeaderElectionID: "redis-operator.rainlib.io", // Leader Election 的标识
LeaderElectionNamespace: "redis-operator", // 选举的命名空间
LeaseDuration: &metav1.Duration{Seconds: 15}, // 租约时长
RenewDeadline: &metav1.Duration{Seconds: 10}, // 续约截止时间
RetryPeriod: &metav1.Duration{Seconds: 2}, // 重试间隔
})
Leader Election 原理

Leader Election 基于 K8s 的 Lease(租约)资源实现。Manager 启动时会尝试创建一个 Lease 对象,创建成功的实例成为 Leader 并定期续约。如果 Leader 宕机无法续约,其他 Standby 实例会竞争成为新 Leader。


八、生产环境最佳实践

8.1 Operator 开发规范

规范说明
最小权限原则RBAC 仅授予必要的权限,避免使用 cluster-admin
幂等 Reconcile所有调谐逻辑必须幂等,确保重复执行结果一致
状态子资源分离使用 Status 子资源记录实际状态,与 Spec 分离
事件记录关键操作必须通过 EventRecorder 记录事件,便于审计和排查
错误处理区分暂时性错误(Requeue)和永久性错误(不 Requeue)
日志规范使用结构化日志,包含关键上下文(资源名、命名空间等)

8.2 监控与日志

// 使用 Prometheus 指标暴露 Operator 运行状态
import (
"sigs.k8s.io/controller-runtime/pkg/metrics"
"github.com/prometheus/client_golang/prometheus"
)

var (
reconcileTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "redis_operator_reconcile_total",
Help: "Total number of reconciliation operations",
},
[]string{"namespace", "name"},
)
reconcileErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "redis_operator_reconcile_errors_total",
Help: "Total number of reconciliation errors",
},
[]string{"namespace", "name"},
)
)

func init() {
metrics.Registry.MustRegister(reconcileTotal, reconcileErrors)
}

推荐的监控大盘指标:

  • Reconcile 成功/失败次数
  • Reconcile 耗时分布
  • 每个 RedisCluster 的 Phase 分布
  • Redis 节点的内存、连接数、命令处理量
  • PVC 使用率

8.3 版本升级策略

API 版本说明:CRD 版本管理使用 apiextensions.k8s.io/v1,支持多版本共存、版本转换和弃用标记。详见官方文档:CRD Versioning

# 使用 CRD 版本管理 API 演进
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: redisclusters.rainlib.io
spec:
versions:
- name: v1alpha1
served: true # 仍然提供服务
storage: false # 不再作为存储版本
deprecated: true # 标记为已弃用
deprecationWarning: "rainlib.io/v1alpha1 RedisCluster is deprecated; use rainlib.io/v1"
- name: v1
served: true
storage: true # 作为存储版本
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
masterCount:
type: integer
minimum: 1
# v1 新增字段
persistence:
type: object
properties:
enabled:
type: boolean
storageClass:
type: string
size:
type: string
CRD 版本升级注意事项
  1. 不可删除存储版本:一旦某个版本被标记为 storage: true,就不能直接删除该版本,必须先创建新版本并迁移数据。
  2. Webhook 版本转换:如果需要在不同 CRD 版本间自动转换,需要实现 Conversion Webhook。
  3. 向后兼容:新增字段应该有默认值,确保旧版本 CR 在新版本 Controller 下仍能正常工作。

九、本章小结与系列总结

核心知识回顾

本文从有状态应用的核心挑战出发,系统性地讲解了 StatefulSet 和 Operator 两个关键概念:

StatefulSet 解决的问题:

  • 稳定的网络标识(Headless Service + 固定 DNS)
  • 持久的存储(VolumeClaimTemplate + PVC)
  • 有序的部署与扩缩容(顺序创建/逆序删除)

Operator 解决的问题:

  • 将运维知识编码为自动化控制器
  • 通过 CRD 扩展 K8s API,引入应用特定的资源类型
  • 通过 Reconciliation Loop 持续调谐实际状态与期望状态
  • 自动处理集群初始化、扩缩容、故障恢复等复杂操作

Operator 高级模式:

  • Finalizer:确保资源删除前的清理逻辑被执行
  • OwnerReference:实现级联删除,避免资源泄漏
  • Webhook:在 API 层面进行验证和变更
  • Leader Election:支持多副本高可用部署

Kubernetes 全景解析系列总结

至此,Kubernetes 全景解析系列的核心内容已经覆盖完毕。让我们回顾整个系列的知识体系:

章节主题核心收获
(0)架构设计与核心概念理解 K8s 的设计哲学与控制面/数据面架构
(1)工作负载管理掌握 Pod/Deployment/StatefulSet/DaemonSet 的使用场景
(2)网络模型理解 CNI、Service、Ingress 的工作原理
(3)存储体系与配置管理掌握 PV/PVC/StorageClass/ConfigMap/Secret
(4)调度策略理解节点选择、亲和性、污点与容忍
(5)安全机制掌握 RBAC、NetworkPolicy、PodSecurity
(6)Helm 包管理学会使用 Helm 打包和分发应用
(7)有状态应用与 Operator掌握 StatefulSet 和 Operator 开发

Kubernetes 的学习是一个持续深化的过程。掌握了本系列的内容,你已经具备了在生产和开发环境中使用 K8s 的核心能力。下一步建议:

  1. 深入实践:选择一个真实的有状态应用(如 PostgreSQL、Kafka),尝试为其编写 Operator
  2. 参与社区:关注 CNCF 生态,学习优秀的开源 Operator 实现(如 Prometheus Operator、Argo CD Operator)
  3. 探索高级主题:Service Mesh(Istio/Linkerd)、Serverless(Knative)、GitOps(Argo CD/Flux)等

"Kubernetes 不是终点,而是起点。理解了它的设计哲学,你就能更好地理解整个云原生生态。"


系列导航:

Kubernetes 全景解析 (6):生产级微服务架构实战

· 阅读需 38 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"纸上得来终觉浅,绝知此事要躬行。"

在前面的系列文章中,我们系统学习了 K8s 的架构设计、工作负载管理、网络模型、存储体系与配置管理。现在是时候将这些知识串联起来,完成一次从零到生产的完整实战演练。

本文将以一个电商微服务系统为蓝本,手把手带你完成以下全流程:

  • 多服务编排与部署
  • Ingress 七层路由与 TLS 终止
  • HPA 弹性伸缩与高可用保障
  • 健康检查与优雅停机
  • Prometheus + Grafana 监控体系
  • ArgoCD GitOps 持续交付

所有 YAML 配置均基于 Kubernetes v1.34(Of Wind & Will)官方 API 验证,可直接应用于你的集群。

API 版本说明

本文所有 YAML 配置使用的 API 版本均经过 Kubernetes v1.34 官方文档校验:

资源类型apiVersion官方文档
Deployment / StatefulSetapps/v1Workload Resources
Servicev1Service Resources
Ingress / IngressClassnetworking.k8s.io/v1Ingress
HorizontalPodAutoscalerautoscaling/v2HPA v2
PodDisruptionBudgetpolicy/v1PDB
ResourceQuota / LimitRangev1Config and Storage Resources
RBACrbac.authorization.k8s.io/v1Authorization Resources
ServiceMonitormonitoring.coreos.com/v1Prometheus Operator
ArgoCD Applicationargoproj.io/v1alpha1ArgoCD CRDs
StorageClassstorage.k8s.io/v1Storage Resources

一、实战场景概述

1.1 电商微服务架构设计

我们以一个典型的电商系统为例,将其拆分为以下五个核心微服务:

服务职责技术栈端口
API Gateway统一入口、路由转发、限流熔断APISIX / Nginx80/443
用户服务注册、登录、用户信息管理Go (Gin)8080
商品服务商品 CRUD、分类管理、搜索Java (Spring Boot)8081
订单服务下单、支付回调、订单查询Node.js (Express)8082
支付服务支付对接、退款、对账Go (Gin)8083

底层依赖两个有状态服务:

服务职责端口
PostgreSQL关系型数据库(用户、订单)5432
Redis缓存、会话管理、分布式锁6379

1.2 微服务架构拓扑

1.3 技术栈选择

层级技术选型选型理由
网关层APISIX高性能、支持 gRPC 转发、插件生态丰富
服务层Go + Java + Node.js模拟真实多语言微服务环境
数据层PostgreSQL 16 + Redis 7成熟稳定、社区活跃
监控层Prometheus + Grafana云原生监控事实标准
日志层Fluent Bit轻量级、资源占用低
部署层ArgoCDGitOps 声明式持续交付

二、基础设施准备

2.1 命名空间与资源配额

生产环境中,不同团队的服务应该隔离在不同的命名空间中,并通过 ResourceQuota 限制资源使用。

k8s/00-namespace/namespace.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: microservices
labels:
app.kubernetes.io/part-of: ecommerce
app.kubernetes.io/managed-by: argocd
---
apiVersion: v1
kind: Namespace
metadata:
name: data
labels:
app.kubernetes.io/part-of: ecommerce
app.kubernetes.io/managed-by: argocd
---
apiVersion: v1
kind: Namespace
metadata:
name: gateway
labels:
app.kubernetes.io/part-of: ecommerce
app.kubernetes.io/managed-by: argocd
k8s/00-namespace/resource-quota.yaml
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: microservices-quota
namespace: microservices
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "50"
services: "20"
persistentvolumeclaims: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: microservices
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "2"
memory: "2Gi"
min:
cpu: "50m"
memory: "64Mi"
type: Container
生产环境注意事项

ResourceQuota 的值应根据集群总容量和业务优先级进行合理分配。建议预留 20% 的资源缓冲,避免某个命名空间的突发流量影响其他业务。LimitRange 确保即使开发者忘记设置资源限制,容器也不会无限制地消耗节点资源。

2.2 ConfigMap 与 Secret 配置管理

将配置从镜像中分离出来,是 12-Factor App 的核心原则之一。

k8s/01-config/configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: microservices
data:
# 数据库连接配置
DB_HOST: "postgresql.data.svc.cluster.local"
DB_PORT: "5432"
DB_NAME: "ecommerce"
DB_POOL_SIZE: "20"
DB_CONNECTION_TIMEOUT: "30"

# Redis 连接配置
REDIS_HOST: "redis.data.svc.cluster.local"
REDIS_PORT: "6379"
REDIS_DB: "0"
REDIS_POOL_SIZE: "50"

# 日志配置
LOG_LEVEL: "info"
LOG_FORMAT: "json"

# 服务间调用超时
SERVICE_TIMEOUT: "10s"
SERVICE_RETRY: "3"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gateway-config
namespace: gateway
data:
# 网关路由配置
UPSTREAM_USER: "user-service.microservices.svc.cluster.local:8080"
UPSTREAM_PRODUCT: "product-service.microservices.svc.cluster.local:8081"
UPSTREAM_ORDER: "order-service.microservices.svc.cluster.local:8082"
UPSTREAM_PAYMENT: "payment-service.microservices.svc.cluster.local:8083"
k8s/01-config/secret.yaml
---
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: microservices
type: Opaque
stringData:
DB_USERNAME: "ecommerce_app"
DB_PASSWORD: "S3cureP@ssw0rd!2026"
---
apiVersion: v1
kind: Secret
metadata:
name: redis-credentials
namespace: microservices
type: Opaque
stringData:
REDIS_PASSWORD: "R3disS3cret!2026"
---
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
namespace: gateway
type: kubernetes.io/tls
stringData:
tls.crt: |
-----BEGIN CERTIFICATE-----
# 此处替换为你的 TLS 证书
-----END CERTIFICATE-----
tls.key: |
-----BEGIN PRIVATE KEY-----
# 此处替换为你的 TLS 私钥
-----END PRIVATE KEY-----
安全警告

切勿将 Secret 明文提交到 Git 仓库! 生产环境应使用以下方案之一:

  • Sealed Secrets(Bitn Labs):加密后可安全提交 Git
  • External Secrets Operator:从 AWS Secrets Manager / HashiCorp Vault 同步
  • SOPS(Mozilla):基于 GPG/KMS 的加密工具

本文示例中的 stringData 仅用于演示,生产环境请务必使用加密方案。

2.3 PV/PVC 持久化存储规划

k8s/02-storage/storageclass.yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs # 根据云厂商调整
parameters:
type: gp3
fsType: ext4
iopsPerGB: "50"
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-hdd
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
fsType: ext4
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode: Immediate
k8s/02-storage/pvc.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-data
namespace: data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-data
namespace: data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi

三、核心服务部署

3.1 API Gateway 部署

网关是整个系统的统一入口,负责路由转发、限流、熔断和认证。

k8s/03-services/gateway-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
namespace: gateway
labels:
app: api-gateway
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: api-gateway
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: api-gateway
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: api-gateway
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 60
containers:
- name: apisix
image: apache/apisix:3.9.0-debian
ports:
- name: http
containerPort: 9080
protocol: TCP
- name: https
containerPort: 9443
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
envFrom:
- configMapRef:
name: gateway-config
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: 9090
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: 9090
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: api-gateway
namespace: gateway
labels:
app: api-gateway
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 9080
protocol: TCP
- name: https
port: 443
targetPort: 9443
protocol: TCP
selector:
app: api-gateway

3.2 用户服务部署

k8s/03-services/user-service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: microservices
labels:
app: user-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: user-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: user-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: user-service
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 30
containers:
- name: user-service
image: registry.example.com/ecommerce/user-service:v1.0.0
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: grpc
containerPort: 9090
protocol: TCP
env:
- name: SERVICE_NAME
value: "user-service"
- name: SERVICE_PORT
value: "8080"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: app-config
key: DB_NAME
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: REDIS_HOST
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
---
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: microservices
labels:
app: user-service
spec:
type: ClusterIP
ports:
- name: http
port: 8080
targetPort: 8080
protocol: TCP
- name: grpc
port: 9090
targetPort: 9090
protocol: TCP
selector:
app: user-service

3.3 订单服务部署

k8s/03-services/order-service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: microservices
labels:
app: order-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: order-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: order-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8082"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: order-service
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 60
containers:
- name: order-service
image: registry.example.com/ecommerce/order-service:v1.0.0
ports:
- name: http
containerPort: 8082
protocol: TCP
env:
- name: SERVICE_NAME
value: "order-service"
- name: SERVICE_PORT
value: "8082"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: app-config
key: DB_NAME
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: REDIS_HOST
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
- name: USER_SERVICE_URL
value: "http://user-service.microservices.svc.cluster.local:8080"
- name: PRODUCT_SERVICE_URL
value: "http://product-service.microservices.svc.cluster.local:8081"
- name: PAYMENT_SERVICE_URL
value: "http://payment-service.microservices.svc.cluster.local:8083"
resources:
requests:
cpu: 300m
memory: 384Mi
limits:
cpu: "1"
memory: 768Mi
livenessProbe:
httpGet:
path: /healthz
port: 8082
initialDelaySeconds: 20
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8082
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: microservices
labels:
app: order-service
spec:
type: ClusterIP
ports:
- name: http
port: 8082
targetPort: 8082
protocol: TCP
selector:
app: order-service

3.4 商品服务部署

k8s/03-services/product-service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
namespace: microservices
labels:
app: product-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: product-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: product-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8081"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: product-service
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 30
containers:
- name: product-service
image: registry.example.com/ecommerce/product-service:v1.0.0
ports:
- name: http
containerPort: 8081
protocol: TCP
env:
- name: SERVICE_NAME
value: "product-service"
- name: SERVICE_PORT
value: "8081"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: app-config
key: DB_NAME
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: REDIS_HOST
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
resources:
requests:
cpu: 300m
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
---
apiVersion: v1
kind: Service
metadata:
name: product-service
namespace: microservices
labels:
app: product-service
spec:
type: ClusterIP
ports:
- name: http
port: 8081
targetPort: 8081
protocol: TCP
selector:
app: product-service

3.5 支付服务部署

k8s/03-services/payment-service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: microservices
labels:
app: payment-service
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: payment-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: payment-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8083"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: payment-service
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 60
containers:
- name: payment-service
image: registry.example.com/ecommerce/payment-service:v1.0.0
ports:
- name: http
containerPort: 8083
protocol: TCP
env:
- name: SERVICE_NAME
value: "payment-service"
- name: SERVICE_PORT
value: "8083"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: app-config
key: DB_NAME
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: PAYMENT_GATEWAY_KEY
valueFrom:
secretKeyRef:
name: payment-secret
key: GATEWAY_API_KEY
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: 8083
initialDelaySeconds: 15
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8083
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: payment-service
namespace: microservices
labels:
app: payment-service
spec:
type: ClusterIP
ports:
- name: http
port: 8083
targetPort: 8083
protocol: TCP
selector:
app: payment-service

3.6 数据库部署(StatefulSet)

有状态服务使用 StatefulSet 部署,确保稳定的网络标识和持久化存储。

k8s/03-services/postgresql-statefulset.yaml
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: data
labels:
app: postgresql
spec:
serviceName: postgresql-headless
replicas: 1
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9187"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 60
containers:
- name: postgresql
image: postgres:16-alpine
ports:
- name: postgresql
containerPort: 5432
protocol: TCP
env:
- name: POSTGRES_DB
value: "ecommerce"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
optional: false
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
optional: false
- name: PGDATA
value: "/var/lib/postgresql/data/pgdata"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
volumeMounts:
- name: postgresql-data
mountPath: /var/lib/postgresql/data
livenessProbe:
exec:
command:
- pg_isready
- -U
- ecommerce_app
- -d
- ecommerce
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- pg_isready
- -U
- ecommerce_app
- -d
- ecommerce
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "pg_ctl stop -m fast"]
- name: postgres-exporter
image: prometheuscommunity/postgres-exporter:v0.15.0
ports:
- name: metrics
containerPort: 9187
protocol: TCP
env:
- name: DATA_SOURCE_NAME
value: "postgresql://ecommerce_app:S3cureP@ssw0rd!2026@localhost:5432/ecommerce?sslmode=disable"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeClaimTemplates:
- metadata:
name: postgresql-data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
---
apiVersion: v1
kind: Service
metadata:
name: postgresql-headless
namespace: data
labels:
app: postgresql
spec:
type: ClusterIP
clusterIP: None
ports:
- name: postgresql
port: 5432
targetPort: 5432
protocol: TCP
selector:
app: postgresql
---
apiVersion: v1
kind: Service
metadata:
name: postgresql
namespace: data
labels:
app: postgresql
spec:
type: ClusterIP
ports:
- name: postgresql
port: 5432
targetPort: 5432
protocol: TCP
selector:
app: postgresql
k8s/03-services/redis-statefulset.yaml
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
namespace: data
labels:
app: redis
spec:
serviceName: redis-headless
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9121"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 30
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- --requirepass
- $(REDIS_PASSWORD)
- --maxmemory
- 1gb
- --maxmemory-policy
- allkeys-lru
- --appendonly
- "yes"
ports:
- name: redis
containerPort: 6379
protocol: TCP
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
volumeMounts:
- name: redis-data
mountPath: /data
livenessProbe:
exec:
command:
- redis-cli
- -a
- $(REDIS_PASSWORD)
- ping
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- redis-cli
- -a
- $(REDIS_PASSWORD)
- ping
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "redis-cli -a $(REDIS_PASSWORD) SHUTDOWN NOSAVE"]
- name: redis-exporter
image: oliver006/redis_exporter:v1.58.0
ports:
- name: metrics
containerPort: 9121
protocol: TCP
env:
- name: REDIS_ADDR
value: "redis://localhost:6379"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
name: redis-headless
namespace: data
labels:
app: redis
spec:
type: ClusterIP
clusterIP: None
ports:
- name: redis
port: 6379
targetPort: 6379
protocol: TCP
selector:
app: redis
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: data
labels:
app: redis
spec:
type: ClusterIP
ports:
- name: redis
port: 6379
targetPort: 6379
protocol: TCP
selector:
app: redis
StatefulSet vs Deployment

对于数据库和缓存等有状态服务,务必使用 StatefulSet 而非 Deployment。StatefulSet 提供了以下保证:

  • 稳定的网络标识:每个 Pod 有固定的 DNS 名称(如 postgresql-0.postgresql-headless.data.svc.cluster.local
  • 有序的部署和扩缩容:Pod 按序号顺序创建和删除
  • 稳定的持久化存储:通过 volumeClaimTemplates,每个 Pod 绑定独立的 PVC,Pod 重建后自动重新挂载

四、服务间通信与配置

4.1 CoreDNS 服务发现

Kubernetes 内置的 CoreDNS 为每个 Service 自动创建 DNS 记录,服务间可以通过标准的 FQDN 进行互相访问:

记录格式示例作用域
<service>user-service同命名空间
<service>.<namespace>user-service.microservices跨命名空间
<service>.<namespace>.svc.cluster.localuser-service.microservices.svc.cluster.local全集群
最佳实践

始终使用完整的跨命名空间 FQDN(如 user-service.microservices.svc.cluster.local),即使服务在同一个命名空间中。这样在后续调整命名空间划分时,不需要修改代码和配置。

4.2 健康检查设计原则

健康检查是保障服务可用性的关键机制。K8s 提供了三种探针:

探针类型用途失败后果
Liveness Probe检测容器是否存活重启容器
Readiness Probe检测是否可以接收流量从 Service Endpoints 中移除
Startup Probe检测应用是否启动完成启动完成前禁用其他探针

以下是健康检查的配置要点:

k8s/04-communication/health-check-example.yaml
# 以订单服务为例,展示完整的健康检查配置
spec:
containers:
- name: order-service
# ...其他配置...
startupProbe:
httpGet:
path: /healthz
port: 8082
failureThreshold: 30 # 最多等待 30 * 10s = 300s
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8082
initialDelaySeconds: 0 # startupProbe 完成后才开始
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3 # 连续 3 次失败则重启
successThreshold: 1
readinessProbe:
httpGet:
path: /readyz
port: 8082
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3 # 连续 3 次失败则摘除流量
successThreshold: 1
生产环境注意事项
  1. Liveness 和 Readiness 必须使用不同的端点。Liveness 检测"进程是否活着",Readiness 检测"是否准备好接收请求"。如果两者使用同一端点,可能导致级联故障——例如数据库短暂不可用时,所有 Pod 同时被 Liveness 重启。
  2. 设置合理的 timeoutSeconds。过短的超时时间会导致误判,建议设置为 P99 响应时间的 2-3 倍。
  3. 对于 Java 等启动较慢的服务,务必配置 Startup Probe,否则 Liveness Probe 可能在应用启动期间就触发重启。

4.3 优雅停机

优雅停机确保 Pod 在被终止时,能够完成正在处理的请求并安全释放资源。

v1.34 新特性:原生 Sleep Lifecycle Hook

Kubernetes v1.34 引入了原生的 sleep action 用于 PreStop 和 PostStart lifecycle hooks,无需再通过 exec 执行 sleep 命令。这提供了更简洁和可靠的优雅停机方式。

详见官方文档:Container Lifecycle Hooks

推荐方式(v1.34+):使用原生 Sleep Hook

k8s/04-communication/graceful-shutdown-v134.yaml
spec:
terminationGracePeriodSeconds: 60 # 给予 60 秒的优雅停机时间
containers:
- name: order-service
lifecycle:
preStop:
sleep:
seconds: 15 # 原生 sleep action
# ...其他配置...

传统方式(v1.34 之前):使用 Exec Hook

k8s/04-communication/graceful-shutdown-legacy.yaml
spec:
terminationGracePeriodSeconds: 60 # 给予 60 秒的优雅停机时间
containers:
- name: order-service
lifecycle:
preStop:
exec:
# 先等待 15 秒,让 Service Endpoints 更新
# 然后发送 SIGTERM 信号,应用开始优雅关闭
command: ["/bin/sh", "-c", "sleep 15"]
# ...其他配置...

优雅停机的完整流程如下:

  1. K8s 向 Pod 发送 SIGTERM 信号
  2. preStop Hook 执行 sleep 15(或原生 sleep action),等待 15 秒(让 Ingress/Service 感知到 Pod 不可用)
  3. 应用收到 SIGTERM 后,停止接收新请求,处理完正在进行的请求
  4. 应用关闭数据库连接池、释放资源
  5. 如果超过 terminationGracePeriodSeconds(60s)仍未退出,K8s 发送 SIGKILL 强制终止
为什么 preStop 需要 sleep?

K8s 在执行 preStop Hook 的同时,会从 Service Endpoints 中移除该 Pod。但 Ingress Controller 和上游代理可能还有缓存,需要一定时间才能感知到变化。sleep 15 确保在应用真正开始关闭之前,所有上游组件都已经停止向该 Pod 转发流量。


五、Ingress 七层路由配置

5.1 Ingress Controller 部署

我们使用 APISIX Ingress Controller 作为七层路由组件。

API 版本说明:Ingress 和 IngressClass 使用 networking.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。networking.k8s.io/v1beta1 已在 v1.22 中移除,请确保使用 v1。详见官方文档:Ingress v1

k8s/05-ingress/ingress-controller.yaml
---
# RBAC 配置
# apiVersion: rbac.authorization.k8s.io/v1 是 Kubernetes v1.34 中的稳定版本
# 详见官方文档:https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/role-v1/
apiVersion: v1
kind: ServiceAccount
metadata:
name: apisix-ingress-controller
namespace: gateway
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: apisix-ingress-controller
rules:
- apiGroups: [""]
resources: ["secrets", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apisix.apache.org"]
resources: ["apisixroutes", "apisixupstreams", "apisixtlsconfigs", "apisixclusters", "apisixpluginconfigs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: apisix-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: apisix-ingress-controller
subjects:
- kind: ServiceAccount
name: apisix-ingress-controller
namespace: gateway
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: apisix-ingress-controller
namespace: gateway
labels:
app: apisix-ingress-controller
spec:
replicas: 2
selector:
matchLabels:
app: apisix-ingress-controller
template:
metadata:
labels:
app: apisix-ingress-controller
spec:
serviceAccountName: apisix-ingress-controller
containers:
- name: apisix-ingress-controller
image: apache/apisix-ingress-controller:1.8.0
args:
- --ingress-class
- apisix
- --apisix-admin-api-version
- v3
- --log-level
- info
- --http-port
- "8080"
env:
- name: APISIX_ADMIN_API_URL
value: "http://apisix-admin.gateway.svc.cluster.local:9180/apisix/admin"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5

5.2 Ingress 路由规则

k8s/05-ingress/ingress-routes.yaml
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: apisix
spec:
controller: apache.org/apisix-ingress-controller
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ecommerce-ingress
namespace: gateway
annotations:
kubernetes.io/ingress.class: apisix
# 启用 CORS
apisix.apache.org/enable-cors: "true"
apisix.apache.org/cors-allow-origin: "https://shop.example.com"
apisix.apache.org/cors-allow-methods: "GET,POST,PUT,DELETE,OPTIONS"
apisix.apache.org/cors-allow-headers: "Authorization,Content-Type"
# 全局限流
apisix.apache.org/plugin-limit-count: |
{
"count": 1000,
"time_window": 1,
"rejected_code": 429,
"key": "remote_addr"
}
spec:
ingressClassName: apisix
tls:
- hosts:
- api.example.com
secretName: tls-secret
rules:
- host: api.example.com
http:
paths:
# 用户服务路由
- path: /api/v1/users
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
# 商品服务路由
- path: /api/v1/products
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
# 订单服务路由
- path: /api/v1/orders
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
# 支付服务路由
- path: /api/v1/payments
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
路由设计说明

在上述配置中,所有业务路由都指向 API Gateway,由 Gateway 负责将请求转发到具体的后端服务。这种设计的好处是:

  • Gateway 统一处理认证、限流、熔断等横切关注点
  • 后端服务不需要暴露到 Ingress 层
  • 路由规则变更只需修改 Gateway 配置,无需修改 Ingress

六、弹性伸缩与高可用

6.1 HPA 弹性伸缩

Horizontal Pod Autoscaler 根据监控指标自动调整 Pod 副本数,是应对流量波动的核心机制。

API 版本说明:HPA 使用 autoscaling/v2,这是 Kubernetes v1.34 中的稳定版本,支持资源指标(CPU/内存)、Pods 指标和外部指标,以及 behavior 字段精细控制扩缩容行为。详见官方文档:HorizontalPodAutoscaler v2

k8s/06-scalability/hpa.yaml
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-service-hpa
namespace: microservices
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
minReplicas: 3
maxReplicas: 20
metrics:
# CPU 利用率目标
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# 内存利用率目标
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # 扩容稳定窗口
policies:
- type: Pods
value: 4 # 每次最多扩容 4 个 Pod
periodSeconds: 60
- type: Percent
value: 100 # 或每次扩容当前副本数的 100%
periodSeconds: 60
selectPolicy: Max # 取两个策略中更激进的
scaleDown:
stabilizationWindowSeconds: 300 # 缩容稳定窗口 5 分钟
policies:
- type: Pods
value: 2 # 每次最多缩容 2 个 Pod
periodSeconds: 120
selectPolicy: Min # 取两个策略中更保守的
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
namespace: microservices
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
# 自定义指标:HTTP 请求延迟 P99
- type: Pods
pods:
metric:
name: http_request_duration_seconds_p99
target:
type: AverageValue
averageValue: "500m" # 500ms
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 6
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 2
periodSeconds: 120
生产环境注意事项
  1. 扩容要快,缩容要慢scaleUp.stabilizationWindowSeconds 应设置较小值(30-60s),scaleDown.stabilizationWindowSeconds 应设置较大值(300-600s),避免因流量短暂下降导致频繁缩容。
  2. 设置合理的 minReplicas:最小副本数不应低于 2,且应通过 Pod 反亲和性分布在不同节点上,确保单节点故障不影响服务可用性。
  3. 自定义指标需要安装 Metrics Server 和 Prometheus Adapter,否则 HPA 无法获取自定义指标。

6.2 Pod 反亲和性与 PDB

API 版本说明:PodDisruptionBudget 使用 policy/v1,这是 Kubernetes v1.34 中的稳定版本。PDB 新增了 unhealthyPodEvictionPolicy 字段,支持 IfHealthyBudgetAlwaysAllow 两种策略,更灵活地控制不健康 Pod 的驱逐行为。详见官方文档:PodDisruptionBudget v1

k8s/06-scalability/pdb.yaml
---
# 确保用户服务至少有 2 个可用 Pod
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: user-service-pdb
namespace: microservices
spec:
minAvailable: 2
selector:
matchLabels:
app: user-service
---
# 确保订单服务至少有 50% 的 Pod 可用
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: order-service-pdb
namespace: microservices
spec:
maxUnavailable: "50%"
selector:
matchLabels:
app: order-service
---
# 确保支付服务至少有 1 个可用 Pod
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payment-service-pdb
namespace: microservices
spec:
minAvailable: 1
selector:
matchLabels:
app: payment-service
PDB 与节点维护

当需要对集群节点进行维护(如升级 K8s 版本、更换硬件)时,PDB 确保驱逐操作不会导致服务可用副本数低于阈值。如果没有 PDB,kubectl drain 可能会一次性驱逐所有 Pod,导致服务中断。


七、监控与日志集成

7.1 Prometheus ServiceMonitor

API 版本说明:ServiceMonitor 使用 monitoring.coreos.com/v1,这是 Prometheus Operator 提供的 CRD,用于定义 Service 的监控目标。详见官方文档:ServiceMonitor CRD

k8s/07-monitoring/servicemonitor.yaml
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: microservices-monitor
namespace: microservices
labels:
release: prometheus # 匹配 Prometheus Operator 的 serviceMonitorSelector
spec:
namespaceSelector:
matchNames:
- microservices
- gateway
selector:
matchLabels:
app.kubernetes.io/part-of: ecommerce
endpoints:
- port: http
path: /metrics
interval: 15s
scrapeTimeout: 10s
honorLabels: true
relabelings:
- sourceLabels: [__meta_kubernetes_pod_name]
targetLabel: pod
- sourceLabels: [__meta_kubernetes_namespace]
targetLabel: namespace
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: data-services-monitor
namespace: data
labels:
release: prometheus
spec:
namespaceSelector:
matchNames:
- data
selector:
matchLabels:
app.kubernetes.io/part-of: ecommerce
endpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s

7.2 Grafana Dashboard 配置

k8s/07-monitoring/grafana-dashboard.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ecommerce-grafana-dashboards
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
microservices-overview.json: |
{
"dashboard": {
"title": "电商微服务总览",
"panels": [
{
"title": "请求 QPS",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(http_requests_total{namespace=\"microservices\"}[5m])) by (app)"
}
]
},
{
"title": "P99 延迟",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{namespace=\"microservices\"}[5m])) by (le, app))"
}
]
},
{
"title": "错误率",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(http_requests_total{namespace=\"microservices\",status=~\"5..\"}[5m])) by (app) / sum(rate(http_requests_total{namespace=\"microservices\"}[5m])) by (app) * 100"
}
]
},
{
"title": "Pod 副本数",
"type": "stat",
"targets": [
{
"expr": "sum(kube_deployment_status_replicas_available{namespace=\"microservices\"}) by (deployment)"
}
]
}
]
}
}

7.3 告警规则

k8s/07-monitoring/alerting-rules.yaml
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: ecommerce-alerts
namespace: microservices
labels:
release: prometheus
spec:
groups:
- name: microservices.alerts
rules:
# 服务不可用告警
- alert: ServiceDown
expr: up{namespace="microservices"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "服务 {{ $labels.app }} 不可用"
description: "{{ $labels.namespace }} 命名空间中的 {{ $labels.instance }} 已下线超过 2 分钟"

# 高错误率告警
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{namespace="microservices",status=~"5.."}[5m])) by (app)
/ sum(rate(http_requests_total{namespace="microservices"}[5m])) by (app) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.app }} 错误率过高"
description: "{{ $labels.app }} 的 5xx 错误率已超过 5%,当前值:{{ $value | humanizePercentage }}"

# P99 延迟告警
- alert: HighLatencyP99
expr: |
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{namespace="microservices"}[5m])) by (le, app)) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.app }} P99 延迟过高"
description: "{{ $labels.app }} 的 P99 延迟已超过 1 秒,当前值:{{ $value }}s"

# Pod 重启告警
- alert: PodRestarting
expr: increase(kube_pod_container_status_restarts_total{namespace="microservices"}[1h]) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} 频繁重启"
description: "{{ $labels.namespace }} 中的 {{ $labels.pod }} 在过去 1 小时内重启了 {{ $value }} 次"

# HPA 达到上限告警
- alert: HPAAtMaxReplicas
expr: kube_hpa_status_current_replicas == kube_hpa_status_max_replicas
for: 15m
labels:
severity: warning
annotations:
summary: "HPA {{ $labels.hpa }} 已达到最大副本数"
description: "{{ $labels.namespace }} 中的 {{ $labels.hpa }} 已达到最大副本数 {{ $value }},可能需要调整上限"

7.4 Fluent Bit 日志收集

说明:Fluent Bit 以 DaemonSet 方式部署,确保每个节点运行一个日志采集代理。RBAC 配置使用 rbac.authorization.k8s.io/v1,DaemonSet 使用 apps/v1。详见官方文档:Fluent Bit Kubernetes Filter

k8s/07-monitoring/fluent-bit.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluent-bit
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluent-bit
rules:
- apiGroups: [""]
resources: ["pods", "namespaces"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fluent-bit
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fluent-bit
subjects:
- kind: ServiceAccount
name: fluent-bit
namespace: monitoring
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: monitoring
labels:
app: fluent-bit
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
serviceAccountName: fluent-bit
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.0.0
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: config
mountPath: /fluent-bit/etc/
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: config
configMap:
name: fluent-bit-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
namespace: monitoring
data:
fluent-bit.conf: |
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf

[INPUT]
Name tail
Path /var/log/containers/*.log
Parser docker
Tag kube.*
Refresh_Interval 10
Mem_Buf_Limit 50MB
Skip_Long_Lines On

[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Merge_Log On
Merge_Log_Key log_processed
K8S-Parser.On On
K8S-Parser.Exclude On

[OUTPUT]
Name elasticsearch
Match kube.*
Host elasticsearch.monitoring.svc.cluster.local
Port 9200
Index ecommerce-logs
Type _doc
Logstash_Format On
Logstash_Prefix ecommerce
Retry_Limit False

parsers.conf: |
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L

八、CI/CD 集成(ArgoCD)

8.1 GitOps 工作流

GitOps 的核心理念是:Git 仓库是唯一的事实来源(Single Source of Truth)。所有环境变更都通过提交代码来触发,ArgoCD 负责将 Git 仓库中的声明式配置同步到 Kubernetes 集群。

8.2 ArgoCD Application 配置

API 版本说明:ArgoCD 使用 argoproj.io/v1alpha1,这是 ArgoCD 的稳定 API 版本。AppProject、Application 和 ApplicationSet 均使用此版本。详见官方文档:ArgoCD CRD Reference

k8s/08-argocd/argocd-app.yaml
---
# ArgoCD 项目定义
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: ecommerce
namespace: argocd
spec:
description: "电商微服务项目"
sourceRepos:
- "https://github.com/your-org/ecommerce-k8s.git"
destinations:
- namespace: microservices
server: https://kubernetes.default.svc
- namespace: data
server: https://kubernetes.default.svc
- namespace: gateway
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: "networking.k8s.io"
kind: IngressClass
orphanedResources:
warn: true
---
# ArgoCD 应用定义
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ecommerce-infra
namespace: argocd
labels:
app.kubernetes.io/part-of: ecommerce
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: ecommerce
source:
repoURL: https://github.com/your-org/ecommerce-k8s.git
targetRevision: main
path: k8s
directory:
recurse: true
jsonnet: false
destination:
server: https://kubernetes.default.svc
namespace: microservices
syncPolicy:
automated:
prune: true # 自动删除 Git 中不存在的资源
selfHeal: true # 自动修复手动变更
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
- ServerSideApply=true
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
---
# ArgoCD 应用集(App of Apps 模式)
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: ecommerce-appset
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/your-org/ecommerce-k8s.git
revision: main
directories:
- path: k8s/*
template:
metadata:
name: "{{ path.basename }}"
spec:
project: ecommerce
source:
repoURL: https://github.com/your-org/ecommerce-k8s.git
targetRevision: main
path: "{{ path }}"
destination:
server: https://kubernetes.default.svc
namespace: "{{ path.basename }}"
syncPolicy:
automated:
prune: true
selfHeal: true
生产环境注意事项
  1. selfHeal: true 要谨慎使用。开启后,任何手动通过 kubectl 修改的配置都会被 ArgoCD 自动覆盖。建议在开发/测试环境开启,生产环境使用手动同步。
  2. prune: true 会删除 Git 中不存在的资源。误删 Git 中的文件可能导致生产环境资源被意外删除。建议配合 syncOptions: PruneLast=true,让 ArgoCD 最后再执行删除操作。
  3. 使用 App of Apps 模式管理多应用。通过 ApplicationSet 可以自动发现 Git 仓库中的目录结构,为每个子目录创建一个 Application,避免手动维护大量 Application 资源。

九、请求完整链路

下面展示一个用户下单请求的完整链路,帮助你理解各组件之间的协作关系。


十、部署与验证

10.1 分步部署

按照依赖关系,从底层到上层依次部署:

# 1. 创建命名空间
kubectl apply -f k8s/00-namespace/

# 2. 部署配置与密钥
kubectl apply -f k8s/01-config/

# 3. 创建存储资源
kubectl apply -f k8s/02-storage/

# 4. 部署数据层(等待 PostgreSQL 和 Redis 就绪)
kubectl apply -f k8s/03-services/postgresql-statefulset.yaml
kubectl apply -f k8s/03-services/redis-statefulset.yaml

# 等待数据层就绪
kubectl wait --for=condition=ready pod \
-l app=postgresql -n data --timeout=120s
kubectl wait --for=condition=ready pod \
-l app=redis -n data --timeout=120s

# 5. 部署业务服务
kubectl apply -f k8s/03-services/user-service.yaml
kubectl apply -f k8s/03-services/product-service.yaml
kubectl apply -f k8s/03-services/order-service.yaml
kubectl apply -f k8s/03-services/payment-service.yaml

# 6. 部署网关
kubectl apply -f k8s/03-services/gateway-deployment.yaml

# 7. 部署 Ingress
kubectl apply -f k8s/05-ingress/

# 8. 部署弹性伸缩与高可用
kubectl apply -f k8s/06-scalability/

# 9. 部署监控
kubectl apply -f k8s/07-monitoring/

10.2 健康检查验证

# 检查所有命名空间下的 Pod 状态
kubectl get pods --all-namespaces -l app.kubernetes.io/part-of=ecommerce

# 检查各服务 Endpoints
kubectl get endpoints -n microservices
kubectl get endpoints -n data
kubectl get endpoints -n gateway

# 检查 Ingress 状态
kubectl get ingress -n gateway
kubectl describe ingress ecommerce-ingress -n gateway

# 检查 HPA 状态
kubectl get hpa -n microservices
kubectl describe hpa user-service-hpa -n microservices

# 检查 PDB 状态
kubectl get pdb -n microservices

# 检查 PVC 绑定状态
kubectl get pvc -n data

# 检查 ArgoCD 应用状态
kubectl get applications -n argocd

10.3 压力测试

使用 hey 工具对用户服务进行压力测试:

# 进入集群内部执行测试(或通过 port-forward)
kubectl run hey-test --image=williamyeh/hey:latest --rm -it --restart=Never -- \
-n 100 -c 20 -m POST \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"testpass"}' \
http://user-service.microservices.svc.cluster.local:8080/api/v1/users/login

# 模拟并发下单请求
kubectl run hey-test --image=williamyeh/hey:latest --rm -it --restart=Never -- \
-n 500 -c 50 -m POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"product_id":1,"quantity":2}' \
http://order-service.microservices.svc.cluster.local:8082/api/v1/orders

# 观察 HPA 是否触发扩容
kubectl get hpa -n microservices -w

10.4 故障注入测试

# 测试 1:删除 Pod,验证自动恢复
kubectl delete pod -l app=user-service -n microservices --grace-period=0 --force
# 观察 Pod 是否自动重建
kubectl get pods -l app=user-service -n microservices -w

# 测试 2:验证优雅停机(不应出现 5xx 错误)
# 在另一个终端持续发送请求
kubectl run curl-test --image=curlimages/curl:latest --rm -it --restart=Never -- \
-s -o /dev/null -w "%{http_code}\n" \
http://user-service.microservices.svc.cluster.local:8080/healthz

# 测试 3:模拟节点故障(需要多节点集群)
kubectl cordon <node-name> # 标记节点不可调度
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
# 观察 Pod 是否迁移到其他节点
kubectl get pods -l app=user-service -n microservices -o wide -w

# 测试 4:验证 PDB 保护
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
# 如果违反 PDB,drain 会被阻止,并提示:
# "error: poddisruptionbudgets policy violation"

十一、生产环境最佳实践总结

经过上述完整的实战演练,以下是生产环境中的关键最佳实践:

架构设计

实践说明
命名空间隔离按团队/环境/业务域划分命名空间,配合 ResourceQuota 限制资源
配置外部化使用 ConfigMap/Secret 管理配置,禁止将配置硬编码在镜像中
密钥加密使用 Sealed Secrets 或 External Secrets Operator 管理敏感信息
服务网格可选对于服务间通信复杂度高的场景,考虑引入 Istio/Linkerd

部署策略

实践说明
滚动更新maxSurge: 1, maxUnavailable: 0 确保更新过程中不中断服务
健康检查三件套Startup + Liveness + Readiness Probe,使用不同端点
优雅停机preStop sleep + terminationGracePeriodSeconds 确保请求处理完成
Pod 反亲和性确保同一服务的 Pod 分布在不同节点上

弹性伸缩

实践说明
HPA 多指标同时关注 CPU、内存和自定义业务指标(如 QPS、延迟)
扩快缩慢扩容窗口短(30-60s),缩容窗口长(300-600s)
PDB 保护为关键服务配置 PDB,防止维护操作导致服务不可用
Cluster Autoscaler配合节点自动伸缩,当 Pod 因资源不足处于 Pending 状态时自动扩容节点

可观测性

实践说明
三大支柱Metrics(Prometheus)+ Logs(Fluent Bit)+ Traces(Jaeger/OpenTelemetry)
告警分级Critical(立即响应)+ Warning(工作时间内处理)+ Info(记录备案)
Dashboard 分层全局总览 -> 服务维度 -> Pod 维度,逐层下钻
SLO/SLI定义明确的服务质量目标(如 99.9% 可用性、P99 < 500ms)

持续交付

实践说明
GitOpsGit 仓库作为唯一事实来源,所有变更通过 PR 审批
环境隔离dev -> staging -> production 逐级发布,每级有独立的 ArgoCD Application
镜像标签策略使用 Git Commit SHA 作为镜像 Tag,确保可追溯性
回滚机制ArgoCD 支持一键回滚到 Git 历史中的任意版本
总结

生产级 Kubernetes 部署不仅仅是"把 YAML 写对",更是一套涵盖架构设计、部署策略、弹性伸缩、可观测性和持续交付的完整体系。本文通过电商微服务的实战案例,展示了如何将这些最佳实践落地到具体的 YAML 配置中。

希望这篇实战指南能够帮助你在实际项目中少走弯路,构建出真正可靠、可扩展的云原生微服务系统。

Kubernetes 全景解析 (5):安全体系与可观测性全景

· 阅读需 20 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

前言

当你的 Kubernetes 集群从开发环境走向生产环境,有两个话题再也无法回避——安全可观测性。安全是底线,确保只有正确的主体才能访问正确的资源;可观测性是生命线,让你在问题发生时能快速定位、快速恢复。

本文将从 K8s 安全的 4C 模型出发,深入 RBAC 权限控制、Pod 安全标准与服务账户管理;随后转向可观测性的三大支柱——指标、日志与链路追踪,构建一套完整的监控与诊断体系。


一、K8s 安全架构概览

1.1 4C 安全模型

Kubernetes 社区提出了经典的 4C 安全模型,将安全分为四个层次,从外到内逐层收紧:

层次名称关注点示例
第 1 层Cloud(云)云平台自身的安全配置IAM 策略、VPC、防火墙规则
第 2 层Cluster(集群)集群组件的访问控制RBAC、网络策略、审计日志
第 3 层Container(容器)容器运行时安全镜像签名、只读根文件系统、非 root 运行
第 4 层Code(代码)应用代码自身的安全输入校验、依赖漏洞、密钥管理
核心原则

4C 模型的关键思想是纵深防御(Defense in Depth)——任何单一层次的安全措施都不足以应对所有威胁,只有层层设防,才能构建真正可靠的安全体系。内层的安全措施不应依赖外层,每一层都应独立提供保护。

1.2 安全最佳实践总览

领域最佳实践
认证启用 TLS 双向认证,禁用匿名访问
授权最小权限原则,使用 RBAC 精细控制
Pod 安全强制非 root 运行,只读根文件系统
网络默认拒绝,按需开放 NetworkPolicy
镜像使用可信仓库,启用镜像签名验证
密钥使用外部密钥管理(Vault/AWS KMS),避免明文存储
审计启用 API Server 审计日志,记录所有变更

二、RBAC:基于角色的访问控制

2.1 核心概念

RBAC(Role-Based Access Control)是 K8s 授权机制中最常用、最推荐的方式。它通过四个核心对象实现权限管理:

对象作用范围说明
Role命名空间级别定义在某个命名空间内的权限规则
ClusterRole集群级别定义集群范围的权限规则
RoleBinding命名空间级别将 Role 绑定到用户/组/服务账户
ClusterRoleBinding集群级别将 ClusterRole 绑定到用户/组/服务账户
Role vs ClusterRole 的关键区别
  • Role 只能管理同一个命名空间内的资源
  • ClusterRole 可以管理所有命名空间以及非命名空间资源(如 Node、PersistentVolume、Namespace)

2.2 RBAC 权限模型

2.3 完整 YAML 示例

Role —— 命名空间管理员

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: namespace-admin
namespace: production
rules:
- apiGroups: ["", "apps", "batch"]
resources:
- pods
- pods/log
- deployments
- services
- configmaps
- secrets
- jobs
- cronjobs
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

ClusterRole —— 只读查看者

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-readonly
rules:
- apiGroups: ["", "apps", "batch", "extensions"]
resources: ["*"]
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/healthz", "/version", "/metrics"]
verbs: ["get"]

RoleBinding —— 将角色绑定到用户

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: bind-namespace-admin
namespace: production
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: dev-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: namespace-admin
apiGroup: rbac.authorization.k8s.io

ClusterRoleBinding —— 将只读角色绑定到服务账户

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: bind-readonly-to-monitor
subjects:
- kind: ServiceAccount
name: prometheus
namespace: monitoring
roleRef:
kind: ClusterRole
name: cluster-readonly
apiGroup: rbac.authorization.k8s.io

2.4 常见 RBAC 模式

模式适用场景实现方式
命名空间管理员团队负责人管理自己的命名空间Role + RoleBinding
只读查看者开发/测试人员查看集群状态ClusterRole + ClusterRoleBinding
开发人员开发者在指定命名空间部署应用Role(限制 verbs) + RoleBinding
运维人员运维管理节点、PV 等集群级资源ClusterRole(节点/PV 权限) + ClusterRoleBinding
聚合角色将多个 ClusterRole 合并为一个统一角色ClusterRole + aggregationRule
ClusterRole 聚合(Aggregated ClusterRoles)

Kubernetes 支持通过 aggregationRule 将多个 ClusterRole 聚合为一个组合角色。控制平面会自动监视匹配标签选择器的 ClusterRole,并将其规则合并到聚合角色的 rules 字段中。默认的用户角色(如 admineditview)就是通过聚合机制实现的,允许集群管理员通过添加匹配标签的 ClusterRole 来扩展默认角色的权限。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.example.com/aggregate-to-monitoring: "true"
rules: [] # 控制平面自动填充规则
注意事项
  • roleRef 一旦创建不可修改,如需变更必须删除重建 Binding
  • 避免使用 verbs: ["*"]resources: ["*"] 的通配符,遵循最小权限原则
  • 定期审计集群中的 RoleBinding 和 ClusterRoleBinding,清理不再需要的权限

三、Pod 安全标准

3.1 从 PSP 到 PSS

Kubernetes 在 v1.21 中废弃了 PodSecurityPolicy(PSP),并在 v1.25 中完全移除。取而代之的是 Pod Security Standards(PSS) + Pod Security Admission(PSA) 机制。

特性PSP(已废弃)PSS + PSA(推荐)
实现方式准入控制器(Admission Controller)内置准入插件
配置粒度每个 Pod 可绑定不同策略按命名空间统一标签
策略继承支持不支持(需借助第三方 Gatekeeper)
状态v1.25 已移除v1.23+ 内置可用

3.2 三种策略级别

级别说明典型控制
Privileged无限制,完全开放无任何控制
Baseline最低限度的安全防护禁止特权容器、禁止挂载宿主机路径、限制 hostNetwork、限制 SELinux 类型、禁止探针/生命周期钩子中的 host 字段(v1.34+)、限制 AppArmor 配置文件、限制安全 sysctl
Restricted高度安全,生产推荐强制非 root、只读根文件系统、限制 Capabilities(仅允许 NET_BIND_SERVICE)、禁止 seccomp profile=unconfined、要求显式设置 seccomp
Baseline 策略的完整控制列表(基于 v1.34 官方文档)

根据 Pod Security Standards 官方文档,Baseline 策略包含以下控制:

控制项说明
HostProcess禁止 Windows HostProcess 容器
Host Namespaces禁止共享 hostNetwork/hostPID/hostIPC
Privileged Containers禁止特权容器
Capabilities限制可添加的 Capabilities(如 AUDIT_WRITE、CHOWN、NET_BIND_SERVICE 等)
HostPath Volumes禁止 hostPath 卷
Host Ports禁止或限制 hostPort
Host Probes / Lifecycle Hooks(v1.34+)禁止探针和生命周期钩子中的 host 字段
AppArmor限制 AppArmor 配置文件类型(RuntimeDefault/Localhost)
SELinux限制 SELinux 类型(container_t/container_init_t/container_kvm_t/container_engine_t),禁止自定义 user/role
/proc Mount Type要求使用默认 /proc 掩码
Seccomp禁止显式设置为 Unconfined
Sysctls仅允许安全 sysctl 子集

3.3 Pod Security Admission 配置

PSA 通过在命名空间上打标签来生效,支持三种执行模式:

# 在命名空间上配置 PSA 标签
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
# enforce: 违反策略的 Pod 将被拒绝
pod-security.kubernetes.io/enforce: restricted
# audit: 违反策略的 Pod 会记录审计日志
pod-security.kubernetes.io/audit: restricted
# warn: 违反策略的 Pod 会触发用户警告
pod-security.kubernetes.io/warn: restricted
# 指定策略版本
pod-security.kubernetes.io/enforce-version: latest

3.4 v1.34 新增安全控制详解

Kubernetes v1.34 在 Pod Security Standards 中引入了多项重要安全控制更新:

3.4.1 Host Probes / Lifecycle Hooks(v1.34+)

这是 v1.34 中 Baseline 策略新增的控制项,禁止在探针和生命周期钩子中使用 host 字段,防止容器通过 HTTP/TCP 探针访问宿主机网络。

受限字段包括:

  • livenessProbe.httpGet.host / readinessProbe.httpGet.host / startupProbe.httpGet.host
  • livenessProbe.tcpSocket.host / readinessProbe.tcpSocket.host / startupProbe.tcpSocket.host
  • lifecycle.postStart.httpGet.host / lifecycle.preStop.httpGet.host
  • lifecycle.postStart.tcpSocket.host / lifecycle.preStop.tcpSocket.host

允许值Undefined/nil 或空字符串 ""

这意味着在 Baseline 策略下,探针和生命周期钩子不能再指向宿主机的 IP 地址或主机名,有效防止了通过探针机制进行的 SSRF(服务端请求伪造)攻击。

3.4.2 SELinux 类型扩展

SELinux 控制在 Baseline 策略中进一步收紧:

  • 允许的 SELinux 类型container_tcontainer_init_tcontainer_kvm_tcontainer_engine_t(自 Kubernetes 1.31起新增)
  • 禁止:自定义 SELinux userrole 选项(允许值仅为 Undefined/""

container_engine_t 类型适用于需要与容器引擎交互的特殊工作负载场景。

3.4.3 安全 Sysctls 扩展

自 Kubernetes 1.29 起,Baseline 策略允许的安全 sysctl 列表新增了以下 TCP keepalive 相关参数:

Sysctl 名称说明新增版本
net.ipv4.tcp_keepalive_timeTCP keepalive 探测间隔since 1.29
net.ipv4.tcp_fin_timeoutTCP FIN 超时时间since 1.29
net.ipv4.tcp_keepalive_intvlTCP keepalive 探测间隔since 1.29
net.ipv4.tcp_keepalive_probesTCP keepalive 探测次数since 1.29

完整的安全 sysctl 列表还包括:kernel.shm_rmid_forcednet.ipv4.ip_local_port_rangenet.ipv4.ip_unprivileged_port_startnet.ipv4.tcp_syncookiesnet.ipv4.ping_group_rangenet.ipv4.ip_local_reserved_ports(since 1.27)。

3.4.4 AppArmor 控制

Baseline 策略对 AppArmor 配置文件的控制:

  • 允许的类型RuntimeDefaultLocalhost,或通过注解 container.apparmor.security.beta.kubernetes.io/* 设置 runtime/defaultlocalhost/*
  • 目的:防止禁用或绕过默认的 AppArmor 安全配置文件

3.5 SecurityContext 配置

SecurityContext 是 Pod 安全的核心配置点,可以在 Pod 级别和容器级别分别设置:

apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: production
spec:
securityContext:
# Pod 级别:所有容器共享
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.25
securityContext:
# 容器级别:仅对当前容器生效
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
为什么需要 emptyDir 挂载?

当设置 readOnlyRootFilesystem: true 时,Nginx 等应用需要写入 /tmp 和缓存目录。通过 emptyDir 提供临时可写目录,既满足了应用需求,又保持了根文件系统的只读安全。


四、服务账户与 Token 管理

4.1 ServiceAccount 概念

Kubernetes 中每个 Pod 都关联一个服务账户(ServiceAccount),用于标识 Pod 的身份。当 Pod 需要访问 API Server 时,使用的就是 ServiceAccount 的凭证。

# 创建专用服务账户
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
automountServiceAccountToken: false

4.2 自动挂载 Token 的风险

默认情况下,K8s 会将 ServiceAccount Token 自动挂载到每个 Pod 的 /var/run/secrets/kubernetes.io/serviceaccount/ 目录。这意味着:

  • 任何能进入容器的人都能获取 Token
  • 如果容器被攻破,攻击者可以利用 Token 访问 API Server
  • 对于不需要访问 API Server 的 Pod,这是不必要的安全暴露
# 方式一:在 Pod 规范中禁用自动挂载
apiVersion: v1
kind: Pod
metadata:
name: app-no-token
spec:
serviceAccountName: app-service-account
automountServiceAccountToken: false
containers:
- name: app
image: myapp:latest
# 方式二:在 ServiceAccount 上全局禁用
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
automountServiceAccountToken: false

4.3 外部 Token 供应

对于需要更精细控制 Token 生命周期(如短时效 Token、Token 轮换)的场景,可以使用 TokenRequest API 获取面向 Pod 的绑定 Token:

# 使用 projected volume 注入短时效 Token
apiVersion: v1
kind: Pod
metadata:
name: app-with-projected-token
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600 # Token 有效期 1 小时
audience: api
最佳实践
  1. 不需要访问 API Server 的 Pod:设置 automountServiceAccountToken: false
  2. 需要访问 API Server 的 Pod:创建专用的 ServiceAccount,配合最小权限的 RBAC
  3. 高安全要求场景:使用 projected volume + 短时效 Token,实现自动轮换

五、可观测性体系概览

5.1 三大支柱

可观测性(Observability)的三大支柱构成了完整的系统诊断能力:

支柱回答的问题典型工具数据特征
Metrics(指标)系统现在是什么状态?Prometheus、Thanos结构化数值,适合聚合与告警
Logs(日志)系统发生了什么?Fluent Bit、Loki、ELK非结构化文本,适合问题排查
Traces(链路追踪)请求经过了哪些路径?Jaeger、Zipkin、Tempo结构化事件流,适合性能分析与故障定位
为什么需要三大支柱?

单一支柱无法回答所有问题。指标告诉你"CPU 使用率 90%",但不能告诉你"是哪个请求导致的";日志告诉你"请求超时了",但不能告诉你"请求在哪个服务卡住了"。只有三者结合,才能快速、准确地定位问题。


六、Prometheus + Grafana:指标监控

6.1 Prometheus 在 K8s 中的部署

在生产环境中,推荐使用 kube-prometheus-stack(基于 Prometheus Operator)一键部署完整的监控栈:

# 通过 Helm 安装 kube-prometheus-stack
# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# helm install prometheus prometheus-community/kube-prometheus-stack \
# --namespace monitoring --create-namespace

Prometheus Operator 引入了四个自定义资源(CRD)来声明式管理监控配置:

CRD作用
Prometheus管理 Prometheus 实例的生命周期
ServiceMonitor基于 Service 选择器自动发现监控目标
PodMonitor基于 Pod 标签直接发现监控目标
PrometheusRule声明式定义告警规则和录制规则

6.2 核心监控指标

级别关键指标说明
容器级container_cpu_usage_seconds_total容器 CPU 使用量
container_memory_working_set_bytes容器实际内存使用(含 Page Cache)
container_fs_usage_bytes容器文件系统使用量
Pod 级kube_pod_status_phasePod 当前阶段(Pending/Running/Succeeded/Failed)
kube_pod_container_status_restarts_totalPod 重启次数
kube_pod_container_status_waiting_reason容器等待原因(ImagePullBackOff 等)
Node 级node_cpu_seconds_total节点 CPU 使用量
node_memory_MemAvailable_bytes节点可用内存
kube_node_status_condition节点状态(Ready/NotReady)

6.3 ServiceMonitor 示例

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: app-service-monitor
namespace: monitoring
labels:
release: prometheus # 匹配 Prometheus 实例的选择器
spec:
selector:
matchLabels:
app: my-application # 匹配目标 Service 的标签
namespaceSelector:
names:
- production # 监控 production 命名空间中的 Service
endpoints:
- port: http # Service 中的端口名
path: /metrics # 指标暴露路径
interval: 30s # 采集间隔
scrapeTimeout: 10s # 采集超时

6.4 告警规则示例

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: app-alerting-rules
namespace: monitoring
labels:
release: prometheus
spec:
groups:
- name: app-alerts
rules:
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[15m]) * 60 * 5 > 0
for: 15m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} 处于 CrashLoopBackOff"
description: "容器 {{ $labels.container }} 在过去 15 分钟内重启超过 5 次"

- alert: HighMemoryUsage
expr: |
(container_memory_working_set_bytes / container_spec_memory_limit_bytes) > 0.9
and container_spec_memory_limit_bytes > 0
for: 5m
labels:
severity: warning
annotations:
summary: "容器 {{ $labels.container }} 内存使用率超过 90%"
description: "当前使用 {{ $value | humanizePercentage }},限制 {{ $labels.container }}"

- alert: NodeNotReady
expr: kube_node_status_condition{condition="Ready",status="true"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: "节点 {{ $labels.node }} 处于 NotReady 状态"
告警设计原则
  • Critical 告警:需要立即响应(如节点宕机、Pod CrashLoop)
  • Warning 告警:需要关注但不必立即处理(如磁盘使用率 > 80%)
  • 设置合理的 for 持续时间,避免瞬时抖动触发误报
  • 告警信息要包含足够的上下文,让值班人员无需额外查询即可判断影响范围

七、日志收集体系

7.1 日志收集模式对比

模式原理优点缺点
Sidecar每个 Pod 注入一个日志采集容器隔离性好,可按应用定制资源开销大,管理复杂
DaemonSet每个 Node 运行一个采集 Agent资源开销低,管理简单需要共享日志卷
直连应用直接推送日志到后端无需额外组件与后端强耦合,侵入应用

7.2 常见方案对比

方案组合特点适用场景
PLG StackPromtail + Loki + Grafana轻量,与 Grafana 深度集成已有 Grafana 的团队
EFK StackFluentd/Fluent Bit + Elasticsearch + Kibana功能强大,全文搜索大规模日志分析
Fluent Bit + LokiFluent Bit + Loki + Grafana极低资源消耗资源敏感的环境

7.3 Fluent Bit DaemonSet 配置

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: logging
labels:
app: fluent-bit
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
serviceAccountName: fluent-bit
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.0.0
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: containers
mountPath: /var/lib/docker/containers
readOnly: true
- name: config
mountPath: /fluent-bit/etc/fluent-bit.conf
subPath: fluent-bit.conf
volumes:
- name: varlog
hostPath:
path: /var/log
- name: containers
hostPath:
path: /var/lib/docker/containers
- name: config
configMap:
name: fluent-bit-config

八、链路追踪

8.1 OpenTelemetry 在 K8s 中的集成

OpenTelemetry(OTel) 是 CNCF 的可观测性标准框架,统一了指标、日志和链路追踪的数据采集。在 K8s 中,通常部署 OpenTelemetry Collector 作为数据的统一入口:

apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
namespace: observability
spec:
replicas: 2
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:0.96.0
args:
- --config=/etc/otelcol/config.yaml
ports:
- containerPort: 4317 # OTLP gRPC
- containerPort: 4318 # OTLP HTTP
- containerPort: 8889 # Prometheus metrics
volumeMounts:
- name: config
mountPath: /etc/otelcol
volumes:
- name: config
configMap:
name: otel-collector-config

8.2 分布式追踪最佳实践

实践说明
统一 SDK 接入所有服务使用 OpenTelemetry SDK,自动注入 Trace Context
合理的采样策略生产环境使用尾部采样(Tail Sampling),基于错误率/延迟/状态码决定是否保留
上下文传播确保跨服务调用时 TraceID 正确传播(HTTP Header、gRPC Metadata)
关联日志与追踪在日志中注入 TraceID,实现日志与追踪的联动查询
设置合理的 Span 层级避免过深的 Span 嵌套,关注关键路径和外部调用
TraceID 与日志联动

在应用日志中添加 TraceID 字段,可以在 Grafana 中实现从告警到日志再到追踪的完整排查链路:

# 应用日志示例
2026-04-09 10:23:45 INFO [trace_id=abc123 span_id=def456] Processing order request from user_1234
2026-04-09 10:23:46 ERROR [trace_id=abc123 span_id=def456] Failed to call payment service: timeout

九、本章小结

本文从安全与可观测性两个维度,系统梳理了 Kubernetes 生产环境中的关键能力:

安全体系方面,我们覆盖了从 4C 安全模型到具体实践的完整链路:

  • RBAC 提供了精细的权限控制,通过 Role/ClusterRole 和 Binding 的组合,实现最小权限原则。ClusterRole 还支持 aggregationRule 聚合机制,便于扩展默认角色权限。
  • Pod Security Standards 替代了已废弃的 PSP,通过 PSA 标签为命名空间设置安全基线。v1.34 中 Baseline 策略新增了 Host Probes/Lifecycle Hooks 控制,SELinux 新增 container_engine_t 类型,安全 sysctl 列表也进一步扩展。
  • ServiceAccount Token 管理 教会我们如何控制 Pod 的 API Server 访问权限,避免不必要的凭证暴露。

可观测性体系方面,我们构建了三大支柱的完整图景:

  • Prometheus + Grafana 负责指标采集、可视化与告警,是监控体系的核心
  • Fluent Bit + Loki 提供轻量高效的日志收集与查询能力
  • OpenTelemetry + Jaeger 实现了分布式链路追踪,让跨服务调用路径一目了然

安全与可观测性不是孤立的话题——良好的可观测性是安全事件响应的基础,而安全策略本身也需要被持续监控。在下一篇文章中,我们将深入 Kubernetes 的调度策略与资源管理,探讨如何让集群的每一份资源都物尽其用。


十、官方文档参考

本文内容基于 Kubernetes v1.34 官方文档校验,以下是相关官方文档链接:

安全相关

可观测性相关


系列导航

章节主题状态
0架构设计与核心概念✅ 已发布
1工作负载与 Pod 生命周期深度解析✅ 已发布
2网络模型与服务发现全链路解析✅ 已发布
3存储体系与配置管理深度剖析✅ 已发布
4调度器、资源管理与弹性伸缩✅ 已发布
5安全体系与可观测性全景✅ 已发布
6生产级微服务架构实战✅ 已发布
7有状态应用与 Operator 模式实战✅ 已发布

Kubernetes 全景解析 (4):调度器、资源管理与弹性伸缩

· 阅读需 21 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

前言

在 Kubernetes 集群中,调度器(Scheduler)扮演着"决策中枢"的角色——每一个 Pod 应该运行在哪个节点上,都由它来决定。而资源管理(Resource Management)与弹性伸缩(Autoscaling)则是保障集群高效、稳定运行的关键机制。这三者共同构成了 K8s 资源层的核心能力。

本文将从资源模型出发,深入剖析 kube-scheduler 的调度机制,全面讲解 HPA/VPA/Cluster Autoscaler 三级弹性伸缩策略,并介绍 ResourceQuota、LimitRange、PriorityClass 等资源治理工具,帮助你构建一套完整的 K8s 资源管理知识体系。


一、资源模型基础

1.1 Kubernetes 可管理资源类型

Kubernetes 对容器可以使用的资源进行了抽象,目前支持以下几种核心资源类型:

资源类型缩写说明可压缩
CPUcpu计算资源,以核心(core)为单位,支持小数(如 0.5 = 500m)
Memorymemory内存资源,以字节为单位(支持 Ki/Mi/Gi)
Ephemeral Storageephemeral-storage临时存储(日志、EmptyDir 等),以字节为单位
Extended Resourcesnvidia.com/gpu扩展资源(如 GPU、Infiniband),由设备插件注册
可压缩 vs 不可压缩资源
  • 可压缩资源(Compressible):CPU 是可压缩的。当 Pod 超过 CPU Limit 时,不会被杀掉,而是被限流(throttled),表现为性能下降。
  • 不可压缩资源(Incompressible):内存和存储是不可压缩的。当 Pod 超过 Memory Limit 时,会被 OOM Killer 杀掉并重启。

理解这一区别对于合理设置资源配额至关重要。

1.2 Request vs Limit

Kubernetes 通过 requestslimits 两个字段来控制容器的资源使用:

  • Request(请求量):调度器依据此值决定将 Pod 调度到哪个节点(保证最低可用资源)
  • Limit(限制量):运行时容器可使用的资源上限(硬限制)
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
spec:
containers:
- name: app
image: nginx:1.25
resources:
requests:
cpu: "250m" # 0.25 核,调度依据
memory: "256Mi" # 256 MiB,调度依据
limits:
cpu: "500m" # 0.5 核,运行时上限
memory: "512Mi" # 512 MiB,运行时上限,超出则 OOM Kill
Limit 可以省略吗?

如果只设置了 requests 而没有设置 limits,在默认情况下(未配置 LimitRange),limits 默认等于节点可分配资源上限,这意味着容器可以无限使用资源。在生产环境中,强烈建议同时设置 requests 和 limits

1.3 QoS 服务质量类别

Kubernetes 根据资源配额的设置方式,将 Pod 分为三个 QoS(Quality of Service)等级。当节点资源不足时,K8s 会优先驱逐低 QoS 的 Pod。

三个 QoS 等级的详细对比:

QoS 等级条件CPU 行为内存行为驱逐优先级
Guaranteed所有容器 Request == Limit(CPU + Memory)稳定,不会被限流超限则 OOM Kill最低(最后驱逐)
Burstable至少一个容器设置了 Request 或 Limit(但不满足 Guaranteed)可能被限流超限则 OOM Kill中等
BestEffort所有容器均未设置 Request 和 Limit最先被限流最先被 OOM Kill最高(最先驱逐)
生产环境建议

对于核心业务(如支付、订单),建议设置为 Guaranteed 级别,确保资源独占;对于一般业务(如日志处理),设置为 Burstable 即可;BestEffort 仅适用于测试或临时任务。


二、kube-scheduler:调度器深度解析

2.1 调度流程概览

kube-scheduler 是 Kubernetes 的核心组件之一,负责将新创建的、未调度的 Pod 分配到合适的节点上。根据官方文档,kube-scheduler 通过**两步操作(2-step operation)**完成节点选择:

kube-scheduler selects a node for the pod in a 2-step operation: 1. Filtering 2. Scoring —— Kubernetes Scheduler 官方文档

Scheduling Profiles vs Scheduling Policies

官方文档指出,有两种方式配置调度器的过滤和打分行为:

  1. Scheduling Profiles(推荐):通过配置插件(Plugins)实现不同调度阶段,包括 QueueSortFilterScoreBindReservePermit 等扩展点。
  2. Scheduling Policies(旧版方式):通过配置 Predicates(过滤)和 Priorities(打分)来定义调度策略。

2.2 过滤阶段(Filtering)

过滤阶段的目标是快速排除不满足条件的节点,缩小候选范围。通过过滤的节点称为可行节点(Feasible Nodes)。以下是常见的过滤策略:

过滤策略说明配置方式
NodeName直接指定节点名称,跳过所有调度逻辑spec.nodeName
nodeSelector基于节点标签的简单匹配spec.nodeSelector
nodeAffinity基于节点标签的高级匹配(支持软约束)spec.affinity.nodeAffinity
podAffinityPod 亲和性,倾向于与某些 Pod 部署在一起spec.affinity.podAffinity
podAntiAffinityPod 反亲和性,倾向于远离某些 Podspec.affinity.podAntiAffinity
Taint/Toleration节点污点与 Pod 容忍度匹配spec.tolerations
资源检查节点剩余资源是否满足 Pod 的 Request自动
Volume 检查节点是否能挂载 Pod 所需的存储卷自动

2.3 打分阶段(Scoring)

经过过滤后,调度器对剩余的候选节点进行打分,选择得分最高的节点。常见的打分策略包括:

打分策略权重说明
NodeResourcesFit默认 1资源均衡分配,倾向于选择资源使用率更均衡的节点
NodeAffinity默认 1满足节点亲和性软约束的节点获得加分
PodAffinity默认 1满足 Pod 亲和性软约束的节点获得加分
ImageLocality默认 1节点上已存在 Pod 所需镜像时加分(减少拉取时间)
TaintToleration默认 1容忍节点污点的 Pod 获得加分
InterPodAffinity默认 1Pod 间亲和/反亲和性评分
VolumeBinding默认 1PV/PVC 绑定评分
自定义调度器与 Scheduler Framework

Kubernetes 允许你编写自定义调度器(Custom Scheduler),通过实现 Scheduler Framework 的扩展点(Extension Points)来插入自定义逻辑。Scheduling Profiles 配置中支持的插件阶段包括:QueueSortFilterScoreBindReservePermitPreBindPostBind 等。更多详情请参阅 kube-scheduler 配置参考

2.4 nodeSelector:简单节点选择

nodeSelector 是最简单的节点选择方式,通过标签匹配来约束 Pod 的调度目标:

apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
nodeSelector:
gpu: "nvidia-tesla-t4" # 只调度到具有此标签的节点
disktype: "ssd"
containers:
- name: cuda-app
image: nvidia/cuda:12.0-base
resources:
limits:
nvidia.com/gpu: "1" # 申请 1 块 GPU

2.5 nodeAffinity:高级节点亲和性

nodeAffinity 支持更灵活的匹配规则,包括硬约束(required)软约束(preferred)

apiVersion: v1
kind: Pod
metadata:
name: affinity-pod
spec:
affinity:
nodeAffinity:
# 硬约束:必须满足,否则 Pod 无法调度
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: ["us-east-1a", "us-east-1b"]
- key: node-type
operator: NotIn
values: ["spot-instance"]
# 软约束:尽量满足,不满足也能调度
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: node-role.kubernetes.io/worker
operator: In
values: ["high-memory"]
containers:
- name: app
image: nginx:1.25
操作符说明

nodeAffinity 支持的操作符:InNotInExistsDoesNotExistGtLt。其中 GtLt 仅用于数值比较。

2.6 podAffinity 与 podAntiAffinity

Pod 亲和性用于控制 Pod 之间的部署位置关系:

apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
# Pod 亲和性:与 cache Pod 部署在同一可用区
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: cache
topologyKey: topology.kubernetes.io/zone
# Pod 反亲和性:不同副本尽量分布在不同节点
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname
containers:
- name: web
image: nginx:1.25
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
topologyKey 的重要性

topologyKey 是亲和性规则的关键字段,它定义了拓扑域的划分维度。常用的值包括:

  • kubernetes.io/hostname:节点级别
  • topology.kubernetes.io/zone:可用区级别
  • topology.kubernetes.io/region:区域级别

选择合适的 topologyKey 对于实现高可用部署至关重要。

2.7 Taint 与 Toleration:污点容忍机制

Taint(污点)是作用于节点上的标记,用于排斥不容忍该污点的 Pod。Toleration(容忍度)是 Pod 上的声明,表示可以容忍特定的污点。

# 给节点添加污点
# kubectl taint nodes node1 dedicated=gpu:NoSchedule

apiVersion: v1
kind: Pod
metadata:
name: gpu-workload
spec:
# 声明容忍度
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: cuda-app
image: nvidia/cuda:12.0-base

Taint 的 effect 类型:

Effect说明
NoSchedule不调度新的不容忍 Pod(已运行的 Pod 不受影响)
PreferNoSchedule尽量不调度(软约束)
NoExecute不调度,且驱逐已运行的不容忍 Pod
常见内置 Taint

Kubernetes 自动为特定场景的节点添加 Taint:

  • node.kubernetes.io/not-ready:节点未就绪
  • node.kubernetes.io/unreachable:节点不可达
  • node.kubernetes.io/memory-pressure:内存压力
  • node.kubernetes.io/disk-pressure:磁盘压力
  • node.kubernetes.io/network-unavailable:网络不可用
  • node.kubernetes.io/unschedulable:节点被标记为不可调度(cordon)

三、HPA:水平 Pod 自动伸缩

3.1 HPA 工作原理

Horizontal Pod Autoscaler(HPA)通过监控 Pod 的资源使用指标,自动调整 Deployment/ReplicaSet/StatefulSet 的副本数量,使应用能够根据负载变化自动扩缩。

3.2 扩缩算法详解

HPA 的核心算法如下:

期望副本数 = ceil(当前副本数 × (当前指标值 / 目标指标值))
算法示例

假设 Deployment 当前有 4 个副本,目标 CPU 利用率为 50%:

  • 当前平均 CPU 利用率为 80% → 期望副本数 = ceil(4 × 80/50) = ceil(6.4) = 7
  • 当前平均 CPU 利用率为 20% → 期望副本数 = ceil(4 × 20/50) = ceil(1.6) = 2

注意:HPA 不会将副本数缩减到低于 spec.replicas.minReplicas(默认为 1)。

3.3 指标类型

HPA 支持四种指标类型:

指标类型数据来源示例
ResourceMetrics Server(CPU/内存)CPU 利用率 50%
Container ResourceMetrics Server(容器级别)容器内存使用量
Object自定义指标(如 Ingress QPS)每秒请求数
External外部指标系统(如 Prometheus)Kafka 队列积压消息数

3.4 行为配置

从 Kubernetes 1.18 开始,HPA 引入了 behavior 字段,允许精细控制扩缩行为:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Resource
resource:
name: memory
target:
type: AverageValue
averageValue: "500Mi"
behavior:
# 缩容冷却窗口与策略
scaleDown:
stabilizationWindowSeconds: 300 # 5 分钟内不重复缩容
policies:
- type: Percent
value: 10 # 每次最多缩容 10%
periodSeconds: 60 # 每 60 秒允许一次缩容
- type: Pods
value: 2 # 或每次最多缩容 2 个 Pod
periodSeconds: 60
selectPolicy: Min # 取两种策略中更保守的
# 扩容策略
scaleUp:
stabilizationWindowSeconds: 0 # 扩容不需要冷却
policies:
- type: Percent
value: 100 # 允许一次扩容 100%
periodSeconds: 15 # 每 15 秒允许一次扩容
- type: Pods
value: 4 # 或每次最多扩容 4 个 Pod
periodSeconds: 15
selectPolicy: Max # 取两种策略中更激进的
扩缩行为建议
  • 扩容应该快:用户不希望等待太久,建议设置较短的 periodSeconds(如 15s)
  • 缩容应该慢:避免频繁缩容导致的服务抖动,建议设置较长的 stabilizationWindowSeconds(如 300s)
  • 使用 selectPolicy:扩容选 Max(激进),缩容选 Min(保守)

四、VPA:垂直 Pod 自动伸缩

4.1 VPA 工作模式

Vertical Pod Autoscaler(VPA)通过调整 Pod 的 CPU 和内存 Request/Limit 来实现垂直伸缩。VPA 支持三种工作模式:

模式说明适用场景
Auto自动更新 Pod 的资源配额(会重启 Pod)非关键业务,可容忍重启
Recreate自动更新,且在更新时重启 Pod同 Auto
Off仅提供建议,不执行变更评估阶段,收集数据

4.2 VPA 与 HPA 的协作与冲突

VPA 与 HPA 的冲突

VPA 和 HPA 不能同时基于 CPU/内存指标工作。原因很简单:HPA 通过增加副本数来降低单个 Pod 的资源使用率,而 VPA 通过增加单个 Pod 的资源配额来满足需求。两者同时基于 CPU/内存工作会导致"扩容循环"——HPA 增加 Pod 数量导致单个 Pod 负载降低,VPA 随之降低资源配额,然后 HPA 又需要更多 Pod 来满足需求。

解决方案:HPA 基于 CPU/内存指标伸缩,VPA 仅基于自定义指标(如内存使用量)工作;或者使用 HPA 基于 CPU 伸缩,VPA 仅调整内存。

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web-app-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
updatePolicy:
updateMode: "Auto" # Auto / Recreate / Off
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
cpu: "100m"
memory: "128Mi"
maxAllowed:
cpu: "2"
memory: "2Gi"
controlledResources: ["cpu", "memory"]
recommenders:
- name: custom-recommender # 可自定义推荐器

五、Cluster Autoscaler:节点级伸缩

5.1 CA 工作原理

Cluster Autoscaler(CA)负责在集群级别自动调整节点数量。当 Pod 因资源不足而无法调度(Pending)时,CA 会自动添加新节点;当节点利用率持续偏低时,CA 会自动移除空闲节点。

CA 的核心工作逻辑:

  1. 扩容触发:检测到有 Pod 处于 Pending 状态,且原因是资源不足
  2. 评估节点组:根据 Pod 的资源需求、节点亲和性等约束,选择合适的节点组
  3. 创建节点:调用云厂商 API 创建新节点
  4. 缩容触发:节点利用率持续低于阈值(默认 50%),且节点上的 Pod 可以被迁移到其他节点
  5. 驱逐 Pod:安全驱逐节点上的 Pod(非 Critical Pod、非 DaemonSet Pod)
  6. 删除节点:调用云厂商 API 删除节点

5.2 CA 与 HPA 的配合

CA 和 HPA 的配合构成了 Kubernetes 的两级弹性伸缩体系

层级组件伸缩维度响应速度粒度
第一级HPAPod 数量秒级细粒度
第二级Cluster Autoscaler节点数量分钟级粗粒度

典型工作流程:负载增加 → HPA 增加 Pod 副本 → 节点资源不足 → Pod Pending → CA 添加新节点 → 新 Pod 被调度到新节点。

5.3 云厂商集成

CA 需要与云厂商的 API 集成来管理节点池:

云厂商节点组实现CA 配置
AWSASG(Auto Scaling Group) / EKS Managed Node Group--nodegroup-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled
GCPMIG(Managed Instance Group) / GKE Node Pool自动发现
AzureVMSS(Virtual Machine Scale Set) / AKS Node Pool--nodegroup-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled
阿里云ECI / ACK 节点池--nodegroup-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled
CA 的安全限制

Cluster Autoscaler 不会驱逐以下 Pod:

  • 通过 PDB(PodDisruptionBudget)保护的 Pod(驱逐会违反最小可用数)
  • 没有被 Controller 管理的裸 Pod(Bare Pod)
  • 使用本地存储的 Pod(数据会随节点删除而丢失)
  • kube-system 命名空间中且不是 DaemonSet 管理的 Pod
  • 配置了 "cluster-autoscaler.kubernetes.io/safe-to-evict": "false" 注解的 Pod

六、资源配额与限制

6.1 ResourceQuota:命名空间级资源配额

ResourceQuota 用于限制一个命名空间中可以创建的资源总量,防止某个团队或项目占用过多集群资源。

apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
# 计算资源配额
requests.cpu: "20" # 该命名空间最多请求 20 核 CPU
requests.memory: "40Gi" # 最多请求 40Gi 内存
limits.cpu: "40" # 最多限制 40 核 CPU
limits.memory: "80Gi" # 最多限制 80Gi 内存
# 对象数量配额
pods: "50" # 最多 50 个 Pod
services: "10" # 最多 10 个 Service
persistentvolumeclaims: "20" # 最多 20 个 PVC
configmaps: "20" # 最多 20 个 ConfigMap
secrets: "20" # 最多 20 个 Secret
# 存储
requests.storage: "100Gi" # 最多请求 100Gi 存储
ResourceQuota 的作用范围

ResourceQuota 只限制设置了 requestslimits 的 Pod。如果一个 Pod 没有设置资源请求,它不会计入 ResourceQuota 的配额,但也不会被调度(除非命名空间配置了 LimitRange 来提供默认值)。

6.2 LimitRange:默认资源限制

LimitRange 用于为命名空间中的 Pod 或 Container 设置默认的资源请求和限制值,确保即使 Pod 的 YAML 中没有声明资源,也能获得合理的默认值。

apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: team-a
spec:
limits:
# Container 级别默认值
- type: Container
default: # 默认 Limit
cpu: "500m"
memory: "512Mi"
defaultRequest: # 默认 Request
cpu: "100m"
memory: "128Mi"
max: # 最大允许值
cpu: "2"
memory: "2Gi"
min: # 最小允许值
cpu: "50m"
memory: "64Mi"
# Pod 级别限制
- type: Pod
max:
cpu: "4"
memory: "4Gi"

6.3 PriorityClass:优先级与抢占

PriorityClass 用于定义 Pod 的优先级。当集群资源不足时,高优先级的 Pod 可以抢占低优先级 Pod 的资源。

# 定义高优先级类
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
description: "核心业务 Pod,优先调度"
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
# 定义低优先级类
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: low-priority
description: "批处理任务,资源空闲时运行"
value: 100
globalDefault: false
preemptionPolicy: PreemptLowerPriority
# 使用优先级类
apiVersion: v1
kind: Pod
metadata:
name: critical-app
spec:
priorityClassName: high-priority
containers:
- name: app
image: nginx:1.25
resources:
requests:
cpu: "1"
memory: "1Gi"
limits:
cpu: "2"
memory: "2Gi"
抢占机制

当高优先级 Pod 无法调度时,调度器会尝试驱逐低优先级 Pod 来释放资源。抢占过程分为两步:

  1. 驱逐(Eviction):找到可以抢占的低优先级 Pod
  2. 等待(Waiting):等待被驱逐的 Pod 优雅终止(graceful shutdown)

系统保留的 PriorityClass:

  • system-cluster-critical(值 2000000000):系统组件专用
  • system-node-critical(值 2000001000):节点关键组件专用(如 kube-proxy)

七、调度策略最佳实践

7.1 生产环境调度建议

场景建议策略
核心业务Guaranteed QoS + nodeAffinity 硬约束 + 高优先级 + PDB 保护
批处理任务BestEffort/Burstable QoS + 低优先级 + 反亲和性避免影响在线业务
GPU 工作负载nodeSelector + Taint/Toleration + ResourceQuota 限制 GPU 数量
高可用部署podAntiAffinity(hostname 级别)+ topologySpreadConstraints
多可用区部署nodeAffinity(zone 级别)+ topologySpreadConstraints

7.2 常见调度问题排查

问题现象可能原因排查命令
Pod 一直 Pending资源不足 / 亲和性不满足 / Taint 不匹配kubectl describe pod <name>
Pod 频繁被驱逐节点资源压力 / QoS 等级过低kubectl get events --sort-by=.metadata.creationTimestamp
节点资源不均衡缺少反亲和性配置 / 打分权重不合理kubectl top nodes
HPA 不生效未安装 Metrics Server / 指标未就绪kubectl get hpa -w
VPA 与 HPA 冲突同时基于 CPU/内存工作检查 VPA 和 HPA 的指标配置
排查 Pending Pod 的黄金法则

当 Pod 处于 Pending 状态时,第一步永远是执行 kubectl describe pod <name>,查看 Events 部分。调度器会在 Events 中详细记录过滤失败的原因,例如:

0/5 nodes are available: 1 Insufficient cpu, 3 node(s) had taint {node.kubernetes.io/disk-pressure:}, that the pod didn't tolerate, 1 node(s) didn't match node selector.

这条信息清晰地告诉你:1 个节点 CPU 不足,3 个节点有磁盘压力污点,1 个节点不匹配 nodeSelector。


八、官方文档参考

本文内容基于 Kubernetes v1.34 官方文档校验,以下是相关官方文档链接:

调度相关

资源管理相关

配额与限制


九、本章小结

本文深入剖析了 Kubernetes 调度与资源管理的核心机制,主要知识点回顾如下:

  1. 资源模型:K8s 通过 Request/Limit 控制容器的资源使用,QoS 等级(Guaranteed/Burstable/BestEffort)决定了资源不足时的驱逐顺序。

  2. 调度器:kube-scheduler 通过官方定义的"Filtering → Scoring"两步操作完成节点选择,随后通过 Binding 将 Pod 绑定到目标节点。nodeSelector/nodeAffinity/podAffinity/Taint-Toleration 提供了灵活的调度约束。

  3. 弹性伸缩

    • HPA:水平伸缩,调整副本数量,响应最快
    • VPA:垂直伸缩,调整资源配额,适合优化资源利用率
    • Cluster Autoscaler:节点级伸缩,配合 HPA 实现完整的弹性能力
  4. 资源治理:ResourceQuota 限制命名空间资源总量,LimitRange 提供默认资源值,PriorityClass 实现优先级与抢占。

  5. 最佳实践:核心业务使用 Guaranteed QoS + 高优先级 + PDB 保护;批处理任务使用低优先级 + 反亲和性;多可用区部署使用 topologySpreadConstraints。

掌握这些机制,你就能在生产环境中构建一个高效、稳定、弹性的 Kubernetes 资源管理体系。

Kubernetes 全景解析 (3):存储体系与配置管理深度剖析

· 阅读需 31 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

前言

在 Kubernetes 的核心能力矩阵中,存储与配置管理是决定应用能否在生产环境稳定运行的关键支柱。容器天生是"用完即弃"的——Pod 被删除后,其文件系统上的所有数据都会随之消失。然而,数据库需要持久化数据文件,日志系统需要保留历史记录,微服务需要在不重新构建镜像的前提下修改配置参数。这些需求共同构成了 K8s 存储体系与配置管理的设计动机。

本文将从存储体系的分层架构出发,逐层深入 Volume、PV/PVC、StorageClass 的设计原理与使用方法,再到 ConfigMap 与 Secret 的配置外部化实践,帮助你建立一套完整的 K8s 存储与配置认知框架。


一、K8s 存储体系概览

1.1 为什么容器需要持久化存储

容器的文件系统由一层层只读的镜像层(Image Layer)和一个可写的容器层(Container Layer)组成。当容器被删除时,可写层也会被一并回收。这意味着:

  • 数据库数据会丢失:MySQL、PostgreSQL 等有状态服务将无法正常工作
  • 日志会丢失:应用产生的日志随容器消亡而消失
  • 文件上传会丢失:用户上传的附件、图片等临时存储在容器内会不可恢复

为了解决这些问题,Kubernetes 引入了 Volume 机制,将外部存储挂载到容器内部的指定路径,使数据独立于容器的生命周期而存在。

1.2 存储体系的分层设计

Kubernetes 的存储体系采用了经典的分层抽象设计,从底层到上层依次为:

层级概念作用域核心职责
L1VolumePod定义 Pod 内部存储的挂载方式
L2PV / PVC集群 / 命名空间解耦存储的供给与消费
L3StorageClass集群实现存储的动态供给与分类

这种分层设计的核心思想是关注点分离:开发人员只需声明"我需要多少存储"(PVC),运维人员负责"提供什么样的存储"(PV/StorageClass),两者通过标准的接口进行匹配。

设计哲学

K8s 存储体系的设计遵循"供需分离"原则。就像云计算中的 IaaS 层将计算资源虚拟化一样,K8s 将存储资源抽象为标准化的 API 对象,使上层应用无需关心底层存储的具体实现(NFS、Ceph、AWS EBS、GCE PD 等)。


二、Volume:Pod 级存储抽象

2.1 Volume 的基本概念

Volume 是 Kubernetes 中最基础的存储抽象,它定义在 Pod 规格中,与 Pod 的生命周期紧密绑定。当 Pod 被创建时,Kubelet 会负责将 Volume 挂载到容器内指定的路径;当 Pod 被删除时,Volume 的处理方式取决于其类型——临时 Volume 会被清除,而持久化 Volume 中的数据则会被保留。

Volume 的几个关键特性:

  • Volume 定义在 Pod 级别:同一个 Pod 中的多个容器可以共享同一个 Volume
  • Volume 的生命周期与 Pod 绑定:Pod 不存在时,Volume 也不存在(临时类型)
  • Volume 支持多种后端:K8s 内置了数十种 Volume 类型,同时通过 CSI(Container Storage Interface)支持第三方存储

2.2 常见 Volume 类型

Volume 类型持久性适用场景说明
emptyDir临时Pod 内容器间共享数据、缓存Pod 删除后数据丢失,可选用内存作为介质
hostPath节点级开发调试、访问节点文件直接挂载宿主机路径,存在安全风险
nfs持久多 Pod 共享文件存储需要外部 NFS 服务器
configMap配置注入配置数据以文件或环境变量形式挂载
secret敏感配置注入密钥、证书Base64 编码存储,支持加密
persistentVolumeClaim持久有状态应用的数据持久化引用 PV,生命周期独立于 Pod
csi取决于后端接入第三方存储通过 CSI 驱动对接各种存储系统

2.3 Volume 生命周期与 Pod 的绑定关系

# Volume 生命周期与 Pod 的关系
# 1. Pod 被调度到某个 Node
# 2. Kubelet 调用 Volume 插件挂载存储到宿主机
# 3. Kubelet 将宿主机上的挂载点绑定到容器内
# 4. Pod 运行期间,容器读写 Volume 中的数据
# 5. Pod 被删除时,Kubelet 执行卸载操作
# 6. 临时 Volume 数据被清除,持久化 Volume 数据被保留
注意

emptyDir 的生命周期严格绑定到 Pod。即使 Pod 被驱逐(Eviction)或节点维护导致 Pod 被重新调度到其他节点,原节点上的 emptyDir 数据也会丢失。

2.4 完整 YAML 示例

以下示例展示了如何在 Pod 中使用多种 Volume:

apiVersion: v1
kind: Pod
metadata:
name: volume-demo
labels:
app: volume-demo
spec:
containers:
# 容器 1:写入数据到共享 Volume
- name: writer
image: busybox:1.36
command: ["/bin/sh", "-c"]
args:
- |
while true; do
date >> /data/log.txt
echo "Writer: data written" >> /data/log.txt
sleep 5
done
volumeMounts:
- name: shared-data
mountPath: /data

# 容器 2:从共享 Volume 读取数据
- name: reader
image: busybox:1.36
command: ["/bin/sh", "-c"]
args: ["tail -f /data/log.txt"]
volumeMounts:
- name: shared-data
mountPath: /data

# 容器 3:使用 hostPath 访问宿主机时间
- name: time-checker
image: busybox:1.36
command: ["/bin/sh", "-c"]
args: ["while true; do cat /host-time/hostname; sleep 10; done"]
volumeMounts:
- name: host-time
mountPath: /host-time

volumes:
# emptyDir:Pod 内容器间共享的临时存储
- name: shared-data
emptyDir:
sizeLimit: 256Mi # 可选:限制存储大小

# hostPath:挂载宿主机目录(仅用于开发/调试)
- name: host-time
hostPath:
path: /etc
type: Directory # Directory/DirectoryOrCreate/File/FileOrCreate/Socket/CharDevice/BlockDevice

三、PV 与 PVC:持久化存储的供需模型

3.1 PV(PersistentVolume):集群级存储资源

PersistentVolume 是集群级别的一块存储资源,由集群管理员创建,或者由 StorageClass 动态供给。PV 独立于 Pod 的生命周期存在,即使使用它的 Pod 被删除,PV 中的数据仍然保留。

PV 的核心属性包括:

  • 容量(Capacity):存储大小,如 10Gi
  • 访问模式(Access Modes):定义如何访问存储
  • 回收策略(Reclaim Policy):定义 PVC 释放后 PV 的处理方式
  • 存储类(StorageClass):定义存储的类型和参数
  • 挂载选项(Mount Options):传递给挂载命令的额外参数

3.2 PVC(PersistentVolumeClaim):命名空间级存储请求

PersistentVolumeClaim 是用户对存储资源的"申请单"。PVC 在命名空间内创建,用户通过 PVC 声明所需的存储大小、访问模式等需求,K8s 控制平面会自动将满足条件的 PV 绑定到该 PVC。

PV 与 PVC 的关系

可以将 PV 理解为"机房里的一块硬盘",PVC 理解为"申请一块硬盘的工单"。管理员提前准备硬盘(静态供给),或者设置自动采购流程(动态供给),用户只需提交工单即可。

3.3 供给模式:静态供给 vs 动态供给

静态供给(Static Provisioning)

管理员预先创建 PV,用户创建 PVC 后,K8s 控制平面根据容量和访问模式进行匹配绑定。这种方式适合存储资源有限、需要精确控制的场景。

动态供给(Dynamic Provisioning)

管理员预先创建 StorageClass,用户在 PVC 中指定 StorageClass,K8s 自动调用对应的 Provisioner 创建 PV 并绑定到 PVC。这种方式是生产环境的主流选择,极大简化了存储管理。

3.4 访问模式

访问模式缩写说明
ReadWriteOnceRWO存储可被单个节点以读写模式挂载
ReadOnlyManyROX存储可被多个节点以只读模式挂载
ReadWriteManyRWX存储可被多个节点以读写模式挂载
ReadWriteOncePodRWOP存储可被单个 Pod 以读写模式挂载(K8s 1.27+ 稳定)
访问模式与存储后端

并非所有存储后端都支持所有访问模式。例如,AWS EBS、GCE PD、Azure Disk 等块存储通常只支持 RWO;而 NFS、CephFS、GlusterFS 等文件存储支持 RWX。在选择存储后端时,务必确认其支持的访问模式是否满足你的需求。

3.5 回收策略

回收策略说明适用场景
RetainPVC 释放后,PV 被保留但标记为 Released,数据不丢失需要手动处理数据的场景
DeletePVC 释放后,PV 和底层存储资源被自动删除动态供给的云盘等临时存储
Recycle执行 rm -rf 清空数据后重新可用已废弃,不建议使用
Recycle 回收策略已废弃

官方明确说明:The Recycle reclaim policy is deprecated. Instead, the recommended approach is to use dynamic provisioning.

Recycle 策略仅对支持该操作的底层卷插件有效,执行基本的 rm -rf /thevolume/* 清理操作。在生产环境中,推荐使用以下替代方案:

  • 动态供给(Dynamic Provisioning):通过 StorageClass 自动创建和管理 PV,设置 reclaimPolicy: DeletereclaimPolicy: Retain
  • Retain + 手动管理:设置 reclaimPolicy: Retain,由管理员手动清理和复用 PV

3.6 PV/PVC 绑定流程

3.7 Storage Object in Use Protection

Storage Object in Use Protection(存储对象使用中保护)机制确保正在被 Pod 使用的 PVC 和已绑定到 PVC 的 PV 不会被意外删除,从而防止数据丢失。

PVC 保护机制

当 PVC 正在被 Pod 使用时,如果用户删除 PVC,PVC 不会立即被删除。PVC 的删除会被推迟,直到它不再被任何 Pod 使用。

PV 保护机制

当管理员删除已绑定到 PVC 的 PV 时,PV 不会立即被删除。PV 的删除会被推迟,直到它不再绑定到 PVC。

查看保护状态

# 查看 PVC 的保护状态
kubectl describe pvc hostpath

输出示例:

Name: hostpath
Namespace: default
StorageClass: example-hostpath
Status: Terminating
Finalizers: [kubernetes.io/pvc-protection] # PVC 保护 finalizer
# 查看 PV 的保护状态
kubectl describe pv task-pv-volume

输出示例:

Name: task-pv-volume
Labels: type=local
Finalizers: [kubernetes.io/pv-protection] # PV 保护 finalizer
StorageClass: standard
Status: Terminating
Claim: default/hostpath
Reclaim Policy: Delete
Access Modes: RWO
Capacity: 1Gi
Finalizer 的作用

Finalizer 是 Kubernetes 中的一种机制,用于确保资源在被删除前完成必要的清理操作。kubernetes.io/pvc-protectionkubernetes.io/pv-protection 这两个 finalizer 确保存储资源在使用中时不会被意外删除。

3.8 PV 删除保护 Finalizer(v1.33 Stable)

FEATURE STATE: Kubernetes v1.33 [stable](默认启用)

除了基础的 Storage Object in Use Protection,Kubernetes v1.33 引入了更强大的 PV 删除保护 finalizer 机制,确保具有 Delete 回收策略的 PV 仅在底层存储被删除后才从 Kubernetes 中移除。

两种 Finalizer

Finalizer 名称引入版本适用范围说明
external-provisioner.volume.kubernetes.io/finalizerv1.31CSI 卷(动态和静态供给)确保底层存储删除后才移除 PV 对象
kubernetes.io/pv-controllerv1.31In-tree 插件卷(仅动态供给)确保底层存储删除后才移除 PV 对象

CSI 卷示例

kubectl describe pv pvc-2f0bab97-85a8-4552-8044-eb8be45cf48d

输出示例:

Name: pvc-2f0bab97-85a8-4552-8044-eb8be45cf48d
Annotations: pv.kubernetes.io/provisioned-by: csi.vsphere.vmware.com
Finalizers: [kubernetes.io/pv-protection external-provisioner.volume.kubernetes.io/finalizer]
StorageClass: fast
Status: Bound
Claim: demo-app/nginx-logs
Reclaim Policy: Delete
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 200Mi
Source:
Type: CSI
Driver: csi.vsphere.vmware.com

In-tree 插件卷示例

kubectl describe pv pvc-74a498d6-3929-47e8-8c02-078c1ece4d78

输出示例:

Name: pvc-74a498d6-3929-47e8-8c02-078c1ece4d78
Annotations: kubernetes.io/provisioned-by: kubernetes.io/vsphere-volume
Finalizers: [kubernetes.io/pv-protection kubernetes.io/pv-controller]
StorageClass: vcp-sc
Status: Bound
Claim: default/vcp-pvc-1
Reclaim Policy: Delete
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 1Gi
Source:
Type: vSphereVolume
Finalizer 的作用

这些 finalizer 确保:

  1. PV 对象仅在底层存储被删除后才从 Kubernetes 中移除
  2. 无论 PV 和 PVC 的删除顺序如何,都能确保底层存储被正确删除
  3. 当启用 CSIMigration{provider} 特性门控时,kubernetes.io/pv-controller 会被 external-provisioner.volume.kubernetes.io/finalizer 替代

3.9 PVC 扩容(v1.24 Stable)

FEATURE STATE: Kubernetes v1.24 [stable](默认启用)

PVC 扩容功能允许用户在 PVC 创建后增加其存储容量,而无需重新创建 PVC。

支持扩容的卷类型

  • CSI 卷(包括部分迁移的卷类型)
  • flexVolume(已废弃)
  • portworxVolume(已废弃)

前提条件

PVC 的 StorageClass 必须设置 allowVolumeExpansion: true

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: example-vol-default
provisioner: vendor-name.example/magicstorage
parameters:
resturl: "http://192.168.10.100:8080"
allowVolumeExpansion: true # 允许扩容

扩容步骤

  1. 编辑 PVC 对象,增加 spec.resources.requests.storage 的值
  2. Kubernetes 控制平面触发底层卷的扩容操作
  3. 不会创建新的 PV,而是扩展现有卷
直接编辑 PV 容量

不要直接编辑 PV 的容量。如果先编辑 PV 的容量,再编辑 PVC 使其与 PV 匹配,Kubernetes 控制平面会认为卷已被手动扩容,不会触发自动扩容操作。

3.10 CSI 卷扩容(v1.24 Stable)

FEATURE STATE: Kubernetes v1.24 [stable](默认启用)

CSI 卷扩容功能默认启用,但需要特定的 CSI 驱动程序支持卷扩容。请参考具体 CSI 驱动程序的文档以获取更多信息。

3.11 VolumeAttributesClasses(VAC)

VolumeAttributesClasses(VAC)是 Kubernetes 中用于管理存储属性的新 API,允许在不重新创建卷的情况下修改存储的某些属性(如性能级别、IOPS、吞吐量等)。

VAC 的作用

  • 修改存储卷的性能参数(IOPS、吞吐量)
  • 切换存储的备份策略
  • 调整存储的加密级别
  • 其他由存储后端支持的属性

VAC 示例

apiVersion: storage.k8s.io/v1alpha1
kind: VolumeAttributesClass
metadata:
name: fast-ssd
driverName: csi.example.com
parameters:
iops: "5000"
throughput: "500"
encryption: "true"
VAC 的使用

VAC 目前仍处于 Alpha 阶段,具体支持程度取决于 CSI 驱动程序的实现。在生产环境使用前,请确认您的存储后端是否支持 VAC。

3.12 完整 YAML 示例

# ============================================
# 1. 静态供给:先创建 PV,再创建 PVC
# ============================================

# PersistentVolume:集群级存储资源
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nfs:
server: 10.0.0.100
path: /data/mysql
# 可选:挂载选项
mountOptions:
- hard
- nfsvers=4.1

---
# PersistentVolumeClaim:用户存储请求
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
namespace: database
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: manual # 必须与 PV 的 storageClassName 一致

---
# 使用 PVC 的 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
namespace: database
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: mysql-pvc

四、StorageClass:存储的"类"与动态供给

4.1 StorageClass 的作用与参数

StorageClass 为管理员提供了一种描述存储"类别"的方式。不同的 StorageClass 可以映射到不同的存储后端、不同的性能等级、不同的备份策略等。用户在创建 PVC 时只需指定 StorageClass,K8s 就会自动完成存储的创建和绑定。

StorageClass 的核心参数:

参数说明示例
provisioner存储供给插件kubernetes.io/aws-ebs
parameters供给插件的具体参数type: gp3, iopsPerGB: "50"
reclaimPolicy默认回收策略Delete
volumeBindingMode卷绑定模式WaitForFirstConsumer
allowVolumeExpansion是否允许扩容true

4.2 动态供给工作流

动态供给的核心流程如下:

  1. 用户创建 PVC,指定 storageClassName
  2. PVC Controller 发现没有现成的 PV 可匹配
  3. PVC Controller 调用对应 StorageClass 的 Provisioner
  4. Provisioner 调用底层存储 API(如 AWS CreateVolume)创建实际的存储卷
  5. Provisioner 创建 PV 对象并关联到该 PVC
  6. PVC 状态变为 Bound,Pod 可以使用该 PVC
WaitForFirstConsumer

对于拓扑感知的存储(如 AWS EBS、GCE PD),建议设置 volumeBindingMode: WaitForFirstConsumer。这样 PV 不会在 PVC 创建时立即分配,而是等到第一个使用该 PVC 的 Pod 被调度后,再在与 Pod 相同的可用区(AZ)创建存储卷,避免跨可用区挂载的问题。

4.3 常见 Provisioner

Provisioner存储类型支持的访问模式说明
kubernetes.io/aws-ebsAWS EBSRWOAWS 弹性块存储
kubernetes.io/gce-pdGCE Persistent DiskRWO/RWXGoogle Cloud 持久磁盘
kubernetes.io/azure-diskAzure DiskRWOAzure 磁盘
kubernetes.io/azure-fileAzure FileROX/RWXAzure 文件存储
nfs.csi.k8s.ioNFSROX/RWXNFS CSI 驱动
cephfs.csi.ceph.comCephFSROX/RWXCeph 文件系统
rbd.csi.ceph.comCeph RBDRWOCeph 块存储
local-path-provisioner本地路径RWO轻量级本地存储,适合开发测试

4.4 完整 YAML 示例

# ============================================
# StorageClass:定义存储类别
# ============================================

# 高性能 SSD 存储类(AWS EBS gp3)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
iopsPerGB: "50"
throughput: "250"
fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

---
# 标准 HDD 存储类(AWS EBS st1)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-hdd
provisioner: kubernetes.io/aws-ebs
parameters:
type: st1
fsType: ext4
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: false

---
# 使用动态供给的 PVC(无需预先创建 PV)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data-pvc
namespace: production
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd # 指定 StorageClass,触发动态供给
resources:
requests:
storage: 50Gi

---
# 本地路径 Provisioner(适合开发/测试环境)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
provisioner: rancher.io/local-path
parameters:
archiveOnDelete: "true"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

五、ConfigMap:配置外部化

5.1 ConfigMap 的创建方式

ConfigMap 用于将非敏感的配置数据从容器镜像中解耦出来,使配置可以在不重新构建镜像的情况下被修改和更新。ConfigMap 支持以下几种创建方式:

方式一:字面量创建

kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=MAX_CONNECTIONS=100 \
--from-literal=FEATURE_FLAGS=dark-mode,experimental-api

方式二:从文件创建

kubectl create configmap nginx-config \
--from-file=nginx.conf=./configs/nginx.conf \
--from-file=default.conf=./configs/default.conf

方式三:从目录创建

# 将目录下所有文件作为 ConfigMap 的键值对
kubectl create configmap app-settings \
--from-file=./configs/

方式四:从 env-file 创建

kubectl create configmap app-env \
--from-env-file=./.env

5.2 在 Pod 中使用 ConfigMap

ConfigMap 可以通过三种方式注入到 Pod 中:

注入方式适用场景热更新说明
环境变量简单配置项不支持Pod 重启后生效
Volume 挂载配置文件支持文件内容实时更新
命令行参数启动参数不支持通过 $(ENV_VAR) 引用

5.3 ConfigMap 热更新机制与限制

当通过 Volume 挂载方式使用 ConfigMap 时,K8s 的 kubelet 会定期(默认每 60 秒)检查 ConfigMap 的更新,并将最新的内容同步到挂载的文件中。应用可以通过监听文件变化来重新加载配置。

热更新的限制
  1. 通过环境变量注入的 ConfigMap 不支持热更新,必须重启 Pod 才能生效
  2. 已挂载的文件内容会延迟更新,更新间隔由 kubelet 的 sync period 决定(默认 60 秒)
  3. 应用需要自行实现配置重载逻辑,K8s 只负责更新文件内容,不会自动通知应用
  4. subPath 挂载不支持热更新,使用 subPath 时,ConfigMap 更新不会反映到容器中

5.4 完整 YAML 示例

# ============================================
# ConfigMap:配置外部化
# ============================================

# 创建 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
# 简单键值对
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
CACHE_TTL: "300"

# 完整配置文件
application.yml: |
server:
port: 8080
shutdown: graceful
spring:
datasource:
url: jdbc:postgresql://db:5432/mydb
hikari:
maximum-pool-size: 20
logging:
level:
root: INFO
com.example: DEBUG

# Redis 配置
redis.conf: |
bind 0.0.0.0
port 6379
maxmemory 256mb
maxmemory-policy allkeys-lru
appendonly yes

---
# Pod 中使用 ConfigMap(三种方式)
apiVersion: v1
kind: Pod
metadata:
name: configmap-demo
namespace: production
spec:
containers:
- name: app
image: my-app:latest
ports:
- containerPort: 8080

# 方式 1:作为环境变量
env:
- name: APP_LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
- name: APP_MAX_CONNECTIONS
valueFrom:
configMapKeyRef:
name: app-config
key: MAX_CONNECTIONS

# 方式 1 补充:一次性导入所有键为环境变量
envFrom:
- configMapRef:
name: app-config
# 可选:前缀
prefix: APP_

# 方式 2:作为 Volume 挂载(支持热更新)
volumeMounts:
- name: config-vol
mountPath: /etc/app/config
readOnly: true
- name: redis-config
mountPath: /etc/redis/redis.conf
subPath: redis.conf # 注意:subPath 不支持热更新

# 方式 3:作为命令行参数
args: ["--log-level", "$(APP_LOG_LEVEL)"]

volumes:
- name: config-vol
configMap:
name: app-config
# 可选:指定要挂载的键
items:
- key: application.yml
path: application.yml
- key: redis.conf
path: redis.conf
# 可选:设置文件权限
defaultMode: 0644
- name: redis-config
configMap:
name: app-config

六、Secret:敏感信息管理

6.1 Secret 的类型

Secret 与 ConfigMap 类似,但专门用于存储敏感信息。Secret 中的数据以 Base64 编码存储(并非加密),在生产环境中应结合加密机制使用。

Secret 类型用途说明
Opaque通用类型默认类型,存储任意键值对
kubernetes.io/dockerconfigjsonDocker 镜像仓库认证用于拉取私有镜像
kubernetes.io/tlsTLS 证书存储 TLS 证书和私钥
kubernetes.io/basic-auth基本认证存储用户名和密码
kubernetes.io/ssh-authSSH 认证存储 SSH 私钥
kubernetes.io/service-account-tokenServiceAccount Token自动为 SA 生成 Token

6.2 与 ConfigMap 的区别

对比维度ConfigMapSecret
数据编码明文存储Base64 编码
大小限制1 MiB1 MiB
etcd 加密通常不加密可配置 EncryptionConfiguration 加密
访问控制标准 RBAC更严格的 RBAC 审计
使用场景应用配置、环境变量密码、密钥、证书、Token
安全警告

Secret 的 Base64 编码不是加密,任何人只要有访问 etcd 的权限就能解码。在生产环境中,务必启用 etcd 静态加密(Encryption at Rest),并严格限制 Secret 的访问权限。

6.3 生产环境 Secret 加密架构与最佳实践

由于 K8s 的 Base64 编码毫无机密性可言(类似于把中文字符翻译成拼音,任何人都能还原),一旦黑客获取了 etcd 数据库的备份文件、或者通过提权直接读取了物理机的磁盘,所有的密码与证书都将彻底暴露。

因此,在生产环境中,必须结合真正的加密机制来保护 Secret 数据。目前主流的三种架构方案如下:

方案一:etcd 静态加密与 KMS (Key Management Service) 插件

Kubernetes 原生支持在存储层进行加密(Encryption at Rest),即 kube-apiserver 在将 Secret 数据写入 etcd 前先将其加密,在客户端读取时再进行解密。

最基础的静态加密是利用本地密钥文件:

# /etc/kubernetes/pki/secrets-encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key> # 本地对称加密密钥
- identity: {} # 兜底:未加密的 Secret 仍可读取

进阶:结合 KMS 插件(生产强烈推荐) 将主密钥放在 apiserver 的本地配置文件中仍有泄漏风险,因此大型生产环境通常会接入云厂商的 KMS(如 AWS KMS、阿里云 KMS)HashiCorp Vault。 在这种架构下,K8s 采用信封加密(Envelope Encryption)

  1. kube-apiserver 随机生成一个 DEK(数据加密密钥)把 Secret 加密。
  2. API Server 通过 Unix Socket 调用 KMS 插件,KMS 插件将 DEK 发给外部密钥管理系统。
  3. 外部 KMS 使用它的 KEK(密钥加密密钥)把 DEK 加密后返回给 K8s。
  4. K8s 将加密后的 DEK加密后的 Secret一并存入 etcd,实现了彻头彻尾的解耦和高安全性。

方案二:GitOps 工作流与 Sealed Secrets

如果在团队中实施 GitOps(如搭配 ArgoCD / FluxCD),所有的 K8s YAML 配置都要提交到 Git 仓库进行版本管理。但如果你将带有 Base64 编码的 Secret YAML 直推到 Github,无异于公开密码。

Bitnami Sealed Secrets 完美解决了这个问题:

  1. 集群中部署一个 Sealed Secrets Controller,它会在集群内部生成一对非对称的公私钥,并严格保护私钥。
  2. 开发者在本地使用 kubeseal 命令行工具,结合前面获取的公钥,将原本的 Secret 加密为一个名为 SealedSecret 的自定义资源(CRD)。
  3. 无论谁拿到这个 SealedSecret 的密文内容都无法解密,因此可以放心地将其公开 Push 到 Git 仓库。
  4. ArgoCD 将其同步进 K8s 集群后,处于集群内的 Controller 识别到该对象,利用极密私钥将其解密,并在集群内存中隐式还原为一个标准的 Kubernetes Secret 对象,供 Pod 挂载消费。

方案三:密码外置化与 External Secrets Operator (ESO)

对于多微服务多集群的企业级架构,将机密信息散落在各个 K8s 集群中维护是非常痛苦的。更现代的做法是引入外置机密治理中心

工作原理

  1. 真正的数据库密码、API Key 等完全不用 kubectl create secret 来建,而是由安全团队统一录入类似 AWS Secrets Manager, Google Secret ManagerHashiCorp Vault 这种专门的极高安全等级保险箱内。
  2. 随后在 K8s 集群部署 External Secrets Operator
  3. 运维人员在 K8s 声明一个 ExternalSecret 对象,指明要去远程保险箱获取哪一个 Key。
  4. 部署的控制器拿着具备精细权限配比的凭证去远程保险箱调接口,读取到数据以后,动态地在 K8s 中生成一个原生 Secret 给应用使用。并在云端密码发生变更时自动回滚和同步(甚至触发目标 Pod 重启)。

这实现了机密数据的"唯一真实数据源 (Single Source of Truth)"理念。

方案四:严格的 RBAC 控制(防线兜底)

不论采用何种加密架构,K8s 内部的 kube-apiserver 依然会自动向有权限的请求方分发解密好的数据。因此,防备越权读取的最后一公里,永远是最小权限原则(Principle of Least Privilege)

# 仅允许特定 ServiceAccount 读取某个指定的 Secret
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: specific-secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials", "api-keys"] # 切忌使用 * 或者留空
verbs: ["get"]

方案五:实操演练(直接窥探 etcd 证明 Base64 脆弱性)

为了更直观地理解为什么必须搭配 KMS/Sealed Secrets 等加密机制,你可以通过 etcdctl 直接查看集群底层的原生数据:

1. 创建一个普通的 Secret

kubectl create secret generic my-secret \
--from-literal=password=super_secret_pwd

2. 登录到 master 节点上,使用 etcdctl 直接读取数据:

ETCDCTL_API=3 etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
get /registry/secrets/default/my-secret

3. 如果未开启静态加密,你会直接看到明文 base64 payload:

/registry/secrets/default/my-secret
... "password": "c3VwZXJfc2VjcmV0X3B3ZA==" ...

任何人只要拿到这串字符,便可用 echo "c3Vw...==" | base64 -d 变为 super_secret_pwd

4. 如果配置了 etcd 静态加密(aescbc),底层返回的将是彻底的密文乱码:

/registry/secrets/default/my-secret
k8s:enc:aescbc:v1:key1:Q@c*a`X&_...

6.4 完整 YAML 示例

# ============================================
# Secret:敏感信息管理
# ============================================

# 1. Opaque Secret:通用密钥
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
# 值必须为 Base64 编码
# echo -n 'my-password' | base64
username: bXktdXNlcm5hbWU= # my-username
password: bXktcGFzc3dvcmQ= # my-password
connection-string: amRiYzpwb3N0Z3Jlc3FsOi8vZGItaG9zdDo1NDMyL215ZGI= # jdbc:postgresql://db-host:5432/mydb
---
# 2. Docker Registry Secret:镜像仓库认证
apiVersion: v1
kind: Secret
metadata:
name: docker-registry-secret
namespace: production
type: kubernetes.io/dockerconfigjson
data:
# 通过命令创建:kubectl create secret docker-registry ...
.dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsidXNlcm5hbWUiOiJ1c2VyIiwicGFzc3dvcmQiOiJwYXNzIiwiYXV0aCI6ImRYTmxjanB3WVhOeiJ9fX0=
---
# 3. TLS Secret:HTTPS 证书
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
namespace: production
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-cert>
tls.key: <base64-encoded-key>
---
# 4. 在 Pod 中使用 Secret
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
# 拉取私有镜像
imagePullSecrets:
- name: docker-registry-secret

containers:
- name: api
image: my-registry.com/api-server:v2.1.0
ports:
- containerPort: 8080

# 方式 1:作为环境变量
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password

# 方式 2:作为 Volume 挂载
volumeMounts:
- name: db-secret-vol
mountPath: /etc/secrets/db
readOnly: true
- name: tls-vol
mountPath: /etc/tls
readOnly: true

volumes:
- name: db-secret-vol
secret:
secretName: db-credentials
defaultMode: 0400 # 仅所有者可读
- name: tls-vol
secret:
secretName: tls-secret
defaultMode: 0400

七、存储选型决策指南

7.1 存储类型对比

存储类型持久性性能共享能力适用场景推荐度
emptyDir临时高(内存)/ 中(磁盘)Pod 内缓存、临时文件、容器间数据共享
hostPath节点级同节点 Pod开发调试、DaemonSet 日志收集
PV/PVC + NFS持久多节点共享文件存储、内容管理系统
PV/PVC + 云盘持久单节点数据库、高性能单实例应用
PV/PVC + Ceph持久多节点企业级存储、大规模集群
CSI 第三方存储取决于后端取决于后端取决于后端特殊存储需求(对象存储、SAN 等)
ConfigMap配置--应用配置、环境变量、配置文件
Secret敏感配置--密码、密钥、证书、Token

7.2 存储选型决策树

生产环境推荐方案
  • 有状态服务(数据库、消息队列):使用云盘 SSD + StorageClass 动态供给,设置 volumeBindingMode: WaitForFirstConsumer
  • 共享文件(上传文件、静态资源):使用 NFS / CephFS / 对象存储(通过 CSI)
  • 应用配置:ConfigMap + Volume 挂载,配合 Reloader 等工具实现自动滚动更新
  • 敏感信息:Secret + etcd 加密 + External Secrets Operator(对接 Vault 或云厂商密钥管理服务)
  • 日志与监控数据:使用 emptyDir + sidecar 模式收集,或直接写入外部存储

八、本章小结

本文深入剖析了 Kubernetes 存储体系与配置管理的核心机制,让我们回顾一下关键要点:

存储体系的三层架构

  • Volume 是最基础的存储抽象,定义在 Pod 级别,生命周期与 Pod 绑定
  • PV/PVC 实现了存储的供需分离,PV 是集群级资源,PVC 是命名空间级请求
  • StorageClass 实现了存储的动态供给,用户无需预先创建 PV

存储保护与安全机制

  • Storage Object in Use Protection:通过 kubernetes.io/pvc-protectionkubernetes.io/pv-protection finalizer 保护正在使用的存储资源
  • PV 删除保护 Finalizer(v1.33 Stable):external-provisioner.volume.kubernetes.io/finalizerkubernetes.io/pv-controller 确保底层存储被正确删除后才移除 PV 对象
  • Recycle 回收策略已废弃:推荐使用动态供给替代

存储扩容与属性管理

  • PVC 扩容(v1.24 Stable):需要 StorageClass 设置 allowVolumeExpansion: true
  • CSI 卷扩容(v1.24 Stable):需要 CSI 驱动程序支持
  • VolumeAttributesClasses(VAC):新 API,允许在不重建卷的情况下修改存储属性

配置管理的两大工具

  • ConfigMap 用于管理非敏感配置,支持环境变量注入和 Volume 挂载(支持热更新)
  • Secret 用于管理敏感信息,生产环境必须配合 etcd 加密和严格 RBAC 使用

选型的核心原则

  • 根据数据的持久性需求选择临时存储或持久化存储
  • 根据数据的共享需求选择块存储(RWO)或文件存储(RWX)
  • 根据数据的敏感性选择 ConfigMap 或 Secret
  • 优先使用 StorageClass 动态供给,减少运维负担

系列导航

章节主题状态
0架构设计与核心概念✅ 已发布
1工作负载与 Pod 生命周期深度解析✅ 已发布
2网络模型与服务发现全链路解析✅ 已发布
3存储体系与配置管理深度剖析✅ 已发布
4调度器、资源管理与弹性伸缩✅ 已发布
5安全体系与可观测性全景✅ 已发布
6生产级微服务架构实战✅ 已发布
7有状态应用与 Operator 模式实战✅ 已发布

相关阅读

Kubernetes 全景解析 (2):网络模型与服务发现全链路解析

· 阅读需 29 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

前言

在 Kubernetes 的众多核心概念中,网络模型与服务发现机制是最为复杂也最为关键的部分之一。一个健康的 K8s 集群,其网络层必须满足 Pod 之间、Pod 与 Node 之间、以及 Pod 与外部世界的无缝通信。同时,随着微服务架构的普及,服务发现、负载均衡、流量路由等能力变得不可或缺。

本文将深入剖析 K8s 网络模型的三层架构、Service 的四种类型与流量路径、Ingress 控制器原理,以及 NetworkPolicy 安全策略,帮助你全面掌握 K8s 网络的核心机制。

一、K8s 网络模型基础

1.1 K8s 网络的三个基本要求

Kubernetes 对集群网络提出了三个基本要求,这些要求构成了 K8s 网络模型的核心:

  1. 所有 Pod 可以在没有 NAT 的情况下相互通信

    • 每个 Pod 都拥有独立的 IP 地址
    • Pod 之间可以直接通信,无需网络地址转换
    • 无论 Pod 位于哪个 Node 上
  2. 所有 Node 可以与所有 Pod 通信

    • Node 可以直接访问 Pod 的 IP
    • 无需额外的端口映射或代理
  3. Pod 看到的自己 IP 与其他 Pod 看到的 IP 相同

    • 不存在 IP 欺骗或地址转换
    • 网络行为可预测
设计理念

K8s 采用"扁平网络"模型,避免了传统容器网络中的复杂 NAT 配置,使得网络行为更加透明和可预测。这种设计大大简化了应用的开发和调试。

1.2 CNI(Container Network Interface)插件机制

Kubernetes 本身不实现具体的网络功能,而是通过 CNI(Container Network Interface)标准将网络实现委托给第三方插件。CNI 定义了一套接口规范,网络插件只需实现这些接口即可与 K8s 集成。

CNI 插件的主要职责包括:

  • IP 地址分配:为 Pod 分配 IP 地址
  • 网络配置:设置 Pod 的网络命名空间、路由表、防火墙规则
  • 网络连接:创建 veth pair、网桥、路由等网络设备

1.3 常见 CNI 插件对比

插件名称网络模式性能功能特性适用场景
FlannelVXLAN/Host-GW中等简单易用,支持多种后端中小规模集群,快速部署
CalicoBGP/纯路由强大的网络策略,支持 IPAM需要细粒度网络控制的企业级应用
CiliumeBPF极高基于 eBPF 的可观测性和安全高性能、高安全要求的场景
Weave NetVXLAN/加密中等内置加密,简单部署对安全性要求较高的场景
选择建议
  • Flannel:适合入门学习和中小规模集群
  • Calico:适合需要网络策略和企业级功能的场景
  • Cilium:适合追求极致性能和可观测性的场景
  • Weave Net:适合对数据加密有强需求的场景

1.4 K8s 网络三层模型

上图展示了 K8s 网络的三层架构:

  • 第 1 层:节点网络 - 物理机或虚拟机的网络,用于节点间通信
  • 第 2 层:Pod 网络 - 覆盖网络(Overlay)或纯路由网络,用于 Pod 间通信
  • 第 3 层:服务网络 - 虚拟 IP 网络,用于服务发现和负载均衡

二、Service:服务发现与负载均衡

2.1 Service 的四种类型

Service 是 K8s 中用于服务发现和负载均衡的核心抽象。它为一组功能相同的 Pod 提供稳定的访问入口,并实现流量分发。

2.1.1 ClusterIP(默认类型)

ClusterIP 是 Service 的默认类型,它为 Service 分配一个集群内部的虚拟 IP(VIP),仅在集群内部可访问。

apiVersion: v1
kind: Service
metadata:
name: nginx-clusterip
namespace: default
spec:
type: ClusterIP
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP

访问方式

  • 集群内部:http://nginx-clusterip.default.svc.cluster.local:80
  • 集群内部(短名):http://nginx-clusterip:80
最佳实践

对于不需要外部访问的内部服务(如数据库、缓存、内部 API),始终使用 ClusterIP 类型以确保安全性。

2.1.2 NodePort

NodePort 在每个 Node 上开放一个端口(默认范围 30000-32767),外部流量可以通过 NodeIP:NodePort 访问 Service。

apiVersion: v1
kind: Service
metadata:
name: nginx-nodeport
namespace: default
spec:
type: NodePort
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30080
protocol: TCP

访问方式

  • 外部访问:http://<任意NodeIP>:30080
  • 集群内部:http://nginx-nodeport.default.svc.cluster.local:80
注意事项

NodePort 会占用 Node 的端口,且端口范围有限(默认 30000-32767)。生产环境不建议直接使用 NodePort 暴露服务,应结合 Ingress 或 LoadBalancer 使用。

2.1.3 LoadBalancer

LoadBalancer 类型会向云服务商申请一个负载均衡器,并将外部流量分发到后端的 Node。

apiVersion: v1
kind: Service
metadata:
name: nginx-loadbalancer
namespace: default
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
# 云服务商特定配置
loadBalancerIP: "203.0.113.10"
externalTrafficPolicy: Local

访问方式

  • 外部访问:http://<LoadBalancerIP>:80
  • 集群内部:http://nginx-loadbalancer.default.svc.cluster.local:80
云服务商依赖

LoadBalancer 类型需要云服务商的支持(如 AWS ELB、GCP Load Balancer、Azure Load Balancer)。在本地开发环境(如 Minikube、Kind)中,通常无法正常工作。

2.1.4 ExternalName

ExternalName 类型将 Service 映射到外部 DNS 名称,不创建 ClusterIP,主要用于访问外部服务。

apiVersion: v1
kind: Service
metadata:
name: external-database
namespace: default
spec:
type: ExternalName
externalName: database.example.com

访问方式

  • 集群内部:http://external-database.default.svc.cluster.local
  • 实际会被 DNS 解析为:database.example.com

2.6 Service 应用协议(appProtocol)

Service 的 appProtocol 字段(v1.20 Stable)允许为每个 Service 端口指定应用层协议,为实现提供更丰富的行为提示。

支持的协议类型

协议描述
IANA 标准服务名httphttpsftpsmtp 等(参考 IANA Service Names
kubernetes.io/h2cHTTP/2 over cleartext(明文 HTTP/2,参考 RFC 7540)
kubernetes.io/wsWebSocket over cleartext(明文 WebSocket,参考 RFC 6455)
kubernetes.io/wssWebSocket over TLS(加密 WebSocket,参考 RFC 6455)
自定义前缀协议mycompany.com/my-custom-protocol

使用示例

apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: myapp
ports:
- name: http
port: 80
targetPort: 9376
protocol: TCP
appProtocol: http # 指定应用协议
- name: grpc
port: 9090
targetPort: 9090
protocol: TCP
appProtocol: kubernetes.io/h2c # HTTP/2 cleartext
appProtocol 的作用

appProtocol 字段的值会自动同步到对应的 Endpoints 和 EndpointSlice 对象中,供 Ingress Controller、Service Mesh 等组件使用,以提供协议感知的路由、健康检查等增强功能。

2.7 Service 名称验证规则(v1.34 Alpha)

FEATURE STATE: Kubernetes v1.34 [alpha](默认关闭)

Kubernetes v1.34 引入了 RelaxedServiceNameValidation 特性门控,允许 Service 对象名称以数字开头。

默认规则(特性门控关闭):

  • Service 名称必须是有效的 RFC 1035 标签名称
  • 必须以字母开头,只能包含字母、数字和连字符(-)

放宽规则(特性门控开启):

  • Service 名称可以是有效的 RFC 1123 标签名称
  • 允许以数字开头
启用 RelaxedServiceNameValidation

要启用此特性,需要在 API Server 和 Controller Manager 的启动参数中添加:

--feature-gates=RelaxedServiceNameValidation=true

2.2 kube-proxy 工作模式

kube-proxy 是 K8s 网络的核心组件,负责维护 Service 的网络规则。它支持三种工作模式:

2.2.1 iptables 模式

iptables 模式使用 iptables 规则实现负载均衡,每个 Service 会创建一系列 iptables 规则。

优点

  • 实现简单,兼容性好
  • 无需额外依赖

缺点

  • 规则数量随 Service 数量线性增长,性能下降明显
  • 不支持高级负载均衡算法(仅支持随机轮询)
  • 调试困难
  • iptables 正逐步被 Linux 内核的 nftables 框架取代

2.2.2 IPVS 模式

IPVS(IP Virtual Server)是基于 Linux 内核的负载均衡技术,性能远高于 iptables。

优点

  • 性能优异,支持大规模 Service
  • 支持多种负载均衡算法(rr、lc、dh、sh、sed、nq)
  • 连接跟踪更精确

缺点

  • 需要加载 IPVS 内核模块
  • 配置相对复杂

2.2.3 nftables 模式(v1.33 GA)

nftables 是 Linux 内核新一代的防火墙和数据包过滤框架,旨在取代传统的 iptables。kube-proxy 的 nftables 模式自 v1.29 引入(Alpha),v1.33 达到 GA(正式可用)。

优点

  • 替代逐步淘汰的 iptables 框架,面向未来
  • 规则处理性能优于 iptables
  • 统一了 IPv4/IPv6/ARP/桥接的过滤框架
  • 内核 5.13+ 及 nft 命令行工具 1.0.1+ 支持

缺点

  • 需要较新的内核版本(>= 5.13)
  • 需要 nft 命令行工具(>= 1.0.1)
  • 生态成熟度仍在提升中
性能优化建议

对于生产环境,建议根据内核版本选择 kube-proxy 模式:

  • 内核 >= 5.13:优先使用 nftables 模式(面向未来,性能更优)
  • 内核 < 5.13:使用 IPVS 模式以获得更好的性能

可以通过修改 kube-proxy 的配置来启用:

apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "nftables" # 可选值: "iptables" | "ipvs" | "nftables"

2.3 DNS 服务发现(CoreDNS)

CoreDNS 是 K8s 集群的默认 DNS 服务器,它为 Service 和 Pod 提供 DNS 解析服务。

DNS 记录格式

资源类型DNS 记录格式示例
Service(普通)<service-name>.<namespace>.svc.cluster.localnginx.default.svc.cluster.local
Service(Headless)<pod-ip>.<service-name>.<namespace>.svc.cluster.local10.244.1.2.nginx.default.svc.cluster.local
Pod<pod-ip>.<namespace>.pod.cluster.local10.244.1.2.default.pod.cluster.local

CoreDNS 配置示例

apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}

2.4 Headless Service 与 StatefulSet 的配合

Headless Service 是一种特殊的 Service,它不分配 ClusterIP,而是直接将 DNS 解析到后端的 Pod IP。这种设计常与 StatefulSet 配合使用,用于有状态应用。

apiVersion: v1
kind: Service
metadata:
name: nginx-headless
namespace: default
spec:
clusterIP: None # Headless Service 的关键配置
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx-stateful
namespace: default
spec:
serviceName: nginx-headless # 关联 Headless Service
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80

DNS 解析结果

# 解析 Headless Service
$ dig nginx-headless.default.svc.cluster.local

;; ANSWER SECTION:
nginx-headless.default.svc.cluster.local. 5 IN A 10.244.1.2
nginx-headless.default.svc.cluster.local. 5 IN A 10.244.2.2
nginx-headless.default.svc.cluster.local. 5 IN A 10.244.3.2

# 解析单个 Pod
$ dig nginx-stateful-0.nginx-headless.default.svc.cluster.local

;; ANSWER SECTION:
nginx-stateful-0.nginx-headless.default.svc.cluster.local. 5 IN A 10.244.1.2
应用场景

Headless Service 适用于以下场景:

  • StatefulSet 应用(如数据库、消息队列)
  • 需要直接访问 Pod IP 的应用
  • 自定义负载均衡策略
  • 服务网格(Service Mesh)

2.8 Service 四种类型流量路径对比

上图展示了四种 Service 类型的流量路径:

  1. NodePort:外部流量通过 NodeIP:NodePort 进入,分发到各个 Node
  2. LoadBalancer:外部流量通过云负载均衡器进入,分发到各个 Node
  3. ClusterIP:集群内部流量通过虚拟 IP 分发到后端 Pod
  4. ExternalName:DNS 解析到外部服务名称

三、Ingress:七层路由入口

3.1 Ingress vs Service 的区别

特性ServiceIngress
网络层四层(L4 - TCP/UDP)七层(L7 - HTTP/HTTPS)
路由能力基于 IP 和端口基于 Host、Path、Header
协议支持TCP/UDPHTTP/HTTPS/gRPC
TLS 终止不支持支持
使用场景服务间通信、内部服务外部访问、多域名路由
核心区别

Service 是四层负载均衡,只能基于 IP 和端口进行路由;Ingress 是七层负载均衡,可以基于 HTTP Host、Path、Header 等进行智能路由。Ingress 通常作为多个 Service 的统一入口。

3.2 Ingress Controller

Ingress Controller 是 Ingress 资源的执行器,负责根据 Ingress 规则配置负载均衡器。常见的 Ingress Controller 包括:

Controller特点适用场景
Nginx Ingress成熟稳定,功能丰富通用场景,生产环境首选
Traefik自动配置,支持多种后端微服务架构,动态环境
APISIX高性能,支持云原生高流量、高并发场景
Istio Gateway服务网格集成已使用 Istio 的场景

Nginx Ingress Controller 部署示例

apiVersion: v1
kind: Namespace
metadata:
name: ingress-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
replicas: 2
selector:
matchLabels:
app: ingress-nginx
template:
metadata:
labels:
app: ingress-nginx
spec:
containers:
- name: controller
image: k8s.gcr.io/ingress-nginx/controller:v1.8.1
args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
---
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx
namespace: ingress-nginx
spec:
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
selector:
app: ingress-nginx

3.3 Ingress 资源配置

3.3.1 基于 Host 的路由

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-host-based
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80

3.3.2 基于 Path 的路由

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-path-based
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api-service
port:
number: 80
- path: /web(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: web-service
port:
number: 80
- path: /(.*)
pathType: ImplementationSpecific
backend:
service:
name: frontend-service
port:
number: 80

3.3.3 TLS 配置

apiVersion: v1
kind: Secret
metadata:
name: tls-secret
namespace: default
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi... # Base64 编码的证书
tls.key: LS0tLS1CRUdJTi... # Base64 编码的私钥
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-tls
namespace: default
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- secure.example.com
secretName: tls-secret
rules:
- host: secure.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: secure-service
port:
number: 443
TLS 最佳实践
  1. 使用 Let's Encrypt 自动获取和更新证书(推荐使用 cert-manager)
  2. 强制 HTTPS 重定向(如 nginx.ingress.kubernetes.io/ssl-redirect: "true")
  3. 定期轮换证书
  4. 使用强加密套件

3.4 Ingress 路由分发流程

上图展示了 Ingress 的路由分发流程:

  1. 外部客户端发起 HTTP/HTTPS 请求
  2. Ingress Controller 接收请求,根据 Ingress 资源规则进行匹配
  3. 根据 HostPath 将请求路由到对应的 Service
  4. Service 将请求负载均衡到后端的 Pod

3.5 Gateway API:下一代服务路由标准

Gateway API 是 Kubernetes 官方项目,专注于 L4 和 L7 路由,代表了 Kubernetes Ingress、负载均衡和服务网格 API 的下一代演进方向。相比传统的 Ingress API,Gateway API 提供了更强大、更灵活的路由能力。

Gateway API 的核心优势

特性Ingress APIGateway API
角色导向单一角色支持基础设施提供者、集群操作者、应用开发者三种角色
表达能力依赖注解实现高级功能原生支持流量加权、Header 匹配等高级路由
可扩展性有限支持自定义资源扩展
跨命名空间不支持支持共享 Gateway 和跨命名空间路由
协议支持主要 HTTP/HTTPS支持 HTTP、gRPC、TCP、UDP 等多种协议
服务网格集成不支持通过 GAMMA 倡议支持服务网格

Gateway API 的核心资源

  • GatewayClass:定义 Gateway 的实现类型(如 Envoy、Nginx、Contour)
  • Gateway:定义网络入口点,监听端口、TLS 配置等
  • HTTPRoute/GRPCRoute:定义路由规则,支持流量权重、Header 匹配等
Gateway API vs Ingress

Gateway API 不是 Ingress 的简单替代,而是一个更强大、更灵活的 API 框架:

  • Gateway API for Ingress:管理南北向流量(从外部到集群内部)
  • Gateway API for Service Mesh (GAMMA):管理东西向流量(集群内部服务间通信)

Gateway API 已成为 SIG-Network 的官方项目,多个主流实现(如 Envoy Gateway、Kong Gateway、Contour)都已支持。

Gateway API 示例

# GatewayClass:定义 Gateway 的实现类型
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller

---
# Gateway:定义网络入口点
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: eg
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "*.example.com"

---
# HTTPRoute:定义路由规则
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: backend
spec:
parentRefs:
- name: eg
hostnames:
- "api.example.com"
rules:
- backendRefs:
- name: api-service
port: 80
何时使用 Gateway API

Gateway API 适合以下场景:

  • 需要更强大的路由能力(流量加权、Header 匹配、灰度发布)
  • 需要跨命名空间共享 Gateway
  • 需要支持多种协议(HTTP、gRPC、TCP、UDP)
  • 计划使用服务网格(Service Mesh)

对于简单的 HTTP/HTTPS 路由需求,传统的 Ingress API 仍然足够。

四、NetworkPolicy:网络策略与安全隔离

4.1 默认网络策略

Kubernetes 集群默认情况下,所有 Pod 之间都是互通的(全通模式)。这种默认行为在多租户环境中可能带来安全风险。

默认全通模式

# 默认情况下,无需任何 NetworkPolicy,所有 Pod 都可以相互通信

默认全拒模式

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: default
spec:
podSelector: {} # 空选择器匹配所有 Pod
policyTypes:
- Ingress
- Egress
安全建议

在生产环境中,建议采用"默认拒绝,显式允许"的策略。首先创建默认拒绝所有流量的 NetworkPolicy,然后为需要通信的 Pod 创建允许规则。

4.2 ingress/egress 规则

NetworkPolicy 支持 ingress(入站)和 egress(出站)两种规则类型。

4.2.1 仅允许入站流量

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-only
namespace: default
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080

4.2.2 仅允许出站流量

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-only
namespace: default
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432

4.2.3 同时允许入站和出站流量

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-both
namespace: default
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53

4.3 标签选择器与策略匹配

NetworkPolicy 使用标签选择器来匹配 Pod 和 Namespace。

选择器类型用途示例
podSelector匹配同一 Namespace 中的 Podapp: frontend
namespaceSelector匹配 Namespacename: production
podSelector + namespaceSelector匹配特定 Namespace 中的 PodNamespace: team: backend, Pod: app: api

复杂选择器示例

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: complex-selector
namespace: default
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
# 允许同一 Namespace 中标签为 app: frontend 的 Pod
- podSelector:
matchLabels:
app: frontend
# 允许 production Namespace 中所有 Pod
- namespaceSelector:
matchLabels:
env: production
# 允许 monitoring Namespace 中标签为 app: prometheus 的 Pod
- namespaceSelector:
matchLabels:
team: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 8080

4.4 NetworkPolicy 规则匹配示意图

上图展示了 NetworkPolicy 的规则匹配逻辑:

  1. 外部流量被拒绝(未在 ingress 规则中)
  2. frontend Namespace 中的 Pod 可以访问 backend(匹配 app: frontend
  3. production Namespace 中的 Pod 可以访问 backend(匹配 env: production
  4. backend Pods 可以访问 database(匹配 app: database
  5. backend Pods 可以访问 DNS(匹配 DNS 规则)
  6. backend Pods 无法访问互联网(未在 egress 规则中)
NetworkPolicy 依赖

NetworkPolicy 的实现依赖于 CNI 插件。并非所有 CNI 插件都支持 NetworkPolicy:

  • 支持:Calico、Cilium、Weave Net、Romana
  • 不支持:Flannel(需要配合 Canal 使用)

在选择 CNI 插件时,如果需要网络策略功能,请确保插件支持 NetworkPolicy。

五、EndpointSlice 与服务网格简介

5.1 EndpointSlice 机制

EndpointSlice 是 K8s 1.17+ 引入的新特性(v1.21 达到 Stable),用于替代传统的 Endpoints 资源。它将 Pod 端点信息分片存储,解决了大规模集群中 Endpoints 资源过大的问题。

Endpoints API 已废弃(v1.33)

自 Kubernetes v1.33 起,传统的 Endpoints API 已被正式标记为废弃(Deprecated)。相比 EndpointSlice,旧的 Endpoints API 存在以下问题:

  • 不支持双栈集群:无法同时表示 IPv4 和 IPv6 端点
  • 缺少新特性支持:不包含 trafficDistribution 等新功能所需的信息
  • 端点截断:当端点数量超过 1000 时,会截断端点列表,导致部分后端无法接收流量

官方推荐所有用户迁移到 EndpointSlice API。EndpointSlice 由 EndpointSlice 控制器自动管理,无需手动创建。

EndpointSlice 的优势

特性Endpoints(已废弃)EndpointSlice(推荐)
存储方式单个大对象分片存储(每个切片最多 100 个端点)
扩展性受限于单个对象大小(1000 端点截断)支持大规模集群
双栈支持不支持支持 IPv4/IPv6 双栈
拓扑感知不支持支持 Zone、Node 等拓扑信息
性能频繁更新导致性能下降增量更新,性能更好
新特性兼容不支持 trafficDistribution 等完整支持

EndpointSlice 示例

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: nginx-service-abc123
namespace: default
labels:
kubernetes.io/service-name: nginx-service
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 80
endpoints:
- addresses:
- "10.244.1.2"
nodeName: node-1
zone: zone-a
conditions:
ready: true
- addresses:
- "10.244.1.3"
nodeName: node-1
zone: zone-a
conditions:
ready: true
- addresses:
- "10.244.2.2"
nodeName: node-2
zone: zone-b
conditions:
ready: true
自动管理

EndpointSlice 由 EndpointSlice 控制器自动管理,无需手动创建。当创建 Service 时,控制器会自动创建对应的 EndpointSlice。

5.2 Service Mesh 与 K8s 网络的关系

Service Mesh(服务网格)是建立在 K8s 网络之上的应用层网络抽象,它通过在每个 Pod 中注入 Sidecar 代理来实现流量管理、安全、可观测性等功能。

主流 Service Mesh 对比

特性IstioLinkerdConsul Connect
架构控制平面 + 数据平面控制平面 + 数据平面控制平面 + 数据平面
SidecarEnvoyproxy-wasmEnvoy
性能中等中等
功能丰富中等中等
学习曲线陡峭平缓平缓

Service Mesh 与 K8s 网络的关系

上图展示了 Service Mesh 与 K8s 网络的层次关系:

  1. 应用层:应用容器通过 gRPC/HTTP 协议通信
  2. Sidecar 代理:拦截应用流量,实现流量管理、安全、可观测性
  3. K8s 网络:Service 和 NetworkPolicy 提供四层网络能力
  4. CNI 网络:提供 Pod 间通信的三层网络能力
  5. 节点网络:物理或虚拟机的二层网络
何时使用 Service Mesh

Service Mesh 并非必需,适合以下场景:

  • 微服务数量多(> 50 个服务)
  • 需要细粒度的流量控制(灰度发布、A/B 测试)
  • 需要统一的安全策略(mTLS、授权)
  • 需要深度可观测性(分布式追踪、指标)

对于小规模集群或简单应用,K8s 原生的 Service 和 Ingress 已足够。

六、网络故障排查常用命令

6.1 Pod 网络排查

# 1. 检查 Pod 的 IP 地址
kubectl get pod <pod-name> -o wide

# 2. 检查 Pod 的网络配置
kubectl exec -it <pod-name> -- ip addr
kubectl exec -it <pod-name> -- ip route

# 3. 检查 Pod 的 DNS 解析
kubectl exec -it <pod-name> -- nslookup kubernetes.default
kubectl exec -it <pod-name> -- cat /etc/resolv.conf

# 4. 测试 Pod 间连通性
kubectl exec -it <pod-name-1> -- ping <pod-ip-2>
kubectl exec -it <pod-name-1> -- curl http://<service-name>:<port>

# 5. 检查 Pod 的网络策略
kubectl get networkpolicy -n <namespace>
kubectl describe networkpolicy <policy-name> -n <namespace>

6.2 Service 网络排查

# 1. 检查 Service 的详细信息
kubectl get svc <service-name> -o yaml
kubectl describe svc <service-name>

# 2. 检查 Endpoints/EndpointSlice
kubectl get endpoints <service-name>
kubectl get endpointslice -l kubernetes.io/service-name=<service-name>

# 3. 检查 kube-proxy 日志
kubectl logs -n kube-system -l k8s-app=kube-proxy

# 4. 检查 iptables 规则(在 Node 上执行)
sudo iptables -t nat -L KUBE-SERVICES -n
sudo iptables -t nat -L KUBE-SVC-<hash> -n

# 5. 检查 IPVS 规则(如果使用 IPVS 模式)
sudo ipvsadm -Ln

6.3 Ingress 网络排查

# 1. 检查 Ingress 资源
kubectl get ingress
kubectl describe ingress <ingress-name>

# 2. 检查 Ingress Controller
kubectl get pods -n ingress-nginx
kubectl logs -n ingress-nginx <ingress-controller-pod>

# 3. 检查 Ingress Controller 配置
kubectl exec -n ingress-nginx <ingress-controller-pod> -- cat /etc/nginx/nginx.conf

# 4. 测试 Ingress 路由
curl -v http://<host>/path
curl -v -H "Host: <host>" http://<ingress-ip>/path

# 5. 检查 TLS 证书
kubectl get secret <tls-secret-name> -o yaml
openssl x509 -in <cert-file> -text -noout

6.4 CNI 网络排查

# 1. 检查 CNI 插件 Pod
kubectl get pods -n kube-system | grep -E "calico|cilium|flannel|weave"

# 2. 检查 CNI 插件日志
kubectl logs -n kube-system <cni-plugin-pod>

# 3. 检查节点网络配置(在 Node 上执行)
ip addr
ip route
brctl show # 如果使用网桥模式

# 4. 检查 veth pair(在 Node 上执行)
ip link show type veth

# 5. 测试节点间连通性
ping <other-node-ip>
traceroute <other-node-ip>
排查思路

网络故障排查的一般思路:

  1. 从内到外:先检查 Pod 内部网络,再检查 Service,最后检查 Ingress
  2. 从下到上:先检查 CNI 网络层,再检查 K8s 网络层,最后检查应用层
  3. 分层验证:每层网络都要验证连通性、DNS 解析、端口监听
  4. 日志优先:优先查看相关组件的日志,获取错误信息

七、本章小结

本文深入剖析了 Kubernetes 网络模型与服务发现的核心机制,涵盖了以下关键内容:

核心要点回顾

  1. K8s 网络模型

    • 三个基本要求:Pod 互通、Node 与 Pod 互通、Pod IP 一致性
    • CNI 插件机制:Flannel、Calico、Cilium、Weave Net 各有优劣
    • 三层网络架构:节点网络、Pod 网络、服务网络
  2. Service 服务发现

    • 四种类型:ClusterIP、NodePort、LoadBalancer、ExternalName
    • kube-proxy 工作模式:iptables、IPVS、nftables(v1.33 GA)
    • CoreDNS 服务发现机制
    • Headless Service 与 StatefulSet 的配合使用
    • appProtocol 字段(v1.20 Stable):支持 HTTP/2、WebSocket 等应用协议
    • RelaxedServiceNameValidation(v1.34 Alpha):允许 Service 名称以数字开头
  3. Ingress 七层路由

    • Ingress vs Service 的本质区别
    • 主流 Ingress Controller:Nginx、Traefik、APISIX
    • 基于 Host、Path 的路由配置
    • TLS 证书管理
    • Gateway API:下一代服务路由标准,支持角色导向、流量加权、跨命名空间等高级功能
  4. NetworkPolicy 网络安全

    • 默认全通 vs 默认全拒策略
    • ingress/egress 规则配置
    • 标签选择器的灵活使用
  5. 高级特性

    • EndpointSlice 机制解决大规模集群问题(Endpoints API v1.33 已废弃
    • Gateway API 与 Ingress 的演进关系
    • Service Mesh 与 K8s 网络的关系
  6. 故障排查

    • Pod、Service、Ingress、CNI 各层排查命令
    • 分层验证的排查思路

最佳实践建议

  1. CNI 插件选择

    • 中小集群:Flannel(简单易用)
    • 企业级应用:Calico(功能丰富)
    • 高性能场景:Cilium(eBPF)
  2. Service 类型选择

    • 内部服务:ClusterIP
    • 外部访问:Ingress + LoadBalancer
    • 特殊场景:NodePort(开发测试)、ExternalName(外部服务)
  3. 安全策略

    • 默认拒绝,显式允许
    • 最小权限原则
    • 定期审计 NetworkPolicy
  4. 性能优化

    • 优先使用 nftables 模式(v1.33 GA),其次 IPVS 模式
    • 使用 EndpointSlice(替代已废弃的 Endpoints API)
    • 合理规划 Service 数量

下一步学习

  • 深入学习特定 CNI 插件的配置和调优
  • 掌握 Service Mesh(Istio/Linkerd)的部署和使用
  • 学习网络可观测性工具(如 Cilium Hubble)
  • 实践网络故障排查和性能优化
  • 探索 Gateway API 在生产环境中的应用

Kubernetes 网络是一个庞大而复杂的主题,本文涵盖了其核心概念和最佳实践。在实际应用中,建议根据具体的业务需求和集群规模,选择合适的网络方案,并持续监控和优化网络性能。


相关阅读

作者简介:本文作者专注于云原生技术,拥有丰富的 Kubernetes 实践经验,致力于分享云原生技术最佳实践。

Kubernetes 全景解析 (1):工作负载与 Pod 生命周期深度解析

· 阅读需 21 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"Pods are the atomic unit of scheduling in Kubernetes — not containers."

在 Kubernetes 的世界里,工作负载 (Workload) 是你与应用交互的核心抽象。无论你是部署一个无状态的 Web 服务、一个有状态的数据库,还是在每个节点上运行监控 Agent,K8s 都提供了专门的工作负载资源来满足需求。而所有这些工作负载的基础,都建立在 Pod 之上。

本文将从 Pod 的本质出发,逐层深入解析 Kubernetes 中五大核心工作负载资源的设计理念、编排策略与最佳实践,帮助你在架构选型时做出正确的决策。


一、Pod:K8s 的原子调度单元

1.1 为什么 Pod 是最小部署单元而非容器

许多初学者会困惑:既然 Docker 已经有了容器概念,为什么 Kubernetes 还要引入 Pod?答案在于 "超亲密容器"(Hyper-privileged Containers) 的设计哲学。

在现实世界中,一个应用往往不是孤立运行的。例如:

  • 一个 Web 服务器需要配合一个日志采集 Sidecar
  • 一个主进程需要配合一个健康检查辅助进程
  • 一个数据管道需要同时运行 ingest 和 transform 两个紧密协作的进程

这些进程需要共享网络命名空间(可以通过 localhost 互相通信)、共享存储卷(可以读写同一份数据),并且需要作为一个原子单元被调度到同一个节点上。Pod 正是为了解决这一需求而诞生的。

核心原则

Pod 是 Kubernetes 中最小的可调度单元。一个 Pod 可以包含一个或多个容器,这些容器共享相同的网络和存储命名空间,始终被调度到同一个节点上,并作为一个整体进行生命周期管理。

1.2 Pod 的设计理念

Pod 的设计围绕三个核心能力展开:

能力说明典型场景
共享网络同一 Pod 内的所有容器共享同一个 IP 地址和端口空间,可以通过 localhost 互相访问主容器 + Sidecar 代理
共享存储Pod 可以声明多个 Volume,这些 Volume 可以被 Pod 内的任意容器挂载主容器写日志,Sidecar 读取并转发
原子调度Pod 内的所有容器作为一个整体被调度到同一个节点保证本地通信的低延迟

Sidecar 模式 是 Pod 多容器设计中最经典的模式。例如,Istio 服务网格通过在每个 Pod 中注入一个 Envoy Sidecar 代理来实现流量管理、安全通信和可观测性,而无需修改应用代码。

1.3 Pod 的 YAML 结构详解

下面是一个完整的 Pod YAML 示例,每个字段都附有详细注释:

apiVersion: v1 # API 版本,Pod 属于核心 v1 组
kind: Pod # 资源类型
metadata:
name: nginx-pod # Pod 名称,在同一个 Namespace 内必须唯一
namespace: default # 命名空间,不指定则使用 default
labels: # 标签,用于选择器和组织资源
app: nginx
tier: frontend
annotations: # 注解,用于存储非标识性的元数据
description: "A sample nginx pod"
spec:
# --- 重启策略 ---
restartPolicy: Always # 容器退出后的重启策略:Always / OnFailure / Never

# --- 节点选择 ---
nodeSelector: # 通过标签选择节点
disktype: ssd
tolerations: # 容忍度,用于调度到有特定 Taint 的节点
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"

# --- 容器定义 ---
containers:
- name: nginx # 容器名称
image: nginx:1.27 # 镜像地址(含标签)
imagePullPolicy: IfNotPresent # 镜像拉取策略:Always / IfNotPresent / Never
ports:
- containerPort: 80 # 容器暴露的端口(仅声明,不自动发布)
protocol: TCP
resources: # 资源请求与限制
requests: # 调度时保证的最小资源量
cpu: "100m" # 100 millicores = 0.1 核
memory: "128Mi"
limits: # 容器可使用的最大资源量
cpu: "500m"
memory: "256Mi"
env: # 环境变量
- name: ENVIRONMENT
value: "production"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
volumeMounts: # 挂载存储卷
- name: nginx-data
mountPath: /usr/share/nginx/html
livenessProbe: # 存活探针
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe: # 就绪探针
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 5
startupProbe: # 启动探针(K8s 1.18+)
httpGet:
path: /startup
port: 80
failureThreshold: 30 # 最多失败 30 次(即最多等待 300 秒)

# --- Init 容器 ---
initContainers:
- name: init-db
image: busybox:1.36
command: ['sh', '-c', 'until nslookup db-service; do echo waiting for db; sleep 2; done']

# --- 存储卷 ---
volumes:
- name: nginx-data
persistentVolumeClaim:
claimName: nginx-pvc # 引用 PVC
- name: config-volume
configMap:
name: nginx-config # 引用 ConfigMap

# --- DNS 配置 ---
dnsPolicy: ClusterFirst # DNS 策略:ClusterFirst / Default / ClusterFirstWithHostNet / None
最佳实践
  1. 始终设置 resources.requestsresources.limits,避免资源争抢导致节点不稳定。
  2. 使用 imagePullPolicy: IfNotPresent 而非 Always(除非使用 :latest 标签),以减少镜像拉取延迟。
  3. 为生产环境的镜像使用明确的 digest(如 nginx@sha256:abc123...),确保部署的可重复性。

1.4 Pod 生命周期

Pod 从创建到终止会经历一系列状态变化。理解这些状态对于排查问题至关重要。

各状态说明:

状态含义
PendingPod 已被 Kubernetes 集群接受,但一个或多个容器尚未创建并就绪。包括等待调度和下载镜像的时间。
RunningPod 已绑定到节点,所有容器已创建,至少一个容器仍在运行,或正在启动/重启中。
SucceededPod 中的所有容器已成功终止,且不会重启。
FailedPod 中的所有容器已终止,且至少一个容器以失败状态退出(非零退出码或被系统终止)。
Unknown由于某种原因无法获取 Pod 状态,通常是与节点通信失败。
CrashLoopBackOff 和 Terminating 不是 Pod Phase

CrashLoopBackOffTerminating 可能会出现在 kubectl 命令的 Status 输出中,但它们不是 Pod 的 phase 值。Pod phase 是 Kubernetes 数据模型中的显式字段,只有五个值:PendingRunningSucceededFailedUnknown

  • CrashLoopBackOff:容器反复崩溃退出,K8s 在每次重启之间增加指数退避等待时间(10s → 20s → 40s → ...,上限 300s)。
  • Terminating:Pod 正在被删除,处于优雅终止过程中(默认 30 秒宽限期)。

参考文档:Pod Lifecycle - Pod phase

1.5 Probe 机制:Liveness / Readiness / Startup

Kubernetes 通过三种探针来监控容器的健康状态:

探针类型作用失败后果
Liveness Probe检测容器是否活着重启容器
Readiness Probe检测容器是否就绪(能否接收流量)从 Service Endpoints 中移除
Startup Probe检测容器是否启动完成在启动期间禁用其他探针

探针支持四种检测方式:

  • httpGet:向容器发送 HTTP GET 请求,2xx/3xx 状态码视为成功。
  • tcpSocket:尝试与容器的指定端口建立 TCP 连接。
  • exec:在容器内执行命令,返回码为 0 视为成功。
  • grpc(v1.24+ 稳定):使用 gRPC 健康检查协议,服务状态为 SERVING 视为成功。
注意事项
  • 不要省略探针。没有探针的 Pod 在容器进程僵死(如死锁)时无法被自动恢复。
  • Startup Probe 是慢启动应用的救星。如果你的应用启动需要较长时间(如 Java 应用加载类库),务必配置 Startup Probe,否则 Liveness Probe 可能在应用尚未就绪时就杀死容器。
  • Readiness Probe 不应过于严格,否则可能导致滚动更新时出现流量中断。

参考文档:Configure Liveness, Readiness and Startup Probes


二、Deployment:无状态应用的编排之王

Deployment 是 Kubernetes 中最常用的工作负载资源,专门用于管理无状态应用。它提供了声明式更新、滚动发布和回滚等生产级特性。

2.1 ReplicaSet 与 Deployment 的关系

Deployment → 管理 → ReplicaSet → 管理 → Pod

ReplicaSet (RS) 是下一层的控制器,负责确保指定数量的 Pod 副本始终在运行。而 Deployment 是更高层的抽象,它在 ReplicaSet 之上增加了版本管理滚动更新能力。

实践建议

在日常使用中,几乎不需要直接操作 ReplicaSet。你应该始终通过 Deployment 来管理应用,让 K8s 自动处理 ReplicaSet 的创建和清理。

2.2 滚动更新(Rolling Update)策略

Deployment 支持两种更新策略:

策略说明适用场景
RollingUpdate(默认)逐步替换旧 Pod 为新 Pod,保证零停机生产环境
Recreate先删除所有旧 Pod,再创建新 Pod不兼容多版本共存的场景

RollingUpdate 通过两个关键参数控制更新节奏:

  • maxSurge:更新期间允许超出期望副本数的最大 Pod 数(可以是绝对数或百分比)。
  • maxUnavailable:更新期间允许不可用的最大 Pod 数(可以是绝对数或百分比)。
默认值

Deployment 默认的滚动更新策略为:maxSurge: 25%maxUnavailable: 25%。这意味着在更新期间,可用副本数至少为 75%,最多为 125%。

参考文档:Deployments - Updating a Deployment

2.3 回滚机制(Rollback)

Deployment 的每一次更新都会创建一个新的 ReplicaSet,并保留历史版本(由 revisionHistoryLimit 控制,默认为 10)。这使得回滚操作变得极其简单:

# 查看部署历史
kubectl rollout history deployment/nginx-deployment

# 回滚到上一个版本
kubectl rollout undo deployment/nginx-deployment

# 回滚到指定版本
kubectl rollout undo deployment/nginx-deployment --to-revision=2

2.4 完整 YAML 示例

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
labels:
app: nginx
spec:
replicas: 3 # 期望的 Pod 副本数
revisionHistoryLimit: 10 # 保留的历史 ReplicaSet 数量
strategy:
type: RollingUpdate # 更新策略:RollingUpdate / Recreate
rollingUpdate:
maxSurge: 1 # 滚动更新时最多多创建 1 个 Pod
maxUnavailable: 0 # 滚动更新时最多允许 0 个 Pod 不可用
selector:
matchLabels: # 必须与 Pod template 的 labels 匹配
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
生产环境建议
  • 设置 maxUnavailable: 0 可以确保滚动更新过程中服务容量不会下降,代价是更新期间需要更多资源(maxSurge 需要大于 0)。
  • 配合 Pod Disruption Budget (PDB) 使用,可以在节点维护时确保最少可用副本数。

三、StatefulSet:有状态应用的首选

StatefulSet 专为需要稳定网络标识持久存储的有状态应用设计,如数据库(MySQL、PostgreSQL)、消息队列(Kafka、RabbitMQ)和分布式存储系统(Elasticsearch、etcd)。

3.1 与 Deployment 的核心区别

特性DeploymentStatefulSet
Pod 标识随机生成的名称(如 nginx-7b9f...固定的序号名称(如 mysql-0, mysql-1
网络标识Pod IP 随重建而变化每个 Pod 有稳定的 DNS 名称
存储所有 Pod 共享相同的 PVC每个 Pod 有独立的 PVC
部署顺序并行创建按序号顺序创建(0 → 1 → 2 → ...)
删除顺序并行删除按序号逆序删除(... → 2 → 1 → 0)
扩缩容随机选择 Pod 删除/创建严格按序号操作

3.2 有序部署/扩展/删除

StatefulSet 的核心特性之一是有序性 (Ordinality)。当创建或扩展 StatefulSet 时,Pod 会按照序号从 0 开始依次创建,且只有前一个 Pod 进入 Running 且 Ready 状态后,才会创建下一个 Pod。

3.3 稳定的网络标识和持久存储

StatefulSet 为每个 Pod 提供以下稳定性保证:

  • 稳定的网络标识:Pod 名称格式为 <statefulset-name>-<ordinal>(如 mysql-0),且关联的 Headless Service 会为每个 Pod 创建一个稳定的 DNS 记录:<pod-name>.<headless-service>.<namespace>.svc.cluster.local
  • 持久存储:通过 volumeClaimTemplates,StatefulSet 会为每个 Pod 自动创建独立的 PVC。即使 Pod 被重新调度到其他节点,只要绑定了相同的 PVC,数据就不会丢失。

3.4 完整 YAML 示例

apiVersion: v1
kind: Service # Headless Service,用于稳定的网络标识
metadata:
name: mysql
labels:
app: mysql
spec:
clusterIP: None # Headless Service:不分配 ClusterIP
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # 必须指向关联的 Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
volumeMounts:
- name: data
mountPath: /var/lib/mysql
livenessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "localhost"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["mysql", "-h", "localhost", "-e", "SELECT 1"]
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates: # 为每个 Pod 自动创建独立的 PVC
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 10Gi
注意事项
  • StatefulSet 不会自动创建 Headless Service,你需要手动创建。
  • 删除 StatefulSet 时,默认不会删除关联的 PVC,以防止数据丢失。如需同时删除,需要手动清理。
  • StatefulSet 的滚动更新默认使用 OnDelete 策略(即手动删除 Pod 后才会重建),如需自动滚动更新,需设置 .spec.updateStrategy.type: RollingUpdate

参考文档:StatefulSets


四、DaemonSet:节点级守护进程

DaemonSet 确保集群中的每个(或特定)节点上都运行一个 Pod 副本。当节点加入集群时,DaemonSet 会自动为其创建 Pod;当节点移除时,这些 Pod 也会被自动回收。

4.1 典型使用场景

场景示例
日志收集Fluentd、Filebeat、Promtail
监控 AgentPrometheus Node Exporter、Datadog Agent
网络插件Calico、Cilium、Flannel
存储守护进程Ceph、GlusterFS 客户端
安全合规Falco(运行时安全检测)、Twistlock

4.2 滚动更新策略

DaemonSet 支持三种更新策略:

策略说明
RollingUpdate(默认)逐个节点更新 Pod,可通过 maxUnavailable 控制并发度
OnDelete手动删除旧 Pod 后才会创建新 Pod
Surging(K8s 1.22+)先创建新 Pod 再删除旧 Pod,节点上会短暂运行两个 Pod

4.3 完整 YAML 示例

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
labels:
app: node-exporter
spec:
selector:
matchLabels:
app: node-exporter
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 最多允许 1 个节点上的 Pod 不可用
template:
metadata:
labels:
app: node-exporter
spec:
hostNetwork: true # 使用宿主机网络(监控场景常见)
hostPID: true # 使用宿主机 PID 命名空间
tolerations: # 容忍所有 Taint,确保在所有节点上运行
- operator: Exists
containers:
- name: node-exporter
image: prom/node-exporter:v1.8.0
args:
- "--web.listen-address=:9100"
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"
ports:
- containerPort: 9100
hostPort: 9100
volumeMounts:
- name: proc
mountPath: /host/proc
readOnly: true
- name: sys
mountPath: /host/sys
readOnly: true
resources:
limits:
cpu: 200m
memory: 100Mi
volumes:
- name: proc
hostPath:
path: /proc
- name: sys
hostPath:
path: /sys
最佳实践
  • DaemonSet 通常需要使用 hostNetwork: truehostPID: truehostPath 卷来访问节点级别的资源。
  • 始终配置 tolerations 以确保 DaemonSet Pod 可以被调度到带有 Taint 的节点(如 Master 节点)。
  • 为 DaemonSet Pod 设置严格的资源限制,避免它们占用过多节点资源影响业务应用。

五、Job 与 CronJob:任务编排

5.1 一次性任务(Job)

Job 用于运行一次性任务,确保 Pod 成功执行完毕后终止。Job 会持续跟踪 Pod 的完成状态,并在失败时根据重试策略重新创建 Pod。

核心字段说明:

字段说明默认值
completions需要成功完成的 Pod 数1
parallelism并行运行的 Pod 数1
backoffLimit最大重试次数6
activeDeadlineSecondsJob 超时时间(秒),超时后标记为失败无限制
ttlSecondsAfterFinishedJob 完成后的自动清理时间(K8s 1.23+)永不清理

5.2 并行任务(Parallelism + Completions)

Job 的 parallelismcompletions 字段可以组合出不同的执行模式:

parallelismcompletions模式
11单次顺序执行
N1N 个 Pod 并行竞争,任一成功即完成
NNN 个 Pod 并行工作队列模式
1N顺序执行 N 个任务

5.3 定时任务(CronJob)

CronJob 基于 Cron 表达式来定期创建 Job。它的调度规则与 Linux 的 crontab 基本一致,格式为:

# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 日 (1 - 31)
# │ │ │ ┌───────────── 月 (1 - 12)
# │ │ │ │ ┌───────────── 星期 (0 - 6, 0 = 周日)
# │ │ │ │ │
# * * * * *
CronJob 时区

从 Kubernetes v1.25 起,CronJob 支持 timeZone 字段,可以指定 Cron 表达式所使用的时区(IANA 时区格式,如 "Asia/Shanghai")。未指定时默认使用 UTC 时区。

参考文档:CronJobs

apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
spec:
schedule: "0 2 * * *" # 每天凌晨 2 点执行
concurrencyPolicy: Forbid # 禁止并发运行
successfulJobsHistoryLimit: 3 # 保留最近 3 个成功的 Job
failedJobsHistoryLimit: 1 # 保留最近 1 个失败的 Job
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 3600 # 超过 1 小时则标记为失败
template:
spec:
containers:
- name: backup
image: postgres:16
command:
- /bin/bash
- -c
- pg_dump -h db-service -U $DB_USER -d $DB_NAME > /backup/$(date +%Y%m%d).sql
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-secret
key: username
- name: DB_NAME
value: "myapp"
volumeMounts:
- name: backup-data
mountPath: /backup
restartPolicy: Never # Job 必须设置为 Never 或 OnFailure
volumes:
- name: backup-data
persistentVolumeClaim:
claimName: backup-pvc
重要提醒
  • Job 的 Pod restartPolicy 必须设置为 NeverOnFailure,不能是 Always
  • concurrencyPolicy: Forbid 确保上一次任务尚未完成时不会启动新任务,对于数据库备份等场景至关重要。
  • 务必设置 successfulJobsHistoryLimitfailedJobsHistoryLimit,避免历史 Job 堆积消耗 etcd 存储空间。

5.4 Job 完整 YAML 示例

apiVersion: batch/v1
kind: Job
metadata:
name: data-migration
spec:
completions: 1 # 需要成功完成 1 次
parallelism: 1 # 同时运行 1 个 Pod
backoffLimit: 3 # 最多重试 3 次
activeDeadlineSeconds: 600 # 超时 10 分钟
ttlSecondsAfterFinished: 86400 # 完成后 24 小时自动清理
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: myapp:v2.0
command: ["python", "manage.py", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url

六、ReplicaSet / ReplicationController(简述)

ReplicationController (RC)

ReplicationController 是 Kubernetes 最早的副本管理机制,用于确保指定数量的 Pod 副本始终运行。它已被 ReplicaSet 取代,目前仅存在于 API 中以保持向后兼容。

ReplicaSet (RS)

ReplicaSet 是 ReplicationController 的升级版,主要改进是支持基于集合的标签选择器(Set-based Selector),使得选择逻辑更加灵活。

特性ReplicationControllerReplicaSet
标签选择器仅支持等值匹配(environment=production支持集合匹配(environment in (production, staging)
推荐使用已废弃作为 Deployment 的底层实现,不直接使用
实践建议

不要直接创建 ReplicaSet。使用 Deployment 来管理无状态应用,Deployment 会在底层自动创建和管理 ReplicaSet。只有在需要执行非常特殊的操作(如手动管理 Pod 副本)时,才考虑直接使用 ReplicaSet。


七、工作负载选择决策树

面对不同的业务需求,如何选择合适的工作负载资源?以下决策树可以帮助你快速做出判断:

快速参考表

工作负载核心特征典型场景Pod 数量
Deployment无状态、可滚动更新、可回滚Web 服务、API 服务、微服务可变(replicas)
StatefulSet有状态、稳定标识、有序部署数据库、消息队列、分布式存储可变(replicas)
DaemonSet每节点一个 Pod日志收集、监控 Agent、网络插件= 节点数
Job一次性任务,完成后终止数据迁移、批处理、CI/CD固定(completions)
CronJob定时创建 Job数据库备份、报表生成、清理任务每次执行创建新 Job

八、本章小结

本文从 Pod 的本质出发,系统地解析了 Kubernetes 中五大核心工作负载资源:

  1. Pod 是 Kubernetes 的原子调度单元,通过共享网络和存储实现了"超亲密容器"的协作模式。理解 Pod 的生命周期和探针机制是排查问题的基础。

  2. Deployment 是无状态应用的首选,通过 ReplicaSet 实现版本管理,支持零停机的滚动更新和一键回滚。

  3. StatefulSet 为有状态应用提供了稳定的网络标识、独立的持久存储和有序的部署/删除策略,是运行数据库和消息队列等场景的最佳选择。

  4. DaemonSet 确保每个节点运行一个 Pod 副本,是部署基础设施组件(日志、监控、网络插件)的标准方式。

  5. Job 与 CronJob 覆盖了一次性任务和定时任务的需求,支持灵活的并行度和重试策略。

选择工作负载资源时,核心判断依据是:应用是否有状态是否需要长期运行是否需要在每个节点上运行。掌握这些工作负载的特性与适用场景,是构建可靠 Kubernetes 应用的基石。

在下一篇文章中,我们将深入探讨 Kubernetes 的 Service 与网络模型,解析 ClusterIP、NodePort、LoadBalancer 和 Ingress 的工作原理与选型策略。


参考文档

本文内容基于 Kubernetes 官方文档校验,以下为核心参考链接:

Kubernetes 全景解析 (0):架构设计与核心概念

· 阅读需 20 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"如果你觉得 Kubernetes 太复杂,那是因为它解决的问题本身就很复杂。"

在云原生(Cloud Native)的世界里,Kubernetes(简称 K8s)已经从"可选技能"变成了"基础设施常识"。无论你是后端工程师、DevOps 工程师,还是架构师,理解 K8s 的设计思想和运行机制,都是构建现代分布式系统的必修课。

然而,K8s 的学习曲线之陡峭也是出了名的。官方文档动辄数千页,概念繁多且相互关联,很容易让人陷入"见树木不见森林"的困境。

本系列文章将从架构设计出发,逐层深入到核心概念、工作负载管理、网络模型、存储体系、调度策略与安全机制,帮助你构建一套完整、系统的 K8s 认知框架。本文作为系列的第零章,将聚焦于最根本的问题:Kubernetes 是什么?它是如何设计的?它由哪些核心部分组成?


一、为什么需要 Kubernetes

1.1 容器化演进的必然之路

要理解 K8s 存在的意义,我们需要先回顾应用部署方式的演进历程:

阶段部署方式隔离性资源利用率启动速度运维复杂度
物理机时代应用直接部署在物理服务器上-
虚拟机时代Hypervisor 划分多个 VM分钟级
容器时代Docker 等容器引擎秒级
编排时代Kubernetes 等编排系统秒级低(自动化)

物理机时代,一个应用独占一台服务器,资源浪费严重。为了提高利用率,我们开始在同一个物理机上部署多个应用,但随之而来的是依赖冲突、端口争抢、一个应用崩溃拖垮整台机器等问题。

虚拟机时代,Hypervisor(如 VMware、KVM)在物理机上虚拟出多个独立的操作系统实例,实现了良好的隔离。但虚拟机本身就很重——一个 VM 动辄几个 GB,启动需要数分钟,而且携带了完整的 Guest OS,大量资源被浪费在运行重复的系统服务上。

容器时代,Docker 横空出世。容器共享宿主机的内核,不需要 Guest OS,一个镜像通常只有几十 MB,启动只需毫秒级。容器通过 Namespace 实现视图隔离,通过 Cgroups 实现资源限制,在轻量和隔离之间找到了一个绝佳的平衡点。

但容器解决的是**"如何打包和运行应用"的问题,却没有解决"如何管理成百上千个容器"**的问题。当你的应用从 3 个容器扩展到 3000 个,跨越几十台机器时,以下问题接踵而至:

  • 哪个容器应该运行在哪台机器上?
  • 容器挂了谁来重启?机器挂了谁来迁移?
  • 如何实现滚动更新而不中断服务?
  • 如何让前端服务发现后端服务的地址?
  • 如何让外部流量均匀地分发到多个实例?

这就是 Kubernetes 登场的时刻。

1.2 K8s 解决的核心问题

Kubernetes 作为一个容器编排平台(Container Orchestration Platform),本质上解决的是一个大规模自动化管理的问题。它将运维人员从手工操作中解放出来,通过声明式配置和自动化控制循环,实现了:

  • 自动部署与回滚:声明期望状态,K8s 负责将实际状态趋近期望状态
  • 服务发现与负载均衡:内置 DNS 和 Service 机制,无需外部注册中心
  • 自动扩缩容:根据 CPU/内存/自定义指标自动调整实例数量
  • 自我修复:容器崩溃自动重启,节点故障自动迁移
  • 滚动更新与蓝绿部署:零停机更新应用

1.3 K8s 的设计哲学

理解 K8s 的设计哲学,比记住一百个命令更有价值。K8s 的三个核心设计理念贯穿了它的每一个组件:

声明式(Declarative)而非命令式(Imperative)

命令式 vs 声明式
  • 命令式:你告诉系统"做什么"——docker run nginxkubectl scale deployment nginx --replicas=3
  • 声明式:你告诉系统"你要什么"——提交一个 YAML 文件描述"我需要 3 个 Nginx 实例",K8s 会持续工作直到实际状态与期望状态一致

声明式的好处是:可重复、可审计、可版本化。同一个 YAML 文件,无论执行多少次,结果都是一致的。

不可变基础设施(Immutable Infrastructure)

K8s 中的容器镜像一旦构建就不应该被修改。需要更新时,你应该构建新镜像、创建新版本,而不是 SSH 进容器里改配置。这与传统"登录服务器打补丁"的运维方式截然不同。

最终一致性(Eventual Consistency)

K8s 不保证你的请求立即生效,但保证系统最终会收敛到期望状态。这种设计牺牲了即时性,换取了极高的可靠性和容错能力。即使你同时提交了多个冲突的变更,系统也能通过控制循环最终达到一个一致的状态。


二、K8s 整体架构

Kubernetes 采用经典的主从架构(Master-Worker Architecture),分为**控制面(Control Plane)数据面(Data Plane)**两个层级。理解这两个层面的职责划分,是理解 K8s 一切行为的基础。

2.1 架构全景图

2.2 控制面组件详解

控制面是 K8s 集群的"大脑",负责全局决策和集群状态的维护。在生产环境中,为了保证高可用,控制面组件通常部署在多个独立的 Master 节点上。

kube-apiserver:集群的统一入口

kube-apiserver 是整个 K8s 系统的唯一入口。所有组件——无论是内部的 Scheduler、Controller,还是外部的 kubectl、CI/CD Pipeline——都通过 API Server 进行通信。

为什么所有通信都要经过 API Server?

这种设计被称为 "Hub-and-Spoke" 模式。它的好处是:

  1. 统一鉴权:所有请求都在一个地方进行认证(Authentication)、授权(Authorization)和准入控制(Admission Control)
  2. 解耦:各组件不需要知道彼此的存在,只需要与 API Server 交互
  3. 审计:所有操作都有统一的日志记录

API Server 本身是无状态的,它可以水平扩展。所有的状态数据都存储在 etcd 中。

etcd:集群的"真相之源"

etcd 是一个分布式的、一致的键值存储系统,基于 Raft 共识算法实现。它是 K8s 集群的唯一数据源(Single Source of Truth)——集群中所有的一切:节点信息、Pod 状态、配置数据、Secret……全部存储在 etcd 中。

etcd 是 K8s 的命脉
  • etcd 的性能直接决定了整个集群的响应速度
  • etcd 的数据丢失意味着集群状态的丢失(虽然 Pod 可以重建,但某些运行时状态无法恢复)
  • 生产环境建议部署 3 或 5 个 etcd 节点(奇数个,满足 Raft 多数派要求)
  • 必须定期备份 etcd 数据

kube-scheduler:调度决策者

当你创建一个 Pod 时,API Server 只是将这个 Pod 的信息记录到了 etcd 中——此时 Pod 还处于 Pending 状态,因为它还没有被分配到任何节点上。Scheduler 的职责就是为每个未调度的 Pod 选择一个最合适的节点。

调度过程分为三个阶段:

  1. 过滤(Filtering):排除不满足条件的节点(资源不足、端口冲突、污点容忍不匹配等)
  2. 打分(Scoring):对剩余节点进行优先级打分(亲和性、镜像本地性、负载均衡等),选择得分最高的节点
  3. 绑定(Binding):将调度决策应用到集群,将 Pod 与选定节点进行绑定
调度框架(Scheduling Framework)

从 v1.19 起,Kubernetes 调度器采用插件化架构(Scheduling Framework),上述三个阶段对应调度框架中的核心扩展点。整个调度过程分为调度周期(Scheduling Cycle)绑定周期(Binding Cycle),调度周期串行执行,绑定周期可并发执行。

参考文档:Scheduling Framework

kube-controller-manager:状态守护者

Controller Manager 运行着多个控制器(Controller),每个控制器都是一个独立的控制循环(Reconciliation Loop),负责监控集群的某一部分状态,并持续将其推向期望状态。

常见的内置控制器包括:

控制器职责
Deployment Controller确保 Deployment 管理的 Pod 副本数符合期望
ReplicaSet Controller确保 ReplicaSet 管理的 Pod 副本数符合期望
Node Controller监控节点健康状态,节点失联时触发 Pod 驱逐
Service Account Controller为命名空间创建默认 ServiceAccount
EndpointSlice Controller维护 Service 与 Pod 的映射关系(推荐使用 EndpointSlice 替代已废弃的 Endpoints)
控制器的本质:控制循环

每个控制器的工作模式都是相同的:

while true:
实际状态 = 从 API Server 获取当前状态
期望状态 = 从配置中获取期望状态
if 实际状态 != 期望状态:
执行调谐操作(创建/删除/更新)
sleep(一段时间)

这就是 K8s 所谓的**"调谐(Reconciliation)"**机制。

cloud-controller-manager:云平台桥梁

如果你在 AWS、GCP、Azure 等云平台上运行 K8s,CCM 负责与云厂商的 API 交互,管理云平台特有的资源:

  • Node Controller:调用云 API 查询节点地址和状态
  • Route Controller:配置云平台的路由规则
  • Service Controller:创建云平台的负载均衡器(如 AWS ELB)

CCM 的引入使得 K8s 的核心代码不需要耦合任何特定云厂商的 SDK,实现了云平台无关性。

2.3 数据面组件详解

数据面(也叫 Worker Node)是实际运行应用工作负载的地方。每个 Worker 节点上运行着三个核心组件:

kubelet:节点上的"车间主任"

kubelet 是运行在每个 Worker 节点上的代理,它的职责是:

  1. 接收指令:从 API Server 获取分配到本节点的 Pod 规格(Spec)
  2. 管理容器:通过 CRI(Container Runtime Interface)调用容器运行时,创建和管理 Pod 中的容器
  3. 健康检查:定期执行 Liveness Probe、Readiness Probe 和 Startup Probe
  4. 状态上报:将节点和 Pod 的状态信息上报给 API Server

你可以把 kubelet 理解为一个"车间主任"——它不制定生产计划(那是 Scheduler 的事),但负责确保分配到自己车间的生产任务被正确执行。

kube-proxy:网络规则的维护者

kube-proxy 运行在每个节点上,负责维护节点的网络规则,实现 Service 的负载均衡和网络代理。它通过操作 iptables 或 IPVS 规则,将访问 Service 的流量转发到后端的 Pod。

简单来说,当你访问一个 Service 的 ClusterIP 时,kube-proxy 维护的规则会将流量自动转发到该 Service 关联的某个 Pod 上。

kube-proxy 是可选组件(v1.34+)

根据 Kubernetes 官方文档,kube-proxy 是一个可选组件。如果你使用的网络插件(CNI)自身实现了 Service 的数据包转发功能,并提供与 kube-proxy 等效的行为,那么你不需要在集群节点上运行 kube-proxy

例如,Cilium 等现代 CNI 插件可以直接替代 kube-proxy 的功能,通过 eBPF 实现更高效的 Service 负载均衡和网络代理。

参考文档:Kubernetes Architecture - kube-proxy (optional)

Container Runtime:容器的实际执行者

kubelet 本身并不直接运行容器,而是通过 CRI(Container Runtime Interface) 标准接口调用容器运行时。常见的 CRI 兼容运行时包括:

  • containerd:目前最主流的选择,Docker 的核心运行时组件独立出来的版本
  • CRI-O:专为 Kubernetes 设计的轻量级运行时,由 Red Hat 主导
  • Kata Containers:提供虚拟机级别的隔离,适用于安全要求极高的场景
Docker 与 K8s 的"分手"

在 K8s 1.20 之前的版本中,kubelet 通过一个名为 dockershim 的内置组件直接与 Docker 通信。但从 K8s 1.24 开始,dockershim 被正式移除。这并不意味着你不能在 K8s 中运行 Docker 镜像——Docker 镜像遵循 OCI 标准,containerd 和 CRI-O 都能运行它们。移除的只是对 Docker daemon 的直接依赖。


三、核心概念模型

Kubernetes 中的一切都被抽象为 API 对象(API Object)。理解这些对象及其关系,是使用 K8s 的基础。

3.1 API 对象与声明式配置

在 K8s 中,你不需要告诉系统"如何做",而是描述"你要什么"。你通过提交 YAML(或 JSON)文件来定义 API 对象,API Server 会将你的声明持久化到 etcd,然后各个控制器会持续工作,确保实际状态趋近于你声明的期望状态。

3.2 YAML 配置结构解析

每个 K8s API 对象的 YAML 配置都遵循以下结构:

apiVersion: apps/v1 # API 版本,标识对象属于哪个 API 组
kind: Deployment # 对象类型,K8s 内置了几十种对象类型
metadata: # 对象的元数据(名称、标签、注解等)
name: nginx-deployment
namespace: default
labels:
app: nginx
tier: frontend
spec: # 对象的规格(期望状态)
replicas: 3 # 期望运行 3 个 Pod 副本
selector: # 选择器,用于关联管理的 Pod
matchLabels:
app: nginx
template: # Pod 模板,定义如何创建 Pod
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
YAML 配置的四个必填字段
  1. apiVersion:指定 API 版本。v1 是核心 API 组,apps/v1networking.k8s.io/v1 等是扩展 API 组
  2. kind:对象类型,如 Pod、Deployment、Service、ConfigMap 等
  3. metadata:对象的"身份证",至少包含 name,通常还包含 labelsannotations
  4. spec:对象的"期望状态",不同类型的对象有不同的 spec 结构

3.3 API 对象层级关系

K8s 的 API 对象之间存在着清晰的层级关系。上层对象管理下层对象,形成了一个从粗粒度到细粒度的管理链。

Pod:K8s 的最小调度单元

初学者常有一个疑问:为什么 K8s 的最小单位是 Pod 而不是 Container?

Pod 是一个逻辑概念,它可以包含一个或多个紧密耦合的容器。这些容器共享:

  • 网络命名空间:同一个 Pod 中的容器可以通过 localhost 互相访问
  • 存储卷:可以挂载共享的 Volume
  • 运行约束:总是被调度到同一个节点上

一个典型的多容器 Pod 场景是"Sidecar 模式":主容器运行应用逻辑,Sidecar 容器负责日志收集、监控数据导出或代理转发。


四、一次请求的完整旅程

理论讲了不少,现在让我们通过一个具体的场景来串联所有知识:当你执行 kubectl apply -f nginx-deployment.yaml 时,K8s 内部到底发生了什么?

4.1 完整时序图

4.2 控制循环(Reconciliation Loop)详解

时序图展示了一次性的创建流程,但 K8s 的真正威力在于其持续运行的控制循环。让我们深入理解这个机制:

第一步:感知变化(Watch 机制)

K8s 的各个组件并不是定期轮询 API Server,而是通过基于 HTTP 长连接的 Watch 机制 来感知变化。当你创建、修改或删除一个对象时,API Server 会通过 Watch 通道将事件推送给所有订阅者。事件类型包括:

  • ADDED:新对象被创建
  • MODIFIED:对象被更新
  • DELETED:对象被删除

这种基于事件驱动的设计,比轮询更高效,延迟更低。

第二步:计算差异(Diff)

控制器收到事件后,会将实际状态期望状态进行对比。例如,Deployment Controller 发现期望的副本数是 3,但当前只有 2 个 Pod 在运行。

第三步:执行调谐(Reconcile)

控制器计算出需要执行的操作后,会通过 API Server 发起调谐请求。在上面的例子中,Deployment Controller 会创建一个新的 ReplicaSet,ReplicaSet Controller 会创建一个新的 Pod。

第四步:重复

整个过程是一个无限循环。即使某个操作失败了,下一轮循环也会再次尝试。这就是 K8s 实现自愈能力的根本原因。

水平扩展:自定义控制器

K8s 的控制器模式不仅限于内置资源。通过 CRD(Custom Resource Definition)Operator 模式,你可以定义自己的 API 对象和对应的控制器。例如,Prometheus Operator 可以管理 Prometheus 实例的生命周期,Cert-Manager Operator 可以自动化 TLS 证书的申请和续期。这就是 K8s 被称为"平台的平台"的原因。


五、本章小结与下一篇预告

本文作为 Kubernetes 全景解析系列的第零章,我们从宏观视角梳理了 K8s 的全貌:

维度核心要点
设计哲学声明式配置、不可变基础设施、最终一致性
架构分层控制面(决策)+ 数据面(执行)
控制面组件API Server(入口)、etcd(存储)、Scheduler(调度)、Controller Manager(控制循环)
数据面组件kubelet(节点代理)、kube-proxy(网络代理)、CRI(容器运行时)
核心抽象一切皆 API 对象,通过 YAML 声明期望状态
运行机制Watch 事件驱动 + 控制循环持续调谐

掌握这些基础知识后,你将不再对 K8s 感到迷茫——它的每一个行为都可以追溯到上述架构和机制中。

下一篇,我们将深入 K8s 的工作负载管理: 从 Pod 的生命周期到 Deployment、StatefulSet、DaemonSet 等工作负载控制器的使用场景与最佳实践。理解了工作负载,你就能真正开始在 K8s 上部署和运行应用了。


系列导航

章节主题状态
0架构设计与核心概念✅ 已发布
1工作负载与 Pod 生命周期深度解析✅ 已发布
2网络模型与服务发现全链路解析✅ 已发布
3存储体系与配置管理深度剖析✅ 已发布
4调度器、资源管理与弹性伸缩✅ 已发布
5安全体系与可观测性全景✅ 已发布
6生产级微服务架构实战✅ 已发布
7有状态应用与 Operator 模式实战✅ 已发布

参考文档

本文内容基于 Kubernetes 官方文档校验,以下为核心参考链接: