1. 先把最核心的抽象模型说清楚
新手入门 K8s,最容易卡住的地方其实不是 yaml 语法,也不是 kubectl 命令,而是脑子里对“Pod 创建”和“服务暴露”这两件事始终串不起来。做容器编排的人应该都有这种经历:明明照着教程把 Pod 跑起来了,日志也正常,可别人就是访问不到;或者干脆把 Service 写出来了,端口也填了,结果 curl 回来一个 connection refused,一脸懵。这篇文章我就用 3 个从简到繁的案例,把从 Pod 创建到服务暴露这条链路彻底捋一遍,帮你建立一张清晰的 K8s 网络模型地图。
我会默认你已经有了一台能跑 kubectl 的实验环境(Minikube、kind、云厂商的托管集群都行),也大概知道容器是怎么一回事。如果你连容器都没跑过,建议先自己 docker run 一个 nginx 把玩两天,再来看 K8s,否则很多概念叠在一起会很痛苦。
1.1 为什么绕不开 Pod 和 Service
K8s 里最核心的调度单位是 Pod,不是容器。这个设计一开始会让很多人不适应:我明明只是想把一个容器跑起来,为什么中间还要套一层 Pod?
你可以把 Pod 理解成一个“容器宿舍”。宿舍里可以住一个或多个容器,它们共享同一套网络命名空间、共享同一个 IP、共享存储卷,互相之间用 localhost 就能通信。为什么要这样设计?因为有些业务场景天生就是“几个进程必须待在同一台机器上协同工作”,比如一个主进程加一个日志收集 sidecar,比如一个 Web 服务加一个本地缓存代理,它们之间通信极其频繁,拆到不同机器上反而增加延迟和故障面。
Pod 创建的本质其实很简单:你写一个 Pod 的 yaml,指定它用什么镜像、暴露哪个端口、挂载哪些存储,然后交给 kubelet 去把容器拉起来。但光有 Pod 还不够。Pod 有生命周期,它可能被杀掉、被重新调度到另一台节点上,每次重启 IP 都可能变。在一个微服务架构里,A 服务要调用 B 服务,如果 B 的地址一直在变,A 就得疯了。这时候就需要 Service 出场。
Service 解决的是“稳定的访问入口”这个问题。它给一组 Pod 提供一个虚拟的 ClusterIP,通过 label selector 把符合条件的 Pod 自动纳入负载均衡池。你只要记住 Service 的名字和端口,不管后端 Pod 怎么换,入口永远不变。这就是容器编排里“编排”二字的重点:不仅管容器的生命周期,还要管服务之间的稳定寻址。
1.2 你说的“服务暴露”,到底暴露的是什么
很多新手把“服务暴露”理解成“把 Pod 的端口映射到宿主机端口”,这个说法虽然不算错,但在 K8s 里不够准确。K8s 里有一整套从内到外的暴露层级,从 ClusterIP 到 NodePort 再到 LoadBalancer 和 Ingress,每一层解决的问题都不一样。
ClusterIP 是 Service 的默认类型,它只在集群内部可达,相当于给一组 Pod 发了一张“内网员工卡”。NodePort 则是在每个节点上开一个随机端口,把访问该端口的流量通过 Service 转发到后端 Pod,相当于在集群入口处开了个接待窗口。LoadBalancer 是在云环境下让云厂商分配一个公网负载均衡器,把流量导到 NodePort 上。而 Ingress 玩的是七层路由,根据域名和 URL 路径把请求分发到不同的 Service 上。
理解这套层级的关键在于:K8s 的流量是从外向里层层透传的,每一层都在解决“谁能访问我”和“怎么找到我”这两个问题。第 2 节到第 4 节我会用三个案例分别演示 ClusterIP、NodePort、Ingress 这三级暴露方式,从最简单的单 Pod 开始,一路延伸到多服务分流,这也是绝大多数生产项目实际走过的路径。
2. 案例一:单副本 Pod 起步,先跑通 ClusterIP
第一个案例我选的是一个最简单的网络请求统计服务,就用一个自研的 HTTP Demo 镜像,监听 8080 端口,提供/hello接口,返回一串 JSON。这个例子不涉及数据库、不涉及配置文件,适合拿来验证整套流程。项目名就叫“某 Demo 统计服务”,代码里的镜像地址我统一写成demo/hello-server:v1,你实际使用的时候替换成自己的镜像就行。
2.1 从一张最小 YAML 开始
我先创建 Pod。在真实项目中你不会直接创建裸 Pod,生产环境里 Deployment 才是主流,但新手阶段我强烈建议你先把裸 Pod 跑通,Access 一下日志,确认自己的 Pod 是最小可用模型,再往上面加控制器。
下面是最小可用的 Pod 定义:
apiVersion: v1 kind: Pod metadata: name: hello-server-pod labels: app: hello-server spec: containers: - name: hello-server image: demo/hello-server:v1 ports: - containerPort: 8080保存为pod-hello.yaml后执行:
kubectl apply -f pod-hello.yaml kubectl get pod -o wideget pod输出里你会看到 Pod 被分配了一个 ClusterIP 网段内的地址,比如10.244.0.5,这个 IP 在集群内部可以直接访问,但在集群外部是 ping 不通、curl 不到的。你可以试着在集群内任一节点上执行curl http://10.244.0.5:8080/hello,如果能拿到响应,说明 Pod 本身没有问题,问题只出在“外部如何找到它”。
2.2 ClusterIP 的访问链路与局限
只靠 Pod IP 显然不满足需求,于是我们给这组 Pod 建一个 Service。这是 Service 的 yaml:
apiVersion: v1 kind: Service metadata: name: hello-server-svc spec: selector: app: hello-server ports: - protocol: TCP port: 80 targetPort: 8080这个 Service 的关键字段只有两个:selector和targetPort。
selector负责把 Service 和 Pod 绑定起来,它会把所有带app: hello-server这个 label 的 Pod 选进自己的后端列表。这里有个新手特别容易踩的坑:label 必须和 Pod 的 metadata.labels 里的 key/value 完全一致,哪怕是多了一个空格、大小写不一致,Service 都会找不到任何后端 Pod,流量进来直接全部丢弃。targetPort就是后端 Pod 里容器真正监听的端口,port则是 Service 对外的虚拟端口。你访问ServiceIP:80,流量会自动转发到后端 Pod 的8080,中间那个映射关系由 kube-proxy 实现。
在集群内部,你可以通过两种方式访问这个服务。一是直接用 ClusterIP,比如curl http://10.99.88.77/hello;二是用 Service 的 DNS 名称,在同一个命名空间里直接curl http://hello-server-svc/hello,K8s 自带的 CoreDNS 会帮我们把 Service 名字解析成 ClusterIP。第二种才是生产中真正惯用的方式,因为 IP 可能变,名字不会变。
2.3 案例一里最容易被忽略的细节
第一个案例跑通之后,我建议你刻意做几个破坏性实验,这比看十篇博客都有用。
先把 Pod 删掉再重建一次,你会发现新 Pod 的 IP 变了,但 Service 不受任何影响,因为 Service 靠的是 label 而非 IP。这就是“稳定访问入口”的含义。再把 Service 里的 selector 故意改错,比如把app: hello-server改成app: hello,然后kubectl get endpoints,你会看到后端列表是空的。这时候你再去 curl ServiceIP,就会连接拒绝或超时,这个现象在真实环境里出现频率极高,原因基本都在 selector 不匹配或者后端 Pod 没起来。
还有一个细节是 Service 的port和targetPort如果都写成 80,表面上能通,但这种写法掩盖了真正的端口映射关系,一旦后端容器端口改了,你会排查得很痛苦。我的习惯是 targetPort 永远写成容器实际监听的端口,port 则根据团队规范来定,两者分开写,一目了然。
3. 案例二:多副本 Deployment + NodePort,让服务能被外部访问
第二个案例我换成了“某商品列表服务”,镜像为demo/product-service:v1。同样监听 8080 端口,提供/products接口。这个案例的核心目的有两个:一是从裸 Pod 过渡到 Deployment,引入多副本和滚动更新;二是把 Service 从 ClusterIP 升级到 NodePort,让集群外部的机器也能访问到服务。
3.1 为什么要用 Deployment 而不是裸 Pod
裸 Pod 的致命问题是:它自己不负责重生。一旦 Pod 所在的节点挂了,或者 Pod 里的容器 OOM 被杀,Pod 就真的没了,服务也就断了。Deployment 的本质相当于一个“监督员”,它描述你期望的最终状态(比如 3 个副本),然后由 Deployment Controller 不断比对当前状态和期望状态,只要有偏差就立刻调整。
Deployment 的 yaml 比裸 Pod 多了一层:
apiVersion: apps/v1 kind: Deployment metadata: name: product-service-deploy spec: replicas: 3 selector: matchLabels: app: product-service template: metadata: labels: app: product-service spec: containers: - name: product-service image: demo/product-service:v1 ports: - containerPort: 8080注意spec.selector.matchLabels和spec.template.metadata.labels必须一致,否则 Deployment 起不来。这个逻辑和 Service 的 selector 同源,都是在用 label 做关联。Deployment 会为每份副本创建一个独立的 Pod,每个 Pod 自动带一个随机后缀,比如product-service-deploy-7d8f9b6c5d-abc12,你可以通过 kubectl get pod 看到。
创建之后,我建议你执行kubectl get deployment && kubectl get rs && kubectl get pod,体会一下三层结构:Deployment 管 ReplicaSet,ReplicaSet 管 Pod 副本数量,Pod 才真正跑容器。这个层级关系是理解 K8s 声明式运维的关键,很多新手跳过了 ReplicaSet 直接看 Deployment,后面一遇到滚动更新就晕了。
3.2 NodePort 的端口范围与访问路径
Service 升级为 NodePort 只需要改一个 type,并在 ports 里增加 nodePort 字段:
apiVersion: v1 kind: Service metadata: name: product-service-svc spec: type: NodePort selector: app: product-service ports: - protocol: TCP port: 80 targetPort: 8080 nodePort: 30080NodePort 会在集群每一台节点的指定端口(本例是 30080)上建立一个入口。外部访问的路径是:任意节点IP:30080→ kube-proxy 根据 Service 规则转发 → 某个后端 Pod 的8080。注意这里的“任意节点”指的是集群里所有的 Node,不只是你执行 kubectl 的那台管理机,只要你 network 能打通,访问任何一个节点 IP 加 30080 都能到达 Service,因为每个节点上的 kube-proxy 都会监听这个端口。
这里有个内置约定要记住:NodePort 的端口范围默认是 30000-32767,超过这个范围 API Server 会直接拒绝创建。我见过有人把 nodePort 写成 8080,结果怎么提交怎么报错,就是因为没查这个默认范围。如果你不写 nodePort 字段,K8s 会自动分配一个这个范围内的随机端口,但不建议长期依赖随机值,生产环境最好在 yaml 里显式固定。
3.3 滚动更新和回滚的实际收益
Deployment 带来了比裸 Pod 更实际的好处:升级镜像时可以滚动更新。想升级到 v2 镜像,只需执行:
kubectl set image deployment/product-service-deploy product-service=demo/product-service:v2或者修改 yaml 里的 image 后重新 apply。K8s 会按照默认策略(maxUnavailable=25%,maxSurge=25%)逐个替换 Pod,先起一个新的,等它 Ready 后再杀掉一个旧的,整个过程服务不中断。你可以开另一个终端跑kubectl get pod -w,会看到 Pod 列表里新旧副本交替出现的画面,非常直观。
如果升级后发现问题,回滚命令同样简单:
kubectl rollout undo deployment/product-service-deploy这个命令会回退到上一个 ReplicaSet 版本,相当于把整个应用状态快照还原。我在实际项目里就用这个功能处理过一次镜像标签打错的事故,一条命令救回了线上服务。虽然平时不建议频繁回滚,但“知道还能回去”本身就让人放心不少。
4. 案例三:Ingress 统一入口,按域名和路径转发到不同服务
第三个案例场景更贴近真实业务:假设你已经有两个服务了,一个商品服务 product-service,一个是用户服务 user-service,每个服务都有自己的 ClusterIP。如果每个服务都开一个 NodePort,对外暴露一堆随机端口,既难记又难管理。这个问题的解法就是 Ingress。
4.1 Ingress 和 Service 的分工
Service 工作在四层(传输层),它管的是端口转发和负载均衡,但它理解不了 HTTP 里的域名、URL、Header。Ingress 工作在七层(应用层),它可以根据你访问的域名和路径,把请求路由到不同的 Service 上。
你可以把 Service 理解成公司内部的前台分机,把 Ingress 理解成总机接线员。总机对外只有一个统一号码,你打进来报部门名称,接线员帮你转到分机。没有总机的话,每个部门都得公布一个分机号,合作方要记一长串号码,非常痛苦。
Ingress 需要两个组件配合:Ingress Controller 和 Ingress 资源。前者是实际干活的负载均衡器,后者是描述路由规则的配置对象。你把路由规则写进 Ingress 资源,Controller 读到后会自动生成 Nginx(或者其他反向代理)的配置文件,并热加载生效。常见的 Controller 有 NGINX Ingress Controller,本地测试用的 kind 或 Minikube 也都自带或者可以方便地安装一个。
4.2 配置 Ingress 解析流程
假设你想让api.example-shop.local这个域名下的/products路由到商品服务,/users路由到用户服务,可以这样写:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-shop-ingress spec: rules: - host: api.example-shop.local http: paths: - path: /products pathType: Prefix backend: service: name: product-service-svc port: number: 80 - path: /users pathType: Prefix backend: service: name: user-service-svc port: number: 80这里的关键是pathType。K8s 支持Prefix和Exact两种匹配方式。Prefix表示前缀匹配,/products会匹配/products、/products/123、/products/abc/def;Exact则必须完整等于/products,多一个斜杠都不行。我建议新手先用 Prefix 感受一下,因为实际业务里路径带参数和子资源很常见。
创建 Ingress 之后,执行kubectl get ingress会看到 ADDRESS 一栏可能还是空的,这是因为 Ingress Controller 还没有把规则同步进来,稍等十几秒再查就有了。然后用命令行测试:
curl -H "Host: api.example-shop.local" http://<你的Ingress地址>/products如果返回商品 JSON,说明七层路由已经生效。注意 Ingress 不像 Service 那样按 IP 访问,它是按域名路由的,所以测试时必须带 Host 头才能命中对应规则。
4.3 本地没有域名时怎么测
很多新手在本地环境实验时会卡在“我没域名”这一步。其实这根本不是问题,最简单的方式就是改本地 hosts 文件,把域名指向你的集群入口地址。
在 Linux/Mac 上编辑/etc/hosts,Windows 编辑C:\Windows\System32\drivers\etc\hosts,加上一行:
127.0.0.1 api.example-shop.local如果你是通过 Minikube 跑实验,可以用minikube ip拿集群入口的真实 IP 来替换 127.0.0.1;如果是 kind,需要先把 Ingress Controller 的 80/443 端口映射到宿主机,总之原则是保证域名能解析到流量入口。
改完 hosts 之后直接curl http://api.example-shop.local/products就能命中 Ingress 规则。这一步做通之后你会明显感觉到,Ingress 把“外部访问入口”从多个随机端口收敛到了一个统一入口,这正是生产环境里最常用的暴露方式。
5. 实战中常见的故障状态与排查思路
三组案例讲完,你可以照着从 Pod 一路走到 Ingress 了。但实际操作中没有一次成功是很正常的,下面我会把新手最容易踩到的几个故障状态和排查思路整理出来,这些基本都是我当年一个个踩过来的。
5.1 Pending、ImagePullBackOff、CrashLoopBackOff
Pending状态的意思是 Pod 还在等待调度,最常见的两个原因:一是节点资源不足,比如 CPU 或内存不够,调度器找不到合适的节点;二是镜像需要访问某个私有仓库,但节点上没有 credentials,导致拉取失败。遇到 Pending 先执行kubectl describe pod <pod-name>,看 Events 部分写了什么,里面会直接告诉你失败原因。
ImagePullBackOff是镜像拉取失败,这个我也经常碰到。原因可能是镜像名打错了、镜像不存在、镜像平台架构不匹配(比如在 arm64 节点上拉了一个 x86_64 镜像),或者私有仓库认证失败。排查手段是kubectl describe pod拉到最后看 Reason,再用kubectl logs看容器日志。如果是私有仓库,记得先建 Secret,再在 Pod 的imagePullSecrets里引用它,这一步漏了必挂。
CrashLoopBackOff表示容器启动后反复崩溃。第一时间不要去看 Pod 状态,直接kubectl logs <pod-name>看应用日志。常见原因包括:启动命令参数配置错误、依赖的环境变量没设置、数据库连接不上、监听端口和容器内存的启动命令不一致。有一次我排查了半天,最后发现是应用里读的配置文件和挂载卷里的配置文件重名,启动参数顺序写反了,这种问题没有捷径,就是层层剥开看日志。
5.2 服务暴露后外部还是访问不了
如果你的 Pod 是 Running,Service 也能查到,但外部访问依然失败,按下面的顺序排查:
先确认 Service 有没有绑定到后端 Pod:
kubectl get endpoints如果 ENDPOINTS 列是空的,说明 selector 没匹配到任何 Pod,去检查 label 是否一致;如果 ENDPOINTS 有 IP,再确认这些 IP 是不是对应目标 Pod 的 IP:
kubectl get pod -o wideget endpoints里的 IP 和 Pod IP 对不上,说明 Service 选的不是你想暴露的那组 Pod,同样和 label 有关。
如果 Endpoints 正常,再从集群内部访问一次 Service 的 ClusterIP,如果内部通、外部不通,问题大概率出在 NodePort 或者网络策略上。K8s 默认不开启 NetworkPolicy 限制,但有些预装环境会自带策略;另外你要确认访问的节点 IP 是集群工作节点的 IP,不是管理机 IP,管理机通常没有 kube-proxy。
如果外网还是不通,很可能你访问的端口根本没暴露出来,比如用了云厂商的安全组或防火墙规则没放行 30080 端口。这个问题和 K8s 本身无关,但往往最耗时间。
5.3 排查工具和命令速查表
我把平时最常用的排查命令整理成一个速查表,贴在下面,建议收藏:
| 需求 | 命令 |
|---|---|
| 看所有资源对象的状态 | kubectl get all -n <namespace> |
| 看某个 Pod 的详细信息和事件 | kubectl describe pod <pod-name> |
| 看容器输出日志 | kubectl logs <pod-name> |
| 看上一个崩溃容器的日志 | kubectl logs --previous <pod-name> |
| 看 Service 的后端列表 | kubectl get endpoints |
| 进入 Pod 内部执行命令 | kubectl exec -it <pod-name> -- /bin/sh |
| 持续观察资源变化 | kubectl get pod -w |
| 查看事件流全局信息 | kubectl get events --sort-by=.lastTimestamp |
这套命令熟练之后,你会发现 K8s 的排查思路其实非常线性:先看资源状态,再看事件,再看日志,最后看网络链路。大多数问题都逃不过这条链路。
6. 一点实操体会
很多人第一次接触 K8s 都会被一大堆抽象概念吓退,这太正常了。我在最开始学习的时候也走了不少弯路——直接去啃架构文档,看了几天还是懵的。后来换个思路,不去研究整个系统怎么设计,而是先盯着一个 Pod 从创建到被访问的完整生命周期,亲手把它跑通,所有的概念才终于各就各位。
如果你想继续深入,下一步可以重点练习两个方向。一是把 Deployment 探针加进去,用过就明白为什么说 readinessProbe 和 livenessProbe 是生产环境必备;二是自己试试用 ConfigMap 和 Secret 给 Pod 注入配置,配合挂载卷,基本上到达了真实项目的入门线。容器编排这门手艺,玩的就是“声明期望状态、观察实际状态、不断调优”的循环,这篇文章只是开头,希望给你铺了一块能踩稳的踏板。