ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

K8s入门实战:从Pod到Service打通容器编排核心链路

K8s入门实战:从Pod到Service打通容器编排核心链路

第一次翻开K8s(Kubernetes)文档的时候,我盯着Pod、Deployment、Service、Ingress这一串名词看了很久,脑子里只有一个疑惑:这些东西到底谁先谁后,哪个才是根?后来带过几个新人,我发现大家卡住的地方出奇一致——不是概念理解不了,而是缺一条能从“最小可运行单元”一路走到“真正对外提供服务”的完整路径。这篇就用3个动手案例,把这条链路从头到尾走一遍。

不管你是刚学完Docker想进阶的开发者,还是在公司里接触过一点微服务但没正经碰过K8s的运维同学,只要把这条路径跑通一遍,你至少能做到三件事:看懂绝大多数YAML文件、说清一个应用部署到K8s要经过哪些环节、遇到Pod起不来知道往哪几个方向排查。如果还能动手敲一遍命令,那收获会比看十篇教程都大。

1. 容器编排到底是什么:K8s解决的核心痛点

1.1 从“单机跑容器”到“一群容器需要管家”

先聊一个最朴素的问题:都有Docker了,为什么还要K8s?

假设你手上只有一个容器,docker run -p 80:80 nginx一条命令搞定,非常爽。但当你开始负责一个项目,需要同时跑前端、后端、数据库、消息队列,加起来七八个容器时,单机Docker就管不过来了。Docker Compose能在单机上统一编排这些容器,可是一旦规模上去,比如公司里有几十台机器、几十个微服务、每个服务还要多副本,问题就开始暴露:

  • 哪个服务该调度到哪台机器上?
  • 某个容器挂了,谁会把它拉起来?还是说只能等人发现再手动重启?
  • 流量大了想加副本,怎么加?
  • 发布新版本,怎么做到不中断服务?

这些问题归结起来就一句话:容器多了,需要有个东西来当“管家”。这个管家要负责分配机器、保证副本数量、管理网络、分配存储、处理滚动发布。K8s就是这个管家,而“容器编排”指的就是这一整套自动化的调度和管理机制。

用大白话类比一下:单容器就像你住单间,只要管好自己就行;到了微服务阶段,就像整栋楼有几百户人家,需要有物业系统来管理水电、安保、电梯、公共区域。K8s就是这套物业系统,Pod是住户,Deployment是物业员,Service是大门门禁。这套类比虽然粗略,但能帮你先把心智模型搭起来。

1.2 声明式思维:K8s和传统运维最大的不同

想要理解K8s,必须先理解“声明式”这个词。这是它和传统运维方式在根子上的区别。

传统方式是命令式的:你告诉系统“先创建这台VM,再装软件,再启动进程,如果不通了就重启”。每一步都要你下指令,系统不会主动推断你要什么。K8s反过来,你只需要告诉它“我期望有3个nginx副本、每个副本用1核CPU和512M内存”,剩下的事情——怎么创建、调度到哪里、一个挂了怎么办——全部由K8s自己处理。

这就是声明式的核心:描述目标状态,而不是描述操作步骤。

我见过太多新手在这个地方绕不出来,总想用写脚本的思路去理解K8s,问“那它到底是先创建Pod还是先创建Deployment?”其实K8s不关心你要它“先做什么、后做什么”,它只关心“当前状态是不是你期望的状态”。如果不是,它就调协(reconcile)到你期望的状态。理解了这个,后面看啥YAML都会顺很多。

1.3 三个绕不开的基础对象:Pod、Deployment、Service

整篇博文会围绕三个核心对象展开,先用一张关系脑图把位置摆清楚:

  • Pod:K8s里最小的调度和运行单元。可以理解为一个“逻辑主机”,里面可以有一个或多个容器。这些容器共享网络命名空间,能用localhost互相访问,也可以共享存储卷。
  • Deployment:一个控制器,负责管理一组Pod的“期望状态”。你告诉它“我要2个副本”,它会创建出来并一直盯住,少了就补,多了就删。
  • Service:一个稳定的访问入口。Pod是会换IP的(重建一次IP就变),Service给一组Pod提供一个固定的虚拟IP和DNS名字,并在这些Pod之间做负载均衡。

三者的关系像一条流水线:Deployment负责生产Pod,Service负责给这堆Pod挂上“门牌号”并分发流量。案例一先接触Pod,案例二用Deployment管起来,案例三用Service把访问入口打通。这条路径走完,你对容器编排的整个主干就基本有数了。

2. 案例一:创建第一个Pod,先跑起来一个最小单元

2.1 环境准备:本地先搭一个单机K8s

动手之前得先有个集群。生产环境一般是多台机器组成的正式集群,但学习阶段完全不需要那么重,本地就有轻量方案。常用的有两类:

  • minikube:官方推出的本地单机方案,总体思路是帮你启动一个虚拟机或容器环境,里面跑一个单节点的K8s。对新手最友好,自带一些辅助插件。
  • kind:全称是Kubernetes in Docker,本质是把K8s节点也跑成容器。启动更快,比较省资源,但有些网络相关的体验不如minikube贴近真实集群。

我个人建议优先试minikube。安装完成后,执行minikube start --cpus=2 --memory=2048启动一个本地集群,等看到“Done”提示,就说明控制面和节点都起来了。可以用kubectl get nodes验证一下,能看到一个状态为Ready的节点,就算环境OK。

这里提醒一句:国内网络环境下,拉取kubeadm和镜像可能较慢,minikube支持通过参数指定镜像仓库和节点镜像地址。常见的做法是设置--image-mirror-country=cn这类参数,或者提前配置好镜像加速器。如果第一次minikube start卡住,先别急着重装,多半是镜像下载问题,查一下网络再试。

2.2 用一条命令跑起第一个Pod:命令式操作

在K8s里,可以通过kubectl命令直接操作。最简单的第一条指令是这样:

kubectl create deployment nginx --image=nginx

这条命令做了什么事情?它创建了一个Deployment,Deployment又替你创建了一个Pod,Pod里的容器镜像就是nginx。执行完之后,看Pod状态:

kubectl get pod

正常情况下,你会看到类似下面的输出:

NAME READY STATUS RESTARTS AGE nginx-9d8f7b6c7-abc12 1/1 Running 0 12s

README是1/1,STATUS是Running,说明容器已正常运行。注意这个Pod名字后半段那串随机字符,是Deployment下面的ReplicaSet自动拼上去的,不用纠结,后面案例二会解释。

再细看Pod的信息:

kubectl get pod -o wide kubectl describe pod nginx-9d8f7b6c7-abc12

-o wide会额外显示Pod所在的节点、Pod IP等信息;describe是K8s排查问题的第一大利器,它会列出这个Pod的事件、挂载、条件等,后面排障章节还会再聊。

这里我想强调一个基础命令模式:kubectl + 动作(get/describe/logs/exec/delete...) + 资源类型 + 名称。这套模式贯穿始终,记住了它,后面所有命令都能推断出个大概。

2.3 用YAML定义Pod:声明式写法的门道

命令操作适合临时验证,但正式场景更推荐用YAML文件。因为YAML能版本化、能评审、能签入Git,别人看代码就能知道你部署了什么。这也是“基础设施即代码”思想的具体落地。

下面是一个最基础的Pod清单文件:

apiVersion: v1 kind: Pod metadata: name: my-nginx labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80

保存成pod-nginx.yaml,然后执行:

kubectl apply -f pod-nginx.yaml kubectl get pod my-nginx

几个关键字段逐个拆一下:

  • apiVersion:API版本。不同资源版本不同,Pod是v1,Deployment是apps/v1,后面记不住就查文档。
  • metadata.name:资源名称,在同一命名空间内必须唯一。
  • metadata.labels:标签。这是K8s组织资源的纽带,Service靠标签选择Pod,Deployment靠标签管理Pod,后面会反复用到。
  • spec.containers:容器列表。每个容器必须有名字、镜像,其他如端口、资源配额、环境变量都是可选但重要。
  • spec.containers.ports.containerPort:声明容器监听的端口。注意它更多是“信息性”的,不写也不会限制端口监听,但建议写,方便别人理解,也让Service选择时更明确。

这里要特别强调 labels 不是摆设。K8s里几乎所有关联关系都靠“标签选择器”来维护。如果你建的Pod写错了labels,后面Service根本找不到它,流量自然不通。新手写YAML最容易翻车的地方就在这里。

2.4 进容器看日志:验证Pod真的活着

Pod跑起来之后,怎么确认它真的在正常工作?最常用的两个操作:看日志、进容器。

kubectl logs my-nginx kubectl exec -it my-nginx -- /bin/bash

第一条会打印nginx的访问日志和启动日志,能看到“start worker process”之类的内容,说明服务进程起来了。第二条是进入容器内部,你可以执行ls、curl localhost之类的命令,直接感受容器里的文件系统和网络。

注意--后面的内容是要在容器内执行的命令。如果镜像里没有bash(部分精简镜像只有sh),可以把/bin/bash换成/bin/sh,这个坑我踩过好多次。

走到这里,你已经成功创建、查看、进入了第一个Pod。这个过程虽然基础,但把“Pod是最小调度单元”这回事落到了实处。

3. 案例二:用Deployment管理Pod,体验自愈与扩缩容

3.1 裸Pod的问题:为什么不能只靠Pod

案例一你创建了一个Pod,假设上面跑的是公司核心API服务。某个凌晨它挂了,你睡得正香,第二天上班一看业务挂了,怎么办?如果用的是裸Pod,K8s只会告诉你“这个Pod已经挂了”,不会帮你拉起一个新的。裸Pod没有自愈能力,这是它最大的短板。

其次,裸Pod没有“副本”的概念。你想让这个服务跑3份,就得手动创建3个Pod,而且这3个Pod也不知道彼此的存在。流量怎么分摊?谁负责协调?都不存在。

Deployment就是来解决这两个问题的。它像一个“监工”,你告诉它“我要2个一模一样的Pod”,它负责创建并持续保证刚好有2个。这2个Pod哪个挂了,它就立刻新起一个补上;你想改成5个,它也立刻调整。

Deployment和Pod、ReplicaSet的关系是这样的:

  • Deployment管理ReplicaSet
  • ReplicaSet负责确保指定数量的Pod存在

Deployment是更上层的概念,它额外带滚动更新、回滚、版本记录能力。ReplicaSet更像“计数器和副本管理器”,平时你不用直接操作它。

3.2 创建Deployment并观察副本管理

还是用nginx,这次用Deployment方式创建,并指定2个副本:

apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80

仔细看这个YAML,和Pod文件对比有两点关键区别。

第一,多了spec.replicas,它声明期望副本数是2。

第二,spec.template里嵌了一个Pod的定义。Deployment不管三七二十一,先把这个template复制若干份,然后让ReplicaSet去盯着数量。

另外spec.selector.matchLabels必须和spec.template.metadata.labels匹配,这个匹配关系是Deployment能找到“自己管辖的Pod”的依据。这段写错了,Deployment会立刻报错。

创建并观察:

kubectl apply -f deploy-nginx.yaml kubectl get deploy nginx-deploy kubectl get rs kubectl get pod

你会看到2个Pod分别被调度到节点上。想看它们在哪个节点、Pod IP是多少:

kubectl get pod -o wide

3.3 模拟故障:体验K8s的自愈能力

接下来做一个实验,手动删掉其中一个Pod:

kubectl delete pod nginx-deploy-xxxx-yyyy

然后立刻再看Pod:

kubectl get pod

到这一步,你会发现K8s在极短时间内(通常几秒)就自动新建了一个Pod,名字后缀是新的一串随机字符,状态迅速变成Running。因为Deployment发现“Pod数量少于期望值2个”,于是通知ReplicaSet补了一个。

这里我想让你刻意留意一下时间点。你可以先kubectl delete pod xxx,马上执行kubectl get pod -w(-w是watch模式,持续监听变化)。你会看到Pod生命周期的变化:被删的那个Terminating,新PodContainerCreating,然后Running。

这种能力在生产里就叫“自愈”。你不需要写任何重启脚本,不需要接告警后半夜起床手动删除,K8s会把状态调协到期望状态。这也是很多人说“K8s是声明式系统”的最佳实证。

3.4 滚动更新与回滚:换个版本不宕机

自愈是Deployment的基本功,滚动更新是它的另一项核心能力。

现在把镜像从nginx:1.25升级到nginx:1.26。有两种方式,一种是改YAML再apply,一种是直接命令行设置:

kubectl set image deployment/nginx-deploy nginx=nginx:1.26

执行后立刻看更新过程:

kubectl rollout status deployment/nginx-deploy

滚动更新的本质不是把2个Pod全删了再建,而是逐个交替:先起一个新的,等它Ready后删一个旧的,再起另一个新的,直到所有节点都变成新版本。这个过程里服务始终有副本在跑,不会出现全体停摆的窗口期。

如果更新后发现有问题,要回滚也很方便:

kubectl rollout undo deployment/nginx-deploy

K8s会按之前的版本记录回滚到上一个可用的版本。这个机制靠的是Deployment的历史版本记录,默认会保留最近10个版本。有时间的话可以kubectl rollout history deployment/nginx-deploy看看版本记录,理解一下“版本管理”这层意思。

关于更新节奏,K8s还给Deployment提供了两个关键参数:maxSurge和maxUnavailable。前者控制“最多能多出几个Pod”,后者控制“最多允许几个Pod不可用”。生产环境如果服务有状态或者资源有限,这两个参数需要仔细调,否则可能因为同时启动太多Pod导致资源竞争。新手先了解这层概念,等真正上线时再精细化。

3.5 手动扩缩容:一行命令加副本

流量上来了,要临时加副本,一条命令搞定:

kubectl scale deployment nginx-deploy --replicas=5 kubectl get pod

执行之后,Deployment会自动调整ReplicaSet的期望副本数,Pod总量从2变成5。如果流量回落,再执行一次--replicas=2就能缩回去。

生产环境还可以配置自动扩缩容(HPA,Horizontal Pod Autoscaler),实现“CPU超过某个阈值就自动加副本”。这块是K8s的进阶内容,新手可以先知道有这个东西,等真正掌握Deployment再说。

这里有个常见的疑问:Pod数量变了,IP变了,客户端怎么找到它们?答案是,光靠Deployment还不够,普通客户端不能直接连Pod IP,就得引出第三个案例的主角——Service。

4. 案例三:用Service暴露服务,从集群内到集群外

4.1 为什么Pod有IP却不能直接访问

你现在有5个nginx副本,每个Pod都有自己的IP。理论上,访问任何一个Pod的IP加80端口,都能访问nginx服务。但现实是:没人会直接去连Pod IP。原因有三个。

第一,Pod IP不稳定。Pod重建后IP就变了,你没法在配置里写死。

第二,IP是集群内部网络地址,从集群外基本路由不到。

第三,多个副本需要负载均衡,直接连某个Pod,无法分摊流量。

Service就是解决这三个问题的稳定层。它给一组Pod提供一个固定的虚拟IP和服务名,外界只要访问这个稳定入口,Service内部帮你转发到各个Pod。

4.2 ClusterIP:集群内部访问的默认方案

创建Service也有两种方式。先看YAML:

apiVersion: v1 kind: Service metadata: name: nginx-svc spec: selector: app: nginx ports: - port: 80 targetPort: 80

这里的关键点是selector.app: nginx,它会把所有带app: nginx标签的Pod都纳入这个Service后端。这就是前面为什么强调labels重要的原因。

将其保存为svc-nginx.yaml,执行:

kubectl apply -f svc-nginx.yaml kubectl get svc

你会看到nginx-svc的CLUSTER-IP一栏有一个IP,这个IP集群内部可以访问。验证一下:

kubectl run test-pod --image=nginx --restart=Never --rm -it -- curl http://nginx-svc

这条命令会创建一个临时Pod,在里面通过Service名nginx-svc访问服务,等价于在集群内部另一个Pod里访问http://nginx-svc:80。这就是Service的域名机制:同一个命名空间内,Service名就是可用的DNS名字。

这里要理解Service的转发细节。Service本身并不是一个进程或代理容器,它更像一个“虚拟服务”入口,真正干活的是每个节点上的kube-proxy组件。kube-proxy会维护一组转发规则,把发往CLUSTER-IP:port的流量均匀分发到后端Pod。想要查看当前Service关联了哪些后端Pod:

kubectl get endpoints

如果Service的selector写错,这个列表会显示为空或缺少IP,流量自然不通。这也是排障时最该先看的东西之一。

4.3 NodePort:把端口开到宿主机上

ClusterIP只有集群内部能访问,那外部怎么办?最基础的做法是把Service类型改成NodePort。

apiVersion: v1 kind: Service metadata: name: nginx-svc spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80 nodePort: 30080

重新apply后查看:

kubectl get svc

注意type变成了NodePort,端口段里显示30080/TCP。NodePort的原理是:在每个节点上开放一个端口(30000-32767之间),任何打到节点IP:30080的请求都会被转发到Service的ClusterIP,再路由到后端Pod。

本地minikube环境下验证,可以开另一个终端跑minikube service nginx-svc --url,它会返回一个可在宿主机浏览器打开的地址。

NodePort的优点是简单,把宿主机端口暴露出去就能访问。缺点是端口范围有限(30000-32767)、所有节点都要占用端口,生产环境直接暴露也不够安全。一般只在测试环境或临时联调时用。线上更推荐下面这个。

4.4 Ingress:更接近生产的暴露方式

说完NodePort,再说Ingress。上一节那种方式相当于给每个服务开一个宿主端口,但生产实践里通常会有很多服务,不可能每加一个服务就开一堆端口。Ingress解决的是“七层HTTP路由”的问题。

注意一个常见误区:Ingress不是一种Service,而是独立的一类API对象。它描述“什么样的外部请求路由到集群内哪个Service”,要真正生效,集群里还必须装一个Ingress Controller组件。这里可以用一句话快速理清两者的关系:Ingress是规则,Controller是执行规则的引擎。没有Controller,Ingress YAML再漂亮也不生效,这个顺序请一定记牢。

一个最简单的Ingress规则示意:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx-ing spec: rules: - host: nginx.demo.com http: paths: - path: / pathType: Prefix backend: service: name: nginx-svc port: number: 80

这个Ingress表示:当外部请求的Host是nginx.demo.com,Controller就把请求转发给nginx-svc这个Service的80端口。

使用Ingress前,需要在本机安装Ingress Controller相关组件,具体命令因环境而异,在minikube里一般是一行命令启用插件,kind则需要额外部署Controller的配置。这些步骤每个环境不太一样,建议以官方安装文档为准。

Ingress的实际优势在于可以按域名、按路径分流,可以配合HTTPS证书,可以做限流和更细粒度的路由控制。因此生产环境普遍把Ingress作为集群统一的流量入口,后端再配合Service解耦。

到这里,从Pod搭建最小单元,到Deployment管副本,再到Service和Ingress打通外部访问,这条主干链路已经完整走了一遍。

5. 新手高频问题排查手册:几个我踩过的坑

5.1 ImagePullBackOff:镜像一直拉不下来

新手遇到最多的报错之一,Pod状态会显示ImagePullBackOff或ErrImagePull。含义是:镜像拉取失败,K8s重试几次也拉不下来。

排查第一步,先看详细事件:

kubectl describe pod <pod-name> kubectl get events | grep -i image

常见原因有几个:

  • 镜像名称写错,或者tag不存在;
  • 镜像在私有仓库,而集群没有配置对应的镜像拉取凭证(imagePullSecrets);
  • 网络原因导致镜像仓库连接超时;
  • 本地临时环境用的仓库地址需要替换。

如果是网络原因,常见的处理是在容器运行时层面配置镜像加速器,或者把镜像名换成可达的镜像仓库地址。确认修改后,重新apply或删掉Pod让Deployment重建即可。

5.2 CrashLoopBackOff:容器反复重启

Pod能创建,但容器起来没几秒就挂,K8s会不断重启它,状态显示CrashLoopBackOff。Band这个状态名字很直白:“崩溃循环,退避”。

先看日志,尤其要看上一次崩溃的日志:

kubectl logs <pod-name> kubectl logs <pod-name> --previous

--previous能查看容器上一次退出前的日志,这是查CrashLoop最常用的命令。

常见诱因包括:应用启动需要但缺失的环境变量、配置错误、启动端口被占用、健康检查探针(livenessProbe)配置太严格导致直接被判定不健康被杀掉。最后一种比较隐蔽,如果日志看不出异常,但容器一直重启,就要检查有没有配探针,以及探针的路径和端口是否正确。

5.3 Pending:Pod一直排在队列里

Pod始终Pending,说明还没有被调度到某个节点上。和新手最相关的两个原因:

  • 集群节点资源不足,比如CPU或内存不够;
  • 镜像本身需要较大存储或特定条件,但没有匹配的节点。

关键的排查命令还是:

kubectl describe pod <pod-name>

在Events部分会看到调度器给出的提示,比如 “0/1 nodes are available: 1 Insufficient cpu”,这就说明节点CPU不够,需要释放资源或减小容器请求的资源量。如果节点不对,则要检查标签和污点相关配置。

很多新手只盯着get pod看状态,其实describe才是真正告诉你“为什么”的工具。任何诡异的K8s问题,先把describe和events打开,信息量会翻倍。

5.4 Service不通:先查Selector和外层路由

按案例三部署完Service,发现通过Service访问不了Pod。这时候查什么?

按顺序来:

kubectl get endpoints <service-name> kubectl describe svc <service-name>

第一个命令看Service后端有没有Pod。若ENDPOINTS为空,几乎可以断定是selector和Pod标签不匹配。检查Deployment里Pod模板的labels和Service的selector是否严格一致,注意大小写和字段名。

如果Endpoints正常,再试集群内部访问。我这里遇到过一个典型情况:Service的targetPort写成了80,但容器实际监听8080,端口不一致也会导致请求超时。所以port(Service对外端口)、targetPort(容器内端口)这两个概念一定要分清。

5.5 排障速查表

把常用排查命令集中放在这里,收藏以后能少走很多弯路:

现象优先命令关注点
Pod一直Pendingkubectl describe pod NAMEEvents里的调度失败原因
Pod反复重启kubectl logs NAME --previous上一次崩溃日志
镜像拉不下来kubectl describe pod NAME 或 get events镜像名、仓库可达性、密钥
Service访问不通kubectl get endpoints SVC_NAMEEndpoints是否为空
不知道去哪看问题kubectl get events --sort-by='.lastTimestamp'整个命名空间近期事件

最后多说几句

我个人带新人的经验里,最容易出效果的学习路径不是先背概念,而是照着这套流程动手敲一遍,再把每个环节故意弄坏一次,体会排障过程。比如故意把selector写错,看看Service怎么找不到后端;把副本缩到0,看看Deployment怎么报错。踩过这几个坑之后,对K8s的理解会比看十遍文档都深。

再分享一个小技巧:平时可以多开一个终端跑kubectl get pod -w,这样你的每一次操作(创建、删除、更新)都能立刻看到Pod状态迁移。这种“状态变化可视化”会帮你把“声明式系统持续调协”这句话真正内化,而不是停留在字面理解。

下一步可以试着扩展的内容包括:ConfigMap和Secret给Pod注入配置、PVC挂载持久化存储、命名空间做多环境隔离、HPA做自动扩缩容。以这条路径为基础再去学这些,会顺很多。K8s确实往上爬会觉得内容多,但把主干路径走扎实之后,剩下的都是往这棵树上挂叶子的事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进