编程 Kubernetes 1.36 深度实战:从「声明式控制循环」到云原生操作系统——控制平面、etcd/Raft、自定义控制器与调度器全栈拆解(2026)

2026-07-08 06:47:38 +0800 CST views 37

Kubernetes 1.36 深度实战:从「声明式控制循环」到云原生操作系统——控制平面、etcd/Raft、自定义控制器与调度器全栈拆解(2026)

引言:Kubernetes 到底是什么

很多人把 Kubernetes 理解成一个「容器调度器」——把 Docker 镜像丢进去,它帮你分配到节点上跑。这个理解既对又错:对的是它确实干这件事;错的是这件事只是 K8s 能力的冰山一角。

更准确的说法是:Kubernetes 是一个以声明式 API 为核心的分布式系统控制平面,容器编排只是它最成功的一个应用。它本质上提供了一套「期望状态(Desired State)→ 实际状态(Actual State)→ 持续协调(Reconcile)」的通用机制,这套机制可以用于任何资源上——Pod、Service、Ingress、CRD,甚至你自定义的「数据库实例」「AI 训练任务」。

2026 年 4 月发布的 Kubernetes 1.36(代号 Haru,日语「春」)是这一年最重要的版本之一:70 项增强、18 项 Stable、25 项 Beta、25 项 Alpha,重点落在安全默认配置强化AI/ML 工作负载支持成熟大规模 API 可扩展性。其中两个 GA 特性尤其值得每一个生产集群关注:User Namespaces 正式稳定Mutating Admission Policies 用 CEL 取代了部分 Webhook

本文不堆砌概念,而是从架构师的视角,把 K8s 的控制平面、存储一致性、调度算法、扩展机制一次性拆透,并配上可运行的 Go 代码YAML,最后给出大规模集群的性能调优清单。读完你不仅能写 YAML,更能理解「为什么 K8s 是这样设计的」。


一、核心概念:声明式控制循环

1.1 命令式 vs 声明式

传统运维是命令式的:「去节点 A 上启动 3 个 nginx 实例」。声明式则是:「我期望有 3 个 nginx 副本在线」——至于怎么达到、在哪达到、挂了怎么恢复,交给系统自己处理。

把这两者的差别落到一个具体例子上会更直观。假设要跑 3 个 nginx:

命令式(Ansible / 脚本风格)

# 你得自己保证「恰好 3 个」,挂了你也得自己发现、自己拉
for i in 1 2 3; do
  ssh node-$i "docker run -d --name nginx-$i -p 80:80 nginx"
done
# 某天 node-2 重启,nginx-2 没了——没人知道,除非你写监控

声明式(K8s 风格)

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:1.27
        ports: [{ containerPort: 80 }]
kubectl apply -f nginx.yaml   # 写一次「期望」,之后全是 K8s 的活儿

关键差异不在「少敲几行命令」,而在于:命令式把「达到目标的过程」交给你,声明式把「维持目标的责任」交给系统。 前者是一次性动作,节点挂了就少一个;后者是持续契约,少一个 K8s 立刻补一个。这也就是为什么 K8s 集群里你很少「手动重启服务」——你改的是期望,系统负责把现实对齐到期望。

K8s 的全部智慧都在这句话里:

用户只声明「想要什么」,K8s 持续把「现状」拉向「期望」。

这就引出了 K8s 最底层、也最被低估的设计模式——调谐循环(Reconcile Loop / Control Loop)

1.2 调谐循环的三个角色

   期望状态 (etcd 中的对象: spec)
        │
        ▼
   [ Controller ]  ──watch──►  实际状态 (节点上的容器 / Endpoint / status 字段)
        │          diff
        ▼
   执行动作 (创建/删除/更新 Pod、更新状态字段)
        │
        └──────►  回到期望状态

每一个内置 Controller(Deployment、ReplicaSet、StatefulSet、Node、Endpoint、Service 等)都在跑这个循环:

  1. 通过 Informer 监听 etcd 中对象的变化(基于 watch 的增量事件,不是轮询);
  2. 把变化事件的 key(namespace/name)塞进一个限速 workqueue
  3. worker 从队列取出 key,调用 Reconcile:读当前实际状态,对比期望状态,执行弥补动作;
  4. 如果出错或状态未达终态,把 key 重新入队(带指数退避)。

理解这一点,你就理解了 K8s 的「自愈」能力从哪来:不是因为 K8s 聪明,而是因为它永不停止地把现实拉回你写下的声明。 一个 Pod 被 kill 了,ReplicaSet Controller 发现 status.replicas < spec.replicas,立刻创建新的——它不在乎「为什么」挂了,只在乎「差距」还在不在。

这里藏着一个被很多人忽略的设计哲学:K8s 选择「水平触发(level-triggered)」而非「边缘触发(edge-triggered)」。它不关心「刚才发生了什么事件」,只在乎「当前状态是否等于期望」。这带来惊人的健壮性——哪怕你错过了一百个 watch 事件、哪怕控制器崩溃重启,只要它重新读一次当前状态,就能继续正确地收敛。对比边缘触发系统(依赖「事件可靠投递」),K8s 不依赖事件的可靠性,只依赖状态的持续比对。这就是为什么在混乱、丢包、组件随时可能重启的生产环境里,K8s 依然能保持最终一致:它的正确性是「状态比对」保证的,不是「事件顺序」保证的。

1.3 为什么这个模式如此强大

因为「调谐」对资源类型是正交的。Deployment 调谐副本数,Ingress 调谐负载均衡器,你写的自定义 Controller 调谐你自己的 CRD——它们共用同一套 watch → queue → reconcile 骨架。这正是 K8s 生态(Operator 模式)爆发的根本原因:你不需要改 K8s 内核,只要注册一个新资源类型 + 一个调谐循环,就能让 K8s「管理」任何东西。 后面第三章我们会亲手写一个。


二、架构分析:控制平面与数据平面

一个生产 K8s 集群分为两大平面。

2.1 控制平面(Control Plane)

API Server:集群的唯一入口

所有读写都经过 kube-apiserver。它做四件事:

  • 认证(Authentication):你是谁?证书、token、OIDC、ServiceAccount。
  • 授权(Authorization):RBAC/ABAC 判断你能不能做。
  • 准入控制(Admission):变更类(Mutating)与校验类(Validating)Webhook/Policies,在对象落库前改写或拒绝。
  • 持久化网关:只跟 etcd 打交道,自身无状态,可水平扩展。

关键认知:API Server 是唯一能直接读写 etcd 的组件。 kubelet、scheduler、controller-manager 全部通过 API Server 间接操作状态。这样 etcd 的访问被收口,一致性与审计才有保证;API Server 前面可以放心挂 LB 做多副本。

etcd:强一致的状态存储

etcd 是一个基于 Raft 共识算法的分布式键值库。所有集群状态——Pod、Node、Secret、Lease——都序列化成 protobuf/JSON 存在这里。

Raft 保证:只要多数节点(quorum)存活,写入就一致且持久。代价是写吞吐受限(每次写要过半数节点确认),所以 etcd 是 K8s 扩展性第一瓶颈。后面性能章节会专门讲怎么护好它。

工程经验:生产 etcd 必须跑在独立、低延迟、SSD 的节点上,且永远不要和会抢占 I/O 的业务混部。一次 etcd 的 WAL fsync 抖动,会让整个集群的写操作集体变慢。

举个真实的战争故事:某团队把 etcd 和日志采集 Agent 放在同一块云盘上,平时无事。某天日志量突增,Agent 疯狂写盘,etcd 的 WAL fsync p99 从 2ms 飙到 200ms+。表象是「kubectl apply 偶尔卡 30 秒」「HPA 反应迟钝」「新 Pod 调度延迟」。排查花了半天——因为节点 CPU、内存都正常,监控面一片绿色,最后是用 etcd_disk_wal_fsync_duration_seconds 这个指标才定位到根因。把 etcd 迁到独立 NVMe 后,所有「玄学变慢」瞬间消失。结论是:etcd 的磁盘,是集群里最不能省的那块硬件。

Controller Manager:一堆调谐循环的集合

kube-controller-manager 把几十个内置 Controller 打包运行(Deployment、Namespace、ServiceAccount、ResourceQuota、Node lifecycle……)。每个 Controller 独立跑循环,互不阻塞。

Scheduler:决策 Pod 落在哪

Scheduler 不直接创建 Pod,而是给未绑定的 Pod 打上 nodeName。它分两阶段:

  • Filter(过滤):剔除不满足硬性约束的节点(资源不足、taint 不容忍、亲和性冲突、端口占用)。
  • Score(打分):对通过过滤的节点算分(资源均衡、拓扑、镜像就近),取最高分绑定。

2.2 数据平面(Data Plane)

  • kubelet:节点上的「大管家」,watch API Server 拿到本节点 Pod 清单,调用 CRI(容器运行时接口)拉镜像、起容器,并持续上报状态(心跳、资源、事件)。
  • 容器运行时:containerd / CRI-O,实现 CRI。1.36 要求运行时支持 CRI v1。
  • kube-proxy / CNI:负责 Pod 网络与 Service 的 iptables/IPVS/eBPF 转发。
  • CSI:存储插件,把 PV 挂进 Pod。

2.3 1.36 三大关键新特性

(1)User Namespaces 正式 GA —— 容器逃逸的最后一道闸

过去容器内 root(UID 0)映射到宿主机 root,一旦容器逃逸就拿到节点 root。User Namespaces 把容器内的 root 映射成宿主机上一个非特权的 UID 区间(如 100000–165535),哪怕进程突破隔离,在宿主机上也是个普通用户。

# 1.36 中,Pod 可声明使用 user namespace(需节点内核与运行时支持)
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  hostUsers: false   # 关键:禁用与宿主机的用户命名空间共享
  containers:
  - name: app
    image: myapp:1.0
    securityContext:
      runAsNonRoot: true
      seccompProfile:
        type: RuntimeDefault

hostUsers: false 在 1.36 配合运行时(containerd 2.x)已可在生产开启,是零信任集群的基线配置。它和 runAsNonRoot + seccomp 组合,把「容器逃逸 = 节点沦陷」变成一个需要层层突破的难题。

(2)Mutating Admission Policies GA —— 用 CEL 干掉一半 Webhook

以前要给 Pod 自动注入 sidecar、改资源限制、加标签,得维护一个 MutatingAdmissionWebhook 服务(起一个 HTTP 服务,配 CA、配失败策略 failurePolicy)。1.36 起,用 CEL(通用表达式语言) 把变更逻辑写成原生 K8s 对象:

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
  name: add-cost-center
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE"]
      resources: ["pods"]
  mutations:
  - patchType: WithAnnotations
    withAnnotations:
      cost-center: "team-payments"
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
  name: add-cost-center-binding
spec:
  policyName: add-cost-center
  matchResources:
    namespaceSelector:
      matchLabels: { tier: prod }

好处:无额外服务、无网络跳数、无 Webhook 挂掉导致集群不可写的运维噩梦;CEL 在 API Server 进程内求值,延迟从毫秒级 Webhook 调用降到微秒级。这是 1.36 对「运维简单性」最实在的贡献。

(3)Dynamic Resource Allocation(DRA)继续成熟 —— 面向 AI/GPU

传统 resources.limits.nvidia.com/gpu: 1 只能整卡、无法表达「需要 A100 且 NVLink 互联的 8 卡拓扑」。DRA 用结构化参数描述设备需求,1.36 增加了设备 taint/toleration 与**可分区设备(partitionable devices)**支持,让多卡、切片、异构加速器的调度成为可能——这正是「AI 工作负载支持日趋成熟」的落点。对跑大模型推理/训练的团队,这意味着 K8s 终于能像管理 CPU 一样精细地管理 GPU 拓扑。一个具体例子——要求「两块通过 NVLink 互联的 A100,且不与别人共享这张卡」:

apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
  name: gpu-a100-nvlink
spec:
  resourceClassName: gpu-a100
  parametersRef:
    apiGroup: gpu.example.com
    kind: GpuRequest
    name: two-a100-nvlink   # 描述:2 卡、NVLink 互联、独占
---
apiVersion: v1
kind: Pod
metadata:
  name: llm-trainer
spec:
  containers:
  - name: trainer
    image: trainer:2.3
    resources:
      claims:
      - name: gpu-a100-nvlink   # 引用上面的 ResourceClaim,而非旧的 limits.nvidia.com/gpu

注意 Pod 不再写 limits.nvidia.com/gpu: 2,而是引用一个语义化的 ResourceClaim——调度器据此走 DRA 的设备筛选,保证拿到的两块卡确实物理互联、且整卡独占。这就是 1.36 把「AI 工作负载支持成熟」从口号变成API 的体现。


三、代码实战

光说不练假把式。下面两段 Go 代码都属于「K8s 扩展机制」的核心,能让你从「用 K8s 的人」变成「改 K8s 行为的人」。

3.1 用 client-go 写一个最小自定义控制器

这是 Operator 的发动机。它监听所有 Pod,给缺少 cost-center 注解的 Pod 自动补上——和 2.3 的 CEL Policy 干同样的事,但用代码实现,能做任何 CEL 表达不了的逻辑(比如查外部 CMDB 决定注解值)。

package main

import (
	"context"
	"fmt"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/util/workqueue"
)

// DemoController 监听 Pod,给缺少 cost-center 注解的 Pod 打标
type DemoController struct {
	clientset kubernetes.Interface
	queue     workqueue.RateLimitingInterface
	synced    cache.InformerSynced
}

func NewDemoController(cfg *rest.Config) *DemoController {
	cs := kubernetes.NewForConfigOrDie(cfg)
	factory := informers.NewSharedInformerFactory(cs, 30*time.Second)
	podInformer := factory.Core().V1().Pods()

	c := &DemoController{
		clientset: cs,
		queue:     workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
	}

	// 事件 handler:只把对象的 key 丢进队列,真正的活儿在 worker 里干
	_, _ = podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			if key, err := cache.MetaNamespaceKeyFunc(obj); err == nil {
				c.queue.Add(key)
			}
		},
		UpdateFunc: func(_, newObj interface{}) {
			if key, err := cache.MetaNamespaceKeyFunc(newObj); err == nil {
				c.queue.Add(key)
			}
		},
	})

	c.synced = podInformer.Informer().HasSynced
	factory.Start(nil) // 启动所有 Informer 的 watch
	return c
}

func (c *DemoController) Run(ctx context.Context, workers int) error {
	if !cache.WaitForCacheSync(ctx.Done(), c.synced) {
		return fmt.Errorf("等待缓存同步超时")
	}
	for i := 0; i < workers; i++ {
		go wait.Until(func() { c.runWorker(ctx) }, time.Second, ctx.Done())
	}
	<-ctx.Done()
	return nil
}

func (c *DemoController) runWorker(ctx context.Context) {
	for c.processNext(ctx) {
	}
}

func (c *DemoController) processNext(ctx context.Context) bool {
	key, quit := c.queue.Get()
	if quit {
		return false
	}
	defer c.queue.Done(key)

	if err := c.reconcile(ctx, key.(string)); err != nil {
		c.queue.AddRateLimited(key) // 出错按指数退避重新入队
	} else {
		c.queue.Forget(key)
	}
	return true
}

// reconcile 就是控制循环的核心:读现状、对比期望、弥补差距
func (c *DemoController) reconcile(ctx context.Context, key string) error {
	ns, name, err := cache.SplitMetaNamespaceKey(key)
	if err != nil {
		return err
	}
	pod, err := c.clientset.CoreV1().Pods(ns).Get(ctx, name, metav1.GetOptions{})
	if err != nil {
		return err // 可能是已被删除,交给 rate-limit 重试或最终 Forget
	}
	if _, ok := pod.Annotations["cost-center"]; ok {
		return nil // 已达标,无需动作
	}
	// 弥补差距:打注解(用 patch 而非 update,避免覆盖并发修改)
	patch := []byte(`{"metadata":{"annotations":{"cost-center":"team-payments"}}}`)
	_, err = c.clientset.CoreV1().Pods(ns).Patch(
		ctx, name, types.StrategicMergePatchType, patch, metav1.PatchOptions{},
	)
	return err
}

func main() {
	cfg, _ := rest.InClusterConfig() // 在 Pod 内运行时用;本地调试可换 clientcmd
	ctrl := NewDemoController(cfg)
	_ = ctrl.Run(context.Background(), 2)
}

几个工程要点:

  • Patch 而不是 Update:避免读-改-写覆盖掉其他控制器的并发修改(经典的丢失更新问题)。
  • workqueue 自带限流(默认 DefaultControllerRateLimiter = 令牌桶 + 指数退避),防止一个坏对象把队列打爆。
  • Informer 有本地缓存(DeltaFIFO + Indexer),reconcile 里的 Get 走本地缓存,不击穿 API Server。这正是 K8s 能撑住成千上万 Controller 的原因。

3.2 写一个自定义调度插件(Scheduler Framework)

如果内置调度器满足不了你的拓扑需求(比如「同可用区的 Pod 尽量分散,跨区副本必须存在」),可以用 Scheduler Framework 的插件机制扩展,而不用 Fork 整个 scheduler。下面写一个 Score 插件,把 Pod 往「已有同类 Pod 最少」的节点推,实现简单的散开。

package main

import (
	"context"

	v1 "k8s.io/api/core/v1"
	framework "k8s.io/kubernetes/pkg/scheduler/framework"
)

const PluginName = "PodSpreadMin"

// SpreadPlugin 让同类 Pod 在节点间尽量分散
type SpreadPlugin struct{}

func (p *SpreadPlugin) Name() string { return PluginName }

// Score 阶段:节点上同类 Pod 越少,分越高(满分 100)
func (p *SpreadPlugin) Score(ctx context.Context, state *framework.CycleState,
	pod *v1.Pod, nodeName string) (int64, *framework.Status) {

	// nodeInfo 由 scheduler 注入;这里用伪逻辑演示思路
	// 真实实现需通过 state 或 SharedLister 统计该节点上同 app 标签的 Pod 数
	sameAppCount := countSameAppOnNode(pod, nodeName)
	score := int64(100) - int64(sameAppCount)*10
	if score < 0 {
		score = 0
	}
	return score, framework.NewStatus(framework.Success, "")
}

func (p *SpreadPlugin) ScoreExtensions() framework.ScoreExtensions {
	return nil // NormalizeScore 留默认
}

// 注册插件
func New(_ *framework.Runtime, _ ...framework.Option) (framework.Plugin, error) {
	return &SpreadPlugin{}, nil
}

// 伪函数:真实环境从 NodeInfo 的 Pods 列表统计
func countSameAppOnNode(pod *v1.Pod, nodeName string) int {
	app := pod.Labels["app"]
	_ = app
	_ = nodeName
	return 0
}

编译后用 kube-scheduler --config scheduler-config.yaml 加载,pluginConfig 里把 PodSpreadMin 放进 score 阶段即可。Scheduler Framework 的真正价值在于:Filter/Score/Reserve/Bind 每个阶段都可插拔,你能在不碰核心代码的前提下,把调度逻辑改造成贴合业务拓扑的样子。

3.3 YAML 实战:生产级工作负载

把前面讲的概念串成一个能在 1.36 集群跑起来的真实例子——高可用 Deployment + 自动扩缩 + 拓扑打散 + 安全加固:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
  labels: { app: payments-api, tier: prod }
spec:
  replicas: 3
  selector:
    matchLabels: { app: payments-api }
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0      # 保证容量不缩水
  template:
    metadata:
      labels: { app: payments-api, tier: prod }
    spec:
      hostUsers: false       # 1.36 安全基线:隔离用户命名空间
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels: { app: payments-api }
      containers:
      - name: api
        image: registry.internal/payments-api:1.8.2
        ports:
        - containerPort: 8080
        resources:
          requests: { cpu: "250m", memory: "256Mi" }
          limits:   { cpu: "1",    memory: "512Mi" }
        readinessProbe:
          httpGet: { path: /healthz, port: 8080 }
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet: { path: /livez, port: 8080 }
          initialDelaySeconds: 15
          periodSeconds: 20
        securityContext:
          runAsNonRoot: true
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: payments-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: payments-api
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

这段 YAML 里埋了 5 个生产经验:maxUnavailable: 0 保证滚动更新不掉容量;topologySpreadConstraints 强制跨可用区打散(故障域隔离);readOnlyRootFilesystem + drop ALL capabilities 把容器攻击面压到最小;探针区分 readiness(摘流量)和 liveness(重启);HPA 基于真实利用率而非绝对值扩缩。


四、性能优化:把控制平面护到数千节点

K8s 扩到一定规模,瓶颈从来不是「能不能调度」,而是控制平面扛不扛得住心跳与 watch 风暴。下面是经过大规模集群验证的调优清单。

4.1 etcd:第一瓶颈

  • 独立节点 + 本地 NVMe SSD,绝不和业务混部;WAL 与数据分盘更佳。
  • 监控 etcd_disk_wal_fsync_duration_seconds 的 p99:超过 10ms 就要警惕,超过 100ms 集群写会明显变慢。
  • 定期 etcdctl defrag 并开启历史压缩--auto-compaction-retention=8h),否则 MVCC 历史无限增长会拖垮读。
  • 合理设置 --quota-backend-bytes(默认 2GB,大集群可到 8GB),避免「空间不足」报错后需要复杂恢复。
  • 控制单个对象的尺寸:不要往 ConfigMap/Secret 里塞几十 MB 的东西,它会进 etcd 且被每个 watch 客户端拉取。

4.2 API Server:收口与节流

  • 开启 watch 缓存--watch-cache-sizes 对大资源(Pod、Endpoints)给足缓存,减少 etcd 直读。
  • 大 List 走分页:客户端用 limit=500&continue= 分页,避免一次性拉空 etcd。
  • 优先 protobuf:client-go 默认用 protobuf 与 API Server 通信,比 JSON 省带宽、省 CPU;自研客户端务必支持。
  • 审计日志采样:全量审计在大规模下 I/O 惊人,对非安全相关请求做采样。
  • 调大 --max-requests-inflight / --max-mutating-requests-inflight,匹配你的并发写入量。

4.3 Scheduler:吞吐与打分

  • PercentageOfNodesToScore:节点很多时不必给每个节点打分,设 30–50% 即可(默认已按集群规模自适应),显著降低调度延迟。
  • 精简打分插件:每多一个 Score 插件,每个 Pod 的调度成本线性上升。只保留真正需要的。
  • 提高并行度--parallelism 让多个 Pod 同时调度。
  • 关注 scheduler_pending_pods 指标:持续 >0 说明调度跟不上创建速度,需要优化过滤/打分或扩容 scheduler。

4.4 大规模集群的「红线」经验

  • 单集群建议上限(经验值,非硬限制):5000 节点 / 15 万 Pod / 30 万容器。再大建议拆多集群( federation / 多控制平面)。
  • EndpointSlice 替代老 Endpoints:老 Endpoints 对象在 Service 后端多时会爆炸,EndpointSlice 把它分片。
  • 用 Label/Field selector 收窄 watch 范围:Informer 不要 List(Watch=true) 全量,用 labelSelector 只关心自己命名空间。
  • kubelet 心跳与状态上报分离:1.36 的 NodeLease 让心跳走轻量 Lease 对象,节点数上千时大幅减轻 API Server 压力。

五、实战排障:把控制平面的知识用起来

讲架构不排障,等于学游泳不下水。下面四个高频故障,每个都对应前面讲过的机制——理解机制,排查就是从「瞎猜」变成「按图索骥」。

5.1 Pod 一直 Pending:调度器的「拒绝信」

Pending 的本质是:Scheduler 的 Filter 阶段一个节点都没放过。排查顺序:

  1. kubectl describe pod <p>Events——通常会直接告诉你原因:0/5 nodes are available: 3 Insufficient cpu, 2 node(s) had untolerated taint
  2. 资源不足requests.cpu/memory 加起来超过节点可分配量。用 kubectl top nodes 看真实余量,注意调度只看 requests 不是 limits
  3. Taint 不容忍:节点有 node-role.kubernetes.io/master:NoSchedule 之类污点,Pod 没配对应 toleration
  4. PVC 绑不上:使用了未 provision 的 StorageClass,或 WaitForFirstConsumer 模式下拓扑不匹配。

对应前面 2.1 的调度两阶段:Filter 失败 = 硬约束不满足,Score 再高也没用。 这也是为什么「为什么我的 Pod 被调度到了资源最紧的节点」这种抱怨,往往源于 Score 插件配置而非 Bug——先确认它过了 Filter,再谈打分。

5.2 CrashLoopBackOff:控制循环在反复救火

BackOff 是 kubelet 在容器反复退出时的指数退避不要只看日志第一屏,要:

  • kubectl logs <p> --previous 看上一次崩溃的日志(当前容器可能还没起来,拿不到);
  • 90% 是配置/依赖问题:连不上数据库(ConfigMap 写错)、迁移没跑、端口写错、健康检查路径 404;
  • 剩下 10% 是 OOM:看 kubectl describeLast State: Terminated, Reason: OOMKilled

这里体现 1.2 的控制循环特性:应用崩溃,kubelet 会按 backoff 不断重启,但永远不会「放弃」——除非你设了 restartPolicy: Never 或 Job 的 backoffLimit。理解这点,你就不会在凌晨被「它为什么不自己停」的告警搞糊涂:它不 停,是因为声明里说它就该一直跑。

5.3 OOMKilled:limits 不是保险箱

limits.memory 是硬上限,超过就被 cgroup 杀(exit 137)。常见误区:

  • 只设 requests 不设 limits,或 limits 远大于 requests,导致节点内存被「超卖」挤爆邻居(noisy neighbor);
  • JVM/Go 应用没识别容器内存限制,按宿主机物理内存设堆,结果在 limits 内被 OOM。Go 1.19+ 默认读 cgroup 限制,但老版本或手动 -Xmx 需显式按 limits 计算;
  • 排查:kubectl top pod 看实时用量,把 requests/limits 调到 P99 用量 ×1.2 左右最稳。

5.4 集群「变慢」但节点没事:八成是 etcd

当你发现 kubectl get 偶尔卡、HPA 不灵敏、Endpoint 更新延迟,而节点 CPU 很闲——去查 etcd:

  • etcd_disk_wal_fsync_duration_seconds 的 p99 是否飙升;
  • etcd_server_proposals_committed_totaletcd_server_proposals_applied_total 是否出现滞后(写积压);
  • 磁盘是否和其他负载混部、是否在机械盘或网络盘上。

这正是 4.1 强调「etcd 独立 SSD」的原因:控制平面的健康,最终收敛为 etcd 的 fsync 延迟。 把这条刻进运维肌肉记忆,能省下无数个半夜救火的电话。

5.5 调试进阶:直接读控制平面的「内心戏」

  • kubectl get events --sort-by=.lastTimestamp:集群在跟你说话,只是默认被刷屏淹没了。
  • kubectl get --raw /apis/...:绕过 kubectl 的格式化,直接读 API Server 原始对象,验证你的 YAML 到底被解析成了什么。
  • kubectl debug node/<n> --image=...:1.36 的节点调试能起一个排障 Pod 进节点命名空间,直接 chroot /host 看 kubelet 日志与容器运行时状态。
  • 给 API Server / scheduler / controller-manager 加 --v=5 日志级别(临时),能看到每一次 watch 事件与调度决策的完整 trace——性能问题定位的终极武器。

六、总结与展望

回看开头那个问题——「K8s 是什么」,现在答案更清楚了:它是一个用声明式控制循环统一了「资源管理」这件事的分布式操作系统内核。容器只是它管理的第一种资源;今天它管理 GPU(DRA)、管理有状态服务(StatefulSet + CSI)、管理 AI 训练任务(Operator)、甚至管理别的 K8s(多集群)。

1.36 这个「春」版本的意义,不在于某个炫目特性,而在于它把两条主线推到了 GA:更安全的默认(User Namespaces)更简单的扩展(CEL Admission Policies)。这两件事共同指向一个方向——让 K8s 从「需要专职 SRE 供奉的神龛」变成「普通团队也能安全驾驭的平台」。

往前看,几个趋势值得关注:

  1. 多运行时(Multi-Runtime)/ WASM 化:随着 WASI 0.3 成熟,部分 sidecar 与轻量工作负载可能从容器转向 WASM 模块,启动快、内存小、隔离强。K8s 正在通过 runtimeClass 与相关提案容纳这条路线。
  2. AI 原生化:DRA 只是开始,未来调度器会原生理解「模型权重分布在哪块本地盘」「推理卡与训练卡的 QoS 分级」,K8s 成为 AI 基础设施的底座。
  3. 控制平面的「无感」:随着 kube-apiserver 水平扩展、etcd 分片(如 kine/独立存储后端)的探索,超大集群的运维负担会持续下降。

但无论怎么演进,声明式 + 控制循环这个内核不会变。把它吃透,你就掌握了读得懂 K8s 过去、也预判得了它未来的钥匙。祝你在云原生的春天里,少踩坑,多写声明。

最后,给不同角色一条落地路径,别让这篇长文停在「读完觉得懂了」:

  • 应用开发:先把第三章的 YAML 模板吃透,重点理解 requests/limits探针topologySpreadConstraints 这三者的相互作用——它们决定了你的服务在线上稳不稳、故障时炸不炸。
  • SRE / 运维:第四章与第五章是日常,把 etcd 指标和排障清单沉淀成团队的 runbook,半夜报警时照着走,别临场发挥。
  • 平台 / 基础设施工程师:第三章的自定义控制器与调度器插件是你的武器库,Operator 模式能把你们团队的领域知识从「文档里的规矩」变成「集群里自动执行的代码」。

一个常被低估的认知:K8s 的技术本身(Raft、watch、调度算法)并不新鲜,它的胜利在于用一套统一的声明式约定,把「人到底该怎么管理基础设施」标准化了。 以前每家大厂都在重复造「配置管理 + 调度器 + 监控」三件套,K8s 把这套共识固化成了 API 与对象模型。理解这一点,你就不会再纠结「要不要自研一个更牛的调度器」——对绝大多数团队而言,真正缺的不是新调度器,而是把已有的声明式契约用对、用满。


本文基于 Kubernetes 1.36(Haru)撰写,代码示例以 client-go / Scheduler Framework 主流版本为准,生产使用前请结合自身集群版本验证 API 兼容性。

复制全文 生成海报 Kubernetes 云原生 控制平面 调度器 etcd

推荐文章

Linux 网站访问日志分析脚本
2024-11-18 19:58:45 +0800 CST
PHP 的生成器,用过的都说好!
2024-11-18 04:43:02 +0800 CST
MySQL 1364 错误解决办法
2024-11-19 05:07:59 +0800 CST
使用Rust进行跨平台GUI开发
2024-11-18 20:51:20 +0800 CST
CSS实现亚克力和磨砂玻璃效果
2024-11-18 01:21:20 +0800 CST
PostgreSQL日常运维命令总结分享
2024-11-18 06:58:22 +0800 CST
程序员茄子在线接单