Subsections of K8s
K8s的理解
一、核心定位:云时代的操作系统
我对 K8s 最根本的理解是:它正在成为数据中心/云环境的“操作系统”。
- 传统操作系统(如 Windows、Linux):管理的是单台计算机的硬件资源(CPU、内存、硬盘、网络),并为应用程序(进程)提供运行环境。
- Kubernetes:管理的是一个集群(由多台计算机组成)的资源,并将这些物理机/虚拟机抽象成一个巨大的“资源池”。它在这个池子上调度和运行的不再是简单的进程,而是容器化了的应用程序。
所以,你可以把 K8s 看作是一个分布式的、面向云原生应用的操作系统。
二、要解决的核心问题:从“动物园”到“牧场”
在 K8s 出现之前,微服务和容器化架构带来了新的挑战:
- 编排混乱:我有成百上千个容器,应该在哪台机器上启动?如何知道它们是否健康?挂了怎么办?如何扩容缩容?
- 网络复杂:容器之间如何发现和通信?如何实现负载均衡?
- 存储管理:有状态应用的数据如何持久化?容器漂移后数据怎么跟走?
- 部署麻烦:如何实现蓝绿部署、金丝雀发布?如何回滚?
这个时期被称为“集装箱革命”后的“编排战争”时期,各种工具(如 Docker Swarm, Mesos, Nomad)就像是一个混乱的“动物园”。
K8s 的诞生(源于 Google 内部系统 Borg 的经验)就是为了系统地解决这些问题,它将混乱的“动物园”管理成了一个井然有序的“牧场”。它的核心能力可以概括为:声明式 API 和控制器模式。
三、核心架构与工作模型:大脑与肢体
K8s 集群主要由控制平面 和工作节点 组成。
控制平面:集群的大脑
- kube-apiserver:整个系统的唯一入口,所有组件都必须通过它来操作集群状态。它是“前台总机”。
- etcd:一个高可用的键值数据库,持久化存储集群的所有状态数据。它是“集群的记忆中心”。
- kube-scheduler:负责调度,决定 Pod 应该在哪个节点上运行。它是“人力资源部”。
- kube-controller-manager:运行着各种控制器,不断检查当前状态是否与期望状态一致,并努力驱使其一致。例如,节点控制器、副本控制器等。它是“自动化的管理团队”。
工作节点:干活的肢体
- kubelet:节点上的“监工”,负责与控制平面通信,管理本节点上 Pod 的生命周期,确保容器健康运行。
- kube-proxy:负责节点上的网络规则,实现 Service 的负载均衡和网络代理。
- 容器运行时:如 containerd 或 CRI-O,负责真正拉取镜像和运行容器。
工作模型的核心:声明式 API 与控制器模式
- 你向
kube-apiserver 提交一个 YAML/JSON 文件,声明你期望的应用状态(例如:我要运行 3 个 Nginx 实例)。 etcd 记录下这个期望状态。- 各种控制器会持续地“观察”当前状态,并与
etcd 中的期望状态进行对比。 - 如果发现不一致(例如,只有一个 Nginx 实例在运行),控制器就会主动采取行动(例如,再创建两个 Pod),直到当前状态与期望状态一致。
- 这个过程是自愈的、自动的。
四、关键对象与抽象:乐高积木
K8s 通过一系列抽象对象来建模应用,这些对象就像乐高积木:
- Pod:最小部署和管理单元。一个 Pod 可以包含一个或多个紧密关联的容器(如主容器和 Sidecar 容器),它们共享网络和存储。这是 K8s 的“原子”。
- Deployment:定义无状态应用。它管理 Pod 的多个副本(Replicas),并提供滚动更新、回滚等强大的部署策略。这是最常用的对象。
- Service:定义一组 Pod 的访问方式。Pod 是“ ephemeral ”的,IP 会变。Service 提供一个稳定的 IP 和 DNS 名称,并作为负载均衡器,将流量分发给后端的健康 Pod。它是“服务的门户”。
- ConfigMap & Secret:将配置信息和敏感数据与容器镜像解耦,实现配置的灵活管理。
- Volume:抽象了各种存储解决方案,为 Pod 提供持久化存储。
- Namespace:在物理集群内部创建多个虚拟集群,实现资源隔离和多租户管理。
- StatefulSet:用于部署有状态应用(如数据库)。它为每个 Pod 提供稳定的标识符、有序的部署和扩缩容,以及稳定的持久化存储。
- Ingress:管理集群外部访问内部服务的入口,通常提供 HTTP/HTTPS 路由、SSL 终止等功能。它是“集群的流量总入口”。
五、核心价值与优势
- 自动化运维:自动化了应用的部署、扩缩容、故障恢复(自愈)、滚动更新等,极大降低了运维成本。
- 声明式配置与不可变基础设施:通过 YAML 文件定义一切,基础设施可版本化、可追溯、可重复。这是 DevOps 和 GitOps 的基石。
- 环境一致性 & 可移植性:实现了“一次编写,随处运行”。无论是在本地开发机、测试环境,还是在公有云、混合云上,应用的行为都是一致的。
- 高可用性与弹性伸缩:轻松实现应用的多副本部署,并能根据 CPU、内存等指标或自定义指标进行自动扩缩容,从容应对流量高峰。
- 丰富的生态系统:拥有一个极其庞大和活跃的社区,提供了大量的工具和扩展(Helm, Operator, Istio等),能解决几乎所有你能想到的问题。
六、挑战与学习曲线
K8s 并非银弹,它也有自己的挑战:
- 复杂性高:概念繁多,架构复杂,学习和运维成本非常高。
- “配置”沉重:YAML 文件可能非常多,管理起来本身就是一门学问。
- 网络与存储:虽然是核心抽象,但其底层实现和理解起来依然有相当的门槛。
总结
在我看来,Kubernetes 不仅仅是一个容器编排工具,它更是一套云原生应用的管理范式。它通过一系列精妙的抽象,将复杂的分布式系统管理问题标准化、自动化和简单化。虽然入门有门槛,但它已经成为现代应用基础设施的事实标准,是任何从事后端开发、运维、架构设计的人员都必须理解和掌握的核心技术。
简单来说,K8s 让你能够像管理一台超级计算机一样,去管理一个由成千上万台机器组成的集群。
Cgroup在K8S中起什么作用
Kubernetes 深度集成 cgroup 来实现容器资源管理和隔离。以下是 cgroup 与 K8s 结合的详细方式:
1. K8s 资源模型与 cgroup 映射
1.1 资源请求和限制
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
ephemeral-storage: "2Gi"
对应 cgroup 配置:
cpu.shares = 256 (250m × 1024 / 1000)cpu.cfs_quota_us = 50000 (500m × 100000 / 1000)memory.limit_in_bytes = 134217728 (128Mi)
2. K8s cgroup 驱动
2.1 cgroupfs 驱动
# kubelet 配置
--cgroup-driver=cgroupfs
--cgroup-root=/sys/fs/cgroup
2.2 systemd 驱动(推荐)
# kubelet 配置
--cgroup-driver=systemd
--cgroup-root=/sys/fs/cgroup
3. K8s cgroup 层级结构
3.1 cgroup v1 层级
/sys/fs/cgroup/
├── cpu,cpuacct/kubepods/
│ ├── burstable/pod-uid-1/
│ │ ├── container-1/
│ │ └── container-2/
│ └── guaranteed/pod-uid-2/
│ └── container-1/
├── memory/kubepods/
└── pids/kubepods/
3.2 cgroup v2 统一层级
/sys/fs/cgroup/kubepods/
├── pod-uid-1/
│ ├── container-1/
│ └── container-2/
└── pod-uid-2/
└── container-1/
4. QoS 等级与 cgroup 配置
4.1 Guaranteed (最高优先级)
resources:
limits:
cpu: "500m"
memory: "128Mi"
requests:
cpu: "500m"
memory: "128Mi"
cgroup 配置:
cpu.shares = 512cpu.cfs_quota_us = 50000oom_score_adj = -998
4.2 Burstable (中等优先级)
resources:
requests:
cpu: "250m"
memory: "64Mi"
# limits 未设置或大于 requests
cgroup 配置:
cpu.shares = 256cpu.cfs_quota_us = -1 (无限制)oom_score_adj = 2-999
4.3 BestEffort (最低优先级)
cgroup 配置:
cpu.shares = 2memory.limit_in_bytes = 9223372036854771712 (极大值)oom_score_adj = 1000
5. 实际 cgroup 配置示例
5.1 查看 Pod 的 cgroup
# 找到 Pod 的 cgroup 路径
cat /sys/fs/cgroup/cpu/kubepods/pod-uid-1/cgroup.procs
# 查看 CPU 配置
cat /sys/fs/cgroup/cpu/kubepods/pod-uid-1/cpu.shares
cat /sys/fs/cgroup/cpu/kubepods/pod-uid-1/cpu.cfs_quota_us
# 查看内存配置
cat /sys/fs/cgroup/memory/kubepods/pod-uid-1/memory.limit_in_bytes
# 安装工具
apt-get install cgroup-tools
# 查看 cgroup 统计
cgget -g cpu:/kubepods/pod-uid-1
cgget -g memory:/kubepods/pod-uid-1
6. K8s 特性与 cgroup 集成
6.1 垂直 Pod 自动缩放 (VPA)
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Auto"
VPA 根据历史使用数据动态调整:
- 修改
resources.requests 和 resources.limits - kubelet 更新对应的 cgroup 配置
6.2 水平 Pod 自动缩放 (HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
HPA 依赖 cgroup 的 CPU 使用率统计进行决策。
6.3 资源监控
# 通过 cgroup 获取容器资源使用
cat /sys/fs/cgroup/cpu/kubepods/pod-uid-1/cpuacct.usage
cat /sys/fs/cgroup/memory/kubepods/pod-uid-1/memory.usage_in_bytes
# 使用 metrics-server 收集
kubectl top pods
kubectl top nodes
7. 节点资源管理
7.1 系统预留资源
# kubelet 配置
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
systemReserved:
cpu: "100m"
memory: "256Mi"
ephemeral-storage: "1Gi"
kubeReserved:
cpu: "200m"
memory: "512Mi"
ephemeral-storage: "2Gi"
evictionHard:
memory.available: "100Mi"
nodefs.available: "10%"
7.2 驱逐策略
当节点资源不足时,kubelet 根据 cgroup 统计:
- 监控
memory.usage_in_bytes - 监控
cpuacct.usage - 触发 Pod 驱逐
8. 故障排查和调试
8.1 检查 cgroup 配置
# 进入节点检查
docker exec -it node-shell /bin/bash
# 查看 Pod cgroup
find /sys/fs/cgroup -name "*pod-uid*" -type d
# 检查资源限制
cat /sys/fs/cgroup/memory/kubepods/pod-uid-1/memory.limit_in_bytes
cat /sys/fs/cgroup/cpu/kubepods/pod-uid-1/cpu.cfs_quota_us
8.2 监控 OOM 事件
# 查看内核日志
dmesg | grep -i "killed process"
# 查看 cgroup OOM 事件
grep "kubepods" /var/log/kern.log | grep -i oom
9. 最佳实践
9.1 合理设置资源限制
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m" # 不要设置过大
memory: "256Mi" # 避免内存浪费
9.2 使用 LimitRange
apiVersion: v1
kind: LimitRange
metadata:
name: mem-limit-range
spec:
limits:
- default:
memory: "256Mi"
defaultRequest:
memory: "128Mi"
type: Container
cgroup 是 K8s 资源管理的基石,通过精细的 cgroup 配置,K8s 实现了多租户环境下的资源隔离、公平调度和稳定性保障。
Headless VS ClusterIP
Q: headless service 和 普通的service 有什么区别? 只是有没有clusterIP?
“有没有 ClusterIP” 只是表面现象,其背后是根本不同的服务发现模式和适用场景。
核心区别:服务发现模式
- 普通 Service:提供的是 “负载均衡” 式的服务发现。
- 它抽象了一组 Pod,你访问的是这个抽象的、稳定的 VIP(ClusterIP),然后由
kube-proxy 将流量转发到后端的某个 Pod。 - 客户端不知道、也不关心具体是哪个 Pod 在处理请求。
- Headless Service:提供的是 “直接 Pod IP” 式的服务发现。
- 它不会给你一个统一的 VIP,而是直接返回后端所有 Pod 的 IP 地址。
- 客户端可以直接与任何一个 Pod 通信,并且知道它正在和哪个具体的 Pod 对话。
详细对比
| 特性 | 普通 Service | Headless Service |
|---|
clusterIP 字段 | 自动分配一个 VIP(如 10.96.123.456),或设置为 None。 | 必须设置为 None。这是定义 Headless Service 的标志。 |
| 核心功能 | 负载均衡。作为流量的代理和分发器。 | 服务发现。作为 Pod 的 DNS 记录注册器,不负责流量转发。 |
| DNS 解析结果 | 解析到 Service 的 ClusterIP。 | 解析到所有与 Selector 匹配的 Pod 的 IP 地址。 |
| 网络拓扑 | 客户端 -> ClusterIP (VIP) -> (由 kube-proxy 负载均衡) -> 某个 Pod | 客户端 -> Pod IP |
| 适用场景 | 标准的微服务、Web 前端/后端 API,任何需要负载均衡的场景。 | 有状态应用集群(如 MySQL, MongoDB, Kafka, Redis Cluster)、需要直接连接特定 Pod 的场景(如 gRPC 长连接、游戏服务器)。 |
DNS 解析行为的深入理解
这是理解两者差异的最直观方式。
假设我们有一个名为 my-app 的 Service,它选择了 3 个 Pod。
1. 普通 Service 的 DNS 解析
- 在集群内,你执行
nslookup my-app(或在 Pod 里用代码查询)。 - 返回结果:1 条 A 记录,指向 Service 的 ClusterIP。
Name: my-app
Address 1: 10.96.123.456
- 你的应用:连接到
10.96.123.456:port,剩下的交给 Kubernetes 的网络层。
2. Headless Service 的 DNS 解析
- 在集群内,你执行
nslookup my-app(注意:Service 的 clusterIP: None)。 - 返回结果:多条 A 记录,直接指向后端所有 Pod 的 IP。
Name: my-app
Address 1: 172.17.0.10
Address 2: 172.17.0.11
Address 3: 172.17.0.12
- 你的应用:会拿到这个 IP 列表,并由客户端自己决定如何连接。比如,它可以:
- 随机选一个。
- 实现自己的负载均衡逻辑。
- 需要连接所有 Pod(比如收集状态)。
与 StatefulSet 结合的“杀手级应用”
Headless Service 最经典、最强大的用途就是与 StatefulSet 配合,为有状态应用集群提供稳定的网络标识。
回顾之前的 MongoDB 例子:
- StatefulSet:
mongodb (3个副本) - Headless Service:
mongodb-service
此时,DNS 系统会创建出稳定且可预测的 DNS 记录,而不仅仅是返回 IP 列表:
这带来了巨大优势:
- 稳定的成员身份:在初始化 MongoDB 副本集时,你可以直接用这些稳定的 DNS 名称来配置成员列表。即使 Pod 重启、IP 变了,它的 DNS 名称永远不变,配置也就永远不会失效。
- 直接 Pod 间通信:在 Kafka 或 Redis Cluster 这样的系统中,节点之间需要直接通信来同步数据。它们可以使用这些稳定的 DNS 名称直接找到对方,而不需要经过一个不必要的负载均衡器。
- 主从选举与读写分离:客户端应用可以通过固定的 DNS 名称(如
mongodb-0...)直接连接到主节点执行写操作,而通过其他名称连接到从节点进行读操作。
总结
你可以这样形象地理解:
所以,“有没有 ClusterIP” 只是一个开关,这个开关背后选择的是两种截然不同的服务发现和流量治理模式。 对于需要直接寻址、有状态、集群化的应用,Headless Service 是必不可少的基石。
Creating A Pod
描述 Kubernetes 中一个 Pod 的创建过程,可以清晰地展示了 K8s 各个核心组件是如何协同工作的。
我们可以将整个过程分为两个主要阶段:控制平面的决策阶段 和 工作节点的执行阶段。
第一阶段:控制平面决策(大脑决策)
用户提交请求
- 用户使用
kubectl apply -f pod.yaml 向 kube-apiserver 提交一个 Pod 定义文件。 kubectl 会验证配置并将其转换为 JSON 格式,通过 REST API 调用发送给 kube-apiserver。
API Server 处理与验证
- kube-apiserver 接收到请求后,会进行一系列操作:
- 身份认证:验证用户身份。
- 授权:检查用户是否有权限创建 Pod。
- 准入控制:可能调用一些准入控制器来修改或验证 Pod 对象(例如,注入 Sidecar 容器、设置默认资源限制等)。
- 所有验证通过后,kube-apiserver 将 Pod 的元数据对象写入 etcd 数据库。此时,Pod 在 etcd 中的状态被标记为
Pending。 - 至此,Pod 的创建请求已被记录,但还未被调度到任何节点。
调度器决策
- kube-scheduler 作为一个控制器,通过 watch 机制持续监听 kube-apiserver,发现有一个新的 Pod 被创建且其
nodeName 为空。 - 调度器开始为这个 Pod 选择一个最合适的节点,它执行两阶段操作:
- 过滤:根据节点资源(CPU、内存)、污点、节点选择器、存储、镜像拉取等因素过滤掉不合适的节点。
- 评分:对剩下的节点进行打分(例如,考虑资源均衡、亲和性等),选择得分最高的节点。
- 做出决策后,kube-scheduler 补丁 的方式更新 kube-apiserver 中该 Pod 的定义,将其
nodeName 字段设置为选定的节点名称。 - kube-apiserver 再次将这个更新后的信息写入 etcd。
第二阶段:工作节点执行(肢体行动)
kubelet 监听到任务
- 目标节点上的 kubelet 同样通过 watch 机制监听 kube-apiserver,发现有一个 Pod 被“分配”到了自己所在的节点(即其
nodeName 与自己的节点名匹配)。 - kubelet 会从 kube-apiserver 读取完整的 Pod 定义。
kubelet 控制容器运行时
- kubelet 通过 CRI 接口调用本地的容器运行时(如 containerd、CRI-O)。
- 容器运行时负责:
- 从指定的镜像仓库拉取容器镜像(如果本地不存在)。
- 根据 Pod 定义创建和启动容器。
配置容器环境
- 在启动容器前后,kubelet 还会通过其他接口完成一系列配置:
- CNI:调用网络插件(如 Calico、Flannel)为 Pod 分配 IP 地址并配置网络。
- CSI:如果 Pod 使用了持久化存储,会调用存储插件挂载存储卷。
状态上报
- 当 Pod 中的所有容器都成功启动并运行后,kubelet 会持续监控容器的健康状态。
- 它将 Pod 的当前状态(如
Running)和 IP 地址等信息作为状态更新,上报给 kube-apiserver。 - kube-apiserver 最终将这些状态信息写入 etcd。
总结流程图
用户 kubectl -> API Server -> (写入) etcd -> Scheduler (绑定节点) -> API Server -> (更新) etcd -> 目标节点 kubelet -> 容器运行时 (拉镜像,启容器) -> CNI/CSI (配网络/存储) -> kubelet -> API Server -> (更新状态) etcd
核心要点:
- 声明式 API:用户声明“期望状态”,系统驱动“当前状态”向其靠拢。
- 监听与协同:所有组件都通过监听 kube-apiserver 来获取任务并协同工作。
- etcd 作为唯一信源:整个集群的状态始终以 etcd 中的数据为准。
- 组件职责分离:Scheduler 只管调度,kubelet 只管执行,API Server 只管交互和存储。
Deleting A Pod
删除一个 Pod 的流程与创建过程相对应,但它更侧重于如何优雅地、安全地终止一个运行中的实例。这个过程同样涉及多个组件的协同。
下面是一个 Pod 的删除流程,但它的核心是体现 Kubernetes 的优雅终止机制。
删除流程的核心阶段
阶段一:用户发起删除指令
- 用户执行命令:用户执行
kubectl delete pod <pod-name>。 - API Server 接收请求:
kubectl 向 kube-apiserver 发送一个 DELETE 请求。- kube-apiserver 会进行认证、授权等验证。
- “标记为删除”:验证通过后,kube-apiserver 不会立即从 etcd 中删除该 Pod 对象,而是会执行一个关键操作:为 Pod 对象设置一个“删除时间戳”(
deletionTimestamp)并将其标记为 Terminating 状态。这个状态会更新到 etcd 中。
阶段二:控制平面与节点的通知
- 组件感知变化:
- 所有监听 kube-apiserver 的组件(如 kube-scheduler, 各个节点的 kubelet)都会立刻感知到这个 Pod 的状态已变为
Terminating。 - Endpoint Controller 会立刻将这个 Pod 的 IP 从关联的 Service 的 Endpoints(或 EndpointSlice)列表中移除。这意味着新的流量不会再被负载均衡到这个 Pod 上。
阶段三:节点上的优雅终止
这是最关键的阶段,发生在 Pod 所在的工作节点上。
kubelet 监听到状态变化:目标节点上的 kubelet 通过 watch 机制发现它管理的某个 Pod 被标记为 Terminating。
触发优雅关闭序列:
- 第1步:执行 PreStop Hook(如果配置了的话)
kubelet 会首先执行 Pod 中容器定义的
preStop 钩子。这是一个在发送终止信号之前执行的特定命令或 HTTP 请求。常见用途包括:- 通知上游负载均衡器此实例正在下线。
- 让应用完成当前正在处理的请求。
- 执行一些清理任务。
- 第2步:发送 SIGTERM 信号
kubelet 通过容器运行时向 Pod 中的每个容器的主进程发送
SIGTERM(信号 15)信号。这是一个“优雅关闭”信号,通知应用:“你即将被终止,请保存状态、完成当前工作并自行退出”。- 注意:
SIGTERM 和 preStop Hook 是并行执行的。Kubernetes 会等待两者中的一个先完成,再进入下一步。
等待终止宽限期
- 在发送
SIGTERM 之后,Kubernetes 不会立即杀死容器。它会等待一个称为 terminationGracePeriodSeconds 的时长(默认为 30 秒)。 - 理想情况下,容器内的应用程序捕获到
SIGTERM 信号后,会开始优雅关闭流程,并在宽限期内自行退出。
阶段四:强制终止与清理
宽限期后的处理:
- 情况A:优雅关闭成功:如果在宽限期内,所有容器都成功停止,kubelet 会通知容器运行时清理容器资源,然后进行下一步。
- 情况B:优雅关闭失败:如果宽限期结束后,容器仍未停止,kubelet 会触发强制杀死。它向容器的主进程发送
SIGKILL(信号 9) 信号,该信号无法被捕获或忽略,会立即终止进程。
清理资源:
- 容器被强制或优雅地终止后,kubelet 会通过容器运行时清理容器资源。
- 同时,kubelet 会清理 Pod 的网络资源(通过 CNI 插件)和存储资源(卸载 Volume)。
上报最终状态:
- kubelet 向 kube-apiserver 发送最终信息,确认 Pod 已完全停止。
- kube-apiserver 随后从 etcd 中正式删除该 Pod 的对象记录。至此,这个 Pod 才真正从系统中消失。
总结流程图
用户 kubectl delete -> API Server -> (在etcd中标记Pod为 Terminating) -> Endpoint Controller (从Service中移除IP) -> 目标节点 kubelet -> 执行 PreStop Hook -> 发送 SIGTERM 信号 -> (等待 terminationGracePeriodSeconds) -> [成功则清理 / 失败则发送 SIGKILL] -> 清理网络/存储 -> kubelet -> API Server -> (从etcd中删除对象)
关键要点
- 优雅终止是核心:Kubernetes 给了应用一个自我清理的机会,这是保证服务无损发布和滚动更新的基石。
- 流量切断先行:Pod 被从 Service 的 Endpoints 中移除是第一步,这确保了在 Pod 开始关闭前,不会有新流量进来。
- 两个关键配置:
terminationGracePeriodSeconds:决定了应用有多长时间来自行关闭。preStop Hook:提供了一个主动执行关闭脚本的机会,比单纯等待 SIGTERM 更可靠。
- 强制终止作为保障:如果应用无法响应优雅关闭信号,Kubernetes 有最后的强制手段来保证资源被释放。
理解这个流程对于设计健壮的、能够正确处理关闭信号的微服务至关重要。
Deployment VS ReplicaSet
下面我会从 架构、工作流、控制循环、数据结构与事件链 等层面详细说明它们是怎么工作的。
🧩 一、核心概念层次关系
先看一下层级:
Deployment → ReplicaSet → Pod
| 层级 | 职责 | 控制器类型 |
|---|
| Deployment | 负责声明“应用版本”和“滚动更新策略” | 高级控制器(managing controller) |
| ReplicaSet | 保证指定数量的 Pod 副本数 | 基础控制器(ensuring controller) |
| Pod | 最小可调度单元,运行实际容器 | 工作负载对象 |
可以理解为:
Deployment 是策略控制器,ReplicaSet 是数量控制器,Pod 是执行单元。
⚙️ 二、Deployment 的工作原理(上层控制器)
1️⃣ Deployment 对象定义
你在创建一个 Deployment 时,例如:
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: nginx
image: nginx:1.25
这会创建一个 Deployment 对象并写入 etcd。
2️⃣ Deployment Controller 发现新对象
kube-controller-manager 中的 Deployment Controller 通过 Informer + Shared Indexer Cache 订阅(watch)Deployment 资源变化。
一旦发现新 Deployment,它会执行以下逻辑:
syncDeployment(deployment):
rsList := list ReplicaSets matching deployment.selector
newRS := findReplicaSetMatchingPodTemplate(deployment.spec.template)
if newRS == nil:
newRS = createReplicaSet(deployment.spec.template)
adjustReplicaCounts(newRS, oldRSList)
3️⃣ ReplicaSet 的创建与管理
- 如果模板(
spec.template)发生变化(例如镜像从 nginx:1.25 改为 nginx:1.26),
Deployment Controller 会创建一个新的 ReplicaSet。 - 旧 ReplicaSet 会被缩容,新的被扩容,形成滚动更新。
例如:
Deployment: webapp
├── ReplicaSet (nginx:1.25) — scale 2
└── ReplicaSet (nginx:1.26) — scale 1
当更新完成后:
Deployment: webapp
└── ReplicaSet (nginx:1.26) — scale 3
4️⃣ 滚动更新策略(核心逻辑)
Deployment 的滚动更新通过 RollingUpdateDeployment 类型实现:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
控制循环每次检查:
- 有多少可用副本?
- 是否可以再启动新的 Pod(受 maxSurge 限制)?
- 是否可以终止旧 Pod(受 maxUnavailable 限制)?
Controller 会在每次 sync 时:
- 创建新 ReplicaSet 的一个 Pod;
- 等待其
Ready; - 再删除旧 ReplicaSet 的一个 Pod;
- 重复直到达到目标状态。
5️⃣ ReplicaSet 与 Deployment 的“拥有关系”
Deployment 创建 ReplicaSet 时会设置 OwnerReference:
metadata:
ownerReferences:
- apiVersion: apps/v1
kind: Deployment
name: webapp
uid: <deployment-uid>
这样 kube-controller-manager 知道 该 ReplicaSet 属于哪个 Deployment,
而 Kubernetes 的垃圾回收器(GC Controller)会自动删除孤立的 ReplicaSet。
🧮 三、ReplicaSet 的内部机制(下层控制器)
ReplicaSet Controller 的逻辑相对简单:
syncReplicaSet(rs):
desired := rs.spec.replicas
actual := countPodsMatchingSelector(rs.selector)
if desired > actual:
createPods(desired - actual)
else if desired < actual:
deletePods(actual - desired)
也就是说,ReplicaSet 只关注Pod 数量是否符合期望。
它并不关心版本,也不关心策略。
1️⃣ Pod 的创建
ReplicaSet Controller 调用 PodTemplateSpec 生成新的 Pod:
pod := newPodFromTemplate(rs.spec.template)
pod.OwnerReferences = [rs.UID]
然后通过 API Server 创建 Pod 对象。
2️⃣ Pod 的调度与状态同步
创建后的 Pod 会由 kube-scheduler 调度到某个 Node 上,
kubelet 拉起容器后汇报状态,ReplicaSet 通过 Informer 感知到变化并更新 status.availableReplicas。
🧠 四、两者的控制循环(Control Loop)
可以用伪代码总结整个层次的循环:
while true:
# Deployment Controller Loop
for each Deployment:
reconcile Deployment → ensure right ReplicaSets exist and scaled properly
# ReplicaSet Controller Loop
for each ReplicaSet:
reconcile ReplicaSet → ensure correct number of Pods exist
# Pod Controller Loop (via kubelet)
for each Pod:
reconcile Pod → ensure container running
整个系统靠 Informer + WorkQueue + Reconcile 实现最终一致性。
Kubernetes 的控制循环是幂等的(idempotent)——无论运行多少次,结果都一致。
🔁 五、事件链(从创建到Pod运行)
| 阶段 | 执行者 | 操作 |
|---|
| 用户 | kubectl apply | 提交 Deployment |
| apiserver | | 将 Deployment 写入 etcd |
| controller-manager | Deployment Controller | 创建 ReplicaSet |
| controller-manager | ReplicaSet Controller | 创建 Pod |
| scheduler | kube-scheduler | 绑定 Node |
| kubelet | 节点上 | 拉取镜像并运行容器 |
| controller-manager | Deployment Controller | 更新 ReplicaSet 状态,完成滚动更新 |
🧰 六、垃圾回收与历史版本
🧩 七、关键点总结
| 项目 | Deployment | ReplicaSet |
|---|
| 职责 | 管理版本与更新策略 | 管理副本数量 |
| 是否直接创建 Pod | 否,通过 ReplicaSet | 是 |
| 更新策略 | 支持滚动、暂停、回滚 | 不支持 |
| 典型控制循环 | 调整 ReplicaSet | 调整 Pod |
| 与 Pod 的关系 | 间接控制 | 直接控制 |
💡 八、类比理解
你可以这样比喻:
- Deployment = “项目经理”
管理不同版本的 ReplicaSet,控制滚动更新节奏。
- ReplicaSet = “小组长”
保证自己手下(Pods)的人数正确。
- Pod = “员工”
实际干活的单位。
Endpoint VS EndpointSlice
Endpoint 和 EndpointSlice 都是 Kubernetes 中用于管理服务后端端点的资源,但 EndpointSlice 是更现代、更高效的解决方案。以下是它们的详细区别:
一、基本概念对比
Endpoint(传统方式)
apiVersion: v1
kind: Endpoints
metadata:
name: my-service
subsets:
- addresses:
- ip: 10.244.1.5
targetRef:
kind: Pod
name: pod-1
- ip: 10.244.1.6
targetRef:
kind: Pod
name: pod-2
ports:
- port: 8080
protocol: TCP
EndpointSlice(现代方式)
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: my-service-abc123
labels:
kubernetes.io/service-name: my-service
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 8080
endpoints:
- addresses:
- "10.244.1.5"
conditions:
ready: true
targetRef:
kind: Pod
name: pod-1
zone: us-west-2a
- addresses:
- "10.244.1.6"
conditions:
ready: true
targetRef:
kind: Pod
name: pod-2
zone: us-west-2b
二、核心架构差异
1. 数据模型设计
| 特性 | Endpoint | EndpointSlice |
|---|
| 存储结构 | 单个大对象 | 多个分片对象 |
| 规模限制 | 所有端点在一个对象中 | 自动分片(默认最多100个端点/片) |
| 更新粒度 | 全量更新 | 增量更新 |
2. 性能影响对比
# Endpoint 的问题:单个大对象
# 当有 1000 个 Pod 时:
kubectl get endpoints my-service -o yaml
# 返回一个包含 1000 个地址的庞大 YAML
# EndpointSlice 的解决方案:自动分片
# 当有 1000 个 Pod 时:
kubectl get endpointslices -l kubernetes.io/service-name=my-service
# 返回 10 个 EndpointSlice,每个包含 100 个端点
三、详细功能区别
1. 地址类型支持
Endpoint:
EndpointSlice:
addressType: IPv4 # 支持 IPv4, IPv6, FQDN
endpoints:
- addresses:
- "10.244.1.5"
conditions:
ready: true
serving: true
terminating: false
hostname: pod-1.subdomain # 支持主机名
nodeName: worker-1
zone: us-west-2a
hints:
forZones:
- name: us-west-2a
2. 拓扑感知和区域信息
EndpointSlice 独有的拓扑功能:
endpoints:
- addresses:
- "10.244.1.5"
conditions:
ready: true
# 拓扑信息
nodeName: node-1
zone: us-west-2a
# 拓扑提示,用于优化路由
hints:
forZones:
- name: us-west-2a
3. 端口定义方式
Endpoint:
subsets:
- ports:
- name: http
port: 8080
protocol: TCP
- name: metrics
port: 9090
protocol: TCP
EndpointSlice:
ports:
- name: http
protocol: TCP
port: 8080
appProtocol: http # 支持应用层协议标识
- name: metrics
protocol: TCP
port: 9090
appProtocol: https
四、实际使用场景
1. 大规模服务(500+ Pods)
Endpoint 的问题:
# 更新延迟:单个大对象的序列化/反序列化
# 网络开销:每次更新传输整个端点列表
# 内存压力:客户端需要缓存整个端点列表
EndpointSlice 的优势:
# 增量更新:只更新变化的切片
# 并行处理:多个切片可以并行处理
# 内存友好:客户端只需关注相关切片
2. 多区域部署
EndpointSlice 的拓扑感知:
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: multi-zone-service-1
labels:
kubernetes.io/service-name: multi-zone-service
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 8080
endpoints:
- addresses:
- "10.244.1.10"
conditions:
ready: true
zone: zone-a
nodeName: node-zone-a-1
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: multi-zone-service-2
labels:
kubernetes.io/service-name: multi-zone-service
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 8080
endpoints:
- addresses:
- "10.244.2.10"
conditions:
ready: true
zone: zone-b
nodeName: node-zone-b-1
3. 金丝雀发布和流量管理
EndpointSlice 提供更细粒度的控制:
# 金丝雀版本的 EndpointSlice
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: canary-service-version2
labels:
kubernetes.io/service-name: my-service
version: "v2" # 自定义标签用于选择
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 8080
endpoints:
- addresses:
- "10.244.3.10"
conditions:
ready: true
五、运维和管理差异
1. 监控方式
Endpoint 监控:
# 检查单个 Endpoint 对象
kubectl get endpoints my-service
kubectl describe endpoints my-service
# 监控端点数量
kubectl get endpoints my-service -o jsonpath='{.subsets[0].addresses[*].ip}' | wc -w
EndpointSlice 监控:
# 检查所有相关切片
kubectl get endpointslices -l kubernetes.io/service-name=my-service
# 查看切片详细信息
kubectl describe endpointslices my-service-abc123
# 统计总端点数量
kubectl get endpointslices -l kubernetes.io/service-name=my-service -o jsonpath='{range .items[*]}{.endpoints[*].addresses}{end}' | jq length
2. 故障排查
Endpoint 排查:
# 检查端点状态
kubectl get endpoints my-service -o yaml | grep -A 5 -B 5 "not-ready"
# 检查控制器日志
kubectl logs -n kube-system kube-controller-manager-xxx | grep endpoints
EndpointSlice 排查:
# 检查切片状态
kubectl get endpointslices --all-namespaces
# 检查端点就绪状态
kubectl get endpointslices -l kubernetes.io/service-name=my-service -o jsonpath='{range .items[*]}{.endpoints[*].conditions.ready}{end}'
# 检查 EndpointSlice Controller
kubectl logs -n kube-system deployment/endpointslice-controller
六、迁移和兼容性
1. 自动迁移
Kubernetes 1.21+ 默认同时维护两者:
# 启用 EndpointSlice 特性门控
kube-apiserver --feature-gates=EndpointSlice=true
kube-controller-manager --feature-gates=EndpointSlice=true
kube-proxy --feature-gates=EndpointSlice=true
2. 检查集群状态
# 检查 EndpointSlice 是否启用
kubectl get apiservices | grep discovery.k8s.io
# 检查特性门控
kube-apiserver -h | grep EndpointSlice
# 验证控制器运行状态
kubectl get pods -n kube-system -l k8s-app=endpointslice-controller
七、性能基准对比
| 场景 | Endpoint | EndpointSlice | 改进 |
|---|
| 1000个Pod更新 | 2-3秒 | 200-300ms | 10倍 |
| 网络带宽使用 | 高(全量传输) | 低(增量传输) | 60-80% 减少 |
| 内存使用 | 高(大对象缓存) | 低(分片缓存) | 50-70% 减少 |
| CPU使用 | 高(序列化成本) | 低(并行处理) | 40-60% 减少 |
八、最佳实践
1. 新集群配置
# kube-apiserver 配置
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --feature-gates=EndpointSlice=true
- --endpointslice-updates-batch-period=1s # 批量更新周期
2. 应用程序适配
// 使用 EndpointSlice 感知的客户端
import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
listers "k8s.io/client-go/listers/discovery/v1"
)
// 监听 EndpointSlice 变化
endpointSliceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
endpointSlice := obj.(*discoveryv1.EndpointSlice)
if endpointSlice.Labels["kubernetes.io/service-name"] == "my-service" {
updateLoadBalancerConfig(endpointSlice)
}
},
})
总结
| 维度 | Endpoint | EndpointSlice |
|---|
| 设计年代 | Kubernetes 早期 | Kubernetes 1.16+ |
| 扩展性 | 差(单对象限制) | 优秀(自动分片) |
| 性能 | 一般(全量更新) | 优秀(增量更新) |
| 功能特性 | 基础功能 | 拓扑感知、多协议等 |
| 推荐使用 | 传统集群/兼容性 | 新集群/大规模部署 |
| 未来方向 | 逐步淘汰 | 标准解决方案 |
简单建议:
- 新集群:直接使用 EndpointSlice
- 现有集群:逐步迁移到 EndpointSlice
- 大规模服务:必须使用 EndpointSlice
- 小规模测试:两者均可,但建议使用 EndpointSlice 以获得更好的未来兼容性
EndpointSlice 代表了 Kubernetes 服务发现架构的现代化方向,特别是在大规模和高动态环境中提供了显著的性能和改进。
ETCD如何调优
好的,Kubernetes 集群的稳定性和性能极大地依赖于其数据存储组件 etcd。对 etcd 进行调优是保障生产环境 K8s 集群高效、稳定运行的关键步骤。
下面我将从核心原则、性能调优参数、操作系统调优、Kubernetes 相关配置、监控与维护等多个维度,详细讲解如何对 K8s 上的 etcd 进行调优。
一、核心原则与前提
硬件是基础:在考虑软件参数调优前,必须确保硬件资源充足且高性能。
- CPU:需要足够的计算能力,特别是在高负载下进行压缩、序列化等操作时。
- 内存:etcd 的内存消耗与总键值对数量和大小正相关。足够的内存是保证性能的关键。建议至少 8GB,生产环境推荐 16GB 或以上。
- 磁盘:这是最重要的因素。必须使用高性能的 SSD(NVMe SSD 最佳)。etcd 的每次写入都需持久化到磁盘,磁盘的写入延迟(Write Latency)直接决定了 etcd 的写入性能。避免使用网络存储(如 NFS)。
- 网络:低延迟、高带宽的网络对于 etcd 节点间同步至关重要。如果 etcd 以集群模式运行,所有节点应位于同一个数据中心或低延迟的可用区。
备份!备份!备份!:在进行任何调优或配置更改之前,务必对 etcd 数据进行完整备份。误操作可能导致数据损坏或集群不可用。
二、etcd 命令行参数调优
etcd 主要通过其启动时的命令行参数进行调优。如果你使用 kubeadm 部署,这些参数通常配置在 /etc/kubernetes/manifests/etcd.yaml 静态 Pod 清单中。
1. 存储配额与压缩
为了防止磁盘耗尽,etcd 设有存储配额。一旦超过配额,它将进入维护模式,只能读不能写,并触发告警。
--quota-backend-bytes:设置 etcd 数据库的后端存储大小上限。默认是 2GB。对于生产环境,建议设置为 8GB 到 16GB(例如 8589934592 表示 8GB)。设置过大会影响备份和恢复时间。--auto-compaction-mode 和 --auto-compaction-retention:etcd 会累积历史版本,需要定期压缩来回收空间。--auto-compaction-mode:通常设置为 periodic(按时间周期)。--auto-compaction-retention:设置保留多长时间的历史数据。例如 "1h" 表示保留 1 小时,"10m" 表示保留 10 分钟。对于频繁变更的集群(如 running many CronJobs),建议设置为较短的周期,如 "10m" 或 "30m"。
示例配置片段(在 etcd.yaml 中):
spec:
containers:
- command:
- etcd
...
- --quota-backend-bytes=8589934592 # 8GB
- --auto-compaction-mode=periodic
- --auto-compaction-retention=10m # 每10分钟压缩一次历史版本
...
2. 心跳与选举超时
这些参数影响集群的领导者选举和节点间的心跳检测,对网络延迟敏感。
--heartbeat-interval:领导者向追随者发送心跳的间隔。建议设置为 100 到 300 毫秒之间。网络环境好可以设小(如 100),不稳定则设大(如 300)。--election-timeout:追随者等待多久没收到心跳后开始新一轮选举。此值必须是心跳间隔的 5-10 倍。建议设置在 1000 到 3000 毫秒之间。
规则:heartbeat-interval * 10 >= election-timeout
示例配置:
- --heartbeat-interval=200
- --election-timeout=2000
3. 快照
etcd 通过快照来持久化其状态。
--snapshot-count:指定在制作一次快照前,最多提交多少次事务。默认值是 100,000。在内存充足且磁盘 IO 性能极高的环境下,可以适当调低此值(如 50000)以在崩溃后更快恢复,但这会略微增加磁盘 IO 负担。通常使用默认值即可。
三、操作系统与运行时调优
1. 磁盘 I/O 调度器
对于 SSD,将 I/O 调度器设置为 none 或 noop 通常能获得更好的性能。
# 查看当前调度器
cat /sys/block/[你的磁盘,如 sda]/queue/scheduler
# 临时修改
echo 'noop' > /sys/block/sda/queue/scheduler
# 永久修改,在 /etc/default/grub 中添加或修改
GRUB_CMDLINE_LINUX_DEFAULT="... elevator=noop"
# 然后更新 grub 并重启
sudo update-grub
2. 文件系统
使用 XFS 或 ext4 文件系统。它们对 etcd 的工作负载有很好的支持。确保使用 ssd 挂载选项。
在 /etc/fstab 中为 etcd 数据目录所在分区添加 ssd 和 noatime 选项:
UUID=... /var/lib/etcd ext4 defaults,ssd,noatime 0 0
3. 提高文件描述符和进程数限制
etcd 可能会处理大量并发连接。
# 在 /etc/security/limits.conf 中添加
* soft nofile 65536
* hard nofile 65536
* soft nproc 65536
* hard nproc 65536
4. 网络参数调优
调整内核网络参数,特别是在高负载环境下。
在 /etc/sysctl.conf 中添加:
net.core.somaxconn = 1024
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 60
net.ipv4.tcp_keepalive_probes = 10
执行 sysctl -p 使其生效。
四、Kubernetes 相关调优
1. 资源请求和限制
在 etcd.yaml 中为 etcd 容器设置合适的资源限制,防止其因资源竞争而饿死。
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "8Gi" # 根据你的 --quota-backend-bytes 设置,确保内存足够
cpu: "2"
2. API Server 的 --etcd-compaction-interval
在 kube-apiserver 的启动参数中,这个参数控制它请求 etcd 进行压缩的周期。建议与 etcd 的 --auto-compaction-retention 保持一致或略大。
五、监控与维护
1. 监控关键指标
使用 Prometheus 等工具监控 etcd,重点关注以下指标:
etcd_disk_wal_fsync_duration_seconds:WAL 日志同步到磁盘的延迟。这是最重要的指标,P99 值应低于 25ms。etcd_disk_backend_commit_duration_seconds:后端数据库提交的延迟。etcd_server_leader_changes_seen_total:领导者变更次数。频繁变更表明集群不稳定。etcd_server_has_leader:当前节点是否认为有领导者(1 为是,0 为否)。etcd_mvcc_db_total_size_in_bytes:当前数据库大小,用于判断是否接近存储配额。
2. 定期进行碎片整理
即使开启了自动压缩,etcd 的数据库文件内部仍会产生碎片。当 etcd_mvcc_db_total_size_in_bytes 接近 --quota-backend-bytes 时,即使实际数据量没那么多,也需要在线进行碎片整理。
# 在任一 etcd 节点上执行
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/path/to/ca.crt \
--cert=/path/to/etcd-client.crt \
--key=/path/to/etcd-client.key \
defrag
注意:执行 defrag 会阻塞所有请求,应在业务低峰期进行,并逐个对集群成员执行。
调优总结与检查清单
- 硬件过关:确认使用 SSD,内存充足。
- 设置存储配额和自动压缩:
--quota-backend-bytes=8G, --auto-compaction-retention=10m。 - 调整心跳与选举超时:
--heartbeat-interval=200, --election-timeout=2000。 - 操作系统优化:I/O 调度器、文件系统挂载选项、文件描述符限制。
- 配置合理的资源限制:防止 etcd 容器因资源不足被 Kill。
- 开启并关注监控:特别是磁盘同步延迟和领导者变更。
- 定期维护:根据监控指标,在需要时进行碎片整理。
对于大多数场景,调整存储配额与压缩、心跳与选举超时以及确保高性能磁盘,就能解决绝大部分性能问题。调优是一个持续的过程,需要结合监控数据不断调整。
Flannel VS Calico
Calico 和 Flannel 是 Kubernetes 中最著名和最常见的两种网络插件(CNI),但它们的设计哲学、实现方式和能力有显著区别。
简单来说:
- Flannel 追求的是简单和易用,提供足够的基础网络功能。
- Calico 追求的是性能和功能,提供强大的网络策略和高性能网络。
下面我们从多个维度进行详细对比。
核心对比一览表
| 特性 | Flannel | Calico |
|---|
| 核心设计哲学 | 简单、最小化 | 高性能、功能丰富 |
| 网络模型 | Overlay 网络 | 纯三层路由(可选 Overlay) |
| 数据平面 | VXLAN(推荐)、Host-gw、UDP | BGP(推荐)、VXLAN、Windows |
| 性能 | 较好(VXLAN有封装开销) | 极高(BGP模式下无封装开销) |
| 网络策略 | 不支持(需安装Cilium等) | 原生支持(强大的网络策略) |
| 安全性 | 基础 | 高级(基于标签的微隔离) |
| 配置与维护 | 非常简单,几乎无需配置 | 相对复杂,功能多配置项也多 |
| 适用场景 | 学习、测试、中小型集群,需求简单 | 生产环境、大型集群、对性能和安全要求高 |
深入剖析
1. 网络模型与工作原理
这是最根本的区别。
注意:Calico也支持VXLAN模式(通常用于网络策略要求BGP但底层网络不支持的场景),但其最佳性能是在BGP模式下实现的。
2. 网络策略
这是两者功能性的一个巨大分水岭。
Flannel:本身不提供任何网络策略能力。它只负责打通网络,让所有Pod默认可以相互通信。如果你需要实现Pod之间的访问控制(微隔离),你必须额外安装一个网络策略控制器,如 Cilium 或 Calico本身(可以只使用其策略部分,与Flannel叠加使用)。
Calico:原生支持强大的Kubernetes NetworkPolicy。你可以定义基于Pod标签、命名空间、端口、协议甚至DNS名称的精细规则,来控制Pod的入站和出站流量。这对于实现“零信任”安全模型至关重要。
3. 性能
- Calico (BGP模式):由于其纯三层的转发机制,无需封装,数据包是原生IP包,其延迟更低,吞吐量更高,CPU消耗也更少。
- Flannel (VXLAN模式):由于存在VXLAN的封装头(通常50字节 overhead),最大传输单元会变小,封装/解封装操作也需要CPU参与,性能相比Calico BGP模式要低一些。但其 Host-gw 后端模式性能很好,前提是节点在同一个二层网络。
4. 生态系统与高级功能
- Calico:功能非常丰富,远不止基础网络。
- 网络策略:如上所述,非常强大。
- IPAM:灵活的IP地址管理。
- 服务网格集成:与Istio有深度集成,可以实施全局的服务到服务策略。
- Windows支持:对Windows节点有良好的支持。
- 网络诊断工具:提供了
calicoctl 等强大的运维工具。
- Flannel:功能相对单一,就是做好网络连通性。它“小而美”,但缺乏高级功能。
如何选择?
选择 Flannel 的情况:
- 新手用户:想要快速搭建一个K8s集群,不想纠结于复杂的网络配置。
- 测试或开发环境:需求简单,只需要Pod能通。
- 中小型集群:对性能和高级网络策略没有硬性要求。
- 底层网络受限:无法配置BGP或主机路由的环境(例如某些公有云基础网络)。
选择 Calico 的情况:
- 生产环境:对稳定性和性能有高要求。
- 大型集群:需要高效的路由和可扩展性。
- 安全要求高:需要实现Pod之间的网络隔离(微隔离)。
- 对网络性能极度敏感:例如AI/ML训练、高频交易等场景。
- 底层网络可控:例如在自建数据中心或云上支持BGP的环境。
总结
| Flannel | Calico |
|---|
| 核心价值 | 简单可靠 | 功能强大 |
| 好比买车 | 丰田卡罗拉:皮实、省心、够用 | 宝马/奥迪:性能强劲、功能齐全、操控精准 |
| 一句话总结 | “让我快速把网络打通” | “我要一个高性能、高安全性的生产级网络” |
在现代Kubernetes部署中,尤其是生产环境,Calico因其卓越的性能和原生的安全能力,已经成为更主流和推荐的选择。而Flannel则在那些“只要能通就行”的简单场景中,依然保持着它的价值。
Headless Service VS ClusterIP
Headless Service vs ClusterIP 详解
这是 Kubernetes 中两种常见的 Service 类型,它们在服务发现和负载均衡方面有本质区别。
🎯 核心区别总结
| 维度 | ClusterIP | Headless Service |
|---|
| ClusterIP 值 | 有固定的虚拟 IP | None (无 ClusterIP) |
| DNS 解析 | 返回 Service IP | 直接返回 Pod IP 列表 |
| 负载均衡 | ✅ kube-proxy 自动负载均衡 | ❌ 客户端自行选择 Pod |
| 适用场景 | 无状态服务 | 有状态服务、服务发现 |
| 典型用例 | Web 应用、API 服务 | 数据库集群、Kafka、Zookeeper |
📋 ClusterIP Service (默认类型)
定义
ClusterIP 是 Kubernetes 默认的 Service 类型,会分配一个虚拟 IP(Cluster IP),作为访问后端 Pod 的统一入口。
YAML 示例
apiVersion: v1
kind: Service
metadata:
name: my-web-service
spec:
type: ClusterIP # 默认类型,可以省略
selector:
app: web
ports:
- protocol: TCP
port: 80 # Service 端口
targetPort: 8080 # Pod 端口
工作原理
┌─────────────────────────────────────────┐
│ ClusterIP Service │
│ (虚拟 IP: 10.96.100.50) │
└────────────┬────────────────────────────┘
│ kube-proxy 负载均衡
│
┌───────┴───────┬──────────┐
▼ ▼ ▼
Pod-1 Pod-2 Pod-3
10.244.1.5 10.244.2.8 10.244.3.12
(app=web) (app=web) (app=web)
DNS 解析行为
# 在集群内部查询 DNS
nslookup my-web-service.default.svc.cluster.local
# 输出:
# Name: my-web-service.default.svc.cluster.local
# Address: 10.96.100.50 ← 返回 Service 的虚拟 IP
# 客户端访问这个 IP
curl http://my-web-service:80
# 请求会被 kube-proxy 自动转发到后端 Pod
# 默认使用 iptables 或 IPVS 做负载均衡
特点
✅ 统一入口:客户端只需知道 Service IP,不关心后端 Pod
✅ 自动负载均衡:kube-proxy 自动在多个 Pod 间分发流量
✅ 服务发现简单:通过 DNS 获取稳定的 Service IP
✅ 屏蔽 Pod 变化:Pod 重启或扩缩容,Service IP 不变
✅ 会话保持:可配置 sessionAffinity: ClientIP
负载均衡方式
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ClusterIP
sessionAffinity: ClientIP # 可选:会话保持(同一客户端固定到同一 Pod)
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 会话超时时间
selector:
app: web
ports:
- port: 80
targetPort: 8080
🔍 Headless Service (无头服务)
定义
Headless Service 是不分配 ClusterIP 的特殊 Service,通过设置 clusterIP: None 创建。
YAML 示例
apiVersion: v1
kind: Service
metadata:
name: my-headless-service
spec:
clusterIP: None # 🔑 关键:设置为 None
selector:
app: database
ports:
- protocol: TCP
port: 3306
targetPort: 3306
工作原理
┌─────────────────────────────────────────┐
│ Headless Service (无 ClusterIP) │
│ DNS 直接返回 │
└────────────┬────────────────────────────┘
│ 没有负载均衡
│ DNS 返回所有 Pod IP
│
┌───────┴───────┬──────────┐
▼ ▼ ▼
Pod-1 Pod-2 Pod-3
10.244.1.5 10.244.2.8 10.244.3.12
(app=database) (app=database) (app=database)
DNS 解析行为
# 在集群内部查询 DNS
nslookup my-headless-service.default.svc.cluster.local
# 输出:
# Name: my-headless-service.default.svc.cluster.local
# Address: 10.244.1.5 ← Pod-1 IP
# Address: 10.244.2.8 ← Pod-2 IP
# Address: 10.244.3.12 ← Pod-3 IP
# 客户端获得所有 Pod IP,自己选择连接哪个
特点
✅ 服务发现:客户端可以获取所有后端 Pod 的 IP
✅ 自主选择:客户端自己决定连接哪个 Pod(负载均衡逻辑由客户端实现)
✅ 稳定 DNS:每个 Pod 有独立的 DNS 记录
✅ 适合有状态服务:数据库主从、集群成员发现
❌ 无自动负载均衡:需要客户端或应用层实现
与 StatefulSet 结合(最常见用法)
# StatefulSet + Headless Service
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
name: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-headless # 🔑 关联 Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
每个 Pod 的独立 DNS 记录
# StatefulSet 的 Pod 命名规则:
# <statefulset-name>-<ordinal>.<service-name>.<namespace>.svc.cluster.local
# 示例:
mysql-0.mysql-headless.default.svc.cluster.local → 10.244.1.5
mysql-1.mysql-headless.default.svc.cluster.local → 10.244.2.8
mysql-2.mysql-headless.default.svc.cluster.local → 10.244.3.12
# 可以直接访问特定 Pod
mysql -h mysql-0.mysql-headless.default.svc.cluster.local -u root -p
# 查询所有 Pod
nslookup mysql-headless.default.svc.cluster.local
🔄 实际对比演示
场景 1:Web 应用(使用 ClusterIP)
# ClusterIP Service
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
type: ClusterIP
selector:
app: nginx
ports:
- port: 80
targetPort: 80
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
# 测试访问
kubectl run test --rm -it --image=busybox -- /bin/sh
# 在 Pod 内执行
nslookup web-service
# 输出:只有一个 Service IP
wget -q -O- http://web-service
# 请求会被自动负载均衡到 3 个 nginx Pod
场景 2:MySQL 主从(使用 Headless Service)
# Headless Service
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
---
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: "password"
# 测试服务发现
kubectl run test --rm -it --image=busybox -- /bin/sh
# 在 Pod 内执行
nslookup mysql
# 输出:返回 3 个 Pod IP
# 可以连接到特定的 MySQL 实例(如主节点)
mysql -h mysql-0.mysql.default.svc.cluster.local -u root -p
# 也可以连接到从节点
mysql -h mysql-1.mysql.default.svc.cluster.local -u root -p
mysql -h mysql-2.mysql.default.svc.cluster.local -u root -p
📊 详细对比
1. DNS 解析差异
# ClusterIP Service
$ nslookup web-service
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: web-service.default.svc.cluster.local
Address: 10.96.100.50 ← Service 虚拟 IP
# Headless Service
$ nslookup mysql-headless
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: mysql-headless.default.svc.cluster.local
Address: 10.244.1.5 ← Pod-1 IP
Address: 10.244.2.8 ← Pod-2 IP
Address: 10.244.3.12 ← Pod-3 IP
2. 流量路径差异
ClusterIP 流量路径:
Client → Service IP (10.96.100.50)
→ kube-proxy (iptables/IPVS)
→ 随机选择一个 Pod
Headless 流量路径:
Client → DNS 查询
→ 获取所有 Pod IP
→ 客户端自己选择 Pod
→ 直接连接 Pod IP
3. 使用场景对比
| 场景 | ClusterIP | Headless |
|---|
| 无状态应用 | ✅ 推荐 | ❌ 不需要 |
| 有状态应用 | ❌ 不适合 | ✅ 推荐 |
| 数据库主从 | ❌ 无法区分主从 | ✅ 可以指定连接主节点 |
| 集群成员发现 | ❌ 无法获取成员列表 | ✅ 可以获取所有成员 |
| 需要负载均衡 | ✅ 自动负载均衡 | ❌ 需要客户端实现 |
| 客户端连接池 | ⚠️ 只能连接到 Service IP | ✅ 可以为每个 Pod 建立连接 |
🎯 典型应用场景
ClusterIP Service 适用场景
1. 无状态 Web 应用
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
type: ClusterIP
selector:
app: frontend
ports:
- port: 80
targetPort: 3000
2. RESTful API 服务
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
type: ClusterIP
selector:
app: api
ports:
- port: 8080
3. 微服务之间的调用
# Service A 调用 Service B
apiVersion: v1
kind: Service
metadata:
name: service-b
spec:
type: ClusterIP
selector:
app: service-b
ports:
- port: 9090
Headless Service 适用场景
1. MySQL 主从复制
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
---
# 应用连接时:
# 写操作 → mysql-0.mysql (主节点)
# 读操作 → mysql-1.mysql, mysql-2.mysql (从节点)
2. Kafka 集群
apiVersion: v1
kind: Service
metadata:
name: kafka
spec:
clusterIP: None
selector:
app: kafka
ports:
- port: 9092
---
# Kafka 客户端可以发现所有 broker:
# kafka-0.kafka:9092
# kafka-1.kafka:9092
# kafka-2.kafka:9092
3. Elasticsearch 集群
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
spec:
clusterIP: None
selector:
app: elasticsearch
ports:
- port: 9200
name: http
- port: 9300
name: transport
---
# 集群内部节点通过 DNS 发现彼此:
# elasticsearch-0.elasticsearch
# elasticsearch-1.elasticsearch
# elasticsearch-2.elasticsearch
4. Redis 集群模式
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
spec:
clusterIP: None
selector:
app: redis
ports:
- port: 6379
name: client
- port: 16379
name: gossip
---
# Redis 客户端获取所有节点进行 cluster slots 查询
🔧 混合使用:两种 Service 同时存在
对于有状态服务,常见做法是同时创建两个 Service:
# 1. Headless Service:用于 StatefulSet 和 Pod 间通信
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
---
# 2. ClusterIP Service:用于客户端负载均衡访问(只读副本)
apiVersion: v1
kind: Service
metadata:
name: mysql-read
spec:
type: ClusterIP
selector:
app: mysql
role: replica # 只选择从节点
ports:
- port: 3306
---
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-headless # 使用 Headless Service
replicas: 3
# ...
使用方式:
# 写操作:直接连接主节点
mysql -h mysql-0.mysql-headless -u root -p
# 读操作:通过 ClusterIP 自动负载均衡到所有从节点
mysql -h mysql-read -u root -p
🛠️ 常见问题
Q1: 如何选择使用哪种 Service?
决策流程:
应用是无状态的?
├─ 是 → 使用 ClusterIP
└─ 否 → 继续
需要客户端感知所有 Pod?
├─ 是 → 使用 Headless Service
└─ 否 → 继续
需要区分不同 Pod(如主从)?
├─ 是 → 使用 Headless Service + StatefulSet
└─ 否 → 使用 ClusterIP
Q2: Headless Service 没有负载均衡怎么办?
方案:
- 客户端负载均衡:应用层实现(如 Kafka 客户端)
- DNS 轮询:部分 DNS 客户端会自动轮询
- 混合方案:同时创建 ClusterIP Service 用于负载均衡
Q3: 如何测试 Headless Service?
# 创建测试 Pod
kubectl run -it --rm debug --image=busybox --restart=Never -- sh
# 测试 DNS 解析
nslookup mysql-headless.default.svc.cluster.local
# 测试连接特定 Pod
wget -O- http://mysql-0.mysql-headless:3306
# 测试所有 Pod
for i in 0 1 2; do
echo "Testing mysql-$i"
wget -O- http://mysql-$i.mysql-headless:3306
done
Q4: ClusterIP Service 能否用于 StatefulSet?
可以,但不推荐:
- ✅ 可以提供负载均衡
- ❌ 无法通过稳定的 DNS 名访问特定 Pod
- ❌ 不适合主从架构(无法区分主节点)
最佳实践:
- StatefulSet 使用 Headless Service
- 如需负载均衡,额外创建 ClusterIP Service
💡 关键要点总结
ClusterIP Service
✅ 默认类型,有虚拟 IP
✅ 自动负载均衡(kube-proxy)
✅ 适合无状态应用
✅ 客户端无需感知后端 Pod
✅ DNS 解析返回 Service IP
Headless Service
✅ 设置 clusterIP: None
✅ DNS 解析返回所有 Pod IP
✅ 适合有状态应用
✅ 支持 Pod 级别的服务发现
✅ 常与 StatefulSet 配合使用
选型建议
- Web 应用、API 服务 → ClusterIP
- 数据库、消息队列、分布式存储 → Headless Service
- 有主从/分片的应用 → Headless Service + StatefulSet
- 需要同时支持负载均衡和直接访问 → 两种 Service 都创建
Helm Principle
Helm 是 Kubernetes 的包管理工具,类似于 Linux 的 apt/yum 或 Python 的 pip,它的核心作用是:
👉 用模板化的方式定义、安装和升级 Kubernetes 应用。
🧩 一、Helm 的核心概念
在理解原理前,先明确 Helm 的几个关键对象:
| 概念 | 说明 |
|---|
| Chart | 一个 Helm 包,描述一组 Kubernetes 资源的模板集合(即一个应用的安装包) |
| Values.yaml | Chart 的参数配置文件,用于填充模板变量 |
| Release | Helm 将 Chart 安装到某个命名空间后的实例,每次安装或升级都是一个 release |
| Repository | 存放打包后 chart (.tgz) 的仓库,可以是 HTTP/OCI 类型(如 Harbor, Artifactory) |
⚙️ 二、Helm 的工作原理流程
从用户角度来看,Helm Client 发出命令(如 helm install),Helm 会通过一系列步骤在集群中生成 Kubernetes 资源。
下面是核心流程图概念(文字版):
┌────────────┐
│ helm client│
└─────┬──────┘
│
▼
1. 解析Chart与Values
│
▼
2. 模板渲染(Helm Template Engine)
│
▼
3. 生成纯YAML清单
│
▼
4. 调用Kubernetes API
│
▼
5. 创建/更新资源(Deployment、Service等)
│
▼
6. 记录Release历史(ConfigMap/Secret)
🔍 三、Helm 工作机制分解
1️⃣ Chart 渲染阶段
Helm 使用 Go 的 text/template 模板引擎 + Sprig 函数库,将模板与 values.yaml 合并生成 Kubernetes YAML 清单。
例如:
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
spec:
replicas: {{ .Values.replicas }}
通过:
helm template myapp ./mychart -f myvalues.yaml
Helm 会本地生成纯 YAML 文件(不部署到集群)。
2️⃣ 部署阶段(Install/Upgrade)
执行:
helm install myapp ./mychart
Helm Client 会将渲染好的 YAML 通过 Kubernetes API 提交到集群(相当于执行 kubectl apply)。
Helm 同时在命名空间中创建一个 “Release 记录”,默认存放在:
namespace: <your-namespace>
kind: Secret
name: sh.helm.release.v1.<release-name>.vN
其中保存了:
- Chart 模板和 values 的快照
- 渲染后的 manifest
- Release 状态(deployed、failed 等)
- 版本号(v1, v2, …)
3️⃣ 升级与回滚机制
当执行:
helm upgrade myapp ./mychart
时,Helm 会:
- 读取旧版本 release secret
- 渲染新模板
- 比较新旧差异(Diff)
- 调用 Kubernetes API 更新对象
- 写入新的 release secret(版本号 +1)
回滚时:
Helm 会取出 v2 的记录,再次 kubectl apply。
4️⃣ 仓库机制(Helm Repository / OCI Registry)
Helm 支持两种包分发方式:
HTTP 仓库(传统)
- 有一个
index.yaml 索引文件 - Chart 以
.tgz 格式存储
OCI 仓库(现代推荐)
🧠 四、Helm 与 Kubernetes 的关系
Helm 本身 不直接管理容器运行,它只是:
- 模板引擎 + 应用生命周期管理器;
- 所有资源最终仍由 Kubernetes 控制器(如 Deployment controller)调度、运行。
Helm 类似于 “上层应用打包器”:
Helm = Chart 模板系统 + Kubernetes API 客户端 + Release 历史追踪
💡 五、常见命令原理对照
| 命令 | Helm 行为 |
|---|
helm install | 渲染模板 → 提交资源 → 创建 release |
helm upgrade | 渲染模板 → diff 旧版本 → 更新资源 → 新 release |
helm rollback | 获取旧版本记录 → 重新提交旧 manifest |
helm uninstall | 删除 Kubernetes 资源 + 删除 release secret |
helm template | 本地渲染模板,不与集群交互 |
helm diff(插件) | 比较新旧渲染结果差异 |
🧩 六、Helm 3 与 Helm 2 的区别(核心)
| Helm 2 | Helm 3 |
|---|
| 需要 Tiller(集群内控制组件) | 无需 Tiller,完全 client-side |
| 安全模型复杂(基于 RBAC 授权) | 安全性更好,直接使用 kubeconfig 权限 |
| Release 存储在 ConfigMap | 默认存储在 Secret |
| 需要 Helm Server 部署 | 纯客户端 |
HPA
HPA(Horizontal Pod Autoscaler)是 Kubernetes 中实现自动水平扩缩容的核心组件。它的实现涉及多个 Kubernetes 组件和复杂的控制逻辑。
一、HPA 架构组成
1. 核心组件
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ HPA Controller │ ◄──│ Metrics API │ ◄──│ Metrics Server │
│ (kube-controller)│ │ (聚合层) │ │ (cAdvisor) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Deployment/ │ │ Custom Metrics │ │ External │
│ StatefulSet │ │ Adapter │ │ Metrics │
└─────────────────┘ └──────────────────┘ └─────────────────┘
二、HPA 工作流程
1. 完整的控制循环
// 简化的 HPA 控制逻辑
for {
// 1. 获取 HPA 对象
hpa := client.AutoscalingV2().HorizontalPodAutoscalers(namespace).Get(name)
// 2. 获取缩放目标(Deployment/StatefulSet等)
scaleTarget := hpa.Spec.ScaleTargetRef
target := client.AppsV1().Deployments(namespace).Get(scaleTarget.Name)
// 3. 查询指标
metrics := []autoscalingv2.MetricStatus{}
for _, metricSpec := range hpa.Spec.Metrics {
metricValue := getMetricValue(metricSpec, target)
metrics = append(metrics, metricValue)
}
// 4. 计算期望副本数
desiredReplicas := calculateDesiredReplicas(hpa, metrics, currentReplicas)
// 5. 执行缩放
if desiredReplicas != currentReplicas {
scaleTarget.Spec.Replicas = &desiredReplicas
client.AppsV1().Deployments(namespace).UpdateScale(scaleTarget.Name, scaleTarget)
}
time.Sleep(15 * time.Second) // 默认扫描间隔
}
2. 详细步骤分解
步骤 1:指标收集
# HPA 通过 Metrics API 获取指标
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/default/pods" | jq .
# 或者通过自定义指标 API
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .
步骤 2:指标计算
// 计算当前指标值与目标值的比率
func calculateMetricRatio(currentValue, targetValue int64) float64 {
return float64(currentValue) / float64(targetValue)
}
// 示例:CPU 使用率计算
currentCPUUsage := 800m # 当前使用 800 milli-cores
targetCPUUsage := 500m # 目标使用 500 milli-cores
ratio := 800.0 / 500.0 # = 1.6
三、HPA 配置详解
1. HPA 资源定义
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
namespace: default
spec:
# 缩放目标
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
# 副本数范围
minReplicas: 2
maxReplicas: 10
# 指标定义
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: packets-per-second
target:
type: AverageValue
averageValue: 1k
- type: Object
object:
metric:
name: requests-per-second
describedObject:
apiVersion: networking.k8s.io/v1
kind: Ingress
name: main-route
target:
type: Value
value: 10k
# 行为配置(Kubernetes 1.18+)
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
- type: Pods
value: 5
periodSeconds: 60
selectPolicy: Min
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15
selectPolicy: Max
四、指标类型和计算方式
1. 资源指标(CPU/Memory)
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization # 利用率模式
averageUtilization: 50
- type: Resource
resource:
name: memory
target:
type: AverageValue # 平均值模式
averageValue: 512Mi
计算逻辑:
// CPU 利用率计算
func calculateCPUReplicas(currentUsage, targetUtilization int32, currentReplicas int32) int32 {
// 当前总使用量
totalUsage := currentUsage * currentReplicas
// 期望副本数 = ceil(当前总使用量 / (单个 Pod 请求量 * 目标利用率))
desiredReplicas := int32(math.Ceil(float64(totalUsage) / float64(targetUtilization)))
return desiredReplicas
}
2. 自定义指标(Pods 类型)
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: 100
计算方式:
期望副本数 = ceil(当前总指标值 / 目标平均值)
3. 对象指标(Object 类型)
metrics:
- type: Object
object:
metric:
name: latency
describedObject:
apiVersion: networking.k8s.io/v1
kind: Ingress
name: my-ingress
target:
type: Value
value: 100
五、HPA 算法详解
1. 核心算法
// 计算期望副本数
func GetDesiredReplicas(
currentReplicas int32,
metricValues []metrics,
hpa *HorizontalPodAutoscaler,
) int32 {
ratios := make([]float64, 0)
// 1. 计算每个指标的比率
for _, metric := range metricValues {
ratio := calculateMetricRatio(metric.current, metric.target)
ratios = append(ratios, ratio)
}
// 2. 选择最大的比率(最需要扩容的指标)
maxRatio := getMaxRatio(ratios)
// 3. 计算期望副本数
desiredReplicas := math.Ceil(float64(currentReplicas) * maxRatio)
// 4. 应用边界限制
desiredReplicas = applyBounds(desiredReplicas, hpa.Spec.MinReplicas, hpa.Spec.MaxReplicas)
return int32(desiredReplicas)
}
2. 平滑算法和冷却机制
// 考虑历史记录的缩放决策
func withStabilization(desiredReplicas int32, hpa *HorizontalPodAutoscaler) int32 {
now := time.Now()
if isScaleUp(desiredReplicas, hpa.Status.CurrentReplicas) {
// 扩容:通常立即执行
stabilizationWindow = hpa.Spec.Behavior.ScaleUp.StabilizationWindowSeconds
} else {
// 缩容:应用稳定窗口
stabilizationWindow = hpa.Spec.Behavior.ScaleDown.StabilizationWindowSeconds
}
// 过滤稳定窗口内的历史推荐值
validRecommendations := filterRecommendationsByTime(
hpa.Status.Conditions,
now.Add(-time.Duration(stabilizationWindow)*time.Second)
)
// 选择策略(Min/Max)
finalReplicas := applyPolicy(validRecommendations, hpa.Spec.Behavior)
return finalReplicas
}
六、高级特性实现
1. 多指标支持
当配置多个指标时,HPA 会为每个指标计算期望副本数,然后选择最大值:
func calculateFromMultipleMetrics(metrics []Metric, currentReplicas int32) int32 {
desiredReplicas := make([]int32, 0)
for _, metric := range metrics {
replicas := calculateForSingleMetric(metric, currentReplicas)
desiredReplicas = append(desiredReplicas, replicas)
}
// 选择最大的期望副本数
return max(desiredReplicas...)
}
2. 扩缩容行为控制
behavior:
scaleDown:
# 缩容稳定窗口:5分钟
stabilizationWindowSeconds: 300
policies:
- type: Percent # 每分钟最多缩容 50%
value: 50
periodSeconds: 60
- type: Pods # 或每分钟最多减少 5 个 Pod
value: 5
periodSeconds: 60
selectPolicy: Min # 选择限制更严格的策略
scaleUp:
stabilizationWindowSeconds: 0 # 扩容立即执行
policies:
- type: Percent # 每分钟最多扩容 100%
value: 100
periodSeconds: 60
- type: Pods # 或每分钟最多增加 4 个 Pod
value: 4
periodSeconds: 60
selectPolicy: Max # 选择限制更宽松的策略
七、监控和调试
1. 查看 HPA 状态
# 查看 HPA 详情
kubectl describe hpa myapp-hpa
# 输出示例:
# Name: myapp-hpa
# Namespace: default
# Reference: Deployment/myapp
# Metrics: ( current / target )
# resource cpu on pods (as a percentage of request): 65% (130m) / 50%
# resource memory on pods: 120Mi / 100Mi
# Min replicas: 2
# Max replicas: 10
# Deployment pods: 3 current / 3 desired
2. HPA 相关事件
# 查看 HPA 事件
kubectl get events --field-selector involvedObject.kind=HorizontalPodAutoscaler
# 查看缩放历史
kubectl describe deployment myapp | grep -A 10 "Events"
3. 指标调试
# 检查 Metrics API 是否正常工作
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" | jq .
# 检查自定义指标
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .
# 直接查询 Pod 指标
kubectl top pods
kubectl top nodes
八、常见问题排查
1. HPA 不扩容
# 检查指标是否可用
kubectl describe hpa myapp-hpa
# 查看 Events 部分是否有错误信息
# 检查 Metrics Server
kubectl get apiservices | grep metrics
kubectl logs -n kube-system -l k8s-app=metrics-server
# 检查资源请求配置
kubectl get deployment myapp -o yaml | grep resources -A 5
2. HPA 频繁震荡
# 调整行为配置
behavior:
scaleDown:
stabilizationWindowSeconds: 600 # 延长缩容稳定窗口
policies:
- type: Pods
value: 1 # 每次只缩容 1 个 Pod
periodSeconds: 300 # 每 5 分钟最多一次
九、性能优化建议
1. 大规模集群优化
# kube-controller-manager 配置优化
apiVersion: v1
kind: Pod
metadata:
name: kube-controller-manager
namespace: kube-system
spec:
containers:
- command:
- kube-controller-manager
- --horizontal-pod-autoscaler-sync-period=30s # 调整同步周期
- --horizontal-pod-autoscaler-downscale-stabilization=5m0s
- --horizontal-pod-autoscaler-initial-readiness-delay=30s
- --horizontal-pod-autoscaler-cpu-initialization-period=5m0s
总结
HPA 的实现涉及:
- 指标收集:通过 Metrics API 聚合资源和使用情况
- 算法计算:基于指标值和目标值计算期望副本数
- 决策平滑:应用稳定窗口和策略避免震荡
- 执行缩放:更新目标资源的副本数
- 状态跟踪:记录缩放历史和当前状态
HPA 的智能化体现在多指标决策、行为控制、冷却机制等方面,使得自动扩缩容既响应迅速又稳定可靠。
More than 1k Nodes
在这个量级上,K8s 不再只是“能跑就行”,而是进入可扩展性、稳定性、可观测性和资源效率的工程化挑战。下面我从架构、控制面、节点管理、网络、存储、安全和运维几个方面系统讲解。
🧠 一、总体思路:大规模集群的本质挑战
当节点规模超过 500~1000 时,Kubernetes 的瓶颈通常出现在:
- 控制平面(API Server / etcd)压力过大;
- 调度器吞吐不足;
- 资源对象(Pod / Node / Secret / ConfigMap 等)过多,导致 List/Watch 延迟;
- 网络和 CNI 插件在高并发下性能下降;
- 监控、日志、事件系统的数据量爆炸;
- 维护和升级变得极度复杂。
所以,大规模集群的重点是:
控制平面分层、节点池分区、流量隔离、观测与调优。
🏗️ 二、控制平面(Control Plane)
1. etcd 优化
- 独立部署:不要和 kube-apiserver 混布,最好是独立的高性能节点(NVMe SSD、本地盘)。
- 使用 etcd v3.5+(性能改进明显),并开启压缩和快照机制。
- 调大
--max-request-bytes 和 --quota-backend-bytes,避免过载。 - 定期 defrag:可用 CronJob 自动化。
- 不要存放短生命周期对象(例如频繁更新的 CRD 状态),可以考虑用外部缓存系统(如 Redis 或 SQL)。
2. API Server 扩展与保护
使用 负载均衡(HAProxy、NGINX、ELB)在多 API Server 之间分流;
调整:
--max-mutating-requests-inflight--max-requests-inflight--target-ram-mb
合理设置 --request-timeout,防止 watch 卡死;
限制大量 client watch 行为(Prometheus、controller-manager 等);
对 client 侧使用 aggregator 或 read-only proxy 来降低负载。
3. Scheduler & Controller Manager
🧩 三、节点与 Pod 管理
1. 节点分区与拓扑
- 按功能/位置划分 Node Pool(如 GPU/CPU/IO 密集型);
- 使用 Topology Spread Constraints 避免集中调度;
- 考虑用 Cluster Federation (KubeFed) 或 多个集群 + 集中管理(如 ArgoCD 多集群、Karmada、Fleet)。
2. 节点生命周期
- 控制 kubelet 心跳频率 (
--node-status-update-frequency); - 通过 Node Problem Detector (NPD) 自动标记异常节点;
- 监控 Pod eviction rate,防止节点频繁漂移;
- 启用 graceful node shutdown 支持。
3. 镜像与容器运行时
- 镜像预热(Image pre-pull);
- 使用 镜像仓库代理(Harbor / registry-mirror);
- 考虑 containerd 代替 Docker;
- 定期清理
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots。
🌐 四、网络(CNI)
1. CNI 选择与调优
2. Service & DNS
💾 五、存储(CSI)
- 使用 分布式存储系统(Ceph、Longhorn、OpenEBS、CSI-HostPath);
- 避免高频小 I/O 的 PVC;
- 定期清理僵尸 PV/PVC;
- 对 CSI driver 开启限流与重试机制。
🔒 六、安全与访问控制
- 开启 RBAC 严格控制;
- 限制 namespace 级资源上限(ResourceQuota, LimitRange);
- 审计日志(Audit Policy)异步存储;
- 对外接口统一走 Ingress Controller;
- 如果有 Operator 或 CRD 资源暴涨,记得定期清理过期对象。
📈 七、可观测性与维护
1. 监控
- Prometheus 集群化(Thanos / VictoriaMetrics);
- 不直接监控所有 Pod,可抽样或聚合;
- kube-state-metrics 与 cAdvisor 数据要限流。
2. 日志
- 统一日志收集(Loki / Elasticsearch / Vector);
- 日志量控制策略(采样、压缩、清理)。
3. 升级与测试
- 使用 灰度升级 / Node pool rolling;
- 每次升级前跑 e2e 测试;
- 对控制平面单独做快照和备份(etcd snapshot)。
⚙️ 八、性能调优与实践经验
🧭 九、扩展方向
当规模继续上升(>3000 节点)时,可以考虑:
- 多集群架构(Cluster Federation / Karmada / Rancher Fleet)
- 控制平面分层(cell-based control plane)
- API Aggregation Layer + Custom Scheduler
Network Policy
1. Network Policy 的设计原理
Kubernetes Network Policy 的设计核心思想是:在默认允许的集群网络中,引入一个“默认拒绝”的、声明式的、基于标签的防火墙。
让我们来分解这个核心思想:
从“默认允许”到“默认拒绝”
- 默认行为:在没有任何 Network Policy 的情况下,Kubernetes 集群内的 Pod 之间是可以自由通信的(取决于 CNI 插件),甚至来自外部的流量也可能直接访问到 Pod。这就像在一个没有防火墙的开放网络里。
- Network Policy 的作用:一旦在某个 Namespace 中创建了一个 Network Policy,它就会像一个“开关”,将这个 Namespace 或特定 Pod 的默认行为变为 “默认拒绝”。之后,只有策略中明确允许的流量才能通过。
声明式模型
- 和其他的 Kubernetes 资源(如 Deployment、Service)一样,Network Policy 也是声明式的。你只需要告诉 Kubernetes“你期望的网络状态是什么”(例如,“允许来自带有
role=frontend 标签的 Pod 的流量访问带有 role=backend 标签的 Pod 的 6379 端口”),而不需要关心如何通过 iptables 或 eBPF 命令去实现它。Kubernetes 和其下的 CNI 插件会负责实现你的声明。
基于标签的选择机制
- 这是 Kubernetes 的核心设计模式。Network Policy 不关心 Pod 的 IP 地址,因为 IP 是动态且易变的。它通过 标签 来选择一组 Pod。
podSelector: 选择策略所应用的 Pod(即目标 Pod)。namespaceSelector: 根据命名空间的标签来选择来源或目标命名空间。namespaceSelector 和 podSelector 可以组合使用,实现非常精细的访问控制。
策略是叠加的
- 多个 Network Policy 可以同时作用于同一个 Pod。最终的规则是所有相关策略的 并集。如果任何一个策略允许了某条流量,那么该流量就是被允许的。这意味着你可以分模块、分层次地定义策略,而不会相互覆盖。
2. Network Policy 的实现方式
一个非常重要的概念是:Network Policy 本身只是一个 API 对象,它定义了一套规范。它的具体实现依赖于 Container Network Interface 插件。
Kubernetes 不会自己实现网络策略,而是由 CNI 插件来负责。这意味着:
- 如果你的 CNI 插件不支持 Network Policy,那么你创建的 Policy 将不会产生任何效果。
- 不同的 CNI 插件使用不同的底层技术来实现相同的 Network Policy 规范。
主流的实现方式和技术包括:
基于 iptables
- 工作原理:CNI 插件(如 Calico 的部分模式、Weave Net 等)会监听 Kubernetes API,当有 Network Policy 被创建时,它会在节点上生成相应的 iptables 规则。这些规则会对进出 Pod 网络接口(veth pair)的数据包进行过滤。
- 优点:成熟、稳定、通用。
- 缺点:当策略非常复杂时,iptables 规则链会变得很长,可能对性能有一定影响。
基于 eBPF
- 工作原理:这是更现代和高效的方式,被 Cilium 等项目广泛采用。eBPF 允许将程序直接注入到 Linux 内核中,在内核层面高效地执行数据包过滤、转发和策略检查。
- 优点:高性能、灵活性极强(可以实现 L3/L4/L7 所有层面的策略)、对系统影响小。
- 缺点:需要较新的 Linux 内核版本。
基于 IPVS 或自有数据平面
- 一些 CNI 插件(如 Antrea,它底层使用 OVS)可能有自己独立的数据平面,并在其中实现策略的匹配和执行。
常见的支持 Network Policy 的 CNI 插件:
- Calico: 功能强大,支持复杂的网络策略,既可以使用 iptables 模式也可以使用 eBPF 模式。
- Cilium: 基于 eBPF,原生支持 Network Policy,并扩展到了 L7(HTTP、gRPC 等)网络策略。
- Weave Net: 提供了对 Kubernetes Network Policy 的基本支持。
- Antrea: 基于 Open vSwitch,也提供了强大的策略支持。
3. Network Policy 的用途
Network Policy 是实现 Kubernetes “零信任” 或 “微隔离” 安全模型的核心工具。其主要用途包括:
实现最小权限原则
- 这是最核心的用途。通过精细的策略,确保一个 Pod 只能与它正常工作所 必需 的其他 Pod 或外部服务通信,除此之外的一切连接都被拒绝。这极大地减少了攻击面。
隔离多租户环境
- 在共享的 Kubernetes 集群中,可以为不同的团队、项目或环境(如 dev, staging)创建不同的命名空间。然后使用 Network Policy 严格限制跨命名空间的访问,确保它们相互隔离,互不干扰。
保护关键基础服务
- 数据库、缓存(如 Redis)、消息队列等后端服务通常不应该被所有 Pod 访问。可以创建策略,只允许特定的前端或中间件 Pod(通过标签选择)访问这些后端服务的特定端口。
# 示例:只允许 role=api 的 Pod 访问 role=db 的 Pod 的 5432 端口
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: api
ports:
- protocol: TCP
port: 5432
控制外部访问
- 使用
ipBlock 字段,可以限制只有来自特定 IP 段(例如公司办公室的 IP)的流量才能访问集群内部的服务。这可以用来替代或补充传统的防火墙规则。
划分应用层次安全边界
- 在一个典型的 Web 应用中,可以创建清晰的层次:
- 前端层: 可以接收来自外部的流量(80/443端口),但只能与后端层通信。
- 后端层: 只能接收来自前端层的流量,并只能与数据层通信。
- 数据层: 只能接收来自后端层的流量,不接受任何其他来源的请求。
总结
| 特性 | 描述 |
|---|
| 设计原理 | 在默认允许的网络中,通过声明式和基于标签的机制,实现“默认拒绝”的精细流量控制。 |
| 实现方式 | 由 CNI 插件负责实现,底层技术包括 iptables、eBPF 等。策略本身是 Kubernetes 的 API 资源。 |
| 主要用途 | 实现微隔离、最小权限原则、多租户隔离、保护关键服务和控制外部访问,是 Kubernetes 网络安全的基石。 |
简单来说,Network Policy 就是 Kubernetes 世界的防火墙规则,它让你能够定义“谁在什么条件下可以访问什么”,是生产环境中保障应用安全不可或缺的一部分。
Node NotReady
当 Kubernetes 中某些 Node 节点状态变为 NotReady 时,这往往意味着 kubelet 无法与控制平面(API Server)正常通信,或该节点上某些关键组件/资源异常。
我们可以从以下两个层面来分析:
1️⃣ 导致节点 NotReady 的常见原因
2️⃣ NotReady 状态对整个集群和业务的影响
🧩 一、Node NotReady 的常见原因分类
kubelet 每 10 秒(默认)向 API Server 报告一次心跳(NodeStatus)。
如果连续 40 秒(默认 --node-monitor-grace-period=40s)没有收到更新,Controller Manager 会将节点标记为 NotReady。
下面按类别详细分析👇
🖧 1. 网络层异常(最常见)
症状:节点能 ping 通外网,但与 control plane 交互超时。
原因包括:
- 节点与 kube-apiserver 之间的网络中断(如防火墙、路由异常、VPC 问题);
- API Server 负载均衡异常(L4/L7 LB 停止转发流量);
- Pod 网络插件(CNI)崩溃,kubelet 无法汇报 Pod 状态;
- 节点 DNS 解析异常(影响 kubelet 访问 API Server)。
排查方式:
# 在节点上检查 API Server 可达性
curl -k https://<apiserver-ip>:6443/healthz
# 检查 kubelet 日志
journalctl -u kubelet | grep -E "error|fail|timeout"
⚙️ 2. kubelet 本身异常
症状:节点长时间 NotReady,重启 kubelet 后恢复。
原因包括:
- kubelet 崩溃 / 死循环;
- 磁盘满,导致 kubelet 无法写临时目录(
/var/lib/kubelet); - 证书过期(
/var/lib/kubelet/pki/kubelet-client-current.pem); - CPU/Mem 资源耗尽,kubelet 被 OOM;
- kubelet 配置文件被改动,重启后加载失败。
排查方式:
systemctl status kubelet
journalctl -u kubelet -n 100
df -h /var/lib/kubelet
💾 3. 节点资源耗尽
症状:Node 状态为 NotReady 或 Unknown,Pod 被驱逐。
可能原因:
- 磁盘使用率 > 90%,触发 kubelet
DiskPressure; - 内存 / CPU 长期 100%,触发
MemoryPressure; - inode 用尽(
df -i); - 临时目录
/var/lib/docker/tmp 或 /tmp 爆满。
排查方式:
kubectl describe node <node-name>
# 查看 conditions
# Conditions:
# Type Status
# ---- ------
# MemoryPressure True
# DiskPressure True
🧱 4. 控制面通信问题(API Server / Controller Manager)
症状:多个节点同时 NotReady。
可能原因:
- API Server 压力过大,导致心跳包无法及时处理;
- etcd 异常(写延迟高);
- Controller Manager 无法更新 NodeStatus;
- 集群负载均衡器(如 haproxy)挂掉。
排查方式:
kubectl get componentstatuses
# 或直接检查控制平面节点
kubectl -n kube-system get pods -l tier=control-plane
🔌 5. 容器运行时 (containerd/docker/crio) 异常
症状:kubelet 报 “Failed to list pod sandbox”。
原因包括:
- containerd 服务挂掉;
- 版本不兼容(kubelet 与 runtime 版本差异过大);
- runtime socket 权限错误;
- overlayfs 损坏;
/var/lib/containerd 或 /run/containerd 文件系统只读。
排查方式:
systemctl status containerd
journalctl -u containerd | tail
crictl ps
⏱️ 6. 时间同步错误
症状:kubelet 心跳被判定过期,但实际节点正常。
原因:
- 节点时间漂移(未启用 NTP / chrony);
- 控制面和节点时间差 > 5 秒;
- TLS 校验失败(证书时间不合法)。
🧰 7. 节点维护或人为操作
包括:
- 节点被 cordon/drain;
- 网络策略阻断 kubelet;
- 人为停掉 kubelet;
- 节点被重装后未清理旧状态(Node UID 冲突)。
⚠️ 二、Node NotReady 的后果与影响
| 影响范围 | 描述 |
|---|
| 1️⃣ Pod 调度 | Scheduler 会避免调度新 Pod 到该节点。 |
| 2️⃣ Pod 驱逐 | Controller Manager 默认在节点 NotReady 超过 300s(--pod-eviction-timeout)后,会驱逐所有 Pod。 |
| 3️⃣ Service Endpoint 缺失 | 该节点上运行的 Pod 从 Service Endpoint 列表中移除,导致负载均衡流量下降。 |
| 4️⃣ DaemonSet 中断 | DaemonSet Controller 不再在该节点上创建/管理 Pod。 |
| 5️⃣ 数据丢失风险 | 若节点上的 Pod 使用本地卷(emptyDir、hostPath),被驱逐后数据会丢失。 |
| 6️⃣ 集群监控告警 | Prometheus / Alertmanager 触发告警(如 KubeNodeNotReady、KubeletDown)。 |
| 7️⃣ 自动扩缩容失效 | Cluster Autoscaler 无法正确评估资源利用率。 |
🧭 三、最佳实践与预防建议
启用 Node Problem Detector (NPD) 自动标记系统级异常;
监控 NodeConditions(Ready、MemoryPressure、DiskPressure);
统一节点健康检查策略(如通过 taints 与 tolerations);
自动修复机制:
- 结合 Cluster API 或自研 Controller 实现 Node 自动替换;
- 若节点 NotReady 超过 10 分钟,自动重建;
定期巡检:
- kubelet、containerd 状态;
- 系统时间同步;
- 磁盘使用率;
- API Server QPS 和 etcd 延迟。
Pause 容器
Kubernetes Pause 容器的用途
Pause 容器是 Kubernetes 中一个非常小但极其重要的基础设施容器。很多人会忽略它,但它是 Pod 网络和命名空间共享的核心。
🎯 核心作用
1. 作为 Pod 的"根容器"(Infrastructure Container)
Pause 容器是每个 Pod 中第一个启动的容器,它的生命周期代表整个 Pod 的生命周期。
Pod 生命周期:
创建 Pod → 启动 Pause 容器 → 启动业务容器 → ... → 业务容器结束 → 删除 Pause 容器 → Pod 销毁
2. 持有和共享 Linux 命名空间
Pause 容器创建并持有以下命名空间,供 Pod 内其他容器共享:
- Network Namespace (网络命名空间) - 最重要!
- IPC Namespace (进程间通信)
- UTS Namespace (主机名)
# 查看 Pod 中的容器
docker ps | grep pause
# 你会看到类似输出:
# k8s_POD_mypod_default_xxx k8s.gcr.io/pause:3.9
# k8s_app_mypod_default_xxx myapp:latest
🌐 网络命名空间共享(最关键的用途)
工作原理
┌─────────────────── Pod ───────────────────┐
│ │
│ ┌─────────────┐ │
│ │ Pause │ ← 创建网络命名空间 │
│ │ Container │ ← 拥有 Pod IP │
│ └──────┬──────┘ │
│ │ (共享网络栈) │
│ ┌──────┴──────┬──────────┬──────────┐ │
│ │ Container A │Container B│Container C│ │
│ │ (业务容器) │ (业务容器)│ (业务容器) │ │
│ └─────────────┴──────────┴──────────┘ │
│ │
│ 所有容器共享: │
│ - 同一个 IP 地址 (Pod IP) │
│ - 同一个网络接口 │
│ - 同一个端口空间 │
│ - 可以通过 localhost 互相访问 │
└────────────────────────────────────────────┘
实际效果
# 示例 Pod
apiVersion: v1
kind: Pod
metadata:
name: multi-container-pod
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
- name: sidecar
image: busybox
command: ['sh', '-c', 'while true; do wget -O- localhost:80; sleep 5; done']
在这个例子中:
- Pause 容器创建网络命名空间并获得 Pod IP (如 10.244.1.5)
- nginx 容器加入这个网络命名空间,监听 80 端口
- sidecar 容器也加入同一网络命名空间
- sidecar 可以通过
localhost:80 访问 nginx,因为它们共享网络栈
🔍 为什么需要 Pause 容器?
问题场景:如果没有 Pause 容器
假设 Pod 中有两个容器 A 和 B:
场景 1:容器 A 先启动,创建网络命名空间
├─ 容器 A 持有网络命名空间 → 拥有 Pod IP
└─ 容器 B 加入容器 A 的网络命名空间
问题:如果容器 A 崩溃重启或被删除,网络命名空间消失
→ 容器 B 失去网络连接
→ Pod IP 改变
→ Service 路由失效 ❌
解决方案:引入 Pause 容器
Pause 容器(持有命名空间) ← 永远不会主动退出
├─ 容器 A 加入
└─ 容器 B 加入
优势:
✅ 容器 A 或 B 崩溃不影响网络命名空间
✅ Pod IP 始终保持稳定
✅ 业务容器可以独立重启
✅ 简化容器间的依赖关系
📦 Pause 容器的特点
1. 极其精简
# pause 容器的代码只有几十行 C 代码
// 核心功能就是:永远 sleep
int main() {
for (;;) pause(); // 无限暂停,等待信号
return 0;
}
镜像大小:约 700KB (相比普通镜像动辄几百 MB)
2. 资源占用极低
# 查看 Pause 容器资源占用
docker stats <pause-container-id>
# 典型输出:
# CPU: 0.00%
# MEM: 0.5 MiB
3. 生命周期管理
- Kubelet 创建 Pod 时首先启动 Pause 容器
- Pause 容器退出 = Pod 销毁
- 业务容器重启不影响 Pause 容器
🛠️ 实际用途场景
场景 1:Sidecar 模式
# 应用 + 日志收集器
spec:
containers:
- name: app
image: myapp
volumeMounts:
- name: logs
mountPath: /var/log
- name: log-collector
image: fluentd
volumeMounts:
- name: logs
mountPath: /var/log
- Pause 容器保证两个容器可以通过共享卷和 localhost 通信
- 即使 app 重启,log-collector 仍能正常工作
场景 2:Service Mesh (如 Istio)
# 应用 + Envoy 代理
spec:
containers:
- name: app
image: myapp
ports:
- containerPort: 8080
- name: istio-proxy # Envoy sidecar
image: istio/proxyv2
- Pause 容器持有网络命名空间
- Envoy 代理拦截所有进出流量
- 应用无需感知代理存在
场景 3:初始化和主容器协作
spec:
initContainers:
- name: init-config
image: busybox
command: ['sh', '-c', 'echo "config" > /config/app.conf']
volumeMounts:
- name: config
mountPath: /config
containers:
- name: app
image: myapp
volumeMounts:
- name: config
mountPath: /config
- Pause 容器在整个过程中保持网络和 IPC 命名空间稳定
🔧 查看和调试 Pause 容器
查看 Pod 中的 Pause 容器
# 方式 1:通过 crictl (推荐)
crictl pods
crictl ps -a | grep pause
# 方式 2:通过 docker (如果使用 Docker 运行时)
docker ps -a | grep pause
docker inspect <pause-container-id>
# 方式 3:查看 Pod 的容器列表
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[*].name}'
Pause 镜像版本
# 查看 kubelet 使用的 pause 镜像
kubectl get pod <pod-name> -o yaml | grep pause
# 常见版本:
# registry.k8s.io/pause:3.9 (最新)
# registry.k8s.io/pause:3.8
# k8s.gcr.io/pause:3.7
⚠️ 常见问题
Q1: 为什么看不到 Pause 容器?
A: 使用 kubectl get pods 不会显示 Pause 容器,它对用户是透明的。需要使用底层运行时命令(如 crictl 或 docker ps)才能看到。
Q2: Pause 容器会影响性能吗?
A: 几乎不会。Pause 容器只占用约 0.5MB 内存,CPU 使用为 0,不执行任何业务逻辑。
Q3: 可以删除 Pause 容器吗?
A: 不能手动删除。删除 Pause 容器会导致整个 Pod 被销毁。
Q4: 不同 Pod 的 Pause 容器是否共享?
A: 不共享。每个 Pod 都有独立的 Pause 容器,确保 Pod 之间的网络和命名空间隔离。
📝 总结
| 用途 | 说明 |
|---|
| 命名空间持有者 | 创建并持有 Network、IPC、UTS 命名空间 |
| 网络基础 | 使 Pod 内所有容器共享同一 IP 和网络栈 |
| 生命周期锚点 | 代表 Pod 的生命周期,容器可独立重启 |
| 简化架构 | 解耦容器间依赖,避免级联故障 |
| 资源高效 | 极小的镜像和资源占用 |
核心价值: Pause 容器是 Kubernetes Pod 抽象的基石,让多个容器能像在同一主机上一样协作,同时保持各自的独立性和可重启性。
需要我详细讲解某个具体场景或深入探讨实现原理吗? 🚀
Pod在K8S中DNS解析流程和顺序
核心概念
- CoreDNS: 从Kubernetes 1.11开始,CoreDNS是默认的DNS服务。它作为一个或多个Pod运行在kube-system命名空间下,并配有一个Kubernetes Service(通常叫
kube-dns)。 - resolv.conf 文件: 每个Pod的
/etc/resolv.conf文件是DNS解析的蓝图。Kubelet会自动生成这个文件并挂载到Pod中。 - DNS策略: 你可以通过Pod Spec中的
dnsPolicy字段来配置DNS策略。
Pod 的 /etc/resolv.conf 解析
这是一个典型的Pod内的/etc/resolv.conf文件内容:
nameserver 10.96.0.10
search <namespace>.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
让我们逐行分析:
1. nameserver 10.96.0.10
- 这是CoreDNS Service的集群IP地址。所有Pod的DNS查询默认都会发送到这个地址。
- 这个IP来自kubelet的
--cluster-dns标志,在启动时确定。
2. search <namespace>.svc.cluster.local svc.cluster.local cluster.local
- 搜索域列表。当你使用不完整的域名(即不是FQDN)时,系统会按照这个列表的顺序,依次将搜索域附加到主机名后面,直到找到匹配的记录。
<namespace>是你的Pod所在的命名空间,例如default。- 搜索顺序:
<pod-namespace>.svc.cluster.localsvc.cluster.localcluster.local
3. options ndots:5
- 这是一个关键的优化/控制选项。
- 规则: 如果一个域名中的点(
.)数量大于或等于这个值(这里是5),系统会将其视为绝对域名(FQDN),并首先尝试直接解析,不会走搜索域列表。 - 反之,如果点数少于5,系统会先依次尝试搜索域,如果都失败了,最后再尝试名称本身。
DNS 解析流程与顺序(详解)
假设你的Pod在default命名空间,并且resolv.conf如上所示。
场景1:解析Kubernetes Service(短名称)
你想解析同一个命名空间下的Service:my-svc。
- 应用程序请求解析
my-svc。 - 系统检查名称
my-svc,点数(0) < 5。 - 进入搜索流程:
- 第一次尝试:
my-svc.default.svc.cluster.local -> 成功! 返回ClusterIP。 - 解析结束。
场景2:解析不同命名空间的Service
你想解析另一个命名空间prod下的Service:my-svc.prod。
- 应用程序请求解析
my-svc.prod。 - 系统检查名称
my-svc.prod,点数(1) < 5。 - 进入搜索流程:
- 第一次尝试:
my-svc.prod.default.svc.cluster.local -> 失败(因为该Service不在default命名空间)。 - 第二次尝试:
my-svc.prod.svc.cluster.local -> 成功! 返回ClusterIP。 - 解析结束。
场景3:解析外部域名(例如 www.google.com)
- 应用程序请求解析
www.google.com。 - 系统检查名称
www.google.com,点数(3) < 5。 - 进入搜索流程:
- 第一次尝试:
www.google.com.default.svc.cluster.local -> 失败。 - 第二次尝试:
www.google.com.svc.cluster.local -> 失败。 - 第三次尝试:
www.google.com.cluster.local -> 失败。
- 所有搜索域都失败了,系统最后尝试名称本身:
www.google.com -> 成功! CoreDNS会将其转发给上游DNS服务器(例如宿主机上的DNS或网络中配置的DNS)。
场景4:解析被认为是FQDN的域名(点数 >= 5)
假设你有一个StatefulSet,Pod的FQDN是web-0.nginx.default.svc.cluster.local。
- 应用程序请求解析
web-0.nginx.default.svc.cluster.local。 - 系统检查名称,点数(4) < 5?注意:这里是4个点,仍然小于5! 所以它仍然会走搜索流程。
- 这会先尝试
web-0.nginx.default.svc.cluster.local.default.svc.cluster.local,显然是错误的。 - 为了避免这种低效行为,最佳实践是在应用程序中配置或使用绝对域名(尾部带点)。
绝对域名示例:
应用程序请求解析 web-0.nginx.default.svc.cluster.local.(注意最后有一个点)。
- 系统识别其为FQDN,直接查询,不经过任何搜索域。这是最有效的方式。
DNS 策略
Pod的dnsPolicy字段决定了如何生成resolv.conf。
ClusterFirst(默认): DNS查询首先被发送到Kubernetes集群的CoreDNS。如果域名不在集群域内(例如cluster.local),查询会被转发到上游DNS。ClusterFirstWithHostNet: 对于使用hostNetwork: true的Pod,如果你想让它使用集群DNS,就需要设置这个策略。Default: Pod直接从宿主机继承DNS配置(即使用宿主的/etc/resolv.conf)。这意味着它不会使用CoreDNS。None: 忽略所有默认的DNS设置。你必须使用dnsConfig字段来提供自定义的DNS配置。
总结与流程图
解析顺序可以简化为以下决策流程:
flowchart TD
A[应用程序发起DNS查询] --> B{查询名称的<br>点数 '.' >= 5?}
B -- 是<br>(视为FQDN) --> C[直接查询该名称]
C --> D{解析成功?}
D -- 是 --> E[返回结果]
D -- 否 --> F[解析失败]
B -- 否<br>(视为短名称) --> G
subgraph G [循环搜索域列表]
direction LR
H[依次将搜索域附加<br>到名称后并查询] --> I{解析成功?}
I -- 是 --> J[返回结果]
end
I -- 循环结束仍失败 --> K[直接查询原始名称]
K --> L{解析成功?}
L -- 是 --> E
L -- 否 --> F关键要点:
- 默认流向: Pod -> CoreDNS Service -> CoreDNS Pod -> (根据域判断)返回K8s记录或转发到上游DNS。
- 搜索域顺序: 命名空间 ->
svc -> cluster.local。 ndots:5的影响: 这是为了在便利性和性能之间取得平衡。对于需要频繁访问的外部域名,为了性能最好在应用程序中配置FQDN(尾部带点)或调整ndots选项。- 调试技巧: 进入Pod并执行
cat /etc/resolv.conf和nslookup或dig命令是诊断DNS问题的第一步。
当执行kubectl exec 命令时,发生了什么?
kubectl exec 的实现原理涉及多个组件协同工作,以下是详细原理分析:
1. 整体架构流程
用户 -> kubectl -> API Server -> Kubelet -> 容器运行时 -> 目标容器
2. 详细执行步骤
步骤1:kubectl 客户端处理
kubectl exec -it <pod-name> -- /bin/bash
- kubectl 解析命令参数
- 构造 Exec API 请求
- 建立与 API Server 的长连接
步骤2:API Server 处理
// API 路径示例
POST /api/v1/namespaces/{namespace}/pods/{name}/exec
- 认证和授权检查
- 验证用户是否有 exec 权限
- 查找目标 Pod 所在节点
- 将请求代理到对应节点的 Kubelet
步骤3:Kubelet 处理
// Kubelet 的 exec 处理逻辑
func (h *ExecHandler) serveExec(w http.ResponseWriter, req *http.Request) {
// 获取容器信息
// 调用容器运行时接口
// 建立数据流传输
}
- 通过 CRI(Container Runtime Interface)调用容器运行时
- 创建到容器的连接
- 管理标准输入、输出、错误流
步骤4:容器运行时执行
// CRI 接口定义
service RuntimeService {
rpc Exec(ExecRequest) returns (ExecResponse) {}
}
- Docker: 使用
docker exec 底层机制 - Containerd: 通过 task 执行命令
- CRI-O: 通过 conmon 管理执行会话
3. 关键技术机制
3.1 流式传输协议
// 使用 SPDY 或 WebSocket 协议
// 支持多路复用的数据流
type StreamProtocol interface {
Stream(stdin io.Reader, stdout, stderr io.Writer) error
}
3.2 终端处理(TTY)
// 伪终端配置
type ExecOptions struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
TTY bool
ptyMaster *os.File
}
3.3 会话管理
// ExecSession 管理执行会话
type ExecSession struct {
id string
stdinPipe io.WriteCloser
stdoutPipe io.ReadCloser
stderrPipe io.ReadCloser
done chan struct{}
}
4. 网络通信流程
客户端 (kubectl)
↓ HTTPS with SPDY/WebSocket
API Server
↓ 代理连接
Kubelet (节点)
↓ CRI gRPC
容器运行时
↓ 容器命名空间
目标容器进程
5. 安全机制
5.1 认证授权
# RBAC 配置示例
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-exec
rules:
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
5.2 安全上下文
// 安全配置
securityContext := &v1.SecurityContext{
RunAsUser: &uid,
RunAsGroup: &gid,
Capabilities: &v1.Capabilities{
Drop: []v1.Capability{"ALL"},
},
}
6. 实际代码示例
kubectl 端实现
func (o *ExecOptions) Run() error {
// 建立与 API Server 的连接
executor, err := remotecommand.NewSPDYExecutor(
o.Config, "POST", req.URL())
// 执行命令
return executor.Stream(remotecommand.StreamOptions{
Stdin: o.In,
Stdout: o.Out,
Stderr: o.ErrOut,
Tty: o.TTY,
})
}
Kubelet 端处理
func (h *ExecHandler) serveExec(w http.ResponseWriter, req *http.Request) {
// 获取容器 ID
containerID := podContainer.ContainerID
// 通过 CRI 执行命令
execRequest := &runtimeapi.ExecRequest{
ContainerId: containerID.ID,
Cmd: cmd,
Tty: tty,
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
}
// 调用容器运行时
runtimeService.Exec(execRequest)
}
7. 容器运行时差异
Docker
// 使用 Docker Engine API
client.ContainerExecCreate()
client.ContainerExecAttach()
Containerd
8. 故障排查要点
- 权限问题: 检查 RBAC 配置
- 网络连通性: API Server ↔ Kubelet 网络
- 容器状态: 目标容器必须处于 Running 状态
- 资源限制: 容器资源是否充足
- 安全策略: Pod Security Policies 限制
这种设计使得 kubectl exec 能够在分布式环境中安全、可靠地执行容器内命令,同时保持了良好的用户体验。
QoS 详解
Kubernetes QoS (Quality of Service) 等级详解
QoS 等级是 Kubernetes 用来管理 Pod 资源和在资源不足时决定驱逐优先级的机制。
🎯 三种 QoS 等级
Kubernetes 根据 Pod 的资源配置自动分配 QoS 等级,共有三种:
1. Guaranteed (保证型) - 最高优先级
2. Burstable (突发型) - 中等优先级
3. BestEffort (尽力而为型) - 最低优先级
📊 QoS 等级详解
1️⃣ Guaranteed (保证型)
定义条件(必须同时满足)
- Pod 中每个容器(包括 Init 容器)都必须设置
requests 和 limits - 对于每个容器,CPU 和内存的
requests 必须等于 limits
YAML 示例
apiVersion: v1
kind: Pod
metadata:
name: guaranteed-pod
spec:
containers:
- name: app
image: nginx
resources:
requests:
memory: "200Mi"
cpu: "500m"
limits:
memory: "200Mi" # 必须等于 requests
cpu: "500m" # 必须等于 requests
特点
✅ 资源保证:Pod 获得请求的全部资源,不会被其他 Pod 抢占
✅ 最高优先级:资源不足时最后被驱逐
✅ 性能稳定:资源使用可预测,适合关键业务
✅ OOM 保护:不会因为节点内存压力被 Kill(除非超过自己的 limit)
适用场景
- 数据库(MySQL, PostgreSQL, Redis)
- 消息队列(Kafka, RabbitMQ)
- 核心业务应用
- 有状态服务
2️⃣ Burstable (突发型)
定义条件(满足以下任一条件)
- Pod 中至少有一个容器设置了
requests 或 limits requests 和 limits 不相等- 部分容器设置了资源限制,部分没有
YAML 示例
场景 1:只设置 requests
apiVersion: v1
kind: Pod
metadata:
name: burstable-pod-1
spec:
containers:
- name: app
image: nginx
resources:
requests:
memory: "100Mi"
cpu: "200m"
# 没有设置 limits,可以使用超过 requests 的资源
场景 2:requests < limits
apiVersion: v1
kind: Pod
metadata:
name: burstable-pod-2
spec:
containers:
- name: app
image: nginx
resources:
requests:
memory: "100Mi"
cpu: "200m"
limits:
memory: "500Mi" # 允许突发到 500Mi
cpu: "1000m" # 允许突发到 1 核
场景 3:混合配置
apiVersion: v1
kind: Pod
metadata:
name: burstable-pod-3
spec:
containers:
- name: app1
image: nginx
resources:
requests:
memory: "100Mi"
limits:
memory: "200Mi"
- name: app2
image: busybox
resources:
requests:
cpu: "100m"
# 只设置 CPU,没有内存限制
特点
✅ 弹性使用:可以使用超过 requests 的资源(burst)
⚠️ 中等优先级:资源不足时,在 BestEffort 之后被驱逐
⚠️ 可能被限流:超过 limits 会被限制(CPU)或 Kill(内存)
✅ 成本优化:平衡资源保证和利用率
适用场景
- Web 应用(流量有波峰波谷)
- 定时任务
- 批处理作业
- 微服务(大部分场景)
3️⃣ BestEffort (尽力而为型)
定义条件
- Pod 中所有容器都没有设置
requests 和 limits
YAML 示例
apiVersion: v1
kind: Pod
metadata:
name: besteffort-pod
spec:
containers:
- name: app
image: nginx
# 完全没有 resources 配置
- name: sidecar
image: busybox
# 也没有 resources 配置
特点
❌ 无资源保证:能用多少资源完全看节点剩余
❌ 最低优先级:资源不足时第一个被驱逐
❌ 性能不稳定:可能被其他 Pod 挤占资源
✅ 灵活性高:可以充分利用节点空闲资源
适用场景
- 开发测试环境
- 非关键后台任务
- 日志收集(可以容忍中断)
- 临时性工作负载
🔍 QoS 等级判定流程图
开始
│
├─→ 所有容器都没设置 requests/limits?
│ └─→ 是 → BestEffort
│
├─→ 所有容器的 requests == limits (CPU和内存)?
│ └─→ 是 → Guaranteed
│
└─→ 其他情况 → Burstable
🚨 资源不足时的驱逐顺序
当节点资源不足(如内存压力)时,Kubelet 按以下顺序驱逐 Pod:
驱逐优先级(从高到低):
1. BestEffort Pod
└─→ 超出 requests 最多的先被驱逐
2. Burstable Pod
└─→ 按内存使用量排序
└─→ 超出 requests 越多,越先被驱逐
3. Guaranteed Pod (最后才驱逐)
└─→ 只有在没有其他选择时才驱逐
实际驱逐示例
# 节点内存不足场景:
节点总内存: 8GB
已用内存: 7.8GB (达到驱逐阈值)
Pod 列表:
- Pod A (BestEffort): 使用 1GB 内存 → 第一个被驱逐 ❌
- Pod B (Burstable): requests=200Mi, 使用 500Mi → 第二个 ❌
- Pod C (Burstable): requests=500Mi, 使用 600Mi → 第三个 ❌
- Pod D (Guaranteed): requests=limits=1GB, 使用 1GB → 保留 ✅
📝 查看 Pod 的 QoS 等级
方法 1:使用 kubectl describe
kubectl describe pod <pod-name>
# 输出中会显示:
# QoS Class: Burstable
方法 2:使用 kubectl get
# 查看所有 Pod 的 QoS
kubectl get pods -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass
# 输出:
# NAME QOS
# nginx-guaranteed Guaranteed
# app-burstable Burstable
# test-besteffort BestEffort
方法 3:使用 YAML 输出
kubectl get pod <pod-name> -o yaml | grep qosClass
# 输出:
# qosClass: Burstable
🎨 QoS 配置最佳实践
生产环境推荐配置
关键业务 - Guaranteed
apiVersion: apps/v1
kind: Deployment
metadata:
name: critical-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: myapp:v1
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "2Gi" # requests == limits
cpu: "1000m"
一般业务 - Burstable
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 5
template:
spec:
containers:
- name: web
image: nginx:latest
resources:
requests:
memory: "256Mi" # 保证最低资源
cpu: "200m"
limits:
memory: "512Mi" # 允许突发到 2 倍
cpu: "500m"
后台任务 - BestEffort 或 Burstable
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanup-job
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: cleanup
image: cleanup:v1
resources:
requests:
memory: "128Mi"
cpu: "100m"
# 不设置 limits,允许使用空闲资源
🔧 QoS 与资源限制的关系
CPU 限制行为
resources:
requests:
cpu: "500m" # 保证至少 0.5 核
limits:
cpu: "1000m" # 最多使用 1 核
- requests:节点调度的依据,保证的资源
- limits:硬限制,超过会被限流(throttle),但不会被 Kill
- 超过 limits 时,进程会被 CPU throttle,导致性能下降
内存限制行为
resources:
requests:
memory: "256Mi" # 保证至少 256Mi
limits:
memory: "512Mi" # 最多使用 512Mi
- requests:调度保证,但可以使用更多
- limits:硬限制,超过会触发 OOM Kill 💀
- Pod 会被标记为
OOMKilled 并重启
🛠️ 常见问题
Q1: 为什么我的 Pod 总是被驱逐?
# 检查 QoS 等级
kubectl get pod <pod-name> -o yaml | grep qosClass
# 如果是 BestEffort 或 Burstable,建议:
# 1. 设置合理的 requests
# 2. 考虑升级到 Guaranteed(关键服务)
# 3. 增加节点资源
Q2: 如何为所有 Pod 设置默认资源限制?
# 使用 LimitRange
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: default
spec:
limits:
- default: # 默认 limits
cpu: "500m"
memory: "512Mi"
defaultRequest: # 默认 requests
cpu: "100m"
memory: "128Mi"
type: Container
Q3: Guaranteed Pod 也会被驱逐吗?
会! 但只在以下情况:
- 使用超过自己的
limits(OOM Kill) - 节点完全不可用(如节点宕机)
- 手动删除 Pod
- DaemonSet 或系统级 Pod 需要资源
Q4: 如何监控 QoS 相关的问题?
# 查看节点资源压力
kubectl describe node <node-name> | grep -A 5 "Conditions:"
# 查看被驱逐的 Pod
kubectl get events --field-selector reason=Evicted
# 查看 OOM 事件
kubectl get events --field-selector reason=OOMKilling
📊 QoS 等级对比表
| 维度 | Guaranteed | Burstable | BestEffort |
|---|
| 配置要求 | requests=limits | requests≠limits 或部分配置 | 无配置 |
| 资源保证 | ✅ 完全保证 | ⚠️ 部分保证 | ❌ 无保证 |
| 驱逐优先级 | 最低(最后驱逐) | 中等 | 最高(第一个驱逐) |
| 性能稳定性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
| 资源利用率 | 低(固定资源) | 高(可突发) | 最高(充分利用) |
| 成本 | 高 | 中 | 低 |
| 适用场景 | 关键业务 | 一般业务 | 测试/临时任务 |
🎯 选型建议
使用 Guaranteed 的场景
- 🗄️ 数据库(MySQL, MongoDB, Cassandra)
- 📨 消息队列(Kafka, RabbitMQ)
- 🔐 认证服务
- 💰 支付系统
- 📊 实时数据处理
使用 Burstable 的场景
- 🌐 Web 应用(80% 的场景)
- 🔄 API 服务
- 🎨 前端应用
- 📦 微服务
- ⚙️ 后台处理
使用 BestEffort 的场景
- 🧪 开发测试
- 📝 日志收集(可容忍中断)
- 🔍 数据探索
- 🛠️ 一次性脚本
💡 关键要点总结
- QoS 是自动分配的,不能手动指定,由资源配置决定
- Guaranteed ≠ 不会被驱逐,只是优先级最低
- 生产环境建议至少使用 Burstable,避免 BestEffort
- requests 影响调度,limits 影响运行时限制
- 内存超限会 OOM,CPU 超限会限流
- 使用 LimitRange 强制资源限制,避免 BestEffort Pod
Scheduler
Kubernetes 调度器(kube-scheduler) 是整个系统中非常关键的组件,它负责决定 哪个 Pod 应该运行在哪个 Node 上。
下面我会分层、逐步详细说明 K8s 调度流程(以 v1.28+ 为例),并解释背后机制。
🌐 整体架构概览
Kubernetes 调度器主要完成以下职责:
- 监听待调度的 Pod(即
spec.nodeName 为空的 Pod) - 为 Pod 选择最合适的 Node
- 将绑定结果写回到 apiserver
🧩 一、调度总体流程
Kubernetes 调度流程主要分为三个阶段:
[Pending Pod] --> [Scheduling Queue]
↓
[PreFilter] → [Filter] → [PostFilter] → [Score] → [Reserve] → [Permit] → [Bind]
1️⃣ 调度入口:监听未绑定的 Pod
- Scheduler 通过 informer 监听所有
Pod 资源。 - 当发现 Pod 没有
spec.nodeName 时,认为它是待调度的。 - Pod 被放入 调度队列(SchedulingQueue) 中。
🧮 二、调度核心阶段详解
🧩 1. PreFilter 阶段
在调度之前,对 Pod 进行一些准备性检查,例如:
- 解析 Pod 所需的资源。
- 检查 PVC、Affinity、Taint/Toleration 是否合理。
- 计算调度所需的 topology spread 信息。
🧠 类似于“预处理”,提前准备好过滤阶段要用的数据。
🧩 2. Filter 阶段(Predicates)
Scheduler 遍历所有可调度的 Node,筛选出满足条件的节点。
常见的过滤插件包括:
| 插件 | 作用 |
|---|
NodeUnschedulable | 过滤掉被标记 unschedulable 的节点 |
NodeName | 如果 Pod 指定了 nodeName,只匹配该节点 |
TaintToleration | 检查 taint / toleration 是否匹配 |
NodeAffinity / PodAffinity | 检查亲和性/反亲和性 |
NodeResourcesFit | 检查 CPU/Memory 等资源是否够用 |
VolumeBinding | 检查 Pod 使用的 PVC 是否能在节点挂载 |
🔎 输出结果:
得到一个候选节点列表(通常是几十个或几百个)。
🧩 3. PostFilter 阶段
- 若没有节点符合条件(即调度失败),进入 抢占逻辑(Preemption)。
- 调度器会尝试在某些节点上“抢占”低优先级的 Pod,以便高优先级 Pod 能调度成功。
🧩 4. Score 阶段(优选)
对剩余候选节点进行打分。
每个插件给节点打分(0–100),然后汇总加权。
常见的评分插件:
| 插件 | 作用 |
|---|
LeastAllocated | 资源使用最少的节点得分高 |
BalancedAllocation | CPU 和内存使用更均衡的节点得分高 |
NodeAffinity | 符合 affinity 的节点加分 |
ImageLocality | 本地已缓存镜像的节点加分 |
InterPodAffinity | 满足 Pod 间亲和性需求的节点加分 |
这些分数会经过 NormalizeScore 标准化到统一范围后求和。
最终输出:
最优节点(Score最高)
🧩 5. Reserve 阶段
暂时在该节点上 预留资源(在调度器内部缓存中标记),防止并发调度冲突。
如果后续失败,会执行 Unreserve 回滚。
🧩 6. Permit 阶段
某些插件可在绑定前再进行校验或等待,例如:
- PodGroup(批调度)
- Scheduler Framework 自定义策略
可能返回:
Success → 继续绑定Wait → 等待事件Reject → 放弃调度
🧩 7. Bind 阶段
最终由 Bind 插件 调用 API 将 Pod 绑定到 Node:
绑定成功后,kubelet 监听到该 Pod,会拉起容器。
⚙️ 三、调度框架(Scheduler Framework)
K8s 1.19+ 后,调度器是通过 插件化框架 实现的。
每个阶段都有对应的插件点:
| 阶段 | 插件接口 | 示例插件 |
|---|
| PreFilter | PreFilterPlugin | PodTopologySpread |
| Filter | FilterPlugin | NodeAffinity |
| Score | ScorePlugin | LeastAllocated |
| Reserve | ReservePlugin | VolumeBinding |
| Bind | BindPlugin | DefaultBinder |
你可以通过写一个自定义调度插件(Go)扩展调度逻辑。
🧭 四、调度失败的情况
常见调度失败的原因:
| 原因 | 表现 |
|---|
| 所有节点资源不足 | Pod 一直 Pending |
| 亲和性限制太严格 | Pod 无法找到符合要求的节点 |
| PVC 无法绑定 | VolumeBinding 阶段失败 |
| 节点被打 taint | 没有 toleration |
| 镜像拉取失败 | Pod 已绑定但容器起不来(kubelet问题) |
🧠 五、总结
| 阶段 | 目的 | 关键点 |
|---|
| SchedulingQueue | 缓冲待调度Pod | FIFO + 优先级调度 |
| PreFilter | 准备数据 | 校验Pod需求 |
| Filter | 过滤节点 | 资源与约束 |
| Score | 打分选优 | 平衡与局部性 |
| Reserve | 预留资源 | 防并发冲突 |
| Bind | 绑定Node | 调度结果落地 |
服务发现
最常见的说法是 “两种核心机制”,但这指的是服务发现的两种基本模式,而不是具体的实现方式。
维度一:两种核心模式
这是从服务发现的基本原理上划分的。
基于客户端服务发现
- 工作原理:客户端(服务消费者)通过查询一个中心化的服务注册中心(如 Consul、Eureka、Zookeeper)来获取所有可用服务实例的列表(通常是 IP 和端口),然后自己选择一个实例并直接向其发起请求。
- 类比:就像你去餐厅吃饭,先看门口的电子菜单(服务注册中心)了解所有菜品和价格,然后自己决定点什么,再告诉服务员。
- 特点:客户端需要内置服务发现逻辑,与服务注册中心耦合。这种方式更灵活,但增加了客户端的复杂性。
基于服务端服务发现
- 工作原理:客户端不关心具体的服务实例,它只需要向一个固定的访问端点(通常是 Load Balancer 或 Proxy,如 Kubernetes Service)发起请求。这个端点负责去服务注册中心查询可用实例,并进行负载均衡,将请求转发给其中一个。
- 类比:就像你去餐厅直接告诉服务员“来份招牌菜”,服务员(负载均衡器)帮你和后厨(服务实例)沟通,最后把菜端给你。
- 特点:客户端无需知道服务发现的具体细节,简化了客户端。这是 Kubernetes 默认采用的方式。
维度二:Kubernetes 中具体的实现方式
在 Kubernetes 内部,我们通常讨论以下几种具体的服务发现实现手段,它们共同构成了 Kubernetes 强大的服务发现能力。
1. 环境变量
当 Pod 被调度到某个节点上时,kubelet 会为当前集群中存在的每个 Service 添加一组环境变量到该 Pod 中。
- 格式:
{SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT。 - 例子:一个名为
redis-master 的 Service 会生成 REDIS_MASTER_SERVICE_HOST=10.0.0.11 和 REDIS_MASTER_SERVICE_PORT=6379 这样的环境变量。 - 局限性:环境变量必须在 Pod 创建之前就存在。后创建的 Service 无法将环境变量注入到已运行的 Pod 中。因此,这通常作为辅助手段。
2. DNS(最核心、最推荐的方式)
这是 Kubernetes 最主要和最优雅的服务发现方式。
- 工作原理:Kubernetes 集群内置了一个 DNS 服务器(通常是 CoreDNS)。当你创建一个 Service 时,Kubernetes 会自动为这个 Service 注册一个 DNS 记录。
- DNS 记录格式:
- 同一命名空间:
<service-name>.<namespace>.svc.cluster.local -> 指向 Service 的 Cluster IP。- 在同一个命名空间内,你可以直接使用
<service-name> 来访问服务。例如,前端 Pod 访问后端服务,只需使用 http://backend-service。
- 不同命名空间:需要使用全限定域名,例如
backend-service.production.svc.cluster.local。
- 优点:行为符合标准,应用无需修改代码,直接使用域名即可访问其他服务。
3. Kubernetes Service
Service 资源对象本身就是服务发现的载体。它提供了一个稳定的访问端点(VIP 或 DNS 名称),背后对应一组动态变化的 Pod。
- ClusterIP:默认类型,提供一个集群内部的虚拟 IP,只能从集群内部访问。结合 DNS 使用,是服务间通信的基石。
- NodePort:在 ClusterIP 基础上,在每个节点上暴露一个静态端口。可以从集群外部通过
<NodeIP>:<NodePort> 访问服务。 - LoadBalancer:在 NodePort 基础上,利用云服务商提供的负载均衡器,将一个外部 IP 地址暴露给 Service。是向公网暴露服务的主要方式。
- Headless Service:一种特殊的 Service,当你不需要负载均衡和单个 Service IP 时,可以通过设置
clusterIP: None 来创建。DNS 查询会返回该 Service 后端所有 Pod 的 IP 地址列表,而不是一个 VIP。这常用于有状态应用(如 Kafka、MySQL 集群)的自定义负载均衡或状态同步。
4. Ingress
虽然 Ingress 主要被用作 HTTP/HTTPS 路由规则的七层代理,但它也是一种高级的服务发现形式。
- 它通过规则将外部流量路由到集群内部相应的 Service。
- 客户端(外部用户)通过访问 Ingress Controller 的地址来发现和访问后端服务。
总结与对比
| 方式 | 原理 | 适用场景 | 特点 |
|---|
| 环境变量 | 将 Service 信息注入 Pod 环境变量 | 旧式应用,辅助手段 | 简单,但有局限性(需先于 Pod 创建) |
| DNS | 为 Service 自动注册域名 | 服务间通信的标准方式 | 推荐,符合惯例,无需代码改造 |
| Service | 提供稳定的虚拟 IP 或 DNS 名称 | 服务暴露和负载均衡的核心抽象 | Kubernetes 服务发现的基石 |
| Ingress | 七层 HTTP 路由 | 对外暴露 Web 服务,基于域名和路径路由 | 更高级的 API 网关模式 |
结论:
- 从模式上讲,Kubernetes 主要采用基于服务端的服务发现模式。
- 从具体实现上讲,Kubernetes 的服务发现是一个以 DNS 为核心、以 Service 为基石的完整体系,并辅以环境变量和 Ingress 等机制。
所以,当有人问“K8s服务发现有哪些方式”时,最准确的回答是:主要有基于 DNS 和 环境变量这两种内部发现机制,而它们都依赖于 Service 这个核心抽象。同时,Ingress 提供了外部到内部的服务发现和路由能力。
Service VS Endpoint
Service 和 Endpoint/EndpointSlice 在 Kubernetes 中有明确的功能分工,它们共同构成了服务发现和负载均衡的基础。以下是详细的区别分析:
一、核心功能定位
Service - 抽象服务层
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
selector:
app: web-server
ports:
- protocol: TCP
port: 80 # 服务端口
targetPort: 8080 # 后端 Pod 端口
type: ClusterIP # 服务类型
Service 的核心功能:
- 服务抽象:提供稳定的虚拟 IP 和 DNS 名称
- 访问入口:定义客户端如何访问服务
- 负载均衡策略:指定流量分发方式
- 服务类型:ClusterIP、NodePort、LoadBalancer、ExternalName
Endpoint/EndpointSlice - 后端实现层
apiVersion: v1
kind: Endpoints
metadata:
name: web-service # 必须与 Service 同名
subsets:
- addresses:
- ip: 10.244.1.5
targetRef:
kind: Pod
name: web-pod-1
- ip: 10.244.1.6
targetRef:
kind: Pod
name: web-pod-2
ports:
- port: 8080
protocol: TCP
Endpoints 的核心功能:
- 后端发现:记录实际可用的 Pod IP 地址
- 健康状态:只包含通过就绪探针检查的 Pod
- 动态更新:实时反映后端 Pod 的变化
- 端口映射:维护 Service port 到 Pod port 的映射
二、详细功能对比
| 功能特性 | Service | Endpoint/EndpointSlice |
|---|
| 抽象级别 | 逻辑抽象层 | 物理实现层 |
| 数据内容 | 虚拟IP、端口、选择器 | 实际Pod IP地址、端口 |
| 稳定性 | 稳定的VIP和DNS | 动态变化的IP列表 |
| 创建方式 | 手动定义 | 自动生成(或手动) |
| 更新频率 | 低频变更 | 高频动态更新 |
| DNS解析 | 返回Service IP | 不直接参与DNS |
| 负载均衡 | 定义策略 | 提供后端目标 |
三、实际工作流程
1. 服务访问流程
客户端请求 → Service VIP → kube-proxy → Endpoints → 实际 Pod
↓ ↓ ↓ ↓ ↓
DNS解析 虚拟IP iptables/ 后端IP列表 具体容器
10.96.x.x IPVS规则 10.244.x.x 应用服务
2. 数据流向示例
# 客户端访问
curl http://web-service.default.svc.cluster.local
# DNS 解析返回 Service IP
nslookup web-service.default.svc.cluster.local
# 返回: 10.96.123.456
# kube-proxy 根据 Endpoints 配置转发
iptables -t nat -L KUBE-SERVICES | grep 10.96.123.456
# 转发到: 10.244.1.5:8080, 10.244.1.6:8080
四、使用场景差异
Service 的使用场景
# 1. 内部服务访问
apiVersion: v1
kind: Service
metadata:
name: internal-api
spec:
type: ClusterIP
selector:
app: api-server
ports:
- port: 8080
# 2. 外部访问
apiVersion: v1
kind: Service
metadata:
name: external-web
spec:
type: LoadBalancer
selector:
app: web-frontend
ports:
- port: 80
nodePort: 30080
# 3. 外部服务代理
apiVersion: v1
kind: Service
metadata:
name: external-database
spec:
type: ExternalName
externalName: database.example.com
Endpoints 的使用场景
# 1. 自动后端管理(默认)
# Kubernetes 自动维护匹配 Pod 的 Endpoints
# 2. 外部服务集成
apiVersion: v1
kind: Service
metadata:
name: legacy-system
spec:
ports:
- port: 3306
---
apiVersion: v1
kind: Endpoints
metadata:
name: legacy-system
subsets:
- addresses:
- ip: 192.168.1.100 # 外部数据库
ports:
- port: 3306
# 3. 多端口复杂服务
apiVersion: v1
kind: Service
metadata:
name: complex-app
spec:
ports:
- name: http
port: 80
- name: https
port: 443
- name: metrics
port: 9090
五、配置和管理差异
Service 配置重点
apiVersion: v1
kind: Service
metadata:
name: optimized-service
annotations:
# 负载均衡配置
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
# 会话保持
service.kubernetes.io/aws-load-balancer-backend-protocol: "http"
spec:
type: LoadBalancer
selector:
app: optimized-app
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
ports:
- name: http
port: 80
targetPort: 8080
# 流量策略(仅对外部流量)
externalTrafficPolicy: Local
Endpoints 配置重点
apiVersion: v1
kind: Endpoints
metadata:
name: custom-endpoints
labels:
# 用于网络策略选择
environment: production
subsets:
- addresses:
- ip: 10.244.1.10
nodeName: worker-1
targetRef:
kind: Pod
name: app-pod-1
namespace: production
- ip: 10.244.1.11
nodeName: worker-2
targetRef:
kind: Pod
name: app-pod-2
namespace: production
# 多端口定义
ports:
- name: http
port: 8080
protocol: TCP
- name: metrics
port: 9090
protocol: TCP
- name: health
port: 8081
protocol: TCP
六、监控和调试差异
Service 监控重点
# 检查 Service 状态
kubectl get services
kubectl describe service web-service
# Service 相关指标
kubectl top services # 如果支持
kubectl get --raw /api/v1/namespaces/default/services/web-service/proxy/metrics
# DNS 解析测试
kubectl run test-$RANDOM --image=busybox --rm -it -- nslookup web-service
Endpoints 监控重点
# 检查后端可用性
kubectl get endpoints
kubectl describe endpoints web-service
# 验证后端 Pod 状态
kubectl get pods -l app=web-server -o wide
# 检查就绪探针
kubectl get pods -l app=web-server -o jsonpath='{.items[*].spec.containers[*].readinessProbe}'
# 直接测试后端连通性
kubectl run test-$RANDOM --image=busybox --rm -it --
# 在容器内: telnet 10.244.1.5 8080
七、性能考虑差异
Service 性能优化
apiVersion: v1
kind: Service
metadata:
name: high-performance
annotations:
# 使用 IPVS 模式提高性能
service.kubernetes.io/service.beta.kubernetes.io/ipvs-scheduler: "wrr"
spec:
type: ClusterIP
clusterIP: None # Headless Service,减少一层转发
selector:
app: high-perf-app
Endpoints 性能优化
# 使用 EndpointSlice 提高大规模集群性能
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: web-service-abc123
labels:
kubernetes.io/service-name: web-service
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 8080
endpoints:
- addresses:
- "10.244.1.5"
conditions:
ready: true
# 拓扑感知,优化路由
zone: us-west-2a
hints:
forZones:
- name: us-west-2a
八、总结
| 维度 | Service | Endpoint/EndpointSlice |
|---|
| 角色 | 服务门面 | 后端实现 |
| 稳定性 | 高(VIP/DNS稳定) | 低(IP动态变化) |
| 关注点 | 如何访问 | 谁能被访问 |
| 配置频率 | 低频 | 高频自动更新 |
| 网络层级 | L4 负载均衡 | 后端目标发现 |
| 扩展性 | 通过类型扩展 | 通过EndpointSlice扩展 |
简单比喻:
- Service 就像餐厅的接待台和菜单 - 提供统一的入口和访问方式
- Endpoints 就像后厨的厨师列表 - 记录实际提供服务的人员和位置
两者协同工作,Service 定义"什么服务可用",Endpoints 定义"谁可以提供这个服务",共同实现了 Kubernetes 强大的服务发现和负载均衡能力。
StatefulSet
StatefulSet 如何具体解决有状态应用的挑战
StatefulSet 的四大核心机制
StatefulSet 通过一系列精心设计的机制,为有状态应用提供了稳定性和可预测性。
1. 稳定的网络标识
解决的问题:有状态应用(如数据库节点)需要稳定的主机名来相互发现和通信,不能使用随机名称。
StatefulSet 的实现:
- 固定的 Pod 名称:Pod 名称遵循固定模式:
<statefulset-name>-<ordinal-index>。- 例如:
redis-cluster-0,redis-cluster-1,redis-cluster-2
- 稳定的 DNS 记录:每个 Pod 都会自动获得一个唯一的、稳定的 DNS 记录:
- 格式:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local - 例子:
redis-cluster-0.redis-service.default.svc.cluster.local
应对场景:
- 在 Redis 集群中,
redis-cluster-0 可以告诉 redis-cluster-1:“我的地址是 redis-cluster-0.redis-service",这个地址在 Pod 的一生中都不会改变,即使它被重新调度到其他节点。
2. 有序的部署与管理
解决的问题:像 Zookeeper、Etcd 这样的集群化应用,节点需要按顺序启动和加入集群,主从数据库也需要先启动主节点。
StatefulSet 的实现:
- 有序部署:当创建 StatefulSet 时,Pod 严格按照索引顺序(0, 1, 2…)依次创建。必须等
Pod-0 完全就绪(Ready)后,才会创建 Pod-1。 - 有序扩缩容:
- 扩容:按顺序创建新 Pod(如从 3 个扩展到 5 个,会先创建
pod-3,再 pod-4)。 - 缩容:按逆序终止 Pod(从
pod-4 开始,然后是 pod-3)。
- 有序滚动更新:同样遵循逆序策略,确保在更新过程中大部分节点保持可用。
应对场景:
- 部署 MySQL 主从集群时,StatefulSet 会确保
mysql-0(主节点)先启动并初始化完成,然后才启动 mysql-1(从节点),从节点在启动时就能正确连接到主节点进行数据同步。
3. 稳定的持久化存储
这是 StatefulSet 最核心的特性!
解决的问题:有状态应用的数据必须持久化,并且当 Pod 发生故障或被调度到新节点时,必须能够重新挂载到它自己的那部分数据。
StatefulSet 的实现:
- Volume Claim Template:在 StatefulSet 的 YAML 中,你可以定义一个
volumeClaimTemplate(存储卷申请模板)。 - 专属的 PVC:StatefulSet 会为每个 Pod 实例根据这个模板创建一个独立的、专用的 PersistentVolumeClaim (PVC)。
mysql-0 -> pvc-name-mysql-0mysql-1 -> pvc-name-mysql-1mysql-2 -> pvc-name-mysql-2
工作流程:
- 当你创建名为
mysql、副本数为 3 的 StatefulSet 时,K8s 会:- 创建 Pod
mysql-0,并同时创建 PVC data-mysql-0,然后将它们绑定。 - 等
mysql-0 就绪后,创建 Pod mysql-1 和 PVC data-mysql-1,然后绑定。 - 以此类推。
- 如果节点故障导致
mysql-1 被删除,K8s 调度器会在其他健康节点上重新创建一个同名的 Pod mysql-1。 - 这个新 Pod
mysql-1 会自动挂载到之前为它创建的、存有它专属数据的 PVC data-mysql-1 上。 - 这样,Pod 虽然"漂移"了,但数据依然跟随,应用可以无缝恢复。
应对场景:
- 对于数据库,每个 Pod 都有自己独立的数据目录。
mysql-0 的数据永远不会和 mysql-1 的数据混淆。这为数据分片(Sharding)和主从复制提供了基础。
4. 稳定的启动顺序与唯一身份
解决的问题:应用启动脚本或配置可能需要知道当前实例的索引号(如,判断自己是否是第一个节点,从而决定是否要初始化集群)。
StatefulSet 的实现:
- Pod 的序号(
0, 1, 2...)就是其稳定的、唯一的身份标识。 - 这个序号可以通过环境变量注入到 Pod 的容器中,供应用程序使用。
总结
StatefulSet 通过「稳定的网络标识 + 有序的部署管理 + 稳定的专属存储」这三驾马车,将有状态应用在动态的、易变的容器环境中"锚定”,使其获得了类似物理机或虚拟机的稳定运行特性。
它并没有让有状态应用本身变得简单,而是为这些应用的复杂需求(如集群化、数据持久化)提供了一个可以在 K8s 上稳定运行的框架和平台。对于运维人员来说,这意味着可以用声明式的方式像管理无状态应用一样,去管理数据库、消息队列等复杂的有状态服务。
StatefulSet 2
答案是:
❌ StatefulSet 不使用 ReplicaSet 来创建 Pod。
✅ StatefulSet Controller 直接创建并管理 Pod 对象,而且每个 Pod 都是有序的、具名的、不可替代的。
接下来我会详细讲清楚 StatefulSet 与 Deployment 的架构差异、实现机制、核心控制循环,让你能在实现 Operator 或高可靠应用时真正理解它的行为。
🧩 一、StatefulSet 与 Deployment 的根本区别
| 项目 | Deployment | StatefulSet |
|---|
| 控制下层对象 | ReplicaSet → Pod | 直接管理 Pod |
| Pod 命名 | 随机 | 固定命名(有序索引) |
| Pod 身份 | 可替换 | 唯一、持久身份(Stable Identity) |
| 更新策略 | 滚动更新(无序) | 有序更新(从 0 开始逐个) |
| 存储 | 通常无状态 | 绑定 PVC,数据与 Pod 一一对应 |
| 常见场景 | Web 服务、API、Job | 数据库、Zookeeper、Kafka、Etcd 等 |
⚙️ 二、StatefulSet 控制器工作原理
StatefulSet Controller 运行在 kube-controller-manager 中。
它同样是一个典型的 Controller + Informer + WorkQueue + Reconcile Loop 架构。
但是它的 核心循环逻辑 与 Deployment/ReplicaSet 明显不同:
1️⃣ Reconcile 流程概览
伪代码如下:
syncStatefulSet(set):
pods := listPodsMatching(set.selector)
sortPodsByOrdinal(pods)
for i in 0..set.spec.replicas-1:
ensurePodExists(set, ordinal=i)
ensurePodPVCExists(set, ordinal=i)
if updateRequired(set, pod[i]):
deletePodAndWaitForReplacement(set, ordinal=i)
也就是说,StatefulSet controller 自己直接创建 Pod 和 PVC,并且顺序控制整个过程。
2️⃣ Pod 命名与序号机制
StatefulSet 会给每个 Pod 一个固定的命名模式:
例如:
这些名字不是随机生成的 UID,而是根据序号确定的。
这让 Pod 可以拥有稳定的网络标识:
这对 Zookeeper、MySQL 集群等“节点必须有固定身份”的应用至关重要。
3️⃣ 存储(PVC)绑定机制
每个 StatefulSet Pod 都可以声明一个 volumeClaimTemplate:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
Controller 会为每个 Pod 创建一个独立的 PVC:
data-web-0
data-web-1
data-web-2
即使 Pod 被删除,这个 PVC 也不会被清理(除非手动删除),
这就实现了状态持久化。
4️⃣ Pod 创建顺序与滚动更新顺序
StatefulSet 有严格的有序逻辑:
创建顺序
- 先创建
web-0; - 等
web-0 变成 Running + Ready; - 再创建
web-1; - 依次类推。
删除顺序
更新顺序(滚动更新)
先更新最高序号 Pod;
等该 Pod Ready 后,再更新下一个;
可配置 podManagementPolicy:
OrderedReady(默认,有序)Parallel(并行)
5️⃣ StatefulSet 更新控制逻辑
当你修改 StatefulSet 的模板(例如更换镜像),控制器会:
- 比较
spec.template hash; - 找出需要更新的 Pod;
- 按序更新;
- 每个 Pod 更新完毕、Ready 后,再进行下一个。
伪代码示例:
for i = replicas-1 downTo 0:
if pod[i].template != set.template:
delete(pod[i])
waitUntilNewPodReady(i)
📦 三、StatefulSet 与 PVC 的生命周期
| 对象 | 何时创建 | 何时删除 |
|---|
| Pod | 当 StatefulSet 第一次启动或扩容时 | 缩容时(最高序号先删) |
| PVC | Pod 第一次被创建时 | 不会自动删除(除非使用 StorageClass Retain 策略外的设置) |
这意味着:
StatefulSet 的 数据持久性是通过 PVC 与序号绑定实现的。
例如:
data-web-0 → 永远属于 web-0
data-web-1 → 永远属于 web-1
🔁 四、StatefulSet 的控制链路(事件流)
| 步骤 | 执行者 | 说明 |
|---|
| 用户 | kubectl apply 创建 StatefulSet | |
| API Server | 写入 etcd | |
| StatefulSet Controller | 检测 StatefulSet 对象变化 | |
| Controller | 创建第一个 Pod + PVC(ordinal=0) | |
| Kubelet | 调度并运行容器 | |
| Controller | 检测 web-0 Ready 后,继续创建 web-1 | |
| 直到 | 所有 replicas Ready | |
| 更新 | 按序滚动更新 Pod | |
🧠 五、StatefulSet 为什么不复用 ReplicaSet?
主要是因为 ReplicaSet 的特性与 StatefulSet 的设计目标相冲突:
| 功能 | ReplicaSet | StatefulSet |
|---|
| Pod 匿名性 | ✅ 所有 Pod 等价 | ❌ 每个 Pod 唯一 |
| 更新无序性 | ✅ 任意替换 | ❌ 必须有序 |
| PVC 管理 | ❌ 无 | ✅ 一一绑定 |
| 网络身份 | ❌ 无 | ✅ 稳定 DNS 名称 |
所以 Kubernetes 设计了专门的 StatefulSet Controller 来实现这一类“有状态副本控制”。
💡 六、StatefulSet 小结
| 特性 | 描述 |
|---|
| 控制对象 | 直接管理 Pod |
| Pod 命名 | 固定命名(带序号) |
| 更新策略 | 有序(OrderedReady)或并行(Parallel) |
| 持久化 | 通过 PVC 一对一绑定 |
| 典型场景 | 数据库、分布式存储、消息队列、协调服务等 |
| 优势 | 稳定网络身份、持久存储、严格更新顺序 |
| 缺点 | 扩缩容慢,调度受限,不适合无状态应用 |
🧭 七、视觉化理解(结构示意)
StatefulSet: kafka
│
├── Pod: kafka-0
│ ├── PVC: data-kafka-0
│ └── Hostname: kafka-0.kafka.default.svc
│
├── Pod: kafka-1
│ ├── PVC: data-kafka-1
│ └── Hostname: kafka-1.kafka.default.svc
│
└── Pod: kafka-2
├── PVC: data-kafka-2
└── Hostname: kafka-2.kafka.default.svc
Talk between 2 pods in different nodes
好的,这是一个非常核心的 Kubernetes 网络问题。不同 Node 上的 Pod 之间的通信过程,清晰地展示了 Kubernetes 网络模型的核心思想:每个 Pod 都拥有一个独立的、扁平的 IP 地址空间,无论它运行在哪个节点上,Pod 之间都可以直接通过这个 IP 进行通信,而无需使用 NAT。
这个过程的实现完全依赖于容器网络接口(CNI)插件,如 Calico、Flannel、Weave Net 等。下面我们以最经典的 Flannel (VXLAN 模式) 和 Calico (BGP 模式) 为例,来阐述这个通信过程。
核心原则
- Pod IP 可达性:Kubernetes 网络模型要求,任何 Pod 的 IP 地址都能被任何其他 Pod 直接访问,无论它们是否在同一个节点上。
- 无 NAT:Pod 到 Pod 的通信不应该经过源地址转换(SNAT)或目的地址转换(DNAT)。Pod 看到的源 IP 和目标 IP 就是真实的 Pod IP。
通用通信流程(抽象模型)
假设有两个 Pod:
- Pod A:在
Node 1 上,IP 为 10.244.1.10 - Pod B:在
Node 2 上,IP 为 10.244.2.20
当 Pod A 试图 ping Pod B 的 IP (10.244.2.20) 时,过程如下:
1. 出站:从 Pod A 到 Node 1
- Pod A 根据其内部路由表,将数据包从自己的网络命名空间内的
eth0 接口发出。 - 目标 IP 是
10.244.2.20。 - 在
Node 1 上,有一个网桥(如 cni0)充当了所有本地 Pod 的虚拟交换机。Pod A 的 eth0 通过一对 veth pair 连接到这个网桥。 - 数据包到达网桥
cni0。
2. 路由决策:在 Node 1 上
Node 1 的内核路由表 由 CNI 插件配置。它查看数据包的目标 IP 10.244.2.20。- 路由表规则大致如下:
Destination Gateway Interface
10.244.1.0/24 ... cni0 # 本地 Pod 网段,走 cni0 网桥
10.244.2.0/24 192.168.1.102 eth0 # 非本地 Pod 网段,通过网关(即 Node 2 的 IP)从物理网卡 eth0 发出
- 路由表告诉内核,去往
10.244.2.0/24 网段的数据包,下一跳是 192.168.1.102(即 Node 2 的物理 IP),并通过 Node 1 的物理网络接口 eth0 发出。
从这里开始,不同 CNI 插件的工作机制产生了差异。
场景一:使用 Flannel (VXLAN 模式)
Flannel 通过创建一个覆盖网络 来解决跨节点通信。
封装:
- 数据包(源
10.244.1.10,目标 10.244.2.20)到达 Node 1 的 eth0 之前,会被一个特殊的虚拟网络设备 flannel.1 截获。 flannel.1 是一个 VXLAN 隧道端点。- 封装:
flannel.1 会将整个原始数据包(作为 payload)封装在一个新的 UDP 数据包 中。- 外层 IP 头:源 IP 是
Node 1 的 IP (192.168.1.101),目标 IP 是 Node 2 的 IP (192.168.1.102)。 - 外层 UDP 头:目标端口通常是 8472 (VXLAN)。
- VXLAN 头:包含一个 VNI,用于标识不同的虚拟网络。
- 内层原始数据包:原封不动。
物理网络传输:
- 这个封装后的 UDP 数据包通过
Node 1 的物理网络 eth0 发送出去。 - 它经过底层物理网络(交换机、路由器)顺利到达
Node 2,因为外层 IP 是节点的真实 IP,底层网络是认识的。
解封装:
- 数据包到达
Node 2 的物理网卡 eth0。 - 内核发现这是一个发往 VXLAN 端口 (8472) 的 UDP 包,于是将其交给
Node 2 上的 flannel.1 设备处理。 flannel.1 设备解封装,剥掉外层 UDP 和 IP 头,露出原始的 IP 数据包(源 10.244.1.10,目标 10.244.2.20)。
入站:从 Node 2 到 Pod B:
- 解封后的原始数据包被送入
Node 2 的网络栈。 Node 2 的路由表查看目标 IP 10.244.2.20,发现它属于本地的 cni0 网桥管理的网段。- 数据包被转发到
cni0 网桥,网桥再通过 veth pair 将数据包送达 Pod B 的 eth0 接口。
简单比喻:Flannel 就像在两个节点之间建立了一条邮政专线。你的原始信件(Pod IP 数据包)被塞进一个标准快递信封(外层 UDP 包)里,通过公共邮政系统(物理网络)寄到对方邮局(Node 2),对方邮局再拆开快递信封,把原始信件交给收件人(Pod B)。
场景二:使用 Calico (BGP 模式)
Calico 通常不使用隧道,而是利用 BGP 协议 和 纯三层路由,效率更高。
路由通告:
Node 1 和 Node 2 上都运行着 Calico 的 BGP 客户端 Felix 和 BGP 路由反射器 BIRD。Node 2 会通过 BGP 协议向网络中的其他节点(包括 Node 1)通告一条路由信息:“目标网段 10.244.2.0/24 的下一跳是我 192.168.1.102”。Node 1 学习到了这条路由,并写入自己的内核路由表(就是我们之前在步骤2中看到的那条)。
直接路由:
- 数据包(源
10.244.1.10,目标 10.244.2.20)根据路由表,直接通过 Node 1 的物理网卡 eth0 发出。 - 没有封装! 数据包保持原样,源 IP 是
10.244.1.10,目标 IP 是 10.244.2.20。 - 这个数据包被发送到
Node 2 的物理 IP (192.168.1.102)。
物理网络传输:
- 数据包经过底层物理网络。这就要求底层网络必须能够路由 Pod IP 的网段。在云环境中,这通常通过配置 VPC 路由表来实现;在物理机房,需要核心交换机学习到这些 BGP 路由或配置静态路由。
入站:从 Node 2 到 Pod B:
- 数据包到达
Node 2 的物理网卡 eth0。 Node 2 的内核查看目标 IP 10.244.2.20,发现这个 IP 属于一个本地虚拟接口(如 caliXXX,这是 Calico 为每个 Pod 创建的),于是直接将数据包转发给该接口,最终送达 Pod B。
简单比喻:Calico 让每个节点都成为一个智能路由器。它们互相告知“哪个 Pod 网段在我这里”。当 Node 1 要发数据给 Node 2 上的 Pod 时,它就像路由器一样,根据已知的路由表,直接找到 Node 2 的地址并把数据包发过去,中间不拆包。
总结对比
| 特性 | Flannel (VXLAN) | Calico (BGP) |
|---|
| 网络模型 | Overlay Network | Pure Layer 3 |
| 原理 | 隧道封装 | 路由通告 |
| 性能 | 有封装/解封装开销,性能稍低 | 无隧道开销,性能更高 |
| 依赖 | 对底层网络无要求,只要节点IP通即可 | 依赖底层网络支持路由(云平台VPC或物理网络配置) |
| 数据包 | 外层Node IP,内层Pod IP | 始终是Pod IP |
无论采用哪种方式,Kubernetes 和 CNI 插件共同协作,最终实现了一个对应用开发者透明的、扁平的 Pod 网络。开发者只需关心 Pod IP 和 Service,而无需理解底层复杂的跨节点通信机制。
如果pod之间访问不通怎么排查?
核心排查思路:从 Pod 内部到外部,从简单到复杂
整个排查过程可以遵循下图所示的路径,逐步深入:
flowchart TD
A[Pod 之间访问不通] --> B[确认基础连通性<br>ping & telnet]
B --> C{ping 是否通?}
C -- 通 --> D[telnet 端口是否通?]
C -- 不通 --> E[检查 NetworkPolicy<br>kubectl get networkpolicy]
D -- 通 --> F[检查应用日志与配置]
D -- 不通 --> G[检查 Service 与 Endpoints<br>kubectl describe svc]
E --> H[检查 CNI 插件状态<br>kubectl get pods -n kube-system]
subgraph G_ [Service排查路径]
G --> G1[Endpoints 是否为空?]
G1 -- 是 --> G2[检查 Pod 标签与 Selector]
G1 -- 否 --> G3[检查 kube-proxy 与 iptables]
end
F --> Z[问题解决]
H --> Z
G2 --> Z
G3 --> Z第一阶段:基础信息收集与初步检查
获取双方 Pod 信息
- 确认两个 Pod 都处于
Running 状态。 - 记录下它们的 IP 地址 和 所在节点。
- 确认它们不在同一个节点上(如果是,排查方法会略有不同)。
明确访问方式
- 直接通过 Pod IP 访问? (
ping <pod-ip> 或 curl <pod-ip>:<port>) - 通过 Service 名称访问? (
ping <service-name> 或 curl <service-name>:<port>) - 这个问题决定了后续的排查方向。
第二阶段:按访问路径深入排查
场景一:直接通过 Pod IP 访问不通(跨节点)
这通常是底层网络插件(CNI) 的问题。
检查 Pod 内部网络
kubectl exec -it <source-pod> -- sh
# 在 Pod 内部执行:
ip addr show eth0 # 查看 IP 是否正确
ip route # 查看路由表
ping <destination-pod-ip> # 测试连通性
检查目标 Pod 的端口监听
kubectl exec -it <destination-pod> -- netstat -tulpn | grep LISTEN
# 或者用 ss 命令
kubectl exec -it <destination-pod> -- ss -tulpn | grep LISTEN
- 如果这里没监听,是应用自身问题,检查应用日志和配置。
检查 NetworkPolicy(网络策略)
- 这是 Kubernetes 的“防火墙”,很可能阻止了访问。
kubectl get networkpolicies -A
kubectl describe networkpolicy <policy-name> -n <namespace>
- 查看是否有策略限制了源 Pod 或目标 Pod 的流量。特别注意
ingress 规则。
检查 CNI 插件状态
- CNI 插件(如 Calico、Flannel)的异常会导致跨节点网络瘫痪。
kubectl get pods -n kube-system | grep -e calico -e flannel -e weave
- 确认所有 CNI 相关的 Pod 都在运行。如果有 CrashLoopBackOff 等状态,查看其日志。
节点层面排查
- 如果以上都正常,问题可能出现在节点网络层面。
- 登录到源 Pod 所在节点,尝试
ping 目标 Pod IP。 - 检查节点路由表:
- 对于 Flannel,你应该能看到到其他节点 Pod 网段的路由。
- 对于 Calico,你应该能看到到每个其他节点 Pod 网段的精确路由。
- 检查节点防火墙:在某些环境中(如安全组、iptables 规则)可能阻止了 VXLAN(8472端口)或节点间 Pod IP 的通信。
# 检查 iptables 规则
sudo iptables-save | grep <pod-ip>
场景二:通过 Service 名称访问不通
这通常是 Kubernetes 服务发现 或 kube-proxy 的问题。
检查 Service 和 Endpoints
kubectl get svc <service-name>
kubectl describe svc <service-name> # 查看 Selector 和 Port 映射
kubectl get endpoints <service-name> # 这是关键!检查是否有健康的 Endpoints
- 如果
ENDPOINTS 列为空:说明 Service 的 Label Selector 没有匹配到任何健康的 Pod。请检查:- Pod 的
labels 是否与 Service 的 selector 匹配。 - Pod 的
readinessProbe 是否通过。
检查 DNS 解析
- 进入源 Pod,测试是否能解析 Service 名称:
kubectl exec -it <source-pod> -- nslookup <service-name>
# 或者
kubectl exec -it <source-pod> -- cat /etc/resolv.conf
- 如果解析失败,检查
kube-dns 或 coredns Pod 是否正常。
kubectl get pods -n kube-system | grep -e coredns -e kube-dns
检查 kube-proxy
kube-proxy 负责实现 Service 的负载均衡规则(通常是 iptables 或 ipvs)。
kubectl get pods -n kube-system | grep kube-proxy
- 确认所有
kube-proxy Pod 都在运行。 - 可以登录到节点,检查是否有对应的 iptables 规则:
sudo iptables-save | grep <service-name>
# 或者查看 ipvs 规则(如果使用 ipvs 模式)
sudo ipvsadm -ln
第三阶段:高级调试技巧
如果上述步骤仍未解决问题,可以尝试以下方法:
使用网络调试镜像
- 部署一个包含网络工具的临时 Pod(如
nicolaka/netshoot)来进行高级调试。
kubectl run -it --rm debug-pod --image=nicolaka/netshoot -- /bin/bash
- 在这个 Pod 里,你可以使用
tcpdump, tracepath, dig 等强大工具。 - 例如,在目标 Pod 的节点上抓包:
# 在节点上执行,监听 Pod 网络对应的接口
sudo tcpdump -i any -n host <source-pod-ip> and host <destination-pod-ip>
检查节点网络连接
- 确认两个节点之间网络是通的(通过节点 IP)。
- 确认 CNI 所需的端口(如 Flannel 的 VXLAN 端口 8472)在节点间是开放的。
总结与排查命令清单
当 Pod 间访问不通时,按顺序执行以下命令:
# 1. 基本信息
kubectl get pods -o wide
kubectl get svc,ep -o wide
# 2. 检查 NetworkPolicy
kubectl get networkpolicies -A
# 3. 检查核心插件
kubectl get pods -n kube-system | grep -e coredns -e kube-proxy -e calico -e flannel
# 4. 进入 Pod 测试
kubectl exec -it <source-pod> -- ping <destination-pod-ip>
kubectl exec -it <source-pod> -- nslookup <service-name>
# 5. 检查目标 Pod 应用
kubectl exec -it <destination-pod> -- netstat -tulpn
kubectl logs <destination-pod>
# 6. 节点层面检查
# 在节点上执行
ip route
sudo iptables-save | grep <relevant-ip>
记住,90% 的 Pod 网络不通问题源于 NetworkPolicy 配置、Service Endpoints 为空,或 CNI 插件故障。按照这个路径排查,绝大多数问题都能被定位和解决。
Talk with API Server
Kubernetes 各模块与 API Server 通信详解
这是理解 Kubernetes 架构的核心问题。API Server 是整个集群的"大脑",所有组件都通过它进行通信。
🎯 Kubernetes 通信架构总览
┌─────────────────────────────────────────────────────────┐
│ API Server (核心) │
│ - RESTful API (HTTP/HTTPS) │
│ - 认证、授权、准入控制 │
│ - etcd 唯一入口 │
└───────┬─────────────────┬─────────────────┬─────────────┘
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼────┐
│Kubelet│ │Scheduler│ │Controller│
│(Node) │ │ │ │ Manager │
└───────┘ └─────────┘ └──────────┘
│
┌───▼────┐
│kube-proxy│
└────────┘
🔐 通信基础:认证、授权、准入
1. 认证 (Authentication)
所有组件访问 API Server 必须先通过认证。
常见认证方式
| 认证方式 | 使用场景 | 实现方式 |
|---|
| X.509 证书 | 集群组件(kubelet/scheduler) | 客户端证书 |
| ServiceAccount Token | Pod 内应用 | JWT Token |
| Bootstrap Token | 节点加入集群 | 临时 Token |
| 静态 Token 文件 | 简单测试 | 不推荐生产 |
| OIDC | 用户认证 | 外部身份提供商 |
X.509 证书认证示例
# 1. API Server 启动参数包含 CA 证书
kube-apiserver \
--client-ca-file=/etc/kubernetes/pki/ca.crt \
--tls-cert-file=/etc/kubernetes/pki/apiserver.crt \
--tls-private-key-file=/etc/kubernetes/pki/apiserver.key
# 2. Kubelet 使用客户端证书
kubelet \
--kubeconfig=/etc/kubernetes/kubelet.conf \
--client-ca-file=/etc/kubernetes/pki/ca.crt
# 3. kubeconfig 文件内容
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority: /etc/kubernetes/pki/ca.crt # CA 证书
server: https://192.168.1.10:6443 # API Server 地址
name: kubernetes
users:
- name: system:node:worker-1
user:
client-certificate: /var/lib/kubelet/pki/kubelet-client.crt # 客户端证书
client-key: /var/lib/kubelet/pki/kubelet-client.key # 客户端密钥
contexts:
- context:
cluster: kubernetes
user: system:node:worker-1
name: default
current-context: default
ServiceAccount Token 认证
# Pod 内自动挂载的 Token
cat /var/run/secrets/kubernetes.io/serviceaccount/token
# eyJhbGciOiJSUzI1NiIsImtpZCI6Ij...
# 使用 Token 访问 API Server
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -k -H "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/default/pods
2. 授权 (Authorization)
认证通过后,检查是否有权限执行操作。
RBAC (Role-Based Access Control) - 最常用
# 1. Role - 定义权限
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
# 2. RoleBinding - 绑定用户/ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: ServiceAccount
name: my-app
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
授权模式对比
| 模式 | 说明 | 使用场景 |
|---|
| RBAC | 基于角色 | 生产环境(推荐) |
| ABAC | 基于属性 | 复杂策略(已过时) |
| Webhook | 外部授权服务 | 自定义授权逻辑 |
| Node | 节点授权 | Kubelet 专用 |
| AlwaysAllow | 允许所有 | 测试环境(危险) |
3. 准入控制 (Admission Control)
授权通过后,准入控制器可以修改或拒绝请求。
常用准入控制器
# API Server 启用的准入控制器
kube-apiserver \
--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,\
DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,\
ValidatingAdmissionWebhook,ResourceQuota,PodSecurityPolicy
| 准入控制器 | 作用 |
|---|
| NamespaceLifecycle | 防止在删除中的 namespace 创建资源 |
| LimitRanger | 强制资源限制 |
| ResourceQuota | 强制命名空间配额 |
| PodSecurityPolicy | 强制 Pod 安全策略 |
| MutatingAdmissionWebhook | 修改资源(如注入 sidecar) |
| ValidatingAdmissionWebhook | 验证资源(自定义校验) |
📡 各组件通信详解
1. Kubelet → API Server
Kubelet 是唯一主动连接 API Server 的组件。
通信方式
Kubelet (每个 Node)
│
├─→ List-Watch Pods (监听分配给自己的 Pod)
├─→ Report Node Status (定期上报节点状态)
├─→ Report Pod Status (上报 Pod 状态)
└─→ Get Secrets/ConfigMaps (拉取配置)
实现细节
// Kubelet 启动时创建 Informer 监听资源
// 伪代码示例
func (kl *Kubelet) syncLoop() {
// 1. 创建 Pod Informer
podInformer := cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
// 列出分配给当前节点的所有 Pod
options.FieldSelector = fields.OneTermEqualSelector("spec.nodeName", kl.nodeName).String()
return kl.kubeClient.CoreV1().Pods("").List(context.TODO(), options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
// 持续监听 Pod 变化
options.FieldSelector = fields.OneTermEqualSelector("spec.nodeName", kl.nodeName).String()
return kl.kubeClient.CoreV1().Pods("").Watch(context.TODO(), options)
},
},
&v1.Pod{},
0, // 不缓存
cache.Indexers{},
)
// 2. 注册事件处理器
podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: kl.handlePodAdditions,
UpdateFunc: kl.handlePodUpdates,
DeleteFunc: kl.handlePodDeletions,
})
// 3. 定期上报节点状态
go wait.Until(kl.syncNodeStatus, 10*time.Second, stopCh)
}
// 上报节点状态
func (kl *Kubelet) syncNodeStatus() {
node := &v1.Node{
ObjectMeta: metav1.ObjectMeta{Name: kl.nodeName},
Status: v1.NodeStatus{
Conditions: []v1.NodeCondition{
{Type: v1.NodeReady, Status: v1.ConditionTrue},
},
Capacity: kl.getNodeCapacity(),
// ...
},
}
// 调用 API Server 更新节点状态
kl.kubeClient.CoreV1().Nodes().UpdateStatus(context.TODO(), node, metav1.UpdateOptions{})
}
Kubelet 配置示例
# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
# API Server 连接配置(通过 kubeconfig)
authentication:
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
webhook:
enabled: true
anonymous:
enabled: false
authorization:
mode: Webhook
clusterDomain: cluster.local
clusterDNS:
- 10.96.0.10
# 定期上报间隔
nodeStatusUpdateFrequency: 10s
nodeStatusReportFrequency: 1m
List-Watch 机制详解
┌─────────────────────────────────────────┐
│ Kubelet List-Watch 工作流程 │
├─────────────────────────────────────────┤
│ │
│ 1. List(初始化) │
│ GET /api/v1/pods?fieldSelector=... │
│ ← 返回所有当前 Pod │
│ │
│ 2. Watch(持续监听) │
│ GET /api/v1/pods?watch=true&... │
│ ← 保持长连接 │
│ │
│ 3. 接收事件 │
│ ← ADDED: Pod nginx-xxx created │
│ ← MODIFIED: Pod nginx-xxx updated │
│ ← DELETED: Pod nginx-xxx deleted │
│ │
│ 4. 本地处理 │
│ - 缓存更新 │
│ - 触发 Pod 生命周期管理 │
│ │
│ 5. 断线重连 │
│ - 检测到连接断开 │
│ - 重新 List + Watch │
│ - ResourceVersion 确保不丢事件 │
└─────────────────────────────────────────┘
HTTP 长连接(Chunked Transfer)
# Kubelet 发起 Watch 请求
GET /api/v1/pods?watch=true&resourceVersion=12345&fieldSelector=spec.nodeName=worker-1 HTTP/1.1
Host: 192.168.1.10:6443
Authorization: Bearer eyJhbGc...
Connection: keep-alive
# API Server 返回(Chunked 编码)
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
{"type":"ADDED","object":{"kind":"Pod","apiVersion":"v1",...}}
{"type":"MODIFIED","object":{"kind":"Pod","apiVersion":"v1",...}}
{"type":"DELETED","object":{"kind":"Pod","apiVersion":"v1",...}}
...
# 连接保持打开,持续推送事件
2. Scheduler → API Server
Scheduler 也使用 List-Watch 机制。
通信流程
Scheduler
│
├─→ Watch Pods (监听未调度的 Pod)
│ └─ spec.nodeName == ""
│
├─→ Watch Nodes (监听节点状态)
│
├─→ Get PVs, PVCs (获取存储信息)
│
└─→ Bind Pod (绑定 Pod 到 Node)
POST /api/v1/namespaces/{ns}/pods/{name}/binding
Scheduler 伪代码
// Scheduler 主循环
func (sched *Scheduler) scheduleOne() {
// 1. 从队列获取待调度的 Pod
pod := sched.NextPod()
// 2. 执行调度算法(过滤 + 打分)
feasibleNodes := sched.findNodesThatFit(pod)
if len(feasibleNodes) == 0 {
// 无可用节点,标记为不可调度
return
}
priorityList := sched.prioritizeNodes(pod, feasibleNodes)
selectedNode := sched.selectHost(priorityList)
// 3. 绑定 Pod 到 Node(调用 API Server)
binding := &v1.Binding{
ObjectMeta: metav1.ObjectMeta{
Name: pod.Name,
Namespace: pod.Namespace,
},
Target: v1.ObjectReference{
Kind: "Node",
Name: selectedNode,
},
}
// 发送 Binding 请求到 API Server
err := sched.client.CoreV1().Pods(pod.Namespace).Bind(
context.TODO(),
binding,
metav1.CreateOptions{},
)
// 4. API Server 更新 Pod 的 spec.nodeName
// 5. Kubelet 监听到 Pod,开始创建容器
}
// Watch 未调度的 Pod
func (sched *Scheduler) watchUnscheduledPods() {
podInformer := cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
// 只监听 spec.nodeName 为空的 Pod
options.FieldSelector = "spec.nodeName="
return sched.client.CoreV1().Pods("").List(context.TODO(), options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
options.FieldSelector = "spec.nodeName="
return sched.client.CoreV1().Pods("").Watch(context.TODO(), options)
},
},
&v1.Pod{},
0,
cache.Indexers{},
)
podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
pod := obj.(*v1.Pod)
sched.queue.Add(pod) // 加入调度队列
},
})
}
Binding 请求详解
# Scheduler 发送的 HTTP 请求
POST /api/v1/namespaces/default/pods/nginx-xxx/binding HTTP/1.1
Host: 192.168.1.10:6443
Authorization: Bearer eyJhbGc...
Content-Type: application/json
{
"apiVersion": "v1",
"kind": "Binding",
"metadata": {
"name": "nginx-xxx",
"namespace": "default"
},
"target": {
"kind": "Node",
"name": "worker-1"
}
}
# API Server 处理:
# 1. 验证 Binding 请求
# 2. 更新 Pod 对象的 spec.nodeName = "worker-1"
# 3. 返回成功响应
# 4. Kubelet 监听到 Pod 更新,开始创建容器
3. Controller Manager → API Server
Controller Manager 包含多个控制器,每个控制器独立与 API Server 通信。
常见控制器
Controller Manager
│
├─→ Deployment Controller
│ └─ Watch Deployments, ReplicaSets
│
├─→ ReplicaSet Controller
│ └─ Watch ReplicaSets, Pods
│
├─→ Node Controller
│ └─ Watch Nodes (节点健康检查)
│
├─→ Service Controller
│ └─ Watch Services (管理 LoadBalancer)
│
├─→ Endpoint Controller
│ └─ Watch Services, Pods (创建 Endpoints)
│
└─→ PV Controller
└─ Watch PVs, PVCs (卷绑定)
ReplicaSet Controller 示例
// ReplicaSet Controller 的核心逻辑
func (rsc *ReplicaSetController) syncReplicaSet(key string) error {
// 1. 从缓存获取 ReplicaSet
rs := rsc.rsLister.Get(namespace, name)
// 2. 获取当前 Pod 列表(通过 Selector)
allPods := rsc.podLister.List(labels.Everything())
filteredPods := rsc.filterActivePods(rs.Spec.Selector, allPods)
// 3. 计算差异
diff := len(filteredPods) - int(*rs.Spec.Replicas)
if diff < 0 {
// 需要创建新 Pod
diff = -diff
for i := 0; i < diff; i++ {
// 调用 API Server 创建 Pod
pod := newPod(rs)
_, err := rsc.kubeClient.CoreV1().Pods(rs.Namespace).Create(
context.TODO(),
pod,
metav1.CreateOptions{},
)
}
} else if diff > 0 {
// 需要删除多余 Pod
podsToDelete := getPodsToDelete(filteredPods, diff)
for _, pod := range podsToDelete {
// 调用 API Server 删除 Pod
err := rsc.kubeClient.CoreV1().Pods(pod.Namespace).Delete(
context.TODO(),
pod.Name,
metav1.DeleteOptions{},
)
}
}
// 4. 更新 ReplicaSet 状态
rs.Status.Replicas = int32(len(filteredPods))
_, err := rsc.kubeClient.AppsV1().ReplicaSets(rs.Namespace).UpdateStatus(
context.TODO(),
rs,
metav1.UpdateOptions{},
)
}
Node Controller 心跳检测
// Node Controller 监控节点健康
func (nc *NodeController) monitorNodeHealth() {
for {
// 1. 列出所有节点
nodes, _ := nc.kubeClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
for _, node := range nodes.Items {
// 2. 检查节点状态
now := time.Now()
lastHeartbeat := getNodeCondition(&node, v1.NodeReady).LastHeartbeatTime
if now.Sub(lastHeartbeat.Time) > 40*time.Second {
// 3. 节点超时,标记为 NotReady
setNodeCondition(&node, v1.NodeCondition{
Type: v1.NodeReady,
Status: v1.ConditionUnknown,
Reason: "NodeStatusUnknown",
})
// 4. 更新节点状态
nc.kubeClient.CoreV1().Nodes().UpdateStatus(
context.TODO(),
&node,
metav1.UpdateOptions{},
)
// 5. 如果节点长时间 NotReady,驱逐 Pod
if now.Sub(lastHeartbeat.Time) > 5*time.Minute {
nc.evictPods(node.Name)
}
}
}
time.Sleep(10 * time.Second)
}
}
4. kube-proxy → API Server
kube-proxy 监听 Service 和 Endpoints,配置网络规则。
通信流程
kube-proxy (每个 Node)
│
├─→ Watch Services
│ └─ 获取 Service 定义
│
├─→ Watch Endpoints
│ └─ 获取后端 Pod IP 列表
│
└─→ 配置本地网络
├─ iptables 模式:更新 iptables 规则
├─ ipvs 模式:更新 IPVS 规则
└─ userspace 模式:代理转发(已废弃)
iptables 模式示例
// kube-proxy 监听 Service 和 Endpoints
func (proxier *Proxier) syncProxyRules() {
// 1. 获取所有 Service
services := proxier.serviceStore.List()
// 2. 获取所有 Endpoints
endpoints := proxier.endpointsStore.List()
// 3. 生成 iptables 规则
for _, svc := range services {
// Service ClusterIP
clusterIP := svc.Spec.ClusterIP
// 对应的 Endpoints
eps := endpoints[svc.Namespace+"/"+svc.Name]
// 生成 DNAT 规则
// -A KUBE-SERVICES -d 10.96.100.50/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-XXXX
chain := generateServiceChain(svc)
for _, ep := range eps.Subsets {
for _, addr := range ep.Addresses {
// -A KUBE-SVC-XXXX -m statistic --mode random --probability 0.33 -j KUBE-SEP-XXXX
// -A KUBE-SEP-XXXX -p tcp -m tcp -j DNAT --to-destination 10.244.1.5:8080
generateEndpointRule(addr.IP, ep.Ports[0].Port)
}
}
}
// 4. 应用 iptables 规则
iptables.Restore(rules)
}
生成的 iptables 规则示例
# Service: nginx-service (ClusterIP: 10.96.100.50:80)
# Endpoints: 10.244.1.5:8080, 10.244.2.8:8080
# 1. KUBE-SERVICES 链(入口)
-A KUBE-SERVICES -d 10.96.100.50/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-NGINX
# 2. KUBE-SVC-NGINX 链(Service 链)
-A KUBE-SVC-NGINX -m statistic --mode random --probability 0.5 -j KUBE-SEP-EP1
-A KUBE-SVC-NGINX -j KUBE-SEP-EP2
# 3. KUBE-SEP-EP1 链(Endpoint 1)
-A KUBE-SEP-EP1 -p tcp -m tcp -j DNAT --to-destination 10.244.1.5:8080
# 4. KUBE-SEP-EP2 链(Endpoint 2)
-A KUBE-SEP-EP2 -p tcp -m tcp -j DNAT --to-destination 10.244.2.8:8080
5. kubectl → API Server
kubectl 是用户与 API Server 交互的客户端工具。
通信流程
kubectl get pods
│
├─→ 1. 读取 kubeconfig (~/.kube/config)
│ - API Server 地址
│ - 证书/Token
│
├─→ 2. 发送 HTTP 请求
│ GET /api/v1/namespaces/default/pods
│
├─→ 3. API Server 处理
│ - 认证
│ - 授权
│ - 从 etcd 读取数据
│
└─→ 4. 返回结果
JSON 格式的 Pod 列表
kubectl 底层实现
// kubectl get pods 的简化实现
func getPods(namespace string) {
// 1. 加载 kubeconfig
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
// 2. 创建 Clientset
clientset, _ := kubernetes.NewForConfig(config)
// 3. 发起 GET 请求
pods, _ := clientset.CoreV1().Pods(namespace).List(
context.TODO(),
metav1.ListOptions{},
)
// 4. 输出结果
for _, pod := range pods.Items {
fmt.Printf("%s\t%s\t%s\n", pod.Name, pod.Status.Phase, pod.Spec.NodeName)
}
}
HTTP 请求详解
# kubectl get pods 发送的实际 HTTP 请求
GET /api/v1/namespaces/default/pods HTTP/1.1
Host: 192.168.1.10:6443
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ij...
Accept: application/json
User-Agent: kubectl/v1.28.0
# API Server 响应
HTTP/1.1 200 OK
Content-Type: application/json
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "12345"
},
"items": [
{
"metadata": {
"name": "nginx-xxx",
"namespace": "default"
},
"spec": {
"nodeName": "worker-1",
"containers": [...]
},
"status": {
"phase": "Running"
}
}
]
}
🔄 核心机制:List-Watch
List-Watch 是 Kubernetes 最核心的通信模式。
List-Watch 架构
┌───────────────────────────────────────────────┐
│ Client (Kubelet/Controller) │
├───────────────────────────────────────────────┤
│ │
│ 1. List(初始同步) │
│ GET /api/v1/pods │
│ → 获取所有资源 │
│ → 本地缓存(Informer Cache) │
│ │
│ 2. Watch(增量更新) │
│ GET /api/v1/pods?watch=true │
│ → 长连接(HTTP Chunked) │
│ → 实时接收 ADDED/MODIFIED/DELETED 事件 │
│ │
│ 3. ResourceVersion(一致性保证) │
│ → 每个资源有版本号 │
│ → Watch 从指定版本开始 │
│ → 断线重连不丢失事件 │
│ │
│ 4. 本地缓存(Indexer) │
│ → 减少 API Server 压力 │
│ → 快速查询 │
│ → 自动同步 │
└───────────────────────────────────────────────┘
// Informer 是 List-Watch 的高级封装
type Informer struct {
Indexer Indexer // 本地缓存
Controller Controller // List-Watch 控制器
Processor *sharedProcessor // 事件处理器
}
// 使用 Informer 监听资源
func watchPodsWithInformer() {
// 1. 创建 SharedInformerFactory
factory := informers.NewSharedInformerFactory(clientset, 30*time.Second)
// 2. 获取 Pod Informer
podInformer := factory.Core().V1().Pods()
// 3. 注册事件处理器
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
pod := obj.(*v1.Pod)
fmt.Printf("Pod ADDED: %s\n", pod.Name)
},
UpdateFunc: func(oldObj, newObj interface{}) {
pod := newObj.(*v1.Pod)
fmt.Printf("Pod UPDATED: %s\n", pod.Name)
},
DeleteFunc: func(obj interface{}) {
pod := obj.(*v1.Pod)
fmt.Printf("Pod DELETED: %s\n", pod.Name)
},
})
// 4. 启动 Informer
factory.Start(stopCh)
// 5. 等待缓存同步完成
factory.WaitForCacheSync(stopCh)
// 6. 从本地缓存查询(不访问 API Server)
pod, _ := podInformer.Lister().Pods("default").Get("nginx-xxx")
}
ResourceVersion 机制
事件流:
┌────────────────────────────────────────┐
│ Pod nginx-xxx created │ ResourceVersion: 100
├────────────────```
├────────────────────────────────────────┤
│ Pod nginx-xxx updated (image changed) │ ResourceVersion: 101
├────────────────────────────────────────┤
│ Pod nginx-xxx updated (status changed) │ ResourceVersion: 102
├────────────────────────────────────────┤
│ Pod nginx-xxx deleted │ ResourceVersion: 103
└────────────────────────────────────────┘
Watch 请求:
1. 初始 Watch: GET /api/v1/pods?watch=true&resourceVersion=100
→ 从版本 100 开始接收事件
2. 断线重连: GET /api/v1/pods?watch=true&resourceVersion=102
→ 从版本 102 继续,不会丢失版本 103 的删除事件
3. 版本过期: 如果 resourceVersion 太旧(etcd 已压缩)
→ API Server 返回 410 Gone
→ Client 重新 List 获取最新状态,然后 Watch
🔐 通信安全细节
1. TLS 双向认证
┌────────────────────────────────────────┐
│ API Server TLS 配置 │
├────────────────────────────────────────┤
│ │
│ Server 端证书: │
│ - apiserver.crt (服务端证书) │
│ - apiserver.key (服务端私钥) │
│ - ca.crt (CA 证书) │
│ │
│ Client CA: │
│ - 验证客户端证书 │
│ - --client-ca-file=/etc/kubernetes/pki/ca.crt │
│ │
│ 启动参数: │
│ --tls-cert-file=/etc/kubernetes/pki/apiserver.crt │
│ --tls-private-key-file=/etc/kubernetes/pki/apiserver.key │
│ --client-ca-file=/etc/kubernetes/pki/ca.crt │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ Kubelet TLS 配置 │
├────────────────────────────────────────┤
│ │
│ Client 证书: │
│ - kubelet-client.crt (客户端证书) │
│ - kubelet-client.key (客户端私钥) │
│ - ca.crt (CA 证书,验证 API Server) │
│ │
│ kubeconfig 配置: │
│ - certificate-authority: ca.crt │
│ - client-certificate: kubelet-client.crt │
│ - client-key: kubelet-client.key │
└────────────────────────────────────────┘
2. ServiceAccount Token 详解
# 每个 Pod 自动挂载 ServiceAccount
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
serviceAccountName: default # 使用的 ServiceAccount
containers:
- name: app
image: nginx
volumeMounts:
- name: kube-api-access-xxxxx
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
- name: kube-api-access-xxxxx
projected:
sources:
- serviceAccountToken:
path: token # JWT Token
expirationSeconds: 3607
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt # CA 证书
- downwardAPI:
items:
- path: namespace
fieldRef:
fieldPath: metadata.namespace # 命名空间
Pod 内访问 API Server
# 进入 Pod
kubectl exec -it my-pod -- sh
# 1. 读取 Token
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# 2. 读取 CA 证书
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# 3. 读取命名空间
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
# 4. 访问 API Server
curl --cacert $CACERT \
--header "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods
# 5. 使用 kubectl proxy(简化方式)
kubectl proxy --port=8080 &
curl http://localhost:8080/api/v1/namespaces/default/pods
ServiceAccount Token 结构
# 解码 JWT Token
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo $TOKEN | cut -d. -f2 | base64 -d | jq
# 输出:
{
"aud": [
"https://kubernetes.default.svc"
],
"exp": 1696867200, # 过期时间
"iat": 1696863600, # 签发时间
"iss": "https://kubernetes.default.svc.cluster.local", # 签发者
"kubernetes.io": {
"namespace": "default", # 命名空间
"pod": {
"name": "my-pod",
"uid": "abc-123"
},
"serviceaccount": {
"name": "default", # ServiceAccount 名称
"uid": "def-456"
}
},
"nbf": 1696863600,
"sub": "system:serviceaccount:default:default" # Subject
}
📊 通信模式总结
1. 主动推送 vs 被动拉取
| 组件 | 通信模式 | 说明 |
|---|
| Kubelet | 主动连接 | List-Watch API Server |
| Scheduler | 主动连接 | List-Watch API Server |
| Controller Manager | 主动连接 | List-Watch API Server |
| kube-proxy | 主动连接 | List-Watch API Server |
| kubectl | 主动请求 | RESTful API 调用 |
| API Server → etcd | 主动读写 | gRPC 连接 etcd |
重要: API Server 从不主动连接其他组件,都是组件主动连接 API Server。
2. 通信协议
┌─────────────────────────────────────────┐
│ API Server 对外暴露的协议 │
├─────────────────────────────────────────┤
│ │
│ 1. HTTPS (主要协议) │
│ - RESTful API │
│ - 端口: 6443 (默认) │
│ - 所有组件使用 │
│ │
│ 2. HTTP (不推荐) │
│ - 仅用于本地测试 │
│ - 端口: 8080 (默认,已废弃) │
│ - 生产环境禁用 │
│ │
│ 3. WebSocket (特殊场景) │
│ - kubectl exec/logs/port-forward │
│ - 基于 HTTPS 升级 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ API Server 对 etcd 的协议 │
├─────────────────────────────────────────┤
│ │
│ gRPC (HTTP/2) │
│ - 端口: 2379 │
│ - mTLS 双向认证 │
│ - 高性能二进制协议 │
└─────────────────────────────────────────┘
🛠️ 实战:监控各组件通信
1. 查看组件连接状态
# 1. 查看 API Server 监听端口
netstat -tlnp | grep kube-apiserver
# tcp 0 0 :::6443 :::* LISTEN 12345/kube-apiserver
# 2. 查看连接到 API Server 的客户端
netstat -anp | grep :6443 | grep ESTABLISHED
# tcp 0 0 192.168.1.10:6443 192.168.1.11:45678 ESTABLISHED (Kubelet)
# tcp 0 0 192.168.1.10:6443 192.168.1.10:45679 ESTABLISHED (Scheduler)
# tcp 0 0 192.168.1.10:6443 192.168.1.10:45680 ESTABLISHED (Controller Manager)
# 3. 查看 API Server 日志
journalctl -u kube-apiserver -f
# I1011 10:00:00.123456 12345 httplog.go:89] "HTTP" verb="GET" URI="/api/v1/pods?watch=true" latency="30.123ms" userAgent="kubelet/v1.28.0" srcIP="192.168.1.11:45678"
# 4. 查看 Kubelet 连接
journalctl -u kubelet -f | grep "Connecting to API"
2. 使用 tcpdump 抓包
# 抓取 API Server 通信(6443 端口)
tcpdump -i any -n port 6443 -A -s 0
# 抓取特定主机的通信
tcpdump -i any -n host 192.168.1.11 and port 6443
# 保存到文件,用 Wireshark 分析
tcpdump -i any -n port 6443 -w api-traffic.pcap
3. API Server Audit 日志
# API Server 审计配置
apiVersion: v1
kind: Policy
rules:
# 记录所有请求元数据
- level: Metadata
verbs: ["get", "list", "watch"]
# 记录创建/更新/删除的完整请求和响应
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
# 启用 Audit 日志
kube-apiserver \
--audit-policy-file=/etc/kubernetes/audit-policy.yaml \
--audit-log-path=/var/log/kubernetes/audit.log \
--audit-log-maxage=30 \
--audit-log-maxbackup=10 \
--audit-log-maxsize=100
# 查看审计日志
tail -f /var/log/kubernetes/audit.log | jq
# 示例输出:
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "Metadata",
"auditID": "abc-123",
"stage": "ResponseComplete",
"requestURI": "/api/v1/namespaces/default/pods?watch=true",
"verb": "watch",
"user": {
"username": "system:node:worker-1",
"groups": ["system:nodes"]
},
"sourceIPs": ["192.168.1.11"],
"userAgent": "kubelet/v1.28.0",
"responseStatus": {
"code": 200
}
}
🔍 高级话题
1. API Server 聚合层 (API Aggregation)
允许扩展 API Server,添加自定义 API。
┌────────────────────────────────────────┐
│ Main API Server (kube-apiserver) │
│ /api, /apis │
└───────────────┬────────────────────────┘
│ 代理请求
┌───────┴────────┐
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ Metrics API │ │ Custom API │
│ /apis/metrics│ │ /apis/my.api/v1 │
└──────────────┘ └─────────────────┘
注册 APIService
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1beta1.metrics.k8s.io
spec:
service:
name: metrics-server
namespace: kube-system
port: 443
group: metrics.k8s.io
version: v1beta1
insecureSkipTLSVerify: true
groupPriorityMinimum: 100
versionPriority: 100
请求路由
# 客户端请求
kubectl top nodes
# 等价于: GET /apis/metrics.k8s.io/v1beta1/nodes
# API Server 处理:
# 1. 检查路径 /apis/metrics.k8s.io/v1beta1
# 2. 查找对应的 APIService
# 3. 代理请求到 metrics-server Service
# 4. 返回结果给客户端
2. API Priority and Fairness (APF)
控制 API Server 的请求优先级和并发限制。
# FlowSchema - 定义请求匹配规则
apiVersion: flowcontrol.apiserver.k8s.io/v1beta3
kind: FlowSchema
metadata:
name: system-nodes
spec:
priorityLevelConfiguration:
name: system # 关联到优先级配置
matchingPrecedence: 900
distinguisherMethod:
type: ByUser
rules:
- subjects:
- kind: Group
group:
name: system:nodes # 匹配 Kubelet 请求
resourceRules:
- verbs: ["*"]
apiGroups: ["*"]
resources: ["*"]
namespaces: ["*"]
---
# PriorityLevelConfiguration - 定义并发限制
apiVersion: flowcontrol.apiserver.k8s.io/v1beta3
kind: PriorityLevelConfiguration
metadata:
name: system
spec:
type: Limited
limited:
assuredConcurrencyShares: 30 # 保证的并发数
limitResponse:
type: Queue
queuing:
queues: 64 # 队列数量
queueLengthLimit: 50 # 每个队列长度
handSize: 6 # 洗牌算法参数
APF 工作流程
请求进入 API Server
│
├─→ 1. 匹配 FlowSchema (按 precedence 排序)
│ - 检查 subject (user/group/serviceaccount)
│ - 检查 resource (API 路径)
│
├─→ 2. 确定 PriorityLevel
│ - system (高优先级,Kubelet/Scheduler)
│ - leader-election (中优先级,Controller Manager)
│ - workload-high (用户请求)
│ - catch-all (默认)
│
├─→ 3. 检查并发限制
│ - 当前并发数 < assuredConcurrencyShares: 立即执行
│ - 超过限制: 进入队列等待
│
└─→ 4. 执行或拒绝
- 队列有空位: 等待执行
- 队列满: 返回 429 Too Many Requests
查看 APF 状态
# 查看所有 FlowSchema
kubectl get flowschemas
# 查看 PriorityLevelConfiguration
kubectl get prioritylevelconfigurations
# 查看实时指标
kubectl get --raw /metrics | grep apiserver_flowcontrol
# 关键指标:
# apiserver_flowcontrol_current_inqueue_requests: 当前排队请求数
# apiserver_flowcontrol_rejected_requests_total: 被拒绝的请求数
# apiserver_flowcontrol_request_concurrency_limit: 并发限制
3. Watch Bookmark
优化 Watch 性能,减少断线重连的代价。
// 启用 Watch Bookmark
watch := clientset.CoreV1().Pods("default").Watch(
context.TODO(),
metav1.ListOptions{
Watch: true,
AllowWatchBookmarks: true, // 🔑 启用 Bookmark
},
)
for event := range watch.ResultChan() {
switch event.Type {
case watch.Added:
// 处理新增事件
case watch.Modified:
// 处理修改事件
case watch.Deleted:
// 处理删除事件
case watch.Bookmark:
// 🔑 Bookmark 事件(无实际数据变更)
// 只是告诉客户端当前的 ResourceVersion
// 用于优化断线重连
pod := event.Object.(*v1.Pod)
currentRV := pod.ResourceVersion
fmt.Printf("Bookmark at ResourceVersion: %s\n", currentRV)
}
}
Bookmark 的作用
没有 Bookmark:
┌──────────────────────────────────────┐
│ 客户端 Watch 从 ResourceVersion 100 │
│ 长时间没有事件(如 1 小时) │
│ 连接断开 │
│ 重连时: Watch from RV 100 │
│ API Server 需要回放 100-200 之间的 │
│ 所有事件(即使客户端不需要) │
└──────────────────────────────────────┘
有 Bookmark:
┌──────────────────────────────────────┐
│ 客户端 Watch 从 ResourceVersion 100 │
│ 每 10 分钟收到 Bookmark │
│ RV 110 (10 分钟后) │
│ RV 120 (20 分钟后) │
│ RV 130 (30 分钟后) │
│ 连接断开 │
│ 重连时: Watch from RV 130 ✅ │
│ 只需回放 130-200 之间的事件 │
└──────────────────────────────────────┘
4. 客户端限流 (Client-side Rate Limiting)
防止客户端压垮 API Server。
// client-go 的默认限流配置
config := &rest.Config{
Host: "https://192.168.1.10:6443",
// QPS 限制
QPS: 50.0, // 每秒 50 个请求
// Burst 限制
Burst: 100, // 突发最多 100 个请求
}
clientset := kubernetes.NewForConfig(config)
// 自定义限流器
import "golang.org/x/time/rate"
rateLimiter := rate.NewLimiter(
rate.Limit(50), // 每秒 50 个
100, // Burst 100
)
// 在发送请求前等待
rateLimiter.Wait(context.Background())
clientset.CoreV1().Pods("default").List(...)
📈 性能优化
1. API Server 侧优化
# API Server 启动参数
kube-apiserver \
# 增加 worker 线程
--max-requests-inflight=400 \
--max-mutating-requests-inflight=200 \
\
# Watch 缓存大小
--watch-cache-sizes=pods#1000,nodes#100 \
\
# etcd 连接池
--etcd-servers-overrides=/events#https://etcd-1:2379 \ # 分离 events
\
# 启用压缩
--enable-aggregator-routing=true \
\
# 内存缓存
--default-watch-cache-size=100
2. Client 侧优化
// 1. 使用 Informer (本地缓存)
factory := informers.NewSharedInformerFactory(clientset, 30*time.Second)
podInformer := factory.Core().V1().Pods()
// 从本地缓存读取,不访问 API Server
pod, _ := podInformer.Lister().Pods("default").Get("nginx")
// 2. 使用 Field Selector 减少数据量
listOptions := metav1.ListOptions{
FieldSelector: "spec.nodeName=worker-1", // 只获取特定节点的 Pod
}
// 3. 使用 Label Selector
listOptions := metav1.ListOptions{
LabelSelector: "app=nginx", // 只获取特定标签的 Pod
}
// 4. 限制返回字段
listOptions := metav1.ListOptions{
Limit: 100, // 分页,每次只返回 100 个
}
// 5. 批量操作
// 不推荐: 循环创建 100 个 Pod(100 次 API 调用)
for i := 0; i < 100; i++ {
clientset.CoreV1().Pods("default").Create(...)
}
// 推荐: 使用 Job/Deployment(1 次 API 调用)
deployment := &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Replicas: int32Ptr(100),
...
},
}
clientset.AppsV1().Deployments("default").Create(deployment)
💡 关键要点总结
通信模式
- 所有组件主动连接 API Server (API Server 从不主动推送)
- List-Watch 是核心机制 (初始 List + 持续 Watch)
- HTTP 长连接 (Chunked Transfer Encoding)
- ResourceVersion 保证一致性 (断线重连不丢事件)
认证授权
- X.509 证书 (集群组件)
- ServiceAccount Token (Pod 内应用)
- RBAC 授权 (细粒度权限控制)
- 准入控制 (请求验证和修改)
性能优化
- Informer 本地缓存 (减少 API Server 压力)
- Field/Label Selector (减少数据传输)
- APF 流量控制 (防止 API Server 过载)
- 客户端限流 (防止客户端压垮 API Server)
最佳实践
- 使用 Informer 而不是轮询
- 合理设置 QPS 和 Burst
- 避免频繁的 List 操作
- 使用 Field Selector 过滤数据
- 启用 Watch Bookmark
- 监控 API Server 指标