跳到主要内容

28 篇博文 含有标签「深度解析」

深度技术文章

查看所有标签

WebRTC 全景实战 (4):SDP 解剖与媒体协商

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

"Offer/Answer 里那一大段文本,就是整个 WebRTC 会话的「合同」。"

Ch3 信令 负责传递 SDP,但 SDP 本身是什么?Ch2createOffer() 产出的字符串,遵循 RFC 8866 SDP 格式。它描述了:用什么编解码器、用什么 ICE 凭证、用什么 DTLS 指纹、媒体方向是什么

本章逐行解剖 WebRTC 中的 SDP,理解 Unified PlanBUNDLErtcp-mux,以及 RTCRtpTransceiver 与 m-line 的对应关系。

WebRTC 全景实战 (3):信令服务器设计与会话状态机

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

"WebRTC 只标准化媒体通道,信令 deliberately 留给应用层。" — WebRTC for the Curious

Ch2 我们用手工复制 JSON 完成了 Offer/Answer 和 ICE Candidate 交换——这足以理解原理,但无法支撑生产。Serge Lachapelle 在 Curious 历史访谈中明确提到:IETF 刻意不对信令重新标准化,因为 SIP 等方案已存在,重新标准化只会引发政治斗争且「不会创造有价值贡献」。

因此,每一个 WebRTC 应用都必须实现自己的信令层。本章从设计原则出发,构建一套可扩展的 WebSocket 信令服务器,并讨论生产环境的安全、重连与 Room 状态管理。

配套代码:examples/webrtc-lab/signaling/

WebRTC 全景实战 (2):第一个 P2P 视频通话

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

"跑通第一个通话,比读完十页 RFC 更有说服力。"

Ch1 我们学会了采集媒体。本章用 RTCPeerConnection 把媒体发给另一个浏览器——第一个完整 P2P 视频通话

Serge Lachapelle 在 WebRTC for the Curious — 历史 中解释了一个关键设计决策:IETF 刻意不对信令(Signaling)重新标准化,因为 SIP 等方案已存在,"重新标准化只会引发政治斗争"。这意味着 WebRTC 只标准化了媒体通道(SDP、ICE、DTLS、SRTP),而 Offer/Answer 的传递方式完全由应用决定——本章先用「复制粘贴 JSON」理解原理,Ch3 再替换为 WebSocket 信令服务器。

配套代码:examples/webrtc-lab/client/ch02-p2p-basic/

WebRTC 全景实战 (1):浏览器媒体 API 与设备管理

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

"在建立任何 P2P 连接之前,你得先拿到媒体。"

上一篇 Ch0 架构全景 我们画了协议栈地图。本章从栈顶最直观的入口开始:如何把摄像头、麦克风和屏幕变成浏览器里的 MediaStream

Serge Lachapelle 在 WebRTC for the Curious — 历史 中回忆:Gmail 语音视频聊天的前身需要分别授权 GIPS 音频、Vidyo 视频、libjingle 网络三个子系统,"每个子系统都有完全不同的 API"。WebRTC 标准化工作的核心目标之一,就是把 媒体采集 这一层统一成开发者今天使用的 navigator.mediaDevices API——让你不必再为每个浏览器插件写一套集成代码。

配套代码:examples/webrtc-lab/client/ch01-media-devices/

WebRTC 全景实战 (0):架构全景与协议栈地图

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

"WebRTC 不是某一个 API,而是一整套让浏览器能够安全、低延迟地交换实时媒体的开放标准。"

如果你曾经尝试在网页里做视频通话,大概率遇到过这样的困惑:RTCPeerConnection 的文档能看懂,但 ICE 一直 failed;SDP 里一堆 a=rtpmap 不知道在协商什么;明明本地预览正常,对方却听不到声音。

这些问题的根源,通常不是某个 API 调用错了,而是缺少一张 WebRTC 协议栈的全景地图

本系列 WebRTC 全景实战 共 16 篇,将从架构认知出发,逐层深入到信令、SDP、ICE、DTLS/SRTP、RTP/RTCP、编解码、拥塞控制、SFU 架构与生产部署,每篇均配套 examples/webrtc-lab 实战代码。本文作为第零章,回答最根本的问题:WebRTC 是什么?它从哪来?它由哪些层组成?

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 资源管理体系。