diff --git a/CLAUDE.md b/CLAUDE.md index b5c16ae..6dfe9fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,8 @@ create time: YYYY-MM-DD HH:mm |------|------|------| | `technical/` | 技术知识点 | Go、Redis、K8s 等 | | `projects/` | 开发项目记录 | 具体参与的模块或系统 | -| `weekly/` | 周/日报 | 按周归档的工作总结 | +| `weekly/` | 周报 | 按周归档的工作总结 | +| `weekly/daily/` | 日报 | 按日期归档的每日记录 | - 文件名统一使用 **kebab-case**(小写 + 短横线),如 `redis-hash-internals.md` - 日期类文件在文件名中包含日期,如 `2026-w25-weekly-report.md` @@ -134,11 +135,13 @@ status: active | completed | on-hold # 可选状态字段 --- -### weekly/ — 周报/日报 +### weekly/ — 周报与日报 -侧重**结构化总结与快速回顾**。 +侧重**结构化总结与快速回顾**。周报和日报分开管理。 -#### 结构模板 +#### 周报 + +路径:`weekly/2026-w25-weekly-report.md` ```markdown --- @@ -163,11 +166,57 @@ week: W25 # 第几周 - 待办事项 & 下周计划 ``` +#### 日报 + +路径:`weekly/daily/2026-07-07-daily.md`,模板:`weekly/daily/template.md` + +日报为**下班前**提交的当日总结,结构:今日完成 → 卡点 → 明日计划。新建日报时复制模板并填入当日日期。 + +```markdown +--- +tags: [daily] +create time: YYYY-MM-DD HH:mm +--- + +# 2026-07-07 日报 + +## 今日完成 +- [ ] 完成事项 + +## GitHub Issues +> 当日无关联 Issue + + + +## 卡点 / 阻塞 +> 当前无卡点 + + + +## 明日计划 +- [ ] 计划事项 + +## 备注 +``` + #### 写作要求 - 简洁条目式,不展开长段落 - 每条完成后打 `[x]`,未完成保留 `[ ]` -- 「问题 / 阻塞」板块用于向上同步风险,务必诚实标注 -- 日期按周归档,文件名格式:`2026-w25-weekly-report.md` +- 「今日完成」需体现对目标的影响,不是流水账 +- 卡点必须注明跟进人和预计反馈时间,无卡点时保留「当前无卡点」占位 +- GitHub Issues 板块按状态分组(进行中 / 待 Review / 已关闭),无 Issue 时保留「当日无关联 Issue」占位 +- 周报「问题 / 阻塞」板块用于向上同步风险,务必诚实标注 +- 周报文件名格式:`2026-w25-weekly-report.md` +- 日报文件名格式:`2026-07-07-daily.md`,按日期自然排序 --- diff --git a/technical/k8s/k8s-01-overview.md b/technical/k8s/k8s-01-overview.md new file mode 100644 index 0000000..1831312 --- /dev/null +++ b/technical/k8s/k8s-01-overview.md @@ -0,0 +1,562 @@ +--- +tags: [k8s, devops, container-orchestration] +create time: 2026-07-07 15:48 +--- + +# Kubernetes 概述 + +## 概述 + +Kubernetes(简称 K8s)是一个开源的**容器编排平台**,由 Google 内部的 Borg 系统演化而来,于 2014 年开源,目前由 CNCF(Cloud Native Computing Foundation)托管。它的核心使命是:**让部署、扩展和管理容器化应用变得自动化且可预测**。简单来说,Docker 解决了「如何打包一个应用」的问题,而 K8s 解决的是「如何在成百上千台机器上高效地运行和管理这些应用」的问题。 + +## 为什么需要 K8s + +### 从 Docker 单机到集群编排 + +让我们先回顾一下容器技术的演进路径: + +1. **裸机/虚拟机时代**:应用直接部署在物理机或 VM 上,手动管理依赖、端口和资源分配。 +2. **Docker 单机时代**:容器解决了环境一致性和依赖打包的问题,但单机资源有限,且面临应用故障恢复、滚动更新等挑战。 +3. **集群编排时代**:当你的服务需要跨多台机器运行时,你需要一个「大脑」来决定:把容器调度到哪台机器?挂了怎么重启?流量怎么分配?——这就是 K8s 存在的意义。 + +> [!tip] 一个类比 +> 如果 Docker 集装箱标准化了「货物打包」,那 K8s 就是自动化码头——它决定集装箱该放哪艘船、航线怎么规划、遇到风暴如何重新调度。 + +### Docker Compose vs Docker Swarm vs Kubernetes + +| 特性 | Docker Compose | Docker Swarm | Kubernetes | +|------|---------------|-------------|------------| +| **定位** | 单机多容器编排 | 轻量级集群编排 | 企业级容器编排平台 | +| **规模** | 单主机 | 中小规模集群 | 大规模生产环境 | +| **学习曲线** | 低 | 中 | 高 | +| **自动扩缩容** | 不支持 | 基础支持 | HPA / VPA / Cluster Autoscaler | +| **自愈能力** | 无 | 基础(重启策略) | 强(Liveness/Readiness Probe + 控制器) | +| **滚动更新** | 不支持 | 支持 | 细粒度控制(maxUnavailable / maxSurge) | +| **生态** | 仅 Docker 生态 | 仅 Docker 生态 | CNCF 生态,插件丰富 | +| **服务发现** | 容器名 DNS | 内置 DNS | CoreDNS + Service 资源 | +| **适用场景** | 本地开发、测试 | 快速上手小集群 | 生产环境首选 | + +> [!info] 为什么 Swarm 没能打败 K8s? +> Docker Swarm 的设计哲学是「简单优先」,但现实世界的生产需求远比想象中复杂。K8s 的声明式 API、丰富的控制器模型、以及 CNCF 生态的飞轮效应,最终让它成为事实标准。 + +## 核心架构 + +K8s 采用经典的 **Master-Worker 架构**(官方文档中 Master 节点现在更常称为 Control Plane)。 + +### 架构总览 + +```mermaid +graph TB + subgraph ControlPlane["Control Plane(控制平面)"] + API["API Server
集群的唯一入口"] + etcd["etcd
分布式 KV 存储"] + sched["Scheduler
Pod 调度决策"] + cm["Controller Manager
维持期望状态"] + end + + subgraph Worker1["Worker Node 1"] + kubelet1["kubelet
管理 Pod 生命周期"] + proxy1["kube-proxy
网络规则管理"] + cr1["Container Runtime
containerd / CRI-O"] + P1a["Pod A"] + P1b["Pod B"] + end + + subgraph Worker2["Worker Node 2"] + kubelet2["kubelet"] + proxy2["kube-proxy"] + cr2["Container Runtime"] + P2a["Pod C"] + P2b["Pod D"] + end + + subgraph Client["用户 / CI/CD"] + kubectl["kubectl / SDK"] + end + + kubectl -->|"REST API"| API + API <-->|"读写"| etcd + API -->|"监听事件"| sched + API -->|"监听事件"| cm + kubelet1 -->|"汇报状态"| API + kubelet2 -->|"汇报状态"| API + kubelet1 --> cr1 + kubelet2 --> cr2 + cr1 --> P1a & P1b + cr2 --> P2a & P2b + proxy1 -.->|"Service 流量"| P1a & P1b + proxy2 -.->|"Service 流量"| P2a & P2b +``` + +### Control Plane 组件详解 + +#### API Server(kube-apiserver) + +API Server 是整个集群的**唯一入口**,所有组件之间的通信都通过它进行。它承担的核心职责: + +- **RESTful API 网关**:暴露 K8s API,所有 `kubectl` 操作最终都到达这里 +- **认证与授权**:RBAC、ServiceAccount、Webhook Token 等 +- **准入控制(Admission Control)**:在资源持久化之前进行校验和修改(如 MutatingWebhook、ValidatingWebhook) +- **Watch 机制**:其他组件通过 Watch API Server 获取资源变更事件 + +> [!tip] 为什么所有组件都要通过 API Server 通信? +> 这是 K8s 的一个重要设计决策——**中心化 API 网关**保证了统一的认证、授权、审计和数据校验逻辑。如果各组件直接读写 etcd,安全策略将难以实施。 + +#### etcd + +etcd 是一个高可用的分布式 KV 存储,是 K8s 的「唯一数据源」。所有集群状态(Pod、Service、ConfigMap 等资源对象)都持久化在 etcd 中。 + +- 基于 **Raft 共识算法**保证数据一致性 +- 生产环境通常部署 3 或 5 个节点(奇数个,便于选举) +- **Watch 机制**:支持高效地监听 key 的变更 + +> [!warning] etcd 是整个集群的 Single Point of Truth +> 如果 etcd 数据丢失且没有备份,整个集群状态将不可恢复。生产环境务必做好 etcd 的定期备份。 + +#### Scheduler(kube-scheduler) + +Scheduler 负责将新创建的 Pod **绑定到合适的 Node** 上。调度过程分为两个阶段: + +1. **过滤(Filtering)**:排除不满足条件的 Node(如资源不足、Taint 不匹配、亲和性规则冲突) +2. **打分(Scoring)**:对剩余 Node 进行评分,选择得分最高的 + +常见调度策略: + +- **资源请求与限制**:基于 Pod 的 `requests` 和 `limits` +- **亲和性与反亲和性**:`nodeAffinity`、`podAffinity`、`podAntiAffinity` +- **Taint 和 Toleration**:驱逐不兼容的 Pod +- **拓扑分布约束**:`topologySpreadConstraints` 跨可用区均匀分布 + +#### Controller Manager(kube-controller-manager) + +Controller Manager 是一组控制器的集合,每个控制器负责一种资源类型的「调谐循环(Reconciliation Loop)」。核心思想:**持续比较「期望状态」和「实际状态」,并采取行动使实际状态趋近期望状态**。 + +常见控制器: + +| 控制器 | 职责 | +|--------|------| +| Deployment Controller | 管理 ReplicaSet 的创建/更新 | +| ReplicaSet Controller | 确保指定数量的 Pod 副本运行 | +| Node Controller | 监控 Node 心跳,处理节点故障 | +| Job Controller | 管理一次性任务的 Pod | +| ServiceAccount Controller | 为新 Namespace 创建默认 ServiceAccount | + +### Worker Node 组件详解 + +#### kubelet + +kubelet 是运行在每个 Worker Node 上的**代理进程**,负责: + +- 从 API Server 获取分配给本节点的 Pod Spec +- 调用 Container Runtime 拉取镜像、启动容器 +- 执行健康检查(Liveness Probe、Readiness Probe、Startup Probe) +- 向 API Server 汇报节点和 Pod 状态 +- 管理 Volume 的挂载和卸载 + +> [!info] kubelet 不是通过 API Server 启动容器的 +> kubelet 通过 **CRI(Container Runtime Interface)** 与容器运行时交互,而非直接调用 Docker CLI。 + +#### kube-proxy + +kube-proxy 负责在每个节点上维护**网络规则**,实现 Service 的负载均衡。它有三种工作模式: + +| 模式 | 实现方式 | 性能 | 推荐度 | +|------|---------|------|--------| +| iptables | Linux iptables 规则 | 中等(规则数线性增长) | 默认模式 | +| IPVS | Linux IPVS(内核级 LB) | 高(哈希表查找) | 大规模集群推荐 | +| nftables | nftables 规则 | 高 | K8s 1.29+ 实验性 | + +#### Container Runtime + +Container Runtime 是真正执行容器生命周期操作的组件。K8s 通过 **CRI(Container Runtime Interface)** 标准化了与运行时的交互方式: + +- **containerd**:目前最主流的选择,Docker 的核心运行时组件被独立出来 +- **CRI-O**:Red Hat 主导,专为 K8s 设计的轻量级运行时 +- **Docker**:自 K8s 1.24 起不再直接支持(dockershim 已移除),但 containerd 仍可使用 + +## 关键概念 + +### Pod + +Pod 是 K8s 中**最小的可部署单元**,而不是容器。一个 Pod 可以包含一个或多个容器,它们: + +- 共享同一个 Network Namespace(即共享 IP 和端口空间) +- 可以通过 Volume 共享存储 +- 总是被调度到同一个 Node 上 + +> [!tip] 为什么需要 Pod 而不是直接管理容器? +> 现实中有些应用需要「伴生容器(Sidecar)」——比如日志收集器、Service Mesh 代理。Pod 将这些紧密耦合的容器打包在一起,共享网络和存储,比独立容器更优雅。 + +```yaml +# 一个包含 Sidecar 的 Pod 示例 +apiVersion: v1 +kind: Pod +metadata: + name: app-with-logging + labels: + app: my-app # 用于 Service 选择和筛选 + env: production +spec: + containers: + - name: app + image: my-app:v1.2.3 + ports: + - containerPort: 8080 + resources: + requests: + cpu: "100m" # 0.1 核 + memory: "128Mi" + limits: + cpu: "500m" # 最多 0.5 核 + memory: "512Mi" + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /ready + port: 8080 + periodSeconds: 3 + - name: log-collector + image: fluentd:v1.16 + volumeMounts: + - name: log-volume + mountPath: /var/log/app + volumes: + - name: log-volume + emptyDir: {} # Pod 内共享的临时目录 +``` + +**Pod 生命周期中的几个关键阶段**: + +| Phase | 含义 | +|-------|------| +| Pending | 已被 API Server 接受,但尚未调度或镜像未拉取 | +| Running | 至少一个容器正在运行 | +| Succeeded | 所有容器正常退出(退出码 0) | +| Failed | 至少一个容器异常退出 | +| Unknown | 无法获取 Pod 状态(通常是 Node 通信故障) | + +### Node + +Node 是 K8s 集群中的**工作机器**(物理机或虚拟机)。每个 Node 包含运行 Pod 所需的服务(kubelet、kube-proxy、Container Runtime)。 + +查看 Node 信息: + +```bash +# 查看集群所有节点 +kubectl get nodes -o wide + +# 查看某个节点的详细信息(资源、条件、运行的 Pod) +kubectl describe node +``` + +Node 有几个重要的**条件(Conditions)**: + +| Condition | 说明 | +|-----------|------| +| Ready | 节点健康且准备好接受 Pod | +| MemoryPressure | 节点内存不足 | +| DiskPressure | 节点磁盘空间不足 | +| PIDPressure | 节点进程数过多 | +| NetworkUnavailable | 节点网络未正确配置 | + +### Namespace + +Namespace 是 K8s 中的**逻辑隔离机制**,用于在同一物理集群中划分多个虚拟集群。适用于: + +- 多团队共享集群(`team-a`、`team-b`) +- 环境隔离(`dev`、`staging`、`production`) +- 资源配额管理(每个 Namespace 设置独立的 ResourceQuota) + +```yaml +# 创建一个 Namespace +apiVersion: v1 +kind: Namespace +metadata: + name: team-backend + labels: + team: backend + env: production +--- +# 在该 Namespace 下设置资源配额 +apiVersion: v1 +kind: ResourceQuota +metadata: + name: team-backend-quota + namespace: team-backend +spec: + hard: + requests.cpu: "10" # 总 CPU 请求上限 10 核 + requests.memory: "20Gi" # 总内存请求上限 20Gi + limits.cpu: "20" + limits.memory: "40Gi" + pods: "50" # 最多 50 个 Pod +``` + +K8s 默认创建的 Namespace: + +| Namespace | 用途 | +|-----------|------| +| `default` | 未指定 Namespace 时的默认值 | +| `kube-system` | K8s 系统组件(CoreDNS、kube-proxy 等) | +| `kube-public` | 公共资源(集群信息等) | +| `kube-node-lease` | 节点心跳 Lease 对象 | + +```bash +# 在指定 Namespace 下操作 +kubectl get pods -n team-backend + +# 设置默认 Namespace(避免每次都输入 -n) +kubectl config set-context --current --namespace=team-backend +``` + +> [!warning] Namespace 不提供网络隔离 +> 默认情况下,不同 Namespace 的 Pod 可以互相通信。如需网络隔离,需要配合 **NetworkPolicy** 实现。 + +### Label & Selector + +Label 是附加在 K8s 对象上的**键值对**,是 K8s 组织和筛选资源的核心机制。你可能会问:「为什么 K8s 不用目录/文件夹的方式来组织资源?」——因为资源之间的关系是多对多的,一个 Pod 可能同时属于某个应用、某个环境、某个团队,Label 的灵活性远超树形结构。 + +```yaml +# Label 的常见命名约定 +metadata: + labels: + app.kubernetes.io/name: my-app # 应用名称 + app.kubernetes.io/version: "1.2.3" # 版本 + app.kubernetes.io/component: frontend # 组件 + app.kubernetes.io/part-of: platform # 所属系统 + app.kubernetes.io/managed-by: helm # 管理工具 + env: production # 环境 + team: backend # 团队 +``` + +**Selector 的三种用法**: + +```bash +# 等值选择器 +kubectl get pods -l app=my-app,env=production + +# 集合选择器 +kubectl get pods -l 'env in (production, staging)' +kubectl get pods -l 'env notin (dev)' + +# 否定选择器 +kubectl get pods -l 'app!=my-app' +``` + +在资源定义中使用 Selector(以 Service 为例): + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-app-svc +spec: + selector: + app: my-app # 匹配所有带 app=my-app 标签的 Pod + env: production + ports: + - port: 80 + targetPort: 8080 +``` + +### Annotation + +Annotation 用于存储**非标识性的附加信息**,这些信息不会被 K8s API 用于筛选,但对工具、库和运维人员很有用。典型的 Annotation 内容: + +```yaml +metadata: + annotations: + # 记录维护者信息 + owner: "backend-team@company.com" + # Git 提交信息(CI/CD 注入) + git-commit: "abc123f" + git-branch: "main" + # 构建时间 + build-time: "2026-07-07T15:00:00Z" + # Ingress 配置(Nginx Ingress Controller 使用) + nginx.ingress.kubernetes.io/proxy-body-size: "50m" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + # Prometheus 自动发现 + prometheus.io/scrape: "true" + prometheus.io/port: "9090" +``` + +> [!tip] Label vs Annotation 如何选择? +> 问自己一个问题:「我需要根据这个信息来筛选或分组资源吗?」—— 如果是,用 Label;如果只是附加的描述性信息,用 Annotation。Label 有长度和字符限制,Annotation 则更宽松。 + +## K8s 与 Docker 的关系 + +### 常见误解 + +很多初学者会问:「K8s 是不是要替代 Docker?」答案是:**不完全是**。准确地说: + +- K8s 替代的是 Docker Swarm(编排层面的竞争) +- K8s **不再直接使用 Docker Engine** 作为容器运行时(自 1.24 起) +- 但你仍然可以**用 Docker 构建镜像**,K8s 运行的是 OCI 标准镜像,与构建工具无关 + +### 容器运行时的演进 + +```mermaid +timeline + title K8s 容器运行时演进 + 2014-2016 : Docker 是唯一选择 + : kubelet 直接调用 Docker API + 2017 : CRI 标准引入 + : dockershim 作为适配层 + 2018 : containerd 从 Docker 拆分 + : CRI-O 项目成熟 + 2020 : dockershim 弃用公告 + 2022-04 : K8s 1.24 移除 dockershim + : containerd 成为主流 + 2024+ : CRI-O 广泛使用 + : Kata / gVisor 沙箱运行时兴起 +``` + +**CRI(Container Runtime Interface)** 是 K8s 定义的一套 gRPC 接口规范,任何实现了 CRI 的运行时都可以被 kubelet 使用: + +| 运行时 | 特点 | 适用场景 | +|--------|------|---------| +| containerd | Docker 核心组件独立版,功能全面 | 通用场景,最广泛使用 | +| CRI-O | 专为 K8s 设计,轻量级 | Red Hat / OpenShift 生态 | +| Kata Containers | 轻量级 VM,安全隔离 | 多租户、不可信工作负载 | +| gVisor (runsc) | 用户态内核沙箱 | 安全敏感场景 | + +> [!info] 理解 CRI 的分层 +> kubelet → CRI (gRPC) → containerd → runc → Linux Kernel(namespaces, cgroups) +> +> 每一层都有明确的职责边界。containerd 负责镜像管理和容器生命周期,runc 负责实际创建 Linux 容器。 + +### 为什么移除 dockershim? + +移除 dockershim 的主要原因: + +1. **维护成本**:dockershim 是 kubelet 内部的一层额外抽象,增加代码复杂度 +2. **性能开销**:kubelet → dockershim → Docker daemon → containerd,调用链过长 +3. **Docker 的「额外功能」**:Docker Engine 包含很多 K8s 不需要的组件(如 Docker Swarm、docker build 等),但作为运行时又必须加载整个 daemon +4. **CRI 标准成熟**:containerd 和 CRI-O 直接实现 CRI,不再需要中间层 + +## 部署方式对比 + +| 方案 | 类型 | 节点数 | 生产就绪 | 学习成本 | 适用场景 | +|------|------|--------|---------|---------|---------| +| **minikube** | 单节点本地集群 | 1 | 否 | 低 | 本地开发和学习,支持 Addons | +| **kind** | Docker 容器模拟 Node | 1-N | 否 | 低 | CI/CD 测试、多节点本地模拟 | +| **kubeadm** | 生产级部署工具 | N | 是 | 中高 | 自建集群、对基础设施有完全控制 | +| **EKS** | AWS 托管 | N | 是 | 中 | AWS 生态用户 | +| **AKS** | Azure 托管 | N | 是 | 中 | Azure 生态用户 | +| **GKE** | Google Cloud 托管 | N | 是 | 中 | GCP 生态用户,Autopilot 模式 | +| **ACK** | 阿里云托管 | N | 是 | 中 | 国内云生态用户 | + +> [!tip] 如何选择? +> - **学习阶段**:minikube 或 kind(零成本,快速启动) +> - **CI/CD**:kind(轻量,测试完即销毁) +> - **小团队生产**:托管服务(减少运维负担) +> - **大规模/定制需求**:kubeadm 或 Cluster API + +## 常见陷阱与最佳实践 + +### 陷阱 + +1. **不设置 Resource Requests/Limits** + ```yaml + # 反面教材:Pod 没有资源声明 + containers: + - name: app + image: my-app:v1 + ``` + 后果:调度器无法做出合理决策,某个 Pod 可能吃掉整个节点的资源,导致其他 Pod 被 OOM Kill。 + +2. **把所有东西塞进一个 Pod** + Pod 中的容器应该有紧密的生命周期耦合。如果两个应用可以独立部署、独立扩缩容,它们应该在不同的 Pod 中。 + +3. **忽略 Liveness/Readiness Probe** + 没有 Probe 的 Pod,K8s 无法知道它是否真的健康。一个「Running」状态的 Pod,内部应用可能已经死锁。 + +4. **在生产环境使用 `latest` 标签** + ```yaml + # 反面教材 + image: my-app:latest + ``` + 你无法确定每次部署用的是哪个版本,回滚更是噩梦。始终使用明确的版本标签(如 `v1.2.3` 或 Git SHA)。 + +5. **直接操作集群资源而不使用 GitOps** + 手动 `kubectl apply` 的变更不可追溯、不可审计。使用 Helm / Kustomize + ArgoCD / Flux 进行声明式管理。 + +### 最佳实践 + +1. **声明式 > 命令式**:始终用 YAML 定义资源,而不是 `kubectl run`/`kubectl create` 的命令式操作。声明式配置可版本控制、可审计、可复现。 + +2. **合理使用 Namespace**:按团队或环境划分 Namespace,配合 ResourceQuota 和 LimitRange 进行资源管控。 + +3. **Pod 反亲和性确保高可用**: + ```yaml + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: ["my-app"] + topologyKey: kubernetes.io/hostname + ``` + 这确保同一应用的多个副本不会被调度到同一个 Node 上。 + +4. **使用 `imagePullPolicy` 控制镜像拉取策略**: + - `Always`:总是拉取(适用于 `latest` 标签,但不推荐用 latest) + - `IfNotPresent`:本地没有才拉取(推荐用于明确版本标签) + - `Never`:从不拉取(仅用于本地开发) + +5. **优雅终止(Graceful Shutdown)**: + ```yaml + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: app + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "sleep 5"] + ``` + `preStop` 中的 `sleep 5` 给 kube-proxy 更新规则留出时间,避免请求被发送到正在终止的 Pod。 + +6. **LimitRange 设置默认值**: + ```yaml + apiVersion: v1 + kind: LimitRange + metadata: + name: default-limits + namespace: team-backend + spec: + limits: + - default: + cpu: "500m" + memory: "256Mi" + defaultRequest: + cpu: "100m" + memory: "128Mi" + type: Container + ``` + 防止单个未配置资源限制的容器拖垮整个节点。 + +## 延伸阅读 + +- [Kubernetes 官方文档](https://kubernetes.io/zh-cn/docs/home/) — 最权威的参考 +- [Kubernetes 官方教程](https://kubernetes.io/zh-cn/docs/tutorials/) — 动手入门 +- [CNCF 技术雷达](https://www.cncf.io/tech-radars/) — 云原生技术选型参考 +- [Kelsey Hightower - Kubernetes The Hard Way](https://github.com/kelseyhightower/kubernetes-the-hard-way) — 手动从零搭建 K8s 集群,深入理解每个组件 +- 《Kubernetes in Action (第二版)》 — Marko Lukša 著,深入原理的经典教材 + +## 关联笔记 + +- [[k8s/k8s-workload-resources]] — Deployment、StatefulSet、DaemonSet 等工作负载详解 +- [[k8s/k8s-networking]] — Service、Ingress、NetworkPolicy 网络模型 +- [[k8s/k8s-storage]] — PV、PVC、StorageClass 存储体系 diff --git a/technical/k8s/k8s-02-architecture.md b/technical/k8s/k8s-02-architecture.md new file mode 100644 index 0000000..c9f8055 --- /dev/null +++ b/technical/k8s/k8s-02-architecture.md @@ -0,0 +1,695 @@ +--- +tags: [k8s, architecture, devops] +create time: 2026-07-07 15:48 +--- + +# Kubernetes 架构深度解析 + +## 概述 + +Kubernetes 采用经典的**控制平面 + 数据平面**分层架构。控制平面(Control Plane)负责全局决策——调度、编排、状态收敛;数据平面(Data Plane)由每个节点上的 Agent 组成,负责 Pod 的实际运行与网络转发。本文从 API Server、etcd、Scheduler、Controller Manager 四大控制平面组件,到 kubelet、kube-proxy、Container Runtime 三大数据平面组件,逐一拆解其内部机制、设计动机与协作关系,最后通过一次完整的 `kubectl apply` 请求流转,串联所有组件的工作链路。 + +--- + +## 控制平面(Control Plane) + +控制平面是 Kubernetes 集群的"大脑",所有声明式状态的写入、决策与协调均在此完成。 + +### API Server(kube-apiserver) + +API Server 是整个集群的**唯一入口**(Single Source of Truth)。所有组件——kubectl、kubelet、Controller Manager、Scheduler——都只和 API Server 通信,绝不直接操作 etcd。 + +#### 为什么这样设计? + +- **统一鉴权**:所有请求经过同一套认证/授权/准入链,避免各组件自行实现安全逻辑。 +- **解耦存储**:API Server 作为 etcd 的代理层,后端存储引擎可替换(理论层面),上层组件无需感知。 +- **可观测性**:集中入口便于审计日志、请求速率限制等横向关注点的统一治理。 + +#### 请求处理链(Request Pipeline) + +一次 API 请求经过以下阶段: + +``` +Client Request + → Authentication(认证) + → Authorization(授权) + → Admission Control(准入控制,Mutating → Validating) + → Schema Validation(对象校验) + → etcd Write(持久化) +``` + +| 阶段 | 作用 | 常见实现 | +|------|------|----------| +| Authentication | 确认"你是谁" | X.509 Client Cert、ServiceAccount Token、OIDC、Webhook Token | +| Authorization | 确认"你能做什么" | RBAC(主流)、ABAC、Webhook | +| Mutating Admission | 修改对象(注入 sidecar、设置默认值) | MutatingWebhookConfiguration | +| Validating Admission | 拒绝非法对象(资源限额、策略校验) | ValidatingAdmissionPolicy、OPA/Gatekeeper | +| Schema Validation | 校验字段类型与必填项 | 内置 OpenAPI Schema | + +> [!info] RBAC 的三个核心概念 +> - **Role / ClusterRole**:定义一组权限(动词 + 资源)。 +> - **RoleBinding / ClusterRoleBinding**:将 Role 绑定到 Subject(User、Group、ServiceAccount)。 +> - **聚合 ClusterRole**:通过 Label Selector 合并多个 ClusterRole,扩展权限时无需修改原角色。 + +#### Watch 机制 + +API Server 支持 **Watch**——客户端可以长轮询监听资源变更,而非反复 List 轮询。其实现原理: + +1. 客户端发送 `GET /api/v1/pods?watch=true&resourceVersion=12345`。 +2. API Server 从 etcd 的 Watch 流中获取变更事件,经序列化后通过 HTTP chunked transfer / HTTP/2 stream 推送给客户端。 +3. 如果客户端的 `resourceVersion` 过旧(已被 API Server compaction),返回 `410 Gone`,客户端需重新 List。 + +> [!tip] Informer 机制 +> Kubernetes client-go 库中的 Informer 封装了 Watch + 本地缓存(Indexer)+ 事件分发(EventHandler)的完整流程。所有 Controller 内部都依赖 Informer 来感知资源变化,避免直接对 API Server 发起大量请求。 + +#### 与 etcd 的交互 + +- **写操作**:API Server 是唯一向 etcd 写入数据的组件。 +- **读操作**:其他组件(Scheduler、Controller Manager)通过 API Server 读取,API Server 内部有 `apiserver cache` 层优化读性能。 +- **一致性保证**:API Server 使用 etcd 的 MVCC(Multi-Version Concurrency Control)实现乐观并发控制,通过 `resourceVersion` 字段做冲突检测。 + +--- + +### etcd + +etcd 是 Kubernetes 的**唯一持久化存储**,存储了集群的所有状态数据:Pods、Services、ConfigMaps、Secrets、Nodes、Namespaces 等。 + +#### Raft 一致性协议 + +etcd 使用 Raft 协议保证分布式一致性。Raft 将节点分为三种角色: + +| 角色 | 职责 | 选举条件 | +|------|------|----------| +| Leader | 处理所有写请求,日志复制到 Follower | 获得多数节点投票 | +| Follower | 接收 Leader 日志,被动同步 | 默认状态 | +| Candidate | 发起选举 | Follower 在 election timeout 内未收到心跳 | + +**写入流程**: + +1. 客户端写请求发送至 Leader。 +2. Leader 将日志条目(Log Entry)追加到本地日志。 +3. Leader 将日志复制到所有 Follower。 +4. **多数派(majority)**确认后,Leader 提交(commit)该条目。 +5. Leader 通知 Follower 提交,状态机应用变更。 + +> [!warning] etcd 集群节点数建议 +> 生产环境建议 **3 或 5 个节点**。3 节点容忍 1 个故障,5 节点容忍 2 个故障。偶数节点(如 4)不增加容错能力反而增加复制开销,不推荐。 + +#### Key-Value 结构 + +etcd 以扁平的 Key 空间存储数据,Kubernetes 的资源路径映射为: + +``` +/registry/pods/default/my-pod +/registry/deployments/default/my-deploy +/registry/services/kube-system/kube-dns +/registry/minions/node-1 +``` + +每个 Value 是对应资源对象的 **Protocol Buffer 编码**。 + +#### Watch 特性 + +etcd 原生支持基于 revision 的 Watch。API Server 的 Watch 机制正是建立在 etcd Watch 之上。etcd 使用 **MVCC** 保留历史版本,支持从任意 revision 回放事件,这也是 `resourceVersion` 机制的底层基础。 + +#### 备份与恢复策略 + +- **定期快照**:`etcdctl snapshot save` 创建一致性快照。 +- **自动 compaction**:通过 `--auto-compaction-retention` 配置历史版本保留时间,防止存储无限增长。 +- **恢复流程**:`etcdctl snapshot restore` 恢复数据到新目录 → 重启 etcd 进程。 + +> [!tip] etcd 的磁盘性能至关重要 +> etcd 对磁盘延迟极其敏感。生产环境应使用 SSD,避免与其他 I/O 密集型应用共享磁盘。可以使用 `fio` 工具检测磁盘 IOPS 和延迟。 + +--- + +### Scheduler(kube-scheduler) + +Scheduler 负责为每个未绑定 Node 的 Pod 选择最优节点。其核心流程是**过滤(Filter)→ 打分(Score)**两阶段调度。 + +#### 调度流程 + +``` +Pod Created (spec.nodeName 为空) + → 1. Filtering(过滤阶段) + - 移除不满足硬性条件的节点 + - 例:资源不足、NodeSelector 不匹配、Taint 无对应 Toleration + → 2. Scoring(打分阶段) + - 对剩余节点按策略打分 + - 默认策略倾向于:资源均衡分布、亲和性匹配、数据局部性 + → 3. Binding(绑定阶段) + - 选择得分最高的节点,写入 Pod.spec.nodeName + - API Server 通知对应节点的 kubelet +``` + +> [!info] 为什么是 Filter + Score 两阶段? +> 过滤阶段是**约束满足问题(CSP)**,用排除法缩小候选集;打分阶段是**优化问题**,在可行解中找最优。两阶段分离使得每一步可以独立扩展和插件化。 + +#### 亲和性与反亲和性 + +| 类型 | 作用域 | 典型场景 | +|------|--------|----------| +| `nodeAffinity` | Pod → Node | 将 Pod 调度到特定标签的节点(如 GPU 节点) | +| `podAffinity` | Pod → Pod | 将相关 Pod 调度到同一拓扑域(如日志收集器靠近应用) | +| `podAntiAffinity` | Pod → Pod | 将同组 Pod 分散到不同节点(高可用) | + +亲和性规则分为两种强度: +- **requiredDuringSchedulingIgnoredDuringExecution**:硬性要求,不满足则不调度。 +- **preferredDuringSchedulingIgnoredDuringExecution**:软性偏好,尽量满足。 + +> [!tip] "IgnoredDuringExecution" 的含义 +> Pod 一旦调度到某节点,即使该节点后来不再满足亲和性条件,也不会被驱逐(除非配置了 `requiredDuringScheduling` 节点亲和)。这是为了避免频繁迁移造成抖动。 + +#### Taint / Toleration + +- **Taint**:施加在 Node 上的排斥标记,格式 `key=value:effect`。 +- **Toleration**:Pod 上声明的容忍声明,允许 Pod 调度到带对应 Taint 的节点。 + +Effect 有三种: + +| Effect | 含义 | +|--------|------| +| `NoSchedule` | 不允许新 Pod 调度到该节点(已运行的不受影响) | +| `PreferNoSchedule` | 尽量不调度,但非强制 | +| `NoExecute` | 新 Pod 不调度 + 已运行且无 Toleration 的 Pod 会被驱逐 | + +> [!warning] Master 节点的 Taint +> 生产集群的 Master 节点通常带有 `node-role.kubernetes.io/control-plane:NoExecute` Taint,防止业务 Pod 消耗控制平面资源。如果需要在 Master 运行监控组件,需添加对应 Toleration。 + +#### PriorityClass + +PriorityClass 定义 Pod 的调度优先级。当集群资源不足时,低优先级 Pod 可能被**抢占(Preemption)**——即 Scheduler 驱逐低优先级 Pod 为高优先级 Pod 腾出空间。 + +```yaml +apiVersion: scheduling.k8s.io/v1 +kind: PriorityClass +metadata: + name: high-priority +value: 1000000 +globalDefault: false +description: "用于关键业务 Pod" +``` + +> [!info] 调度框架(Scheduling Framework) +> Kubernetes 1.19+ 引入 Scheduling Framework,将调度过程拆分为多个扩展点(QueueSort、PreFilter、Filter、PostFilter、Score、Reserve、Permit、Bind、PostBind),插件可以在任意扩展点介入,极大提升了调度器的可扩展性。 + +--- + +### Controller Manager(kube-controller-manager) + +Controller Manager 是一组**控制器(Controller)**的集合进程,每个控制器负责一种资源类型的持续协调。 + +#### 控制循环(Reconcile Loop)原理 + +所有控制器遵循同一个模式——**声明式控制循环**: + +```mermaid +graph LR + A[Observe: 观察当前状态] --> B[Diff: 比较期望状态与实际状态] + B --> C{存在差异?} + C -->|是| D[Act: 执行操作收敛差异] + D --> A + C -->|否| E[Wait: 等待下一次事件触发] + E --> A +``` + +这就是 Kubernetes 的核心哲学:**不要告诉系统"怎么做"(命令式),而是告诉它"要什么"(声明式),系统自行收敛到期望状态。** + +#### 常见控制器详解 + +##### Deployment Controller + +- 监听 Deployment 对象的变更。 +- 当 `.spec.replicas` 或 `.spec.template` 发生变化时,创建或更新对应的 **ReplicaSet**。 +- 支持 **Rolling Update**(滚动更新):创建新版 ReplicaSet 逐步扩容,同时缩减旧版。 +- 支持 **Rollback**:通过 `kubectl rollout undo` 切换回旧版 ReplicaSet。 + +**Deployment → ReplicaSet → Pod** 的层级关系: + +``` +Deployment (my-app) + ├── ReplicaSet (my-app-7d8b9f, revision 2) ← 当前版本 + │ ├── Pod-1 + │ ├── Pod-2 + │ └── Pod-3 + └── ReplicaSet (my-app-4a6c2e, revision 1) ← 旧版本(保留用于回滚) +``` + +##### ReplicaSet Controller + +- 确保指定数量的 Pod 副本始终运行。 +- 当 Pod 数量 < `replicas` 时创建新 Pod。 +- 当 Pod 数量 > `replicas` 时删除多余 Pod(优先删除处于异常状态的 Pod)。 +- 使用 **Label Selector** 匹配归属的 Pod。 + +##### Node Controller + +- 监控每个 Node 的健康状态(通过 kubelet 上报的 `NodeStatus`)。 +- 当 Node 的 `Ready` 条件变为 `Unknown`(默认 40 秒未收到心跳)时,标记为 `NodeNotReady`。 +- 经过 `--pod-eviction-timeout`(默认 5 分钟)后,驱逐该节点上的所有 Pod,触发 Scheduler 重新调度。 + +**Node 状态条件**: + +| Condition | 含义 | +|-----------|------| +| `Ready` | kubelet 心跳正常 | +| `MemoryPressure` | 节点内存不足 | +| `DiskPressure` | 节点磁盘不足 | +| `PIDPressure` | 节点进程数过多 | +| `NetworkUnavailable` | 网络插件未就绪 | + +##### Job Controller + +- 管理批处理任务的生命周期。 +- 确保 Job 中的 Pod 成功运行指定次数(`.spec.completions`)。 +- 支持三种模式:单 Pod Job、固定完成次数 Job、工作队列 Job。 +- 通过 `.spec.backoffLimit` 控制失败重试次数。 +- `ttlSecondsAfterFinished` 可在完成后自动清理 Job。 + +> [!tip] 控制器如何避免重复处理? +> Informer 维护一个本地缓存的 `resourceVersion`,控制器在处理完一个事件后将当前版本记录下来。下次事件如果版本相同则跳过。此外,控制器的 Worker Queue 会对同一资源进行去重合并。 + +--- + +## 数据平面(Data Plane) + +数据平面由集群中每个工作节点上的组件组成,负责 Pod 的实际创建、运行与网络通信。 + +### kubelet + +kubelet 是运行在每个节点上的 **Node Agent**,是控制平面与数据平面之间的桥梁。 + +#### Pod 生命周期管理 + +kubelet 监听 API Server 中分配到本节点的 Pod,按照以下流程管理: + +```mermaid +stateDiagram-v2 + [*] --> Pending: API Server 创建 Pod + Pending --> ContainerCreating: kubelet 接收 + ContainerCreating --> Running: 所有容器启动成功 + Running --> Succeeded: 所有容器正常退出(Exit 0) + Running --> Failed: 容器异常退出 + Running --> Running: 健康检查通过 + Failed --> Running: 重启策略触发(RestartPolicy) + Succeeded --> [*] + Failed --> [*]: 超过重启次数上限 +``` + +**Pod 阶段(Phase)**: + +| Phase | 含义 | +|-------|------| +| `Pending` | 已被 API Server 接受,但容器尚未全部创建 | +| `Running` | 至少一个容器正在运行 | +| `Succeeded` | 所有容器正常终止 | +| `Failed` | 至少一个容器异常终止 | +| `Unknown` | 无法获取 Pod 状态(通常是节点失联) | + +#### CRI 调用链 + +kubelet 通过 **CRI(Container Runtime Interface)** 与容器运行时交互。CRI 是基于 gRPC 的标准接口: + +``` +kubelet + → CRI Client(内置) + → Runtime Service(管理容器生命周期:RunPodSandbox、CreateContainer、StartContainer、StopContainer) + → Image Service(管理镜像:PullImage、ListImages、RemoveImage) + → containerd(主流实现) +``` + +> [!info] 为什么引入 CRI? +> Kubernetes 早期绑定 Docker,切换运行时需要重新编译 kubelet。CRI 作为抽象层,使得 kubelet 可以对接任何实现了 CRI 接口的运行时(containerd、CRI-O 等),实现了**运行时可插拔**。 + +#### 健康检查执行 + +kubelet 执行三类探针(Probe): + +| 探针类型 | 作用 | 失败后果 | +|----------|------|----------| +| **Liveness Probe** | 检测容器是否存活 | 重启容器 | +| **Readiness Probe** | 检测容器是否就绪接收流量 | 从 Service Endpoint 中移除 | +| **Startup Probe** | 检测容器是否启动完成 | 在 Startup Probe 成功前,Liveness/Readiness 不生效 | + +探针支持三种探测方式: + +- **httpGet**:发送 HTTP GET 请求,2xx/3xx 视为成功。 +- **tcpSocket**:尝试建立 TCP 连接,连接成功视为成功。 +- **exec**:在容器内执行命令,退出码为 0 视为成功。 + +> [!warning] 没有 Readiness Probe 的后果 +> 如果不配置 Readiness Probe,容器一启动就会被加入 Service Endpoint,可能在应用初始化完成前就收到流量,导致请求失败。生产环境中**强烈建议为每个 Pod 配置 Readiness Probe**。 + +--- + +### kube-proxy + +kube-proxy 运行在每个节点上,负责实现 **Service** 的网络转发逻辑。 + +#### Service 实现原理 + +Service 是 Kubernetes 提供的**服务发现与负载均衡**抽象。一个 Service 通过 Label Selector 匹配一组后端 Pod,并分配一个稳定的虚拟 IP(ClusterIP)。 + +当客户端访问 `ServiceIP:Port` 时,kube-proxy 负责将流量转发到后端 Pod。 + +#### iptables 模式 vs IPVS 模式 + +| 维度 | iptables 模式 | IPVS 模式 | +|------|---------------|-----------| +| **实现方式** | 使用 iptables 规则链做 DNAT | 使用内核 IPVS(IP Virtual Server)模块 | +| **规则数量** | 随 Service/Endpoint 数量线性增长 | 使用 Hash 表,复杂度 O(1) | +| **性能** | Service 数量 < 1000 时表现良好 | Service 数量 > 1000 时性能优势明显 | +| **负载均衡算法** | 随机(概率相等) | 支持多种:rr、lc、dh、sh、sed、nq | +| **连接追踪** | 依赖 conntrack 模块 | IPVS 自带连接追踪 | +| **会话保持** | 不支持 | 支持(Client-IP 会话亲和) | +| **内核要求** | 标准 Linux 内核 | 需要 IPVS 内核模块(`ip_vs`、`ip_vs_rr` 等) | +| **适用场景** | 中小规模集群 | 大规模集群(Service 数量多) | + +> [!tip] nftables 模式(Kubernetes 1.29+) +> Kubernetes 1.29 引入 nftables 代理模式作为 iptables 的替代。nftables 是 iptables 的继任者,提供更好的规则匹配性能和更简洁的语法。未来可能成为默认模式。 + +**iptables 模式的转发链路**: + +``` +Client → KUBE-SERVICES Chain + → KUBE-SVC-XXXX (匹配 Service ClusterIP:Port) + → KUBE-SEP-YYYY (DNAT 到后端 Pod IP:Port,随机选择) + → Pod +``` + +**IPVS 模式的转发链路**: + +``` +Client → IPVS Virtual Server (Service ClusterIP:Port) + → IPVS Real Server (后端 Pod IP:Port,按算法选择) + → Pod +``` + +> [!info] ClusterIP、NodePort、LoadBalancer 三种 Service 类型的关系 +> - **ClusterIP**:分配集群内部虚拟 IP,kube-proxy 通过 iptables/IPVS 实现转发。 +> - **NodePort**:在 ClusterIP 基础上,在每个节点开放一个端口(30000-32767),外部可通过 `NodeIP:NodePort` 访问。 +> - **LoadBalancer**:在 NodePort 基础上,调用云厂商 API 创建外部负载均衡器,将流量转发到 NodePort。 + +--- + +### Container Runtime(containerd) + +containerd 是目前 Kubernetes 最主流的容器运行时,从 Docker 中剥离出来的**工业级容器运行时**。 + +#### containerd 架构 + +```mermaid +graph TB + K[kubelet] -->|CRI gRPC| CRI[CRI Plugin] + CRI --> CT[Container Service] + CRI --> SS[Sandbox Service] + CRI --> IS[Image Service] + CT --> T[Task Manager] + T --> R[runc / youki] + IS --> CS[Content Store] + SS --> NS[Network Sandbox] +``` + +containerd 的核心模块: + +| 模块 | 职责 | +|------|------| +| **CRI Plugin** | 实现 CRI gRPC 接口,翻译 kubelet 请求 | +| **Container Service** | 管理容器的元数据(创建、删除、查询) | +| **Task Manager** | 管理容器进程的生命周期(start、stop、pause) | +| **Content Store** | 管理镜像层(layer)的存储,支持 content-addressable 存储 | +| **Snapshotter** | 管理容器文件系统快照(overlayfs、btrfs 等) | +| **Events** | 发布容器生命周期事件 | + +#### CRI 接口规范 + +CRI 定义了两组 gRPC Service: + +1. **RuntimeService**:管理 Pod Sandbox 和容器的生命周期。 + +```protobuf +service RuntimeService { + rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse); + rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse); + rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse); + rpc StartContainer(StartContainerRequest) returns (StartContainerResponse); + rpc StopContainer(StopContainerRequest) returns (StopContainerResponse); + rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse); + rpc ListContainers(ListContainersRequest) returns (ListContainersResponse); + rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse); + // ... +} +``` + +2. **ImageService**:管理镜像的拉取、查询与删除。 + +```protobuf +service ImageService { + rpc PullImage(PullImageRequest) returns (PullImageResponse); + rpc ListImages(ListImagesRequest) returns (ListImagesResponse); + rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse); + rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse); +} +``` + +> [!info] Pod Sandbox 是什么? +> Pod Sandbox 是 CRI 中对 Pod 级别资源的抽象,包括网络命名空间、PID 命名空间等共享资源。containerd 中对应的是 `pause` 容器(也叫 infra 容器),它的唯一作用是持有命名空间,业务容器通过加入这些命名空间实现网络共享。 + +--- + +## 请求流转全过程 + +从 `kubectl apply -f deployment.yaml` 到 Pod 变为 `Running` 状态,整个请求经过以下完整路径: + +```mermaid +sequenceDiagram + actor User as 用户 + participant kubectl as kubectl + participant API as API Server + participant etcd as etcd + participant DC as Deployment Controller + participant RSC as ReplicaSet Controller + participant SC as Scheduler + participant KL as kubelet + participant CRI as containerd + participant Pod as Pod + + User->>kubectl: kubectl apply -f deploy.yaml + kubectl->>kubectl: 本地 YAML 校验 + kubectl->>API: POST /apis/apps/v1/namespaces/{ns}/deployments + + Note over API: Authentication (证书认证) + Note over API: Authorization (RBAC 鉴权) + Note over API: Admission Control (Mutating + Validating) + + API->>etcd: 写入 Deployment 对象 + etcd-->>API: OK + + Note over API: 返回 201 Created 给 kubectl + API-->>kubectl: Deployment created + kubectl-->>User: deployment.apps/xxx created + + Note over DC: Deployment Controller 通过 Watch 感知变更 + + DC->>API: 创建 ReplicaSet + API->>etcd: 写入 ReplicaSet 对象 + etcd-->>API: OK + + Note over RSC: ReplicaSet Controller 通过 Watch 感知变更 + + loop 创建 3 个 Pod (replicas=3) + RSC->>API: 创建 Pod (spec.nodeName 为空) + API->>etcd: 写入 Pod 对象 + etcd-->>API: OK + end + + Note over SC: Scheduler 通过 Watch 感知未调度的 Pod + + loop 每个 Pod + SC->>SC: Filter: 过滤不满足条件的节点 + SC->>SC: Score: 对候选节点打分 + SC->>API: Bind Pod to Node + API->>etcd: 更新 Pod.spec.nodeName + etcd-->>API: OK + end + + Note over KL: kubelet 通过 Watch 感知分配到本节点的 Pod + + KL->>CRI: RunPodSandbox (创建 pause 容器) + CRI-->>KL: Sandbox ID + + KL->>CRI: CreateContainer (创建业务容器) + CRI-->>KL: Container ID + + KL->>CRI: StartContainer (启动业务容器) + CRI->>Pod: 容器进程启动 + CRI-->>KL: OK + + KL->>API: 更新 Pod Status 为 Running + API->>etcd: 更新 Pod Status + etcd-->>API: OK +``` + +### 关键阶段说明 + +1. **kubectl 阶段**:kubectl 做本地 YAML 解析与校验,然后通过 REST API 发送给 API Server。 +2. **API Server 阶段**:经过完整的认证→授权→准入链后,将对象持久化到 etcd。 +3. **Deployment Controller 阶段**:Watch 到 Deployment 变更,创建对应的 ReplicaSet。 +4. **ReplicaSet Controller 阶段**:Watch 到 ReplicaSet 变更,创建指定数量的 Pod 对象(此时 Pod 尚未绑定节点)。 +5. **Scheduler 阶段**:Watch 到未调度的 Pod,执行 Filter + Score 两阶段调度,将结果写入 `Pod.spec.nodeName`。 +6. **kubelet 阶段**:目标节点的 kubelet Watch 到分配给自己的 Pod,通过 CRI 接口调用 containerd 创建和启动容器。 +7. **状态更新**:kubelet 将 Pod 状态上报给 API Server,完成整个流程。 + +> [!tip] 整个过程大约需要几秒到几十秒 +> 时间取决于镜像大小、节点资源状况、网络速度等因素。可以通过 `kubectl describe pod` 查看每个阶段的时间戳,定位瓶颈所在。 + +--- + +## 高可用架构 + +生产环境需要部署多 Master 节点以消除单点故障。 + +### 多 Master 部署 + +```mermaid +graph TB + LB[负载均衡器
如 HAProxy / 云 LB] + + subgraph Master 集群 + M1[Master 1
API Server + Scheduler + Controller Manager] + M2[Master 2
API Server + Scheduler + Controller Manager] + M3[Master 3
API Server + Scheduler + Controller Manager] + end + + subgraph etcd 集群 + E1[etcd 1] + E2[etcd 2] + E3[etcd 3] + end + + subgraph Worker 集群 + W1[Node 1] + W2[Node 2] + W3[Node 3] + end + + LB --> M1 + LB --> M2 + LB --> M3 + + M1 --- E1 + M2 --- E2 + M3 --- E3 + + E1 --- E2 + E2 --- E3 + + W1 --> LB + W2 --> LB + W3 --> LB +``` + +### 组件高可用机制 + +| 组件 | 高可用机制 | 说明 | +|------|-----------|------| +| **API Server** | 多实例 + 负载均衡 | 无状态组件,可水平扩展 | +| **etcd** | Raft 共识协议 | 3/5 节点集群,容忍 (n-1)/2 故障 | +| **Scheduler** | Leader Election | 多实例运行,仅 Leader 执行调度 | +| **Controller Manager** | Leader Election | 多实例运行,仅 Leader 执行控制循环 | + +> [!info] Leader Election 机制 +> Scheduler 和 Controller Manager 使用 Kubernetes Lease 对象实现 Leader Election。每个实例启动时尝试获取 Lease,获取成功者成为 Leader 并定期续租。Leader 故障后,其他实例在 Lease 过期后重新竞选。 + +### etcd 集群拓扑选择 + +**堆叠式(Stacked)拓扑**: + +- etcd 与 Master 组件部署在同一节点。 +- 优点:部署简单,节点数少。 +- 缺点:Master 节点故障同时丢失 etcd 成员,可靠性较低。 + +**外部式(External)etcd 拓扑**: + +- etcd 独立部署在专用节点上。 +- 优点:Master 故障不影响 etcd 集群,可靠性更高。 +- 缺点:节点数更多,运维复杂度增加。 + +| 维度 | 堆叠式 | 外部式 | +|------|--------|--------| +| 节点数 | 3 Master = 3 etcd | 3 Master + 3 etcd = 6 | +| 容错能力 | 1 节点故障 | 1 Master 故障 + 1 etcd 故障 | +| 部署复杂度 | 低 | 高 | +| 适用场景 | 测试/小规模生产 | 大规模生产 | + +### 负载均衡方案 + +- **硬件 LB**:F5 等(传统数据中心)。 +- **软件 LB**:HAProxy + Keepalived(自建集群)、Nginx(四层代理)。 +- **云厂商 LB**:AWS NLB、阿里云 SLB、GCP Load Balancer(托管集群通常内置)。 + +> [!warning] API Server 的负载均衡是四层(TCP),不是七层(HTTP) +> 因为 API Server 使用 HTTP/2 + gRPC,七层负载均衡可能破坏长连接和流式请求。应使用 L4 负载均衡(如 HAProxy TCP mode、NLB)。 + +--- + +## 常见陷阱与最佳实践 + +### 陷阱 + +1. **etcd 磁盘性能不足导致集群卡顿** + - etcd 是延迟敏感型应用,磁盘 I/O 慢会导致 Leader 选举频繁、API Server 超时。 + - 解决:使用 SSD,监控 `etcd_disk_wal_fsync_duration_seconds` 指标。 + +2. **RBAC 权限配置不当** + - 过于宽松的 ClusterRole 绑定带来安全风险;过于严格导致组件无法工作。 + - 解决:遵循最小权限原则,使用 `kubectl auth can-i` 验证权限。 + +3. **未配置 Pod 资源请求(requests/limits)** + - 没有 requests,Scheduler 无法做出合理的调度决策。 + - 没有 limits,单个 Pod 可能耗尽节点资源,影响其他 Pod。 + - 解决:始终为容器设置 `resources.requests` 和 `resources.limits`。 + +4. **kubelet 证书过期** + - kubeadm 默认签发的证书有效期为 1 年。 + - 解决:设置证书自动轮换(`--rotate-certificates`),或使用外部 CA 签发长周期证书。 + +5. **使用 `latest` 镜像标签** + - `latest` 标签不可追溯,回滚时无法确定之前的版本。 + - 解决:始终使用明确的版本标签(如 `v1.2.3`)或镜像摘要(sha256)。 + +### 最佳实践 + +1. **控制平面组件监控** + - 监控 API Server 的请求延迟、错误率(`apiserver_request_duration_seconds`)。 + - 监控 etcd 的 Leader 切换次数、提案提交延迟。 + - 监控 Scheduler 的调度延迟与 Pending Pod 数量。 + +2. **分离 Master 与 Worker 节点** + - Master 节点不运行业务 Pod(通过 Taint 机制)。 + - 避免业务负载影响控制平面稳定性。 + +3. **etcd 定期备份** + - 至少每日一次快照,保留 7 天。 + - 备份存储在独立于集群的存储位置。 + - 定期演练恢复流程。 + +4. **合理设置 Pod Disruption Budget (PDB)** + - 在节点维护(drain)时,PDB 保证最少可用 Pod 数量。 + - 示例:`minAvailable: 2` 确保滚动维护时至少 2 个 Pod 运行。 + +5. **使用 Pod Priority and Preemption** + - 关键系统组件(如 CoreDNS、Ingress Controller)设置高优先级。 + - 避免资源紧张时关键组件被驱逐。 + +--- + +## 延伸阅读 + +- [[k8s-01-overview]] — Kubernetes 基础概念概览 +- Kubernetes 官方文档 - Architecture: https://kubernetes.io/docs/concepts/overview/components/ +- etcd 官方文档: https://etcd.io/docs/ +- Kubernetes Scheduler 源码: https://github.com/kubernetes/kubernetes/tree/master/pkg/scheduler +- containerd 官方文档: https://containerd.io/docs/ +- 《Kubernetes in Action》第二版 — Marko Lukša +- 《Programming Kubernetes》— Michael Hausenblas, Stefan Schimanski diff --git a/technical/k8s/k8s-03-pod.md b/technical/k8s/k8s-03-pod.md new file mode 100644 index 0000000..4253962 --- /dev/null +++ b/technical/k8s/k8s-03-pod.md @@ -0,0 +1,725 @@ +--- +tags: [k8s, pod, devops] +create time: 2026-07-07 15:48 +--- + +# Pod — Kubernetes 最小调度单元 + +## 概述 + +Pod 是 Kubernetes 中**最小的可部署和调度单元**。它不是一个容器,而是一组(一个或多个)共享网络和存储资源的容器的逻辑封装。为什么 Kubernetes 不直接调度容器,而是引入了 Pod 这一抽象层?因为在实际业务中,多个紧密协作的进程往往需要共享同一个 IP 地址、端口空间和卷挂载——比如一个 Web 应用容器和一个负责收集日志的 sidecar 容器。Pod 正是为了解决这种「亲密耦合进程协同运行」的需求而设计的。理解 Pod,是掌握 Kubernetes 一切上层抽象(Deployment、StatefulSet、DaemonSet 等)的基础。 + +## Pod 的本质 + +### 共享网络命名空间 + +同一个 Pod 内的所有容器共享**同一个网络命名空间(Network Namespace)**,这意味着: + +- 它们拥有**相同的 IP 地址** +- 它们可以**通过 localhost 互相通信** +- 端口空间是共享的,因此同一 Pod 内的容器**不能绑定相同的端口** + +> [!question] 为什么不直接调度容器? +> 因为单个容器通常只运行一个进程(这是容器设计的最佳实践)。但在很多场景下,多个进程需要紧密协作——比如 Nginx + 日志收集器、主应用 + 配置热加载 sidecar。如果让调度器去理解「哪些容器应该放在一起」,复杂度会急剧上升。Pod 将这个决策交给了用户:你来决定哪些容器属于同一个 Pod。 + +### Pause 容器 — Pod 的「灵魂」 + +每个 Kubernetes Pod 启动时,都会先创建一个特殊的**Pause 容器**(也叫 infra 容器),它是 Pod 的第一个容器,也是整个 Pod 的「根进程」。其他业务容器通过加入 Pause 容器的 Network Namespace 和 PID Namespace 来实现共享。 + +```mermaid +graph TB + subgraph Pod + P["Pause 容器
(infra container)"] + C1["业务容器 A"] + C2["业务容器 B
(sidecar)"] + C3["业务容器 C"] + end + + P -->|"提供 Network NS"| C1 + P -->|"提供 Network NS"| C2 + P -->|"提供 Network NS"| C3 + P -->|"提供 PID NS (可选)"| C1 + P -->|"提供 PID NS (可选)"| C2 + P -->|"提供 PID NS (可选)"| C3 +``` + +Pause 容器的职责非常简单: + +1. **持有 Network Namespace** — 确保 Pod 的 IP 地址在所有容器生命周期内保持不变 +2. **充当 PID 1** — 回收僵尸进程(zombie reaping) +3. **极轻量** — 通常只有几百 KB,使用 `registry.k8s.io/pause` 镜像 + +> [!info] Pause 容器的代码只有几百行 +> 它的核心逻辑就是一个无限循环的 `pause()` 系统调用。正因为足够简单,它几乎不会出问题,可以稳定地为 Pod 内所有容器提供基础运行环境。 + +### 容器与 Pod 的关系(1:N) + +一个 Pod 可以包含多个容器,典型模式如下: + +| 模式 | 说明 | 示例 | +|------|------|------| +| 单容器 Pod | 最常见的场景,一个 Pod 运行一个应用容器 | 一个 Go Web 服务 | +| 多容器 Pod (sidecar) | 主容器 + 辅助容器,辅助容器扩展主容器功能 | Nginx + Filebeat 日志收集 | +| 多容器 Pod (adapter) | 辅助容器将主容器的输出转换为标准格式 | 应用日志 → Fluentd 标准格式 | +| 多容器 Pod (ambassador) | 辅助容器代理网络请求 | 应用 → Envoy Proxy → 外部服务 | + +```yaml +# 一个多容器 Pod 的简单示例:Web 服务 + 日志收集 sidecar +apiVersion: v1 +kind: Pod +metadata: + name: web-with-logging +spec: + containers: + - name: web + image: nginx:1.25 + ports: + - containerPort: 80 + volumeMounts: + - name: log-volume + mountPath: /var/log/nginx + - name: log-collector + image: fluent/fluent-bit:latest + volumeMounts: + - name: log-volume + mountPath: /var/log/nginx + readOnly: true + volumes: + - name: log-volume + emptyDir: {} +``` + +两个容器共享 `log-volume` 卷,Web 容器写入日志,Fluent Bit 容器读取并转发——这是经典的日志收集 sidecar 模式。 + +## Pod 生命周期 + +Pod 的生命周期由**阶段(Phase)**和**条件(Condition)**共同描述。从创建到销毁,Pod 经历以下主要阶段: + +```mermaid +stateDiagram-v2 + [*] --> Pending: kubectl apply / API 创建 + Pending --> Running: 至少一个容器启动成功 + Pending --> Failed: 拉取镜像失败 / 调度失败 + Running --> Succeeded: 所有容器正常退出(exit 0) + Running --> Failed: 容器异常退出 / OOMKilled + Running --> Unknown: Node 失联, 无法获取状态 + Succeeded --> [*] + Failed --> [*] + Unknown --> Running: Node 恢复通信 + Unknown --> Failed: 确认 Pod 已丢失 +``` + +### Init Containers + +Init Containers 是在**主容器启动之前**按顺序运行的容器。它们主要用于完成一些前置条件的检查和准备工作。 + +**与主容器的区别:** + +| 特性 | Init Container | 主容器 (App Container) | +|------|---------------|----------------------| +| 执行顺序 | 严格按顺序依次执行 | 并行启动 | +| 失败行为 | 失败后 Pod 重启(遵循 restartPolicy) | 失败后根据探针配置重启 | +| 探针支持 | 不支持 liveness/readiness probe | 支持 | +| 用途 | 前置条件检查、等待依赖、初始化配置 | 运行业务逻辑 | +| 资源计算 | 独立于主容器的资源请求 | 作为整体调度依据 | + +> [!tip] 何时使用 Init Container? +> 当你需要在主容器启动前完成某些「只执行一次」的任务时——比如等待数据库就绪、下载配置文件、执行数据库迁移等——Init Container 是最佳选择。 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-with-init +spec: + initContainers: + # 第一个 Init Container:等待数据库就绪 + - name: wait-for-db + image: busybox:1.36 + command: + - sh + - -c + - | + until nc -z mysql-service 3306; do + echo "Waiting for MySQL..." + sleep 2 + done + echo "MySQL is ready!" + # 第二个 Init Container:执行数据库迁移 + - name: db-migration + image: myapp/migration:v1.2 + env: + - name: DB_HOST + value: "mysql-service" + containers: + - name: app + image: myapp/api:v1.2 + ports: + - containerPort: 8080 +``` + +上述示例中,两个 Init Container **严格按顺序**执行:先等 MySQL 可达,再执行数据库迁移。只有两者都成功(exit 0)后,主容器 `app` 才会启动。 + +### 容器探针 + +Kubernetes 提供三种探针来监控容器的健康状态: + +| 探针类型 | 作用 | 失败后果 | 典型场景 | +|---------|------|---------|---------| +| `livenessProbe` | 检测容器是否**存活** | 杀死容器并重启 | 进程死锁、无限循环 | +| `readinessProbe` | 检测容器是否**就绪**接收流量 | 从 Service 的 Endpoints 中移除 | 启动预热、临时过载 | +| `startupProbe` | 检测容器是否**启动完成** | 杀死容器并重启 | 启动耗时长的应用(如 Java) | + +> [!warning] startupProbe 与 livenessProbe 的关系 +> 当配置了 `startupProbe` 时,在它成功之前,`livenessProbe` 和 `readinessProbe` 都不会执行。这是为了防止启动时间较长的应用被误判为不健康。`startupProbe` 的 `failureThreshold * periodSeconds` 应该大于应用的最大启动时间。 + +**探针支持的检测方式:** + +- **httpGet** — 发送 HTTP GET 请求,2xx/3xx 视为成功 +- **tcpSocket** — 尝试 TCP 连接,端口可达视为成功 +- **exec** — 执行命令,exit code 0 视为成功 +- **grpc** — gRPC 健康检查协议(Kubernetes 1.24+) + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: probes-demo +spec: + containers: + - name: app + image: myapp:v1 + ports: + - containerPort: 8080 + + # 启动探针:最多给 60 秒启动时间 (10 次 x 6 秒) + startupProbe: + httpGet: + path: /healthz + port: 8080 + failureThreshold: 10 + periodSeconds: 6 + + # 存活探针:每 15 秒检测一次,连续 3 次失败则重启 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 0 # startupProbe 成功后才开始 + periodSeconds: 15 + failureThreshold: 3 + timeoutSeconds: 5 + + # 就绪探针:每 5 秒检测一次,失败则从 Endpoints 移除 + readinessProbe: + httpGet: + path: /ready + port: 8080 + periodSeconds: 5 + failureThreshold: 3 + timeoutSeconds: 3 +``` + +> [!tip] 最佳实践 +> 生产环境中建议同时配置三种探针:`startupProbe` 保护慢启动应用、`livenessProbe` 检测死锁、`readinessProbe` 控制流量切换。`readinessProbe` 的检测频率可以比 `livenessProbe` 更高,因为它的失败不会触发重启。 + +### Pod 阶段(Phase)与条件(Condition) + +**Pod Phase** 是对 Pod 在其生命周期中所处位置的高层概括: + +| Phase | 说明 | +|-------|------| +| `Pending` | Pod 已被 API Server 接受,但尚未调度或容器镜像未拉取完成 | +| `Running` | Pod 已绑定到节点,且至少有一个容器处于运行状态 | +| `Succeeded` | 所有容器正常退出(exit 0),不会再重启 | +| `Failed` | 所有容器已终止,且至少有一个容器是非正常退出 | +| `Unknown` | 无法获取 Pod 状态,通常是与 Node 的通信中断 | + +**Pod Conditions** 提供了更细粒度的状态信息,是一个数组,每个 Condition 包含以下字段: + +```yaml +conditions: + - type: PodScheduled # 条件类型 + status: "True" # True / False / Unknown + lastProbeTime: null # 上次探测时间 + lastTransitionTime: "2026-07-07T07:00:00Z" # 上次状态转换时间 + reason: "SchedulerCompletedSchedulingReason" + message: "Successfully assigned default/my-pod to node-1" +``` + +常见的 Condition Type: + +| Type | 说明 | +|------|------| +| `PodScheduled` | Pod 已被调度到某个节点 | +| `Initialized` | 所有 Init Container 已成功完成 | +| `ContainersReady` | 所有容器都已就绪 | +| `Ready` | Pod 可以对外提供服务,被纳入 Service Endpoints | + +```mermaid +graph LR + A["PodScheduled
status: True"] --> B["Initialized
status: True"] + B --> C["ContainersReady
status: True"] + C --> D["Ready
status: True"] +``` + +> [!info] Condition vs Phase +> Phase 是一个全局性的、互斥的状态值(一个 Pod 在同一时刻只有一个 Phase)。而 Conditions 是一组独立的布尔标志,一个 Pod 可以同时有多个 Condition 为 True。Conditions 通常用于精确排查 Pod 启动过程中的具体卡点。 + +## 资源管理 + +### Requests 与 Limits + +Kubernetes 通过 `requests` 和 `limits` 两个维度来管理容器的资源: + +- **requests(请求量)** — 调度器依据此值决定 Pod 放到哪个节点。是容器运行所需的**最小**资源量 +- **limits(限制量)** — 容器可使用的**最大**资源量。超出限制时的行为取决于资源类型 + +```yaml +resources: + requests: + cpu: "250m" # 0.25 核 + memory: "128Mi" # 128 MiB + limits: + cpu: "500m" # 0.5 核 + memory: "256Mi" # 256 MiB +``` + +> [!question] CPU 超限和 Memory 超限的处理方式为什么不同? +> CPU 是**可压缩资源(compressible)**——超出 limit 时会被节流(throttle),进程不会被杀死。而内存是**不可压缩资源(incompressible)**——超出 limit 时进程会被 OOMKilled。这是因为 CPU 可以通过时间片轮转来降级服务,但内存一旦不足,系统无法「回收」已分配的内存页面。 + +### QoS(Quality of Service)三级分类 + +Kubernetes 根据 requests 和 limits 的配置将 Pod 分为三个 QoS 等级: + +| QoS 等级 | 条件 | OOM 优先级 | 调度优先级 | +|----------|------|-----------|-----------| +| **Guaranteed** | 每个容器的 requests == limits | 最低(最后被杀) | 最高 | +| **Burstable** | 至少一个容器设置了 requests,且 requests < limits | 中等 | 中等 | +| **BestEffort** | 所有容器均未设置 requests 和 limits | 最高(最先被杀) | 最低 | + +```yaml +# Guaranteed — requests 等于 limits +containers: + - name: critical-app + resources: + requests: + cpu: "1" + memory: "1Gi" + limits: + cpu: "1" + memory: "1Gi" + +--- +# Burstable — requests 小于 limits +containers: + - name: normal-app + resources: + requests: + cpu: "250m" + memory: "128Mi" + limits: + cpu: "1" + memory: "512Mi" + +--- +# BestEffort — 未设置任何 requests / limits +containers: + - name: batch-job + image: busybox + # 未配置 resources 字段 +``` + +> [!warning] 生产环境严禁 BestEffort +> BestEffort Pod 在节点资源紧张时会被**最先驱逐**。在生产环境中,至少应为所有容器设置合理的 `requests`。 + +### OOM(Out of Memory)行为 + +当节点内存不足时,kubelet 会按照以下顺序选择 Pod 进行驱逐: + +1. **BestEffort** Pod(OOM Score 最高) +2. **Burstable** Pod 中,实际内存使用量超过 requests 最多的 +3. **Guaranteed** Pod(OOM Score 最低,最后被杀) + +同时,Linux 内核的 OOM Killer 也会根据进程的 `oom_score_adj` 值来决定杀哪个进程。Kubernetes 为不同 QoS 等级的容器设置了不同的 `oom_score_adj`: + +| QoS 等级 | oom_score_adj | +|----------|--------------| +| Guaranteed | -998 | +| Burstable | 2 ~ 1000(按比例) | +| BestEffort | 1000 | + +### CPU 节流机制 + +当容器的 CPU 使用超过 limit 时,Linux 内核的 CFS(Completely Fair Scheduler)会通过 **CFS Bandwidth Throttling** 机制限制容器的 CPU 时间片分配。 + +表现症状: +- 容器进程运行变慢,响应延迟升高 +- `container_cpu_cfs_throttled_periods_total` 指标持续增长 +- 应用程序的 P99 延迟出现周期性毛刺 + +> [!tip] 避免 CPU 节流 +> 如果发现应用延迟升高且 CPU throttling 严重,考虑:① 提高 CPU limit;② 将 limit 设置为与 request 相同(Guaranteed QoS,不会被节流但调度更严格);③ 使用 `CPU Manager Policy: static` 获得独占 CPU 核心。 + +## Sidecar 模式 + +Sidecar 模式是 Pod 多容器设计中最常用的架构模式。通过在 Pod 中添加辅助容器(sidecar),可以在不修改主应用代码的前提下扩展其功能。 + +### 日志收集 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-logging-sidecar +spec: + containers: + - name: app + image: myapp:v2 + volumeMounts: + - name: shared-logs + mountPath: /var/log/app + - name: log-shipper + image: fluent/fluent-bit:2.2 + volumeMounts: + - name: shared-logs + mountPath: /var/log/app + readOnly: true + - name: fluentbit-config + mountPath: /fluent-bit/etc/ + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "200m" + memory: "128Mi" + volumes: + - name: shared-logs + emptyDir: {} + - name: fluentbit-config + configMap: + name: fluentbit-config +``` + +### 服务网格代理(Service Mesh Proxy) + +这是 Istio、Linkerd 等服务网格的典型数据面模式——每个应用 Pod 旁注入一个 Envoy sidecar 代理: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-with-proxy + annotations: + sidecar.istio.io/inject: "true" +spec: + containers: + - name: app + image: myapp:v3 + ports: + - containerPort: 8080 + # 以下通常由 Istio 自动注入,这里手动展示 + - name: istio-proxy + image: envoyproxy/envoy:v1.28 + args: + - proxy + - sidecar + - --domain + - $(POD_NAMESPACE).svc.cluster.local + ports: + - containerPort: 15001 # Envoy 入口 + - containerPort: 15006 # Envoy 出口 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace +``` + +### 配置热更新(Reloader Sidecar) + +某些应用不支持热加载配置文件,此时可以用一个 sidecar 监听 ConfigMap 变化并通知主容器: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-config-reload +spec: + containers: + - name: app + image: nginx:1.25 + volumeMounts: + - name: config-volume + mountPath: /etc/nginx/conf.d + - name: config-reloader + image: jimmidyson/configmap-reload:v0.13 + args: + - --volume-dir=/config + - --webhook-url=http://localhost:8080/-/reload + volumeMounts: + - name: config-volume + mountPath: /config + readOnly: true + volumes: + - name: config-volume + configMap: + name: nginx-config +``` + +## Pod 调度基础 + +Kubernetes 调度器(kube-scheduler)负责将 Pod 分配到合适的节点上。Pod 本身可以通过以下字段影响调度决策: + +### nodeSelector + +最简单的调度约束,通过标签匹配将 Pod 调度到特定节点: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: gpu-task +spec: + nodeSelector: + gpu: "true" # 必须调度到带有 gpu=true 标签的节点 + disktype: "ssd" + containers: + - name: ml-trainer + image: pytorch:v2 +``` + +### nodeName + +直接指定节点名称,**跳过调度器**: + +```yaml +spec: + nodeName: node-3 # 直接绑定到 node-3,不经过调度器 +``` + +> [!warning] 慎用 nodeName +> 使用 `nodeName` 会绕过调度器的资源检查、亲和性计算和污点容忍判断。如果目标节点资源不足,Pod 会一直处于 Pending 状态。一般仅用于测试或特殊运维场景。 + +### 亲和性(Affinity)简介 + +亲和性提供了比 `nodeSelector` 更丰富的调度规则: + +- **nodeAffinity** — 节点亲和性,支持硬性(required)和软性(preferred)规则 +- **podAffinity** — Pod 亲和性,将 Pod 调度到与其他 Pod 相近的位置 +- **podAntiAffinity** — Pod 反亲和性,将 Pod 调度到与其他 Pod 远离的位置 + +```yaml +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: # 硬性要求 + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + preferredDuringSchedulingIgnoredDuringExecution: # 软性偏好 + - weight: 80 + preference: + matchExpressions: + - key: zone + operator: In + values: + - cn-east-1a +``` + +> [!info] 深入阅读 +> 调度器的完整机制(包括 Taint/Toleration、PriorityClass、拓扑分布约束等)将在 [[k8s-scheduler]] 中详细展开。 + +## 静态 Pod(Static Pod) + +静态 Pod 是由 **kubelet 直接管理**的 Pod,不通过 API Server 创建。kubelet 会监视指定目录(默认为 `/etc/kubernetes/manifests/`)中的 YAML 文件,并自动创建和维护对应的 Pod。 + +### 特点 + +| 特性 | 静态 Pod | 普通 Pod | +|------|---------|---------| +| 管理者 | kubelet | API Server / Controller Manager | +| 存储位置 | 节点本地文件系统 | etcd | +| 是否出现在 API Server 中 | 是(kubelet 创建 mirror pod) | 是 | +| 是否受控制器管理 | 否 | 是(Deployment、StatefulSet 等) | +| 是否可被 kubectl 删除 | 否(需删除文件) | 是 | + +### 用途 + +静态 Pod 最重要的用途是**部署控制平面组件**: + +```bash +# kubeadm 部署的集群中,控制平面组件都是静态 Pod +/etc/kubernetes/manifests/ +├── kube-apiserver.yaml +├── kube-controller-manager.yaml +├── kube-scheduler.yaml +└── etcd.yaml +``` + +```yaml +# /etc/kubernetes/manifests/kube-apiserver.yaml(简化示例) +apiVersion: v1 +kind: Pod +metadata: + name: kube-apiserver + namespace: kube-system + labels: + component: kube-apiserver +spec: + containers: + - name: kube-apiserver + image: registry.k8s.io/kube-apiserver:v1.30.0 + command: + - kube-apiserver + - --advertise-address=192.168.1.10 + - --etcd-servers=https://127.0.0.1:2379 + - --service-cluster-ip-range=10.96.0.0/12 + ports: + - containerPort: 6443 + hostNetwork: true +``` + +> [!info] 为什么控制平面用静态 Pod? +> 这是一个「鸡生蛋」的问题:kube-apiserver 是 Kubernetes 集群的核心,如果 kube-apiserver 本身也依赖 API Server 来创建,那集群就无法启动。静态 Pod 由 kubelet 独立管理,不依赖 API Server,因此可以用来部署 API Server 本身。 + +## 常见陷阱与最佳实践 + +### 镜像拉取策略(imagePullPolicy) + +| 策略 | 行为 | 适用场景 | +|------|------|---------| +| `Always` | 每次创建 Pod 都拉取镜像 | 使用 `:latest` 标签时(默认策略) | +| `IfNotPresent` | 本地不存在时才拉取 | 使用固定版本标签(如 `:v1.2.3`) | +| `Never` | 从不拉取,仅使用本地镜像 | 离线环境或测试场景 | + +```yaml +containers: + - name: app + image: myapp:v1.2.3 # 使用固定标签 + imagePullPolicy: IfNotPresent # 推荐:避免不必要的拉取 +``` + +> [!warning] :latest 标签陷阱 +> 当镜像标签为 `:latest` 或不指定标签时,`imagePullPolicy` 默认为 `Always`。这意味着每次 Pod 重建都会重新拉取镜像,带来不确定性。**生产环境必须使用固定版本标签**(如 `:v1.2.3` 或 SHA256 digest)。 + +### 优雅终止(Graceful Shutdown) + +当 Kubernetes 删除一个 Pod 时,会经历以下流程: + +```mermaid +sequenceDiagram + participant User as 用户/API + participant API as API Server + participant Kubelet as kubelet + participant App as 应用进程 + + User->>API: 删除 Pod + API->>Kubelet: 标记 Pod 为 Terminating + Note over API: 从 Service Endpoints 移除 + + Kubelet->>App: 执行 preStop hook + App-->>App: 执行清理逻辑 + App-->>Kubelet: preStop 完成 + + Kubelet->>App: 发送 SIGTERM 信号 + App-->>App: 开始优雅关闭 + + alt 在 terminationGracePeriodSeconds 内退出 + App-->>Kubelet: 进程正常退出 + else 超时未退出 + Kubelet->>App: 发送 SIGKILL 强制杀死 + end +``` + +**关键配置项:** + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: graceful-demo +spec: + terminationGracePeriodSeconds: 60 # 默认 30s,给足够时间清理 + containers: + - name: app + image: myapp:v1 + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - | + # 1. 通知应用停止接收新请求 + curl -s http://localhost:8080/admin/drain + # 2. 等待正在进行的请求处理完成 + sleep 15 + # 3. 关闭数据库连接池 + echo "Shutting down gracefully..." +``` + +> [!tip] preStop 的妙用 +> preStop hook 在 SIGTERM 之前执行,可以用来做「通知式关闭」——比如先从注册中心注销、停止接收新连接、等待进行中的请求完成。配合 `terminationGracePeriodSeconds` 调大值,可以实现零请求丢失的优雅关闭。 + +### PID 1 信号问题 + +在 Linux 中,PID 1 进程有特殊行为:**默认不响应 SIGTERM 信号**(为了兼容 init 系统)。如果容器的入口进程是 shell 脚本,shell 不会将 SIGTERM 转发给子进程,导致容器无法优雅退出。 + +**问题示例:** + +```dockerfile +# 有问题的 Dockerfile +FROM node:20 +COPY server.js . +CMD node server.js # node 进程成为 PID 1,但不一定正确处理信号 +``` + +**解决方案:** + +```dockerfile +# 方案 1:使用 exec 形式(推荐) +CMD ["node", "server.js"] # node 直接成为 PID 1 + +# 方案 2:使用 tini 作为 init 进程 +RUN apt-get update && apt-get install -y tini +ENTRYPOINT ["tini", "--"] +CMD ["node", "server.js"] + +# 方案 3:在 Pod Spec 中设置(Kubernetes 1.28+) +# 通过 feature gate: SidecarContainers +``` + +```yaml +# 方案 3:Pod 级别的 init 进程设置 +apiVersion: v1 +kind: Pod +metadata: + name: signal-demo +spec: + # Kubernetes 1.28+ 支持设置 Pod 的 PID namespace + # 确保 PID 1 被正确处理 + containers: + - name: app + image: myapp:v1 +``` + +### 其他注意事项 + +- **不要在同一 Pod 内运行多个不相关的服务** — 这违反了 Pod 的设计初衷,应该拆分为多个 Pod +- **使用 Liveness Probe 要谨慎** — 错误的 liveness 探针配置会导致容器反复重启(restart loop),建议先用 readiness 验证探针逻辑 +- **设置资源 requests** — 不设 requests 的 Pod 会变成 BestEffort QoS,在节点压力下最先被驱逐 +- **避免使用 hostNetwork** — 除非必要(如 CNI 插件、Ingress Controller),否则不要使用 hostNetwork,它会占用宿主机端口并绕过 Kubernetes 网络策略 + +## 延伸阅读 + +- [[k8s-02-architecture]] — Kubernetes 整体架构,理解 Pod 在集群中的位置 +- [[k8s-04-deployment-and-replicaset]] — Deployment 如何管理 Pod 的副本与滚动更新 +- [[k8s-scheduler]] — 调度器完整机制,包括亲和性、污点容忍、拓扑分布等 diff --git a/technical/k8s/k8s-04-deployment-and-replicaset.md b/technical/k8s/k8s-04-deployment-and-replicaset.md new file mode 100644 index 0000000..a0087b5 --- /dev/null +++ b/technical/k8s/k8s-04-deployment-and-replicaset.md @@ -0,0 +1,792 @@ +--- +tags: [k8s, deployment, replicaset, devops] +create time: 2026-07-07 15:48 +--- + +# Deployment 与 ReplicaSet + +## 概述 + +Deployment 是 Kubernetes 中最常用的无状态工作负载控制器,它在 ReplicaSet 之上提供了声明式更新、滚动发布、版本回滚等高级能力。日常开发中,绝大多数无状态服务(Web API、微服务后端、定时任务 Worker)都通过 Deployment 来管理 Pod 的生命周期。理解 Deployment 与 ReplicaSet 的协作关系,是掌握 Kubernetes 应用交付模型的基础。 + +## ReplicaSet 详解 + +### 什么是 ReplicaSet + +ReplicaSet(简称 RS)的核心职责只有一件事:**确保指定数量的 Pod 副本始终处于运行状态**。如果某个 Pod 意外退出或被删除,RS 会立即创建新的 Pod 来弥补差额;如果 Pod 数量超过期望值,RS 会删除多余的 Pod。 + +```yaml +# 一个独立的 ReplicaSet 示例(通常不直接创建,由 Deployment 自动管理) +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: nginx-rs + namespace: default +spec: + replicas: 3 # 期望副本数 + selector: + matchLabels: + app: nginx # Label Selector:通过标签匹配 Pod + template: + metadata: + labels: + app: nginx # Pod 必须携带此标签才能被 RS 管理 + spec: + containers: + - name: nginx + image: nginx:1.25 + ports: + - containerPort: 80 +``` + +### Label Selector 机制 + +RS 通过 **Label Selector** 来发现和管理 Pod,而非维护一个固定的 Pod 列表。这意味着: + +- 手动创建的 Pod 只要标签匹配,也会被 RS 纳入管理(数量超出则删除,不足则新建) +- 修改某个 Pod 的标签可以将其从 RS 管理中"摘除",这在调试时非常有用 + +```bash +# 将一个 Pod 从 RS 中移除(不删除 Pod,仅修改标签使其脱离管理) +kubectl label pod nginx-rs-xxxx app=detached --overwrite + +# 此时 RS 会发现少了一个副本,自动新建一个 Pod 来补足 +``` + +> [!tip] 调试技巧 +> 需要排查某个 Pod 问题时,先把它从 RS 中"摘出来"(修改标签),这样 RS 不会因为 Pod 异常而自动重建,你可以从容排查。 + +### ReplicaSet vs ReplicationController + +ReplicationController(RC)是 RS 的前身,两者核心区别在于 Selector 语义: + +| 对比维度 | ReplicationController | ReplicaSet | +|----------|----------------------|------------| +| Selector 类型 | 仅支持等式(`env=prod`) | 支持等式 + 集合(`env in (prod, staging)`) | +| 标签匹配灵活性 | 低 | 高 | +| 推荐程度 | 已废弃,不建议使用 | 推荐使用 | +| 滚动更新 | 需要 `kubectl rolling-update`(客户端执行) | 由 Deployment 在服务端协调 | + +RC 已经是遗留概念,新资源应统一使用 ReplicaSet。不过实践中你几乎不会直接创建 RS,而是通过 Deployment 间接管理。 + +## Deployment 核心机制 + +### 声明式管理 + +Deployment 采用声明式模型:你只需描述"期望状态"(desired state),Kubernetes 控制面会持续将其与"实际状态"(actual state)做对比,自动执行必要的操作来弥合差距。这个循环叫做 **Reconcile Loop**(调谐循环)。 + +```mermaid +graph LR + A["用户提交 Deployment
(desired state)"] --> B["Deployment Controller"] + B --> C{"desired == actual ?"} + C -->|No| D["创建/删除/更新
ReplicaSet / Pod"] + D --> B + C -->|Yes| E["状态稳定
不做任何操作"] +``` + +工作流程: + +1. 用户通过 `kubectl apply` 提交 Deployment YAML,声明 `replicas: 3`、`image: nginx:1.25` 等期望状态 +2. Deployment Controller 观察到当前没有匹配的 RS,创建一个新的 RS +3. RS Controller 观察到 Pod 数量为 0,创建 3 个 Pod +4. 持续监控:如果某个 Pod 崩溃,RS Controller 检测到 actual < desired,立即补充新 Pod + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deploy + namespace: default +spec: + replicas: 3 # 声明:我想要 3 个副本 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.25 # 声明:我想用这个镜像 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi +``` + +### 滚动更新(Rolling Update) + +滚动更新是 Deployment 最核心的价值。当你修改了 Pod 模板(比如升级镜像版本),Deployment 不会一次性杀掉所有旧 Pod,而是**逐批替换**,在更新过程中始终保持服务可用。 + +#### 关键参数 + +| 参数 | 默认值 | 含义 | +|------|--------|------| +| `maxSurge` | 25% | 更新过程中允许超出 `replicas` 的最大 Pod 数量 | +| `maxUnavailable` | 25% | 更新过程中允许不可用的最大 Pod 数量 | + +> [!info] 参数计算 +> 对于 `replicas: 4`、`maxSurge: 25%`、`maxUnavailable: 25%` 的配置: +> - maxSurge = ceil(4 * 0.25) = 1,最多可以有 5 个 Pod 同时存在 +> - maxUnavailable = floor(4 * 0.25) = 1,至少保证 3 个 Pod 可用 +> +> 这意味着每批替换 1 个 Pod,共需 4 批完成更新。 + +#### 滚动更新过程 + +```mermaid +sequenceDiagram + participant U as 用户 + participant D as Deployment Controller + participant RS1 as ReplicaSet v1 + participant RS2 as ReplicaSet v2 + + U->>D: kubectl apply(image: nginx:1.26) + D->>RS2: 创建新 RS(image: 1.26, replicas: 0) + D->>RS2: scale up +1(新建 1 个新 Pod) + Note over RS1,RS2: maxSurge=1, 新 Pod 启动 + D->>RS1: scale down -1(销毁 1 个旧 Pod) + Note over RS1,RS2: maxUnavailable=1, 保证可用 + D->>RS2: scale up +1 + D->>RS1: scale down -1 + Note over RS1,RS2: 重复直到 RS1=0, RS2=4 + D->>RS1: 保留 RS1(revision=1, 可回滚) +``` + +逐批替换的过程(以 `replicas: 4` 为例): + +| 阶段 | RS v1 (旧) | RS v2 (新) | 总 Pod 数 | 可用 Pod | +|------|-----------|-----------|----------|---------| +| 初始 | 4 | 0 | 4 | 4 | +| 第 1 批 | 4 → 3 | 0 → 1 | 5 | 4 | +| 第 2 批 | 3 → 2 | 1 → 2 | 4 | 4 | +| 第 3 批 | 2 → 1 | 2 → 3 | 4 | 4 | +| 第 4 批 | 1 → 0 | 3 → 4 | 4 | 4 | + +#### 滚动更新配置 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deploy +spec: + replicas: 4 + strategy: + type: RollingUpdate # 更新策略 + rollingUpdate: + maxSurge: 1 # 最多多 1 个 Pod + maxUnavailable: 1 # 最多少 1 个 Pod + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.25 + readinessProbe: # 重要:就绪探针决定新 Pod 何时接收流量 + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 +``` + +> [!warning] readinessProbe 的重要性 +> 滚动更新依赖 `readinessProbe` 判断新 Pod 是否就绪。如果没有配置就绪探针,新 Pod 一创建就被视为 Ready,可能导致流量打到尚未初始化完成的 Pod 上,造成请求失败。 + +### 回滚策略 + +Kubernetes 为每次 Deployment 变更自动记录 revision(版本号),支持快速回滚到任意历史版本。 + +```bash +# 查看 rollout 历史 +kubectl rollout history deployment nginx-deploy + +# 输出示例: +# REVISION CHANGE-CAUSE +# 1 +# 2 +# 3 + +# 查看某个 revision 的详细信息 +kubectl rollout history deployment nginx-deploy --revision=2 + +# 回滚到上一个版本 +kubectl rollout undo deployment nginx-deploy + +# 回滚到指定版本 +kubectl rollout undo deployment nginx-deploy --to-revision=1 + +# 查看 rollout 状态(是否完成、耗时等) +kubectl rollout status deployment nginx-deploy +``` + +#### revisionHistoryLimit 配置 + +默认保留 10 个历史 revision。过多的历史 RS 会占用 etcd 资源,过少则可能丢失回滚能力。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deploy +spec: + revisionHistoryLimit: 5 # 只保留最近 5 个 revision + # 设为 0 则禁用回滚(不推荐) + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + annotations: + kubernetes.io/change-cause: "升级到 nginx 1.26" # 记录变更原因,显示在 rollout history 中 + spec: + containers: + - name: nginx + image: nginx:1.26 +``` + +> [!tip] 记录变更原因 +> 在 Pod template 的 `annotations` 中添加 `kubernetes.io/change-cause`,这样 `kubectl rollout history` 会显示每次变更的原因,方便排查。 + +### 扩缩容 + +#### 手动扩缩容 + +```bash +# 命令行直接扩缩 +kubectl scale deployment nginx-deploy --replicas=5 + +# 条件扩缩(只有当前副本数为 3 时才执行) +kubectl scale deployment nginx-deploy --replicas=5 --current-replicas=3 +``` + +也可以直接修改 YAML 中的 `replicas` 字段后 `kubectl apply`。 + +#### HPA 自动扩缩 + +Horizontal Pod Autoscaler(HPA)根据 CPU、内存或自定义指标自动调整 Deployment 的副本数。 + +```yaml +# 前置条件:集群中需要安装 Metrics Server +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: nginx-hpa + namespace: default +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deploy # 目标 Deployment + minReplicas: 2 # 最小副本数 + maxReplicas: 10 # 最大副本数 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 # CPU 使用率超过 60% 时扩容 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 # 内存使用率超过 80% 时扩容 + behavior: + scaleUp: + stabilizationWindowSeconds: 60 # 扩容稳定窗口:60s 内取最大值 + policies: + - type: Pods + value: 4 # 每次最多扩 4 个 Pod + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 300 # 缩容稳定窗口:300s,防止抖动 + policies: + - type: Percent + value: 10 # 每次最多缩 10% + periodSeconds: 60 +``` + +```bash +# 创建 HPA +kubectl apply -f hpa.yaml + +# 或用命令行快速创建 +kubectl autoscale deployment nginx-deploy --cpu-percent=60 --min=2 --max=10 + +# 查看 HPA 状态 +kubectl get hpa nginx-hpa +``` + +> [!warning] HPA 与手动 scale 冲突 +> 如果同时使用 HPA 和手动 `kubectl scale`,HPA 会在下一次指标采集时覆盖手动设置。两者不应混用。 + +## Deployment 更新策略 + +Deployment 支持两种更新策略,通过 `spec.strategy.type` 指定: + +| 对比维度 | RollingUpdate | Recreate | +|----------|--------------|----------| +| 更新方式 | 逐批替换 Pod | 先杀全部旧 Pod,再创建全部新 Pod | +| 更新期间可用性 | 始终可用(前提:正确配置探针) | 有短暂不可用窗口 | +| 资源占用 | 可能临时需要更多资源(maxSurge > 0 时) | 只需要新版本的资源 | +| 适用场景 | 大多数无状态服务 | 不能有两个版本同时运行的场景 | +| 典型用例 | Web 服务、API 后端 | 涉及全局独占资源(如本地文件锁、全局数据库迁移) | + +```yaml +# Recreate 策略示例 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: legacy-app +spec: + replicas: 3 + strategy: + type: Recreate # 全量替换,更新期间服务不可用 + selector: + matchLabels: + app: legacy + template: + metadata: + labels: + app: legacy + spec: + containers: + - name: app + image: legacy-app:2.0 +``` + +> [!info] 什么时候用 Recreate? +> 当新旧版本不能共存时使用 Recreate,比如:应用使用了本地文件锁、需要执行不可逆的数据库 Schema 迁移、或者旧版本会干扰新版本的初始化。大多数情况下 RollingUpdate 是更好的选择。 + +## 金丝雀发布与蓝绿部署 + +Deployment 本身不直接支持金丝雀发布和蓝绿部署,但可以通过组合多个 Deployment + Service 来实现。 + +### 金丝雀发布(Canary Release) + +核心思路:同时运行两个版本的 Deployment,通过控制副本比例来控制流量分配。 + +```yaml +# 主 Deployment:稳定版本,占 90% 流量 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-stable +spec: + replicas: 9 # 9 个副本 + selector: + matchLabels: + app: myapp + track: stable + template: + metadata: + labels: + app: myapp + track: stable # 核心标签 app: myapp 相同,Service 可以选中 + spec: + containers: + - name: app + image: myapp:v1.0 +--- +# 金丝雀 Deployment:新版本,占 10% 流量 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-canary +spec: + replicas: 1 # 1 个副本 + selector: + matchLabels: + app: myapp + track: canary + template: + metadata: + labels: + app: myapp + track: canary + spec: + containers: + - name: app + image: myapp:v2.0 +``` + +```yaml +# Service 通过 app: myapp 同时选中两个 Deployment 的 Pod +apiVersion: v1 +kind: Service +metadata: + name: myapp-svc +spec: + selector: + app: myapp # 不指定 track,两个版本都会被选中 + ports: + - port: 80 + targetPort: 8080 +``` + +流量分配:9 个旧版本 Pod + 1 个新版本 Pod = 约 10% 流量到达新版本。验证无误后,逐步调整副本比例(如 `canary: 5, stable: 5`),最终全部切换到新版本。 + +```mermaid +graph LR + Client --> Service + Service -->|~90%| Pod1["Pod v1.0 x9"] + Service -->|~10%| Pod2["Pod v2.0 x1"] +``` + +### 蓝绿部署(Blue-Green Deployment) + +核心思路:同时运行蓝(当前版本)和绿(新版本)两套完整的 Deployment,通过切换 Service 的 selector 来瞬间完成流量切换。 + +```yaml +# 蓝色 Deployment(当前生产版本) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-blue +spec: + replicas: 3 + selector: + matchLabels: + app: myapp + version: blue + template: + metadata: + labels: + app: myapp + version: blue + spec: + containers: + - name: app + image: myapp:v1.0 +--- +# 绿色 Deployment(新版本,先部署但不接收流量) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-green +spec: + replicas: 3 + selector: + matchLabels: + app: myapp + version: green + template: + metadata: + labels: + app: myapp + version: green + spec: + containers: + - name: app + image: myapp:v2.0 +``` + +```yaml +# Service:切换 selector.version 即可瞬间切换流量 +apiVersion: v1 +kind: Service +metadata: + name: myapp-svc +spec: + selector: + app: myapp + version: blue # 当前指向蓝色,切换为 green 即完成上线 + ports: + - port: 80 + targetPort: 8080 +``` + +```bash +# 切换流量到绿色版本 +kubectl patch service myapp-svc -p '{"spec":{"selector":{"version":"green"}}}' + +# 回滚:切换回蓝色版本 +kubectl patch service myapp-svc -p '{"spec":{"selector":{"version":"blue"}}}' +``` + +蓝绿部署的优点是切换瞬间完成(无中间状态),缺点是需要双倍资源。适合对上线/回滚速度要求极高的场景。 + +## Pod 模板字段详解 + +`spec.template.spec` 是 Pod 模板的核心,以下是常用字段的逐一说明: + +```yaml +spec: + template: + metadata: + labels: # Pod 标签,用于 Service Selector 和 ReplicaSet 匹配 + app: myapp + version: v1 + annotations: # 非标识性元数据,供工具或控制器使用 + kubernetes.io/change-cause: "初始部署" + spec: + # ---- 容器定义 ---- + containers: + - name: app # 容器名称,Pod 内唯一 + image: myapp:v1.0 # 镜像地址 + imagePullPolicy: IfNotPresent # Always / IfNotPresent / Never + ports: + - containerPort: 8080 # 容器监听端口(声明性质,不强制) + env: # 环境变量 + - name: APP_ENV + value: "production" + - name: DB_PASSWORD + valueFrom: + secretKeyRef: # 从 Secret 读取敏感信息 + name: db-secret + key: password + resources: # 资源请求与限制 + requests: # 调度依据:节点至少需要这么多资源 + cpu: 100m + memory: 128Mi + limits: # 上限:超出会被 OOM Kill 或 CPU 限流 + cpu: 500m + memory: 256Mi + readinessProbe: # 就绪探针:决定 Pod 是否接收流量 + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: # 存活探针:失败则重启容器 + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 20 + startupProbe: # 启动探针:慢启动应用专用,通过前忽略 liveness + httpGet: + path: /healthz + port: 8080 + failureThreshold: 30 + periodSeconds: 10 + + # ---- 初始化容器 ---- + initContainers: # 在主容器启动前顺序执行 + - name: init-db-migration + image: myapp:v1.0 + command: ["./migrate.sh"] + + # ---- 卷挂载 ---- + volumes: + - name: config-vol + configMap: + name: app-config # 挂载 ConfigMap 作为文件 + - name: data-vol + emptyDir: {} # 临时卷,Pod 删除后数据丢失 + + # ---- 调度相关 ---- + nodeSelector: # 简单节点选择 + disk: ssd + affinity: # 高级亲和性规则 + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/os + operator: In + values: ["linux"] + tolerations: # 容忍污点 + - key: "dedicated" + operator: "Equal" + value: "gpu" + effect: "NoSchedule" + + # ---- 其他 ---- + terminationGracePeriodSeconds: 30 # 优雅终止等待时间 + serviceAccountName: myapp-sa # 使用的 ServiceAccount +``` + +> [!tip] imagePullPolicy 最佳实践 +> - 使用 `:latest` 标签时,必须设置 `imagePullPolicy: Always`,否则可能使用本地缓存的旧镜像 +> - 使用固定版本标签(如 `:v1.2.3`)时,`IfNotPresent` 更高效,避免重复拉取 +> - `Never` 仅用于本地开发环境(如 minikube 中直接使用本地构建的镜像) + +## 常见陷阱与最佳实践 + +### 更新卡住排查 + +滚动更新卡住是常见问题,通常由以下原因导致: + +```bash +# 1. 查看 Deployment 状态 +kubectl rollout status deployment nginx-deploy +# 可能输出:Waiting for rollout to finish: 2 out of 4 new replicas have been updated... + +# 2. 查看 Deployment 的 Conditions +kubectl get deployment nginx-deploy -o yaml | grep -A 20 conditions: +# 常见条件: +# - Progressing: False(更新未推进) +# - Available: True/False(是否有足够可用 Pod) + +# 3. 查看新 ReplicaSet 的事件 +kubectl describe rs nginx-deploy-xxxxxx # 查看新 RS 的 Events +# 常见原因: +# - FailedScheduling:资源不足,没有节点能满足 requests +# - ImagePullBackOff:镜像拉取失败(镜像名错、私有仓库无凭证) +# - CrashLoopBackOff:新容器启动后立即崩溃 + +# 4. 查看 Pod 事件 +kubectl get pods -l app=nginx --sort-by=.metadata.creationTimestamp +kubectl describe pod nginx-deploy-xxxxxx-yyyyy +``` + +常见原因和解决方案: + +| 症状 | 原因 | 解决方案 | +|------|------|---------| +| ImagePullBackOff | 镜像不存在或无权限 | 检查镜像名称、imagePullSecrets | +| CrashLoopBackOff | 应用启动失败 | 查看 Pod 日志 `kubectl logs` | +| Pending | 资源不足或节点不满足约束 | 检查 `kubectl describe pod` 中的 Events | +| readinessProbe 一直失败 | 健康检查路径或端口错误 | 确认探针配置与应用实际端口一致 | + +> [!warning] 超时回滚 +> Deployment 有一个 `progressDeadlineSeconds` 参数(默认 600 秒),如果更新在该时间内未完成,Deployment 会标记为 `Progressing=False`。注意:这**不会自动回滚**,只是标记状态。需要手动执行 `kubectl rollout undo` 或编写自动化脚本。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deploy +spec: + progressDeadlineSeconds: 300 # 5 分钟内未完成更新则标记失败 + minReadySeconds: 10 # Pod Ready 后等待 10 秒才视为 Available + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.26 +``` + +### imagePullPolicy 导致的镜像缓存问题 + +```bash +# 场景:修改了 YAML 中的 image 字段但没改 tag,apply 后 Pod 没有更新 +# 原因:imagePullPolicy 默认为 IfNotPresent,本地已有旧镜像就不会重新拉取 + +# 解决方案 1:始终使用不同的 tag(推荐) +image: myapp:v1.0.1 # 而不是 myapp:latest + +# 解决方案 2:强制设置 imagePullPolicy +imagePullPolicy: Always + +# 解决方案 3:使用 SHA256 digest(最精确) +image: myapp@sha256:abc123... +``` + +### Deployment vs StatefulSet 选择 + +| 特性 | Deployment | StatefulSet | +|------|-----------|-------------| +| Pod 名称 | 随机(`nginx-5d6b8f7c9-abc12`) | 有序(`web-0`, `web-1`, `web-2`) | +| Pod 启动顺序 | 并行 | 有序(0 → 1 → 2) | +| Pod 终止顺序 | 并行(默认) | 逆序(2 → 1 → 0) | +| 网络标识 | 不稳定 | 稳定(`web-0.ng-svc`) | +| 持久存储 | 共享或无 | 每个 Pod 独立 PVC | +| 适用场景 | 无状态服务 | 有状态服务(数据库、MQ、ZooKeeper) | +| 滚动更新 | 替换 Pod | 逐个更新 | + +> [!info] 选择原则 +> 如果你的应用满足以下任意条件,应考虑 StatefulSet: +> - 需要稳定的网络标识(如 ZooKeeper 集群节点互相发现) +> - 需要持久化的、与 Pod 绑定的存储(如 MySQL 数据目录) +> - 需要有序的部署/扩缩/更新 +> +> 其余场景一律使用 Deployment。 + +### 其他最佳实践 + +1. **始终配置资源请求和限制**:没有 requests 的 Pod 会被调度器视为零消耗,可能导致多个 Pod 被调度到同一节点,引发资源争抢。 + +2. **始终配置 readinessProbe**:没有就绪探针,新 Pod 创建后立即被标记为 Ready,流量可能打到未初始化完成的 Pod。 + +3. **不要直接管理 Deployment 创建的 ReplicaSet**:RS 的生命周期由 Deployment 控制器管理,手动修改 RS(如 scale)会被 Deployment 覆盖。 + +4. **使用 `kubectl apply` 而非 `kubectl create`**:apply 支持声明式管理,可以幂等执行,适合 GitOps 工作流。 + +5. **合理设置 minReadySeconds**:该参数让 Pod Ready 后再等待一段时间才被视为 Available,可以用来检测"启动后很快崩溃"的场景。 + +```yaml +# 推荐的最小 Deployment 模板 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + labels: + app: myapp +spec: + replicas: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 300 + minReadySeconds: 10 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 # 保证更新期间始终有足够的可用 Pod + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: myapp + image: myapp:v1.0.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 20 +``` + +## 延伸阅读 + +- [[k8s-03-pod]] — Pod 的生命周期、调度机制、资源管理 +- [[k8s-05-service-and-networking]] — Service 类型、Ingress、网络策略 +- [Kubernetes 官方文档 - Deployments](https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/deployment/) +- [Kubernetes 官方文档 - ReplicaSet](https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/replicaset/) +- [Kubernetes 官方文档 - HPA](https://kubernetes.io/zh-cn/docs/tasks/run-application/horizontal-pod-autoscale/) diff --git a/technical/k8s/k8s-05-service-and-networking.md b/technical/k8s/k8s-05-service-and-networking.md new file mode 100644 index 0000000..cb933e5 --- /dev/null +++ b/technical/k8s/k8s-05-service-and-networking.md @@ -0,0 +1,641 @@ +--- +tags: [k8s, service, networking, devops] +create time: 2026-07-07 15:48 +--- + +# Kubernetes Service 与网络 + +## 概述 + +Kubernetes 中 Pod 的生命周期是短暂的——它们随时可能被销毁重建,每次重建都会获得新的 IP 地址。如果服务之间直接通过 Pod IP 通信,任何一次 Pod 重启都会导致调用方断连。**Service** 正是为了解决这个问题而生:它为一组动态变化的 Pod 提供一个稳定的访问入口(虚拟 IP),配合 DNS 和负载均衡,让服务发现变得透明且可靠。 + +## K8s 网络模型 + +Kubernetes 对集群网络有三个核心要求,任何 CNI(Container Network Interface)插件都必须满足: + +| 原则 | 说明 | +|------|------| +| **Pod-to-Pod** | 集群内任意两个 Pod 可以直接通过 IP 通信,无需 NAT | +| **Pod-to-Service** | Pod 通过 Service 的虚拟 IP(ClusterIP)访问后端 Pod,由 kube-proxy 实现负载均衡 | +| **External-to-Service** | 集群外部流量可以通过 NodePort、LoadBalancer 或 Ingress 进入集群 | + +### CNI 插件简介 + +CNI 插件负责为 Pod 分配 IP 地址并配置网络连通性。常见实现: + +- **Calico**:基于 BGP 路由,支持 NetworkPolicy,性能优秀 +- **Flannel**:简单轻量,使用 VXLAN overlay,适合小规模集群 +- **Cilium**:基于 eBPF,高性能,支持 L7 网络策略 +- **Weave Net**:自带加密,配置简单 + +> [!tip] 如何选择 +> 小规模或学习环境用 Flannel 足够;生产环境推荐 Calico 或 Cilium,它们对 NetworkPolicy 的支持更完善。 + +## Service 详解 + +### ClusterIP + +**默认类型**,为 Service 分配一个集群内部虚拟 IP,仅集群内可访问。 + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-app-svc + namespace: default +spec: + type: ClusterIP # 默认值,可省略 + selector: + app: my-app + ports: + - protocol: TCP + port: 80 # Service 端口 + targetPort: 8080 # Pod 端口 +``` + +流量路径:`Client Pod → ClusterIP(虚拟 IP)→ kube-proxy → 后端 Pod:8080` + +ClusterIP 是一个"虚拟 IP"——它不存在于任何网卡上,只存在于 iptables/IPVS 规则中。 + +### NodePort + +在 ClusterIP 的基础上,在**每个节点**上开一个端口(默认范围 30000-32767),外部流量可通过 `:` 访问。 + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-app-nodeport +spec: + type: NodePort + selector: + app: my-app + ports: + - protocol: TCP + port: 80 # Service 端口(集群内部访问) + targetPort: 8080 # Pod 端口 + nodePort: 30080 # 节点端口(可选,不指定则自动分配) +``` + +> [!info] iptables 规则链路 +> 当外部请求到达 `:30080` 时,kube-proxy 在每个节点上创建的 iptables 规则会将流量 DNAT 到后端 Pod 的 IP:Port。这意味着即使请求落在没有运行目标 Pod 的节点上,流量也会被正确转发。 + +```mermaid +flowchart LR + A[External Client] -->|NodeIP:30080| B[Node] + B -->|iptables DNAT| C[Pod on Node A] + B -->|iptables DNAT| D[Pod on Node B] + B -->|iptables DNAT| E[Pod on Node C] +``` + +### LoadBalancer + +在 NodePort 的基础上,向云厂商申请一个外部负载均衡器(如 AWS ELB、GCP CLB),自动将外部流量分发到各节点的 NodePort。 + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-app-lb + annotations: + # 云厂商特定注解,例如 AWS: + service.beta.kubernetes.io/aws-load-balancer-type: nlb +spec: + type: LoadBalancer + selector: + app: my-app + ports: + - protocol: TCP + port: 80 + targetPort: 8080 +``` + +> [!warning] 继承关系 +> `LoadBalancer` = `ClusterIP` + `NodePort` + 云厂商 LB。如果没有云厂商支持(如裸金属环境),LoadBalancer 类型的 Service 会一直停留在 `` 状态。此时可以考虑 MetalLB 作为替代方案。 + +### ExternalName + +将 Service 映射到一个外部 DNS 名称(CNAME 记录),不涉及任何代理和负载均衡。 + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: external-db + namespace: default +spec: + type: ExternalName + externalName: db.example.com # 映射到外部域名 +``` + +**适用场景**: +- 集群内应用需要访问外部数据库或 API +- 跨命名空间/跨集群服务映射 +- 迁移过程中临时将内部 Service 指向外部服务 + +当 Pod 查询 `external-db.default.svc.cluster.local` 时,CoreDNS 会返回 `db.example.com` 的 CNAME 记录,Pod 再对 `db.example.com` 发起真正的 DNS 查询。 + +### Service 类型对比 + +| 类型 | ClusterIP | NodePort | LoadBalancer | ExternalName | +|------|-----------|----------|--------------|--------------| +| **默认** | 是 | 否 | 否 | 否 | +| **集群内访问** | 支持 | 支持 | 支持 | 通过 CNAME | +| **集群外访问** | 不支持 | 支持(NodeIP:Port) | 支持(外部 IP) | 不适用 | +| **端口范围** | 任意 | 30000-32767 | 任意(LB 端口) | 无端口 | +| **典型场景** | 内部微服务互调 | 开发测试暴露 | 生产环境对外服务 | 访问外部服务 | +| **云厂商依赖** | 无 | 无 | 需要 | 无 | + +## Endpoints 与 EndpointSlices + +### Endpoints + +每个 Service 背后都有一个同名的 **Endpoints** 对象,记录了当前所有健康 Pod 的 IP:Port 列表。当 Pod 就绪(Readiness Probe 通过)时自动加入,Pod 删除时自动移除。 + +```bash +# 查看 Service 对应的 Endpoints +kubectl get endpoints my-app-svc +# NAME ENDPOINTS AGE +# my-app-svc 10.244.1.5:8080,10.244.2.8:8080 5m +``` + +### EndpointSlices + +Kubernetes 1.21 引入 EndpointSlices 作为 Endpoints 的替代方案。 + +> [!info] 为什么需要 EndpointSlices? +> 在大规模集群中,一个 Service 可能关联成百上千个 Pod。Endpoints 对象会将所有 Pod IP 存储在单个对象中,导致: +> 1. **更新风暴**:任何一个 Pod 变化都要更新整个 Endpoints 对象 +> 2. **内存压力**:kube-proxy 和所有节点的 kubelet 都要 watch 这个大对象 +> 3. **API Server 压力**:每次更新都要全量推送 +> +> EndpointSlices 将后端列表拆分成多个小切片(默认每个切片最多 100 个端点),增量更新,大幅降低开销。 + +```bash +# 查看 EndpointSlices +kubectl get endpointslices -l kubernetes.io/service-name=my-app-svc +``` + +```mermaid +flowchart TB + subgraph Endpoints 旧方式 + S1[Service] --> E1[Endpoints
全部 Pod IP 在一个对象中] + end + subgraph EndpointSlices 新方式 + S2[Service] --> ES1[EndpointSlice-0
Pod 1-100] + S2 --> ES2[EndpointSlice-1
Pod 101-200] + S2 --> ES3[EndpointSlice-2
Pod 201-300] + end +``` + +## DNS 解析机制 + +### CoreDNS + +Kubernetes 使用 **CoreDNS** 作为集群内置 DNS 服务(1.12 之前使用 kube-dns)。每个 Service 创建后,CoreDNS 会自动为其注册 DNS 记录。 + +### Service DNS 格式 + +| 资源类型 | DNS 格式 | 示例 | +|----------|----------|------| +| 普通 Service | `..svc.cluster.local` | `my-app.default.svc.cluster.local` | +| Headless Service | `..svc.cluster.local` → 解析到所有 Pod IP | 直接返回 Pod IP 列表 | +| Pod | `..pod.cluster.local` | `10-244-1-5.default.pod.cluster.local` | + +> [!tip] 同命名空间内可以省略后缀 +> 在 `default` 命名空间内的 Pod 访问同命名空间的 Service,可以直接用 `my-app-svc`,无需写全限定域名。 + +### Headless Service + +当 Service 的 `clusterIP` 设置为 `None` 时,它成为一个 **Headless Service**。此时 DNS 查询不会返回一个虚拟 IP,而是直接返回所有后端 Pod 的 IP 列表。 + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-app-headless +spec: + clusterIP: None # Headless + selector: + app: my-app + ports: + - port: 80 + targetPort: 8080 +``` + +**典型应用场景**: +- **StatefulSet**:有状态应用(如数据库集群)需要知道每个 Pod 的独立地址 +- **自定义负载均衡**:客户端自己实现负载均衡逻辑 +- **服务发现**:客户端需要获取所有后端地址 + +```bash +# 查询 Headless Service 的 DNS +nslookup my-app-headless.default.svc.cluster.local +# Name: my-app-headless.default.svc.cluster.local +# Address: 10.244.1.5 +# Address: 10.244.2.8 +# Address: 10.244.3.12 +``` + +## kube-proxy 深入 + +kube-proxy 是运行在每个节点上的网络代理,负责实现 Service 的负载均衡。它有三种工作模式: + +### iptables 模式(默认) + +kube-proxy 监听 Service 和 Endpoints 的变化,在每个节点上维护 iptables 规则链。 + +```mermaid +flowchart TB + A[请求到达 ClusterIP] --> B[KUBE-SERVICES 链] + B --> C{匹配 Service?} + C -->|是| D[KUBE-SVC-XXX 链] + D --> E[概率匹配 -m statistic --mode random] + E -->|概率 50%| F[KUBE-SEP-AAA 链 → Pod A] + E -->|概率 50%| G[KUBE-SEP-BBB 链 → Pod B] + C -->|否| H[REJECT] +``` + +**规则链路详解**: +1. `KUBE-SERVICES`:总入口,匹配目标 IP:Port 确定 Service +2. `KUBE-SVC-XXX`:Service 级别链,使用 `statistic` 模块做随机负载均衡 +3. `KUBE-SEP-XXX`:Endpoint 级别链,执行 DNAT 将目标地址转换为 Pod IP:Port + +> [!warning] iptables 模式的局限 +> - 规则数量与 Service × Pod 数量成正比,大规模集群下规则链很长 +> - 线性遍历匹配,时间复杂度 O(n) +> - 不支持连接追踪以外的负载均衡算法 + +### IPVS 模式 + +IPVS(IP Virtual Server)是 Linux 内核中的四层负载均衡器,性能远优于 iptables。 + +```yaml +# kube-proxy 配置启用 IPVS +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +kind: KubeProxyConfiguration +mode: "ipvs" +ipvs: + scheduler: "rr" # 负载均衡算法:rr / lc / sh / sed / nq +``` + +**支持的负载均衡算法**: + +| 算法 | 全称 | 说明 | +|------|------|------| +| `rr` | Round Robin | 轮询(默认) | +| `lc` | Least Connections | 最少连接数 | +| `sh` | Source Hashing | 源地址哈希(会话保持) | +| `sed` | Shortest Expected Delay | 最短预期延迟 | +| `nq` | Never Queue | 永不排队 | + +### iptables vs IPVS 对比 + +| 维度 | iptables | IPVS | +|------|----------|------| +| **性能** | 规则多时下降明显 | 哈希表查找,O(1) | +| **最大 Service 数** | 约 5000 开始明显退化 | 可支撑数万 | +| **负载均衡算法** | 仅随机 | rr / lc / sh / sed / nq | +| **连接保持** | 有限支持 | 原生支持 | +| **依赖** | iptables(用户态工具) | ipvs 内核模块 | +| **调试** | `iptables -L -n -t nat` | `ipvsadm -Ln` | + +> [!tip] 生产建议 +> 节点 Service 数超过 1000 时,强烈建议切换到 IPVS 模式。 + +## Ingress 基础 + +Service(NodePort/LoadBalancer)工作在四层(TCP/UDP),而 **Ingress** 工作在七层(HTTP/HTTPS),提供基于域名和路径的路由能力。 + +### Ingress Controller + +Ingress 资源本身只是一份声明式配置,真正执行路由逻辑的是 **Ingress Controller**。常见的 Ingress Controller: + +- **Nginx Ingress Controller**:最广泛使用,社区版 + F5 版 +- **Traefik**:自动服务发现,配置简单 +- **HAProxy**:高性能,企业级特性丰富 +- **Istio Gateway**:Service Mesh 生态中的网关 + +### Path-based 路由 + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: path-based-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx + rules: + - host: app.example.com + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: api-svc + port: + number: 80 + - path: /web + pathType: Prefix + backend: + service: + name: web-svc + port: + number: 80 +``` + +### Host-based 路由 + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: host-based-ingress +spec: + ingressClassName: nginx + rules: + - host: api.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: api-svc + port: + number: 80 + - host: web.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: web-svc + port: + number: 80 +``` + +### TLS 终止 + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: tls-ingress + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" +spec: + ingressClassName: nginx + tls: + - hosts: + - app.example.com + secretName: app-tls-secret # 存放证书的 Secret + rules: + - host: app.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: my-app-svc + port: + number: 80 +``` + +证书需要提前创建为 Kubernetes Secret: + +```bash +kubectl create secret tls app-tls-secret \ + --cert=tls.crt \ + --key=tls.key +``` + +```mermaid +flowchart LR + A[Client] -->|HTTPS| B[Ingress Controller
TLS 终止] + B -->|HTTP| C[Service: my-app-svc] + C --> D[Pod A] + C --> E[Pod B] +``` + +## NetworkPolicy + +默认情况下,Kubernetes 集群中所有 Pod 之间可以互相通信。**NetworkPolicy** 允许你定义精细的网络访问控制规则。 + +> [!warning] 前提条件 +> NetworkPolicy 需要 CNI 插件支持。Flannel 不支持 NetworkPolicy,Calico 和 Cilium 支持。 + +### 默认全通策略 + +如果不创建任何 NetworkPolicy,所有流量都是放行的: + +```mermaid +flowchart LR + A[Pod A] -->|允许| B[Pod B] + B -->|允许| A + A -->|允许| C[Pod C] + C -->|允许| B +``` + +### 限制 Pod 间通信 + +**示例:只允许前端 Pod 访问后端 Pod 的 8080 端口** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: backend-policy + namespace: default +spec: + podSelector: + matchLabels: + app: backend # 策略作用于 backend Pod + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: frontend # 只允许来自 frontend Pod + ports: + - protocol: TCP + port: 8080 # 只允许访问 8080 端口 +``` + +**示例:限制出站流量,只允许访问指定 DNS** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: restrict-egress + namespace: default +spec: + podSelector: + matchLabels: + app: my-app + policyTypes: + - Egress + egress: + - to: + - namespaceSelector: + matchLabels: + name: kube-system # 允许访问 kube-system 命名空间 + ports: + - protocol: UDP + port: 53 # 允许 DNS 查询 + - to: + - ipBlock: + cidr: 10.0.0.0/8 # 允许访问内网 + ports: + - protocol: TCP + port: 443 +``` + +> [!info] 策略叠加规则 +> - 一旦某个 Pod 被任何 NetworkPolicy 选中,未被策略明确允许的流量将被**拒绝** +> - 多个 NetworkPolicy 取**并集**(任一策略允许即放行) +> - 只定义 `policyTypes: [Ingress]` 不影响出站,反之亦然 + +```mermaid +flowchart TB + subgraph 策略生效后 + A[frontend Pod] -->|允许: TCP 8080| B[backend Pod] + C[other Pod] -->|拒绝| B + B -->|未定义 Egress 策略
默认放行| D[任意目标] + end +``` + +## 常见陷阱与最佳实践 + +### Service 无法访问排查清单 + +当 Service 访问不通时,按以下顺序排查: + +1. **确认 Pod 运行正常** + ```bash + kubectl get pods -l app=my-app -o wide + kubectl logs + kubectl describe pod + ``` + +2. **确认 Endpoints 已注册** + ```bash + kubectl get endpoints my-app-svc + # 如果 ENDPOINTS 为 ,说明没有健康的 Pod 匹配 selector + ``` + +3. **确认 selector 匹配** + ```bash + # 对比 Service selector 和 Pod labels + kubectl get svc my-app-svc -o jsonpath='{.spec.selector}' + kubectl get pods --show-labels + ``` + +4. **确认端口正确** + ```bash + # 检查 targetPort 是否与 Pod 实际监听端口一致 + kubectl get svc my-app-svc -o yaml | grep -A5 ports + kubectl exec -- ss -tlnp + ``` + +5. **确认 DNS 解析** + ```bash + kubectl run test --rm -it --image=busybox -- nslookup my-app-svc + ``` + +6. **确认网络策略未阻断** + ```bash + kubectl get networkpolicy -n + ``` + +7. **确认 kube-proxy 正常** + ```bash + kubectl get pods -n kube-system -l k8s-app=kube-proxy + # 检查 iptables 规则是否存在 + iptables -t nat -L KUBE-SERVICES | grep my-app-svc + ``` + +### Session Affinity + +默认情况下 Service 使用轮询负载均衡。如果需要会话保持(同一客户端始终访问同一 Pod),可以启用 Session Affinity: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: sticky-svc +spec: + selector: + app: my-app + sessionAffinity: ClientIP # 基于客户端 IP 保持会话 + sessionAffinityConfig: + clientIP: + timeoutSeconds: 1800 # 会话超时时间(默认 10800 秒) + ports: + - port: 80 + targetPort: 8080 +``` + +> [!warning] Session Affinity 注意事项 +> - 在 iptables 模式下,基于源 IP 的会话保持可能在 NAT 环境下失效 +> - IPVS 模式支持更精细的会话保持(`sh` 算法) +> - 对于 HTTP 应用,更推荐使用 Cookie-based Affinity(在 Ingress 层实现) + +### External Traffic Policy + +控制外部流量如何路由到 Pod,只对 NodePort 和 LoadBalancer 类型 Service 生效: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-app-svc +spec: + type: LoadBalancer + externalTrafficPolicy: Local # 或 Cluster(默认) + selector: + app: my-app + ports: + - port: 80 + targetPort: 8080 +``` + +| 策略 | 行为 | 优点 | 缺点 | +|------|------|------|------| +| **Cluster(默认)** | 流量可能被转发到其他节点上的 Pod | 负载均衡更均匀 | 多一跳转发,丢失源 IP | +| **Local** | 流量只转发到本节点上的 Pod | 保留源 IP,减少转发延迟 | Pod 分布不均时负载不均衡 | + +```mermaid +flowchart TB + subgraph "externalTrafficPolicy: Cluster" + A1[Client] -->|请求| B1[Node 1] + B1 -->|可能转发| C1[Node 2 的 Pod] + B1 -->|本地| D1[Node 1 的 Pod] + end + subgraph "externalTrafficPolicy: Local" + A2[Client] -->|请求| B2[Node 1] + B2 -->|仅本地| D2[Node 1 的 Pod] + B2 -.->|不转发| C2[Node 2 的 Pod] + end +``` + +### 其他最佳实践 + +- **使用命名空间隔离 Service**:避免命名冲突,便于权限管理 +- **合理设置 readinessProbe**:Pod 未就绪时不会被加入 Endpoints,避免流量打到未准备好的实例 +- **避免使用 ExternalName 映射到集群内部 Service**:可能导致 DNS 解析循环 +- **大规模集群启用 EndpointSlices**:减少 API Server 和 kube-proxy 的压力 +- **生产环境使用 IPVS 模式**:在 Service 数量较多时性能显著优于 iptables +- **为关键 Service 配置 PodDisruptionBudget**:确保滚动更新和节点维护时服务可用性 + +## 延伸阅读 + +- [[k8s-01-overview]] — Kubernetes 基础概念总览 +- [[k8s-04-deployment-and-replicaset]] — Deployment 与 ReplicaSet 详解 diff --git a/technical/k8s/k8s-06-configmap-and-secret.md b/technical/k8s/k8s-06-configmap-and-secret.md new file mode 100644 index 0000000..9eac792 --- /dev/null +++ b/technical/k8s/k8s-06-configmap-and-secret.md @@ -0,0 +1,703 @@ +--- +tags: [k8s, configmap, secret, devops] +create time: 2026-07-07 15:48 +--- + +# ConfigMap 与 Secret — K8s 配置管理 + +## 概述 + +> [!info] 十二要素应用原则(12-Factor App) +> 配置应当与代码严格分离。镜像是不可变的构建产物,而运行时环境(数据库地址、API Key、特性开关)应当通过外部注入,而非硬编码在镜像中。Kubernetes 的 **ConfigMap** 和 **Secret** 正是实现这一原则的核心机制——它们将配置数据从 Pod 定义中解耦,使得同一镜像可以部署到不同环境而无需重建。 + +简而言之: + +- **ConfigMap**:存放非敏感的配置数据(如 Config 文件、命令行参数、环境变量)。 +- **Secret**:存放敏感数据(如密码、Token、TLS 证书),并提供额外的安全保障。 + +下图展示了配置数据如何从集群外部流入 Pod 内部: + +```mermaid +flowchart LR + subgraph Cluster["K8s Cluster"] + CM[ConfigMap] + SEC[Secret] + ETCD[(etcd)] + CM --> ETCD + SEC --> ETCD + end + + subgraph Pod["Pod"] + ENV["环境变量注入"] + VOL["Volume 挂载\n(/etc/config, /etc/secret)"] + end + + ETCD -->|kubelet sync| VOL + ETCD -->|Pod 启动时注入| ENV + + subgraph External["外部管理"] + VAULT["HashiCorp Vault"] + AWS_SM["AWS Secrets Manager"] + end + + External -->|CSI Driver / Sync| SEC +``` + +--- + +## ConfigMap 详解 + +### 创建方式 + +#### 1. 命令式创建(kubectl create configmap) + +```bash +# 方式一:从字面值创建 +kubectl create configmap app-config \ + --from-literal=APP_ENV=production \ + --from-literal=LOG_LEVEL=info \ + --from-literal=DB_HOST=mysql.default.svc.cluster.local + +# 方式二:从单个文件创建 +kubectl create configmap nginx-conf \ + --from-file=nginx.conf + +# 方式三:从目录创建(目录下所有文件各成为一个 key) +kubectl create configmap app-config-files \ + --from-file=config/ + +# 方式四:指定 key 名称(而非使用文件名作为 key) +kubectl create configmap app-conf \ + --from-file=my-config=./app.properties + +# 方式五:从 .env 文件创建(key=value 格式) +kubectl create configmap env-config \ + --from-env-file=.env +``` + +> [!tip] `--from-env-file` vs `--from-file` +> `--from-env-file` 解析 `KEY=VALUE` 格式,每个键值对成为一个独立的 key;而 `--from-file` 将整个文件内容作为单个 key 的 value。二者用途不同,不要混淆。 + +#### 2. 声明式创建(YAML) + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config + namespace: default + labels: + app: my-app +data: + # 简单键值对 + APP_ENV: "production" + LOG_LEVEL: "info" + DB_PORT: "3306" + + # 多行配置文件作为 value + application.properties: | + server.port=8080 + spring.datasource.url=jdbc:mysql://mysql:3306/app + spring.datasource.username=root + logging.level.root=INFO + + # JSON 格式的配置 + features.json: | + { + "enableDarkMode": true, + "maxRetries": 3, + "timeout": 30 + } +``` + +查看创建结果: + +```bash +kubectl describe configmap app-config +kubectl get configmap app-config -o yaml +``` + +### 挂载方式 + +ConfigMap 中的数据注入到 Pod 有两种主要方式:**环境变量注入** 和 **Volume 挂载**。二者在行为上有显著差异。 + +| 特性 | 环境变量注入 | Volume 挂载 | +|------|-------------|------------| +| 注入时机 | Pod 启动时 | Pod 启动时 + 运行时可更新 | +| 热更新支持 | 不支持,需重启 Pod | 支持(kubelet 定期同步) | +| 适用场景 | 简单键值对、启动参数 | 配置文件(nginx.conf 等) | +| 引用方式 | `configMapKeyRef` / `envFrom` | `volumes.configMap` | +| 文件权限 | 不涉及 | 可设置 `defaultMode` | +| 多 key 批量注入 | `envFrom` 批量导入 | 整个目录挂载 | + +#### 环境变量注入 + +**方式一:逐个 key 引用(`configMapKeyRef`)** + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-pod +spec: + containers: + - name: app + image: my-app:1.0 + env: + # 引用 ConfigMap 中的单个 key + - name: APP_ENV # 容器内的环境变量名 + valueFrom: + configMapKeyRef: + name: app-config # ConfigMap 名称 + key: APP_ENV # ConfigMap 中的 key 名 + - name: LOG_LEVEL + valueFrom: + configMapKeyRef: + name: app-config + key: LOG_LEVEL + optional: true # ConfigMap 或 key 不存在时不报错 +``` + +**方式二:批量导入(`envFrom`)** + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-pod +spec: + containers: + - name: app + image: my-app:1.0 + envFrom: + - configMapRef: + name: app-config + prefix: "CFG_" # 可选前缀,避免命名冲突 +``` + +> [!warning] `envFrom` 会跳过无效 key +> ConfigMap 中如果存在不符合环境变量命名规范的 key(如包含 `-` 或以数字开头),这些 key 会被静默跳过,不会报错。使用时需注意 key 的命名规范。 + +#### Volume 挂载 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-pod +spec: + containers: + - name: app + image: nginx:1.25 + volumeMounts: + - name: config-volume + mountPath: /etc/nginx/conf.d # 挂载到容器内目录 + readOnly: true + - name: app-properties + mountPath: /app/config # 挂载整个目录 + volumes: + # 挂载 ConfigMap 中指定的 key + - name: config-volume + configMap: + name: nginx-conf + items: + - key: default.conf + path: default.conf # 文件名(挂载后的文件名) + mode: 0644 # 文件权限 + # 挂载 ConfigMap 全部 key(每个 key 成为一个文件) + - name: app-properties + configMap: + name: app-config + defaultMode: 0644 # 默认文件权限 +``` + +### 热更新机制 + +ConfigMap 通过 Volume 挂载后,当 ConfigMap 的内容在集群中被更新时,kubelet 会自动将新内容同步到 Pod 的挂载目录中。这个过程的延迟取决于: + +1. **kubelet sync 周期**:默认每 **1 分钟**(`--sync-frequency` 参数控制)。 +2. **传播延迟**:从 API Server 到 kubelet 的 watch 通知延迟。 + +```mermaid +sequenceDiagram + participant User as 用户/kubectl + participant API as API Server + participant Kubelet as Kubelet (Node) + participant Pod as Pod Container + + User->>API: kubectl apply -f configmap.yaml + API->>Kubelet: watch 通知 ConfigMap 变更 + Note over Kubelet: 等待 sync 周期(最多 1 分钟) + Kubelet->>Pod: 更新 Volume 挂载文件(原子替换) + Note over Pod: 应用需 reload 配置(如 nginx -s reload) +``` + +> [!warning] 热更新 ≠ 自动生效 +> ConfigMap 文件更新后,**应用本身必须能够感知文件变化并重新加载配置**。例如 Nginx 需要 `nginx -s reload`,Spring Boot 需要 `@RefreshScope`。否则虽然文件内容已更新,但应用仍使用旧配置。 + +**环境变量方式不可热更新**:环境变量在 Pod 启动时由 kubelet 注入到容器的进程环境中,运行期间不会再更新。要更新环境变量,只能删除并重建 Pod。 + +### 不可变 ConfigMap + +从 Kubernetes 1.21 开始,可以将 ConfigMap 标记为不可变(immutable),这是一个重要的性能优化手段。 + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config-v1 +immutable: true # 标记为不可变 +data: + APP_ENV: "production" + LOG_LEVEL: "info" +``` + +**为什么需要不可变 ConfigMap?** + +- **减轻 API Server 压力**:普通 ConfigMap 会被 kubelet watch,任何变更都会触发全集群的通知广播。对于大规模集群(数千节点),大量 watch 事件会显著增加 API Server 的负担。 +- **防止意外修改**:标记为 immutable 后,该 ConfigMap 的内容不可更改(只能删除重建)。 +- **推荐场景**:发布后不再变更的配置(如已上线的版本配置)。 + +> [!tip] 不可变 ConfigMap 的更新策略 +> 由于 immutable ConfigMap 无法修改,更新配置时需要创建一个新的 ConfigMap(如 `app-config-v2`),然后更新 Deployment 引用新的 ConfigMap 名称。可以结合 Deployment 的滚动更新实现零停机配置切换。 + +--- + +## Secret 详解 + +### 类型 + +Kubernetes 内置了多种 Secret 类型,用于不同场景。以下是常用类型的对比: + +| 类型 | 用途 | data 字段说明 | 典型场景 | +|------|------|--------------|---------| +| `Opaque` | 通用型(默认) | 用户自定义的任意 key-value | 数据库密码、API Key 等 | +| `kubernetes.io/tls` | TLS 证书 | 必须含 `tls.crt` 和 `tls.key` | Ingress TLS 终止 | +| `kubernetes.io/dockerconfigjson` | 镜像仓库凭证 | 必须含 `.dockerconfigjson` | 私有镜像仓库拉取 | +| `kubernetes.io/basic-auth` | 基本认证 | `username` + `password` | Git/HTTP Basic Auth | +| `kubernetes.io/ssh-auth` | SSH 认证 | 必须含 `ssh-privatekey` | Git SSH 拉取 | +| `kubernetes.io/service-account-token` | SA Token | 自动创建 | Pod 身份认证(已逐步被 Bound SA Token 替代) | + +#### Opaque Secret 示例 + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: db-credentials +type: Opaque +data: + # 值必须是 Base64 编码 + username: cm9vdA== # echo -n 'root' | base64 + password: czNjcjN0UEBzc3cwcmQ= # echo -n 's3cr3tP@ssw0rd' | base64 +# 也可以用 stringData(自动编码,方便调试,生产慎用) +stringData: + connection-string: "mysql://root:s3cr3tP@ssw0rd@mysql:3306/app" +``` + +#### TLS Secret 示例 + +```bash +# 通过 kubectl 创建 TLS Secret +kubectl create secret tls my-tls-secret \ + --cert=tls.crt \ + --key=tls.key +``` + +```yaml +# 等价的 YAML 声明 +apiVersion: v1 +kind: Secret +metadata: + name: my-tls-secret +type: kubernetes.io/tls +data: + tls.crt: + tls.key: +``` + +#### Docker Registry Secret + +```bash +# 创建 Docker Registry 凭证 +kubectl create secret docker-registry regcred \ + --docker-server=registry.example.com \ + --docker-username=admin \ + --docker-password=P@ssw0rd \ + --docker-email=admin@example.com +``` + +```yaml +# 在 Pod 中使用 +apiVersion: v1 +kind: Pod +metadata: + name: private-app +spec: + imagePullSecrets: + - name: regcred # 引用 Docker Registry Secret + containers: + - name: app + image: registry.example.com/my-app:1.0 +``` + +### 编码 vs 加密 + +> [!danger] Base64 编码不等于加密 +> Secret 的 `data` 字段使用 Base64 编码,但这只是编码格式,**不是加密**。任何拥有集群读权限的人都可以轻松解码。`kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d` 即可还原明文。 + +**etcd 加密(EncryptionConfiguration)** + +Kubernetes 支持在 etcd 层面对 Secret 进行真正的加密存储: + +```yaml +# /etc/kubernetes/encryption-config.yaml +apiVersion: apiserver.config.k8s.io/v1 +kind: EncryptionConfiguration +resources: + - resources: + - secrets + - configmaps # 1.25+ 也支持加密 ConfigMap + providers: + # 第一个 provider 用于解密(兼容) + - aescbc: + keys: + - name: key1 + secret: <32-byte-base64-encoded-key> # head -c 32 /dev/urandom | base64 + # fallback(不加密,仅读取旧数据) + - identity: {} +``` + +> [!tip] 加密流程 +> 启用 EncryptionConfiguration 后,新写入的 Secret 会使用配置的 provider 加密存储。已有的旧 Secret 在下次写入(update)时才会重新加密。读取时 API Server 自动解密,对客户端透明。 + +#### 更安全的加密方案对比 + +| 方案 | 加密层次 | 密钥管理 | 适用场景 | +|------|---------|---------|---------| +| Base64(默认) | 无加密 | 无 | 仅编码格式 | +| aescbc(EncryptionConfiguration) | etcd 存储层 | 静态密钥(文件) | 基础加密需求 | +| KMS Plugin(AWS KMS、GCP KMS) | etcd 存储层 | 外部 KMS 托管 | 生产环境推荐 | +| 外部密钥管理(Vault) | 应用层 | 专业密钥管理系统 | 高安全要求 | + +### 挂载方式 + +Secret 的挂载方式与 ConfigMap 基本一致,但有以下关键差异: + +| 特性 | ConfigMap | Secret | +|------|-----------|--------| +| 存储介质 | 普通目录(tmpfs 或磁盘) | **tmpfs(内存文件系统)**,不落盘 | +| 默认文件权限 | `0644` | **`0400`**(仅 owner 可读) | +| 适用内容 | 非敏感配置 | 密码、Token、证书等敏感数据 | +| etcd 存储 | 明文 | 可加密(EncryptionConfiguration) | + +#### 环境变量注入 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-with-secret +spec: + containers: + - name: app + image: my-app:1.0 + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: db-credentials + key: password + - name: DB_USERNAME + valueFrom: + secretKeyRef: + name: db-credentials + key: username + # 也可以批量注入所有 Secret key + # envFrom: + # - secretRef: + # name: db-credentials + # prefix: "SECRET_" +``` + +#### Volume 挂载 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: app-with-tls +spec: + containers: + - name: app + image: nginx:1.25 + volumeMounts: + - name: tls-certs + mountPath: /etc/tls + readOnly: true + - name: db-secret + mountPath: /etc/secrets + readOnly: true + volumes: + - name: tls-certs + secret: + secretName: my-tls-secret + defaultMode: 0400 # 仅 owner 可读 + items: + - key: tls.crt + path: server.crt + - key: tls.key + path: server.key + - name: db-secret + secret: + secretName: db-credentials + defaultMode: 0440 # owner + group 可读 +``` + +> [!info] tmpfs 存储 +> Secret 通过 Volume 挂载时使用 tmpfs(内存文件系统),数据不会写入节点的磁盘。这避免了敏感数据在磁盘上残留的风险,但会占用少量内存。 + +### 安全最佳实践 + +#### 1. 使用外部密钥管理系统 + +在生产环境中,推荐使用专业的密钥管理系统而非原生 Secret: + +```mermaid +flowchart TD + subgraph External["外部密钥管理"] + VAULT["HashiCorp Vault"] + AWS["AWS Secrets Manager"] + GCP["GCP Secret Manager"] + end + + subgraph K8s["Kubernetes"] + CSI["Secrets Store CSI Driver"] + SYNC["External Secrets Operator"] + POD["Pod"] + end + + VAULT --> CSI + AWS --> CSI + GCP --> CSI + CSI -->|挂载为 Volume| POD + + VAULT --> SYNC + AWS --> SYNC + SYNC -->|同步为 K8s Secret| POD +``` + +- **Secrets Store CSI Driver**:将外部密钥直接挂载为 Volume,不在 etcd 中存储。 +- **External Secrets Operator**:从外部系统同步密钥为 K8s Secret,适合需要原生 Secret 兼容的场景。 + +#### 2. RBAC 权限控制 + +```yaml +# 限制特定 ServiceAccount 只能读取特定 Secret +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: secret-reader + namespace: production +rules: + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["db-credentials", "api-keys"] # 仅限特定 Secret + verbs: ["get"] # 仅允许读取 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: app-secret-reader + namespace: production +subjects: + - kind: ServiceAccount + name: my-app-sa + namespace: production +roleRef: + kind: Role + name: secret-reader + apiGroup: rbac.authorization.k8s.io +``` + +#### 3. 审计日志 + +启用 API Server 的审计日志,追踪 Secret 的读写操作: + +```yaml +# /etc/kubernetes/audit-policy.yaml +apiVersion: audit.k8s.io/v1 +kind: Policy +rules: + # 记录所有对 Secret 的读写操作 + - level: Metadata + resources: + - group: "" + resources: ["secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +``` + +#### 4. 其他安全建议 + +- **避免在 Pod Spec 中直接写 `stringData`**:生产环境应使用 `data` + Base64,或通过 CI/CD 管道注入。 +- **定期轮换密钥**:结合 External Secrets Operator 实现自动轮换。 +- **避免将 Secret 推送到 Git**:使用 `.gitignore` 排除 Secret YAML,或使用 Sealed Secrets / SOPS 加密后再提交。 +- **最小权限原则**:每个 ServiceAccount 只应拥有它所需的最小 Secret 访问权限。 + +--- + +## 配置热更新实践 + +在生产环境中,单纯的 ConfigMap Volume 挂载虽然支持热更新,但应用本身还需要 reload 机制。以下是几种常见的无重启配置刷新方案。 + +### 方案一:ConfigMap Reloader(Sidecar) + +[stakater/ConfigMapReloader](https://github.com/stakater/Reloader) 是一个 Kubernetes Controller,当它检测到关联的 ConfigMap/Secret 发生变更时,会自动触发 Deployment 的滚动更新(rollout restart),从而实现配置刷新。 + +```yaml +# 部署 Reloader 后,在 Deployment 中添加注解 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app + annotations: + # Reloader 监听这些 ConfigMap/Secret 的变更 + reloader.stakater.com/reload: "app-config,db-credentials" +spec: + template: + spec: + containers: + - name: app + image: my-app:1.0 +``` + +> [!tip] 原理 +> Reloader 并非真正实现"热更新",而是通过修改 Pod Template 的 annotation 触发 Deployment 的滚动更新。本质上是重启 Pod,但对用户无感知(滚动更新保证零停机)。 + +### 方案二:应用内 Watch 机制 + +一些应用框架原生支持配置文件监听: + +```go +// Go 应用使用 fsnotify 监听配置文件变更 +import "github.com/fsnotify/fsnotify" + +func watchConfig(path string, reload func()) { + watcher, _ := fsnotify.NewWatcher() + defer watcher.Close() + watcher.Add(path) + + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + log.Println("Config file modified, reloading...") + reload() + } + case err := <-watcher.Errors: + log.Println("Watch error:", err) + } + } +} +``` + +### 方案三:SubPath 挂载 + 特殊处理 + +当使用 `subPath` 挂载时,ConfigMap 的更新不会自动传播到容器。如果必须使用 `subPath`,需要配合其他方案(如 Reloader 触发重启): + +```yaml +volumeMounts: + - name: config-volume + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf # 只挂载单个文件到指定路径 +``` + +> [!warning] SubPath 无法热更新 +> 使用 `subPath` 挂载的文件不会随 ConfigMap 更新而更新。这是因为 kubelet 为 subPath 创建的是符号链接或独立 bind mount,不在正常的 sync 范围内。如果需要热更新,请避免使用 `subPath`。 + +--- + +## 常见陷阱与最佳实践 + +### 陷阱一:ConfigMap 大小限制(1MB) + +Kubernetes 对 ConfigMap 和 Secret 有 **1MB** 的大小限制(这是 etcd 的 kv 对大小限制决定的)。 + +``` +# 错误:ConfigMap 超过 1MB 会创建失败 +The ConfigMap "xxx" is invalid: data: Too long: must have at most 1048576 bytes +``` + +**解决方案**: + +- 拆分为多个 ConfigMap。 +- 使用外部配置存储(如 Consul、Nacos)。 +- 对于大型配置文件,考虑挂载到 PV。 + +### 陷阱二:环境变量命名冲突 + +当使用 `envFrom` 批量导入时,多个 ConfigMap/Secret 中可能存在同名 key,后导入的会覆盖先前的。 + +```yaml +envFrom: + - configMapRef: + name: base-config # 含 APP_ENV=staging + - configMapRef: + name: override-config # 含 APP_ENV=production(覆盖) +``` + +> [!tip] 使用 prefix 避免冲突 +> 为每个 `envFrom` 来源设置不同的 `prefix`,如 `base-config` 用 `BASE_`,`override-config` 用 `OVERRIDE_`。 + +### 陷阱三:Secret 的 watch 机制开销 + +Pod 通过 Volume 引用 Secret 时,kubelet 会 watch 该 Secret 的变更。在大规模集群中,大量 Secret 的 watch 可能导致 API Server 压力过大。 + +**缓解措施**: + +- 使用 `immutable: true` 标记不会变更的 Secret。 +- 合并小 Secret,减少 watch 对象数量。 +- 使用 KMS Provider 加密时,注意解密操作的性能开销。 + +### 陷阱四:文件权限与安全上下文 + +Secret 默认权限为 `0400`,如果容器进程以非 root 用户运行,可能无法读取。 + +```yaml +# 解决方案:调整 Secret 文件权限 +volumes: + - name: secret-volume + secret: + secretName: app-secret + defaultMode: 0444 # 所有用户可读 + # 或者精确设置 + items: + - key: config.yaml + path: config.yaml + mode: 0644 +``` + +### 最佳实践总结 + +| 实践 | 说明 | +|------|------| +| ConfigMap 与 Config 文件分离 | 不同环境的配置使用不同的 ConfigMap | +| 使用 `immutable: true` | 已发布配置标记为不可变,减轻 API Server 压力 | +| Secret 使用外部管理系统 | 生产环境使用 Vault/KMS,而非原生 Secret | +| 避免 `envFrom` 无差别导入 | 明确列出需要的 key,避免意外覆盖 | +| 避免 `subPath` 挂载需要热更新的配置 | `subPath` 不支持热更新,优先使用目录挂载 | +| 设置合理的 RBAC | 每个 ServiceAccount 只授予最小必要权限 | +| 配置版本管理 | ConfigMap 名称带版本号(如 `app-config-v3`),便于回滚 | +| CI/CD 中管理 Secret | 使用 Sealed Secrets、SOPS 或 External Secrets Operator | + +--- + +## 延伸阅读 + +- [[k8s-03-pod]] — Pod 生命周期与资源管理 +- [[k8s-04-deployment-and-replicaset]] — Deployment 滚动更新与回滚 +- [Kubernetes 官方文档 - ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) +- [Kubernetes 官方文档 - Secret](https://kubernetes.io/docs/concepts/configuration/secret/) +- [stakater/Reloader - ConfigMap/Secret 自动触发重启](https://github.com/stakater/Reloader) +- [Secrets Store CSI Driver](https://secrets-store-csi-driver.sigs.k8s.io/) +- [External Secrets Operator](https://external-secrets.io/) diff --git a/technical/k8s/k8s-07-storage.md b/technical/k8s/k8s-07-storage.md new file mode 100644 index 0000000..4002c27 --- /dev/null +++ b/technical/k8s/k8s-07-storage.md @@ -0,0 +1,625 @@ +--- +tags: [k8s, storage, pv, pvc, statefulset, devops] +create time: 2026-07-07 15:48 +--- + +# Kubernetes 存储 — PV、PVC 与 StatefulSet + +## 概述 + +容器的本质是短暂的(ephemeral):Pod 被删除后,其内部所有文件随之消散。这对无状态服务无关紧要,但对数据库、消息队列等有状态应用来说是致命的。Kubernetes 通过分层存储抽象 —— **Volume → PersistentVolume(PV)→ PersistentVolumeClaim(PVC)→ StorageClass** —— 将"存储供给"与"存储消费"解耦,让开发者无需关心底层基础设施,只需声明"我需要多大、什么访问模式的存储",即可自动获得持久化能力。配合 StatefulSet,K8s 还为有状态应用提供了稳定的网络标识和有序的生命周期管理。 + +## 卷(Volume)基础 + +Volume 是 K8s 最早引入的存储概念,与 Pod 同生命周期(除非是持久卷类型)。它定义在 Pod spec 中,可被同一 Pod 内的多个容器共享。 + +### 常见卷类型 + +| 卷类型 | 生命周期 | 典型用途 | 是否持久 | +|--------|----------|----------|----------| +| `emptyDir` | Pod 存续期间 | 容器间共享临时文件、缓存 | 否 | +| `hostPath` | 节点存续期间 | 访问宿主机文件(如 Docker socket) | 节点级持久 | +| `nfs` | 独立于 Pod | 多节点共享存储 | 是 | +| `configMap` / `secret` | Pod 存续期间 | 注入配置和敏感信息 | 否 | +| `persistentVolumeClaim` | 独立于 Pod | 绑定 PV,正式的持久化方案 | 是 | + +### emptyDir 示例 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: shared-data +spec: + containers: + - name: writer + image: busybox + command: ["sh", "-c", "echo hello > /data/msg && sleep 3600"] + volumeMounts: + - name: cache + mountPath: /data + - name: reader + image: busybox + command: ["sh", "-c", "cat /data/msg && sleep 3600"] + volumeMounts: + - name: cache + mountPath: /data + volumes: + - name: cache + emptyDir: {} +``` + +> [!tip] emptyDir 的 `medium: Memory` 配置可将其挂载为 tmpfs(RAM 磁盘),适用于对 I/O 延迟极敏感的场景,但会计入容器内存用量。 + +### hostPath 注意事项 + +hostPath 直接暴露宿主机目录,存在安全风险。生产环境应优先使用 PVC。如果必须使用,建议设置 `type` 字段限制行为: + +```yaml +volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket # 仅允许已存在的 socket 文件 +``` + +hostPath 的 `type` 可选值:`DirectoryOrCreate`、`Directory`、`FileOrCreate`、`File`、`Socket`、`CharDevice`、`BlockDevice`。 + +## 持久卷(PersistentVolume) + +PersistentVolume(PV)是集群级别的存储资源,由管理员预先创建或由 StorageClass 动态供给。它与 Pod 完全解耦 —— Pod 删除后 PV 仍然存在。 + +### PV 详解 + +```yaml +apiVersion: v1 +kind: PersistentVolume +metadata: + name: pv-nfs-data +spec: + capacity: + storage: 10Gi # 存储容量 + accessModes: + - ReadWriteMany # 访问模式 + persistentVolumeReclaimPolicy: Retain # 回收策略 + storageClassName: slow # 存储类名称 + nfs: + server: 192.168.1.100 + path: /exports/data +``` + +关键字段说明: + +| 字段 | 说明 | +|------|------| +| `capacity.storage` | PV 的存储容量,只能声明一种资源(目前仅支持 storage) | +| `accessModes` | 定义卷的挂载方式,可声明多种,但实际只能使用一种 | +| `persistentVolumeReclaimPolicy` | PVC 释放后 PV 的处理策略:Retain / Delete / Recycle | +| `storageClassName` | 关联的 StorageClass 名称,为空字符串表示不关联任何类 | +| `volumeMode` | `Filesystem`(默认)或 `Block`(原始块设备) | +| `mountOptions` | 挂载选项,如 `["hard", "nfsvers=4.1"]` | + +> [!info] PV 的状态包括 `Available`(未绑定)、`Bound`(已绑定到 PVC)、`Released`(PVC 已删除但 PV 未回收)、`Failed`(自动回收失败)。 + +### 回收策略(Reclaim Policy) + +当 PVC 被删除后,PV 的回收策略决定了其后续行为。 + +| 策略 | 行为 | 适用场景 | +|------|------|----------| +| **Retain** | PV 状态变为 `Released`,数据保留,需管理员手动处理 | 有保留价值的数据 | +| **Delete** | PV 及底层存储资源一并删除 | 动态供给的临时存储 | +| **Recycle** | 执行 `rm -rf /volume/*`,PV 回到 `Available`(已废弃) | 仅兼容简单存储后端 | + +```mermaid +stateDiagram-v2 + [*] --> Available: PV 创建 + Available --> Bound: PVC 绑定 + Bound --> Released: PVC 删除 + + Released --> Available: Recycle (rm -rf) + Released --> [*]: Delete (删除底层存储) + Released --> Released: Retain (等待手动处理) + + state "管理员手动操作" as manual + Released --> manual: 需人工介入 + manual --> Available: 修复后重新上线 + manual --> [*]: 彻底删除 +``` + +> [!warning] Recycle 策略已被废弃,且仅对 NFS 和 hostPath 有效。生产环境应优先使用动态供给 + Delete 策略,或 Retain 策略配合手动运维。 + +### 访问模式(Access Modes) + +| 模式 | 缩写 | 含义 | +|------|------|------| +| ReadWriteOnce | RWO | 单节点读写挂载 | +| ReadOnlyMany | ROX | 多节点只读挂载 | +| ReadWriteMany | RWX | 多节点读写挂载 | +| ReadWriteOncePod | RWOP | 单 Pod 读写(K8s 1.22+) | + +各存储后端对访问模式的支持情况: + +| 存储后端 | RWO | ROX | RWX | RWOP | +|----------|-----|-----|-----|------| +| 本地磁盘 (local) | 支持 | - | - | 支持 | +| AWS EBS | 支持 | - | - | 支持 | +| GCE PD | 支持 | 支持 | - | 支持 | +| Azure Disk | 支持 | - | - | 支持 | +| Azure File | 支持 | 支持 | 支持 | - | +| Ceph RBD | 支持 | 支持 | - | - | +| CephFS | 支持 | 支持 | 支持 | - | +| NFS | 支持 | 支持 | 支持 | - | +| Longhorn | 支持 | 支持 | 支持 | - | + +> [!tip] 为什么不是所有存储都支持 RWX?因为多节点同时写入需要分布式文件系统级别的并发控制(如分布式锁、一致性协议)。块存储(EBS、PD)天然不支持多写,必须通过 NFS/文件系统层封装才能实现 RWX。 + +## 持久卷声明(PersistentVolumeClaim) + +PVC 是用户对存储资源的"请求"。用户不直接创建或管理 PV,而是通过 PVC 声明需求(容量、访问模式、存储类),K8s 负责将合适的 PV 绑定到 PVC。 + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi + storageClassName: fast-ssd +``` + +### PVC 绑定机制 + +PVC 与 PV 的绑定有两种模式: + +#### 静态供给(Static Provisioning) + +管理员手动创建 PV 资源,用户创建 PVC 后,K8s 控制面中的 PV controller 根据容量、访问模式、存储类等条件进行匹配。 + +#### 动态供给(Dynamic Provisioning) + +用户创建 PVC 后,若集群中没有匹配的 PV,且 PVC 指定了 StorageClass,K8s 会调用对应的 provisioner 自动创建 PV 并完成绑定。这是生产环境中最常用的方式。 + +```mermaid +flowchart TD + A[用户创建 PVC] --> B{是否有匹配的 Available PV?} + B -->|是| C[绑定 PV 到 PVC] + B -->|否| D{PVC 指定了 StorageClass?} + D -->|否| E[PVC 持续 Pending] + D -->|是| F[StorageClass Provisioner 创建 PV] + F --> C + C --> G[Pod 挂载 PVC 使用] + + style E fill:#f96,stroke:#333 + style G fill:#6f9,stroke:#333 +``` + +绑定匹配规则: +1. PV 与 PVC 的 `storageClassName` 必须一致 +2. PV 的 `accessModes` 必须包含 PVC 请求的模式 +3. PV 的 `capacity.storage` 必须 >= PVC 请求的容量 +4. PV 的 `volumeMode` 必须匹配 +5. 若存在多个匹配 PV,优先选择容量最小的满足者(最小匹配原则) +6. 可通过标签选择器(`selector`)进一步筛选 + +> [!info] 一对一绑定:一个 PV 只能绑定一个 PVC,一个 PVC 也只能绑定一个 PV。绑定后即使 PV 的标签被修改,绑定关系也不会解除。 + +### StorageClass + +StorageClass 定义了存储的"类别",是动态供给的核心。它告诉 K8s 使用哪个 provisioner、以什么参数创建存储。 + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: fast-ssd +provisioner: kubernetes.io/aws-ebs # 存储驱动 +parameters: + type: gp3 # 传递给 provisioner 的参数 + fsType: ext4 + iopsPerGB: "50" +reclaimPolicy: Delete # PV 回收策略 +volumeBindingMode: WaitForFirstConsumer # 绑定时机 +allowVolumeExpansion: true # 允许扩容 +mountOptions: + - debug +``` + +关键字段详解: + +| 字段 | 说明 | 常见值 | +|------|------|--------| +| `provisioner` | 存储驱动标识 | `kubernetes.io/aws-ebs`、`rancher.io/local-path`、`csi.hetzner.cloud` 等 | +| `parameters` | 传递给 provisioner 的键值对,不同驱动参数不同 | EBS: `type`/`fsType`;Ceph: `pool`/`clusterID` | +| `reclaimPolicy` | 动态创建的 PV 的回收策略,默认 `Delete` | `Retain`、`Delete` | +| `volumeBindingMode` | PV 创建和绑定的时机 | `Immediate`(默认)、`WaitForFirstConsumer` | +| `allowVolumeExpansion` | 是否允许 PVC 扩容,默认 `false` | `true`、`false` | +| `mountOptions` | 动态创建的 PV 的挂载选项 | NFS: `["hard","nfsvers=4.1"]` | + +> [!warning] `volumeBindingMode: WaitForFirstConsumer` 的重要性:默认的 `Immediate` 模式会在 PVC 创建时立即绑定/创建 PV,这可能导致 PV 创建在与 Pod 不同的可用区(尤其在多可用区集群中),最终 Pod 调度失败。`WaitForFirstConsumer` 会延迟到 Pod 调度时才创建 PV,确保存储与计算在同一拓扑域。 + +```mermaid +sequenceDiagram + participant User as 用户 + participant API as API Server + participant SC as StorageClass + participant PV Controller + participant Provisioner + + Note over User,Provisioner: Immediate 模式 + User->>API: 创建 PVC + API->>PV Controller: 协调 PVC + PV Controller->>Provisioner: 创建 PV + Provisioner-->>PV Controller: PV Ready + PV Controller->>API: 绑定 PV ↔ PVC + + Note over User,Provisioner: WaitForFirstConsumer 模式 + User->>API: 创建 PVC (Pending) + User->>API: 创建 Pod + API->>PV Controller: Pod 调度完成,触发绑定 + PV Controller->>Provisioner: 在 Pod 所在节点/可用区创建 PV + Provisioner-->>PV Controller: PV Ready + PV Controller->>API: 绑定 PV ↔ PVC +``` + +## StatefulSet + +StatefulSet 是 K8s 为有状态应用设计的工作负载控制器,解决了 Deployment 无法满足的三个核心需求: + +1. **稳定的网络标识**:Pod 名称固定(`pod-0`、`pod-1`),通过 Headless Service 提供 DNS 记录(`pod-0.service-name.namespace.svc.cluster.local`) +2. **稳定的持久存储**:每个 Pod 拥有独立的 PVC,Pod 重建后 PVC 不变 +3. **有序的部署/扩缩/删除**:Pod 按序号顺序操作,前一个就绪后才启动下一个 + +### StatefulSet vs Deployment + +| 特性 | Deployment | StatefulSet | +|------|-----------|-------------| +| Pod 名称 | 随机后缀 | 有序编号(`-0`、`-1`、`-2`) | +| 部署顺序 | 并行 | 顺序(前一个 Ready 后才启动下一个) | +| 删除顺序 | 并行 | 逆序 | +| 网络标识 | 不稳定 | 稳定,配合 Headless Service | +| 存储 | 共享 PVC 或无存储 | 每个 Pod 独立 PVC(通过 `volumeClaimTemplates`) | +| 缩容 | 随机删除 | 从最大序号开始删除 | + +### volumeClaimTemplates + +`volumeClaimTemplates` 是 StatefulSet 的核心特性 —— 它会为每个 Pod 自动创建独立的 PVC。 + +```yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mysql +spec: + serviceName: mysql # 必须关联 Headless Service + replicas: 3 + selector: + matchLabels: + app: mysql + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + volumeMounts: + - name: data + mountPath: /var/lib/mysql + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: fast-ssd + resources: + requests: + storage: 20Gi +``` + +上例中,K8s 会自动创建三个 PVC:`data-mysql-0`、`data-mysql-1`、`data-mysql-2`,每个绑定独立的 PV。 + +> [!info] StatefulSet 缩容时,PVC 不会被删除。这意味着缩容后再扩容,新 Pod 会重新挂载原来的 PVC,数据不丢失。这是 StatefulSet 最重要的特性之一。 + +## CSI(Container Storage Interface) + +CSI 是 K8s 存储插件的标准化接口规范(自 K8s 1.13 GA),取代了早期的 in-tree volume plugin 方式。 + +### 为什么需要 CSI + +早期 K8s 的存储驱动代码直接存在于 K8s 主仓库(in-tree),导致: +- 存储供应商必须将代码合入 K8s 仓库,发布周期受限于 K8s +- K8s 仓库臃肿,维护负担重 +- 驱动 bug 可能影响 K8s 核心组件稳定性 + +CSI 将存储驱动移出 K8s 核心(out-of-tree),通过 gRPC 接口标准化通信。 + +### CSI 架构 + +```mermaid +graph TD + subgraph "Kubernetes 组件" + AK[API Server] + KC[kube-controller-manager] + KM[kubelet] + end + + subgraph "CSI 组件(集群级别)" + CP[CSI Controller Plugin] + CS[CSI Driver 注册] + end + + subgraph "CSI 组件(节点级别)" + NP[CSI Node Plugin] + end + + subgraph "存储后端" + S[Cloud Provider / SAN / NFS] + end + + AK --> KC + KC -->|创建/删除/扩容| CP + CP --> S + KM -->|挂载/卸载| NP + NP --> S + CS -.->|注册| AK +``` + +CSI 的三个核心组件: +- **CSI Controller Plugin**:集群级别,负责卷的创建、删除、快照、扩容等控制面操作 +- **CSI Node Plugin**:节点级别,负责卷的挂载(NodePublishVolume)、卸载(NodeUnpublishVolume)等数据面操作 +- **CSIDriver 对象**:在 K8s 中注册驱动,声明能力(如 `attachRequired`、`podInfoOnMount`) + +### 常见 CSI 驱动 + +| 存储后端 | CSI 驱动名称 | 说明 | +|----------|-------------|------| +| AWS EBS | `ebs.csi.aws.com` | AWS 官方,替代 in-tree aws-ebs | +| GCE PD | `pd.csi.storage.gke.io` | GCP 官方 | +| Azure Disk | `disk.csi.azure.com` | Azure 官方 | +| Ceph RBD | `rbd.csi.ceph.com` | Ceph 官方 | +| Longhorn | `driver.longhorn.io` | Rancher 开源分布式存储 | +| OpenEBS | `openebs.io/local` / `openebs.io/zfs-localpv` | 开源容器原生存储 | +| Hetzner Cloud | `csi.hetzner.cloud` | Hetzner 云盘 | +| 本地路径 | `rancher.io/local-path` | Local Path Provisioner,开发测试常用 | + +### CSI Driver 部署示例 + +以 AWS EBS CSI Driver 为例,通常通过 Helm 安装: + +```bash +helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver +helm install aws-ebs-csi-driver aws-ebs-csi-driver/aws-ebs-csi-driver \ + --namespace kube-system +``` + +安装后会在集群中创建 `ebs.csi.aws.com` CSIDriver 对象和对应的 DaemonSet(Node Plugin)+ Deployment(Controller Plugin)。 + +## 实战示例 + +以下完整示例演示如何部署一个带持久化存储的 MySQL StatefulSet。 + +### 1. StorageClass + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: mysql-ssd +provisioner: rancher.io/local-path # 本地开发环境使用 local-path +reclaimPolicy: Retain # 数据库场景用 Retain 更安全 +volumeBindingMode: WaitForFirstConsumer +allowVolumeExpansion: true +``` + +### 2. Headless Service + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: mysql + labels: + app: mysql +spec: + ports: + - port: 3306 + targetPort: 3306 + name: mysql + clusterIP: None # Headless Service,StatefulSet 必需 + selector: + app: mysql +``` + +### 3. StatefulSet + +```yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mysql +spec: + serviceName: mysql + replicas: 1 # 单节点 MySQL,生产建议配合 Operator 做主从 + selector: + matchLabels: + app: mysql + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + ports: + - containerPort: 3306 + name: mysql + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-secret + key: ROOT_PASSWORD + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 10 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: mysql-data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: mysql-ssd + resources: + requests: + storage: 10Gi +``` + +### 4. Secret(MySQL root 密码) + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: mysql-secret +type: Opaque +stringData: + ROOT_PASSWORD: "MyS3cur3P@ssw0rd" +``` + +### 5. 客户端 Pod 测试连接 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: mysql-client +spec: + containers: + - name: mysql-client + image: mysql:8.0 + command: ["sleep", "3600"] +``` + +进入客户端 Pod 后,使用 StatefulSet 的稳定 DNS 名称连接: + +```bash +kubectl exec -it mysql-client -- bash +mysql -h mysql-0.mysql.default.svc.cluster.local -u root -p +``` + +> [!tip] DNS 解析规则:`...svc.cluster.local`。`mysql-0.mysql` 即可解析,完整 FQDN 为 `mysql-0.mysql.default.svc.cluster.local`。 + +## 常见陷阱与最佳实践 + +### PVC 持续 Pending + +**现象**:PVC 长时间处于 Pending 状态,Pod 无法启动。 + +**排查步骤**: + +```bash +# 查看 PVC 事件 +kubectl describe pvc + +# 查看是否有匹配的 PV +kubectl get pv + +# 查看 StorageClass 是否存在 +kubectl get storageclass +``` + +**常见原因**: +- 没有匹配的 Available PV(静态供给场景下) +- StorageClass 的 provisioner 未安装或配置错误 +- `volumeBindingMode: WaitForFirstConsumer` 下,没有 Pod 消费该 PVC +- 底层存储配额不足或权限不足 + +### 动态供给配置错误 + +**现象**:PVC Pending,事件中显示 `waiting for first consumer to be created before binding`。 + +**排查**:确认 `volumeBindingMode` 设置是否符合预期。如果 Pod 已经存在但仍未绑定,检查: +- Pod 是否成功调度到节点(Pod 事件) +- CSI Node Plugin 是否在目标节点正常运行(DaemonSet Pod 状态) +- Provisioner 的参数是否正确(如 `parameters.type` 是否为合法值) + +### RWX 不支持的存储类型 + +**现象**:PVC 声明 `ReadWriteMany` 但一直 Pending。 + +**原因**:块存储(EBS、GCE PD、Azure Disk)不支持 RWX。解决方案: +- 使用支持 RWX 的存储后端:NFS、CephFS、Azure File、Longhorn、OpenEBS +- 使用 ReadWriteOnce(如果应用本身不支持多写,如 MySQL 单实例) +- 引入分布式锁机制或应用层复制 + +### PVC 扩容 + +扩容需要 StorageClass 设置 `allowVolumeExpansion: true`,且 CSI 驱动支持在线扩容: + +```bash +# 编辑 PVC 请求的容量 +kubectl patch pvc mysql-data-mysql-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}' + +# 查看扩容进度 +kubectl describe pvc mysql-data-mysql-0 +``` + +> [!warning] PVC 扩容只能增大不能缩小。部分存储后端支持在线扩容(无需重启 Pod),部分需要 unmount 后才能扩容,具体取决于 CSI 驱动实现。 + +### 数据备份策略 + +PV 中的数据没有内置的备份机制,需要额外配置: + +- **VolumeSnapshot**:K8s 1.20+ GA,通过 CSI 驱动创建卷快照 +- **Velero**:开源的 K8s 备份恢复工具,支持卷快照 + 资源对象的完整备份 +- **应用层备份**:对于数据库,使用 `mysqldump`、`pg_dump` 等原生工具定期备份 +- **存储层快照**:利用云平台的 EBS Snapshot、Azure Snapshot 等定期快照 + +```yaml +# VolumeSnapshot 示例 +apiVersion: snapshot.storage.k8s.io/v1 +kind: VolumeSnapshot +metadata: + name: mysql-snapshot +spec: + volumeSnapshotClassName: csi-aws-ebs-snapclass + source: + persistentVolumeClaimName: mysql-data-mysql-0 +``` + +### 最佳实践总结 + +1. **始终使用 `volumeBindingMode: WaitForFirstConsumer`**,避免多可用区集群中存储与计算分离 +2. **数据库场景使用 `reclaimPolicy: Retain`**,防止误删 PVC 导致数据丢失 +3. **为 StatefulSet 配置 Readiness Probe**,确保 K8s 在 Pod 真正就绪后才启动下一个 +4. **生产环境使用 CSI 驱动替代 in-tree plugin**,获得更好的维护性和功能支持 +5. **定期备份 PV 数据**,不要依赖 K8s 的存储抽象作为唯一的持久化保障 +6. **监控 PV 使用率**,设置 PVC 扩容告警,避免磁盘满导致服务中断 +7. **避免使用 `hostPath`** 作为生产存储方案,除非有明确的安全和隔离措施 + +## 延伸阅读 + +- [[k8s-03-pod]] — Pod 基础与生命周期 +- [[k8s-02-architecture]] — Kubernetes 整体架构 diff --git a/technical/k8s/k8s-08-observability.md b/technical/k8s/k8s-08-observability.md new file mode 100644 index 0000000..df53385 --- /dev/null +++ b/technical/k8s/k8s-08-observability.md @@ -0,0 +1,677 @@ +--- +tags: [k8s, observability, logging, monitoring, devops] +create time: 2026-07-07 15:48 +--- + +# Kubernetes 可观测性 — 日志、监控与健康检查 + +## 概述 + +可观测性(Observability)是理解系统内部状态的能力,在 Kubernetes 中通常由三大支柱构成:**Logging(日志)**、**Metrics(指标)** 和 **Tracing(链路追踪)**。日志告诉你「发生了什么」,指标告诉你「系统状态如何」,追踪告诉你「请求经过了哪里」。三者相互补充,缺一不可。在 K8s 环境中,由于 Pod 生命周期短暂、服务动态伸缩,传统的 SSH 进机器查日志方式不再适用,必须依赖系统化的可观测性方案。本文将从健康检查探针入手,逐步覆盖日志采集、指标监控和事件管理的完整实践。 + +--- + +## 健康检查(Probe)深入 + +Kubernetes 通过探针(Probe)机制持续监测容器的健康状态,自动完成故障隔离与自愈。这是 K8s 可观测性的第一道防线——不需要外部工具,原生能力就能实现基本的服务可用性保障。 + +### 三种探针详解 + +K8s 提供三种探针,各有分工: + +| 探针 | 作用 | 失败后果 | 典型场景 | +|------|------|----------|----------| +| **livenessProbe** | 检测容器是否存活 | 杀死容器并重启(遵循 restartPolicy) | 应用死锁、无限循环 | +| **readinessProbe** | 检测容器是否就绪接收流量 | 从 Service 的 Endpoints 中摘除 | 依赖未就绪、预热加载中 | +| **startupProbe** | 检测容器是否完成启动 | 杀死容器并重启 | 慢启动应用(如 Java、大型模型加载) | + +#### 配置参数说明 + +所有探针共享以下参数: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `initialDelaySeconds` | int | 0 | 容器启动后多久开始第一次探测 | +| `periodSeconds` | int | 10 | 探测间隔(秒) | +| `timeoutSeconds` | int | 1 | 探测超时时间(秒) | +| `failureThreshold` | int | 3 | 连续失败多少次后判定为不健康 | +| `successThreshold` | int | 1 | 连续成功多少次后恢复(liveness 固定为 1) | + +> [!tip] startupProbe 与 livenessProbe 的协作 +> 当 `startupProbe` 配置后,`livenessProbe` 和 `readinessProbe` 会在 startupProbe 成功之后才生效。这意味着你可以用一个宽松的 startupProbe(比如 `failureThreshold: 30`、`periodSeconds: 10`,即允许最多 300 秒启动时间),而 livenessProbe 保持较严格的配置,避免慢启动应用被误杀。 + +### 探测方式 + +K8s 支持四种探测机制,选择哪种取决于应用特性和暴露的端口。 + +#### exec — 执行命令探测 + +适用于:自定义健康检查逻辑、需要检查内部状态的场景(如数据库连接池)。 + +```yaml +livenessProbe: + exec: + command: + - /bin/sh + - -c + - "pg_isready -U postgres" + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 +``` + +容器内执行指定命令,退出码为 0 表示健康,非 0 表示不健康。适用于需要检查进程内部状态(如自定义脚本检测连接池)的场景。 + +#### httpGet — HTTP 请求探测 + +适用于:Web 服务、API 服务,最常用的探测方式。 + +```yaml +livenessProbe: + httpGet: + path: /healthz + port: 8080 + httpHeaders: + - name: X-Custom-Header + value: "k8s-probe" + initialDelaySeconds: 10 + periodSeconds: 15 + timeoutSeconds: 3 + failureThreshold: 3 +``` + +向容器的指定端口和路径发起 HTTP GET 请求,2xx/3xx 状态码视为健康。建议为健康检查设计轻量级的专用端点,避免在 `/` 上做复杂业务校验。 + +#### tcpSocket — TCP 端口探测 + +适用于:非 HTTP 协议的 TCP 服务(如数据库、Redis、gRPC 服务端口)。 + +```yaml +readinessProbe: + tcpSocket: + port: 3306 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 +``` + +尝试建立 TCP 连接,连接成功即视为健康。适合 MySQL、Redis 等数据库服务,或尚不支持 HTTP 健康检查端点的遗留服务。 + +#### gRPC — gRPC 健康检查探测 + +适用于:gRPC 微服务(K8s 1.24+ GA)。 + +```yaml +readinessProbe: + grpc: + port: 50051 + # 可选:指定服务名 + # service: "my-grpc-service" + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 +``` + +利用 gRPC 标准的 [Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) 进行探测,比 httpGet 更自然地适配 gRPC 服务。需要应用实现 `grpc.health.v1.Health` 服务。 + +### 探针设计原则 + +> [!tip] 原则一:慢启动应用必须用 startupProbe +> Java 应用、机器学习模型加载、大型缓存预热等场景,启动时间可能超过 livenessProbe 的 `initialDelaySeconds`。此时应该用 startupProbe 兜底,而不是无限增大 `initialDelaySeconds`——后者会导致所有重启都白白等待固定时长。 + +```yaml +# 典型的慢启动应用探针配置 +startupProbe: + httpGet: + path: /healthz + port: 8080 + failureThreshold: 30 # 允许最多 30 次失败 + periodSeconds: 10 # 每 10 秒检查一次 + # 总共允许 30 × 10 = 300 秒启动时间 + +livenessProbe: + httpGet: + path: /healthz + port: 8080 + periodSeconds: 15 + timeoutSeconds: 3 + failureThreshold: 3 # 启动后 3 次失败即重启 +``` + +> [!tip] 原则二:readiness ≠ liveness,职责不能混 +> - **readinessProbe**:「我准备好接收请求了吗?」——可以是暂时性的(如依赖 Redis 还没连上),摘除流量后恢复即可重新加入。 +> - **livenessProbe**:「我还活着吗?」——失败意味着进程级故障(死锁、崩溃),需要重启。 +> - 常见错误:把依赖检查(如数据库连接)放在 livenessProbe 里。数据库短暂不可达会导致 Pod 被重启,反而加重问题。 + +> [!tip] 原则三:健康端点要轻量 +> livenessProbe 的 `/healthz` 端点不应该检查下游依赖(数据库、Redis 等),否则下游抖动会导致 Pod 被误杀。readinessProbe 可以做适度的依赖检查,但也要控制超时。 + +> [!warning] 避免探针与应用抢占资源 +> exec 探针会 fork 子进程,在资源受限的容器中可能加剧 OOM。高并发场景下优先使用 httpGet 或 tcpSocket。 + +#### 探针决策流程图 + +```mermaid +flowchart TD + A[应用需要健康检查] --> B{启动时间 > 30 秒?} + B -->|是| C[配置 startupProbe] + B -->|否| D{需要检查依赖?} + C --> D + D -->|是, 如 DB/Redis| E[readinessProbe 检查依赖] + D -->|否| F[readinessProbe 基础端口/路径] + E --> G{livenessProbe 如何配置?} + F --> G + G --> H[仅检查进程存活
不检查下游依赖] + H --> I{协议类型?} + I -->|HTTP| J[httpGet /healthz] + I -->|gRPC| K[grpc health check] + I -->|TCP/数据库| L[tcpSocket port] + I -->|自定义逻辑| M[exec command] +``` + +--- + +## 日志管理 + +### K8s 日志架构 + +在 Kubernetes 中,日志的生命周期跨越三个层级:容器层、节点层、集群层。理解这个链路是做好日志管理的前提。 + +```mermaid +flowchart LR + subgraph 容器层 + A[应用进程] -->|stdout / stderr| B[容器运行时
CRI] + end + + subgraph 节点层 + B -->|写入文件| C["/var/log/pods/"] + C --> D[kubelet 日志轮转] + end + + subgraph 集群层 + D --> E[日志采集 Agent
Fluentd / Promtail / Filebeat] + E --> F[存储后端
Elasticsearch / Loki / S3] + F --> G[查询 UI
Kibana / Grafana] + end + + A -.->|Sidecar 模式| H[日志 Sidecar 容器] + H --> F +``` + +K8s 推荐的最佳实践是:**应用将日志输出到 stdout/stderr**,由 kubelet 负责写入节点磁盘,再由集群级采集工具统一收集。这样做解耦了应用和日志基础设施,应用无需关心日志的存储和传输。 + +### 节点级日志 + +#### 目录结构 + +kubelet 将每个容器的 stdout/stderr 写入节点上的日志文件,路径格式为: + +``` +/var/log/pods/__//.log +``` + +实际示例: + +``` +/var/log/pods/default_nginx-7d8b49557c-abc123_5d1f3a2e-1234-5678-abcd-ef0123456789/ +├── nginx/ +│ ├── 0.log # 第 0 次启动的日志 +│ └── 1.log # 第 1 次重启后的日志 +└── nginx/ + └── 0.log -> /var/log/containers/nginx-xxx.log # 符号链接 +``` + +`/var/log/containers/` 目录下的文件是指向 `/var/log/pods/` 的符号链接,文件名包含容器名和容器 ID,方便按容器名查找。 + +#### 日志轮转(Log Rotation) + +kubelet 通过以下参数控制日志轮转(配置在 kubelet 的 `--container-log-*` 参数中): + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `--container-log-max-size` | 单个日志文件最大大小 | 10Mi | +| `--container-log-max-files` | 每个容器保留的日志文件数 | 5 | + +在 `KubeletConfiguration` 中配置: + +```yaml +apiVersion: kubelet.config.k8s.io/v1beta1 +kind: KubeletConfiguration +containerLogMaxSize: "50Mi" +containerLogMaxFiles: 10 +``` + +> [!warning] 节点磁盘空间 +> 日志量大的应用(如高并发网关)可能快速占满磁盘。务必设置合理的轮转策略,并配合监控节点磁盘使用率(`node_filesystem_avail_bytes` 指标)。 + +### 集群级方案 + +#### 方案对比 + +| 维度 | EFK(Elasticsearch + Fluentd + Kibana) | Loki + Promtail + Grafana | +|------|------------------------------------------|---------------------------| +| **存储模型** | 全文索引,存储成本高 | 标签索引 + 压缩块存储,成本低 | +| **查询语言** | KQL / Lucene | LogQL(类 PromQL 语法) | +| **全文搜索** | 原生支持,性能好 | 不支持原生全文搜索,需配合标签 | +| **资源消耗** | 高(JVM 内存、磁盘 IO) | 低(Go 二进制,无全文索引) | +| **与 Metrics 集成** | 需额外对接 Prometheus | 天然与 Prometheus/Grafana 生态融合 | +| **适用场景** | 大规模日志分析、全文检索需求强 | 资源敏感、与 Prometheus 混合部署 | +| **社区趋势** | 成熟但逐渐被替代 | CNCF 毕业项目,增长迅速 | + +#### EFK 架构 + +```mermaid +flowchart LR + A[Pod stdout/stderr] --> B[节点日志文件] + B --> C[Fluentd DaemonSet] + C --> D[Elasticsearch] + D --> E[Kibana] + E --> F[用户查询] +``` + +Fluentd 以 DaemonSet 形式部署在每个节点上,tail 节点日志文件并推送到 Elasticsearch。 + +#### Loki + Promtail 架构 + +```mermaid +flowchart LR + A[Pod stdout/stderr] --> B[节点日志文件] + B --> C[Promtail DaemonSet] + C --> D[Loki] + D --> E[Grafana] + E --> F[用户查询] +``` + +Promtail 同样以 DaemonSet 部署,但它只提取标签(namespace、pod、container)和日志行,发送给 Loki 进行存储。Loki 的核心设计理念是「like Prometheus, but for logs」——只对标签建索引,日志内容以压缩块存储。 + +### 结构化日志 + +非结构化日志(纯文本)在大规模集群中难以高效查询。结构化日志以 JSON 格式输出,便于日志系统自动解析和过滤。 + +#### JSON 日志输出示例(Go / zap) + +```go +import "go.uber.org/zap" + +logger, _ := zap.NewProduction() +defer logger.Sync() + +logger.Info("request processed", + zap.String("method", "GET"), + zap.String("path", "/api/v1/users"), + zap.Int("status", 200), + zap.Duration("latency", 150*time.Millisecond), + zap.String("request_id", "abc-123"), +) +``` + +输出: + +```json +{"level":"info","ts":1720340880.123,"msg":"request processed","method":"GET","path":"/api/v1/users","status":200,"latency":0.15,"request_id":"abc-123"} +``` + +#### 结构化日志最佳实践 + +1. **统一字段命名**:团队内约定日志字段名(如 `request_id` 而非 `reqId` / `rid`),便于跨服务聚合。 +2. **必须包含的字段**:时间戳(`ts`)、日志级别(`level`)、消息(`msg`)、请求 ID(`request_id`)、服务名(`service`)。 +3. **避免在日志中输出敏感信息**:密码、Token、身份证号等必须脱敏。 +4. **日志级别控制**:生产环境默认 `info`,可通过环境变量动态调整为 `debug` 以便排查问题。 + +> [!tip] kubectl 查看结构化日志 +> 使用 `kubectl logs` 结合 `jq` 可以快速过滤 JSON 日志: +> ```bash +> kubectl logs my-pod -c my-container | jq 'select(.level == "error")' +> kubectl logs my-pod -c my-container | jq 'select(.latency > 0.5)' +> ``` + +--- + +## 指标监控 + +### Metrics Server + +Metrics Server 是 Kubernetes 内置的轻量级指标采集组件,提供 CPU 和内存使用数据,是 `kubectl top` 和 HPA(基于资源指标)的基础设施。 + +#### 工作原理 + +```mermaid +flowchart LR + A[kubelet] -->|Summary API| B[Metrics Server] + B -->|Metrics API| C[kubectl top] + B -->|Metrics API| D[HPA Controller] +``` + +- 每个 kubelet 暴露 **Summary API**(`/stats/summary`),包含该节点上所有 Pod 的 CPU/内存使用数据。 +- Metrics Server 定期(默认 15 秒)从所有 kubelet 拉取数据,聚合后通过 **Metrics API**(`metrics.k8s.io`)暴露。 + +#### 安装 + +```bash +kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml +``` + +验证: + +```bash +kubectl top nodes +kubectl top pods -n default +``` + +#### 局限性 + +- 只提供 CPU 和内存指标,不支持磁盘、网络等。 +- 数据只保留短期窗口(约 1 分钟),不支持历史查询。 +- 不支持自定义指标——需要 Prometheus 等外部系统。 + +> [!info] Metrics Server vs Prometheus +> Metrics Server 是 K8s 内置的「最小可用」指标方案,适合 `kubectl top` 和基于资源的 HPA。如果你需要更丰富的指标、历史数据和告警,必须部署 Prometheus。 + +### Prometheus + Grafana + +Prometheus 是 CNCF 毕业项目,是 Kubernetes 生态中最主流的监控方案。Grafana 作为可视化层,提供丰富的仪表盘。 + +#### 架构概览 + +```mermaid +flowchart LR + subgraph K8s 集群 + A[应用 /export_metrics] -->|pull| B[Prometheus Server] + C[kube-state-metrics] -->|pull| B + D[Node Exporter] -->|pull| B + E[ServiceMonitor CRD] -.->|配置发现| B + end + + B --> F[Grafana] + B --> G[Alertmanager] + G --> H[邮件 / Slack / 钉钉] + B --> I[长期存储
Thanos / VictoriaMetrics] +``` + +核心组件: + +- **Prometheus Server**:时序数据库 + 采集引擎,定期从 targets 拉取(pull)指标。 +- **kube-state-metrics**:将 K8s 对象状态(Pod、Deployment、Node 等)转换为 Prometheus 指标。 +- **Node Exporter**:采集节点级系统指标(CPU、内存、磁盘、网络)。 +- **ServiceMonitor CRD**:由 Prometheus Operator 提供,以声明式方式配置采集目标。 +- **Alertmanager**:告警路由、去重、分组、静默。 + +#### ServiceMonitor 示例 + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: my-app + namespace: monitoring + labels: + release: prometheus # 匹配 Prometheus 实例的标签选择器 +spec: + namespaceSelector: + matchNames: + - default + selector: + matchLabels: + app: my-app # 匹配目标 Service 的标签 + endpoints: + - port: metrics # Service 中定义的端口名 + path: /metrics + interval: 15s +``` + +> [!info] ServiceMonitor 的工作方式 +> Prometheus Operator 会 watch ServiceMonitor 资源的变化,自动更新 Prometheus 的采集配置。运维人员不再需要手写 `prometheus.yml` 的 scrape 配置,实现了采集配置的 GitOps 管理。 + +#### 常见 K8s 监控指标 + +| 指标名称 | 含义 | 告警建议 | +|----------|------|----------| +| `container_cpu_usage_seconds_total` | 容器累计 CPU 使用时间 | 持续 > 80% requests 告警 | +| `container_memory_working_set_bytes` | 容器内存工作集 | 接近 limits 时告警 | +| `kube_pod_restart_total` | Pod 重启次数 | 短时间内重启 > 3 次告警 | +| `kube_pod_status_phase` | Pod 状态(Pending/Running/Failed) | Pending > 5 分钟告警 | +| `kube_node_status_condition` | 节点状态(Ready/NotReady) | NotReady 告警 | +| `kube_deployment_status_replicas_available` | Deployment 可用副本数 | 小于期望副本数告警 | +| `node_filesystem_avail_bytes` | 节点可用磁盘空间 | < 10% 剩余告警 | + +### HPA 自定义指标 + +HPA 默认支持基于 CPU/Memory 的扩缩容,但实际场景中,基于请求速率(QPS)、队列长度、延迟等自定义指标更有意义。 + +#### 前置条件 + +1. 安装 Prometheus Adapter(将 Prometheus 指标转换为 K8s Custom Metrics API): + ```bash + helm install prometheus-adapter prometheus-community/prometheus-adapter \ + --namespace monitoring \ + --set prometheus.url=http://prometheus-server.monitoring.svc + ``` + +2. 确认 Custom Metrics API 可用: + ```bash + kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq . + ``` + +#### HPA 配置示例(基于 HTTP 请求速率) + +```yaml +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: my-app-hpa + namespace: default +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: my-app + minReplicas: 2 + maxReplicas: 20 + metrics: + # 自定义指标:每 Pod 每秒请求数 + - type: Pods + pods: + metric: + name: http_requests_per_second + target: + type: AverageValue + averageValue: "100" # 每个 Pod 目标 100 QPS + behavior: + scaleUp: + stabilizationWindowSeconds: 30 + policies: + - type: Percent + value: 50 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 300 # 缩容冷却 5 分钟,避免抖动 + policies: + - type: Percent + value: 25 + periodSeconds: 60 +``` + +> [!tip] 扩缩容行为配置 +> `behavior` 字段(K8s 1.18+)允许精细控制扩缩容速度。建议扩容激进(快速应对流量高峰)、缩容保守(`stabilizationWindowSeconds` 设大,避免频繁抖动导致连接中断)。 + +--- + +## 事件(Events) + +Kubernetes Events 是集群中发生的重大状态变更记录,是排查问题的第一手信息。 + +### 查看事件 + +```bash +# 当前 namespace 的事件,按时间排序 +kubectl get events --sort-by='.lastTimestamp' + +# 所有 namespace 的事件 +kubectl get events -A + +# 只看 Warning 级别事件 +kubectl get events --field-selector type=Warning + +# 查看特定 Pod 的事件 +kubectl describe pod my-pod +``` + +### 常见事件类型 + +| Reason | 类型 | 含义 | +|--------|------|------| +| `Scheduled` | Normal | Pod 成功调度到节点 | +| `Pulling` / `Pulled` | Normal | 正在拉取 / 已拉取镜像 | +| `Created` / `Started` | Normal | 容器已创建 / 已启动 | +| `Unhealthy` | Warning | 健康检查失败 | +| `BackOff` | Warning | 重启退避(CrashLoopBackOff) | +| `FailedScheduling` | Warning | 调度失败(资源不足、亲和性冲突等) | +| `OOMKilling` | Warning | 容器因内存不足被杀死 | +| `Evicted` | Warning | Pod 被驱逐(磁盘压力、内存压力等) | + +### 事件 TTL 与持久化 + +> [!warning] Events 会被自动清理 +> 默认情况下,Events 的 TTL(Time To Live)为 **1 小时**。这意味着如果一个 Pod 在 2 小时前 OOMKilled,你通过 `kubectl describe pod` 看不到相关事件。 + +解决方法: + +1. **调整 TTL**(K8s 1.19+): + ```bash + # 在 kube-apiserver 中设置 + --event-ttl=24h + ``` + +2. **持久化到日志系统**:将 Events 采集到 Elasticsearch/Loki 等系统中长期保存。 + - 使用 kube-state-metrics 的 `kube_event_*` 指标采集到 Prometheus。 + - 使用专门的 Events 采集工具(如 `event-exporter`)推送到日志系统。 + +3. **event-exporter 示例**: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: event-exporter + namespace: monitoring + spec: + template: + spec: + containers: + - name: event-exporter + image: ghcr.io/resmoio/kubernetes-event-exporter:latest + args: + - -conf=/data/config.yaml + volumeMounts: + - name: config + mountPath: /data + volumes: + - name: config + configMap: + name: event-exporter-config + ``` + +--- + +## 常见陷阱与最佳实践 + +### 陷阱一:日志量爆炸 + +**问题**:高并发服务的 debug 日志未关闭,一天产生几十 GB 日志,磁盘告警或 Elasticsearch 索引膨胀。 + +**解决方案**: +- 生产环境默认 `info` 级别,通过环境变量或配置中心动态调整。 +- 使用 Fluentd/Promtail 的 filter 功能丢弃无用日志(如 kube-system 的健康检查日志)。 +- 设置 Elasticsearch 的 ILM(Index Lifecycle Management)策略,自动过期删除旧索引。 + +```yaml +# Fluentd filter 示例:丢弃 kube-system 命名空间的日志 + + @type grep + + key $.kubernetes.namespace_name + pattern /^kube-system$/ + + +``` + +### 陷阱二:探针超时与应用启动时间不匹配 + +**问题**:Java 应用启动需要 60 秒,但 livenessProbe 的 `initialDelaySeconds` 只设了 30 秒,导致每次重启都进入 CrashLoopBackOff。 + +**解决方案**:使用 startupProbe 代替盲目增大 `initialDelaySeconds`(详见探针设计原则一)。 + +### 陷阱三:监控指标标签爆炸 + +**问题**:在 Prometheus 指标中使用 `user_id`、`request_path` 等高基数(high cardinality)标签,导致时间序列数暴涨,Prometheus 内存 OOM。 + +**解决方案**: +- 高基数字段(如 request_id、user_id)放到日志中,不作为指标标签。 +- `request_path` 应归一化(如 `/api/v1/users/123` → `/api/v1/users/:id`)。 +- 使用 Prometheus 的 `metric_relabel_configs` 丢弃不需要的标签。 + +```yaml +# Prometheus 配置:限制标签基数 +metric_relabel_configs: + - source_labels: [__name__] + regex: "container_.*" + action: keep + - regex: "pod_name" + action: labeldrop +``` + +### 陷阱四:readinessProbe 失败但 livenessProbe 正常 + +**问题**:readinessProbe 依赖 Redis,Redis 短暂不可达,Pod 被从 Endpoints 摘除。但 livenessProbe 正常,Pod 不会被重启。流量被摘除后 Pod 中积压的请求超时,用户报错。 + +**解决方案**: +- readinessProbe 的依赖检查要设置合理的超时(如 2 秒内无响应即判定失败)。 +- 在应用内部实现熔断机制,不要完全依赖 K8s 探针。 +- 确保 readinessProbe 恢复时,Pod 能快速重新加入 Endpoints(`successThreshold: 1`)。 + +### 陷阱五:kubectl top 数据不准 + +**问题**:`kubectl top pods` 显示的 CPU 使用率为 0 或与实际不符。 + +**原因**: +- Metrics Server 启动后需要约 1-2 分钟才能采集到数据。 +- 容器的 CPU request 未设置,百分比无法计算。 +- Metrics Server 的 `--kubelet-insecure-tls` 未配置导致与 kubelet 通信失败。 + +**解决方案**: + +```bash +# 检查 Metrics Server 是否正常运行 +kubectl get deployment metrics-server -n kube-system + +# 检查是否有错误日志 +kubectl logs -n kube-system deployment/metrics-server + +# 使用 --resource 选项查看原始数值而非百分比 +kubectl top pods --containers +``` + +### 最佳实践汇总 + +| 领域 | 实践 | +|------|------| +| 日志 | stdout 输出、JSON 结构化、统一字段命名、合理轮转 | +| 探针 | startupProbe 兜底慢启动、liveness 不查依赖、readiness 做适度依赖检查 | +| 监控 | Prometheus + Grafana 标准栈、ServiceMonitor 声明式配置 | +| 告警 | 分级告警(Warning/Critical)、去重分组、值班轮转 | +| 指标 | 避免高基数标签、区分 RED(Rate/Error/Duration)指标与 USE(Utilization/Saturation/Errors)指标 | +| 事件 | 配置持久化、重要事件告警 | + +--- + +## 延伸阅读 + +- [[k8s-03-pod]] — Pod 生命周期、调度与资源管理 +- [[k8s-02-architecture]] — K8s 控制平面与节点组件架构 +- [Kubernetes 官方文档 - Logging Architecture](https://kubernetes.io/docs/concepts/cluster-administration/logging/) +- [Kubernetes 官方文档 - Configure Liveness, Readiness and Startup Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) +- [Prometheus Operator 文档](https://prometheus-operator.dev/) +- [Grafana Loki 官方文档](https://grafana.com/docs/loki/latest/) +- [Google SRE Book - Monitoring Distributed Systems](https://sre.google/sre-book/practical-alerting/) diff --git a/weekly/daily/2026-07-07-daily.md b/weekly/daily/2026-07-07-daily.md new file mode 100644 index 0000000..b6c743f --- /dev/null +++ b/weekly/daily/2026-07-07-daily.md @@ -0,0 +1,37 @@ +--- +tags: [daily] +create time: 2026-07-07 15:15 +--- + +# 2026-07-07 日报 + +## 今日完成 +- [x] 配置开发环境:brew、Claude Code、Fenno API、Goenv、Go、Nvm、Node.js、pnpm、tsc +- [x] 安装远程 SSH 连接工具 electerm +- [x] 配置 Git SSH、GitHub CLI +- [x] 配置 Obsidian 及相关插件(Better Export PDF、Claudian、Git、Style Settings) +- [x] 配置 Obsidian 主题:Phycat + +## GitHub Issues +> 当日无关联 Issue + + + +## 卡点 / 阻塞 +> 当前无卡点 + + + +## 明日计划 +- [ ] 参与部署 K8s + +## 备注 diff --git a/weekly/daily/template.md b/weekly/daily/template.md new file mode 100644 index 0000000..729916f --- /dev/null +++ b/weekly/daily/template.md @@ -0,0 +1,33 @@ +--- +tags: [daily] +create time: +--- + +# YYYY-MM-DD 日报 + +## 今日完成 +- [ ] + +## GitHub Issues +> 当日无关联 Issue + + + +## 卡点 / 阻塞 +> 当前无卡点 + + + +## 明日计划 +- [ ] + +## 备注