diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 3a6e805..d9fce0d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,5 +1,6 @@ import { defineConfig } from 'vitepress' import { createPostsSidebar } from './utils/content.mts' +import { withMermaid } from 'vitepress-plugin-mermaid' const nav = [ { @@ -27,12 +28,13 @@ const sidebar = { '/posts/': createPostsSidebar(process.cwd()) } -export default defineConfig({ +export default withMermaid(defineConfig({ lang: 'zh-CN', title: 'NCHJ 的笔记', description: '技术笔记与工程实践整理', lastUpdated: true, cleanUrls: true, + mermaid: {}, themeConfig: { nav, sidebar, @@ -47,4 +49,4 @@ export default defineConfig({ lightModeSwitchTitle: '切换到浅色模式', darkModeSwitchTitle: '切换到深色模式' } -}) +})) diff --git a/docs/posts/k8s-training/01-k8s-architecture.md b/docs/posts/k8s-training/01-k8s-architecture.md new file mode 100644 index 0000000..c4cc868 --- /dev/null +++ b/docs/posts/k8s-training/01-k8s-architecture.md @@ -0,0 +1,294 @@ +--- +title: "第1章:Kubernetes 核心概念与架构" +date: "2026-05-15" +category: "k8s" +tags: + - "k8s" + - "AI生成" +--- + +# 第1章:Kubernetes 核心概念与架构 + +## 🎯 学习目标 +- 理解 K8S 是什么、为什么需要它 +- 掌握 K8S 集群架构(Master + Worker) +- 理解核心组件的职责和协作关系 +- 能回答面试中"K8S 架构"相关问题 + +--- + +## 📖 学习要点 + +### 1.1 什么是 Kubernetes + +**定义**:Kubernetes(简称 K8S)是一个开源的**容器编排平台**,用于自动部署、扩缩和管理容器化应用。 + +**核心能力**: +| 能力 | 说明 | 一句话理解 | +|------|------|-----------| +| 服务发现与负载均衡 | 自动分配流量到健康的 Pod | 不用手动配 Nginx upstream | +| 自动部署与回滚 | 声明式地管理应用状态 | git 一样管理基础设施 | +| 自动装箱 | 根据资源需求调度 Pod 到节点 | 自动找合适的服务器放容器 | +| 自我修复 | 自动重启失败的容器,替换不健康的 Pod | 容器挂了自动拉起来 | +| 水平扩缩 | 根据负载自动增减 Pod 数量 | 流量大了自动加机器 | +| Secret 和配置管理 | 管理敏感信息和应用配置 | 配置和代码分离 | + +**K8S vs Docker Compose 的区别**(面试高频): + +``` +Docker Compose: +- 单机编排 +- 适合开发环境 +- docker-compose up 即可 +- 没有自愈能力 + +Kubernetes: +- 集群编排(多节点) +- 适合生产环境 +- 需要学习成本 +- 自愈、自动扩缩、滚动更新 +``` + +### 1.2 集群架构 + +```mermaid +flowchart TB + subgraph K8S_Cluster["K8S Cluster"] + subgraph Master["Master Node(s)"] + API_Server["API Server"] + Scheduler["Scheduler"] + etcd["etcd"] + Controller_Manager["Controller Manager"] + Cloud_Controller["Cloud Controller"] + end + + Master -->|"REST API"| Workers + end + + subgraph Workers["Worker Nodes"] + W1["Worker 1"]:::worker + W2["Worker 2"]:::worker + WN["Worker N"]:::worker + + W1 --> kubelet1["kubelet"] + W1 --> kubeproxy1["kube-proxy"] + W1 --> pods1["Pod Pod"] + + W2 --> kubelet2["kubelet"] + W2 --> kubeproxy2["kube-proxy"] + W2 --> pods2["Pod Pod"] + + WN --> kubeletN["kubelet"] + WN --> kubeproxyN["kube-proxy"] + WN --> podsN["Pod Pod"] + end + + classDef worker fill:#f9f,stroke:#333,stroke-width:2px +``` + +> **图 1-1:K8S 集群架构图** + +### 1.3 核心组件详解 + +#### Master 节点组件(控制面) + +**① API Server(apiserver)** +- K8S 的"前台",所有组件和用户都通过它通信 +- 唯一入口,提供 REST API +- 认证、授权、准入控制 +- **面试考点**:为什么所有组件都通过 API Server 通信而不是直接访问 etcd? + > 解耦、安全(etcd 不暴露)、审计、统一鉴权 + +**② etcd** +- 分布式键值存储,保存集群所有数据 +- K8S 的"数据库" +- 状态:集群状态、配置、Secret 数据都存在这里 +- **面试考点**:etcd 挂了会怎样? + > Master 功能失效,不能创建新资源,但已有 Pod 继续运行 + +**③ Scheduler(调度器)** +- 决定 Pod 运行在哪个 Node 上 +- 调度决策因素:资源请求、亲和性、污点容忍、数据本地性等 +- **工作流程**:Pod 创建 → Scheduler 观察 → 选择最优 Node → 绑定 + +**④ Controller Manager(控制器管理器)** +- 运行多个控制器的进程 +- 核心:"控制循环"(Reconcile Loop):不断观察期望状态 vs 实际状态,驱动实际状态趋近期望状态 +- 常见控制器: + - **Deployment Controller**:管理 ReplicaSet,保证 Pod 副本数 + - **Node Controller**:监控节点健康 + - **ReplicaSet Controller**:保证 Pod 数量 + - **Service Account Controller**:管理默认 SA + +#### Worker 节点组件 + +**⑤ kubelet** +- 每个 Node 上的"代理人" +- 接收 PodSpec(来自 API Server),确保容器按 Spec 运行 +- 健康检查、向 Master 汇报节点状态 +- **面试考点**:kubelet 挂了会怎样? + > 该 Node 上的 Pod 不受管理,Master 会将其标记为 NotReady + +**⑥ kube-proxy** +- 维护节点上的网络规则 +- 实现 Service 的负载均衡(iptables/IPVS 模式) +- 让 Service 能将流量路由到正确的 Pod + +### 1.4 核心资源对象(K8S 词汇表) + +``` +Namespace → 集群内虚拟分区(多租户隔离) +Pod → 最小部署单元,一个或多个容器 +Deployment → 管理 Pod 的声明式更新(无状态应用) +ReplicaSet → 保证 Pod 副本数(通常不直接使用) +Service → Pod 的稳定网络入口 + 负载均衡 +ConfigMap → 存非敏感配置 +Secret → 存敏感信息(密码、证书) +Ingress → HTTP/HTTPS 路由(7层负载均衡) +PersistentVolume → 持久化存储 +PersistentVolumeClaim → 存储申请 +``` + +### 1.5 声明式 vs 命令式(面试必考) + +``` +命令式(Imperative): + kubectl run nginx --image=nginx + kubectl create deployment ... + → 告诉 K8S "做什么操作" + +声明式(Declarative): + kubectl apply -f deployment.yaml + → 告诉 K8S "我要什么状态",K8S 自己搞定 + + 优势: + ✅ Git 友好(版本控制) + ✅ 可审计(谁改了什么) + ✅ 幂等(多次执行结果一致) + ✅ 可回滚 +``` + +--- + +## 📝 习题 + +### 选择题 + +**Q1.** Kubernetes 中负责存储集群所有状态数据的组件是? +- A) API Server +- B) Scheduler +- C) etcd +- D) kubelet + +**Q2.** 以下哪个不是 Master 节点的组件? +- A) Controller Manager +- B) kubelet +- C) API Server +- D) Scheduler + +**Q3.** K8S 中最小的调度和部署单元是? +- A) Container +- B) Pod +- C) Deployment +- D) Service + +**Q4.** kube-proxy 的主要职责是? +- A) 调度 Pod 到合适的节点 +- B) 管理集群状态数据 +- C) 维护网络规则,实现 Service 负载均衡 +- D) 监控 Pod 健康状态 + +**Q5.** 声明式 API 的优势不包括以下哪个? +- A) 幂等性 +- B) Git 友好 +- C) 执行速度更快 +- D) 可审计 + +### 简答题 + +**Q6.** 请简述一个 Pod 从 `kubectl apply -f pod.yaml` 到实际运行经历的完整流程。 + +**Q7.** 如果 etcd 集群不可用,K8S 集群会发生什么?已有 Pod 还能继续工作吗? + +**Q8.** 请解释 K8S 中"控制循环"(Control Loop / Reconcile Loop)的概念。 + +--- + +## ✅ 参考答案 + +### A1. C) etcd +etcd 是 K8S 的唯一状态存储,所有集群数据(节点信息、Pod 状态、配置等)都存在 etcd 中。 + +### A2. B) kubelet +kubelet 运行在每个 Worker 节点上,负责管理该节点上的容器。它不是 Master 组件。 + +### A3. B) Pod +Pod 是 K8S 的最小调度和部署单元。一个 Pod 可以包含一个或多个容器,它们共享网络和存储命名空间。 + +### A4. C) 维护网络规则,实现 Service 负载均衡 +kube-proxy 运行在每个节点上,通过 iptables 或 IPVS 规则,将访问 Service 的流量转发到后端的 Pod。 + +### A5. C) 执行速度更快 +声明式 API 并不比命令式执行更快,有时因为 diff 计算反而更慢。其核心优势是幂等、Git 友好、可审计。 + +### A6. Pod 创建完整流程 +``` +1. 用户执行 kubectl apply -f pod.yaml +2. kubectl 将请求发送给 API Server(认证+授权+准入控制) +3. API Server 将 Pod 配置写入 etcd +4. Scheduler 感知到有未绑定的 Pod,执行调度算法 +5. Scheduler 选出最优 Node,将绑定信息写入 etcd +6. 目标 Node 上的 kubelet 监听到分配给自己的 Pod +7. kubelet 调用容器运行时(CRI)拉取镜像、创建容器 +8. kubelet 执行健康检查,向 API Server 汇报 Pod 状态 +9. Pod 变为 Running 状态 +``` + +### A7. etcd 不可用的影响 +- **Master 功能完全失效**:无法创建/更新/删除任何资源 +- **已有 Pod 继续运行**:kubelet 是直接管理容器的,不依赖 etcd +- **新 Pod 无法调度**:Scheduler 无法读取/写入状态 +- **自愈失效**:Controller Manager 无法感知状态变化 +- **结论**:集群变成"只读"状态,已有工作负载不受影响但不能管理 + +### A8. 控制循环(Reconcile Loop) +控制循环是 K8S 控制器模式的核心: +1. 控制器观察(Watch)API Server 中的资源状态 +2. 对比**期望状态**(YAML 中定义的)和**实际状态**(集群中真实的) +3. 如果不一致,执行动作使实际状态趋近期望状态 +4. 循环重复 + +``` +while true: + 实际状态 = getCurrentState() + if 实际状态 != 期望状态: + 执行动作使实际状态 → 期望状态 + sleep(一段时间) +``` + +**类比**:恒温器 —— 你设定 25°C(期望状态),恒温器不断检测室温(实际状态),低于 25°C 就开暖气。 + +--- + +## 🔧 实操练习 + +```bash +# 设置集群连接 +export KUBECONFIG=~/.kube/config + +# 创建自己的 namespace(所有练习在这个 namespace 下操作) +kubectl create namespace k8s-training + +# 查看集群信息 +kubectl get ns +kubectl cluster-info + +# 练习后清理 +kubectl delete namespace k8s-training +``` + +> ⚠️ **提醒**:所有实操练习都在 `k8s-training` namespace 下进行,练习完务必删除资源,Sealos 是付费的! + +--- + +**下一章**:[第2章:Pod 与工作负载管理](./02-pod-and-workloads.md) diff --git a/docs/posts/k8s-training/02-pod-and-workloads.md b/docs/posts/k8s-training/02-pod-and-workloads.md new file mode 100644 index 0000000..a376a99 --- /dev/null +++ b/docs/posts/k8s-training/02-pod-and-workloads.md @@ -0,0 +1,1321 @@ +--- +title: "第2章:Pod 与工作负载管理" +date: "2026-05-15" +category: "k8s" +tags: + - "k8s" + - "AI生成" +--- + +# 第2章:Pod 与工作负载管理 + +## 🎯 学习目标 +- 理解 Pod 的生命周期和核心字段 +- 掌握 Deployment、StatefulSet、DaemonSet、Job/CronJob 的使用场景 +- 能手写 YAML 并部署应用 +- 理解滚动更新和回滚机制 + +--- + +## 📖 学习要点 + +### 2.1 Pod 是什么(最底层的运行单元) + +**一句话理解**:Pod 是 K8S 中**真正运行的实体**。不管你写的是 Deployment、StatefulSet 还是 DaemonSet,最终跑在节点上的都是 Pod。 + +```mermaid +flowchart TB + subgraph Pod["Pod (IP: 10.244.1.5)"] + App["Container (App)
Port: 8080"] + Sidecar["Container (Sidecar)
Port: 9090"] + Pause["Pause Container
(共享网络)"] + + App <--> Pause + Sidecar <--> Pause + end + + note1["共享: Network Namespace"] + note2["共享: Volume (挂载点)"] +``` + +> **图 2-1:Pod 结构图** + +**为什么 K8S 不直接管理容器而要加一层 Pod?** +- 有些应用天然需要多个进程协作(如日志收集 sidecar) +- Pod 内容器共享 IP 和端口空间,可以通过 localhost 通信 +- Pod 是调度的原子单位——同 Pod 的容器一定在同一节点 + +### 2.2 Pod 的两种使用方式(⭐ 核心!先搞懂这个再往下看) + +Pod 有两种创建方式,**理解这个就理解了 Deployment 里"隐含的 Pod"是怎么回事**: + +```mermaid +flowchart LR + subgraph Way1["方式一:直接创建 Pod(独立 Pod)"] + A1["你写 Pod YAML"] + A1 --> B1["kubectl apply -f pod.yaml → K8S 直接创建 Pod"] + B1 --> C1["你要自己管一切:Pod 挂了就没了,没人帮你重启,没有自动扩缩,没有滚动更新"] + end + + subgraph Way2["方式二:通过 Deployment 管理 Pod(托管 Pod)⭐"] + A2["你写 Deploy YAML"] + A2 --> B2["kubectl apply -f deploy.yaml"] + B2 --> C2["Deployment 根据 template 自动帮你创建 Pod
Pod 挂了自动重启
想多副本?改 replicas 就行
想更新?改 image 就自动滚动更新"] + end +``` + +> **图 2-2:Pod 的两种创建方式** + +**关键结论**: +> **Pod 永远是 K8S 的运行单位,但你不一定需要直接写 Pod YAML。** +> 写 Deployment YAML 时,Pod 被"嵌在" Deployment 的 `template` 字段里——这就是所谓的"隐含"。 + +--- + +### 2.3 Deployment 与 Pod 的关系(⭐⭐⭐ 面试必问,必须彻底理解) + +#### 先看本质:Deployment = Pod 的管理者 + +```mermaid +flowchart TB + subgraph Deploy["Deployment(管理者)"] + direction TB + D1["我想要 3 个 Pod,标签都是 app=nginx,用 nginx:1.25"] + D2["selector: ← 用标签来\"认领\" Pod
app: nginx"] + D3["replicas: 3 ← 要维护 3 个副本"] + D4["template: ← Pod 的\"模子\"(模板)
metadata.labels.app: nginx ← 新创建的 Pod 打上这个标签
spec: ← 容器怎么定义"] + end + + Deploy -->|"Deployment Controller 看着这个模板
发现\"实际只有 1 个 Pod,我想要 3 个\"
→ 自动创建 2 个新 Pod"| Pods + + subgraph Pods[""] + P1["Pod-1
app=nginx
1.25"] + P2["Pod-2
app=nginx
1.25"] + P3["Pod-3
app=nginx
1.25"] + end +``` + +> **图 2-3:Deployment 与 Pod 的关系** + +#### Deployment YAML 中的"三套标签"(最容易混淆的地方) + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx # ① Deployment 自己的标签 +spec: + selector: + matchLabels: + app: nginx # ② 选择器:Deployment 用来找 Pod 的条件 + template: + metadata: + labels: + app: nginx # ③ Pod 模板的标签:新 Pod 会被打上这个标签 + spec: + containers: + - name: nginx + image: nginx:1.25 +``` + +**② 和 ③ 必须匹配!这是铁律。** 为什么? + +``` +Deployment Controller 的工作循环: + +while true: + 实际Pod数 = 根据选择器②(selector) 找标签=app:nginx 的 Pod + if 实际Pod数 < replicas: + 用模板③(template) 创建新 Pod + if 实际Pod数 > replicas: + 删除多余的 Pod + sleep(一会儿) + +如果 ② 和 ③ 不匹配会怎样? + → Deployment 用 app:nginx 去找 Pod + → 但新创建的 Pod 标签是 app:web(不匹配) + → Deployment 找不到自己创建的 Pod! + → 会无限创建 Pod,永远觉得"不够" + → 所以 K8S 直接拒绝这种配置(API Server 校验) +``` + +#### selector 是怎么工作的?(标签选择器机制) + +标签(Label) = 附加在资源上的键值对,用于分组和筛选。 + +```yaml +# Pod 上的标签 +metadata: + labels: + app: nginx # key: app, value: nginx + tier: frontend + env: production + +# Deployment 用 selector 找这些 Pod +spec: + selector: + matchLabels: # 精确匹配:标签必须完全一致 + app: nginx + # matchExpressions: # 表达式匹配(高级用法) + # - key: tier + # operator: In + # values: ["frontend", "backend"] +``` + +**selector 的工作过程**: +``` +集群中所有 Pod: + Pod-A labels: {app: nginx, tier: frontend} ✅ 匹配 app=nginx + Pod-B labels: {app: redis, tier: cache} ❌ 不匹配 + Pod-C labels: {app: nginx, env: production} ✅ 匹配 app=nginx + Pod-D labels: {app: nginx, tier: backend} ✅ 匹配 app=nginx + +selector: matchLabels: {app: nginx} +→ 匹配到 Pod-A, Pod-C, Pod-D → 这些就是"我的 Pod" +``` + +> **类比**:selector 是"招聘条件"(要 app=nginx),labels 是"简历标签"。Deployment 根据招聘条件筛所有 Pod 的简历,匹配的就是自己管的。 + +#### Pod 什么时候"独立",什么时候"被隐含"? + +| 场景 | Pod 的存在形式 | 你需要写什么 | 谁管 Pod 的生命周期 | +|------|--------------|------------|-----------------| +| **临时调试** | 独立 Pod | `kind: Pod` YAML | **没人管**,挂了就没了 | +| **部署 Web 服务** | 被 Deployment 管理 | `kind: Deployment` YAML(内含 template) | **Deployment 管**,自动重启/扩缩/更新 | +| **部署 MySQL** | 被 StatefulSet 管理 | `kind: StatefulSet` YAML | **StatefulSet 管** | +| **部署日志 Agent** | 被 DaemonSet 管理 | `kind: DaemonSet` YAML | **DaemonSet 管** | +| **运行一次性任务** | 被 Job 管理 | `kind: Job` YAML | **Job 管** | + +**面试回答模板**: +> "Pod 是 K8S 的最小运行单元。绝大多数情况下我们不会直接创建 Pod,而是通过 Deployment 等工作负载来间接管理。Deployment 的 `template` 字段就是 Pod 的定义模板,Deployment Controller 会根据这个模板自动创建和管理 Pod。Deployment 通过 `selector` 标签选择器来识别哪些 Pod 是自己管理的。" + +#### 独立 Pod vs Deployment 管理 Pod 的完整对比 + +```yaml +# ====== 方式一:独立 Pod(你手动管理一切) ====== +apiVersion: v1 +kind: Pod +metadata: + name: my-nginx # 直接给 Pod 起名 + labels: + app: nginx +spec: + containers: + - name: nginx + image: nginx:1.25 + ports: + - containerPort: 80 +# 问题: +# ❌ Pod 挂了没人重启 +# ❌ 只能运行 1 个实例 +# ❌ 没有滚动更新 +# ❌ 手动扩缩(不可能) + +# ====== 方式二:Deployment(K8S 帮你管一切) ====== +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment # Deployment 的名字(不是 Pod 的) +spec: + replicas: 3 # 要 3 个 Pod(不是 1 个!) + selector: # 用标签找"我的 Pod" + matchLabels: + app: nginx + template: # ⭐ Pod 模板(和上面的 Pod YAML 的 spec 基本一样) + metadata: + labels: + app: nginx # Pod 的标签(必须匹配 selector) + spec: + containers: + - name: nginx + image: nginx:1.25 + ports: + - containerPort: 80 +# 优势: +# ✅ Pod 挂了自动重建 +# ✅ 自动维护 3 个副本 +# ✅ 滚动更新 nginx:1.25 → nginx:1.26 +# ✅ kubectl scale 一条命令扩缩 +``` + +#### Deployment 创建 Pod 的完整过程 + +```mermaid +flowchart TB + A["你执行 kubectl apply -f deployment.yaml"] --> B["1. API Server 保存 Deployment 对象到 etcd"] + B --> C["2. Deployment Controller 发现:
有人创建了一个 replicas=3 的 Deployment
selector 要求 app=nginx 的 Pod
当前 0 个 Pod 匹配
差 3 个,我来创建!"] + C --> D["3. Deployment Controller 自动创建一个 ReplicaSet
(ReplicaSet 是 Deployment 的中间层,通常不直接操作)"] + D --> E["4. ReplicaSet Controller 根据 template 创建 3 个 Pod
→ Pod 名自动生成: nginx-deployment-abc1234-5xyz
→ Pod 自动打上 template.metadata.labels"] + E --> F["5. Scheduler 调度 Pod 到具体的 Node"] + F --> G["6. kubelet 在 Node 上拉取镜像、启动容器"] + G --> H["7. 你执行 kubectl get pods 看到的是这 3 个 Pod,
不是 Deployment 本身"] + + note1["实际层级关系:
Deployment
└── ReplicaSet (自动创建,通常不直接操作)
    └── Pod (实际运行的)
    └── Pod
    └── Pod"] +``` + +> **图 2-4:Deployment 创建 Pod 的完整过程** + +#### 一句话总结 Deployment 和 Pod 的关系 + +> **Deployment 是"图纸+管理者",Pod 是"产品"。Deployment 的 `template` 是生产 Pod 的模具,`selector` 是质检标准(通过标签检查产品是否合格),`replicas` 是产量要求。K8S 的 Controller 就是工厂,不断检查实际产量是否达标。** + +### 2.4 Pod YAML 核心字段(必背) + +下面是独立 Pod 的完整 YAML,注意:**Deployment 的 `template` 部分(`template.metadata` + `template.spec`)和这个 Pod YAML 的结构几乎完全一样**。 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: my-app + labels: # 标签:用于选择器匹配 + app: my-app + env: production + annotations: # 注解:存储元数据,不可用于选择 + description: "Main application pod" +spec: # ← 这部分和 Deployment 的 template.spec 完全一样 + containers: + - name: app + image: nginx:1.25 # 镜像 + imagePullPolicy: IfNotPresent # Always/Never/IfNotPresent + ports: + - containerPort: 80 + env: # 环境变量 + - name: DB_HOST + value: "mysql-service" + resources: # 资源限制(⭐面试高频) + requests: # 调度依据(保证最少给这么多) + cpu: "100m" # 100 millicore = 0.1 核 + memory: "128Mi" + limits: # 硬限制(超过会 OOM Kill / CPU 限流) + cpu: "500m" + memory: "256Mi" + livenessProbe: # 存活探针:挂了就重启容器 + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 10 # 首次探测延迟 + periodSeconds: 5 # 探测间隔 + readinessProbe: # 就绪探针:没准备好就不接入流量 + httpGet: + path: /ready + port: 80 + initialDelaySeconds: 5 + periodSeconds: 3 + restartPolicy: Always # Always/OnFailure/Never +``` + +### 2.5 探针类型(面试必考) + +| 探针 | 作用 | 失败后果 | +|------|------|---------| +| **livenessProbe** | 容器是否存活 | 重启容器 | +| **readinessProbe** | 是否可以接收流量 | 从 Service Endpoints 中移除 | +| **startupProbe** | 容器是否启动完成 | 未通过则 liveness/readiness 不生效 | + +三种探测方式: +```yaml +# 1. HTTP 探测 +livenessProbe: + httpGet: + path: /healthz + port: 8080 + +# 2. TCP 探测 +livenessProbe: + tcpSocket: + port: 3306 + +# 3. 命令探测 +livenessProbe: + exec: + command: + - cat + - /tmp/healthy +``` + +### 2.6 工作负载类型对比 + +| 类型 | 使用场景 | 特点 | restartPolicy | +|------|---------|------|---------------| +| **Deployment** | 无状态应用(Web服务、API) | 滚动更新、回滚、扩缩容 | Always | +| **StatefulSet** | 有状态应用(数据库、MQ) | 稳定网络标识、有序部署/删除、持久存储 | Always | +| **DaemonSet** | 每个节点运行一个(日志收集、监控) | 新节点自动加入,节点删除自动清理 | Always | +| **Job** | 一次性任务(数据处理、ML训练、迁移脚本) | 运行到完成即退出,支持并行/重试/超时 | Never / OnFailure | +| **CronJob** | 定时任务(备份、报表、清理) | Cron 表达式调度,管理 Job 的生命周期 | Never / OnFailure | + +> **⭐ 关键区别**:Deployment 的 Pod 退出后会被重建(维持副本数),Job 的 Pod 完成后不会重建。这就是 `restartPolicy` 必须设为 `Never` 或 `OnFailure` 的原因——告诉 kubelet 任务完成就不再重启。 + +**ECS → K8S 迁移参考:** +| AWS ECS | K8S 对应 | 补充工具 | +|---------|---------|---------| +| ECS Service | Deployment | — | +| ECS Task(一次性) | Job | — | +| ECS Task + SQS Trigger | Job + KEDA ScaledJob | KEDA(见 2.9 节) | +| ECS Scheduled Task | CronJob | — | + +### 2.7 Deployment YAML 详解(最重要的资源) + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 # 副本数 + selector: # ⭐ 必须!选择器匹配 Pod + matchLabels: + app: nginx + strategy: # 更新策略 + type: RollingUpdate # RollingUpdate / Recreate + rollingUpdate: + maxSurge: 1 # 滚动更新时最多多出几个 Pod + maxUnavailable: 0 # 滚动更新时最多不可用几个 Pod + template: # Pod 模板(和 Pod YAML 的 spec 部分一样) + metadata: + labels: + app: nginx # ⭐ 必须和 selector.matchLabels 匹配! + spec: + containers: + - name: nginx + image: nginx:1.25 + ports: + - containerPort: 80 + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "256Mi" +``` + +**面试考点:RollingUpdate 策略** +``` +旧版本: [Pod1] [Pod2] [Pod3] + ↓ +新版本: [Pod1] [Pod2] [Pod3-new] ← maxSurge=1, maxUnavailable=0 + ↓ + [Pod1] [Pod2-new] [Pod3-new] + ↓ + [Pod1-new] [Pod2-new] [Pod3-new] ← 完成 +``` + +### 2.8 Job 与 CronJob 深度讲解(批处理任务) + +> **类比 AWS ECS**:如果你在 AWS 上用 ECS Task 运行一次性的 heavy 任务,K8S 的 `Job` 就是对等概念。每个 Job 会起一个独立容器,运行完自动退出并可以自动清理。 + +#### Job 核心概念 + +Job 负责管理"运行到完成"的任务,与 Deployment 的核心区别: + +| 对比项 | Deployment | Job | +|--------|-----------|-----| +| 目的 | 持续运行的服务 | 运行到完成的任务 | +| Pod 退出后 | 自动重建(保持副本数) | 不重建(任务完成) | +| `restartPolicy` | Always | Never 或 OnFailure | +| 使用场景 | Web 服务、API | 数据处理、ML 训练、迁移脚本 | + +#### Job YAML 完整示例 + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: data-process-job + namespace: default +spec: + # ===== 核心控制字段 ===== + completions: 1 # 需要成功完成的 Pod 数量(默认 1) + parallelism: 1 # 同时运行的 Pod 数量(默认 1) + backoffLimit: 3 # 失败重试次数(超过后 Job 标记为失败) + activeDeadlineSeconds: 600 # 整个 Job 的超时时间(秒) + ttlSecondsAfterFinished: 300 # 完成后 5 分钟自动清理(包括 Pod 日志) + + # ===== Pod 模板 ===== + template: + metadata: + labels: + app: data-processor + job-type: batch + spec: + restartPolicy: Never # ⭐ Job 中必须设为 Never 或 OnFailure + containers: + - name: worker + image: python:3.11 + command: ["python", "/app/process.py"] + env: + - name: TASK_ID + value: "job-20260401-001" + - name: INPUT_BUCKET + value: "s3://my-bucket/input/" + resources: + requests: + cpu: "2" # 重型任务申请足够资源 + memory: "4Gi" + limits: + cpu: "4" + memory: "8Gi" + volumeMounts: + - name: workdir + mountPath: /tmp/work + volumes: + - name: workdir + emptyDir: {} +``` + +#### restartPolicy 的选择 + +``` +restartPolicy: Never + - Pod 失败 → 启动新的 Pod(不复用) + - 直到 backoffLimit 次失败后,Job 标记为 Failed + - 适合:幂等任务、需要干净环境的任务 + +restartPolicy: OnFailure + - Pod 失败 → 在同一个 Pod 内重启容器 + - 适合:轻量级任务、希望复用环境的任务 +``` + +#### 并行 Job 模式(处理大量任务) + +**模式一:固定完成数(completions > parallelism)** +```yaml +spec: + completions: 10 # 总共要完成 10 个任务 + parallelism: 3 # 每次并行 3 个 Pod + # K8S 自动调度:先起 3 个,某个完成后立刻补一个,直到完成 10 个 +``` + +**模式二:WorkQueue 模式(消费队列)** +```yaml +spec: + completions: 10 + parallelism: 5 + completionMode: Indexed # ⭐ 每个 Pod 有唯一的 JOB_COMPLETION_INDEX 环境变量 + # Pod 0 → 处理任务 0 + # Pod 1 → 处理任务 1 + # ... +``` +Pod 内通过 `$JOB_COMPLETION_INDEX` 知道自己该处理哪个分片: +```python +import os +shard_id = int(os.environ["JOB_COMPLETION_INDEX"]) +# 从队列或存储中取第 shard_id 个任务 +``` + +#### 动态创建 Job(程序化触发) + +这是与 ECS `RunTask` 最接近的模式:你的调度服务通过 K8S API 动态创建 Job: + +```python +# Python 示例(使用 kubernetes client-go 的 Python 版) +from kubernetes import client, config + +config.load_incluster_config() # 在 K8S 内部运行时 +# config.load_kube_config() # 本地调试时 + +batch_v1 = client.BatchV1Api() + +def create_task_job(task_id: str, input_data: str): + job = client.V1Job( + api_version="batch/v1", + kind="Job", + metadata=client.V1ObjectMeta( + name=f"task-{task_id}", + labels={"task-id": task_id} + ), + spec=client.V1JobSpec( + ttl_seconds_after_finished=300, + backoff_limit=3, + template=client.V1PodTemplateSpec( + spec=client.V1PodSpec( + restart_policy="Never", + containers=[client.V1Container( + name="worker", + image="your-worker:latest", + env=[client.V1EnvVar(name="TASK_ID", value=task_id), + client.V1EnvVar(name="INPUT", value=input_data)] + )] + ) + ) + ) + ) + batch_v1.create_namespaced_job(namespace="default", body=job) +``` + +#### CronJob(定时任务) + +```yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: daily-report +spec: + schedule: "0 2 * * *" # 每天凌晨 2 点(Cron 表达式) + concurrencyPolicy: Forbid # Forbid(禁止并发) / Allow / Replace + successfulJobsHistoryLimit: 3 # 保留最近 3 个成功的 Job 记录 + failedJobsHistoryLimit: 1 # 保留最近 1 个失败的 Job 记录 + startingDeadlineSeconds: 60 # 错过触发时间后 60 秒内还允许补跑 + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: reporter + image: your-reporter:latest + command: ["python", "generate_report.py"] +``` + +**Cron 表达式速查:** +``` +┌───────────── 分钟 (0-59) +│ ┌───────────── 小时 (0-23) +│ │ ┌───────────── 日 (1-31) +│ │ │ ┌───────────── 月 (1-12) +│ │ │ │ ┌───────────── 星期几 (0-6, 0=周日) +│ │ │ │ │ +* * * * * + +0 2 * * * 每天凌晨 2 点 +*/15 * * * * 每 15 分钟 +0 9 * * 1 每周一上午 9 点 +0 0 1 * * 每月 1 号零点 +``` + +--- + +### 2.9 任务调度架构:一任务一容器的正确姿势 + +> **核心问题**:业务系统发起一个重型任务,需要为它独立起一个容器运行。CPU/内存 Auto Scaling(HPA)解决不了这个问题,业务系统也不应该直接调 K8S API。那正确的架构是什么? + +#### 为什么 HPA 解决不了这个问题 + +这是一个常见的认知误区,必须澄清: + +| 维度 | HPA(水平 Pod 自动伸缩) | 任务级伸缩 | +|------|----------------------|----------| +| **伸缩对象** | 扩/缩 Deployment 的**副本数** | 为每个任务**独立创建**一个 Pod | +| **触发条件** | CPU/内存使用率超过阈值 | 有新任务进队列 | +| **Pod 生命周期** | Pod 持续运行,处理多个请求 | Pod 处理完一个任务后退出 | +| **任务隔离** | 多个任务共享同一批 Pod | 每个任务有自己独占的 Pod | +| **适用场景** | Web 服务、无状态 API | ML 训练、视频转码、数据处理 | + +**HPA 的本质**:你有 3 个 Worker Pod,CPU 高了变成 6 个——但这 6 个 Pod 仍然是竞争同一个队列,没有"任务和容器 1:1 绑定"的概念。 + +**一任务一容器的本质**:不是扩副本,而是**动态创建 Job**——每个 Job 有自己独立的环境、资源、生命周期,完成后自动销毁。 + +--- + +#### 架构分层原则:业务系统不应直接调 K8S API + +```mermaid +flowchart LR + subgraph Wrong["❌ 错误做法(强耦合)"] + A1["业务系统"] -->|"直接调用"| K8S["K8S API"] + K8S -->|"创建 Job"| J["Job"] + end + + subgraph Correct["✅ 正确做法(解耦)"] + A2["业务系统"] -->|"写入"| MQ["消息队列 / 数据库"] + MQ -->|"调度层"| Dispatcher["调度层"] + Dispatcher -->|"K8S API"| K8S2["K8S API"] + K8S2 -->|"创建 Job"| J2["Job"] + end +``` + +> **图 2-5:业务系统与 K8S 交互的正确架构** + +**为什么业务系统不能直接调 K8S API?** +1. **职责混乱**:业务系统需要知道 K8S 集群地址、ServiceAccount 权限、Job YAML 结构——把基础设施细节泄漏到业务层 +2. **强依赖**:K8S 集群迁移、升级,业务系统跟着改 +3. **权限风险**:给业务系统 K8S API 权限,意味着一旦业务系统被攻破,集群暴露 +4. **可观测性差**:任务状态分散在 K8S Event 里,业务侧看不到 + +--- + +#### 三种架构模式 + +根据业务系统的交互方式,有三种成熟方案: + +--- + +##### 模式 A:消息队列 + KEDA ScaledJob(推荐,消息队列场景) + +```mermaid +flowchart LR + A["业务系统"] -->|"写消息"| MQ["SQS/Kafka/Redis"] + MQ -->|"KEDA 监听"| KEDA["KEDA"] + KEDA -->|"自动创建 Job"| TaskPod["任务 Pod"] + KEDA -.->|"(K8S 集群内的 KEDA 组件,
业务系统完全无感 K8S)"| note["⚠️"] +``` + +> **图 2-6:消息队列 + KEDA ScaledJob 架构** + +**KEDA 是 CNCF 项目,本质是 K8S 集群内的一个 Controller**,它盯着外部队列的深度,自动帮你创建/销毁 Job。业务系统只写队列,和 K8S 零耦合。 + +**安装 KEDA:** +```bash +helm repo add kedacore https://kedacore.github.io/charts +helm install keda kedacore/keda --namespace keda --create-namespace +``` + +**ScaledJob 配置(SQS 示例):** +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledJob +metadata: + name: heavy-task-scaledjob + namespace: default +spec: + # ===== Job 模板 ===== + jobTargetRef: + template: + spec: + restartPolicy: Never + containers: + - name: worker + image: your-heavy-worker:latest + env: + - name: AWS_REGION + value: "ap-east-1" + resources: + requests: + cpu: "4" + memory: "8Gi" + limits: + cpu: "8" + memory: "16Gi" + + # ===== 扩缩策略 ===== + pollingInterval: 5 # 每 5 秒检查队列 + maxReplicaCount: 20 # 最大并发 Job 数 + scalingStrategy: + strategy: "accurate" # 队列 N 条消息 → 起 N 个 Job + + # ===== 触发器(SQS)===== + triggers: + - type: aws-sqs-queue + metadata: + queueURL: https://sqs.ap-east-1.amazonaws.com/123/task-queue + queueLength: "1" # 1 条消息 = 1 个 Job + awsRegion: "ap-east-1" + authenticationRef: + name: keda-aws-creds +--- +# AWS 认证 +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: keda-aws-creds +spec: + secretTargetRef: + - parameter: awsAccessKeyID + name: aws-secret + key: AWS_ACCESS_KEY_ID + - parameter: awsSecretAccessKey + name: aws-secret + key: AWS_SECRET_ACCESS_KEY +``` + +**其他常用触发源:** +```yaml +# Kafka(按 consumer lag 触发) +- type: kafka + metadata: + bootstrapServers: kafka:9092 + consumerGroup: heavy-task-group + topic: heavy-tasks + lagThreshold: "1" # lag 每增加 1 → 起 1 个 Job + +# Redis List +- type: redis + metadata: + address: redis:6379 + listName: task-queue + listLength: "1" + +# RabbitMQ +- type: rabbitmq + metadata: + host: amqp://user:pass@rabbitmq:5672/ + queueName: heavy-tasks + queueLength: "1" +``` + +--- + +##### 模式 B:Dispatcher 服务(推荐,数据库记录场景) + +业务系统只写一行数据库记录(如 `status=pending`),由一个专职的 **Dispatcher 微服务**轮询 DB,发现新任务后调用 K8S API 创建 Job,并负责维护任务状态回写。 + +```mermaid +flowchart TB + A["业务系统"] -->|"INSERT"| DB["tasks 表 (status=pending)"] + DB --> B["Dispatcher 服务(K8S 内的一个 Deployment)"] + B --> C1["轮询 DB,发现 pending 任务"] + B --> C2["调用 K8S API 创建 Job"] + B --> C3["更新 tasks 表 status=running"] + B --> C4["监听 Job 完成事件,回写 status=done/failed"] + C2 --> J["Job Pod(处理任务)"] + J --> DB2["任务完成,向 DB 写入结果"] +``` + +> **图 2-7:Dispatcher 服务架构** + +**Dispatcher 实现思路(Python 伪代码):** +```python +# dispatcher.py —— 作为 K8S Deployment 运行,有 K8S API 权限 +import time +from kubernetes import client, config +from db import get_pending_tasks, update_task_status + +config.load_incluster_config() # 在 K8S 内自动加载 ServiceAccount 权限 +batch_v1 = client.BatchV1Api() + +def dispatch_loop(): + while True: + tasks = get_pending_tasks(limit=10) # 每次取最多 10 个待处理任务 + for task in tasks: + # 防止重复调度:先更新状态,再创建 Job(乐观锁) + updated = update_task_status(task.id, + old_status="pending", + new_status="dispatching") + if not updated: + continue # 已被其他 Dispatcher 实例抢走,跳过 + + job = build_job(task) + try: + batch_v1.create_namespaced_job("default", job) + update_task_status(task.id, "dispatching", "running", + job_name=f"task-{task.id}") + except Exception as e: + update_task_status(task.id, "dispatching", "pending") # 回滚 + + time.sleep(3) # 轮询间隔 3 秒 + +def build_job(task): + return client.V1Job( + metadata=client.V1ObjectMeta(name=f"task-{task.id}"), + spec=client.V1JobSpec( + ttl_seconds_after_finished=300, + backoff_limit=2, + template=client.V1PodTemplateSpec( + spec=client.V1PodSpec( + restart_policy="Never", + containers=[client.V1Container( + name="worker", + image="your-worker:latest", + env=[ + client.V1EnvVar("TASK_ID", task.id), + client.V1EnvVar("DB_URL", task.db_url), + ], + resources=client.V1ResourceRequirements( + requests={"cpu": "2", "memory": "4Gi"}, + limits={"cpu": "4", "memory": "8Gi"}, + ) + )] + ) + ) + ) + ) +``` + +**Dispatcher 的 ServiceAccount 配置(最小权限原则):** +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: task-dispatcher + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: job-creator + namespace: default +rules: +- apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["create", "get", "list", "watch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: dispatcher-job-creator + namespace: default +subjects: +- kind: ServiceAccount + name: task-dispatcher +roleRef: + kind: Role + name: job-creator + apiGroup: rbac.authorization.k8s.io +--- +# Dispatcher Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: task-dispatcher +spec: + replicas: 1 # 通常单副本避免重复调度(或用选主机制) + selector: + matchLabels: + app: task-dispatcher + template: + metadata: + labels: + app: task-dispatcher + spec: + serviceAccountName: task-dispatcher # ⭐ 绑定最小权限 SA + containers: + - name: dispatcher + image: your-dispatcher:latest + resources: + requests: + cpu: "100m" + memory: "128Mi" +``` + +> **注意**:Dispatcher 通常只跑一个副本。如果需要高可用多副本,任务状态更新必须使用**乐观锁**(如上方的 `old_status=pending` 条件更新)或**分布式锁**,防止同一任务被多个 Dispatcher 重复调度。 + +--- + +##### 模式 C:Argo Workflows(复杂 DAG 工作流场景) + +当任务有依赖关系(步骤 A 完成后才能执行步骤 B/C,或者需要分支、并行、重试逻辑),推荐 **Argo Workflows**。 + +```mermaid +flowchart LR + A["业务系统"] -->|"提交 Workflow 定义"| Argo["Argo Server(K8S 内)"] + Argo -->|"按 DAG 顺序创建 Pod"| Pods["Pod"] +``` + +> **图 2-8:Argo Workflows 架构** + + +```yaml +# 示例:视频处理 Workflow(下载 → 转码 → 上传,可并行多路转码) +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: video-process- +spec: + entrypoint: video-pipeline + templates: + - name: video-pipeline + dag: + tasks: + - name: download + template: download-step + - name: transcode-hd + dependencies: [download] # 依赖 download 完成 + template: transcode-step + arguments: + parameters: [{name: quality, value: "1080p"}] + - name: transcode-sd + dependencies: [download] # 和 transcode-hd 并行 + template: transcode-step + arguments: + parameters: [{name: quality, value: "480p"}] + - name: upload + dependencies: [transcode-hd, transcode-sd] # 等两路都完成 + template: upload-step + + - name: download-step + container: + image: downloader:latest + resources: + requests: {cpu: "1", memory: "2Gi"} + + - name: transcode-step + inputs: + parameters: [{name: quality}] + container: + image: ffmpeg:latest + command: ["ffmpeg", "-i", "input.mp4", "-q:v", "{{inputs.parameters.quality}}", "output.mp4"] + resources: + requests: {cpu: "4", memory: "8Gi"} + + - name: upload-step + container: + image: uploader:latest +``` + +--- + +#### 方案选择指南 + +| 业务系统的动作 | 推荐方案 | 核心优势 | +|---|---|---| +| 往 **SQS/Kafka/RabbitMQ/Redis** 写一条消息 | **KEDA ScaledJob** | 零代码调度,K8S 原生,业务完全解耦 | +| 往 **数据库** 写一行记录(status=pending) | **Dispatcher 服务** | 灵活、可定制,状态双向同步 | +| 触发**多步骤有依赖**的任务链 | **Argo Workflows** | DAG 编排,可视化,内置重试/超时 | +| **定时触发**(不需要外部触发) | **CronJob** | K8S 原生,无需额外组件 | + +> **架构原则总结**:业务系统只负责**表达意图**(写队列 / 写 DB),调度层负责**如何实现**(什么时候起 Pod、起几个、用多少资源)。这两层的职责必须清晰分离。 + +--- + +### 2.10 常用 kubectl 命令速查 + +```bash +# 查看资源 +kubectl get pods -n k8s-training +kubectl get deployment -n k8s-training +kubectl get all -n k8s-training + +# 查看详情 +kubectl describe pod -n k8s-training +kubectl logs -n k8s-training +kubectl logs -f -n k8s-training # 实时日志 + +# 部署 +kubectl apply -f deployment.yaml +kubectl delete -f deployment.yaml + +# 扩缩 +kubectl scale deployment nginx --replicas=5 -n k8s-training + +# 滚动更新 +kubectl set image deployment/nginx nginx=nginx:1.26 -n k8s-training + +# 回滚 +kubectl rollout status deployment/nginx -n k8s-training # 查看更新状态 +kubectl rollout history deployment/nginx -n k8s-training # 查看历史 +kubectl rollout undo deployment/nginx -n k8s-training # 回滚到上一版本 + +# 进入容器 +kubectl exec -it -- /bin/bash -n k8s-training + +# 端口转发(调试用) +kubectl port-forward svc/nginx 8080:80 -n k8s-training +``` + +--- + +## 📝 习题 + +### 选择题 + +**Q1.** Pod 中多个容器共享什么? +- A) CPU 和内存 +- B) 网络命名空间和存储卷 +- C) 环境变量 +- D) 以上全部 + +**Q2.** livenessProbe 和 readinessProbe 的区别是? +- A) 没有区别 +- B) livenessProbe 失败重启容器,readinessProbe 失败移除流量 +- C) readinessProbe 失败重启容器,livenessProbe 失败移除流量 +- D) 只能用 HTTP 探测方式 + +**Q3.** 以下哪个适合使用 StatefulSet 部署? +- A) Nginx Web 服务 +- B) Spring Boot API +- C) MySQL 主从集群 +- D) 日志收集 Agent + +**Q4.** Deployment 的 `maxSurge: 1, maxUnavailable: 0` 含义是? +- A) 最多多出1个 Pod,最多0个不可用 +- B) 最多多出1个不可用的 Pod +- C) 每次只更新1个 Pod +- D) 更新时不允许任何 Pod 停止 + +**Q5.** 资源限制中 `requests` 和 `limits` 的区别? +- A) 没有区别 +- B) requests 是调度依据,limits 是运行时硬限制 +- C) requests 是硬限制,limits 是调度依据 +- D) requests 用于 CPU,limits 用于内存 + +### 简答题 + +**Q6.** Deployment YAML 中的 `selector.matchLabels` 和 `template.metadata.labels` 是什么关系?如果不匹配会怎样? + +**Q7.** 如果你在集群中手动创建了一个标签为 `app: nginx` 的独立 Pod,它会不会被某个 Deployment 管理?为什么? + +**Q8.** 请解释 DaemonSet 的使用场景,并举例说明。 + +**Q9.** 请描述 `kubectl rollout undo` 回滚的过程。 + +**Q10.** 直接创建 Pod(`kind: Pod`)和通过 Deployment 创建 Pod 有什么区别?分别适用于什么场景? + +**Q11(⭐ 高频)** Job 中 `restartPolicy` 为什么必须设为 `Never` 或 `OnFailure`?`backoffLimit` 和 `activeDeadlineSeconds` 分别控制什么? + +**Q12(⭐⭐ 架构题)** 业务系统有一个 heavy 任务,每个任务需要独立容器运行,CPU/内存很高。有人说"用 HPA 扩容就行了",这个说法对吗?为什么?正确的做法是什么? + +**Q13(⭐ 架构题)** 业务系统发起任务时,应该直接调用 K8S API 创建 Job 吗?如果不应该,中间应该有什么层?分别适用于什么场景(消息队列场景 vs 数据库记录场景)? + +### 动手题 + +**Q11.** 请编写一个 Deployment YAML,要求: +- 3 副本 +- 镜像:`nginx:1.25` +- 资源限制:CPU 100m~500m,内存 64Mi~256Mi +- HTTP 存活探针:路径 `/`,端口 80 +- 标签:`app=nginx, tier=frontend` + +--- + +## ✅ 参考答案 + +### A1. B) 网络命名空间和存储卷 +同一 Pod 内的容器共享网络命名空间(相同 IP,通过 localhost 通信)和挂载的存储卷。CPU/内存是各自配置的。 + +### A2. B) livenessProbe 失败重启容器,readinessProbe 失败移除流量 +- **livenessProbe**:容器"死了" → K8S 重启它 +- **readinessProbe**:容器"还没准备好" → 从 Service 负载均衡中暂时移除,流量不会打到它 + +### A3. C) MySQL 主从集群 +StatefulSet 提供稳定的网络标识(Pod 名称不变)和稳定的持久存储,适合有状态应用。Nginx/Spring Boot 用 Deployment,日志 Agent 用 DaemonSet。 + +### A4. A) 最多多出1个 Pod,最多0个不可用 +`maxSurge: 1` = 更新过程中最多比期望多1个 Pod;`maxUnavailable: 0` = 更新过程中不允许任何 Pod 不可用。这是最保守的更新策略,保证零停机。 + +### A5. B) requests 是调度依据,limits 是运行时硬限制 +- **requests**:Scheduler 调度 Pod 时参考,确保 Node 有足够剩余资源 +- **limits**:运行时硬限制,CPU 超限会限流(throttle),内存超限会 OOM Kill + +### A6. selector.matchLabels 和 template.metadata.labels 的关系 + +**必须匹配!** 这是 Deployment 工作机制的核心: + +``` +Deployment Controller 的工作循环: +1. 用 selector.matchLabels 去 K8S 里找所有匹配的 Pod +2. 数一下找到了几个 +3. 如果少于 replicas → 用 template 创建新的 Pod(新 Pod 会自动打上 template.metadata.labels) +4. 如果多于 replicas → 删除多余的 Pod +5. 重复 + +如果 selector 和 template.labels 不匹配: + → selector 说"我要找 app=nginx 的 Pod" + → template 创建出来的 Pod 标签却是 app=web + → Controller 找不到自己创建的 Pod(标签不匹配) + → Controller 觉得"0 个 Pod,我想要 3 个"→ 再创建 3 个 + → 新的 3 个也不匹配 → 再创建 → 无限循环! + → 所以 K8S API Server 直接在校验阶段拒绝这种配置 +``` + +**简单记:selector 是"找人的条件",template.labels 是"新人身上的标签"。条件和新人的标签必须一致,否则 Deployment 认不出自己创建的 Pod。** + +### A7. 手动创建的 Pod 会不会被 Deployment 管理? + +**会被!** 但不推荐这样做。 + +``` +Deployment: selector = {app: nginx}, replicas = 3 +你手动: kubectl apply -f pod.yaml (labels: {app: nginx}) + +结果: + Deployment Controller 发现集群中有 4 个 app=nginx 的 Pod + 但 replicas 只要求 3 个 + → Deployment 会删除"多余的"那个(可能是你手动创建的) + → 你手动创建的 Pod 随时可能被 Deployment 清理掉! +``` + +**结论**:不要在 Deployment 管理的范围内手动创建标签匹配的 Pod,它会被当作"多余的副本"删掉。 + +### A8. DaemonSet 使用场景 +DaemonSet 确保**每个(或特定的)Node 上都运行一个 Pod 副本**。常见场景: +- **日志收集**:Fluentd/Filebeat,每个节点收集日志 +- **监控**:Prometheus Node Exporter,每个节点采集指标 +- **网络插件**:Calico/Flannel 的 Agent +- **存储守护进程**:每个节点上的存储守护进程 + +新节点加入集群时,DaemonSet 自动在新节点上创建 Pod。 + +### A9. kubectl rollout undo 回滚过程 +``` +1. K8S 维护 Deployment 的 ControllerRevision(历史版本记录) +2. kubectl rollout undo 时,Controller 将旧版本的 Pod 模板恢复 +3. 创建新的 ReplicaSet 使用旧版本模板 +4. 滚动更新机制将当前 Pod 逐步替换为旧版本 +5. 回滚过程和更新过程一样是渐进的 + +# 回滚到指定版本 +kubectl rollout undo deployment/nginx --to-revision=2 +``` + +### A10. 独立 Pod vs Deployment 管理 Pod + +| 对比项 | 独立 Pod (`kind: Pod`) | Deployment 管理 Pod | +|--------|----------------------|-------------------| +| Pod 挂了 | 没人管,永久丢失 | 自动重建新 Pod | +| 副本数 | 永远只有 1 个 | replicas 控制,随意扩缩 | +| 更新 | 删了重建,有停机时间 | 滚动更新,零停机 | +| 你需要写 | 完整的 Pod YAML | Deployment YAML(内含 template) | +| 生命周期 | **你自己管** | **K8S 管你** | +| 适用场景 | 临时调试、一次性任务、CI 测试 | 生产部署的所有无状态应用 | + +### A11. Job 的 restartPolicy 和控制字段 + +**为什么必须设 `Never` 或 `OnFailure`**: +Deployment 的默认 `restartPolicy: Always` 意味着"容器退出就重启",这对长期服务是对的。但 Job 的容器正常退出(exit code 0)代表任务完成,不应该被重启。如果用 `Always`,任务完成后会被反复重启,变成死循环。 + +- `restartPolicy: Never`:失败时起新 Pod(日志保留在失败 Pod 上,便于排查) +- `restartPolicy: OnFailure`:失败时在原 Pod 重启容器(节省资源) + +**`backoffLimit` vs `activeDeadlineSeconds`**: +| 字段 | 含义 | 区别 | +|------|------|------| +| `backoffLimit: 3` | 最多失败重试 3 次,超过则 Job 标为 Failed | 按**次数**限制 | +| `activeDeadlineSeconds: 600` | Job 整体超时 600 秒,超时则强制终止 | 按**时间**限制 | + +两者可以同时设置,任意一个触发都会终止 Job。 + +### A12. HPA 能解决"一任务一容器"问题吗? + +**不能,这是两个完全不同的维度。** + +HPA 的本质是"扩 Deployment 的副本数"。假设你有 3 个 Worker Pod,CPU 高了 HPA 扩到 6 个——这 6 个 Pod 仍然是一个 Worker Pool,竞争同一个队列,没有任何任务隔离。 + +| | HPA | 任务级伸缩(Job per task)| +|--|-----|---| +| 扩容结果 | 更多 Worker Pod | 更多独立 Job | +| 任务隔离 | 无(多任务共享 Pod) | 完全隔离(1任务1Pod)| +| 触发条件 | CPU/内存使用率 | 队列消息数 / DB pending 记录数 | +| Pod 退出 | 异常(会被重建) | 正常(任务完成标志)| + +**正确方案**:用 `Job`(而非 Deployment)承载任务,每个任务创建一个独立的 Job。 + +### A13. 业务系统应该直接调 K8S API 吗? + +**不应该。** 原因: +1. 职责混乱:业务代码需要知道 K8S 集群地址、Job YAML 格式、ServiceAccount 权限 +2. 强依赖:集群迁移时业务系统一起改 +3. 权限风险:业务系统被攻破则集群暴露 + +**正确做法**:在业务系统和 K8S 之间加一个**调度层**,业务系统只表达意图。 + +| 业务系统的动作 | 调度层方案 | +|---|---| +| 往 **MQ(SQS/Kafka)** 写消息 | **KEDA ScaledJob**:监听队列,自动为每条消息创建 Job,业务完全无感知 K8S | +| 往 **数据库** 写 pending 记录 | **Dispatcher 服务**:专职微服务轮询 DB,发现 pending 任务后调 K8S API 创建 Job,并负责状态回写 | +| 复杂多步骤工作流 | **Argo Workflows**:业务提交 Workflow 定义,引擎负责按 DAG 调度 Pod | + +架构原则:**业务系统表达意图(写队列/写DB),调度层决定如何实现(何时起Pod、起几个、资源多少)。** + +### A14. Deployment YAML(原 A11) + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + namespace: k8s-training + labels: # ① Deployment 自身的标签(不影响 Pod 选择) + app: nginx + tier: frontend +spec: + replicas: 3 # 要维护 3 个 Pod 副本 + selector: # ② 选择器:用标签找"我管的 Pod" + matchLabels: + app: nginx # 找标签包含 app=nginx 的 Pod + tier: frontend + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: # ③⭐ Pod 模板:这就是"隐含的 Pod 定义" + metadata: + labels: # ④ 新 Pod 的标签(必须和 ② selector 匹配!) + app: nginx + tier: frontend # Deployment 用这个标签来认领自己创建的 Pod + spec: # ⑤ 下面就是 Pod 的 spec,和独立 Pod YAML 的 spec 一模一样 + containers: + - name: nginx + image: nginx:1.25 + ports: + - containerPort: 80 + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "256Mi" + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 5 +# 部署后,kubectl get pods 会看到 3 个自动创建的 Pod: +# nginx-deployment-abc1234-5xyz1 +# nginx-deployment-abc1234-5xyz2 +# nginx-deployment-abc1234-5xyz3 +# 每个都自动带有 labels: {app: nginx, tier: frontend} +``` + +--- + +## 🔧 实操练习 + +```bash +export KUBECONFIG=~/.kube/config +kubectl create namespace k8s-training + +# 部署 Nginx Deployment +kubectl apply -f - <10.244.1.5"] <-->|"直接通信"| CNI["CNI 插件
(Calico等)"] + CNI <--> PodB["Pod B
10.244.2.3"] + + note1["K8S 网络三大要求:
1. 所有 Pod 之间可以直接通信(不经过 NAT)
2. 所有 Node 和所有 Pod 之间可以直接通信
3. Pod 看到自己的 IP 和其他 Pod 的 IP 是一样的"] +``` + +> **图 3-1:K8S 网络模型** + +### 3.2 为什么需要 Service? + +**问题**:Pod IP 是不稳定的! +- Pod 每次重建 IP 都会变 +- 水平扩缩时 Pod 数量会变 +- 客户端不可能记住一堆会变的 IP + +**解决**:Service 提供稳定的 VIP(虚拟 IP)和 DNS 名称 + +```mermaid +flowchart TB + SVC["Service
ClusterIP
10.0.0.100"] -->|"负载均衡"| P1["Pod 1
.1.5"] + SVC --> P2["Pod 2
.2.3"] + SVC --> P3["Pod 3
.2.4"] + + note1["Pod 3 挂了?Service 自动从 Endpoints 移除
新 Pod 加入?Service 自动加入 Endpoints"] +``` + +> **图 3-2:Service 工作原理** + +### 3.3 Service 四种类型(面试必背) + +| 类型 | 说明 | 使用场景 | +|------|------|---------| +| **ClusterIP** | 默认值,仅集群内部可访问 | 内部服务通信(微服务间) | +| **NodePort** | 每个节点开放一个端口 | 开发测试、简单外部访问 | +| **LoadBalancer** | 云厂商创建外部负载均衡 | 生产环境对外暴露服务 | +| **ExternalName** | DNS 级别的 CNAME 映射 | 引用外部服务(如 RDS) | + +#### ClusterIP Service(最常用) + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx-service + namespace: k8s-training +spec: + type: ClusterIP # 默认值 + selector: # 通过 label 选择后端 Pod + app: nginx + ports: + - port: 80 # Service 端口 + targetPort: 80 # Pod 端口 + protocol: TCP +``` + +```bash +# 集群内部访问方式: +# 1. Service IP +curl http://10.0.0.100:80 + +# 2. DNS(推荐)⚡ +curl http://nginx-service.k8s-training.svc.cluster.local + +# 3. 同 namespace 内可简写 +curl http://nginx-service +``` + +#### NodePort Service + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx-nodeport +spec: + type: NodePort + selector: + app: nginx + ports: + - port: 80 # Service 端口 + targetPort: 80 # Pod 端口 + nodePort: 30080 # 节点端口(30000-32767) +``` + +``` +外部请求 → NodeIP:30080 → kube-proxy(iptables) → Pod:80 +``` + +#### LoadBalancer Service + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx-lb +spec: + type: LoadBalancer + selector: + app: nginx + ports: + - port: 80 + targetPort: 80 +``` + +> 云厂商(AWS/GCP/阿里云)会自动创建一个 ELB/SLB 并分配公网 IP。Sealos 环境也支持。 + +### 3.4 DNS 服务发现(面试高频) + +K8S 内置 DNS 服务(CoreDNS),自动为 Service 创建 DNS 记录: + +``` +完整 FQDN 格式: +..svc.cluster.local + +示例: +nginx-service.k8s-training.svc.cluster.local + +简写规则: +- 同 namespace: nginx-service +- 跨 namespace: nginx-service.k8s-training +- 完整: nginx-service.k8s-training.svc.cluster.local +``` + +**面试题**:Pod 内 `nginx-service` 是如何解析到 Service ClusterIP 的? +> Pod 的 `/etc/resolv.conf` 中配置了 `nameserver` 指向 CoreDNS Service IP,`search` 配置了 namespace 和 `svc.cluster.local`。所以 `nginx-service` 会先尝试 `nginx-service.k8s-training.svc.cluster.local`。 + +### 3.5 Headless Service + +将 `clusterIP: None`,DNS 直接解析到 Pod IP: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: mysql-headless +spec: + clusterIP: None # Headless! + selector: + app: mysql + ports: + - port: 3306 +``` + +``` +# 普通 Service DNS 解析: +mysql-service → 10.0.0.100 (ClusterIP) + +# Headless Service DNS 解析: +mysql-headless → 10.244.1.5, 10.244.2.3, 10.244.2.4 (直接是 Pod IP 列表) +``` + +**使用场景**:StatefulSet 需要每个 Pod 有独立的 DNS 记录: +``` +mysql-0.mysql-headless.namespace.svc.cluster.local → 10.244.1.5 +mysql-1.mysql-headless.namespace.svc.cluster.local → 10.244.2.3 +``` + +### 3.6 Ingress(7层路由) + +Service 是 4层(TCP/UDP),Ingress 是 7层(HTTP/HTTPS)。 + +Ingress 后端的 Service 一般使用 ClusterIP,因为 Ingress Controller 运行在集群内部,通过 ClusterIP 访问后端 Pod,而 NodePort 或 LoadBalancer 是用来暴露 Ingress Controller 本身的。 + +在生产环境中,Ingress Controller 通常通过 LoadBalancer 类型的 Service 暴露,云厂商会提供外部 IP,将外部流量引入集群,再由 Ingress Controller 根据 Ingress 规则转发到后端 ClusterIP Service。 + +```mermaid +flowchart TB + User["互联网用户"] -->|"请求"| Ingress["Ingress
Controller
(Nginx/Traefik)"] + + Ingress -->|"HTTP 路由规则"| Rules["Ingress 资源

host: api.example.com → api-svc
host: www.example.com → web-svc
/api/* → api-svc
/* → web-svc"] + + Rules --> apiSVC["api-svc
ClusterIP"] + Rules --> webSVC["web-svc
ClusterIP"] +``` + +> **图 3-3:Ingress 工作原理** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: app-ingress + namespace: k8s-training + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx # 指定 Ingress Controller + rules: + - host: app.example.com # 域名 + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: api-service # 得先建 service 一般是clusterip,也可以是其他的,但是没必要 + port: + number: 80 + - path: / + pathType: Prefix + backend: + service: + name: web-service + port: + number: 80 + tls: # HTTPS 配置 + - hosts: + - app.example.com + secretName: tls-secret +``` + +**Ingress vs Service LoadBalancer(面试对比)**: +| 对比项 | LoadBalancer Service | Ingress | +|--------|---------------------|---------| +| 层级 | 4层(TCP) | 7层(HTTP) | +| 成本 | 每个 Service 一个 LB | 多个 Service 共享一个 LB | +| 路由能力 | 无 | 基于域名、路径路由 | +| TLS | 自己处理 | 统一管理证书 | + +### 3.7 NetworkPolicy(网络策略) + +默认 K8S 中所有 Pod 之间网络是互通的。NetworkPolicy 可以限制流量: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: api-policy + namespace: k8s-training +spec: + podSelector: + matchLabels: + app: api + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: web # 只允许 label=web 的 Pod 访问 + ports: + - port: 8080 +``` + +--- + +## 📝 习题 + +### 选择题 + +**Q1.** K8S 中 Service 的默认类型是? +- A) NodePort +- B) LoadBalancer +- C) ClusterIP +- D) ExternalName + +**Q2.** 同一 namespace 下的 Pod 如何通过 DNS 访问名为 `user-service` 的 Service? +- A) user-service.k8s-training.svc.cluster.local +- B) user-service +- C) A 和 B 都可以 +- D) 需要直接用 Service IP + +**Q3.** NodePort 端口的默认范围是? +- A) 1-1024 +- B) 3000-4000 +- C) 30000-32767 +- D) 80-443 + +**Q4.** Headless Service 的 clusterIP 设置为什么? +- A) 0.0.0.0 +- B) None +- C) 空 +- D) 127.0.0.1 + +**Q5.** Ingress 工作在哪一层? +- A) 第2层(数据链路层) +- B) 第3层(网络层) +- C) 第4层(传输层) +- D) 第7层(应用层) + +### 简答题 + +**Q6.** Service 和 Pod 之间是怎么关联的?Service 如何感知 Pod 的变化? + +**Q7.** 请解释 Ingress 和 Service 的关系。有了 Service 为什么还需要 Ingress? + +**Q8.** 什么场景下需要用 Headless Service? + +### 动手题 + +**Q9.** 请为上一章的 Nginx Deployment 创建: +1. 一个 ClusterIP Service +2. 一个 NodePort Service +3. 通过端口转发测试 Service 是否正常工作 + +--- + +## ✅ 参考答案 + +### A1. C) ClusterIP +不指定 type 时默认是 ClusterIP,只能在集群内部访问。 + +### A2. C) A 和 B 都可以 +同 namespace 下可以用短名称 `user-service`,K8S DNS 会自动补全。 + +### A3. C) 30000-32767 +NodePort 默认范围是 30000-32767,可以通过 kube-apiserver 的 `--service-node-port-range` 参数修改。 + +### A4. B) None +`clusterIP: None` 表示 Headless Service,DNS 直接返回 Pod IP 而不是 ClusterIP。 + +### A5. D) 第7层(应用层) +Ingress 工作在 HTTP/HTTPS 层,可以基于域名和路径做路由。 + +### A6. Service 与 Pod 的关联 +``` +1. Service 创建时定义 selector(标签选择器) +2. K8S Endpoints Controller 持续监控匹配 selector 的 Pod +3. 将匹配的 Pod IP:Port 写入 Service 的 Endpoints 对象 +4. kube-proxy 监听 Endpoints 变化,更新 iptables/IPVS 规则 +5. 流量到达 Service VIP 时,由 iptables/IPVS 负载均衡到 Endpoints 中的 Pod + +# 查看自动关联的 Endpoints +kubectl get endpoints nginx-service +``` + +### A7. Ingress 和 Service 的关系 +**Service 是 4 层负载均衡**:只按 IP:Port 转发,不知道 HTTP 域名和路径。 +**Ingress 是 7 层路由**: +- 一个 Ingress 可以根据域名/路径路由到多个 Service +- 统一管理 TLS 证书 +- 共享一个外部 IP(而不是每个 Service 一个 LoadBalancer) + +简单说:**Service 是内部负载均衡,Ingress 是外部流量入口的路由器**。 + +### A8. Headless Service 使用场景 +1. **StatefulSet**:需要每个 Pod 有稳定、独立的 DNS 记录(如 `mysql-0.mysql`) +2. **自行负载均衡**:客户端自己决定连接哪个 Pod(如自定义负载均衡算法) +3. **直接 Pod 发现**:需要知道每个 Pod 的具体 IP + +### A9. Service YAML + 测试 + +```yaml +# ClusterIP Service +apiVersion: v1 +kind: Service +metadata: + name: nginx-clusterip + namespace: k8s-training +spec: + type: ClusterIP + selector: + app: nginx + ports: + - port: 80 + targetPort: 80 + +--- +# NodePort Service +apiVersion: v1 +kind: Service +metadata: + name: nginx-nodeport + namespace: k8s-training +spec: + type: NodePort + selector: + app: nginx + ports: + - port: 80 + targetPort: 80 + nodePort: 30080 +``` + +```bash +# 测试 +kubectl port-forward svc/nginx-clusterip 8080:80 -n k8s-training +# 然后另一个终端: +curl http://localhost:8080 +``` + +--- + +## 🔧 实操练习 + +```bash +export KUBECONFIG=~/.kube/config +kubectl create namespace k8s-training + +# 1. 部署应用 +kubectl apply -f - < 1. Secret 存储的值是 **base64 编码**(不是加密!只是编码) +> 2. Secret 在传输和存储时可以启用 **加密 at rest**(etcd 加密) +> 3. Secret 访问需要 **RBAC 权限控制** +> 4. Secret 有大小限制(1MB) +> 5. 生产环境建议使用 **External Secrets Operator** 或 Vault + +#### Secret YAML + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: app-secret + namespace: k8s-training +type: Opaque # 通用类型 +data: + # ⚠️ data 字段的值必须是 base64 编码 + username: YWRtaW4= # echo -n "admin" | base64 + password: cEBzc3cwcmQ= # echo -n "p@ssw0rd" | base64 +--- +# stringData 字段可以直接写明文(K8S 自动编码) +apiVersion: v1 +kind: Secret +metadata: + name: app-secret + namespace: k8s-training +type: Opaque +stringData: # ⭐ 明文,方便使用 + username: admin + password: p@ssw0rd +``` + +#### Secret 常见类型 + +| 类型 | 用途 | +|------|------| +| `Opaque` | 通用(密码、Key) | +| `kubernetes.io/dockerconfigjson` | Docker 镜像仓库凭证 | +| `kubernetes.io/tls` | TLS 证书 | +| `kubernetes.io/basic-auth` | Basic Auth | + +#### Docker 镜像仓库凭证(拉私有镜像必备) + +```bash +# 创建镜像仓库凭证 +kubectl create secret docker-registry regcred \ + --docker-server=registry.example.com \ + --docker-username=admin \ + --docker-password=pass123 \ + -n k8s-training + +# 在 Pod 中引用 +spec: + imagePullSecrets: + - name: regcred + containers: + - name: app + image: registry.example.com/myapp:v1 +``` + +### 4.4 PV / PVC 存储模型 + +**为什么需要 PV/PVC?** +- Pod 是临时的,重建后数据丢失 +- 需要持久化存储(数据库、文件上传等) +- 不同云厂商/存储类型差异大,需要抽象 + +``` +管理员创建 PV ──┐ + ├──→ 用户创建 PVC 绑定 ──→ Pod 使用 PVC +存储类供应 PV ──┘ +``` + +#### PersistentVolume(PV) + +```yaml +apiVersion: v1 +kind: PersistentVolume +metadata: + name: mysql-pv +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteOnce # 单节点读写 + persistentVolumeReclaimPolicy: Retain + storageClassName: manual # 定义了一个名称 + # hostPath 是最简单的,生产用 NFS/Ceph/云盘 + hostPath: + path: /data/mysql + type: DirectoryOrCreate +``` + +#### PersistentVolumeClaim(PVC) + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc + namespace: k8s-training +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi # 申请 5Gi + storageClassName: manual # 和上面定义的名称符合 +``` + +#### Pod 挂载 PVC + +```yaml +spec: + containers: + - name: mysql + image: mysql:8.0 + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql # MySQL 数据目录 + volumes: + - name: mysql-data + persistentVolumeClaim: + claimName: mysql-pvc # 引用 PVC +``` + +#### accessModes 类型 + +| 模式 | 缩写 | 说明 | +|------|------|------| +| ReadWriteOnce | RWO | 单节点读写(最常用) | +| ReadOnlyMany | ROX | 多节点只读 | +| ReadWriteMany | RWX | 多节点读写(如 NFS) | + +#### 回收策略 + +| 策略 | 说明 | +|------|------| +| Retain | PVC 删除后保留数据,需手动清理 | +| Delete | PVC 删除时自动删除 PV 和数据 | +| Recycle | 已废弃,执行 rm -rf | + +### 4.5 StorageClass(动态供应) + +生产环境推荐使用 StorageClass,自动创建 PV: + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: fast-ssd +provisioner: kubernetes.io/aws-ebs # 云厂商 provisioner +parameters: + type: gp3 + fsType: ext4 +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer # 延迟绑定,调度后再创建 +``` + +```yaml +# PVC 指定 storageClassName,自动创建 PV +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: dynamic-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: fast-ssd # ⭐ 使用 StorageClass + resources: + requests: + storage: 20Gi +``` + +### 4.6 emptyDir 和 hostPath + +```yaml +# emptyDir:Pod 临时存储,Pod 删除后数据丢失 +# 用途:容器间共享数据、缓存 +volumes: +- name: cache + emptyDir: {} + +# hostPath:挂载 Node 本地目录 +# 用途:开发测试,生产不推荐(Pod 漂移到其他 Node 数据丢失) +volumes: +- name: host-data + hostPath: + path: /var/data + type: DirectoryOrCreate +``` + +--- + +## 📝 习题 + +### 选择题 + +**Q1.** ConfigMap 中 data 字段的值是什么格式? +- A) 明文 +- B) base64 编码 +- C) AES 加密 +- D) MD5 哈希 + +**Q2.** Secret 中存储的密码是安全的吗? + +- A) 是,Secret 自动加密 +- B) 不是,默认只 base64 编码,需要额外配置 etcd 加密 +- C) Secret 不存储敏感信息 +- D) 取决于 Pod 的权限 + +**Q3.** PV 的 accessModes 中 `ReadWriteOnce` 表示? +- A) 多个节点可以读写 +- B) 只有一个节点可以读写 +- C) 所有节点只读 +- D) 只能写一次 + +**Q4.** Pod 删除后,哪种 Volume 的数据会保留? +- A) emptyDir +- B) hostPath +- C) persistentVolumeClaim +- D) B 和 C 都可能 + +**Q5.** 动态存储供应需要什么? +- A) 手动创建 PV +- B) StorageClass + Storage Provisioner +- C) ConfigMap +- D) Secret + +### 简答题 + +**Q6.** 请比较 ConfigMap 和 Secret 的区别。 + +**Q7.** 请解释 PV、PVC、StorageClass 三者的关系。 + +**Q8.** 为什么生产环境不建议使用 hostPath? + +--- + +## ✅ 参考答案 + +### A1. A) 明文 +ConfigMap 的 data 字段直接存明文字符串。而 Secret 的 data 字段是 base64 编码。 + +### A2. B) 不是,默认只 base64 编码 +base64 不是加密,只是编码,任何人都能解码。要安全需要: +1. 配置 etcd 加密(EncryptionConfiguration) +2. 启用 RBAC 限制 Secret 访问 +3. 考虑使用 Vault/External Secrets + +### A3. B) 只有一个节点可以读写 +ReadWriteOnce = RWO = 单节点读写。这是最常见的模式。 + +### A4. D) B 和 C 都可能 +- emptyDir:Pod 删除数据丢失 ❌ +- hostPath:数据在 Node 本地,Pod 删除但文件还在 ✅ +- PVC:数据在持久化存储中,Pod 删除不影响 ✅ + +### A5. B) StorageClass + Storage Provisioner +StorageClass 定义存储类型,Provisioner 负责自动创建 PV。PVC 指定 storageClassName 后自动完成供应。 + +### A6. ConfigMap vs Secret + +| 对比项 | ConfigMap | Secret | +|--------|-----------|--------| +| 存储内容 | 非敏感配置 | 敏感信息 | +| 数据格式 | 明文 | base64 编码 | +| 大小限制 | 1MB | 1MB | +| 安全性 | 普通 RBAC | 需要更严格的 RBAC | +| etcd 加密 | 通常不需要 | 建议启用 | +| 使用方式 | 环境变量/文件 | 环境变量/文件/imagePullSecrets | +| 生产建议 | 可以 | 用 Vault/External Secrets | + +### A7. PV、PVC、StorageClass 的关系 + +``` +StorageClass:存储"类型"的模板(如 SSD、HDD) + ↓ 定义 +Provisioner:自动创建 PV 的插件 + ↓ 创建 +PersistentVolume(PV):集群级别的存储资源(类似"磁盘") + ↓ 被绑定 +PersistentVolumeClaim(PVC):用户对存储的请求(类似"申请单") + ↓ 被使用 +Pod:通过 volumeMounts 挂载 PVC + +静态供应:管理员手动创建 PV → PVC 绑定 PV +动态供应:PVC 指定 StorageClass → Provisioner 自动创建 PV +``` + +### A8. 不建议 hostPath 的原因 +1. **数据不安全**:Node 故障时数据丢失 +2. **调度问题**:Pod 漂移到其他 Node 后找不到原来的数据 +3. **安全风险**:可以访问宿主机文件系统 +4. **隔离性差**:多 Pod 可能冲突 +5. **不适合集群环境**:违反了 Pod 的可移植性 + +--- + +## 🔧 实操练习 + +```bash +export KUBECONFIG=~/.kube/config +kubectl create namespace k8s-training + +# 1. 创建 ConfigMap +kubectl create configmap app-config \ + --from-literal=APP_ENV=production \ + --from-literal=LOG_LEVEL=info \ + -n k8s-training + +# 2. 创建 Secret +kubectl create secret generic app-secret \ + --from-literal=username=admin \ + --from-literal=password=MySecret123 \ + -n k8s-training + +# 3. 查看创建的资源 +kubectl get configmap,secret -n k8s-training + +# 4. 查看 Secret 的值(解码) +kubectl get secret app-secret -o jsonpath='{.data.password}' -n k8s-training | base64 -d +echo + +# 5. 创建使用 ConfigMap 和 Secret 的 Pod +kubectl apply -f - < +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "myapp.fullname" . }} + labels: + {{- include "myapp.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "myapp.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "myapp.selectorLabels" . | nindent 8 }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 80 + env: + {{- range $key, $val := .Values.config }} + - name: {{ $key }} + value: {{ $val | quote }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 10 }} +``` + +**模板语法要点**: + +```go +{{ .Values.xxx }} # 引用 values.yaml 的值 +{{ .Chart.Name }} # 引用 Chart 元数据 +{{ include "func" . }} # 调用 _helpers.tpl 中定义的函数 +{{- ... | nindent 4 }} # 缩进4格 +{{- range $key, $val := . }} # 循环 +{{- if .Values.xxx }} # 条件判断 +``` + +### 5.6 Helm 常用命令 + +```bash +# === 仓库管理 === +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +helm search repo nginx + +# === 创建 Chart === +helm create myapp # 生成标准 Chart 结构 + +# === 安装/部署 === +# 使用默认值 +helm install myapp ./myapp -n k8s-training + +# 使用自定义 values +helm install myapp ./myapp -n k8s-training \ + -f values-prod.yaml + +# 覆盖单个值 +helm install myapp ./myapp -n k8s-training \ + --set replicaCount=3 \ + --set image.tag=1.26 + +# === 查看 === +helm list -n k8s-training # 查看已安装的 Release +helm status myapp -n k8s-training # 查看状态 +helm get values myapp -n k8s-training # 查看当前 values + +# === 升级 === +helm upgrade myapp ./myapp -n k8s-training \ + --set image.tag=1.26 + +# === 回滚 === +helm rollback myapp 1 -n k8s-training # 回滚到 revision 1 +helm history myapp -n k8s-training # 查看历史 + +# === 卸载 === +helm uninstall myapp -n k8s-training + +# === 调试 === +helm template myapp ./myapp # 本地渲染模板(不部署) +helm lint ./myapp # 检查 Chart 语法 +helm diff upgrade myapp ./myapp # 查看变更(需要 helm-diff 插件) +``` + +### 5.7 使用公共 Chart 部署 + +```bash +# 安装 Bitnami Nginx +helm repo add bitnami https://charts.bitnami.com/bitnami +helm install my-nginx bitnami/nginx -n k8s-training \ + --set replicaCount=2 \ + --set service.type=ClusterIP + +# 安装 MySQL +helm install my-db bitnami/mysql -n k8s-training \ + --set auth.rootPassword=MyRootPassword123 \ + --set primary.persistence.size=5Gi +``` + +### 5.8 多环境配置管理 + +```bash +# 目录结构 +myapp/ +├── values.yaml # 默认值 +├── values-dev.yaml # 开发环境 +├── values-staging.yaml # 预发布环境 +└── values-prod.yaml # 生产环境 + +# 部署到不同环境 +helm install myapp-dev ./myapp -n dev -f values-dev.yaml +helm install myapp-prod ./myapp -n prod -f values-prod.yaml +``` + +**values-prod.yaml 示例**: +```yaml +replicaCount: 3 + +image: + tag: "1.0.0" + +resources: + requests: + cpu: "500m" + memory: "512Mi" + limits: + cpu: "2000m" + memory: "2Gi" + +ingress: + enabled: true + host: app.example.com +``` + +--- + +## 📝 习题 + +### 选择题 + +**Q1.** Helm 中,一个 Chart 的运行实例称为什么? +- A) Chart +- B) Release +- C) Template +- D) Package + +**Q2.** `helm rollback myapp 1` 的作用是? +- A) 删除 myapp +- B) 回滚 myapp 到 revision 1 +- C) 更新 myapp 到版本1 +- D) 查看 myapp 的历史 + +**Q3.** Helm 模板中 `{{ .Values.replicaCount }}` 的作用是? +- A) 定义 replicaCount 的值 +- B) 引用 values.yaml 中 replicaCount 的值 +- C) 创建一个名为 replicaCount 的资源 +- D) 删除 replicaCount 配置 + +**Q4.** `helm template` 命令的作用是? +- A) 部署应用到 K8S +- B) 创建新的 Chart +- C) 本地渲染模板,查看最终 YAML(不部署) +- D) 从仓库下载 Chart + +**Q5.** 如何覆盖 values.yaml 中的单个值? +- A) `helm install --value key=value` +- B) `helm install --set key=value` +- C) `helm install --override key=value` +- D) `helm install --env key=value` + +### 简答题 + +**Q6.** Helm 相比直接 `kubectl apply` 有什么优势? + +**Q7.** 请描述 Helm 的 Chart、Release、Repository 三者的关系。 + +**Q8.** 如何实现多环境(dev/prod)的 Helm 配置管理? + +### 动手题 + +**Q9.** 请创建一个 Helm Chart,部署一个 Nginx 应用,要求: +- Chart 名称:webapp +- 支持自定义副本数、镜像标签、Service 类型 +- 包含 Deployment 和 Service 模板 +- 支持 ConfigMap 配置注入 + +--- + +## ✅ 参考答案 + +### A1. B) Release +Chart 是包模板,Release 是 Chart 的一个部署实例。同一个 Chart 可以安装多次,每次产生不同的 Release。 + +### A2. B) 回滚 myapp 到 revision 1 +rollback 会回滚到指定的历史版本(revision),之前的版本会被保留在 history 中。 + +### A3. B) 引用 values.yaml 中 replicaCount 的值 +`{{ .Values.xxx }}` 是 Go 模板语法,引用 values.yaml 中的配置值。 + +### A4. C) 本地渲染模板,查看最终 YAML(不部署) +`helm template` 非常有用,可以在不部署的情况下看到渲染后的 K8S YAML,用于调试模板。 + +### A5. B) `helm install --set key=value` +`--set` 用于覆盖 values.yaml 中的值。也可以用 `-f values-override.yaml` 覆盖多个值。 + +### A6. Helm 的优势 +| 特性 | kubectl apply | Helm | +|------|--------------|------| +| 部署多个资源 | 多条命令 | 一条命令 | +| 参数化 | 手动修改 YAML | values.yaml | +| 多环境 | 多份 YAML | 多个 values 文件 | +| 升级/回滚 | 手动管理 | 内置支持 | +| 版本管理 | 自己用 Git | 内置 history | +| 共享/复用 | 复制粘贴 | Chart 仓库 | + +### A7. Chart、Release、Repository 的关系 +- **Repository**:Chart 的远程仓库(集中存放) +- **Chart**:从 Repository 下载的"应用包"(包含模板和默认配置) +- **Release**:Chart 在集群上的一个运行实例(一次 `helm install` = 一个 Release) + +``` +Repository (仓库) → 包含多个 Chart (包) +Chart (包) → 可以创建多个 Release (实例) +Release (实例) → 运行在 K8S 集群中 +``` + +### A8. 多环境配置管理 +``` +# 方案1:多 values 文件(推荐) +values.yaml # 公共默认值 +values-dev.yaml # 开发环境覆盖 +values-prod.yaml # 生产环境覆盖 + +helm install -f values.yaml -f values-dev.yaml ./chart +helm install -f values.yaml -f values-prod.yaml ./chart + +# 方案2:--set 覆盖 +helm install --set replicaCount=1 ./chart # dev +helm install --set replicaCount=3 ./chart # prod + +# 方案3:--set-file 引用环境变量文件 +helm install --set-file config.env=env/dev.env ./chart +``` + +### A9. Helm Chart 实现参见实操练习部分。 + +--- + +## 🔧 实操练习 + +```bash +export KUBECONFIG=~/.kube/config +kubectl create namespace k8s-training + +# 1. 创建 Chart +helm create webapp +cd webapp + +# 2. 修改 values.yaml +cat > values.yaml <<'EOF' +replicaCount: 2 + +image: + repository: nginx + tag: "1.25" + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 80 + +resources: + requests: + cpu: "50m" + memory: "32Mi" + limits: + cpu: "200m" + memory: "128Mi" + +config: + APP_ENV: production + LOG_LEVEL: info +EOF + +# 3. 简化 templates/deployment.yaml(用 Helm 默认生成的即可) +# 4. 简化 templates/service.yaml(用 Helm 默认生成的即可) + +# 5. 本地渲染检查 +helm template mywebapp ./webapp + +# 6. 安装 +helm install mywebapp ./webapp -n k8s-training + +# 7. 查看状态 +helm status mywebapp -n k8s-training +helm list -n k8s-training +kubectl get all -n k8s-training + +# 8. 升级(改副本数) +helm upgrade mywebapp ./webapp -n k8s-training --set replicaCount=5 +kubectl get pods -n k8s-training + +# 9. 回滚 +helm rollback mywebapp 1 -n k8s-training + +# 10. 查看 history +helm history mywebapp -n k8s-training + +# ⚠️ 清理! +helm uninstall mywebapp -n k8s-training +cd .. +rm -rf webapp +kubectl delete namespace k8s-training +``` + +--- + +**上一章**:[第4章:ConfigMap、Secret 与存储](./04-config-storage.md) | **下一章**:[第6章:部署实战](./06-practice-project.md) | **进阶**:[第7章:调度与弹性伸缩](./07-scheduling-autoscaling.md) diff --git a/docs/posts/k8s-training/06-practice-project.md b/docs/posts/k8s-training/06-practice-project.md new file mode 100644 index 0000000..0d789b5 --- /dev/null +++ b/docs/posts/k8s-training/06-practice-project.md @@ -0,0 +1,32 @@ +--- +title: "第6章:综合部署实战" +date: "2026-05-15" +category: "k8s" +tags: + - "k8s" + - "AI生成" +--- + +# 第6章:综合部署实战 + +> 实战项目使用 Helm Chart 打包一个完整的应用部署系统,覆盖前面 5 章的所有核心知识点。 + +→ **请前往 [实战项目目录](./practice/README.md) 开始练习** + +## 本实战覆盖的知识点 + +| 知识点 | 说明 | +|--------|------| +| Namespace | 多租户资源隔离 | +| Deployment | Pod 管理、副本数、资源限制 | +| Liveness/Readiness Probe | 容器健康检测 | +| RollingUpdate | 零停机更新策略 | +| Service (ClusterIP) | 内部负载均衡 | +| ConfigMap / Secret | 配置与敏感信息管理 | +| Ingress / PVC | HTTP 路由与持久化存储(可选) | +| Helm Chart | 包管理、模板化配置 | +| DNS 服务发现 | CoreDNS 自动解析 | + +--- + +**上一章**:[第5章:Helm 包管理](./05-helm.md) | **下一章**:[第7章:调度与弹性伸缩](./07-scheduling-autoscaling.md) | **实战**:[practice/README.md](./practice/README.md) diff --git a/docs/posts/k8s-training/07-scheduling-autoscaling.md b/docs/posts/k8s-training/07-scheduling-autoscaling.md new file mode 100644 index 0000000..3c76aed --- /dev/null +++ b/docs/posts/k8s-training/07-scheduling-autoscaling.md @@ -0,0 +1,946 @@ +--- +title: "第7章:调度与弹性伸缩" +date: "2026-05-15" +category: "k8s" +tags: + - "k8s" + - "AI生成" +--- + +# 第7章:调度与弹性伸缩 + +## 🎯 学习目标 +- 理解 Scheduler 的调度决策机制 +- 掌握 nodeSelector、nodeAffinity、podAffinity、podAntiAffinity 的使用场景和区别 +- 掌握 Taint/Toleration(污点与容忍度) +- 掌握 HPA(水平自动扩缩)和 VPA(垂直自动扩缩) +- 能在面试中清晰回答调度和弹性伸缩相关问题 + +--- + +## 📖 学习要点 + +### 7.1 调度概述 + +回顾第1章,Scheduler 的职责是**决定 Pod 运行在哪个 Node 上**。调度是一个两步过程: + +```mermaid +flowchart TB + subgraph Step1["第一步:过滤(Filtering / Predicate)"] + direction TB + F1["排除不满足条件的 Node:"] + F2["❌ 资源不够(CPU/内存 requests 超出 Node 可分配)"] + F3["❌ nodeSelector 不匹配"] + F4["❌ nodeAffinity 不满足"] + F5["❌ Taint(污点)未被 Toleration 容忍"] + F6["❌ PVC 存储不可用"] + F7["❌ Pod 反亲和性冲突"] + F8["→ 剩下的叫\"可调度节点\"(Feasible Nodes)"] + end + + subgraph Step2["第二步:打分(Scoring / Priority)"] + direction TB + S1["给每个可调度节点打分,选最高的:"] + S2["✅ 资源均衡(选剩余资源最均衡的)"] + S3["✅ podAffinity(靠近或远离某些 Pod)"] + S4["✅ 数据本地性(PV 和 Pod 在同一 Node)"] + S5["→ 选得分最高的 Node → 绑定(Binding)"] + end + + Step1 --> Step2 +``` + +> **图 7-1:调度流程(过滤 + 打分)** + +**面试回答模板**: +> "K8S 的调度分两步:先过滤排除不满足条件的 Node,再打分选最优的。过滤条件包括资源请求、节点选择器、亲和性、污点容忍等。打分考虑资源均衡、亲和性权重、数据本地性等因素。" + +--- + +### 7.2 节点选择:nodeSelector → nodeAffinity + +#### 7.2.1 nodeSelector(最简单) + +通过标签精确匹配 Node: + +```yaml +# 先给 Node 打标签 +# kubectl label nodes node-1 disktype=ssd + +apiVersion: v1 +kind: Pod +metadata: + name: nginx +spec: + nodeSelector: + disktype: ssd # 只有标签包含 disktype=ssd 的 Node 才能调度 + containers: + - name: nginx + image: nginx:1.25 +``` + +**局限**:只能精确匹配,不能表达"或"、"非"等逻辑。 + +#### 7.2.2 nodeAffinity(节点亲和性)⭐ 面试高频 + +比 nodeSelector 更强大,支持丰富的表达式: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-deployment +spec: + template: + spec: + affinity: + nodeAffinity: + # 硬性要求:必须满足,否则 Pod 无法调度(Pending) + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In # 值在列表中 + values: ["amd64"] + - key: disktype + operator: NotIn # 值不在列表中 + values: ["hdd"] + + # 软性偏好:尽量满足,不满足也能调度到其他 Node + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 80 # 权重 1-100 + preference: + matchExpressions: + - key: node-type + operator: In + values: ["high-memory"] + - weight: 20 + preference: + matchExpressions: + - key: zone + operator: In + values: ["cn-beijing-a"] + containers: + - name: app + image: myapp:1.0 +``` + +#### operator 支持的运算符 + +| operator | 含义 | 示例 | +|----------|------|------| +| `In` | 值在列表中 | `values: ["ssd", "nvme"]` | +| `NotIn` | 值不在列表中 | `values: ["hdd"]` | +| `Exists` | 标签存在(不关心值) | 不需要 values | +| `DoesNotExist` | 标签不存在 | 不需要 values | +| `Gt` | 值大于(数字) | `values: ["8"]` | +| `Lt` | 值小于(数字) | `values: ["4"]` | + +#### 硬亲和 vs 软亲和(面试必问) + +| 类型 | 关键词 | 不满足时 | +|------|--------|---------| +| **硬亲和**(必须) | `requiredDuringScheduling...` | Pod 永远 Pending | +| **软亲和**(偏好) | `preferredDuringScheduling...` | 仍然调度,只是不优先 | + +> `IgnoredDuringExecution` 的含义:Pod 已经运行后,即使 Node 标签变了,**不会**驱逐 Pod。这是默认行为。 + +**面试回答模板**: +> "nodeAffinity 分为 required(硬)和 preferred(软)。required 不满足 Pod 会 Pending,preferred 只是偏好。operator 支持 In、NotIn、Exists、DoesNotExist、Gt、Lt 六种。后缀 IgnoredDuringExecution 表示 Pod 运行后 Node 标签变了不会驱逐。" + +--- + +### 7.3 Pod 亲和与反亲和(⭐⭐⭐ 面试高频) + +#### 7.3.1 podAffinity(Pod 亲和性) + +**控制 Pod 和 Pod 之间的调度关系**——"让我的 Pod 靠近某些 Pod"。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend +spec: + template: + spec: + affinity: + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app: cache # 找标签为 app=cache 的 Pod + topologyKey: kubernetes.io/hostname # 在同一 Node 上 + # topologyKey: topology.kubernetes.io/zone # 在同一可用区 + containers: + - name: frontend + image: myapp:1.0 +``` + +**场景**:前端应用需要和缓存部署在同一 Node,减少网络延迟。 + +#### 7.3.2 podAntiAffinity(Pod 反亲和性) + +**让 Pod 远离某些 Pod**——"不要把我的 Pod 和某些 Pod 放在一起"。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api-server +spec: + replicas: 3 + template: + spec: + affinity: + podAntiAffinity: + # 硬反亲和:每个 Pod 必须在不同 Node + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app: api-server + topologyKey: kubernetes.io/hostname + + # 软反亲和:尽量分布在不同可用区(权重分摊) + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: api-server + topologyKey: topology.kubernetes.io/zone + containers: + - name: api + image: myapp:1.0 +``` + +#### topologyKey 是什么? + +`topologyKey` 定义了"靠近"的范围(拓扑域): + +| topologyKey | 含义 | 效果 | +|-------------|------|------| +| `kubernetes.io/hostname` | 同一个 Node | Pod 必须在同一/不同 Node | +| `topology.kubernetes.io/zone` | 同一个可用区 | Pod 在同一/不同 AZ | +| `topology.kubernetes.io/region` | 同一个地域 | Pod 在同一/不同地域 | + +#### nodeAffinity vs podAffinity 对比(面试必背) + +| 对比项 | nodeAffinity | podAffinity | +|--------|-------------|-------------| +| 关注点 | Pod → Node 的关系 | Pod → Pod 的关系 | +| 基于什么选择 | Node 的标签 | 已有 Pod 的标签 | +| 典型场景 | Pod 必须在 SSD Node / GPU Node | 前端靠近缓存 / API 分散部署 | +| 是否需要 topologyKey | ❌ 不需要 | ✅ 必须指定 | + +**面试回答模板**: +> "podAntiAffinity 常用来实现高可用——同一个 Deployment 的多个副本分散在不同 Node 或不同可用区,避免单节点故障导致服务不可用。硬反亲和保证分布,软反亲和只是偏好。topologyKey 定义拓扑域范围,hostname 是节点级别,zone 是可用区级别。" + +--- + +### 7.4 污点与容忍度(Taint & Toleration)⭐ 面试高频 + +#### 7.4.1 概念 + +``` +Taint(污点):打在 Node 上的"排斥标记",排斥不想被调度的 Pod +Toleration(容忍度):Pod 声明"我能容忍某些污点" +``` + +**类比**: +- Node 污点 = 房间门上的"闲人免进"牌子 +- Pod 容忍度 = "我有通行证,可以进" + +```bash +# 给 Node 打污点 +kubectl taint nodes node-1 dedicated=gpu:NoSchedule +# kubectl taint nodes key=value:effect +``` + +#### 7.4.2 Taint 效果(effect) + +| effect | 含义 | Pod 没有 Toleration 时 | +|--------|------|----------------------| +| **NoSchedule** | 新 Pod 不会被调度到此 Node | ✅ 不调度(已有的不受影响) | +| **PreferNoSchedule** | 尽量不调度到此 Node | ⚠️ 尽量不调度,实在没地方也能调度 | +| **NoExecute** | 新不调度 + 已有的 Pod 如果没 Toleration 会被驱逐 | 🚫 不调度 + 驱逐已有 Pod | + +#### 7.4.3 YAML 示例 + +```yaml +# Pod 声明容忍度 +apiVersion: v1 +kind: Pod +metadata: + name: gpu-task +spec: + tolerations: + # 精确匹配:容忍 dedicated=gpu:NoSchedule + - key: "dedicated" + operator: "Equal" + value: "gpu" + effect: "NoSchedule" + + # 存在即容忍:容忍所有 effect 为 NoSchedule 的 dedicated 污点 + - key: "dedicated" + operator: "Exists" + effect: "NoSchedule" + + # 容忍所有污点(危险!慎用) + - operator: "Exists" + containers: + - name: cuda-app + image: cuda-app:1.0 +``` + +#### 7.4.4 常见使用场景 + +**场景1:Master 节点隔离** +```bash +# K8S 默认给 Master 节点打污点 +kubectl taint nodes master-node node-role.kubernetes.io/control-plane:NoSchedule + +# 只有声明了 Toleration 的系统 Pod(如 CoreDNS)才能运行在 Master 上 +``` + +**场景2:GPU 专用节点** +```bash +# 给 GPU Node 打污点 +kubectl taint nodes gpu-node-1 hardware=gpu:NoSchedule + +# 只有 GPU 任务 Pod 声明容忍度才能调度上去 +``` + +**场景3:节点故障自动驱逐(NoExecute)** +```bash +# Node 不健康时自动打 NoExecute 污点 +# 没有容忍度的 Pod 会被驱逐到其他 Node +# 配合 tolerationSeconds 可以延迟驱逐: + tolerations: + - key: "node.kubernetes.io/not-ready" + operator: "Exists" + effect: "NoExecute" + tolerationSeconds: 300 # 等待 300 秒再驱逐(给 Node 恢复时间) +``` + +#### 7.4.5 Taint vs nodeAffinity(面试对比) + +| 对比项 | Taint(污点) | nodeAffinity(节点亲和性) | +|--------|-------------|--------------------------| +| 方向 | Node 排斥 Pod | Pod 选择 Node | +| 谁定义规则 | Node 上配置 | Pod 上配置 | +| 作用 | "我不欢迎你" | "我想去你那里" | +| 类比 | 房间挂牌子"闲人免进" | 人拿钥匙"我能进这个房间" | +| 典型场景 | Master 隔离、GPU 专用 | Pod 必须在 SSD Node | + +> 两者可以**组合使用**:Node 打 Taint 限制谁能来,Pod 用 nodeAffinity 表达想去哪。 + +**面试回答模板**: +> "Taint 是 Node 上的排斥标记,Toleration 是 Pod 上的容忍声明。三个 effect:NoSchedule 不调度新 Pod,PreferNoSchedule 尽量不调度,NoExecute 还会驱逐已有 Pod。常见用途是 Master 节点隔离和专用节点。和 nodeAffinity 的区别是方向相反——Taint 是 Node 排斥 Pod,Affinity 是 Pod 选择 Node。" + +--- + +### 7.5 水平自动扩缩 HPA(⭐⭐⭐ 面试必考) + +#### 7.5.1 什么是 HPA? + +HPA(Horizontal Pod Autoscaler)根据指标**自动调整 Pod 副本数**。 + +``` +没有 HPA: + 手动 kubectl scale → 流量来了来不及 → 手动扩容有延迟 + +有 HPA: + CPU 超过 80% → 自动扩到 5 个副本 + CPU 降到 30% → 自动缩回 2 个副本 + 完全自动,无需人工干预! +``` + +```mermaid +flowchart TB + Metrics["Metrics Server
(采集 Pod 的 CPU/内存指标)"] --> HPA["HPA Controller
(每 15-30 秒检查一次)"] + HPA -->|"期望副本数 = ceil(当前指标值 / 目标指标值)"| Calc["计算"] + Calc -->|"例:目标 CPU = 50%,
当前 4 Pod 共 240%
期望副本 = ceil(240/50) = 5
不够 → 扩容到 5 个"| Deploy["调整 Deployment 的 replicas"] +``` + +> **图 7-2:HPA 工作流程** + +#### 7.5.2 前提条件 + +```bash +# 1. 必须安装 Metrics Server(采集资源指标) +# 大多数云集群默认已安装,检查: +kubectl top pods # 能显示数据说明已安装 + +# 如果未安装(如 Sealos): +kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml + +# 2. Pod 必须配置 resources.requests(HPA 基于 requests 计算百分比) +# 没有 requests 的 Pod 其指标会被忽略 +``` + +#### 7.5.3 HPA YAML 示例 + +```yaml +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: app-hpa + namespace: k8s-training +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: app-deployment # ⭐ 控制哪个 Deployment + minReplicas: 2 # 最少 2 个副本 + maxReplicas: 10 # 最多 10 个副本 + metrics: + # 指标1:CPU 利用率 + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 # 目标:平均 CPU 利用率 70% + + # 指标2:内存利用率 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 # 目标:平均内存利用率 80% + + # 指标3:自定义指标(需要 Prometheus Adapter) + # - type: Pods + # pods: + # metric: + # name: http_requests_per_second + # target: + # type: AverageValue + # averageValue: "1000" + + behavior: # 扩缩行为控制(可选,但推荐配置) + scaleDown: + stabilizationWindowSeconds: 300 # 缩容稳定窗口:5分钟内不重复缩容 + policies: + - type: Percent + value: 10 # 每次最多缩容 10% + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 0 # 扩容不等待(立即响应) + policies: + - type: Percent + value: 100 # 每次最多扩容 100%(翻倍) + periodSeconds: 15 + - type: Pods + value: 4 # 或每次最多增加 4 个 + periodSeconds: 15 + selectPolicy: Max # 多个策略取最大值 +``` + +#### 7.5.4 快速创建 HPA + +```bash +# 命令行快速创建(基于 CPU) +kubectl autoscale deployment app-deployment \ + --cpu-percent=70 \ + --min=2 --max=10 \ + -n k8s-training + +# 查看 HPA 状态 +kubectl get hpa -n k8s-training +# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE +# app-hpa Deployment/app-deployment 45%/70% 2 10 3 5m + +# 查看 HPA 事件(了解扩缩原因) +kubectl describe hpa app-hpa -n k8s-training +``` + +#### 7.5.5 HPA 扩缩公式 + +``` +期望副本数 = ceil(当前所有 Pod 的指标总量 / 目标指标值) + +示例: + Deployment 有 3 个 Pod,CPU requests 都是 100m + 目标 CPU 利用率 = 70% + 当前 3 个 Pod 的 CPU 使用率:80%、60%、50% + + 当前平均利用率 = (80 + 60 + 50) / 3 = 63.3%(低于 70%,不扩容) + + 如果负载增加,3 个 Pod 的 CPU 使用率变为:90%、85%、80%: + 当前平均利用率 = (90 + 85 + 80) / 3 = 85%(高于 70%) + 期望副本数 = ceil(3 × 85 / 70) = ceil(3.64) = 4 → 扩容到 4 个 +``` + +#### 7.5.6 HPA 注意事项(面试加分) + +``` +1. Pod 必须配置 resources.requests + → HPA 基于 requests 计算百分比,没配 requests 的 Pod 不参与计算 + +2. 冷启动问题 + → 扩容需要等 Pod 启动并注册到 Service(readinessProbe 通过) + → 配置 startupProbe 和合理的 readinessProbe 可以加速 + +3. 避免抖动 + → 配置 behavior.scaleDown.stabilizationWindowSeconds(默认 5 分钟) + → 避免频繁扩缩 + +4. 多指标 HPA + → 取所有指标计算出的最大副本数(最坏的指标决定扩多少) + +5. HPA 与 PDB(PodDisruptionBudget)配合 + → PDB 限制最大不可用 Pod 数,HPA 缩容时不会违反 PDB +``` + +--- + +### 7.6 VPA 与 Cluster Autoscaler(了解即可) + +#### 7.6.1 VPA(Vertical Pod Autoscaler) + +HPA 是水平扩缩(加/减 Pod 数量),VPA 是**垂直扩缩**(调整单个 Pod 的 CPU/内存 requests)。 + +``` +HPA: 2 个 Pod(1核2G)→ 4 个 Pod(1核2G) ← 加副本 +VPA: 2 个 Pod(1核2G)→ 2 个 Pod(2核4G) ← 加资源 +``` + +| 对比 | HPA | VPA | +|------|-----|-----| +| 扩缩方式 | 增减副本数 | 调整 requests/limits | +| 适用场景 | 无状态应用 | 资源需求波动大 | +| 注意 | 需要 Metrics Server | 需要 VPA Controller | +| **重要** | **可与 VPA 共存(某些模式)** | **默认会重启 Pod 生效** | + +> 面试中了解 VPA 的概念即可,知道它和 HPA 的区别就行。大多数场景用 HPA。 + +#### 7.6.2 Cluster Autoscaler(CA) + +HPA/VPA 调整 Pod 级别,CA 调整**节点级别**——自动增减 Node 数量。 + +```mermaid +flowchart TB + subgraph L1["Layer 1: HPA(Pod 级别)"] + L1A["流量增大,Pod 不够 → 增加副本"] + end + + subgraph L2["Layer 2: VPA(Pod 级别)"] + L2A["Pod 资源不够 → 增加单 Pod 的资源"] + end + + subgraph L3["Layer 3: Cluster Autoscaler(Node 级别)"] + L3A["所有 Node 的资源都被 Pod 占满了"] + L3A --> L3B["Pod 处于 Pending 状态"] + L3B --> L3C["CA 自动向云厂商申请新 Node 加入集群"] + L3C --> L3D["Pod 调度到新 Node"] + L3D --> L3E["反过来,Node 利用率低 → CA 缩减 Node"] + end +``` + +> **图 7-3:三级自动伸缩体系** + +> CA 是云厂商级别的能力(AWS/阿里云/GCP),在 Sealos 这种托管平台上不适用。面试中知道有这个东西就行。 + +--- + +### 7.7 调度场景综合案例 + +#### 场景:部署高可用 API 服务 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api-server + namespace: production +spec: + replicas: 6 + selector: + matchLabels: + app: api-server + template: + metadata: + labels: + app: api-server + spec: + # 1. 容忍 Master 节点的污点(如果允许调度到 Master) + # tolerations: + # - key: "node-role.kubernetes.io/control-plane" + # operator: "Exists" + # effect: "NoSchedule" + + affinity: + # 2. 硬反亲和:每个副本必须在不同 Node(高可用基础) + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app: api-server + topologyKey: kubernetes.io/hostname + + # 3. 软亲和:尽量分布在不同可用区(跨 AZ 容灾) + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: api-server + topologyKey: topology.kubernetes.io/zone + + # 4. 软亲和:靠近缓存 Pod(减少延迟) + podAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 50 + podAffinityTerm: + labelSelector: + matchLabels: + app: redis-cache + topologyKey: kubernetes.io/hostname + + # 5. 节点亲和:必须部署在 x86 架构上 + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: ["amd64"] + + containers: + - name: api + image: myapp:2.0 + resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + cpu: "1000m" + memory: "1Gi" + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 3 +--- +# 6. HPA:根据 CPU 自动扩缩 +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: api-server-hpa + namespace: production +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: api-server + minReplicas: 6 + maxReplicas: 20 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 +``` + +--- + +## 📝 习题 + +### 选择题 + +**Q1.** nodeAffinity 中 `requiredDuringSchedulingIgnoredDuringExecution` 的含义是? +- A) 调度时尽量满足,运行后标签变了会驱逐 Pod +- B) 调度时必须满足,运行后标签变了不驱逐 Pod +- C) 调度和运行时都必须满足 +- D) 仅在运行时检查 + +**Q2.** 以下哪个不是 Taint 的 effect 类型? +- A) NoSchedule +- B) PreferNoSchedule +- C) NoExecute +- D) NoTolerate + +**Q3.** podAntiAffinity 的 `topologyKey: kubernetes.io/hostname` 表示什么? +- A) 所有 Pod 必须在同一 Node +- B) 同一 Deployment 的 Pod 不能在同一 Node +- C) Pod 必须调度到特定主机名的 Node +- D) Node 的 hostname 必须匹配 + +**Q4.** HPA 根据什么指标自动扩缩? +- A) Node 的 CPU 利用率 +- B) Deployment 所有 Pod 的平均 CPU 利用率 +- C) 集群总 CPU 使用量 +- D) Service 的 QPS + +**Q5.** 以下哪种情况下 Pod 会被驱逐(Evicted)? +- A) Node 有 NoSchedule 污点,Pod 没有对应 Toleration +- B) Node 有 NoExecute 污点,Pod 没有对应 Toleration +- C) Node 有 PreferNoSchedule 污点,Pod 没有对应 Toleration +- D) Pod 没有配置 nodeAffinity + +### 简答题 + +**Q6.** 请解释 nodeAffinity 和 podAffinity 的区别,分别适用什么场景? + +**Q7.** Taint(污点)和 nodeAffinity(节点亲和性)有什么区别? + +**Q8.** HPA 的工作原理是什么?扩容公式怎么计算? + +**Q9.** 为什么 HPA 要求 Pod 必须配置 resources.requests? + +### 动手题 + +**Q10.** 请编写一个 Deployment + HPA 的 YAML,要求: +- Deployment: 3 副本,Nginx 镜像,CPU requests=100m, limits=500m +- HPA: 目标 CPU 利用率 70%,最小 2 副本,最大 10 副本 +- podAntiAffinity: 硬性要求,副本不能在同一 Node + +--- + +## ✅ 参考答案 + +### A1. B) 调度时必须满足,运行后标签变了不驱逐 Pod +- `required` = 必须满足 +- `DuringScheduling` = 调度时检查 +- `IgnoredDuringExecution` = 运行后不检查(不驱逐) + +### A2. D) NoTolerate +三种 effect:NoSchedule、PreferNoSchedule(也称 SoftNoSchedule)、NoExecute。没有 NoTolerate。 + +### A3. B) 同一 Deployment 的 Pod 不能在同一 Node +`podAntiAffinity` + `topologyKey: hostname` = 同标签的 Pod 不能在同一 hostname 的 Node 上。 + +### A4. B) Deployment 所有 Pod 的平均 CPU 利用率 +HPA 监控 scaleTargetRef 指定的 Deployment 下所有 Pod 的平均指标值。 + +### A5. B) Node 有 NoExecute 污点,Pod 没有对应 Toleration +只有 NoExecute 会驱逐已有 Pod。NoSchedule 只阻止新 Pod 调度。 + +### A6. nodeAffinity vs podAffinity +``` +nodeAffinity(节点亲和性): + - Pod 对 Node 的偏好 + - 基于 Node 的标签 + - 例:Pod 必须调度到 SSD Node + - 不需要 topologyKey + +podAffinity(Pod 亲和性): + - Pod 对其他 Pod 的偏好 + - 基于已有 Pod 的标签 + - 例:前端 Pod 靠近缓存 Pod + - 必须指定 topologyKey +``` + +### A7. Taint vs nodeAffinity +``` +Taint(污点):Node 主动排斥 Pod + - 配置在 Node 上 + - "我不欢迎你" + - 没有容忍度的 Pod 不能调度 + +nodeAffinity(节点亲和性):Pod 主动选择 Node + - 配置在 Pod 上 + - "我想去你那里" + - 没有匹配的 Node Pod 不会调度 + +类比: + Taint = 房间门上贴"闲人免进" + nodeAffinity = 人说"我想进标有SSD的房间" +``` + +### A8. HPA 工作原理 +``` +1. Metrics Server 采集每个 Pod 的 CPU/内存使用数据 +2. HPA Controller 每 15-30 秒检查一次 +3. 计算公式:期望副本数 = ceil(当前所有 Pod 指标总量 / 目标指标值) +4. 如果期望 > maxReplicas → 扩到 maxReplicas +5. 如果期望 < minReplicas → 保持 minReplicas +6. 如果期望 > 当前副本数 → 扩容 +7. 如果期望 < 当前副本数 → 等待稳定窗口后缩容 + +扩缩行为可以通过 behavior 字段精细控制。 +``` + +### A9. HPA 需要 requests 的原因 +``` +HPA 基于指标"百分比"计算: + CPU 利用率 = 实际使用量 / requests + +如果没配 requests: + - 无法计算百分比(分母为零) + - HPA 会忽略该 Pod 的指标 + - 导致扩缩不准确 + +例:requests.cpu = 100m,实际使用 70m → 利用率 70% +``` + +### A10. Deployment + HPA YAML + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-hpa-demo + namespace: k8s-training +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app: nginx + topologyKey: kubernetes.io/hostname + containers: + - name: nginx + image: nginx:1.25 + ports: + - containerPort: 80 + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "256Mi" +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: nginx-hpa + namespace: k8s-training +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-hpa-demo + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 +``` + +--- + +## 🔧 实操练习 + +```bash +export KUBECONFIG=~/.kube/config +kubectl create namespace k8s-training + +# 1. 查看 Node 标签(了解 Node 信息) +kubectl get nodes --show-labels + +# 2. 给 Node 打标签(模拟不同节点类型) +kubectl label nodes disktype=ssd --overwrite + +# 3. 部署带 nodeSelector 的 Pod +kubectl apply -f - < | grep -A5 Taints + +# 6. 测试 podAntiAffinity(需要多 Node 环境) +kubectl apply -f - < disktype- +``` + +--- + +**上一章**:[第6章:综合部署实战](./06-practice-project.md) | **下一章**:[第8章:安全与权限](./08-security.md) diff --git a/docs/posts/k8s-training/08-security.md b/docs/posts/k8s-training/08-security.md new file mode 100644 index 0000000..25e9bbc --- /dev/null +++ b/docs/posts/k8s-training/08-security.md @@ -0,0 +1,1083 @@ +--- +title: "第8章:安全与权限" +date: "2026-05-15" +category: "k8s" +tags: + - "k8s" + - "AI生成" +--- + +# 第8章:安全与权限 + +## 🎯 学习目标 +- 理解 K8S 安全架构的四大层面 +- 掌握 RBAC 的核心概念和配置(⭐ 面试高频) +- 理解 NetworkPolicy 网络隔离策略 +- 了解 SecurityContext 容器安全上下文 +- 能在面试中清晰回答安全相关问题 + +--- + +## 📖 学习要点 + +### 8.1 K8S 安全架构全景 + +K8S 安全分为四个层面: + +```mermaid +flowchart TB + subgraph Security["K8S 安全四层模型"] + A["① 认证(Authentication)
\"你是谁?\"
→ Token、证书、OIDC、ServiceAccount"] + B["② 授权(Authorization / RBAC)
\"你能做什么?\"
→ Role、ClusterRole、RoleBinding"] + C["③ 准入控制(Admission Control)
\"你的请求安全吗?\"
→ LimitRanger、ResourceQuota、PodSecurity"] + D["④ 网络策略(Network Policy)
\"谁能和谁通信?\"
→ NetworkPolicy、Service Mesh"] + end + + Request["用户请求"] --> Auth["①认证"] --> Authz["②授权"] --> Admission["③准入控制"] --> etcd["写入 etcd"] +``` + +> **图 8-1:K8S 安全四层模型** + +--- + +### 8.2 RBAC(⭐⭐⭐ 面试必考) + +RBAC = Role-Based Access Control(基于角色的访问控制),是 K8S 的授权机制。 + +#### 8.2.1 核心概念 + +``` +四个核心对象: + +Role → 定义"能做什么"(Namespace 级别) +ClusterRole → 定义"能做什么"(集群级别) + +RoleBinding → 把 Role 绑定到"谁" +ClusterRoleBinding → 把 ClusterRole 绑定到"谁" + +绑定对象("谁"): + User → 外部用户(通过证书/OIDC 认证) + Group → 用户组 + ServiceAccount → Pod 的身份(最常用!) +``` + +```mermaid +flowchart LR + Role["Role
(权限规则)"] -->|"绑定到"| RB["RoleBinding
(角色绑定)"] + RB -->|"授予"| SA["ServiceAccount(Pod 身份)
或 User / Group(用户/用户组)"] +``` + +> **图 8-2:RBAC 核心概念** + +#### 8.2.2 Role 和 ClusterRole + +```yaml +# Role:Namespace 级别的权限 +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: developer + namespace: production # ⭐ 只在 production namespace 生效 +rules: +- apiGroups: [""] # "" = core API group(Pod、Service 等) + resources: ["pods", "services", "configmaps"] + verbs: ["get", "list", "watch", "create", "update"] +- apiGroups: ["apps"] # apps API group(Deployment、StatefulSet 等) + resources: ["deployments"] + verbs: ["get", "list", "watch", "create", "update", "patch"] +- apiGroups: [""] + resources: ["secrets"] # Secret 单独限制 + verbs: ["get", "list"] # 只允许查看,不允许创建/删除 +``` + +```yaml +# ClusterRole:集群级别的权限 +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-reader # ⭐ 全集群生效 +rules: +- apiGroups: [""] + resources: ["nodes", "namespaces", "pods"] + verbs: ["get", "list", "watch"] # 只读权限 +``` + +#### 8.2.3 RoleBinding 和 ClusterRoleBinding + +```yaml +# RoleBinding:把 Role 绑定到 ServiceAccount +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: developer-binding + namespace: production +subjects: +- kind: ServiceAccount # 绑定给 ServiceAccount + name: app-sa # ServiceAccount 名称 + namespace: production +# - kind: User # 也可以绑定给外部用户 +# name: zhangsan +# - kind: Group # 或用户组 +# name: developers +roleRef: + kind: Role # 引用的 Role + name: developer # Role 名称 + apiGroup: rbac.authorization.k8s.io +``` + +```yaml +# ClusterRoleBinding:把 ClusterRole 绑定到所有 Namespace 的 SA +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: read-all-binding +subjects: +- kind: ServiceAccount + name: monitor-sa + namespace: monitoring +roleRef: + kind: ClusterRole + name: cluster-reader + apiGroup: rbac.authorization.k8s.io +``` + +#### 8.2.4 ServiceAccount(Pod 的身份) + +```yaml +# 每个 Namespace 默认有一个 default ServiceAccount +# 建议为每个应用创建独立的 SA + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: app-sa + namespace: production +--- +# 在 Pod 中使用 ServiceAccount +apiVersion: v1 +kind: Pod +metadata: + name: my-app + namespace: production +spec: + serviceAccountName: app-sa # ⭐ 指定 SA(不指定则用 default) + containers: + - name: app + image: myapp:1.0 +``` + +#### 8.2.5 内置 ClusterRole(⭐ 面试常考) + +K8S 自带了几个常用的 ClusterRole,不需要自己创建: + +| ClusterRole | 权限范围 | 典型用途 | +|-------------|----------|----------| +| `cluster-admin` | **全集群所有权限** | 集群管理员(慎用!) | +| `admin` | Namespace 内几乎所有权限(含 RBAC) | Namespace 负责人 | +| `edit` | Namespace 内读写权限(不含 RBAC) | 普通开发者 | +| `view` | Namespace 内只读权限 | 只读观察者 | + +```bash +# 给开发者 devuser 授予 production namespace 的 edit 权限 +kubectl create rolebinding devuser-edit \ + --clusterrole=edit \ + --user=devuser \ + --namespace=production + +# 给监控 SA 授予全集群只读(ClusterRoleBinding) +kubectl create clusterrolebinding monitor-view \ + --clusterrole=view \ + --serviceaccount=monitoring:prometheus-sa +``` + +> ⚠️ 重要区别: +> - `kubectl create rolebinding --clusterrole=edit -n production` → ClusterRole + **RoleBinding** = 只在 production 生效 +> - `kubectl create clusterrolebinding --clusterrole=edit` → ClusterRole + **ClusterRoleBinding** = 全集群生效 + +#### 8.2.6 RoleBinding 引用 ClusterRole(⭐ 常见混淆点) + +这是一个非常实用但容易混淆的用法:**ClusterRole 可以被 RoleBinding 引用**,此时权限仅在 RoleBinding 所在的 Namespace 生效。 + +``` +场景:你希望在每个 Namespace 里都有相同的权限规则(如"只读 Pod"), + 但不想在每个 Namespace 里都创建一模一样的 Role。 + +解决方案: + 1. 创建一个 ClusterRole(定义规则) + 2. 在每个 Namespace 里用 RoleBinding 引用这个 ClusterRole +``` + +```yaml +# ClusterRole 定义规则(集群级别,共享使用) +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pod-reader +rules: +- apiGroups: [""] + resources: ["pods", "pods/log"] + verbs: ["get", "list", "watch"] +--- +# RoleBinding 在 team-a namespace 引用 ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: team-a-pod-reader + namespace: team-a # ⭐ 只在 team-a 生效 +subjects: +- kind: ServiceAccount + name: app-sa + namespace: team-a +roleRef: + kind: ClusterRole # ⭐ 引用的是 ClusterRole(而不是 Role) + name: pod-reader + apiGroup: rbac.authorization.k8s.io +``` + +```mermaid +flowchart LR + subgraph Table["RBAC 组合对比表"] + direction TB + H["组合方式"]:::header --> A["roleRef 类型"]:::header --> B["binding 类型"]:::header --> C["作用范围"]:::header + R1["常规用法"] --> R1A["Role"] --> R1B["RoleBinding"] --> R1C["同 Namespace"] + R2["跨 NS 复用"] --> R2A["ClusterRole"] --> R2B["RoleBinding"] --> R2C["⭐ 仅 RoleBinding 所在 NS"] + R3["全集群授权"] --> R3A["ClusterRole"] --> R3B["ClusterRoleBinding"] --> R3C["全集群"] + end + + classDef header fill:#f96 +``` + +> **图 8-3:RBAC 组合对比** + +#### 8.2.7 聚合 ClusterRole(Aggregated ClusterRole) + +通过标签聚合多个 ClusterRole,实现权限的动态扩展: + +```yaml +# 聚合 ClusterRole:自动合并所有带 rbac.example.com/aggregate-to-monitoring=true 标签的 ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: monitoring +aggregationRule: + clusterRoleSelectors: + - matchLabels: + rbac.example.com/aggregate-to-monitoring: "true" +rules: [] # 聚合 ClusterRole 的 rules 由 controller 自动填充,这里不需要手写 + +--- +# 被聚合的 ClusterRole(打上标签即可自动加入) +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: monitoring-endpoints + labels: + rbac.example.com/aggregate-to-monitoring: "true" # ⭐ 关键标签 +rules: +- apiGroups: [""] + resources: ["endpoints", "pods", "services"] + verbs: ["get", "list", "watch"] +``` + +> 💡 内置 ClusterRole(`admin`、`edit`、`view`)都使用了聚合机制。安装 CRD 时,运营商可以通过打标签的方式自动扩展这些内置权限,而无需修改原有 ClusterRole。 + +#### 8.2.8 API Group 详解(⭐ 写 Role 必须掌握) + +RBAC rules 中的 `apiGroups` 决定了 `resources` 属于哪个 API Group: + +``` +常用 API Group 对应关系: + +apiGroups: [""] # core group(空字符串) + resources: pods, services, configmaps, secrets, nodes, + namespaces, endpoints, persistentvolumeclaims, events + +apiGroups: ["apps"] + resources: deployments, replicasets, statefulsets, daemonsets + +apiGroups: ["batch"] + resources: jobs, cronjobs + +apiGroups: ["autoscaling"] + resources: horizontalpodautoscalers + +apiGroups: ["networking.k8s.io"] + resources: ingresses, networkpolicies + +apiGroups: ["rbac.authorization.k8s.io"] + resources: roles, rolebindings, clusterroles, clusterrolebindings + +apiGroups: ["storage.k8s.io"] + resources: storageclasses, volumeattachments + +apiGroups: ["apiextensions.k8s.io"] + resources: customresourcedefinitions (CRDs) +``` + +```bash +# 查询某个资源属于哪个 API Group +kubectl api-resources | grep deployment +# NAME SHORTNAMES APIVERSION NAMESPACED KIND +# deployments deploy apps/v1 true Deployment +# ^^^^ +# 这里 apps 就是 apiGroup +``` + +```yaml +# 示例:给 Dispatcher 服务授予创建 Job 的权限 +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: job-creator + namespace: processing +rules: +- apiGroups: ["batch"] # ⭐ Job 属于 batch group + resources: ["jobs"] + verbs: ["create", "get", "list", "watch", "delete"] +- apiGroups: [""] # ⭐ Pod 日志属于 core group + resources: ["pods", "pods/log"] + verbs: ["get", "list", "watch"] +``` + +#### 8.2.9 subresources(子资源)权限 + +某些操作需要单独授权**子资源**: + +```yaml +rules: +# Pod 本身的增删查 +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "delete"] + +# Pod 日志(pods/log 是子资源) +- apiGroups: [""] + resources: ["pods/log"] # ⭐ 子资源格式:资源/子资源 + verbs: ["get"] + +# Pod exec(远程执行命令) +- apiGroups: [""] + resources: ["pods/exec"] # ⭐ kubectl exec 需要此权限 + verbs: ["create"] + +# Deployment 扩缩容(scale 子资源) +- apiGroups: ["apps"] + resources: ["deployments/scale"] # ⭐ kubectl scale 需要此权限 + verbs: ["update"] +``` + +``` +常见子资源速查: + pods/log → kubectl logs + pods/exec → kubectl exec + pods/portforward → kubectl port-forward + pods/status → 更新 Pod 状态 + deployments/scale → kubectl scale + services/proxy → kubectl proxy +``` + +#### 8.2.10 RBAC 调试与审计(⭐ 必会命令) + +```bash +# ===== 权限检查 ===== + +# 检查当前用户是否有某个权限 +kubectl auth can-i create pods +kubectl auth can-i list secrets -n production + +# 检查指定 SA 是否有权限(impersonation) +kubectl auth can-i list pods \ + --as=system:serviceaccount:production:app-sa \ + -n production + +# 列出当前用户所有权限(K8S 1.14+) +kubectl auth can-i --list -n production + +# 列出指定 SA 的所有权限 +kubectl auth can-i --list \ + --as=system:serviceaccount:monitoring:prometheus-sa \ + -n monitoring + +# ===== 查看 RBAC 资源 ===== + +# 查看所有 Role 和 RoleBinding +kubectl get roles,rolebindings -n production + +# 查看 ClusterRole 的详细规则 +kubectl describe clusterrole edit + +# 查看谁绑定了某个 ClusterRole +kubectl get clusterrolebindings -o wide | grep cluster-admin + +# 找出某个 SA 被绑定到哪些 Role/ClusterRole +kubectl get rolebindings,clusterrolebindings -A \ + -o jsonpath='{range .items[*]}{.metadata.name}{" → "}{range .subjects[*]}{.kind}/{.name}{" "}{end}{"\n"}{end}' \ + | grep "app-sa" + +# ===== 权限问题排查流程 ===== +# 1. 查看错误信息(通常是 403 Forbidden) +kubectl get pods -n production +# Error: pods is forbidden: User "xxx" cannot list resource "pods" ... + +# 2. 确认当前身份 +kubectl auth whoami # K8S 1.28+ +kubectl config current-context + +# 3. 检查权限 +kubectl auth can-i list pods --as=system:serviceaccount:production:app-sa -n production + +# 4. 找到对应的 SA 和 RoleBinding +kubectl describe rolebinding -n production | grep -A5 "app-sa" + +# 5. 修复:添加缺失权限 +kubectl edit role -n production +``` + +#### 8.2.11 生产级 RBAC 架构(多团队场景) + +**场景**:一个 K8S 集群,team-a 和 team-b 各有自己的 Namespace,SRE 团队负责全集群运维。 + +```mermaid +flowchart TB + subgraph Cluster["K8S 集群"] + subgraph NS_A["Namespace: team-a"] + SA_A1["app-sa"] + SA_A2["ci-sa"] + SA_A3["mon-sa"] + SA_A1 --> RB_A1["RoleBinding"] + SA_A2 --> RB_A2["RoleBinding"] + SA_A3 --> RB_A3["RoleBinding"] + RB_A1 -.-> CR_E["ClusterRole: edit"] + RB_A2 -.-> CR_E + RB_A3 -.-> CR_V["ClusterRole: view"] + end + + subgraph NS_B["Namespace: team-b"] + SA_B1["app-sa"] + SA_B2["ci-sa"] + SA_B1 --> RB_B1["RoleBinding"] + SA_B2 --> RB_B2["RoleBinding"] + RB_B1 -.-> CR_E + RB_B2 -.-> CR_E + end + + subgraph ClusterLevel["集群级别(ClusterRoleBinding)"] + SRE["sre-team"] -.-> CR_ADMIN["ClusterRole: cluster-admin"] + MON["monitoring-sa"] -.-> CR_V + ING["ingress-controller-sa"] -.-> CR_ING["ClusterRole: ingress-viewer"] + end + end +``` + +> **图 8-4:多团队 RBAC 架构** + +```yaml +# 完整示例:为 team-a 的 CI/CD 流水线配置 RBAC + +# 1. 创建 ServiceAccount +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ci-deployer + namespace: team-a +--- +# 2. 创建 Role(只允许 CI/CD 需要的操作) +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: ci-deployer-role + namespace: team-a +rules: +# 允许管理 Deployment +- apiGroups: ["apps"] + resources: ["deployments", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] +# 允许管理 ConfigMap(不含 Secret) +- apiGroups: [""] + resources: ["configmaps", "services"] + verbs: ["get", "list", "watch", "create", "update", "patch"] +# 只允许查看 Pod(不允许创建/删除) +- apiGroups: [""] + resources: ["pods", "pods/log"] + verbs: ["get", "list", "watch"] +# 允许查看 Job 状态(部署后验证) +- apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch"] +# 注意:❌ 不授予 secrets 权限(CI 环境不需要读敏感信息) +# 注意:❌ 不授予 rolebindings 权限(防止 CI 提升自身权限) +--- +# 3. RoleBinding 绑定 +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: ci-deployer-binding + namespace: team-a +subjects: +- kind: ServiceAccount + name: ci-deployer + namespace: team-a +roleRef: + kind: Role + name: ci-deployer-role + apiGroup: rbac.authorization.k8s.io +``` + +#### 8.2.12 RBAC 最佳实践(面试加分) + +``` +✅ 最小权限原则 + → 每个 ServiceAccount 只授予必需的权限 + → 不要直接用 default SA + → 定期审计(kubectl auth can-i --list) + +✅ 分离读写权限 + → developer Role: 可以 get/list/create/update + → viewer Role: 只能 get/list/watch + +✅ 限制 Secret 访问 + → 大部分应用不需要访问 Secret + → 把 Secret 权限单独抽到一个 Role + → CI/CD SA 通常不需要读 Secret(通过注入而不是读取) + +✅ 使用命名规范 + → Role/Binding: -(如 dev-readonly) + → ServiceAccount: -sa(如 api-sa, ci-deployer) + +✅ 优先使用内置 ClusterRole + → view/edit/admin 已覆盖大多数场景 + → 通过 RoleBinding 引用 ClusterRole,避免重复定义 + +✅ 使用 RoleBinding + ClusterRole 代替重复的 Role + → 多个 Namespace 需要相同规则时,创建一个 ClusterRole, + 在每个 Namespace 用 RoleBinding 引用 + +❌ 不要使用 ClusterRoleBinding 授予非必要权限 + → ClusterRoleBinding 是全集群级别的 + → 能用 RoleBinding 的就不要用 ClusterRoleBinding + +❌ 不要授予通配符权限 + → resources: ["*"] 或 verbs: ["*"] 等于放弃最小权限 + → cluster-admin 只给极少数人 + +❌ 防止权限提升(Privilege Escalation) + → 不要给应用授予 create rolebindings/clusterrolebindings 权限 + → 否则应用可以给自己提权 +``` + +**面试回答模板**: +> "RBAC 是 K8S 的授权机制,通过 Role/ClusterRole 定义权限规则,通过 RoleBinding/ClusterRoleBinding 绑定到 User/Group/ServiceAccount。有几个关键点容易混淆:一是 ClusterRole 可以被 RoleBinding 引用,此时权限只在该 Namespace 生效,这是多 Namespace 复用权限规则的常用方式;二是 API Group 要写对,Deployment 在 apps 组,Job 在 batch 组;三是子资源需要单独授权,比如 pods/exec 和 pods/log。生产实践中我们遵循最小权限原则,每个应用独立 SA,用 `kubectl auth can-i --list` 做定期审计。" + +--- + +### 8.3 NetworkPolicy(网络策略) + +#### 8.3.1 为什么需要 NetworkPolicy? + +``` +默认情况下: + K8S 中所有 Pod 之间网络完全互通(零信任反面) + +有了 NetworkPolicy: + 可以精确控制: + → 哪些 Pod 可以被访问 + → 哪些 Pod 可以访问外部 + → 允许哪些端口和协议 +``` + +> ⚠️ NetworkPolicy 需要 CNI 插件支持(Calico、Cilium 支持,Flannel 默认不支持)。 + +#### 8.3.2 NetworkPolicy 示例 + +**示例1:只允许前端 Pod 访问 API Pod** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: api-allow-frontend + namespace: production +spec: + podSelector: + matchLabels: + app: api # 这个 Policy 作用于 label=app:api 的 Pod + policyTypes: + - Ingress # 控制入站流量 + ingress: + - from: + - podSelector: + matchLabels: + app: frontend # 只允许 label=app:frontend 的 Pod + ports: + - port: 8080 # 只允许 8080 端口 + protocol: TCP +``` + +**效果**: +``` +frontend Pod → api Pod:8080 ✅ 允许 +database Pod → api Pod:8080 ❌ 拒绝 +任何 Pod → api Pod:3306 ❌ 拒绝(只开了 8080) +api Pod → 外部 ✅ 允许(出站默认不受限,除非设 Egress) +``` + +**示例2:禁止 Pod 访问外部网络** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-external + namespace: production +spec: + podSelector: + matchLabels: + app: sensitive-app + policyTypes: + - Egress # 控制出站流量 + egress: + - to: + - podSelector: {} # 只允许访问同 namespace 内的 Pod + ports: + - port: 53 # DNS + protocol: UDP + - to: # 允许访问特定 Service + - namespaceSelector: + matchLabels: + name: production +``` + +**示例3:完全隔离 Namespace** + +```yaml +# 默认拒绝所有入站流量(白名单模式) +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all-ingress + namespace: production +spec: + podSelector: {} # 作用于所有 Pod(空选择器 = 所有) + policyTypes: + - Ingress + # ingress 为空 = 拒绝所有入站 +``` + +#### 8.3.3 NetworkPolicy 要点总结 + +``` +1. 选择器(podSelector) + → 空 {} = 该 namespace 的所有 Pod + → 有条件 = 只有匹配标签的 Pod + +2. 策略类型(policyTypes) + → Ingress:入站(谁可以访问我) + → Egress:出站(我可以访问谁) + → 不指定 Ingress = 不限制入站 + → 不指定 Egress = 不限制出站 + +3. 规则模式 + → 白名单模式:先 deny-all,再逐条允许(推荐) + → 黑名单模式:逐条拒绝(不推荐,容易遗漏) + +4. DNS 注意事项 + → 即使限制了出站,也要放行 UDP 53 端口(DNS) + → 否则 Pod 无法解析域名 +``` + +--- + +### 8.4 SecurityContext(安全上下文) + +控制 Pod 和容器的安全属性: + +#### 8.4.1 Pod 级别 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: secure-pod +spec: + securityContext: + runAsNonRoot: true # ⭐ 强制容器以非 root 运行(面试高频) + runAsUser: 1000 # 以 UID 1000 运行 + runAsGroup: 3000 # 以 GID 3000 运行 + fsGroup: 2000 # 挂载的 Volume 文件属组 + seccompProfile: # Seccomp 安全配置文件 + type: RuntimeDefault + containers: + - name: app + image: myapp:1.0 + securityContext: + allowPrivilegeEscalation: false # ⭐ 禁止提权(面试高频) + readOnlyRootFilesystem: true # 根文件系统只读 + capabilities: + drop: ["ALL"] # 丢弃所有 Linux capabilities + add: ["NET_BIND_SERVICE"] # 只添加必需的 +``` + +#### 8.4.2 核心安全配置(面试记忆表) + +| 配置 | 作用 | 推荐值 | +|------|------|--------| +| `runAsNonRoot` | 禁止以 root 运行 | `true` | +| `allowPrivilegeEscalation` | 禁止 setuid 提权 | `false` | +| `readOnlyRootFilesystem` | 根文件系统只读 | `true`(需要时挂载 writable volume) | +| `capabilities.drop: ["ALL"]` | 丢弃所有 Linux capabilities | 推荐 | +| `seccompProfile` | 系统调用过滤 | `RuntimeDefault` | + +#### 8.4.3 PodSecurity admission(Pod 安全准入) + +K8S 1.25+ 使用 Pod Security Standards 替代了旧的 PodSecurityPolicy: + +| 级别 | 说明 | 策略 | +|------|------|------| +| **Privileged** | 无限制 | 不推荐 | +| **Baseline** | 基本安全(防止提权) | 最小安全要求 | +| **Restricted** | 严格安全 | 推荐 | + +```yaml +# 在 Namespace 上应用安全标准 +apiVersion: v1 +kind: Namespace +metadata: + name: production + labels: + pod-security.kubernetes.io/enforce: restricted # 强制执行 + pod-security.kubernetes.io/audit: restricted # 审计记录 + pod-security.kubernetes.io/warn: restricted # 告警 +``` + +--- + +### 8.5 ResourceQuota(资源配额) + +限制 Namespace 的总资源用量,防止一个团队占用所有资源: + +```yaml +apiVersion: v1 +kind: ResourceQuota +metadata: + name: team-quota + namespace: team-a +spec: + hard: + # 计算资源限制 + requests.cpu: "10" # 该 namespace 所有 Pod 的 CPU requests 总和不超过 10 核 + requests.memory: "20Gi" # 内存 requests 不超过 20Gi + limits.cpu: "20" # CPU limits 不超过 20 核 + limits.memory: "40Gi" # 内存 limits 不超过 40Gi + + # 对象数量限制 + pods: "50" # 最多 50 个 Pod + services: "10" # 最多 10 个 Service + persistentvolumeclaims: "20" # 最多 20 个 PVC + configmaps: "50" # 最多 50 个 ConfigMap + secrets: "50" # 最多 50 个 Secret +``` + +```bash +# 查看配额使用情况 +kubectl describe resourcequota team-quota -n team-a + +# 输出示例: +# Name: team-quota +# Resource Used Hard +# -------- ---- ---- +# limits.cpu 3 20 +# limits.memory 6Gi 40Gi +# pods 5 50 +# services 3 10 +``` + +--- + +### 8.6 LimitRange(默认资源限制) + +为没有配置 resources 的 Pod 设置默认值: + +```yaml +apiVersion: v1 +kind: LimitRange +metadata: + name: default-limits + namespace: production +spec: + limits: + - type: Container + default: # 默认 limits(没设 limits 的 Pod 自动获得) + cpu: "500m" + memory: "256Mi" + defaultRequest: # 默认 requests(没设 requests 的 Pod 自动获得) + cpu: "100m" + memory: "64Mi" + max: # 最大限制(不能超过) + cpu: "2000m" + memory: "2Gi" + min: # 最小限制(不能低于) + cpu: "50m" + memory: "32Mi" +``` + +**面试回答模板**: +> "LimitRange 是 namespace 级别的默认资源限制,为没有配置 resources 的 Pod 自动填充 default 和 defaultRequest。ResourceQuota 是 namespace 级别的总量限制。两者配合使用:LimitRange 确保每个 Pod 有合理配置,ResourceQuota 确保整个 namespace 不超标。" + +--- + +## 📝 习题 + +### 选择题 + +**Q1.** RBAC 中,定义集群级别权限的对象是? +- A) Role +- B) RoleBinding +- C) ClusterRole +- D) ServiceAccount + +**Q2.** Pod 默认使用哪个 ServiceAccount? +- A) admin-sa +- B) default +- C) kube-system +- D) 无 ServiceAccount + +**Q3.** 以下哪个 NetworkPolicy 配置会拒绝所有入站流量? +- A) `podSelector: {app: nginx}`,不设置 ingress +- B) `podSelector: {}`,设置 `policyTypes: [Ingress]`,ingress 为空 +- C) `podSelector: {app: nginx}`,设置 `policyTypes: [Egress]` +- D) 设置 `ingress: [{from: [{podSelector: {}}]}]` + +**Q4.** `runAsNonRoot: true` 的作用是? +- A) 容器以 root 用户运行 +- B) 容器不能以 root 用户运行(如果镜像默认是 root 会启动失败) +- C) 禁止容器访问网络 +- D) 限制容器的内存使用 + +**Q5.** ResourceQuota 和 LimitRange 的区别是? +- A) ResourceQuota 限制单个 Pod,LimitRange 限制整个 Namespace +- B) ResourceQuota 限制整个 Namespace 的总量,LimitRange 为单个 Pod 设置默认值 +- C) 两者完全一样 +- D) ResourceQuota 限制网络,LimitRange 限制存储 + +**Q6.** 用 `RoleBinding` 引用 `ClusterRole`,权限生效范围是? +- A) 全集群 +- B) 仅 ClusterRole 所在的 Namespace +- C) 仅 RoleBinding 所在的 Namespace +- D) 两个 Namespace 都生效 + +**Q7.** 以下哪个操作需要 `pods/exec` 权限? +- A) `kubectl get pods` +- B) `kubectl logs mypod` +- C) `kubectl exec -it mypod -- bash` +- D) `kubectl delete pod mypod` + +### 简答题 + +**Q8.** 请简述 RBAC 的四个核心对象及其作用。 + +**Q9.** 为什么不应该直接使用 default ServiceAccount?最佳实践是什么? + +**Q10.** NetworkPolicy 默认行为是什么?如何实现白名单安全策略? + +**Q11.** 请解释 SecurityContext 中 `allowPrivilegeEscalation: false` 的作用。 + +**Q12.** 你需要给 Dispatcher 服务(在 processing namespace)配置 RBAC,使其能够在 processing namespace 内创建和查看 Job,但不能删除 Job,也不能操作其他资源。请写出 ServiceAccount、Role、RoleBinding 的 YAML。 + +--- + +## ✅ 参考答案 + +### A1. C) ClusterRole +Role 是 Namespace 级别,ClusterRole 是集群级别。 + +### A2. B) default +每个 Namespace 自动创建一个 default ServiceAccount。建议为每个应用创建独立 SA。 + +### A3. B) `podSelector: {}`,设置 `policyTypes: [Ingress]`,ingress 为空 +空 podSelector = 所有 Pod。policyTypes 指定 Ingress 但 ingress 规则为空 = 拒绝所有入站。 + +### A4. B) 容器不能以 root 用户运行 +如果镜像的 USER 是 root(UID 0),设置 `runAsNonRoot: true` 后 Pod 会启动失败。 + +### A5. B) ResourceQuota 限制整个 Namespace 的总量,LimitRange 为单个 Pod 设置默认值 + +### A6. C) 仅 RoleBinding 所在的 Namespace +RoleBinding 引用 ClusterRole 时,权限范围由 Binding 决定,只在 RoleBinding 所在的 Namespace 生效。这是 ClusterRole 跨 Namespace 复用权限规则的常见方式,而非扩大权限范围。 + +### A7. C) `kubectl exec -it mypod -- bash` +- `kubectl get pods` → 需要 `pods` 的 `list` 权限 +- `kubectl logs` → 需要 `pods/log` 子资源的 `get` 权限 +- `kubectl exec` → 需要 `pods/exec` 子资源的 `create` 权限 +- `kubectl delete pod` → 需要 `pods` 的 `delete` 权限 + +### A8. RBAC 四个核心对象 +``` +Role:Namespace 级别的权限规则(能访问哪些 API、执行哪些操作) +ClusterRole:集群级别的权限规则(也可被 RoleBinding 引用,只在该 NS 生效) +RoleBinding:将 Role/ClusterRole 绑定到 User/Group/ServiceAccount(Namespace 内生效) +ClusterRoleBinding:将 ClusterRole 绑定到主体(全集群生效) +``` + +### A9. 不使用 default SA 的原因 +``` +1. default SA 权限不可控——不同版本的 K8S 对 default SA 的权限策略不同 +2. 无法区分应用——多个 Pod 用同一个 SA,审计时无法区分 +3. 最小权限原则——每个应用应该只有必要的权限 + +最佳实践: + - 每个应用创建独立 ServiceAccount + - 通过 RoleBinding 授予最小必要权限 + - 在 Pod spec 中显式指定 serviceAccountName + - 定期用 kubectl auth can-i --list 审计权限 +``` + +### A10. NetworkPolicy 默认行为 +``` +默认:所有 Pod 之间网络完全互通(无限制) + +白名单安全策略实现: + 1. 先创建一个"拒绝所有入站"的 NetworkPolicy + 2. 再逐条创建"允许特定来源"的 NetworkPolicy + + # 步骤1:默认拒绝 + podSelector: {}, policyTypes: [Ingress], ingress: [] + + # 步骤2:允许前端访问 API + podSelector: {app: api}, ingress: [{from: [{podSelector: {app: frontend}}]}] +``` + +### A11. allowPrivilegeEscalation: false +``` +allowPrivilegeEscalation 控制容器是否可以"提权": + + false → 容器进程不能通过 setuid/setgid 提升权限 + true → 允许提权(默认值) + +为什么重要? + - 即使以普通用户运行(runAsUser: 1000), + 如果进程有 setuid 位,仍然可以提权到 root + - 设置为 false + runAsNonRoot: true = 双重保险 + +这是 CIS K8S Benchmark 的必检项。 +``` + +### A12. Dispatcher RBAC 配置 +```yaml +# ServiceAccount +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dispatcher-sa + namespace: processing +--- +# Role:只允许创建和查看 Job +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: dispatcher-role + namespace: processing +rules: +- apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["create", "get", "list", "watch"] # ❌ 不包含 delete +- apiGroups: [""] + resources: ["pods", "pods/log"] # 查看 Job 创建的 Pod 日志 + verbs: ["get", "list", "watch"] +--- +# RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: dispatcher-binding + namespace: processing +subjects: +- kind: ServiceAccount + name: dispatcher-sa + namespace: processing +roleRef: + kind: Role + name: dispatcher-role + apiGroup: rbac.authorization.k8s.io + +# 验证: +# kubectl auth can-i create jobs --as=system:serviceaccount:processing:dispatcher-sa -n processing +# → yes +# kubectl auth can-i delete jobs --as=system:serviceaccount:processing:dispatcher-sa -n processing +# → no +``` + +--- + +## 🔧 实操练习 + +```bash +export KUBECONFIG=~/.kube/config +kubectl create namespace k8s-training + +# 1. 创建 ServiceAccount +kubectl create sa app-sa -n k8s-training + +# 2. 创建 Role(只允许操作 Pod) +kubectl apply -f - < # 详细描述(含 Events) +kubectl get pod -o yaml # 完整 YAML + +# 编辑资源(直接打开编辑器) +kubectl edit deployment nginx -n k8s-training +# 等同于:kubectl get → 修改 → kubectl apply + +# 查看资源变更(diff) +kubectl diff -f deployment.yaml # 查看本地 YAML 和集群差异 + +# 打补丁(局部更新) +kubectl patch deployment nginx -p '{"spec":{"replicas":5}}' -n k8s-training + +# 临时注入(调试用,不修改原始 YAML) +kubectl set image deployment/nginx nginx=nginx:1.26 -n k8s-training +kubectl set resources deployment/nginx -c nginx --limits=cpu=1,memory=1Gi -n k8s-training +``` + +#### 9.1.4 调试命令 + +```bash +# 查看日志 +kubectl logs -n k8s-training +kubectl logs -c # 多容器时指定容器 +kubectl logs --previous # 上一次崩溃的日志(CrashLoopBackOff 必备) +kubectl logs -l app=nginx # 按标签查看多个 Pod 日志 +kubectl logs -f # 实时跟踪(类似 tail -f) +kubectl logs --tail=100 # 最近 100 行 +kubectl logs --since=1h # 最近 1 小时 + +# 进入容器 +kubectl exec -it -- /bin/bash +kubectl exec -it -- /bin/sh # Alpine 等轻量镜像 + +# 端口转发 +kubectl port-forward svc/nginx 8080:80 # 本地 8080 → Service 80 +kubectl port-forward pod/nginx 8080:80 # 直接转发到 Pod + +# 临时运行 Pod(调试用) +kubectl run debug --image=busybox:1.36 --rm -it --restart=Never -- sh +kubectl run curl-debug --image=curlimages/curl --rm -it --restart=Never -- curl http://nginx-svc + +# 文件拷贝 +kubectl cp :/path/file ./local-file # 从 Pod 拷出 +kubectl cp ./local-file :/path/file # 拷入 Pod + +# 查看 RBAC 权限 +kubectl auth can-i create deployments -n k8s-training +kubectl auth can-i create deployments --as=system:serviceaccount:k8s-training:app-sa -n k8s-training +kubectl auth can-i --list --as=system:serviceaccount:k8s-training:app-sa -n k8s-training +``` + +--- + +### 9.2 Pod 生命周期与状态转换 + +```mermaid +stateDiagram-v2 + [*] --> Pending: kubectl apply + Pending --> Running: Scheduler 分配 Node + Running --> Succeeded: 正常退出 + Running --> Failed: 探针失败 + Running --> Unknown: Node 不可达 + Unknown --> Running: Node 恢复 + + note right of Pending: 资源已创建,等待调度 + note right of Running: 容器已启动(Running 或 Running+Ready) + note right of Succeeded: 用于 Job/CronJob(正常退出) + note right of Failed: 容器启动失败 + note right of Unknown: Node 不可达 +``` + +> **图 9-1:Pod 生命周期状态转换图** + +**面试关键:理解 Pending 不一定是异常,可能是正常的调度等待。** + +--- + +### 9.3 常见异常排查(⭐⭐⭐ 面试必考) + +#### 9.3.1 排查万能公式 + +``` +第一步:看状态 + kubectl get pods + +第二步:看详情 + kubectl describe pod + → 重点看 Events 部分(最底部) + +第三步:看日志 + kubectl logs + kubectl logs --previous # CrashLoopBackOff 时看上次日志 + +第四步:看指标 + kubectl top pod +``` + +#### 9.3.2 Pending + +``` +原因排查: + kubectl describe pod + → Events: 0/3 nodes available, ... + +常见原因: + ❌ 资源不足:Node 的 CPU/内存 requests 已满 + ❌ nodeSelector 没有匹配的 Node + ❌ nodeAffinity 不满足 + ❌ Taint 不被 Toleration 容忍 + ❌ PVC 未绑定(PVC Pending 导致 Pod Pending) + ❌ podAntiAffinity 冲突(没有足够的 Node) + +解决: + → kubectl describe node 查看资源使用情况 + → kubectl get nodes --show-labels 检查标签 + → kubectl describe node | grep Taints 检查污点 + → kubectl get pvc 检查 PVC 状态 +``` + +#### 9.3.3 CrashLoopBackOff + +``` +原因排查: + kubectl logs --previous # ⭐ 关键!看崩溃前的日志 + kubectl describe pod + +常见原因: + ❌ 应用启动失败(配置错误、依赖不可用) + ❌ livenessProbe 配置不合理(应用正常但探针判断失败) + ❌ OOM Kill(内存超过 limits) + ❌ 命令/参数错误 + +解决: + → kubectl logs --previous 看崩溃原因 + → kubectl describe pod 看 State.Last State.Terminated(含退出码和原因) + → 检查 livenessProbe 配置(initialDelaySeconds 是否够) + → 检查 resources.limits.memory 是否太小 +``` + +#### 9.3.4 ImagePullBackOff / ErrImagePull + +``` +原因排查: + kubectl describe pod + → Events: Failed to pull image ... + +常见原因: + ❌ 镜像名/标签错误 + ❌ 私有仓库无凭证(需要 imagePullSecrets) + ❌ 网络不通(无法访问镜像仓库) + ❌ 镜像不存在(tag 错误) + +解决: + → 检查 image 字段拼写和 tag + → kubectl create secret docker-registry 创建凭证 + → kubectl apply -f deployment.yaml 更新 imagePullSecrets + → 在 Node 上手动 docker pull 测试连通性 +``` + +#### 9.3.5 Running 但 Not Ready + +``` +原因排查: + kubectl describe pod + +常见原因: + ❌ readinessProbe 失败(应用还没准备好) + ❌ 应用启动慢(Java/Spring Boot 等需要预热) + ❌ 依赖服务不可用(数据库连接失败) + +解决: + → 检查 readinessProbe 配置 + → 增加 initialDelaySeconds + → 配置 startupProbe(给慢启动应用缓冲) + → kubectl logs 查看应用日志 +``` + +#### 9.3.6 OOMKilled + +``` +原因排查: + kubectl describe pod + → State.Last State.Terminated.Reason: OOMKilled + → State.Last State.Terminated.OOMKilled: true + +常见原因: + ❌ limits.memory 设置太小 + ❌ 应用存在内存泄漏 + ❌ JVM -Xmx 设置接近 limits + +解决: + → 增大 resources.limits.memory + → JVM 应用:-Xmx < limits.memory * 0.8 + → 分析内存使用:kubectl top pod → 持续观察 +``` + +**面试回答模板**: +> "排查 Pod 问题我一般遵循四步:先 kubectl get pods 看状态,再 describe 看 Events,然后 logs 看日志,最后 top 看资源。最常见的是 CrashLoopBackOff,我会先用 `--previous` 看上次崩溃日志,再结合 describe 中的退出码和终止原因判断。如果是 Pending 则检查资源、亲和性、污点容忍和 PVC 状态。" + +--- + +### 9.4 Events 事件系统 + +Events 是 K8S 中记录"发生了什么"的审计日志,是排查问题的第一入口。 + +```bash +# 查看所有事件 +kubectl get events -n k8s-training + +# 查看事件详情(按时间排序,最新的在上面) +kubectl get events -n k8s-training --sort-by='.lastTimestamp' + +# 只看 Warning 事件 +kubectl get events -n k8s-training --field-selector type=Warning + +# 只看某个 Pod 的事件 +kubectl get events -n k8s-training --field-selector involvedObject.name= + +# 持续监控事件 +kubectl get events -n k8s-training -w + +# 查看事件详情 +kubectl describe pod # Events 在输出最底部 +``` + +Events 的常见类型: +``` +Normal 类型: + Pulling → 正在拉取镜像 + Pulled → 镜像拉取完成 + Created → 容器已创建 + Started → 容器已启动 + Scheduled → Pod 已调度到 Node + Killing → 正在终止容器 + +Warning 类型: + FailedScheduling → 调度失败(资源不足、亲和性不满足) + FailedAttachVolume → 挂载 Volume 失败 + FailedMount → Mount 失败 + InsufficientCPU → CPU 不够 + InsufficientMemory → 内存不够 + BackOff → 容器启动失败,正在退避重试 +``` + +> Events 有 TTL(默认 1 小时),不会永久保存。长期事件监控需要使用日志系统。 + +--- + +### 9.5 日志体系 + +#### 9.5.1 容器日志 + +``` +K8S 中容器日志的标准输出: + stdout → 容器标准输出 + stderr → 容器错误输出 + → kubelet 自动采集,kubectl logs 可查看 + → 日志文件存储在 Node 的 /var/log/containers/ +``` + +```bash +# 基本日志 +kubectl logs + +# 多容器 Pod +kubectl logs -c + +# 之前的容器(崩溃后重启,旧容器日志丢失除非用 --previous) +kubectl logs --previous + +# 跟踪日志 +kubectl logs -f + +# 多 Pod 日志聚合(按标签) +kubectl logs -l app=nginx --all-containers=true +``` + +#### 9.5.2 集群组件日志 + +```bash +# Node 上的组件日志 +# kubelet 日志(在 Node 上) +journalctl -u kubelet -f + +# kube-proxy 日志 +journalctl -u kube-proxy -f + +# Master 组件日志(在 Master Node 上) +journalctl -u kube-apiserver -f +journalctl -u kube-scheduler -f +journalctl -u kube-controller-manager -f + +# 或者通过 Pod 查看(组件以静态 Pod 运行时) +kubectl logs -n kube-system kube-apiserver- +kubectl logs -n kube-system etcd- +``` + +#### 9.5.3 集中式日志方案 + +```mermaid +flowchart LR + subgraph EFK["方案1:EFK Stack(传统)"] + F1["Fluentd
(采集)"] --> E1["Elasticsearch
(存储/搜索)"] + E1 --> K1["Kibana
(可视化)"] + end + + subgraph PLG["方案2:PLG Stack(更现代)"] + P1["Promtail
(采集)"] --> L1["Loki
(存储)"] + L1 --> G1["Grafana
(可视化)"] + end + + subgraph Cloud["方案3:云厂商托管"] + C1["阿里云 SLS / AWS CloudWatch / 腾讯云 CLS"] + end +``` + +> **图 9-2:集中式日志方案** + +--- + +### 9.6 监控告警概览 + +#### 9.6.1 Prometheus + Grafana(行业标准) + +```mermaid +flowchart TB + subgraph Prometheus["Prometheus
(采集、存储、告警)"] + P["Prometheus
(数据源)"] + end + + P --> G["Grafana
(面板)"] + P --> A["AlertManager
(告警通知)"] + + subgraph Targets["采集目标"] + K["kubelet
(Node/Pod 指标)"] + KSM["kube-state-metrics
(K8S 资源状态)"] + NE["node-exporter
(Node 硬件指标)"] + BB["blackbox-exporter
(探针)"] + APP["应用自带 /metrics 端点"] + end + + K & KSM & NE & BB & APP --> P +``` + +> **图 9-3:Prometheus + Grafana 监控体系架构** + +#### 9.6.2 关键监控指标 + +``` +Node 级别: + → CPU 使用率、内存使用率、磁盘 IO、网络流量 + → kubelet 状态 + +Pod 级别: + → CPU/Memory 使用率 vs requests/limits + → Pod 重启次数 + → OOM Kill 次数 + +集群级别: + → Pod 数量、Deployment 状态 + → PV 使用率 + → 证书过期时间 + → etcd 集群健康 + +业务级别: + → HTTP 请求量 / 错误率 / 延迟(RED 指标) + → 消息队列积压 + → 数据库连接池 +``` + +--- + +### 9.7 集群维护操作 + +#### 9.7.1 节点维护(cordon / drain) + +```bash +# 标记 Node 不可调度(不再调度新 Pod,已有 Pod 不受影响) +kubectl cordon + +# 驱逐 Node 上的所有 Pod(维护前使用) +kubectl drain --ignore-daemonsets --delete-emptydir-data + +# cordon + drain 的典型场景: +# 1. Node 需要升级/维护 +# 2. 缩容集群 +# 3. 节点硬件故障 + +# 恢复 +kubectl uncordon +``` + +#### 9.7.2 版本升级策略 + +``` +K8S 版本号:v1.28.3 + → 1 = 大版本 + → 28 = 小版本 + → 3 = 补丁版本 + +升级规则(面试了解): + → 小版本只能升级 +1(1.27 → 1.28,不能跳到 1.29) + → 大版本不能跳(不能直接从 1.26 → 1.28) + → 控制面先升级,再逐个升级 Worker + +多云/托管集群(EKS/ACK/GKE): + → 控制面升级由云厂商管理 + → Worker Node 升级通过滚动替换 +``` + +#### 9.7.3 备份与恢复 + +``` +etcd 备份(集群最重要的备份): + # 备份 etcd 快照 + ETCDCTL_API=3 etcdctl snapshot save snapshot.db \ + --endpoints=https://127.0.0.1:2379 \ + --cacert=/etc/kubernetes/pki/etcd/ca.crt \ + --cert=/etc/kubernetes/pki/etcd/server.crt \ + --key=/etc/kubernetes/pki/etcd/server.key + + # 恢复(⚠️ 慎重!) + ETCDCTL_API=3 etcdctl snapshot restore snapshot.db + +资源清单备份(YAML 级别): + # 导出所有资源为 YAML + kubectl get all -A -o yaml > cluster-backup.yaml + + # 或使用 Velero(专业备份工具) + velero backup create daily-backup --include-namespaces=production +``` + +--- + +### 9.8 常见运维场景 + +#### 场景1:滚动更新卡住 + +```bash +# 现象:kubectl rollout status 一直不完成 + +# 排查 +kubectl get pods # 看是否有旧 Pod 没退出 +kubectl describe deployment # 看 Events 和 Conditions + +# 常见原因 +# 1. 新 Pod 的 readinessProbe 不通过 → 旧 Pod 不会被终止 +# 2. 旧 Pod 的 preStop hook 阻塞 +# 3. 资源不足,新 Pod Pending + +# 强制回滚 +kubectl rollout undo deployment/ +``` + +#### 场景2:Namespace 删除卡住 + +```bash +# 现象:kubectl delete ns 一直 Terminating + +# 原因:有资源仍在使用该 Namespace + +# 解决:强制删除(最后手段) +kubectl patch namespace -p '{"metadata":{"finalizers":null}}' --type=merge +``` + +#### 场景3:Pod 频繁重启 + +```bash +# 排查步骤 +kubectl get pod -o yaml | grep -A5 restartCount # 看重启次数 +kubectl describe pod | grep -A10 "Last State" # 看上次的终止原因 +kubectl logs --previous # 看崩溃日志 + +# 常见原因: +# OOMKilled → 增大 limits.memory +# Exit Code 137 → 被 SIGKILL(通常是 OOM) +# Exit Code 1 → 应用错误(看日志) +# Exit Code 0 → 正常退出(可能是探针配置问题) +# Exit Code 139 → 段错误(程序崩溃) +``` + +--- + +## 📝 习题 + +### 选择题 + +**Q1.** Pod 处于 CrashLoopBackOff 状态,首先应该用什么命令排查? +- A) `kubectl describe node` +- B) `kubectl logs --previous` +- C) `kubectl top pods` +- D) `kubectl get events` + +**Q2.** `kubectl cordon ` 的作用是? +- A) 驱逐 Node 上的所有 Pod +- B) 标记 Node 不可调度(不影响已有 Pod) +- C) 删除 Node +- D) 重启 Node + +**Q3.** Pod 处于 Pending 状态,最不可能的原因是? +- A) Node 资源不足 +- B) 镜像拉取失败 +- C) PVC 未绑定 +- D) podAntiAffinity 冲突 + +**Q4.** `kubectl drain ` 的作用是? +- A) 清空 Node 上的所有日志 +- B) 安全驱逐 Node 上的所有 Pod(DaemonSet 除外) +- C) 删除 Node +- D) 暂停 Node + +**Q5.** Events 的默认保留时间是? +- A) 永久 +- B) 1 小时 +- C) 24 小时 +- D) 7 天 + +### 简答题 + +**Q6.** 请描述 Pod 常见异常状态(Pending、CrashLoopBackOff、ImagePullBackOff、OOMKilled)的排查步骤。 + +**Q7.** `kubectl describe pod ` 的输出中,哪些部分对排查问题最有价值? + +**Q8.** 生产环境的日志方案应该考虑哪些方面? + +### 场景题 + +**Q9.** 用户反馈服务不可用,你收到告警。请描述从接到告警到定位问题的完整排查流程。 + +**Q10.** 一个 Deployment 的滚动更新卡住了,`kubectl rollout status` 一直显示等待。请列出排查步骤。 + +--- + +## ✅ 参考答案 + +### A1. B) `kubectl logs --previous` +CrashLoopBackOff 意味着容器反复崩溃重启,`--previous` 可以看到上一次崩溃前的日志。 + +### A2. B) 标记 Node 不可调度(不影响已有 Pod) +cordon 只是设置 `Unschedulable: true`,已有 Pod 继续运行。drain 才会驱逐 Pod。 + +### A3. B) 镜像拉取失败 +镜像拉取失败会导致 ImagePullBackOff/ErrImagePull,不是 Pending。Pending 的常见原因是资源不足、亲和性不满足、PVC 未绑定等调度问题。 + +### A4. B) 安全驱逐 Node 上的所有 Pod(DaemonSet 除外) +drain = cordon(标记不可调度)+ 驱逐 Pod。通常需要 `--ignore-daemonsets` 和 `--delete-emptydir-data`。 + +### A5. B) 1 小时 +Events 默认 TTL 为 1 小时(`--event-ttl=1h`),不是永久保存。 + +### A6. 常见异常排查步骤 +``` +Pending: + 1. kubectl describe pod → Events 部分 + 2. 检查 "0/N nodes available" + 3. 检查资源:kubectl describe node → Allocatable + 4. 检查亲和性:nodeSelector/nodeAffinity/podAntiAffinity + 5. 检查污点:kubectl describe node | grep Taints + 6. 检查 PVC:kubectl get pvc + +CrashLoopBackOff: + 1. kubectl logs --previous(看崩溃日志) + 2. kubectl describe pod → Last State.Terminated + 3. 检查退出码和原因(OOMKilled、Error、Completed) + 4. 检查 livenessProbe 配置 + +ImagePullBackOff: + 1. kubectl describe pod → Events + 2. 检查 image 拼写和 tag + 3. 检查 imagePullSecrets + 4. 在 Node 上手动 docker pull 测试 + +OOMKilled: + 1. kubectl describe pod → Last State.Terminated + 2. 增大 limits.memory + 3. JVM 应用:-Xmx < limits * 0.8 +``` + +### A7. describe 输出中最有价值的部分 +``` +1. Events(最底部) + → 记录了 Pod 的完整事件链 + → Warning 类型的事件是排查关键 + +2. Conditions + → PodScheduled:是否已调度 + → Initialized:Init 容器是否完成 + → Ready:是否就绪 + → ContainersReady:容器是否就绪 + +3. Last State.Terminated(如果 Pod 重启过) + → Exit Code(退出码) + → Reason(OOMKilled、Error 等) + → Finished At(终止时间) + +4. Status.ContainerStatuses + → State:当前状态(Running/Waiting/Terminated) + → Last Termination State:上次终止信息 + → Restart Count:重启次数 +``` + +### A8. 生产环境日志方案 +``` +需求: + 1. 集中采集:所有 Pod 日志统一采集 + 2. 结构化存储:方便搜索和分析 + 3. 可视化:Dashboard 查看日志 + 4. 告警:错误日志触发告警 + 5. 保留策略:按合规要求保留(如 30 天) + +方案选型: + - EFK(Elasticsearch + Fluentd + Kibana):功能全面但资源消耗大 + - PLG(Promtail + Loki + Grafana):轻量,与 Grafana 生态集成好 + - 云厂商托管(阿里云 SLS / AWS CloudWatch):免运维 + +最佳实践: + - 应用日志写 stdout/stderr(不要写文件) + - 使用结构化 JSON 日志格式 + - 设置合理的日志级别(生产 info/warn/error) + - 敏感信息脱敏 +``` + +### A9. 服务不可用排查流程 +``` +1. 确认影响范围 + → kubectl get pods -A -o wide | grep -v Running + → 是单个 Pod 还是大规模故障? + +2. 检查 Service 和 Endpoints + → kubectl get svc -n + → kubectl get endpoints + → Endpoints 是否有健康的 Pod IP? + +3. 检查 Pod 状态 + → kubectl get pods -n + → 有没有 Pending/CrashLoopBackOff 的 Pod? + +4. 如果有异常 Pod + → kubectl describe pod → 看 Events + → kubectl logs [--previous] + +5. 检查 Node 状态 + → kubectl get nodes + → 有没有 NotReady 的 Node? + +6. 检查资源 + → kubectl top nodes + → kubectl top pods -n + +7. 快速止血 + → 如果是单个 Pod 问题:kubectl rollout restart deployment/ + → 如果是配置问题:kubectl rollout undo deployment/ + → 如果是资源问题:临时扩容或清理无用资源 +``` + +### A10. 滚动更新卡住排查 +``` +1. kubectl rollout status deployment/ # 确认卡在哪个阶段 + +2. kubectl get pods # 查看新/旧 Pod 状态 + → 新 Pod 是否 Running? + → 新 Pod 是否 Ready? + → 旧 Pod 是否在 Terminating? + +3. 如果新 Pod Not Ready: + → kubectl logs (检查应用启动) + → 检查 readinessProbe(initialDelaySeconds 够不够) + → 检查新 Pod 是否有资源 + +4. 如果旧 Pod 一直 Terminating: + → 检查 preStop hook(是否有 sleep 阻塞) + → 检查 terminationGracePeriodSeconds + → kubectl describe pod + +5. 强制处理: + → kubectl rollout undo deployment/ # 回滚 + → 或手动删除旧 Pod:kubectl delete pod --grace-period=0 --force +``` + +--- + +## 🔧 实操练习 + +```bash +export KUBECONFIG=~/.kube/config +kubectl create namespace k8s-training + +# 1. 部署一个应用 +kubectl apply -f - </dev/null | head -5 + +# 6. 练习资源查看 +kubectl top pods -n k8s-training # 如果 Metrics Server 已安装 + +# ⚠️ 清理! +kubectl delete namespace k8s-training +``` + +--- + +**上一章**:[第8章:安全与权限](./08-security.md) | **下一章**:[第10章:面试高频题](./10-interview-questions.md) diff --git a/docs/posts/k8s-training/10-interview-questions.md b/docs/posts/k8s-training/10-interview-questions.md new file mode 100644 index 0000000..c77bbb5 --- /dev/null +++ b/docs/posts/k8s-training/10-interview-questions.md @@ -0,0 +1,780 @@ +--- +title: "第10章:K8S 面试高频题 + 答案模板" +date: "2026-05-15" +category: "k8s" +tags: + - "k8s" + - "AI生成" +--- + +# 第10章:K8S 面试高频题 + 答案模板 + +## 🎯 使用说明 +- ⭐ = 必考题(每场面试几乎必问) +- 面试时按"核心概念 → 深入细节 → 实际经验"的结构回答 +- 每道题都有【一句话回答】和【展开回答】,根据时间选择 + +--- + +## 一、K8S 架构篇 + +### ⭐ Q1: 请简述 Kubernetes 的架构 + +**一句话回答**:K8S 采用 Master-Worker 架构,Master 节点负责管理和调度(API Server、Scheduler、Controller Manager、etcd),Worker 节点运行容器(kubelet、kube-proxy)。 + +**展开回答**: +```mermaid +flowchart TB + subgraph Master["Master 节点(控制面)"] + API["API Server
唯一入口,认证/授权/准入控制,所有组件通过它通信"] + etcd["etcd
分布式KV存储,保存集群所有状态数据"] + Scheduler["Scheduler
调度器,决定Pod运行在哪个Node"] + CM["Controller Manager
控制器管理器,维护集群状态"] + end + + subgraph Worker["Worker 节点(数据面)"] + kubelet["kubelet
每个Node上的代理,管理容器生命周期"] + kubeproxy["kube-proxy
维护网络规则,实现Service负载均衡"] + end +``` + +**加分点**:提到"所有组件通过 API Server 通信而非直接访问 etcd"体现了对安全和解耦的理解。 + +--- + +### ⭐ Q2: Pod 从创建到运行的完整流程 + +**一句话回答**:kubectl → API Server(认证+写入etcd)→ Scheduler(调度)→ kubelet(创建容器)→ Running。 + +**展开回答**: +``` +1. kubectl 发送请求到 API Server +2. API Server 进行认证、授权、准入控制 +3. 将 Pod 配置写入 etcd(此时状态为 Pending) +4. Scheduler 通过 Watch 机制感知未绑定的 Pod +5. Scheduler 执行调度算法,选择最优 Node +6. 将绑定信息(nodeName)写入 etcd +7. 目标 Node 的 kubelet 感知到分配给自己的 Pod +8. kubelet 调用 CRI(容器运行时接口)拉取镜像 +9. 创建容器,配置网络和存储 +10. 执行健康检查,向 API Server 汇报状态 +11. Pod 变为 Running +``` + +--- + +### Q3: etcd 挂了会怎样? + +**回答要点**: +- Master 功能完全失效(无法创建/更新/删除资源) +- 已有 Pod 继续运行(kubelet 直接管理容器) +- 自愈、调度、扩缩等所有控制功能停止 +- **类比**:etcd 是大脑,大脑罢工但手脚还能动 + +--- + +## 二、Pod 与工作负载篇 + +### ⭐ Q4: 什么是 Pod?为什么 K8S 不直接管理容器? + +**一句话回答**:Pod 是 K8S 最小调度单元,包含一个或多个共享网络和存储的容器。 + +**为什么需要 Pod**: +1. 有些应用天然需要多个进程协作(如主容器 + 日志收集 sidecar) +2. 同 Pod 容器共享 IP 和端口空间,通过 localhost 通信 +3. Pod 是调度的原子单位——同 Pod 的容器一定在同一 Node +4. 同 Pod 的容器共享存储卷(Volume) + +--- + +### ⭐ Q5: livenessProbe、readinessProbe、startupProbe 的区别 + +**一句话回答**:liveness 探测"死了就重启",readiness 探测"没准备好就不接流量",startup 给慢启动应用缓冲时间。 + +``` +livenessProbe(存活探针): + 失败 → 重启容器 + 场景:应用死锁、无限循环 + 类比:心跳监测 + +readinessProbe(就绪探针): + 失败 → 从 Service Endpoints 中移除(不接收流量) + 场景:应用启动中加载缓存、预热中 + 类比:门面检查 + +startupProbe(启动探针): + 存在时,前几次 liveness/readiness 失败不会触发动作 + 场景:Java/Spring Boot 等启动慢的应用 + 作用:给应用足够的启动时间 +``` + +--- + +### ⭐ Q6: requests 和 limits 的区别 + +**一句话回答**:requests 是调度依据(保证最少给多少),limits 是运行时硬限制(最多能用多少)。 + +``` +requests(请求值): + - Scheduler 调度 Pod 时参考,确保 Node 有足够剩余资源 + - Node 的可分配资源 = 总资源 - 已分配的 requests + - 不设置 requests = 默认等于 limits + +limits(限制值): + - CPU 超过 limits → 限流(throttle),不会 OOM + - 内存超过 limits → OOM Kill 容器 + - 不设置 limits = 可以使用 Node 的所有剩余资源(危险!) + +最佳实践: + ✅ requests 和 limits 都设置 + ✅ requests ≤ limits + ✅ CPU: limits / requests ≈ 2~4(允许突发) + ✅ 内存: requests ≈ limits(避免 OOM) +``` + +--- + +### ⭐ Q7: Deployment、StatefulSet、DaemonSet 的区别 + +**一句话回答**:Deployment 无状态,StatefulSet 有状态,DaemonSet 每个节点一个。 + +| 特性 | Deployment | StatefulSet | DaemonSet | +|------|-----------|-------------|-----------| +| 场景 | Web、API | DB、MQ | 日志、监控 | +| Pod 名称 | 随机(*-abc123) | 固定有序(mysql-0,1,2) | 固定 | +| 网络标识 | 不稳定 | 稳定 DNS | 不稳定 | +| 存储共享 | 不保证 PVC 绑定 | 每个Pod独立PVC | 不需要 | +| 扩缩 | 随机并行 | 有序(逆序删除) | 不扩缩 | +| Headless SVC | 不需要 | 需要 | 不需要 | + +--- + +## 三、Service 与网络篇 + +### ⭐ Q8: Service 的类型有哪些? + +**回答**: +``` +1. ClusterIP(默认) + - 仅集群内部可访问 + - 用于微服务间通信 + - 分配虚拟IP,通过 kube-proxy(iptables/IPVS)负载均衡 + +2. NodePort + - 每个Node开放端口(30000-32767) + - 外部通过 NodeIP:NodePort 访问 + - 适合开发测试 + +3. LoadBalancer + - 云厂商创建外部LB(AWS ELB/阿里云SLB) + - 自动分配公网IP + - 生产环境对外暴露服务 + +4. ExternalName + - DNS级CNAME映射 + - 引用集群外服务 +``` + +--- + +### ⭐ Q9: Pod 之间如何通信? + +**回答**: +``` +1. 同 Pod 内容器:通过 localhost(共享网络命名空间) +2. 同 Node Pod 间:通过 cni0 网桥(如 Calico 的虚拟网卡) +3. 跨 Node Pod 间:通过 CNI 插件的隧道或路由方案 + - Calico:BGP 路由(推荐) + - Flannel:VXLAN 隧道 + - Cilium:eBPF(性能最优) + +关键规则: +- 每个Pod都有独立IP +- Pod间通信不经过NAT +- 所有Node和所有Pod间可以直接通信 +``` + +--- + +### Q10: Headless Service 是什么?什么场景用? + +**回答**: +``` +clusterIP: None,DNS直接返回Pod IP而不是ClusterIP。 + +使用场景: +1. StatefulSet:需要每个Pod有稳定独立的DNS记录 + mysql-0.mysql-headless → 10.244.1.5 + mysql-1.mysql-headless → 10.244.2.3 + +2. 自定义负载均衡:客户端自己选择连接哪个Pod +``` + +--- + +## 四、配置与存储篇 + +### ⭐ Q11: ConfigMap 和 Secret 的区别 + +**一句话回答**:ConfigMap 存非敏感配置(明文),Secret 存敏感信息(base64编码,但不是加密)。 + +``` +ConfigMap: + - 明文存储 + - 环境变量、配置文件 + - 例:DB地址、日志级别 + +Secret: + - base64编码存储(注意:不是加密!) + - 支持etcd加密(需要配置EncryptionConfiguration) + - 需要严格RBAC控制 + - 例:密码、Token、证书 + - 特殊类型:dockerconfigjson(镜像仓库凭证)、tls(证书) + +面试加分:提到 base64 不等于加密,生产用 Vault/External Secrets +``` + +--- + +### Q12: PV、PVC、StorageClass 的关系 + +**一句话回答**:StorageClass 定义存储类型,PVC 申请存储,PV 提供存储。动态供应时 PVC + StorageClass 自动创建 PV。 + +``` +静态供应: + 管理员手动创建 PV → PVC 绑定 PV → Pod 使用 PVC + +动态供应(推荐): + PVC 指定 StorageClass → Provisioner 自动创建 PV → Pod 使用 PVC + +accessModes: + - ReadWriteOnce (RWO): 单节点读写,最常用 + - ReadOnlyMany (ROX): 多节点只读 + - ReadWriteMany (RWX): 多节点读写(NFS等) + +reclaimPolicy: + - Retain: 保留数据,手动清理 + - Delete: 自动删除 +``` + +--- + +## 五、Helm 篇 + +### ⭐ Q13: 什么是 Helm?有什么优势? + +**一句话回答**:Helm 是 K8S 的包管理器,将多个 K8S 资源打包为一个 Chart,支持模板化配置、版本管理和一键部署。 + +``` +核心优势: +1. 一条命令部署(vs kubectl apply 多个文件) +2. 模板化配置(values.yaml 参数化) +3. 多环境管理(values-dev.yaml / values-prod.yaml) +4. 内置升级/回滚(helm upgrade / rollback) +5. 版本历史(helm history) +6. 生态丰富(公共 Chart 仓库) + +核心概念: +- Chart: K8S 应用的包(模板 + 默认值) +- Release: Chart 的一个运行实例 +- Repository: Chart 仓库 +- Values: 自定义配置值 +``` + +--- + +## 六、调度与弹性伸缩篇(⭐ 新增) + +### ⭐ Q14: nodeAffinity 和 podAffinity 的区别? + +**一句话回答**:nodeAffinity 控制 Pod 和 Node 的关系(Pod 选择 Node),podAffinity 控制 Pod 和 Pod 的关系(Pod 靠近/远离其他 Pod)。 + +``` +nodeAffinity(节点亲和性): + - 关注:Pod → Node + - 基于:Node 的标签 + - 不需要 topologyKey + - 场景:Pod 必须调度到 SSD Node / GPU Node + +podAffinity / podAntiAffinity(Pod 亲和/反亲和): + - 关注:Pod → Pod + - 基于:已有 Pod 的标签 + - 必须指定 topologyKey + - 场景:前端靠近缓存 / API 副本分散部署(高可用) + +topologyKey 常用值: + - kubernetes.io/hostname → 同一 Node + - topology.kubernetes.io/zone → 同一可用区 + +硬 vs 软: + - required = 必须满足,不满足 Pending + - preferred = 偏好满足,不满足也能调度 +``` + +--- + +### ⭐ Q15: Taint(污点)和 Toleration(容忍度)是什么? + +**一句话回答**:Taint 是 Node 上的排斥标记,Toleration 是 Pod 声明"我能容忍某些污点"。 + +``` +三个 effect: + NoSchedule → 新 Pod 不调度(已有不受影响) + PreferNoSchedule → 尽量不调度 + NoExecute → 不调度 + 驱逐不匹配的已有 Pod + +典型场景: + - Master 节点隔离(默认有 control-plane:NoSchedule 污点) + - GPU 专用节点(只有 GPU 任务能调度) + - Node 故障自动驱逐(node.kubernetes.io/not-ready:NoExecute) + +Taint vs nodeAffinity: + - Taint = Node 排斥 Pod("闲人免进") + - nodeAffinity = Pod 选择 Node("我想进这房间") + - 两者可组合使用 +``` + +--- + +### ⭐ Q16: HPA 的工作原理是什么? + +**一句话回答**:HPA(Horizontal Pod Autoscaler)根据 Pod 的 CPU/内存等指标自动调整 Deployment 的副本数。 + +``` +工作流程: + 1. Metrics Server 采集 Pod 的资源使用数据 + 2. HPA Controller 每 15-30 秒检查 + 3. 计算期望副本数 = ceil(当前指标总量 / 目标指标值) + 4. 调整 Deployment 的 replicas + +前提条件: + - 必须安装 Metrics Server + - Pod 必须配置 resources.requests(HPA 基于此计算百分比) + +扩缩行为控制(behavior): + scaleDown.stabilizationWindowSeconds → 缩容稳定窗口(避免抖动) + scaleUp → 扩容策略 + +面试加分: + - HPA 是"水平"扩缩(加减副本数),VPA 是"垂直"扩缩(调整 requests) + - Cluster Autoscaler 是节点级别(加减 Node) + - 三者组成完整的三级自动伸缩体系 +``` + +--- + +## 七、安全篇(⭐ 新增) + +### ⭐ Q17: RBAC 是什么?核心概念? + +**一句话回答**:RBAC 是 K8S 的授权机制,通过 Role 定义权限、RoleBinding 绑定到主体(User/Group/ServiceAccount)。 + +``` +四个核心对象: + Role → Namespace 级别权限规则 + ClusterRole → 集群级别权限规则(也可被 RoleBinding 引用) + RoleBinding → 绑定 Role/ClusterRole 到主体(Namespace 内) + ClusterRoleBinding → 绑定 ClusterRole 到主体(全集群) + +主体(subjects): + User → 外部用户 + Group → 用户组 + ServiceAccount → Pod 的身份(最常用) + +最佳实践: + ✅ 每个应用创建独立 ServiceAccount + ✅ 最小权限原则(只授予必要的 API 操作) + ✅ Secret 访问权限单独控制 + ✅ 能用 RoleBinding 就不用 ClusterRoleBinding +``` + +--- + +### ⭐ Q17-A: RoleBinding 引用 ClusterRole 和 ClusterRoleBinding 有什么区别? + +**一句话回答**:权限范围由 Binding 决定,不由 Role 决定。RoleBinding 无论引用 Role 还是 ClusterRole,都只在 RoleBinding 所在的 Namespace 生效。 + +``` +三种组合方式: + +① Role + RoleBinding(同 Namespace) + 作用范围:RoleBinding 所在 Namespace + +② ClusterRole + RoleBinding(跨 NS 复用规则) + 作用范围:⭐ 仅 RoleBinding 所在 Namespace + 优点:ClusterRole 作为"规则模板",避免每个 NS 重复定义相同的 Role + +③ ClusterRole + ClusterRoleBinding(全集群) + 作用范围:整个集群 + +面试加分回答: + "内置的 ClusterRole(如 view/edit/admin)就是利用这个机制复用的。 + 给某个团队的 SA 授予某个 Namespace 的 edit 权限时, + 我会用 RoleBinding 引用内置 ClusterRole:edit,而非自己写 Role, + 这样既复用了内置权限规则,又不扩大到全集群。" +``` + +--- + +### ⭐ Q17-B: 写 Role 时如何确定 apiGroups 填什么? + +**一句话回答**:用 `kubectl api-resources` 查资源所属的 API Version,取 `/` 前的部分就是 apiGroup。 + +``` +核心速查表: + apiGroups: [""] → Pod、Service、ConfigMap、Secret、Node、 + PVC、Namespace、Endpoints、Events + apiGroups: ["apps"] → Deployment、StatefulSet、DaemonSet、ReplicaSet + apiGroups: ["batch"] → Job、CronJob + apiGroups: ["autoscaling"] → HorizontalPodAutoscaler + apiGroups: ["networking.k8s.io"] → Ingress、NetworkPolicy + apiGroups: ["rbac.authorization.k8s.io"] → Role、RoleBinding、 + ClusterRole、ClusterRoleBinding + apiGroups: ["storage.k8s.io"] → StorageClass + +验证命令: + kubectl api-resources | grep -E "^deployments" + # NAME SHORTNAMES APIVERSION NAMESPACED KIND + # deployments deploy apps/v1 true Deployment + # ↑ + # apiGroup = "apps" +``` + +--- + +### Q17-C: 子资源(subresource)权限是什么?常见的有哪些? + +**一句话回答**:子资源是某个资源的特定操作入口,需要单独授权,格式为 `resources: ["父资源/子资源"]`。 + +``` +常见子资源速查: + pods/log → kubectl logs(需要 get 权限) + pods/exec → kubectl exec(需要 create 权限) + pods/portforward → kubectl port-forward(需要 create 权限) + pods/status → 更新 Pod 状态 + deployments/scale → kubectl scale(需要 update 权限) + +错误示例(常见坑): + 你授权了 pods 的 get 权限,但 kubectl logs 仍然报 403。 + 原因:logs 是子资源,需要单独授权 pods/log。 + +正确配置: + rules: + - apiGroups: [""] + resources: ["pods"] # Pod 本体 + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["pods/log"] # 日志子资源(单独授权) + verbs: ["get"] + - apiGroups: [""] + resources: ["pods/exec"] # exec 子资源(单独授权) + verbs: ["create"] +``` + +--- + +### Q17-D: 如何排查 RBAC 权限问题? + +**一句话回答**:用 `kubectl auth can-i` 验证权限,用 `describe` 查找绑定关系。 + +``` +排查步骤: + +1. 确认报错类型 + Error: pods is forbidden: User "system:serviceaccount:ns:app-sa" + cannot list resource "pods" in API group "" in the namespace "ns" + +2. 验证权限 + kubectl auth can-i list pods \ + --as=system:serviceaccount:ns:app-sa -n ns + # → no + +3. 列出该 SA 的所有权限 + kubectl auth can-i --list \ + --as=system:serviceaccount:ns:app-sa -n ns + +4. 找到所有绑定该 SA 的 RoleBinding + kubectl get rolebindings,clusterrolebindings -A \ + -o wide | grep "app-sa" + +5. 查看 Role/ClusterRole 的规则 + kubectl describe role -n ns + +6. 添加缺失权限 + kubectl edit role -n ns +``` + +--- + +### Q18: NetworkPolicy 的作用?默认行为是什么? + +**回答**: +``` +作用:控制 Pod 之间的网络流量(入站 Ingress / 出站 Egress) + +默认行为: + - 没有任何 NetworkPolicy → 所有 Pod 之间网络完全互通 + +创建 NetworkPolicy 后: + - 只影响匹配 podSelector 的 Pod + - 只影响 policyTypes 中指定的方向(Ingress/Egress) + - 规则是叠加的(多个 Policy 取并集) + +注意: + - 需要支持 NetworkPolicy 的 CNI(Calico、Cilium 支持,Flannel 不支持) + - 限制出站时别忘了放行 DNS(UDP 53) + - 推荐白名单模式:先 deny-all,再逐条允许 +``` + +--- + +### Q19: Pod 安全最佳实践有哪些? + +**回答**: +``` +SecurityContext: + ✅ runAsNonRoot: true → 禁止 root 运行 + ✅ allowPrivilegeEscalation: false → 禁止提权 + ✅ readOnlyRootFilesystem: true → 根文件系统只读 + ✅ drop ALL capabilities → 丢弃所有 Linux capabilities + ✅ seccompProfile: RuntimeDefault → 系统调用过滤 + +PodSecurity Standards: + - Baseline:基本安全(防止提权) + - Restricted:严格安全(推荐生产使用) + +镜像安全: + ✅ 使用非 root 基础镜像 + ✅ 定期扫描镜像漏洞(Trivy) + ✅ 使用最小化基础镜像(distroless/Alpine) + ✅ 固定镜像 tag(不用 latest) + +RBAC: + ✅ 不使用 default ServiceAccount + ✅ 每个 Pod 使用独立 SA + ✅ 最小权限原则 +``` + +--- + +## 八、实战场景篇 + +### ⭐ Q20: 如何实现零停机部署? + +**回答**: +``` +1. Deployment 滚动更新策略 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # 允许最多多1个Pod + maxUnavailable: 0 # 不允许任何Pod不可用 + +2. readinessProbe 保护 + - 新Pod通过readinessProbe后才接入流量 + - 老Pod在新Pod就绪前继续服务 + +3. 优雅关闭 + - preStop hook: sleep 10(等待连接排空) + - terminationGracePeriodSeconds: 30 + +4. Ingress/Service 层面 + - 多个版本并行运行 + - 金丝雀发布(Canary) + +5. 健康检查端点 + - 确保应用真正就绪后才报告healthy +``` + +--- + +### ⭐ Q21: Pod 常见异常状态怎么排查? + +**回答**: +``` +通用排查公式: + kubectl get pods → 看状态 + kubectl describe pod → 看 Events + kubectl logs [--previous] → 看日志 + kubectl top pod → 看资源 + +Pending → 资源不足 / 亲和性不满足 / PVC 未绑定 + → kubectl describe pod → Events: 0/N nodes available + +CrashLoopBackOff → 应用崩溃反复重启 + → kubectl logs --previous(看上次崩溃日志) + → kubectl describe pod → Last State.Terminated + +ImagePullBackOff → 镜像拉取失败 + → kubectl describe pod → Events: Failed to pull image + → 检查 imagePullSecrets + +OOMKilled → 内存超限 + → kubectl describe pod → Last State: OOMKilled + → 增大 limits.memory + +Running 但 Not Ready → readinessProbe 未通过 + → 检查 readinessProbe 配置 + → 增加 initialDelaySeconds + → 配置 startupProbe +``` + +--- + +### Q22: 如何进行容器资源调优? + +**回答**: +``` +1. 资源估算 + - 压测确定应用的实际需求 + - 使用 VPA(Vertical Pod Autoscaler)辅助推荐 + +2. CPU 建议 + - requests: 正常负载的 70-80% + - limits: requests 的 2-4 倍 + - 利用 CPU 的可压缩性(超限只会限流,不会杀死) + +3. 内存建议 + - requests ≈ limits(避免 OOM Kill) + - 预留 20-30% buffer + - 内存不可压缩,超限直接 OOM Kill + +4. JVM 应用特别注意 + - -Xmx 应小于 limits.memory 的 80% + - 留出堆外内存空间 + +5. 监控 + - 使用 Prometheus + Grafana 监控资源使用率 + - 设置 HPA(水平自动扩缩) +``` + +--- + +### Q23: K8S 中如何实现蓝绿部署和金丝雀发布? + +**蓝绿部署**: +``` +1. 部署 v2 Deployment(使用不同 label) +2. 更新 Service selector 指向 v2 +3. 如果出问题,Service selector 切回 v1 + +特点:切换即时,但需要双倍资源 +``` + +**金丝雀发布**(Canary): +``` +1. v2 Deployment 只有 1 个副本 +2. 逐步增加 v2 比例(10% → 50% → 100%) +3. 观察错误率和性能指标 +4. 有问题立即回滚 + +工具:Flagger、Argo Rollouts、Istio +``` + +--- + +## 九、进阶题 + +### Q24: K8S 的控制循环(Reconcile Loop)是什么? + +**回答**: +``` +控制循环是 K8S 控制器模式的核心: +1. 控制器观察(Watch)API Server 中的资源状态 +2. 对比"期望状态"(YAML中定义)和"实际状态"(集群中真实的) +3. 如果不一致,执行动作使实际状态趋近期望状态 +4. 循环重复 + +类比:恒温器——你设定 25°C(期望状态),恒温器不断检测室温(实际状态),低于 25°C 就开暖气。 + +每个控制器都有自己的控制循环: +- Deployment Controller:对比期望副本数 vs 实际 Pod 数 +- Node Controller:监控 Node 健康状态 +- Service Controller:维护 Endpoints +``` + +--- + +### Q25: Pod 一直处于 Pending,如何排查? + +**回答**: +``` +1. kubectl describe pod +2. 查看 Events 部分: + - "Insufficient cpu" → Node CPU 不足 + - "Insufficient memory" → Node 内存不足 + - "didn't match Pod's node affinity" → 亲和性不满足 + - "node(s) had taints that the pod didn't tolerate" → 污点不匹配 + - "persistentvolumeclaim not found/not bound" → PVC 问题 + +3. 逐一检查: + → kubectl describe node → 看 Allocatable vs Capacity + → kubectl get nodes --show-labels → 检查标签 + → kubectl describe node | grep Taints → 检查污点 + → kubectl get pvc → 检查 PVC 状态 + → 检查 podAntiAffinity 是否有足够的 Node +``` + +--- + +### Q26: etcd 和 API Server 的关系?为什么所有组件通过 API Server 通信? + +**回答**: +``` +关系: + - etcd = 数据库(存储集群所有状态) + - API Server = 统一入口(唯一操作 etcd 的组件) + +为什么所有组件通过 API Server 通信而非直接访问 etcd: + 1. 安全:etcd 不暴露给外部,通过 API Server 做认证/授权 + 2. 解耦:组件不需要知道 etcd 的拓扑和配置 + 3. 审计:所有操作经过 API Server,可以记录审计日志 + 4. 一致性:API Server 提供乐观并发控制(resourceVersion) + 5. 变更通知:组件通过 Watch API Server 而非 Watch etcd +``` + +--- + +### Q27: 滚动更新卡住了怎么排查? + +**回答**: +``` +1. kubectl rollout status deployment/ → 确认卡在哪 +2. kubectl get pods → 看新/旧 Pod 状态 + +新 Pod Not Ready? + → readinessProbe 未通过 + → 检查 initialDelaySeconds 是否够 + → kubectl logs + +旧 Pod 一直 Terminating? + → preStop hook 阻塞 + → 应用没处理 SIGTERM 信号 + → terminationGracePeriodSeconds 不够 + +解决: + → kubectl rollout undo deployment/ → 回滚 + → 修复问题后重新触发更新 +``` + +--- + +## 🎯 模拟面试(JD:能使用 K8S 进行部署) + +### 面试官可能的问题顺序 + +1. **自我介绍后**:你用过 K8S 吗?简单说说 K8S 的架构。 +2. **深入基础**:Pod 和容器的关系?什么是探针? +3. **资源管理**:requests 和 limits?资源不够会怎样? +4. **网络通信**:Service 的类型?Pod 之间怎么通信? +5. **配置管理**:怎么管理配置?ConfigMap 和 Secret 区别? +6. **调度**:Pod 怎么被调度到 Node?亲和性和污点了解吗? +7. **弹性伸缩**:怎么自动扩缩?HPA 怎么配? +8. **实际部署**:你平时怎么部署应用到 K8S?用过 Helm 吗? +9. **安全**:RBAC 了解吗?怎么控制 Pod 权限? +10. **故障排查**:Pod 起不来你怎么排查?什么流程? +11. **进阶**:怎么实现零停机部署?滚动更新策略? + +--- + +**上一章**:[第9章:运维与故障排查](./09-operations.md) | **实战**:[第6章:部署实战](./06-practice-project.md) diff --git a/docs/posts/k8s-training/index.md b/docs/posts/k8s-training/index.md new file mode 100644 index 0000000..f47d878 --- /dev/null +++ b/docs/posts/k8s-training/index.md @@ -0,0 +1,112 @@ +--- +title: "K8S 培训 - 从零到面试通关" +date: "2026-05-15" +category: "k8s" +tags: + - "k8s" + - "AI生成" +--- + +# K8S 培训 - 从零到面试通关 + +## 📋 培训大纲 + +| 章节 | 主题 | 核心知识点 | +|------|------|-----------| +| [第1章](./01-k8s-architecture.md) | K8S 核心概念与架构 | Master/Worker 组件、资源对象、声明式API | +| [第2章](./02-pod-and-workloads.md) | Pod 与工作负载管理 | Pod生命周期、探针、Deployment/StatefulSet/DaemonSet | +| [第3章](./03-service-networking.md) | Service 与网络 | 4种Service类型、DNS服务发现、Ingress | +| [第4章](./04-config-storage.md) | ConfigMap、Secret 与存储 | 配置管理、PV/PVC、StorageClass | +| [第5章](./05-helm.md) | Helm 包管理 | Chart、Release、values模板化 | +| [第6章](./practice/README.md) | 综合部署实战 | Helm Chart + 一键部署脚本 | +| [第7章](./07-scheduling-autoscaling.md) | 调度与弹性伸缩 | nodeAffinity、podAffinity/AntiAffinity、Taint/Toleration、HPA/VPA | +| [第8章](./08-security.md) | 安全与权限 | RBAC、NetworkPolicy、SecurityContext、ResourceQuota | +| [第9章](./09-operations.md) | 运维与故障排查 | kubectl 高级用法、Events、日志体系、排障流程 | +| [第10章](./10-interview-questions.md) | 面试高频题 | 27道必考题 + 答案模板 + 模拟面试流程 | + +## 🎯 学习路径 + +``` +第1天:第1章 + 第2章(架构 + Pod) → 理解 K8S 是什么 +第2天:第3章 + 第4章(网络 + 存储) → 掌握核心资源 +第3天:第5章 + 第6章实战(Helm + 实操部署) → 学会工具化部署 +第4天:第7章 + 第8章(调度 + 安全) → 掌握进阶能力 +第5天:第9章 + 第10章(运维 + 面试题) → 冲刺面试 +``` + +## 📌 章节关系图 + +```mermaid +flowchart LR + subgraph Basic["基础篇(第1-4章)"] + B1["1. 架构"] + B2["2. Pod/工作负载"] + B3["3. Service/网络"] + B4["4. 配置/存储"] + end + + subgraph Tools["工具篇(第5-6章)"] + T1["5. Helm 包管理"] + T2["6. 综合实战部署"] + end + + subgraph Advanced["进阶篇(第7-10章)"] + A1["7. 调度与弹性伸缩"] + A2["8. 安全与权限"] + A3["9. 运维与故障排查"] + A4["10. 面试高频题"] + end + + Basic -->|"→"| Tools + Tools -->|"→"| Advanced +``` + +> **学习路径图** + +## 🔧 环境准备 + +```bash +# 设置集群连接 +export KUBECONFIG=~/.kube/config.yaml + +# 创建训练 namespace +kubectl create namespace k8s-training + +# 练习完清理 +kubectl delete namespace k8s-training +``` + +## ⚠️ 重要提醒 + +**Sealos 是付费集群!** 所有实操练习完成后务必清理资源。 + +- 每章练习完后执行 `kubectl delete namespace k8s-training` +- 实战项目完成后执行 `./practice/cleanup.sh` + +## 📚 面试能力自测 + +学习完成后,检查以下问题是否都能回答: + +### 基础必会(JD:能使用 K8S 进行部署) + +- [ ] 能画出 K8S 架构图(Master + Worker) +- [ ] 能说出 Pod 创建的完整流程 +- [ ] 能手写 Deployment YAML(含探针和资源限制) +- [ ] 能解释 4 种 Service 类型的区别和使用场景 +- [ ] 能说出 ConfigMap 和 Secret 的区别 +- [ ] 能说出 requests 和 limits 的区别 +- [ ] 能说出 liveness/readiness/startup 探针的区别 +- [ ] 能解释 RollingUpdate 策略 +- [ ] 能使用 Helm 部署和回滚应用 +- [ ] 能排查 Pod 常见异常(Pending/CrashLoopBackOff) + +### 进阶加分(面试脱颖而出) + +- [ ] 能解释 nodeAffinity 和 podAffinity 的区别和使用场景 +- [ ] 能解释 Taint/Toleration 的工作机制和三种 effect +- [ ] 能配置 HPA 实现自动扩缩 +- [ ] 能说出 RBAC 的四个核心对象 +- [ ] 能解释 NetworkPolicy 的白名单安全策略 +- [ ] 能说出 Pod 安全最佳实践(SecurityContext) +- [ ] 能说出三级自动伸缩体系(HPA/VPA/CA) +- [ ] 能描述完整的故障排查流程 diff --git a/docs/posts/k8s-training/practice/README.md b/docs/posts/k8s-training/practice/README.md new file mode 100644 index 0000000..cf535dc --- /dev/null +++ b/docs/posts/k8s-training/practice/README.md @@ -0,0 +1,119 @@ +--- +title: "K8S培训综合实战项目" +date: "2026-05-15" +category: "k8s" +tags: + - "k8s" + - "AI生成" +--- + +# K8S培训综合实战项目 + +## 项目概述 + +这是一个覆盖 K8S 所有核心知识点的完整部署系统,使用 Helm Chart 打包,一条命令部署。 + +## 涉及的知识点清单 + +| 知识点 | 对应文件 | 说明 | +|--------|---------|------| +| Namespace | deploy.sh | 多租户资源隔离 | +| Deployment | templates/deployment.yaml | Pod 管理、副本数、资源限制 | +| Liveness Probe | templates/deployment.yaml | 容器存活检测 | +| Readiness Probe | templates/deployment.yaml | 流量就绪检测 | +| RollingUpdate | templates/deployment.yaml | 零停机更新策略 | +| Service (ClusterIP) | templates/service.yaml | 内部负载均衡 | +| Endpoints | 自动生成 | Service 与 Pod 的关联 | +| ConfigMap | templates/configmap.yaml | 非敏感配置管理 | +| Secret | templates/secret.yaml | 敏感信息管理(base64) | +| Ingress | templates/ingress.yaml | HTTP 路由(可选) | +| PVC | templates/pvc.yaml | 持久化存储(可选) | +| Helm Chart | Chart.yaml + values.yaml | 包管理、模板化 | +| Helm Install/Upgrade/Rollback | deploy.sh | 生命周期管理 | +| DNS 服务发现 | deploy.sh 验证步骤 | CoreDNS 自动解析 | +| 资源限制 (requests/limits) | values.yaml | Pod 调度和运行时资源控制 | + +## 使用方法 + +### 1. 部署 + +```bash +cd k8s-training/practice +chmod +x deploy.sh cleanup.sh +./deploy.sh +``` + +### 2. 手动操作练习 + +```bash +export KUBECONFIG=~/.kube/config +NS=k8s-training + +# 查看 Pod +kubectl get pods -n $NS + +# 查看日志 +kubectl logs -f deployment/training-app -n $NS + +# 端口转发测试 +kubectl port-forward svc/training-app 8080:80 -n $NS +# 另一个终端访问:curl http://localhost:8080 + +# 扩缩容 +kubectl scale deployment training-app --replicas=5 -n $NS + +# 进入 Pod +kubectl exec -it deployment/training-app -n $NS -- sh + +# 查看 ConfigMap +kubectl get configmap training-app-config -o yaml -n $NS + +# 查看 Secret(解码) +kubectl get secret training-app-secret -o jsonpath='{.data.DB_PASSWORD}' -n $NS | base64 -d +``` + +### 3. Helm 操作练习 + +```bash +# 查看渲染后的 YAML +helm template training-app ./helm-chart -n $NS + +# 升级(修改值) +helm upgrade training-app ./helm-chart -n $NS --set replicaCount=5 + +# 查看历史 +helm history training-app -n $NS + +# 回滚 +helm rollback training-app 1 -n $NS + +# 查看 Release 状态 +helm status training-app -n $NS +``` + +### 4. 清理(⚠️ 必做!付费集群!) + +```bash +./cleanup.sh +``` + +## 目录结构 + +``` +practice/ +├── helm-chart/ +│ ├── Chart.yaml # Chart 元数据 +│ ├── values.yaml # 默认配置(副本数、镜像、资源限制等) +│ └── templates/ +│ ├── _helpers.tpl # 模板辅助函数 +│ ├── deployment.yaml # Deployment + 探针 + 资源限制 +│ ├── service.yaml # ClusterIP Service +│ ├── configmap.yaml # ConfigMap +│ ├── secret.yaml # Secret +│ ├── ingress.yaml # Ingress(可选) +│ ├── pvc.yaml # PVC(可选) +│ └── NOTES.txt # 安装后提示 +├── deploy.sh # 一键部署 & 验证脚本 +├── cleanup.sh # 清理脚本 +└── README.md # 本文件 +``` diff --git a/docs/posts/k8s-training/practice/cleanup.sh b/docs/posts/k8s-training/practice/cleanup.sh new file mode 100755 index 0000000..1fe1365 --- /dev/null +++ b/docs/posts/k8s-training/practice/cleanup.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# =========================================== +# K8S培训综合实战 - 清理脚本 +# =========================================== + +set -e + +KUBECONFIG_PATH="${KUBECONFIG:-$HOME/.kube/config}" +NS="k8s-training" +RELEASE_NAME="training-app" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +ok() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +export KUBECONFIG=$KUBECONFIG_PATH + +echo "================================================" +echo " 🔧 清理 K8S 培训资源" +echo "================================================" +echo "" + +# 1. 卸载 Helm Release +info "卸载 Helm Release: $RELEASE_NAME" +if helm list -n $NS 2>/dev/null | grep -q $RELEASE_NAME; then + helm uninstall $RELEASE_NAME -n $NS + ok "Release 已卸载" +else + warn "Release 不存在,跳过" +fi + +# 2. 删除 Namespace(会删除该 NS 下所有资源) +info "删除 Namespace: $NS" +if kubectl get ns $NS > /dev/null 2>&1; then + kubectl delete namespace $NS --timeout=60s + ok "Namespace 已删除" +else + warn "Namespace 不存在,跳过" +fi + +# 3. 确认清理干净 +echo "" +info "验证清理结果:" +REMAINING=$(kubectl get all -n $NS 2>&1 || true) +if echo "$REMAINING" | grep -q "No resources"; then + ok "所有资源已清理干净 ✅" +elif echo "$REMAINING" | grep -qi "not found"; then + ok "Namespace 已完全删除 ✅" +else + warn "可能还有残留资源,请手动检查:" + echo "$REMAINING" +fi + +echo "" +echo "================================================" +ok "清理完成!" +echo "================================================" diff --git a/docs/posts/k8s-training/practice/deploy.sh b/docs/posts/k8s-training/practice/deploy.sh new file mode 100755 index 0000000..c75fbc0 --- /dev/null +++ b/docs/posts/k8s-training/practice/deploy.sh @@ -0,0 +1,289 @@ +#!/bin/bash +# =========================================== +# K8S培训综合实战 - 一键部署 & 验证脚本 +# 覆盖所有知识点: +# 1. Namespace 隔离 +# 2. Deployment(Pod + 资源限制 + 探针 + 滚动更新) +# 3. Service(ClusterIP + DNS服务发现) +# 4. ConfigMap(非敏感配置) +# 5. Secret(敏感信息) +# 6. Helm 包管理(安装/升级/回滚) +# =========================================== + +set -e + +KUBECONFIG_PATH="${KUBECONFIG:-$HOME/.kube/config}" +NS="k8s-training" +RELEASE_NAME="training-app" +CHART_DIR="./helm-chart" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +info() { echo -e "${BLUE}[INFO]${NC} $1"; } +ok() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +fail() { echo -e "${RED}[FAIL]${NC} $1"; } +step() { echo -e "\n${CYAN}📌 $1${NC}"; } + +echo "================================================" +echo " K8S培训综合实战 - 部署 & 验证" +echo "================================================" +echo "" + +# 检查 KUBECONFIG +info "检查集群连接..." +export KUBECONFIG=$KUBECONFIG_PATH +if kubectl cluster-info > /dev/null 2>&1; then + ok "集群连接成功" +else + warn "无法获取集群信息,但继续尝试..." +fi + +# ========================================== +# 步骤0:权限检查 +# ========================================== +step "步骤0:权限检查" +echo " 知识点:RBAC(基于角色的访问控制)" +echo "" + +CAN_CREATE_NS=$(kubectl auth can-i create namespaces 2>/dev/null || echo "no") +CAN_CREATE_POD=$(kubectl auth can-i create pods -n $NS 2>/dev/null || echo "no") +CAN_CREATE_DEPLOY=$(kubectl auth can-i create deployments -n $NS 2>/dev/null || echo "no") + +if [ "$CAN_CREATE_NS" = "no" ] && [ "$CAN_CREATE_POD" = "no" ]; then + warn "当前用户没有创建 namespace 和 pod 的权限" + echo "" + echo " 当前用户:$(kubectl config view --minify 2>/dev/null | grep 'namespace\|user' || echo 'unknown')" + echo "" + info "解决方案:" + echo " 1. 使用完整的 kubeconfig 文件(含管理员凭证)" + echo " 2. 联系集群管理员授予相应权限" + echo " 3. 使用 minikube/kind 搭建本地练习集群" + echo "" + info "本地练习集群推荐:" + echo " # macOS" + echo " brew install kind" + echo " kind create cluster --name k8s-training" + echo " export KUBECONFIG=\$(kind get kubeconfig-path --name k8s-training)" + echo "" + echo " # 或使用 minikube" + echo " brew install minikube" + echo " minikube start --cpus 2 --memory 4096" + echo "" + echo " # 或使用 Docker Desktop 自带的 K8S" + echo " # 设置 → Kubernetes → Enable Kubernetes" + echo "" + + # 即使没有实际集群权限,仍然展示学习内容 + info "虽然无法实际部署,但可以查看所有模板和配置:" + echo "" + info "📋 Helm Chart 模板渲染(知识点:helm template):" + if [ -d "$CHART_DIR" ]; then + helm template $RELEASE_NAME $CHART_DIR --namespace $NS 2>/dev/null || echo " (需要安装 helm)" + fi + echo "" + ok "培训材料已准备就绪,建议在本地 K8S 集群上实操" + exit 0 +fi + +ok "权限检查通过,继续部署..." + +# ========================================== +# 步骤1:创建 Namespace +# 知识点:多租户隔离 +# ========================================== +step "步骤1:创建 Namespace" +echo " 知识点:Namespace 用于多租户隔离、资源分组" +echo "" + +if kubectl get ns $NS > /dev/null 2>&1; then + warn "Namespace $NS 已存在,跳过创建" +else + kubectl create namespace $NS + ok "Namespace $NS 创建成功" +fi + +# ========================================== +# 步骤2:使用 Helm 安装应用 +# 知识点:Helm Chart 一键部署 +# ========================================== +step "步骤2:Helm 安装应用" +echo " 知识点:Helm = K8S 包管理器,一条命令部署所有资源" +echo " 将创建:Deployment + Service + ConfigMap + Secret" +echo "" + +cd "$(dirname "$0")" + +# 先渲染模板看看(知识点:helm template) +info "渲染 Helm 模板..." +helm template $RELEASE_NAME $CHART_DIR --namespace $NS > /tmp/training-rendered.yaml +ok "模板渲染成功,查看渲染结果: cat /tmp/training-rendered.yaml" + +# 安装 +if helm list -n $NS 2>/dev/null | grep -q $RELEASE_NAME; then + warn "Release $RELEASE_NAME 已存在,执行升级..." + helm upgrade $RELEASE_NAME $CHART_DIR -n $NS + ok "Release 升级成功" +else + helm install $RELEASE_NAME $CHART_DIR -n $NS + ok "Release $RELEASE_NAME 安装成功" +fi + +# 等待 Pod 就绪 +info "等待 Pod 就绪..." +kubectl wait --for=condition=ready pod \ + -l app.kubernetes.io/instance=$RELEASE_NAME \ + -n $NS --timeout=120s 2>/dev/null || warn "Pod 等待超时,继续验证..." + +# ========================================== +# 步骤3:验证 Deployment +# ========================================== +step "步骤3:验证 Deployment" +echo " 知识点:Pod 副本数、资源限制、探针" +echo "" + +PODS=$(kubectl get pods -n $NS -l app.kubernetes.io/instance=$RELEASE_NAME -o name 2>/dev/null || true) +if [ -z "$PODS" ]; then + warn "没有找到 Pod,可能部署失败" +else + POD_COUNT=$(echo "$PODS" | wc -l | tr -d ' ') + info "Pod 数量: $POD_COUNT(期望: 2)" + [ "$POD_COUNT" -eq 2 ] && ok "副本数正确" || warn "副本数不完全匹配" + + echo "" + info "Pod 状态:" + kubectl get pods -n $NS -l app.kubernetes.io/instance=$RELEASE_NAME + + echo "" + FIRST_POD=$(echo "$PODS" | head -1) + info "资源配置(知识点:requests 和 limits):" + kubectl get $FIRST_POD -n $NS -o jsonpath='CPU Request: {.spec.containers[0].resources.requests.cpu} +CPU Limit: {.spec.containers[0].resources.limits.cpu} +Mem Request: {.spec.containers[0].resources.requests.memory} +Mem Limit: {.spec.containers[0].resources.limits.memory} +' + echo "" +fi + +# ========================================== +# 步骤4:验证 Service +# ========================================== +step "步骤4:验证 Service" +echo " 知识点:ClusterIP Service、标签选择器、Endpoints" +echo "" + +info "Service 信息:" +kubectl get svc -n $NS -l app.kubernetes.io/instance=$RELEASE_NAME 2>/dev/null || warn "未找到 Service" + +echo "" +info "Endpoints(Service 如何关联 Pod):" +kubectl get endpoints -n $NS -l app.kubernetes.io/instance=$RELEASE_NAME 2>/dev/null || warn "未找到 Endpoints" + +# ========================================== +# 步骤5:验证 ConfigMap +# ========================================== +step "步骤5:验证 ConfigMap" +echo " 知识点:非敏感配置外部化、配置注入" +echo "" + +info "ConfigMap 内容:" +kubectl get configmap ${RELEASE_NAME}-config -n $NS -o yaml 2>/dev/null | grep -A 20 "^data:" || \ + kubectl get configmap -n $NS -o yaml 2>/dev/null || warn "未找到 ConfigMap" + +# ========================================== +# 步骤6:验证 Secret +# ========================================== +step "步骤6:验证 Secret" +echo " 知识点:敏感信息管理(base64编码,非加密!)" +echo "" + +info "Secret keys(隐藏值):" +kubectl get secret ${RELEASE_NAME}-secret -n $NS -o jsonpath='{.data}' 2>/dev/null | python3 -c " +import sys, json +data = json.load(sys.stdin) +for k in data: + print(f' {k}: [HIDDEN - base64 encoded]') +" 2>/dev/null || kubectl get secret ${RELEASE_NAME}-secret -n $NS 2>/dev/null || warn "未找到 Secret" + +info "解码验证(知识点:base64 不是加密):" +DB_HOST=$(kubectl get secret ${RELEASE_NAME}-secret -n $NS -o jsonpath='{.data.DB_HOST}' 2>/dev/null || true) +if [ -n "$DB_HOST" ]; then + echo -n " DB_HOST 解码: " && echo "$DB_HOST" | base64 -d 2>/dev/null && echo + DB_PASS=$(kubectl get secret ${RELEASE_NAME}-secret -n $NS -o jsonpath='{.data.DB_PASSWORD}' 2>/dev/null) + echo -n " DB_PASSWORD 解码: " && echo "$DB_PASS" | base64 -d 2>/dev/null && echo +fi +warn "⚠️ base64 只是编码不是加密!生产环境需要 etcd 加密或使用 Vault" + +# ========================================== +# 步骤7:DNS 服务发现测试 +# ========================================== +step "步骤7:DNS 服务发现测试" +echo " 知识点:CoreDNS 自动为 Service 创建 DNS 记录" +echo "" + +info "创建临时 Pod 测试 DNS 解析..." +kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -n $NS -- \ + sh -c "nslookup ${RELEASE_NAME}.${NS}.svc.cluster.local 2>/dev/null || echo 'DNS test completed'" 2>/dev/null || \ + warn "DNS 测试跳过(可能权限不足)" + +# ========================================== +# 步骤8:滚动更新 & 回滚演示 +# ========================================== +step "步骤8:滚动更新 & 回滚" +echo " 知识点:RollingUpdate 策略、maxSurge、maxUnavailable" +echo "" + +info "执行滚动更新(修改镜像版本 nginx:1.25 → nginx:1.26)..." +# 记录升级前的版本号,回滚时用它,避免写死 revision 1 +CURRENT_REVISION=$(helm history $RELEASE_NAME -n $NS 2>/dev/null | tail -1 | awk '{print $1}') +helm upgrade $RELEASE_NAME $CHART_DIR -n $NS --set image.tag=1.26 +info "等待更新完成..." +sleep 10 +helm history $RELEASE_NAME -n $NS + +echo "" +info "回滚到上一版本(revision $CURRENT_REVISION)..." +helm rollback $RELEASE_NAME $CURRENT_REVISION -n $NS +sleep 10 +ok "回滚完成" + +echo "" +info "版本历史:" +helm history $RELEASE_NAME -n $NS + +# ========================================== +# 验证总结 +# ========================================== +echo "" +echo "================================================" +echo " ✅ 验证总结" +echo "================================================" +echo "" +echo " 已验证的知识点:" +echo " ✅ Namespace - 多租户资源隔离" +echo " ✅ Deployment - Pod 管理、副本数、资源限制" +echo " ✅ Liveness Probe - 存活探针" +echo " ✅ Readiness Probe - 就绪探针" +echo " ✅ Service - 内部负载均衡 (ClusterIP)" +echo " ✅ Endpoints - Service 与 Pod 的关联" +echo " ✅ ConfigMap - 非敏感配置管理" +echo " ✅ Secret - 敏感信息管理(base64编码)" +echo " ✅ DNS 服务发现 - CoreDNS 自动解析" +echo " ✅ Helm Install - 一键部署" +echo " ✅ Helm Upgrade - 滚动更新" +echo " ✅ Helm Rollback - 版本回滚" +echo " ✅ Helm History - 版本历史" +echo " ✅ RollingUpdate - 零停机更新策略" +echo "" +echo "================================================" + +echo "清理命令:" +echo " cd $(pwd)" +echo " ./cleanup.sh" +echo "" diff --git a/docs/posts/k8s-training/practice/helm-chart/Chart.yaml b/docs/posts/k8s-training/practice/helm-chart/Chart.yaml new file mode 100644 index 0000000..bfdd700 --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: training-app +description: K8S培训综合实战 - 覆盖所有知识点的完整部署系统 +type: application +version: 0.1.0 +appVersion: "1.0.0" +keywords: + - k8s-training + - practice +maintainers: + - name: devops-team diff --git a/docs/posts/k8s-training/practice/helm-chart/templates/NOTES.txt b/docs/posts/k8s-training/practice/helm-chart/templates/NOTES.txt new file mode 100644 index 0000000..58cfaea --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/templates/NOTES.txt @@ -0,0 +1,23 @@ +🎉 训练应用部署成功! + +应用信息: + Release名称: {{ .Release.Name }} + Namespace: {{ .Release.Namespace }} + 副本数: {{ .Values.replicaCount }} + 镜像: {{ .Values.image.repository }}:{{ .Values.image.tag }} + +访问方式: + 集群内部:http://{{ include "training-app.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} + +{{- if .Values.ingress.enabled }} + 外部访问:http://{{ .Values.ingress.host }} +{{- end }} + +常用命令: + 查看Pod: kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} + 查看日志: kubectl logs -f deployment/{{ include "training-app.fullname" . }} -n {{ .Release.Namespace }} + 端口转发: kubectl port-forward svc/{{ include "training-app.fullname" . }} 8080:80 -n {{ .Release.Namespace }} + 查看配置: kubectl get configmap {{ include "training-app.fullname" . }}-config -o yaml -n {{ .Release.Namespace }} + 查看密钥: kubectl get secret {{ include "training-app.fullname" . }}-secret -o yaml -n {{ .Release.Namespace }} + +⚠️ 记得练习完清理资源:helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }} diff --git a/docs/posts/k8s-training/practice/helm-chart/templates/_helpers.tpl b/docs/posts/k8s-training/practice/helm-chart/templates/_helpers.tpl new file mode 100644 index 0000000..28e50f6 --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/templates/_helpers.tpl @@ -0,0 +1,29 @@ +{{/* +通用标签 +*/}} +{{- define "training-app.labels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +{{- end }} + +{{/* +选择器标签 +*/}} +{{- define "training-app.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +完整名称 +*/}} +{{- define "training-app.fullname" -}} +{{- if contains .Chart.Name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} diff --git a/docs/posts/k8s-training/practice/helm-chart/templates/configmap.yaml b/docs/posts/k8s-training/practice/helm-chart/templates/configmap.yaml new file mode 100644 index 0000000..0ddd61d --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/templates/configmap.yaml @@ -0,0 +1,14 @@ +# =========================================== +# ConfigMap 模板 +# 知识点:非敏感配置外部化、配置与代码分离 +# =========================================== +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "training-app.fullname" . }}-config + labels: + {{- include "training-app.labels" . | nindent 4 }} +data: +{{- range $key, $val := .Values.config }} + {{ $key }}: {{ $val | quote }} +{{- end }} diff --git a/docs/posts/k8s-training/practice/helm-chart/templates/deployment.yaml b/docs/posts/k8s-training/practice/helm-chart/templates/deployment.yaml new file mode 100644 index 0000000..0f50349 --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/templates/deployment.yaml @@ -0,0 +1,74 @@ +# =========================================== +# Deployment 模板 +# 知识点:Pod、副本、资源限制、探针、滚动更新 +# =========================================== +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "training-app.fullname" . }} + labels: + {{- include "training-app.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + {{- with .Values.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "training-app.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "training-app.selectorLabels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + # ========== 知识点:环境变量注入 ========== + # ConfigMap 注入(非敏感配置) + env: + {{- range $key, $val := .Values.config }} + - name: {{ $key }} + valueFrom: + configMapKeyRef: + name: {{ include "training-app.fullname" $ }}-config + key: {{ $key }} + {{- end }} + # Secret 注入(敏感信息) + {{- range $key, $val := .Values.secrets }} + - name: {{ $key }} + valueFrom: + secretKeyRef: + name: {{ include "training-app.fullname" $ }}-secret + key: {{ $key }} + {{- end }} + # ========== 知识点:资源限制 ========== + resources: + {{- toYaml .Values.resources | nindent 10 }} + # ========== 知识点:存活探针 ========== + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 10 }} + # ========== 知识点:就绪探针 ========== + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 10 }} + # ========== 知识点:持久化存储挂载 ========== + {{- if .Values.persistence.enabled }} + volumeMounts: + - name: data + mountPath: /app/data + {{- end }} + {{- if .Values.persistence.enabled }} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "training-app.fullname" . }}-pvc + {{- end }} diff --git a/docs/posts/k8s-training/practice/helm-chart/templates/ingress.yaml b/docs/posts/k8s-training/practice/helm-chart/templates/ingress.yaml new file mode 100644 index 0000000..d90b69d --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/templates/ingress.yaml @@ -0,0 +1,27 @@ +# =========================================== +# Ingress 模板 +# 知识点:7层HTTP路由、域名路由、TLS +# =========================================== +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "training-app.fullname" . }} + labels: + {{- include "training-app.labels" . | nindent 4 }} + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: {{ .Values.ingress.className }} + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: {{ .Values.ingress.path }} + pathType: {{ .Values.ingress.pathType }} + backend: + service: + name: {{ include "training-app.fullname" . }} + port: + number: {{ .Values.service.port }} +{{- end }} diff --git a/docs/posts/k8s-training/practice/helm-chart/templates/pvc.yaml b/docs/posts/k8s-training/practice/helm-chart/templates/pvc.yaml new file mode 100644 index 0000000..325a9e3 --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/templates/pvc.yaml @@ -0,0 +1,21 @@ +# =========================================== +# PVC 模板 +# 知识点:持久化存储、动态供应 +# =========================================== +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "training-app.fullname" . }}-pvc + labels: + {{- include "training-app.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- if .Values.persistence.storageClassName }} + storageClassName: {{ .Values.persistence.storageClassName }} + {{- end }} +{{- end }} diff --git a/docs/posts/k8s-training/practice/helm-chart/templates/secret.yaml b/docs/posts/k8s-training/practice/helm-chart/templates/secret.yaml new file mode 100644 index 0000000..6a693f2 --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/templates/secret.yaml @@ -0,0 +1,16 @@ +# =========================================== +# Secret 模板 +# 知识点:敏感信息管理、base64 编码 +# 注意:stringData 中的值 K8S 会自动 base64 编码 +# =========================================== +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "training-app.fullname" . }}-secret + labels: + {{- include "training-app.labels" . | nindent 4 }} +type: Opaque +stringData: +{{- range $key, $val := .Values.secrets }} + {{ $key }}: {{ $val | quote }} +{{- end }} diff --git a/docs/posts/k8s-training/practice/helm-chart/templates/service.yaml b/docs/posts/k8s-training/practice/helm-chart/templates/service.yaml new file mode 100644 index 0000000..1d84c7d --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/templates/service.yaml @@ -0,0 +1,19 @@ +# =========================================== +# Service 模板 +# 知识点:ClusterIP Service、标签选择器、端口映射 +# =========================================== +apiVersion: v1 +kind: Service +metadata: + name: {{ include "training-app.fullname" . }} + labels: + {{- include "training-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "training-app.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP diff --git a/docs/posts/k8s-training/practice/helm-chart/values.yaml b/docs/posts/k8s-training/practice/helm-chart/values.yaml new file mode 100644 index 0000000..a17f3db --- /dev/null +++ b/docs/posts/k8s-training/practice/helm-chart/values.yaml @@ -0,0 +1,87 @@ +# =========================================== +# K8S培训综合实战 - values.yaml +# 覆盖所有知识点: +# - Deployment(Pod、资源限制、探针、滚动更新) +# - Service(ClusterIP) +# - ConfigMap(非敏感配置) +# - Secret(敏感信息) +# - Ingress(HTTP路由) +# =========================================== + +# 全局配置 +global: + namespace: k8s-training + +# Deployment 配置 +replicaCount: 2 + +image: + repository: nginx + tag: "1.25" + pullPolicy: IfNotPresent + +# 资源限制 +resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "256Mi" + +# 滚动更新策略 +strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + +# 存活探针 +livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 5 + +# 就绪探针 +readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 3 + +# Service 配置 +service: + type: ClusterIP + port: 80 + targetPort: 80 + +# ConfigMap 配置(非敏感) +config: + APP_ENV: production + LOG_LEVEL: info + TZ: Asia/Shanghai + +# Secret 配置(敏感)- 注意:生产环境应使用外部 Secret 管理 +secrets: + DB_HOST: mysql-service + DB_PORT: "3306" + DB_USER: app_user + DB_PASSWORD: ChangeMe123! + +# Ingress 配置 +ingress: + enabled: false + className: "nginx" + host: app.example.com + path: / + pathType: Prefix + +# PVC 配置(可选持久化存储) +persistence: + enabled: false + accessMode: ReadWriteOnce + size: 1Gi + storageClassName: "" diff --git a/package-lock.json b/package-lock.json index 6e3e4b4..8681392 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,8 @@ "name": "nchj.github.io", "devDependencies": { "gray-matter": "^4.0.3", - "vitepress": "^1.6.4" + "vitepress": "^1.6.4", + "vitepress-plugin-mermaid": "^2.0.17" } }, "node_modules/@algolia/abtesting": { @@ -268,6 +269,21 @@ "node": ">= 14.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -318,6 +334,22 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, "node_modules/@docsearch/css": { "version": "3.8.2", "resolved": "https://repo.huaweicloud.com/repository/npm/@docsearch/css/-/css-3.8.2.tgz", @@ -777,6 +809,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@iconify/utils": { + "version": "3.1.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/@iconify/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://repo.huaweicloud.com/repository/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -784,6 +829,42 @@ "dev": true, "license": "MIT" }, + "node_modules/@mermaid-js/mermaid-mindmap": { + "version": "9.3.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/@mermaid-js/mermaid-mindmap/-/mermaid-mindmap-9.3.0.tgz", + "integrity": "sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.0.0", + "khroma": "^2.0.0", + "non-layered-tidy-tree-layout": "^2.0.2" + } + }, + "node_modules/@mermaid-js/mermaid-mindmap/node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/@mermaid-js/parser/-/parser-1.1.1.tgz", + "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@chevrotain/types": "~11.1.1" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://repo.huaweicloud.com/repository/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -1260,6 +1341,321 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://repo.huaweicloud.com/repository/npm/@types/estree/-/estree-1.0.8.tgz", @@ -1267,6 +1663,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://repo.huaweicloud.com/repository/npm/@types/hast/-/hast-3.0.4.tgz", @@ -1312,6 +1716,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://repo.huaweicloud.com/repository/npm/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://repo.huaweicloud.com/repository/npm/@types/unist/-/unist-3.0.3.tgz", @@ -1333,6 +1746,18 @@ "dev": true, "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", "resolved": "https://repo.huaweicloud.com/repository/npm/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", @@ -1688,6 +2113,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://repo.huaweicloud.com/repository/npm/copy-anything/-/copy-anything-4.0.5.tgz", @@ -1704,6 +2139,16 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dev": true, + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://repo.huaweicloud.com/repository/npm/csstype/-/csstype-3.2.3.tgz", @@ -1711,68 +2156,656 @@ "dev": true, "license": "MIT" }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://repo.huaweicloud.com/repository/npm/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/cytoscape": { + "version": "3.33.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/cytoscape/-/cytoscape-3.33.3.tgz", + "integrity": "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.10" } }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", "dev": true, "license": "MIT", "dependencies": { - "dequal": "^2.0.0" + "cose-base": "^1.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/emoji-regex-xs": { - "version": "1.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://repo.huaweicloud.com/repository/npm/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" }, "engines": { "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://repo.huaweicloud.com/repository/npm/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://repo.huaweicloud.com/repository/npm/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dompurify": { + "version": "3.4.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/dompurify/-/dompurify-3.4.3.tgz", + "integrity": "sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://repo.huaweicloud.com/repository/npm/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", @@ -1869,6 +2902,14 @@ "node": ">=6.0" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://repo.huaweicloud.com/repository/npm/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -1925,6 +2966,41 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://repo.huaweicloud.com/repository/npm/is-extendable/-/is-extendable-0.1.1.tgz", @@ -1962,6 +3038,41 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/katex": { + "version": "0.16.46", + "resolved": "https://repo.huaweicloud.com/repository/npm/katex/-/katex-0.16.46.tgz", + "integrity": "sha512-WHy4Coo+bGZyH7NwJKHkS04YFsFcarWbAEOAC3EMndzdN6VSZqklLLIgfxzyaW9jDoeGYJX9SWbJPKpecox0Uw==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "peer": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "dev": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://repo.huaweicloud.com/repository/npm/kind-of/-/kind-of-6.0.3.tgz", @@ -1972,6 +3083,21 @@ "node": ">=0.10.0" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://repo.huaweicloud.com/repository/npm/magic-string/-/magic-string-0.30.21.tgz", @@ -1989,6 +3115,20 @@ "dev": true, "license": "MIT" }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", "resolved": "https://repo.huaweicloud.com/repository/npm/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", @@ -2011,6 +3151,37 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mermaid": { + "version": "11.15.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/mermaid/-/mermaid-11.15.0.tgz", + "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "es-toolkit": "^1.45.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://repo.huaweicloud.com/repository/npm/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -2138,6 +3309,14 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/oniguruma-to-es": { "version": "3.1.1", "resolved": "https://repo.huaweicloud.com/repository/npm/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", @@ -2150,6 +3329,22 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://repo.huaweicloud.com/repository/npm/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -2164,6 +3359,26 @@ "dev": true, "license": "ISC" }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://repo.huaweicloud.com/repository/npm/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://repo.huaweicloud.com/repository/npm/postcss/-/postcss-8.5.8.tgz", @@ -2249,6 +3464,13 @@ "dev": true, "license": "MIT" }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://repo.huaweicloud.com/repository/npm/rollup/-/rollup-4.60.1.tgz", @@ -2294,6 +3516,34 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://repo.huaweicloud.com/repository/npm/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://repo.huaweicloud.com/repository/npm/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/search-insights": { "version": "2.17.3", "resolved": "https://repo.huaweicloud.com/repository/npm/search-insights/-/search-insights-2.17.3.tgz", @@ -2396,6 +3646,14 @@ "node": ">=0.10.0" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://repo.huaweicloud.com/repository/npm/superjson/-/superjson-2.2.6.tgz", @@ -2416,6 +3674,17 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://repo.huaweicloud.com/repository/npm/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://repo.huaweicloud.com/repository/npm/trim-lines/-/trim-lines-3.0.1.tgz", @@ -2427,6 +3696,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.10" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://repo.huaweicloud.com/repository/npm/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -2500,6 +3780,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://repo.huaweicloud.com/repository/npm/vfile/-/vfile-6.0.3.tgz", @@ -2632,6 +3927,20 @@ } } }, + "node_modules/vitepress-plugin-mermaid": { + "version": "2.0.17", + "resolved": "https://repo.huaweicloud.com/repository/npm/vitepress-plugin-mermaid/-/vitepress-plugin-mermaid-2.0.17.tgz", + "integrity": "sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@mermaid-js/mermaid-mindmap": "^9.3.0" + }, + "peerDependencies": { + "mermaid": "10 || 11", + "vitepress": "^1.0.0 || ^1.0.0-alpha" + } + }, "node_modules/vue": { "version": "3.5.32", "resolved": "https://repo.huaweicloud.com/repository/npm/vue/-/vue-3.5.32.tgz", diff --git a/package.json b/package.json index 46adedd..66e42ed 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "gray-matter": "^4.0.3", - "vitepress": "^1.6.4" + "vitepress": "^1.6.4", + "vitepress-plugin-mermaid": "^2.0.17" } }