资讯动态

K8s网络模型:从Pod创建到Service暴露的完整链路解析

发布时间:2026/10/21 13:56:03 来源:尧图企业网站定制
1. 先把最核心的抽象模型说清楚新手入门 K8s最容易卡住的地方其实不是 yaml 语法也不是 kubectl 命令而是脑子里对“Pod 创建”和“服务暴露”这两件事始终串不起来。做容器编排的人应该都有这种经历明明照着教程把 Pod 跑起来了日志也正常可别人就是访问不到或者干脆把 Service 写出来了端口也填了结果 curl 回来一个 connection refused一脸懵。这篇文章我就用 3 个从简到繁的案例把从 Pod 创建到服务暴露这条链路彻底捋一遍帮你建立一张清晰的 K8s 网络模型地图。我会默认你已经有了一台能跑 kubectl 的实验环境Minikube、kind、云厂商的托管集群都行也大概知道容器是怎么一回事。如果你连容器都没跑过建议先自己 docker run 一个 nginx 把玩两天再来看 K8s否则很多概念叠在一起会很痛苦。1.1 为什么绕不开 Pod 和 ServiceK8s 里最核心的调度单位是 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 的 yamlapiVersion: 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/helloK8s 自带的 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 管 ReplicaSetReplicaSet 管 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-servicedemo/product-service:v2或者修改 yaml 里的 image 后重新 apply。K8s 会按照默认策略maxUnavailable25%maxSurge25%逐个替换 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/defExact则必须完整等于/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/hostsWindows 编辑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、CrashLoopBackOffPending状态的意思是 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 是 RunningService 也能查到但外部访问依然失败按下面的顺序排查先确认 Service 有没有绑定到后端 Podkubectl get endpoints如果 ENDPOINTS 列是空的说明 selector 没匹配到任何 Pod去检查 label 是否一致如果 ENDPOINTS 有 IP再确认这些 IP 是不是对应目标 Pod 的 IPkubectl 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 注入配置配合挂载卷基本上到达了真实项目的入门线。容器编排这门手艺玩的就是“声明期望状态、观察实际状态、不断调优”的循环这篇文章只是开头希望给你铺了一块能踩稳的踏板。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑