1. Kubernetes到底在解决什么问题:从一台机器到一群机器的痛苦跃迁
先说个大多数人都经历过的场景。你在一台服务器上装好了Docker,写好了Dockerfile,docker run一把梭跑起来,端口映射好,日志能看,一切正常。这时候老板说:咱们业务要扩容了,再上三台机器。你一下子傻眼了——多出来的机器怎么跟原来那台协同?容器IP变了怎么办?某个容器挂了谁来拉起来?流量怎么在多台机器之间分配?这时候你才真正意识到,单机Docker解决的是"怎么把一个应用装进标准箱子",而Kubernetes解决的是"怎么把成千上万个标准箱子有序地摆满整个仓库,还要保证它们各自该干嘛干嘛"。
说白了,Kubernetes就是一套自动化容器调度和运维系统。它的核心价值可以用三个词概括:调度、编排、自愈。调度解决"容器该跑在哪台机器上",编排解决"多个容器之间的配合关系",自愈解决"容器挂了怎么办"。这篇指南的路线也很明确,先从零开始搭一个能跑的环境,再理解它背后的设计逻辑,然后一步步把它扩展成一个多节点的分布式集群,最后把应用真正部署上去跑通。
很多人学习Kubernetes最大的误区在于:一上来就对着架构图背Pod、Deployment、Service这些名词。这就像还没开过车就先背发动机原理图——背得再熟,打火那一刻还是不知道踩哪个踏板。所以这篇指南我不打算从架构全景图开始,而是带着你亲手把一个最小集群搭起来,让容器真正跑起来,再回头看概念,你会觉得每个名词都是"本来就该长这样"。
2. 入门第一课:先建立一个"声明式"的心智模型
2.1 命令式与声明式,差的不只是习惯
Kubernetes跟Docker最大的思维方式差异,在于它是声明式的。Docker是命令式的——你输入docker run nginx,Docker就执行了"创建并运行一个nginx容器"这个动作。Kubernetes正相反,你提交的不是"我要运行nginx"这个命令,而是"nginx这个应用的期望状态是:3个副本,每个副本需要1核CPU和512M内存"这样一份描述文件,Kubernetes负责去实现并维护这个状态。
我打个比方。命令式就像你打电话给管理员说"去把三号房间的灯打开",管理员照做了,但灯被风吹灭了他不会主动去开。声明式则是你写了一张纸条:"三号房间的灯必须长期保持亮着",管理员得想各种办法保证这个结果,灯泡烧了换灯泡,断路器跳了合闸。这张纸条在Kubernetes里就是YAML文件,管理员就是集群里的各种控制组件。
这个心智模型的建立极其重要。你后续会遇到的所有Kubernetes问题,几乎都可以归结为一句话:当前状态偏离了期望状态。排查思路也就是反过来说,哪些机制负责纠偏,为什么它们没生效。命令式思维会让你总想着"再去ssh到某台机器上修一下",而声明式思维会让你回到"修改期望状态描述,让系统自己去纠正"这条正轨上来。
2.2 从零搭一个最简集群:把每个组件都跑明白
工具选择上,如果你只是想快速体验,用Docker Desktop自带的Kubernetes就行,你也可以理解为一个内置的单节点集群。但要真正理解组件之间的配合,我建议用kubeadm在虚拟机或云主机上搭一个三节点集群——一台master(控制平面),两台node(工作节点)。规格上不用太高,2核4G的机器三台就够了,做实验完全够用。
基础环境先确认三件事:所有节点装好容器运行时(推荐containerd,别用Docker了,后面细说原因),所有节点禁用swap,所有节点的时间同步正常。这三条少一条,kubeadm初始化大概率报错。
接下来就是标准三步。第一步,在master节点上执行kubeadm init --pod-network-cidr=10.244.0.0/16,指定一个Pod网段,后面装网络插件要用。初始化成功后输出末尾有一段kubeadm join命令和token,一定要复制保存好。第二步,在其他node节点上执行那串join命令,节点就加入了集群。第三步,装一个Pod网络插件,推荐Calico或者Flannel,二选一:
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml # 或者 kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml等两三分钟,kubectl get nodes看到所有节点都是Ready状态,一个最小的集群就成立了。
这时候零散的组件是什么关系呢?简单来说,kubectl提交YAML到kube-apiserver,kube-scheduler负责决定Pod放到哪个节点,kubelet在节点上真正启动容器,etcd存储所有状态。你在master上敲的每一个kubectl命令,本质上都是在跟kube-apiserver对话;你提交的每一份YAML,最终状态都会被记录到etcd里。
3. 理解集群的正常运转规律:不要被"分布式"三个字吓住
3.1 节点的自我管理机制
你现在有了一个真实的三节点集群,但应该感觉到它跟之前单机Docker的本质区别还没体现出来。让我做个实验:随便在某个node节点上systemctl stop kubelet,模拟这台机器故障。你观察一下会发生什么?等大约一两分钟,kubectl get pods -o wide会发现,之前跑在这台节点上的Pod被标记为Terminating状态,然后控制器自动在其他正常节点上创建了替代的Pod。全程你什么都没有做。
这就是Kubernetes自愈能力的直接体现。它背后的逻辑是这样的:kubelet定期向kube-apiserver汇报自己的心跳,超过一定时间没有心跳,控制器就会认为节点不可达,然后按照Pod的副本策略在别的节点上重建Pod。这个机制你可能听过,但亲手看过一次Pod在不同节点间"漂移",你对"控制器"这个词的理解会完全不同——它不是写死的代码,而是运行中的控制循环,时刻在对比期望状态与实际状态。
另一个观察窗口是kubectl describe node。你能看到节点的容量、已分配资源、污点(Taints)和标签。污点这个概念可以这么理解:节点上贴着一些"负面标签",比如node.kubernetes.io/unreachable,意思是"这机器联系不上,新Pod别往这放"。反过来,Pod上也可以加容忍度(Tolerations),表示"我就是要往这种节点上跑"。比如你要给集群加一台专门跑大模型推理的GPU机器,就给这台机器打上污点和标签,再让对应的Pod带上容忍度和节点选择器,一套调度规则就清清楚楚了。
3.2 为什么Pod是"一组容器"而不是"一个容器"
很多人第一个困惑就是:Docker里最小运行单位是容器,为什么到了Kubernetes变成了Pod?这个设计其实是经历过生产环境毒打之后做出的改造。
有些场景单个容器撑不住。比如一个日志采集容器和一个业务容器必须同一台机器上运行,共享网络栈,日志采集容器才能抓到业务容器的访问日志;再比如主容器启动之前需要一个Init容器预热挂载目录、等待依赖服务就绪。Kubernetes里Pod就是这样一个最小的调度单元,可以包含多个容器,它们共享同一个网络命名空间和存储卷,lifecycle紧密绑定,要么一起调度到某台机器,要么一起消失。
真正的生产判断技巧是:多个容器是否必须部署在一起?是否紧密耦合成一个"逻辑主机"?如果答案是肯定的,放进同一个Pod;如果不一定,应该拆成多个Pod。很多人刚上手时容易把Pod直接等同于容器,这会造成两个典型问题:一是明明可以把多个进程放一个Pod里共进退,却非要多起几个Pod,然后手动处理它们之间的通信和依赖;二是反过来,本来应该独立的Pod,因为嫌麻烦被塞进了一个Pod,导致无法单独扩缩容。记住一条原则:Pod是内聚的,Deployment才是横向扩展的外层。
4. 第一个实战作业:在集群上部署Nginx并理清流量怎么进来的
4.1 从kubectl run到YAML,一份nginx配置的完整拆解
跟着我的思路做第一个作业:在集群里部署一个Nginx,然后从集群外部访问它。如果你直接用kubectl create deployment nginx --image=nginx:latest,命令式创建的虽然快,但学不到什么东西。我建议你直接写一份YAML,因为声明式才是Kubernetes的核心工作方式。
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 resources: requests: cpu: "100m" memory: "128Mi" limits: cpu: "500m" memory: "256Mi"这份文件的信息量比看上去大得多。第一点,metadata.labels和selector.matchLabels必须一致,这是Deployment找到自己管理的Pod的关键,不一致的话Deployment会直接工作异常。第二点,spec.template是Pod的模板,任何新Pod的创建都以这个模板为准。第三点,resources里的requests相当于"预订"资源,调度器据此决定Pod放置到哪台节点;limits是"硬上限",超了就会触发OOM或CPU限流。这组配置在生产环境必须写,不写的话Pod可能无限抢占机器资源,拖垮同一个节点上的其他应用。
kubectl apply -f nginx-deployment.yaml,然后kubectl get pods -o wide能看到三个Pod被分散调度到不同节点(也不一定,这跟集群规模和调度器策略有关)。这个结果本身就是观察调度器工作方式的好窗口。
4.2 用Service把流量打通:ClusterIP、NodePort和Ingress的取舍
Deployment保证了Pod存在,但Pod是"短命"的——随时可能被替换、迁移、IP漂移。你不可能让用户去记一个随时变化的Pod IP。Service就是这个稳定的接入层,它给一组Pod提供一个稳定的虚拟IP(DNS名字),并做负载均衡转发。
再往上一层的经典操作是:创建一个Service暴露Nginx服务。写一个service的YAML:
apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx ports: - protocol: TCP port: 80 targetPort: 80 type: NodePortkubectl apply -f后kubectl get svc会看到nginx-service有类似10.108.x.x:80的ClusterIP和<节点IP>:31568的NodePort。这个31568端口是随机分配的,范围默认30000-32767。现在从浏览器访问集群内任意节点的IP:31568,多刷新几次,你会发现每次请求都可能被负载均衡到不同的Nginx Pod上。实测中kube-proxy转发带来了一定的随机性,而这正是我们想要的负载均衡效果。
再往后你会遇到的进阶问题是NodePort的局限性:每暴露一个服务就得开一个随机端口,服务多了端口管理混乱,端口范围上限也摆在那里。生产环境更常见的做法是:在所有节点前面加一个外部负载均衡器,配合Kubernetes的Ingress资源做7层路由。Ingress相当于一个智能流量入口,可以根据域名、URL路径把请求转发到不同Service。在云厂商环境(阿里云、腾讯云、AWS)里,创建LoadBalancer类型的Service或Ingress,云厂商会自动创建对应的负载均衡实例并绑上公网IP,这是另一个层面的工作了。
5. 从1到N的集群进阶:存储、配置热更新和细粒度升级
5.1 有状态应用的存储怎么办
Networks和Deployment解决的是无状态应用。但数据库、消息队列这些有状态应用,需要持久化数据,Pod一旦被重新调度,数据不能丢。这就引入了PV(PersistentVolume)和PVC(PersistentVolumeClaim)这套存储抽象。
理解它们最简单的方式是PV/PVC类比"运维人员准备的硬盘"与"开发者填写的领用申请单"。PV定义了存储的容量、类型(本地盘、云盘、NFS)、访问模式;PVC是使用方提出需求,比如"我需要5G能读写一次的存储",Kubernetes里的存储控制器会把满足条件的PV和PVC进行绑定。Pod里声明引用PVC,就能真正使用这块存储。
做实际实验最简单的方式是HostPath或local类型的PV,把宿主机目录挂载给Pod。但这有个致命限制:如果Pod被调度到别的节点,数据就留在原节点了,跟没存有什么区别。所以生产环境有状态应用要考虑分布式存储方案,云上就用云厂商的云盘或文件存储,自建机房可以用Ceph、Longhorn、GlusterFS这一类方案。选择存储和选择基础架构一样,是一个一旦运行起来就难以重来的决策,值得斟酌。
5.2 配置管理:ConfigMap和Secret的使用边界
每个应用都有配置文件和敏感信息。早期大家把配置写死在镜像里,导致的后果是每次改配置都必须重新构建镜像、重新发布。Kubernetes的ConfigMap和Secret把配置从应用镜像中剥离出来。
ConfigMap存明文配置,Secret存敏感内容(密码、证书)。两者映射到Pod里有两种方式:以环境变量的方式注入,或者以文件的方式挂载到容器路径里。我强烈推荐后者,因为以文件挂载支持热更新,ConfigMap变化后,挂载的文件内容会同步更新(虽然应用自己不一定自动重载,需要辅助手段触发)。环境变量方式在Pod创建时就固化了,改配置后只能重建Pod才生效,这点值得注意。
围绕Secret有个常见误区:它只是把内容存成了base64编码,这不算加密。api-server所在主机上拿到etcd数据的人可以直接读出来。生产环境必须开启etcd加密存储,或者用外部密钥管理(云厂商KMS、Vault),Secret的保密价值才能真正成立。
ConfigMap和Secret在Deployment里的写法:
spec: template: spec: containers: - name: nginx image: nginx:1.25 volumeMounts: - name: nginx-config mountPath: /etc/nginx/conf.d volumes: - name: nginx-config configMap: name: nginx-configmap5.3 渐进式升级:滚动更新的速度与安全平衡
Deployment之所以是Kubernetes最常用的部署对象,除了副本管理,还因为它自带滚动更新能力。默认策略是RollingUpdate,新Pod创建成功并就绪后,旧Pod才被销毁,整个过程不停服。这比全部杀掉再重建的Recreate策略优雅太多,也是生产环境默认推荐的更新方式。
更新的执行也很简单,改镜像版本后kubectl set image deployment/nginx-deployment nginx=nginx:1.26或者直接改YAML再kubectl apply。如果你想观察滚动更新的过程,可以试试这样一个命令组合:
kubectl rollout status deployment/nginx-deployment它能实时显示更新的进度,这在生产环境比较实用。
更进阶的场景是金丝雀发布和灰度发布:让一小部分流量跑新版本,验证稳定后再全量切换。Kubernetes原生Deployment做这个比较费劲,目前生产环境更通行的有两条路:一是用服务网格(Istio、Linkerd等)做流量权重的精细控制;二是借助Argo Rollouts这类工具来管理渐进式交付。到了这个阶段,你基本已经跨过了"会用"的门槛,进入"如何发布得更稳"的运维深水区了。
再提醒一个经验之谈:建议在Deployment中设置maxSurge和maxUnavailable参数。默认值虽然没有问题,但大流量服务值得更精细地控制,避免瞬时CPU打高或Pod全部不可用。我在生产环境一般配maxSurge: 25%和maxUnavailable: 0%,保证任何时刻至少有75%的实例在线,升级期间系统吞吐完全不受影响。
6. 生产环境的真实挑战:缩容误伤、驱逐风暴和资源抢占有理有据
6.1 节点重启时发生了什么:PodDisruptionBudget保护关键业务
练到这一步,你已经能搭建集群、部署应用、对外暴露流量、做滚动更新了。但真正的生产问题并不是功能性问题——反而是"正常维护"这件事会引起事故。比如你要给某台节点做内核升级,重新启动机器。没有保护的情况下,节点上的Pod直接全部被驱逐,如果业务没有多副本,服务就中断几秒甚至几分钟。
Kubernetes给出的对策是PodDisruptionBudget(PDB)。PDB的作用是规定"自愿中断"情况下最少保留多少个Pod。比如业务有3个副本,PDB要求最少有2个可用,那么节点管理操作同一时间只允许"自愿中断"1个Pod。运维和开发也能借助这个机制更清晰地沟通。
apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: nginx-pdb spec: minAvailable: 2 selector: matchLabels: app: nginx这里要特别区分"自愿中断"和"非自愿中断"。节点硬件故障、被OOM杀死属于非自愿中断,PDB管不了;而是运维主动做的排空节点、升级,这类才是PDB发挥作用的场景。很多公司吃了这个亏才知道:PDB不是锦上添花,而是维护操作的安全带。建议给所有有副本数的生产关键应用都建一份PDB。
6.2 资源争抢为什么在Kubernetes里更危险
前面说过,给容器设置requests和limits很重要。Kubernetes的kubelet在节点上会做两件事:一是根据容器的requests做调度决策,保证节点上所有容器的requests总和不超过节点容量;二是根据limits做运行时限制,超内存就OOM,超CPU就限流(CPU绑核)。
我曾经踩过一个印象非常深刻的坑。一台8核16G的节点上,跑着一些没有设置limits的Java应用和几个定时任务。平时没问题,某次业务高峰,Java应用突然开始疯狂申请内存,直接把节点内存打满。kubelet强制驱逐了它,但被驱逐的Pod在其他节点上立刻被重建——因为Deployment的replicas要求就是要保证副本数。于是其他节点的内存也被打满,连锁反应之下整个集群多个节点的内存全部告急,最终核心业务Pod也受到波及。这就像几个抢座位的人,没划座次时每个人都拼命往前挤,最后连讲台都被挤塌了。
解决办法就是规范每个应用的资源规格。但也要注意,limits不是设置得越高越好——如果所有容器都把requests压得很高,集群的调度效率就会大幅下降,很多Pod会因为没有"足够的资源"而处于Pending状态。这是一个精细的平衡活:先测量应用的常规水位,requests设为常规值,limits留出几倍的峰谷余量;然后通过HPA(水平Pod自动扩缩容)让副本数随着负载动态伸缩。以Nginx为例,kubectl get hpa、按CPU使用率设置minReplicas: 3, maxReplicas: 10,负载高时自动扩容,降低时自动缩容,集群资源利用率一下就上来了。
6.3 排障方法论:一个Pod从Pending到Running的完整检查链路
掌握了上面这些,最后的模块是排障。Kubernetes的排障跟传统运维最大的不同在于,你不能只盯一台机器,而要在"期望状态→实际状态"的差距里找原因。一个Pod从提交到Running要经过好几个节点,每一步都可能出问题。
我自己排障时习惯按这个顺序操作:
第一步,kubectl get events --sort-by=.lastTimestamp,看最近集群发生了什么。70%的问题,event里直接给了答案,比如镜像拉取失败、探针未通过、配额不足。
第二步,kubectl describe pod <pod-name>,看这个Pod本身的状态和Condition,定位是调度失败还是启动失败。调度失败会提示0/3 nodes are available,然后跟踪具体原因:是资源不足、有污点不匹配、还是节点有unreachable这种异常状态。
第三步,如果Pod已经启动了但反复重启,进去看日志和探针。kubectl logs -f <pod-name> --previous能看到上一次退出的日志,配合kubectl exec进入容器排查。探针是另一大类常见问题——readinessProbe失败会让Pod从Service的endpoints中被移除,livenessProbe失败会让kubelet反复重启容器。日志里业务可能一切正常,但探针认为"不健康"就一直在重启。
这套链路多跑几次之后,你会发现自己已经能从"看到Pod红色就慌",变成"扫一眼event和describe就基本锁定了问题方向"。故障现场不再是一团迷雾,而是一张有逻辑的排查地图。
7. 后记:从入门到真正上生产,我说点实在的
在写这篇指南的时候,我一直有一种感觉,Kubernetes难的不是某个具体组件怎么用,而是整套思维方式和自己以往的习惯不同。如果你从Docker迁移过来,最需要克服的就是"手动管理一切"的冲动;如果你从传统运维过来,还需要适应"基础设施即代码+声明显式运维"的节奏;如果你是开发者,最重要的是分清"应用该做什么"和"平台该做什么"的边界,别把两者搅在一起。
作为一个身边不少团队都栽过跟头的老兵,我最后想分享几点习惯养成建议:
第一,所有变更先走YAML,别敲裸命令。裸命令无法版本化、无法走审批流,线上出问题回滚极其痛苦。所有YAML进Git仓库,是Kubernetes运维的最低底线。
第二,至少把etcd的定期备份做成自动化定时任务。很多人觉得etcd备份不是自己的事,直到集群状态损坏、丢失所有Pod和Service定义的时候才追悔莫及。备份做起来并不复杂,etcdctl snapshot save走一遍就能跑通。
第三,每个季度主动做一次故障演习。把某台节点直接关机,看看集群能不能自愈、PDB是否生效、监控告警是否正常。不要等真实故障来考验你,那时候往往伴随着告警风暴和老板的夺命连环call。
Kubernetes里的东西越学越多,但总有一条主线贯穿始终:系统自始至终都在努力让实际状态向期望状态靠拢。你把这条主线理解透了,剩下的就是具体API和工具的熟练度问题,需要的是时间和生产环境的反复锤炼。