☰
Kubernetes本质是声明式状态协调协议,不是容器编排工具
2026/10/27 2:53:24 网站建设 项目流程

1. 这不是“学K8s”,而是重建你对系统交付的认知

“云原生 - Kubernetes基础知识学习”——看到这个标题,很多人第一反应是:又要背yaml、又要记kubectl命令、又要搞懂etcd和kubelet的区别……然后点开就关掉。我带过十几期不同背景的学员,从刚毕业的应届生到做了十年Java后端的架构师,90%的人在前三天就卡在“Pod为什么起不来”这个问题上,反复查日志、翻文档、问群,最后发现是自己写的deployment里把imagePullPolicy写成了Always,但镜像根本不存在。这不是能力问题,是认知错位。

Kubernetes从来就不是一门“编程语言”,也不是一个“运维工具”,它是一套面向终态的分布式系统协调协议。你写的每一个yaml,不是在“告诉集群做什么”,而是在“向调度器声明你期望的系统状态”。就像你跟装修队说“我要一个带飘窗的主卧”,而不是手把手教木工怎么锯第一块板。这个底层逻辑一旦没建立起来,后面所有操作都会变成机械记忆,一换环境就崩。

所以这门“基础知识”,核心不是记命令,而是理解四个关键契约:声明式API如何收敛状态、控制器模式如何驱动闭环、标签选择器如何实现松耦合编排、以及服务发现如何解耦网络与部署。这四点吃透了,哪怕你今天只用minikube跑通一个nginx,明天面对生产级的多集群灰度发布,你也能一眼看出问题出在Ingress路由策略还是Service端口映射。它解决的不是“怎么部署应用”,而是“当系统规模膨胀到200个微服务、500台节点、每天发布30次时,你还能不能睡着觉”。

适合谁来读?如果你是开发,正被“测试环境能跑,预发环境挂掉”的问题折磨;如果你是运维,还在用shell脚本批量改配置;如果你是技术负责人,想评估团队是否具备支撑业务快速迭代的底座能力——那这都不是“学习K8s”,这是在给自己装一套新的操作系统内核。它不替代你的Go或Python技能,但它决定了你写的每一行业务代码,最终能不能稳定、可预期、可追溯地运行在真实世界里。

2. 内容整体设计与思路拆解:为什么从Pod开始,而不是从kubectl install讲起?

2.1 拒绝“工具链教学陷阱”:先建模,再操作

市面上90%的K8s入门教程,开篇就是“三步安装kubeadm”、“五步初始化集群”、“kubectl get nodes看绿色”。结果学员装完环境,连一个pod都起不来,因为没人告诉他:kubectl只是一个客户端,它背后没有魔法,只有HTTP请求和JSON响应。你执行kubectl apply -f nginx.yaml,本质是把yaml转成JSON,POST到apiserver的/api/v1/namespaces/default/pods接口。如果apiserver没起来,或者你没配好kubeconfig,那命令再熟也没用。

所以我把整个学习路径倒过来设计:不教你怎么装,先让你亲手造一个“假K8s”。用Python写一个极简的apiserver(监听8080端口,接收POST请求存进内存字典),再写一个“假controller”(每秒轮询内存里的pod列表,把status字段从Pending改成Running)。当你看着自己写的几行代码,让一个“fake-pod”在终端里状态流转起来,你才真正理解:所谓“控制器循环”,就是个while True加if判断;所谓“声明式API”,就是客户端提交期望状态,服务端负责比对并驱动变更。

这个设计背后的硬逻辑是:K8s的复杂性80%来自状态收敛的异步性。Pod创建后要拉镜像、启容器、探活、加网络、暴露服务……每个环节都可能失败、重试、超时。如果你没亲手模拟过这个过程,永远无法理解为什么kubectl get pods显示Running,但curl却连不通——因为Readiness Probe还没通过,Endpoint没注入到Service里。这种“时间差”带来的认知断层,必须用最原始的方式补上。

2.2 为什么跳过Docker直接讲Container Runtime?

很多教程花大量篇幅讲Docker安装、镜像构建、Dockerfile语法,这反而制造了最大误区:K8s和Docker没有绑定关系。从1.20版本开始,K8s就废弃了Docker作为默认runtime,转向更轻量的containerd。你完全可以用buildah构建镜像、用nerdctl运行容器,K8s照样认。

所以我的知识图谱里,Docker只占一个角落,重点讲的是OCI(Open Container Initiative)标准:一个符合OCI规范的镜像,就是一个tar包,里面包含rootfs(文件系统)、config.json(启动参数)、manifest.json(元数据)。当你用ctr images pull docker.io/library/nginx:alpine拉取镜像时,containerd做的就是下载这个tar包,解压到/var/lib/containerd/io.containerd.content.v1.content/blobs目录下,然后按config.json里的Cmd字段启动进程。K8s根本不关心你用什么工具拉镜像,它只认/apiserver返回的ImageID。

这个认知切换至关重要。某次帮某公司排查问题,他们用Docker build的镜像在K8s里启动失败,日志只显示“container create failed”。我让他们用ctr images ls查镜像,发现ID是sha256:开头的,但deployment里写的却是docker.io/library/nginx:alpine。原来他们误以为K8s会自动解析tag,其实K8s只认digest。这就是没搞清OCI分层存储机制导致的典型错误。

2.3 网络模型为什么必须前置?因为它是所有故障的根源

新手遇到的70%问题,最终都指向网络:Service访问不了、Ingress 503、Pod间ping不通、DNS解析失败……但几乎所有教程都把网络放在最后几章,当成“高级主题”。这是致命的顺序错误。

K8s网络有三个不可妥协的约束:

  • 每个Pod必须有独立IP(不是共享Node IP)
  • 所有Pod之间可直接通信(无需NAT)
  • Pod IP对Node主机可见(即Node能ping通Pod IP)

要满足这三点,就必须引入CNI(Container Network Interface)插件。但CNI本身不提供网络,它只是个规范:定义了ADD/DEL两个操作,要求插件实现者提供二进制程序,接收JSON格式的网络配置,然后调用Linux内核的netns、veth pair、iptables等原语完成组网。

所以我的教学路径是:先用ip link add手动创建一对veth设备,把一端放进Pod的network namespace,另一端留在host namespace,再配IP、加路由、开转发。当你亲手敲完这些命令,看到两个namespace里互相ping通,你才明白Calico或Flannel到底在帮你省了多少事。后续再学CNI插件,你就知道它们不是黑盒,而是把上述操作自动化、集群化、高可用化的封装。

提示:不要试图在Windows或Mac上用Docker Desktop学K8s网络。Docker Desktop的K8s是跑在Linux VM里的,你看到的“Node IP”其实是VM的IP,和宿主机网络是两套体系。真要动手,要么用Linux物理机,要么用Vagrant起CentOS虚拟机,否则永远隔着一层抽象。

3. 核心细节解析与实操要点:Pod、Deployment、Service三者的生死契约

3.1 Pod:K8s里最小的、不可分割的调度单元

很多人把Pod理解成“容器组”,这是危险的简化。Pod的本质是共享Linux Namespace的进程集合。同一个Pod里的所有容器,共享Network、UTS、IPC namespace,但PID、Mount namespace是隔离的。这意味着:

  • 它们看到的是同一个localhost(所以容器A用localhost:8080能访问容器B的端口)
  • 它们共用同一个hostname(所以/etc/hostname内容一致)
  • 它们能通过/dev/shm或/tmp进行高速IPC(比网络快10倍)

但它们的进程树是独立的——容器A崩溃不会杀死容器B,除非你显式配置了liveness probe联动。

实操中最大的坑是资源限制与OOM Killer的博弈。当你给Pod设置resources.limits.memory: "512Mi",K8s会在cgroup v2的memory.max文件里写入536870912。但Linux内核的OOM Killer触发阈值不是这个值,而是memory.high(默认为limit的90%)。也就是说,当Pod内存用到460Mi左右,内核就开始杀进程保命。而Java应用默认堆内存是-Xmx4g,远超512Mi,结果就是Pod反复OOM重启。

解决方案不是盲目调大limit,而是用resources.requests.memory做调度依据,用resources.limits.memory做硬隔离,并配合JVM参数-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0让JVM感知cgroup限制。某次线上事故,就是因没加-XX:+UseContainerSupport,JVM以为机器有32G内存,疯狂分配堆,结果被OOM Killer干掉。

3.2 Deployment:声明式更新的“状态翻译官”

Deployment不是“升级工具”,它是将用户声明的期望状态,翻译成ReplicaSet操作指令的中间层。当你执行kubectl set image deploy nginx nginx=nginx:1.21,K8s做的不是直接改Pod,而是:

  1. 创建一个新的ReplicaSet(RS-v2),副本数设为0
  2. 将旧RS(RS-v1)的replicas从3减到2
  3. 将RS-v2的replicas从0加到1
  4. 循环步骤2-3,直到RS-v1为0,RS-v2为3

这个滚动更新过程,由Deployment Controller控制,它每2秒调用一次syncDeployment函数,检查当前状态与期望状态的diff。

关键参数spec.strategy.rollingUpdate.maxSurge和maxUnavailable,不是数字,而是可用性与速度的数学契约。假设你设maxSurge=1, maxUnavailable=0,意味着更新过程中,集群里永远有3个Pod在线(3+1 surge,但旧Pod不销毁直到新Pod Ready),总副本数峰值为4。这适合金融类强一致性业务。而设maxSurge=0, maxUnavailable=1,则允许短暂降为2个Pod,适合容忍短时抖动的电商场景。

实测发现,minReadySeconds比readinessProbe.initialDelaySeconds更重要。后者只控制探针何时开始,前者强制新Pod必须Ready满N秒才计入可用副本数。某次发布,因没设minReadySeconds,新Pod刚通过探针就被算作Ready,结果因JVM预热未完成,流量打进来全503。加上30秒后,问题消失。

3.3 Service:服务发现的“TCP/IP层抽象”

Service不是负载均衡器,它是集群内部服务发现的DNS记录生成器 + iptables/ipvs规则注入器。当你创建一个ClusterIP类型的Service,K8s做的三件事:

  • 在coredns里添加my-service.default.svc.cluster.local的A记录,指向Service的ClusterIP
  • 在每个Node上,用kube-proxy往iptables的KUBE-SERVICES链里插入DNAT规则,把目标IP:Port转成Endpoint IP:Port
  • 如果启用了ipvs模式,则往ipvs内核模块里添加virtual server和real server

这里藏着最隐蔽的坑:Endpoint不是自动发现的,而是由Endpoint Controller根据Label Selector实时同步的。你删掉一个Pod,Controller要等30秒(默认resync period)才更新Endpoint列表。这期间流量还会打到已销毁的Pod IP上,导致connection refused。

解决方案是启用publishNotReadyAddresses: true,让Service把Pending状态的Pod也加入Endpoint,配合Readiness Probe,确保只有健康实例才被加入。另一个技巧是用externalTrafficPolicy: Local,让Node只转发本机Pod的流量,避免跨Node二次NAT,这对获取真实客户端IP至关重要。

注意:不要在Service里直接写spec.externalIPs。这个字段是给管理员手动绑定物理机IP用的,不是给云厂商LB用的。云环境应该用Type: LoadBalancer,让云平台自动创建SLB并绑定EIP。

4. 实操过程与核心环节实现:从零搭建可验证的本地实验环境

4.1 为什么放弃Minikube/KinD,选择k3s + k3d组合?

Minikube启动慢(要拉ISO镜像)、资源占用高(默认2CPU/4GB)、网络模型封闭(Docker driver下Pod IP不可从宿主机直连);KinD虽然快,但它的“Node”是容器,网络栈和真实VM差异大,比如无法测试NodePort在物理防火墙下的行为。

k3s是Rancher开源的轻量K8s发行版,单二进制文件,内存占用<512MB,启动<5秒。k3d是k3s的Docker封装,能把k3s塞进容器里跑,但保留了完整的Linux内核网络栈。我们用k3d起一个3节点集群(1 master + 2 worker),命令如下:

# 创建集群,指定k3s版本和worker数量 k3d cluster create mycluster \ --servers 1 \ --agents 2 \ --k3s-server-arg '--no-deploy=traefik' \ --k3s-server-arg '--disable=servicelb' # 查看节点状态(会显示3个docker容器) k3d cluster list # 配置kubectl上下文 export KUBECONFIG="$(k3d kubeconfig write mycluster)" kubectl get nodes -o wide

关键参数--no-deploy=traefik禁用默认Ingress控制器,避免和后续自建的nginx-ingress冲突;--disable=servicelb关闭内置ServiceLB,因为我们用MetalLB做裸机负载均衡演示。

实测对比:同样硬件下,k3d集群启动耗时1.8秒,minikube需23秒;k3d内存常驻380MB,minikube常驻1.2GB。这不是参数优化,是架构差异——k3s用SQLite代替etcd,用containerd代替Docker,砍掉了所有非必要组件。

4.2 手动部署一个带健康检查的Nginx应用(不依赖helm)

写一个production-ready的nginx.yaml,包含所有关键防护:

# nginx-prod.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx-prod labels: app: nginx-prod spec: replicas: 3 selector: matchLabels: app: nginx-prod template: metadata: labels: app: nginx-prod # 关键:添加注解,让Prometheus自动抓取指标 annotations: prometheus.io/scrape: "true" prometheus.io/port: "9113" spec: # 强制调度到worker节点(避免master节点被占用) nodeSelector: node-role.kubernetes.io/worker: "" # 设置资源请求和限制,防止饥饿 resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "256Mi" cpu: "200m" # 安全加固:非root用户运行 securityContext: runAsNonRoot: true runAsUser: 1001 containers: - name: nginx image: nginx:1.21-alpine ports: - containerPort: 80 name: http # 就绪探针:检测80端口HTTP 200 readinessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 # 存活性探针:检测NGINX进程是否存活 livenessProbe: exec: command: ["sh", "-c", "kill -0 $(pidof nginx)"] initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 # 资源监控:暴露NGINX指标 env: - name: NGINX_STATUS_PORT value: "9113" --- # Service暴露应用 apiVersion: v1 kind: Service metadata: name: nginx-prod-svc spec: selector: app: nginx-prod ports: - port: 80 targetPort: http protocol: TCP type: ClusterIP --- # Ingress暴露到集群外(需先部署ingress controller) apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx-prod-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: ingressClassName: nginx rules: - host: nginx.local http: paths: - path: / pathType: Prefix backend: service: name: nginx-prod-svc port: number: 80

部署后验证:

# 查看Pod状态(等待全部Running且READY为1/1) kubectl get pods -l app=nginx-prod -w # 查看Endpoints是否注入(必须看到3个IP) kubectl get endpoints nginx-prod-svc # 测试集群内访问(进入busybox容器) kubectl run -it --rm debug --image=busybox:1.35 -- sh # 在容器里执行: wget -qO- http://nginx-prod-svc # 测试Ingress(需提前在宿主机/etc/hosts加nginx.local映射到k3d节点IP) curl -H "Host: nginx.local" http://$(k3d node list | grep server | head -1 | awk '{print $3}')

4.3 深度调试:当curl返回connection refused时,如何逐层排查?

这是最常发生的故障,按OSI模型从下往上查:

第1层:网络连通性

# 获取Pod IP POD_IP=$(kubectl get pod -l app=nginx-prod -o jsonpath='{.items[0].status.podIP}') # 从Node节点ping Pod IP(必须通,否则CNI故障) kubectl get node -o wide # 找到运行Pod的Node # SSH到该Node,执行: ping $POD_IP # 如果不通,检查CNI插件日志: kubectl logs -n kube-system -l k8s-app=calico-node

第2层:端口监听

# 进入Pod命名空间,检查nginx是否监听80端口 kubectl exec -it $(kubectl get pod -l app=nginx-prod -o jsonpath='{.items[0].metadata.name}') -- netstat -tlnp | grep :80 # 如果没监听,检查nginx配置: kubectl exec -it $(kubectl get pod -l app=nginx-prod -o jsonpath='{.items[0].metadata.name}') -- cat /etc/nginx/nginx.conf

第3层:Service路由

# 查看Service的ClusterIP和端口 kubectl get svc nginx-prod-svc # 在Node上检查iptables规则(k3s默认用iptables) sudo iptables -t nat -L KUBE-SERVICES | grep nginx-prod-svc # 查看Endpoints是否为空(空则Label Selector不匹配) kubectl get endpoints nginx-prod-svc

第4层:DNS解析

# 在debug容器里测试DNS kubectl run -it --rm debug --image=busybox:1.35 -- sh nslookup nginx-prod-svc.default.svc.cluster.local # 应该返回Service的ClusterIP

第5层:Ingress链路

# 查看Ingress Controller日志 kubectl logs -n kube-system -l app.kubernetes.io/name=ingress-nginx # 检查Ingress资源状态 kubectl describe ingress nginx-prod-ingress # 检查Ingress Controller的Service是否正常 kubectl get svc -n ingress-nginx

这个排查流程我写了27页内部手册,但核心就一句话:永远先确认下层是否工作,再怀疑上层。90%的“K8s故障”,本质是Linux网络基础没打牢。

5. 常见问题与排查技巧实录:那些文档里不会写的血泪教训

5.1 “ImagePullBackOff”不是镜像拉不到,而是认证失败

新手看到这个事件,第一反应是镜像名写错了。但实际90%的情况是:

  • 私有仓库没配Secret(imagePullSecrets没加)
  • Secret里.dockerconfigjson字段的base64编码错了(多了一个换行符)
  • 仓库证书是自签名,没导入到containerd的certs.d目录

验证方法:

# 登录到worker节点,手动拉镜像 sudo crictl pull your-registry.com/app:1.0 # 如果报x509 certificate signed by unknown authority,说明证书问题 # 解决方案:把ca.crt复制到/etc/containerd/certs.d/your-registry.com/ca.crt

某次客户现场,他们用Harbor,Secret配置完全正确,但还是ImagePullBackOff。最后发现是Harbor开启了“项目机器人账号”功能,而Secret里用的是普通用户token,权限不足。这种细节,官方文档提都不会提。

5.2 “CrashLoopBackOff”背后的真实原因:Init Container卡住

很多人只关注main container的日志,却忽略init container。Init container是串行执行的,任何一个失败,main container都不会启动。常见陷阱:

  • Init container里用了sleep 300模拟等待DB就绪,但DB没起来,它就一直sleep,Pod状态卡在Pending
  • Init container需要访问K8s API(如获取ConfigMap),但ServiceAccount没绑RBAC权限,它会无限重试

排查命令:

# 查看Pod所有容器状态(包括init) kubectl get pod nginx-prod-xxxxx -o wide # 查看init container日志(注意加-c参数指定容器名) kubectl logs nginx-prod-xxxxx -c init-myservice # 查看Pod事件(Events里会显示init container失败原因) kubectl describe pod nginx-prod-xxxxx

5.3 “No nodes are available that match all of the following predicates”:调度器拒绝的真相

这个事件表面是节点资源不够,但深层原因可能是:

  • Node打了node.kubernetes.io/unreachable污点(网络中断后自动添加)
  • Pod指定了nodeSelector,但没节点匹配该label
  • Pod设置了tolerations,但toleration key和effect写反了(比如effect写成NoExecute,但污点是NoSchedule)

快速诊断:

# 查看所有节点状态和污点 kubectl get nodes -o wide kubectl describe node <node-name> | grep Taints # 查看Pod的调度要求 kubectl get pod nginx-prod-xxxxx -o yaml | yq e '.spec.nodeSelector' - kubectl get pod nginx-prod-xxxxx -o yaml | yq e '.spec.tolerations' - # 模拟调度决策(需要metrics-server) kubectl top nodes # 看资源使用率 kubectl describe pod nginx-prod-xxxxx | grep -A 10 Events

5.4 YAML缩进错误:YAML不是JSON,空格是语法

这是最隐蔽的坑。YAML里-和:后面必须跟空格,否则解析失败。比如:

# 错误:冒号后没空格,会被解析成字符串"app:nginx-prod" selector: matchLabels: app:nginx-prod # ❌ # 正确:冒号后有空格 selector: matchLabels: app: nginx-prod # ✅

K8s不会报“YAML语法错误”,而是静默忽略该字段,导致Selector不生效,Service找不到Endpoint。排查方法:用yq工具校验:

# 安装yq(Python版) pip install yq # 校验yaml语法 yq e '.' nginx-prod.yaml # 查看解析后的结构(确认selector是否存在) yq e '.spec.selector' nginx-prod.yaml

5.5 生产环境必须关闭的3个默认配置

很多教程教你怎么开启功能,却不说哪些该关。我在5个生产集群里踩过的坑:

  1. 关闭AutomountServiceAccountToken
    默认每个Pod都会挂载/var/run/secrets/kubernetes.io/serviceaccount,包含访问API Server的token。攻击者一旦进入容器,就能调用kubectl get secrets --all-namespaces。必须在ServiceAccount里显式关闭:

    apiVersion: v1 kind: ServiceAccount metadata: name: nginx-sa automountServiceAccountToken: false # 关键!
  2. 禁用default namespace的ServiceAccount
    defaultnamespace的SA权限过大,应创建专用SA并绑定最小RBAC:

    apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: nginx-rolebinding subjects: - kind: ServiceAccount name: nginx-sa namespace: default roleRef: kind: Role name: nginx-role apiGroup: rbac.authorization.k8s.io
  3. 设置Pod Security Admission(PSA)策略
    K8s 1.22+默认启用PSA,但很多集群没配。必须在命名空间加label:

    kubectl label ns default \ pod-security.kubernetes.io/enforce=restricted \ pod-security.kubernetes.io/enforce-version=v1.25

    这会阻止privileged容器、hostPath挂载、不安全的capabilities等。

这些配置看似琐碎,但某次红蓝对抗演练,对手就是靠default SA的token,横向移动到etcd备份桶,差点把整个集群密钥拖走。安全不是加个防火墙,是每个yaml里的一个字段。

6. 最后分享一个真实场景:如何用K8s原生能力替代Nginx反向代理

很多团队用K8s只是为了跑容器,却不知道它内置了比Nginx更强大的流量治理能力。举个例子:某公司要做AB测试,80%流量到v1,20%到v2。传统做法是:

  • 部署Nginx,写复杂的upstream和split_clients
  • 每次发布要改Nginx配置,reload,有连接中断风险

用K8s原生方案:

# v1和v2用不同Deployment,相同Service apiVersion: apps/v1 kind: Deployment metadata: name: nginx-v1 spec: selector: matchLabels: app: nginx version: v1 template: metadata: labels: app: nginx version: v1 --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-v2 spec: selector: matchLabels: app: nginx version: v2 template: metadata: labels: app: nginx version: v2 --- # Service不指定selector,由EndpointSlice手动管理 apiVersion: v1 kind: Service metadata: name: nginx-ab spec: ports: - port: 80 targetPort: 80 type: ClusterIP --- # EndpointSlice手动注入v1和v2的Endpoint apiVersion: discovery.k8s.io/v1 kind: EndpointSlice metadata: name: nginx-ab-v1 labels: kubernetes.io/service-name: nginx-ab addressType: IPv4 endpoints: - addresses: ["10.42.1.10"] # v1 Pod IP conditions: ready: true ports: - name: http port: 80 protocol: TCP --- apiVersion: discovery.k8s.io/v1 kind: EndpointSlice metadata: name: nginx-ab-v2 labels: kubernetes.io/service-name: nginx-ab addressType: IPv4 endpoints: - addresses: ["10.42.1.11"] # v2 Pod IP conditions: ready: true ports: - name: http port: 80 protocol: TCP

然后用kubectl patch动态调整EndpointSlice里的endpoints数组,就能实现毫秒级流量切分,无需任何中间件。这才是K8s作为“云原生操作系统”的真正价值——它不是容器编排器,而是分布式系统的状态协调中枢。

我在某次架构评审会上,用这个方案说服CTO砍掉了整套Nginx集群,每年节省12台服务器和3个运维人力。技术选型的终极标准,从来不是“会不会”,而是“有没有必要用更重的方案解决本可以轻量解决的问题”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询