前言
如果你写过 Java 后端服务,一定熟悉这样的场景:应用启动时从配置中心拿到一组数据库或下游服务的地址,然后通过连接池去访问它们。在传统部署模式下,IP 和端口是相对稳定的;但在 Kubernetes 里,Pod 是短暂的(ephemeral),随时可能被创建、销毁、漂移,"这一刻还在运行的后端集合,下一刻可能完全不同"。官方文档在 Service 一章开篇就提出了这个核心问题:如果一组 Pod(后端)为集群内的另一组 Pod(前端)提供能力,前端如何知道并持续跟踪应该连接哪个 IP?
Kubernetes 给出的答案不是一个单一组件,而是一整套分层协作的网络体系:从每个 Pod 独享的集群级 IP,到 CNI 插件实现的 Pod 网络,再到 Service 提供的稳定虚拟 IP、EndpointSlice 维护的后端清单、kube-proxy 在内核态完成的流量转发、CoreDNS 提供的服务发现、Ingress 与 Gateway API 承载的集群入口流量,最后到 NetworkPolicy 定义的隔离边界。这套体系里,Kubernetes 自身只实现了其中一小部分(API、控制面逻辑、kube-proxy 等),其余大量能力由外部组件(CNI 插件、云厂商负载均衡、Ingress Controller 等)按统一的 API 契约来补全。
为什么值得系统学?因为网络问题是 Kubernetes 生产环境故障的重灾区:服务访问不通、DNS 解析间歇性失败、NodePort 端口冲突、Ingress 配置了却不生效、加了 NetworkPolicy 之后整个 namespace 互相访问不了……这些问题的表象千奇百怪,但根因几乎都能归结到对网络模型某个环节的理解偏差。本文以官方文档(当前版本 v1.37,以当前版本官方文档为准)为主线,逐段带读网络模型与 Service / Ingress 的核心章节,帮你把这条链路一次打通。
整体架构与核心概念
先看全景。官方文档在 "Services, Load Balancing, and Networking" 总览页把 Kubernetes 网络模型拆解为几块拼图,我们把它画成一张架构图:
┌─────────────────────────────────────────┐
│ Kubernetes 控制面 │
│ apiserver / endpointslice-controller │
│ (维护 Service、EndpointSlice 对象) │
└──────────────┬──────────────────────────┘
│ watch / list
┌──────────────────────────────┼──────────────────────────────┐
│ │ │
┌───────┴────────┐ ┌──────────┴─────────┐ ┌─────────┴────────┐
│ Node 1 │ │ Node 2 │ │ Node N │
│ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌────────────┐ │
│ │ kube-proxy │ │ │ │ kube-proxy │ │ │ │ kube-proxy │ │
│ │(iptables/ │ │ │ │(iptables/ │ │ │ │(iptables/ │ │
│ │ nftables) │ │ │ │ nftables) │ │ │ │ nftables) │ │
│ └─────┬──────┘ │ │ └─────┬──────┘ │ │ └─────┬──────┘ │
│ ┌─────┴──────┐ │ │ ┌─────┴──────┐ │ │ ┌─────┴──────┐ │
│ │ CNI 插件 │ │ │ │ CNI 插件 │ │ │ │ CNI 插件 │ │
│ │ (pod 网络) │ │ │ │ (pod 网络) │ │ │ │ (pod 网络) │ │
│ └─────┬──────┘ │ │ └─────┬──────┘ │ │ └─────┬──────┘ │
│ Pod A Pod B │ │ Pod C Pod D │ │ Pod E Pod F │
└────────────────┘ └────────────────────┘ └──────────────────┘
▲ ▲ ▲
└────────── Cluster Network ──┴──────────────────────────────┘
│
外部客户端 ──► Ingress Controller / Gateway ──► Service(VIP) ──► Pod
│
CoreDNS(服务发现)
结合官方总览页,这套模型的要点可以归纳为四条:
- 每个 Pod 在集群内拥有唯一 IP,同一 Pod 内的容器共享网络命名空间,可通过 localhost 通信。
- Pod 网络(集群网络)保证所有 Pod 无需 NAT、无需代理即可直接互通(除非人为做了网络分段)。
- Service API 为一组会动态变化的后端 Pod 提供一个长期稳定的 IP 或主机名;控制面自动维护 EndpointSlice 对象记录当前后端,服务代理(如 kube-proxy)监听 Service 与 EndpointSlice 变化,把数据面规则编程到内核或云 API 中。
- Gateway API(及其前身 Ingress)负责把集群内的服务暴露给集群外客户端;NetworkPolicy 则是内置的、控制 Pod 间以及 Pod 与外部之间流量的 API。
值得注意的是责任边界:Pod 网络命名空间的搭建由实现 CRI 的系统级软件完成;Pod 网络本身由 CNI 插件管理;服务代理默认由 kube-proxy 实现,但有些 CNI 会用自己的服务代理;NetworkPolicy 一般也由 Pod 网络实现(CNI)来落实——如果插件不支持,API 依然存在但毫无效果。这是贯穿全文的线索:API 由 Kubernetes 定义,效果由插件实现。下表先给出各组件的职责速览:
| 组件 / API | 归属 | 核心职责 | 典型实现 |
|---|---|---|---|
| CNI 插件 | 外部组件 | Pod 网络连通、IPAM、NetworkPolicy 执行 | Calico、Cilium、Flannel |
| Service + EndpointSlice | Kubernetes 内置 API | 稳定访问入口、后端清单维护 | 控制面自动管理 |
| kube-proxy | Kubernetes 默认提供 | Service 虚拟 IP 的流量转发(iptables/nftables) | 可被 CNI 自带代理替代 |
| CoreDNS | 附加组件(addon) | Service/Pod 的 DNS 记录 | cluster add-on |
| Ingress / Gateway API | API 冻结 / 扩展 API | 集群南北向 HTTP(S) 流量入口 | NGINX Ingress、Envoy Gateway 等 |
| NetworkPolicy | 内置 API | 三层/四层流量隔离 | 由 CNI 插件执行 |
你现在应该掌握了什么:Kubernetes 网络不是一个组件而是一组契约,控制面管"该是什么",插件和数据面管"实际怎么通";阅读下文时始终带着"这个能力是谁实现的"这个问题。
Pod 网络模型与 CNI 职责
官方 Cluster Networking 一页把要解决的通信问题分成四类:紧耦合的容器间通信(由 Pod 与 localhost 解决)、Pod-to-Pod 通信、Pod-to-Service 通信、External-to-Service 通信。传统多机部署中,跨主机容器互通往往需要显式建链或把端口映射到宿主机;Kubernetes 拒绝走这条路,它的哲学是"把 Pod 当作 VM 或物理机对待"——端口分配、命名、服务发现、负载均衡、应用配置、迁移,都按"每台 Pod 是一台独立主机"的心智模型来设计。
模型落地为三条硬性规则(官方原文):每个 Pod 获得集群内唯一 IP;同一 Pod 内所有容器共享该 Pod 的私有网络命名空间,可经 localhost 互访;在未被有意分段的前提下,任意 Pod 可以不经过代理、不做地址转换(NAT)地与任意其他 Pod 直接通信,无论它们是否在同一节点上(Windows 上 host-network Pod 例外);节点上的 agent(kubelet、系统守护进程)能与本节点所有 Pod 通信。
Kubernetes 自身并不实现这些规则。总览页写得很清楚:Pod 网络命名空间的设置由实现 CRI 的系统软件负责,而 Pod 网络本身由 "pod network implementation" 管理——在 Linux 上,绝大多数容器运行时通过 CNI(Container Network Interface)与 Pod 网络实现交互,因此这些实现通常被称为 CNI 插件。这带来两个实际影响:
第一,选型差异巨大。官方 Cluster Networking 页指出,不同 CNI 插件能力差异悬殊:有的只做"增删网卡"这类基础功能,有的还提供编排系统集成、多插件链式调用、高级 IPAM 等能力。对一个 Java 团队来说,这意味着"我在集群里能不能用 NetworkPolicy"这个问题在选 CNI 的那一刻就已经决定了大半。
第二,IP 地址规划是集群级事务。官方文档要求集群为 Pod、Service、Node 从互不重叠的地址段中分配 IP:网络插件负责给 Pod 分配,kube-apiserver 通过 --service-cluster-ip-range 负责给 Service 分配,kubelet 或 cloud-controller-manager 负责给 Node 分配。三类地址必须显式规划且互不重叠,否则排障时会出现各种"莫名不通"。
你现在应该掌握了什么:Pod 网络的三条规则(唯一 IP、localhost 共享、全集群免 NAT 直通的"CNI 职责"),以及"模型是 Kubernetes 定的、网络是 CNI 插件做的"这一责任划分。
Service 与四种类型
带着"前端如何找到后端"的问题,我们进入官方 Service 文档。文档开头的论述值得逐句品味:用 Deployment 运行的应用会动态创建和销毁 Pod,你无法预知某个时刻有多少 Pod 健康、甚至不知道健康的 Pod 叫什么名字;如果前端需要自行跟踪后端 IP,应用就必须内嵌一套服务发现逻辑——这正是 Service 要解耦掉的东西。
Service 是 Kubernetes 里的一个 API 对象(和 Pod、ConfigMap 一样),每个 Service 定义一组逻辑端点以及访问策略。最常见的定义方式是指定 selector。官方示例:一组监听 TCP 9376、带标签 app.kubernetes.io/name=MyApp 的 Pod,可以用如下 Service 发布:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
apply 之后,Service 获得默认类型 ClusterIP 和一个集群 IP;文档特别提醒:Service 可以把任意 port 映射到任意 targetPort,默认 targetPort 与 port 相同;Pod 端口可以有名字,Service 的 targetPort 可以直接引用端口名而不是数字,这样后端升级改端口号时客户端无感知。控制面上,Service 的控制器持续扫描匹配 selector 的 Pod,并相应更新 EndpointSlice。
Service 对象名必须是合法的 RFC 1123 label name——这解释了为什么 Service 名不能带下划线和大写字母,对习惯 Java 驼峰命名的同学是一个小的迁移成本。
接下来是文档的 "Service type" 一节,四种类型,层层递进:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
selector:
app.kubernetes.io/name: MyApp
ports:
- port: 80
targetPort: 80
nodePort: 30007
四种类型对比:
| 类型 | 暴露方式 | 适用场景 | 关键字段 |
|---|---|---|---|
| ClusterIP(默认) | 集群内部 IP,仅集群内可达 | 服务间内部调用 | .spec.clusterIP,可设为 None 变 headless |
| NodePort | 每个节点 IP 上的静态端口(默认区间 30000-32767) | 自建负载均衡、非标准环境 | .spec.ports[*].nodePort |
| LoadBalancer | 云厂商外部负载均衡器异步创建 | 云上对外服务 | .status.loadBalancer、allocateLoadBalancerNodePorts、loadBalancerClass |
| ExternalName | DNS CNAME 映射,不做任何代理 | 把集群外服务"伪装"成集群内名字 | spec.externalName |
文档里几个容易被忽略但生产关键的细节:
- NodePort 的端口区间由
--service-node-port-range决定(默认 30000-32767),且为了降低冲突,官方把区间分为两段:静态段 30000-30085 供用户手动指定,动态段 30086-32767 供控制面自动分配,上段耗尽才动用下段。手写nodePort要自己承担冲突责任。 - NodePort 监听哪些 IP 与 kube-proxy 模式有关:iptables/ipvs 模式默认监听节点所有 IP,
nftables模式默认只监听主 IP,可通过--nodeport-addresses或配置文件的nodePortAddresses调整。 - LoadBalancer 的实现路径:Kubernetes 通常先等价地做一个 NodePort,再由 cloud-controller-manager 配置外部负载均衡把流量转发到该 node port。自 v1.24 起可用
spec.allocateLoadBalancerNodePorts: false让直连 Pod 的 LB 实现跳过 node port 分配(注意已分配的 node port 不会自动回收,需逐个删除),也可用loadBalancerClass指定非默认的负载均衡实现(该字段设置后不可修改)。 loadBalancerIP字段已在 v1.24 弃用,语义跨实现不一致且不支持双栈,官方建议改用云厂商 annotation。- ExternalName 的陷阱:客户端用的主机名与 ExternalName 指向的名字不同,对 HTTP/HTTPS 这类依赖主机名的协议会导致 Host 头不匹配、TLS 证书不匹配的诡异错误。这一点在踩坑一节还会展开。
你现在应该掌握了什么:Service 如何用 selector + 端口映射把动态后端收敛成一个稳定入口,四种类型各自的暴露层级,以及 NodePort 端口区间、LoadBalancer 实现路径这些生产细节。
EndpointSlice 与 kube-proxy 转发机制
Service 只是"名字和 VIP",真正把流量送到后端的是数据面。先讲后端清单:官方 Service 文档与 EndpointSlice 文档说明,控制面会为带 selector 的 Service 自动创建 EndpointSlice 对象,按 IP 协议族、协议、端口和 Service 名的唯一组合分组;默认每个 EndpointSlice 最多 100 个端点(可用 kube-controller-manager 的 --max-endpoints-per-slice 调整,上限 1000)。旧的 Endpoints API 自 v1.33 起已弃用——它不支持双栈、不支持 trafficDistribution 等新特性、且单个对象放不下时会静默截断端点列表(超过 1000 后端时设置 endpoints.kubernetes.io/over-capacity: truncated 注解)。官方明确建议所有客户端改用 EndpointSlice。
再看转发机制。官方 "Virtual IPs and Service Proxies" 参考页开宗明义:每个节点都运行 kube-proxy(除非你部署了替代组件),它负责为非 ExternalName 类型的 Service 实现虚拟 IP 机制。每个 kube-proxy 实例监听 Service 与 EndpointSlice 的新增和删除,对每个 Service 调用对应模式的 API,把访问该 Service clusterIP:port 的流量捕获并重定向到某个后端端点,并由控制循环保证节点上的规则与 API server 状态可靠同步。
文档还回答了一个经典疑问:为什么不用"多 A 记录 + DNS 轮询"而要用代理?原因有三:DNS 实现不尊重 TTL 的历史包袱、应用常常只做一次解析后永久缓存、低 TTL 会给 DNS 带来难以管理的负载。换句话说,DNS 做发现、代理做转发,分工不能互换。
iptables 模式的流程(官方示例):控制面给 Service 分配 VIP(如 10.0.0.1:1234)→ 所有节点的 kube-proxy 观察到该 Service → 在节点上安装一系列 iptables 规则,从 VIP 重定向到按 Service 划分的规则链,再链到每个后端端点的规则,用 DNAT 把包改写到后端 → 客户端连 VIP 时规则生效,随机(或按会话亲和)选中后端,且不重写客户端源 IP(文档特别注明:经 NodePort 或负载均衡器进来的流量则会改变源 IP)。
三种 Linux 模式的对比(以当前版本官方文档为准):
| 模式 | 状态 | 后端机制 | 特点 |
|---|---|---|---|
| iptables | v1.37 默认(未来版本将改为 nftables) | netfilter iptables 规则 | 每 Service/端点几条规则,大规模集群规则同步慢,可用 minSyncPeriod/syncPeriod 调优 |
| ipvs | 自 v1.35 弃用,v1.40 起默认禁用,v1.43 移除 | 内核 IPVS 哈希表 | 支持 rr/wrr/lc/sh 等多种调度算法,但从未正确覆盖 Service 全部边界场景,官方建议用 nftables 替代 |
| nftables | 需内核 5.13+ | nftables API | 官方定位的继任者,规则同步与包处理都更快,NodePort 默认只监听主 IP |
这里对 Java 读者有个类比:iptables 模式像"为每个后端写一条 if 规则",规则数是 Service 数 × 端点数的量级,几万 Pod 的集群就是几万条规则,全量同步会抖;ipvs/nftables 则像"一张哈希路由表",天然为查找优化。官方给 iptables 模式的调优建议是:minSyncPeriod 默认 1s 适用于多数集群,若 sync_proxy_rules_duration_seconds 平均远大于 1 秒才考虑调大;v1.28 起 iptables 模式已改为增量更新,旧版本"调大 minSyncPeriod"的经验在新集群应回退为默认值。
你现在应该掌握了什么:后端清单位于 EndpointSlice(Endpoints API 已弃用且会截断),转发由每节点的 kube-proxy 按模式在内核编程规则完成,以及三种模式的现状与取舍。
DNS 服务发现与 Headless Service
集群内的客户端找 Service 有两种官方支持的方式:环境变量和 DNS。环境变量由 kubelet 在 Pod 启动时注入,形如 REDIS_PRIMARY_SERVICE_HOST、REDIS_PRIMARY_SERVICE_PORT 等——但文档里的注释点破了它的死穴:必须先创建 Service 再创建客户端 Pod,否则环境变量就是空的。所以实践中几乎总是用 DNS。
官方 "DNS for Services and Pods" 一页描述了记录布局:普通 Service 获得 my-svc.my-namespace.svc.cluster-domain.example 的 A/AAAA 记录,解析到集群 IP;命名 Service 端口还会生成 SRV 记录 _port-name._port-protocol.my-svc.my-namespace.svc.cluster-domain.example。Pod 的 DNS 搜索域由 kubelet 写入 /etc/resolv.conf:
nameserver 10.32.0.10
search <namespace>.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
一个 test 命名空间的 Pod 查 data 查不到(解析落在 test 命名空间),查 data.prod 或全称才能命中 prod 命名空间的 Service——跨命名空间访问必须带命名空间限定,这是新人最容易犯的发现类错误。ndots:5 的含义也值得 Java 同学留意:名字里点数少于 5 会先按搜索域补全再试,这意味着查询外部域名(如 db.example.com)会产生多次集群内解析尝试,间歇性慢半拍往往与此有关(以当前版本官方文档与各 CoreDNS 配置为准)。
然后是 headless Service。官方定义:把 .spec.clusterIP 显式设为 "None"(注意字符串 None 与字段留空是两回事),Kubernetes 不分配集群 IP,kube-proxy 不处理这类 Service,平台不做任何负载均衡与代理,而是由集群 DNS 直接返回后端 Pod IP 集合。有 selector 时,endpoints 控制器创建 EndpointSlice,DNS 返回指向 Pod 的 A/AAAA 记录,客户端拿到集合后自行挑选(或轮询)。无 selector 时则需配合手写 EndpointSlice。典型场景是有状态服务:客户端需要自己决定连哪个副本(比如自己实现分片路由或长连接亲和),此时平台层负载均衡反而是干扰。
对 Java 读者,headless Service 直接对应客户端侧的"直连多实例"编程模型:
// 解析 headless Service 返回全部 Pod IP,由客户端自己选后端
InetAddress[] addresses = InetAddress.getAllByName(
"cassandra-headless.data.svc.cluster.local");
// 例如按一致性哈希或本地性挑一个地址建立长连接
for (InetAddress addr : addresses) {
System.out.println("candidate backend: " + addr.getHostAddress());
}
配合 Pod 的 hostname/subdomain 字段还可以得到稳定的 Pod FQDN(如 busybox-1.busybox-subdomain.my-namespace.svc.cluster-domain.example),StatefulSet 的"稳定网络标识"正是建立在这套机制上。另外 setHostnameAsFQDN: true 可以让容器内 hostname 直接返回 FQDN,但要小心 Linux 内核 nodename 64 字符上限,FQDN 超长会导致 Pod 卡在 Pending。
Pod 的 DNS 策略有四个值:Default(继承节点配置)、ClusterFirst(未命中集群域后缀的查询转发上游,未指定时的默认值)、ClusterFirstWithHostNet(hostNetwork Pod 必须显式设置,否则退化为 Default)、None(完全由 dnsConfig 自定义,nameservers 最多 3 个、search 域最多 32 个)。
你现在应该掌握了什么:DNS 是集群内服务发现的事实标准(环境变量有顺序陷阱);headless Service 把"选哪个后端"的责任交还给客户端,这正是有状态服务需要的语义。
Ingress、Ingress Controller 与 Gateway API 的演进
Service 解决的是"集群内稳定入口",Ingress 解决的是"集群外 HTTP 入口"。官方 Ingress 文档开篇给出结论性事实:Ingress API 自 v1.19 起 Stable,但已被冻结(frozen)——不会移除、受 GA API 稳定性保证约束,但不再开发新特性;Kubernetes 官方建议新项目使用 Gateway。这是本文写作时(以当前版本官方文档为准)最重要的演进信号,选型的同学请直接把这个结论抄进技术评审记录。
Ingress 本身只是规则声明:"把匹配 host/path 的外部 HTTP(S) 请求路由到哪个 Service 的哪个端口"。官方反复强调一个前提:光创建 Ingress 资源没有任何效果,集群里必须有 Ingress Controller 在跑,由它去兑现(fulfill)Ingress——通常是配出一个负载均衡或反向代理。Controller 种类繁多(NGINX、HAProxy、Traefik、Envoy 系如 Contour/Emissary/Higress、云厂商的 AWS/GCE/AKS/OCI 控制器等),行为细节各异,"理想情况下所有 controller 都符合参考规范,现实中略有不同",文档建议选型前务必读对应 controller 的文档。
一个最小 Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
spec:
ingressClassName: nginx-example
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 80
带读这节时抓住几个关键词:每个 path 必须显式声明 pathType(Exact 精确匹配、Prefix 按 / 分隔的元素级前缀匹配——注意 /aaa/bbb 不匹配 /aaa/bbbxyz,官方给了一整张匹配示例表、ImplementationSpecific 交给实现);多条规则同时命中时最长路径优先,等长则 Exact 优先;host 支持 *.foo.com 形式的单段通配。TLS 一节说明 Ingress 只支持 443 单端口、TLS 在入口点终结(到 Service/Pod 的流量是明文),Secret 必须含 tls.crt/tls.key,且 tls.hosts 必须与 rules.host 显式匹配——因为默认规则上 TLS 无法工作(证书没法签给"所有可能的子域")。
Ingress 如何找到 controller?答案是 IngressClass 资源 + ingressClassName 字段(v1.18 之前的老式做法是 kubernetes.io/ingress.class annotation,从未被正式定义)。可以给某个 IngressClass 打上 ingressclass.kubernetes.io/is-default-class: "true" 作为默认;但要注意文档里的警告:集群里若有多个默认 IngressClass,准入控制器会直接拒绝创建未指定 ingressClassName 的新 Ingress。
再看继任者 Gateway API。官方把它定义为"一组提供动态基础设施供给与高级流量路由的 API kinds",四个设计原则:面向角色(基础设施提供方/集群运维/应用开发者分层协作)、可移植(CRD 定义、实现众多)、表达力强(header 匹配、流量权重等过去只能靠 Ingress annotation 实现的能力成为一等公民)、可扩展(可在 API 各层挂接自定义资源)。资源模型是四个稳定 kind:GatewayClass(由哪个控制器管理)、Gateway(一段流量处理基础设施实例,定义监听端口与协议)、HTTPRoute / GRPCRoute(把流量按规则路由到 Service 后端)。一个典型 HTTPRoute:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: example-httproute
spec:
parentRefs:
- name: example-gateway
hostnames:
- "www.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /login
backendRefs:
- name: example-svc
port: 8080
官方请求流描述也适合带读:客户端先解析 Gateway 地址,反向代理按 Host 头匹配 Gateway+HTTPRoute 派生的配置,可按 match 规则做 header/path 匹配,可按 filter 规则改写请求,最后转发到后端。默认 Gateway 只接受同命名空间的 Route,跨命名空间需要配置 allowedRoutes——这是它和 Ingress"一个集群一个入口控制器说了算"的心智差异:Gateway 把"入口归谁管、路由归谁管"拆开,天然适合平台团队与业务团队分工。Ingress 迁移到 Gateway 需要一次性转换(Gateway API 不包含 Ingress kind),官方有专门的 ingress migration 指南。
你现在应该掌握了什么:Ingress 已冻结、Gateway 是官方推荐的演进方向;Ingress 资源只是声明,生效依赖 Ingress Controller 与 IngressClass 的绑定;Gateway API 用 GatewayClass/Gateway/HTTPRoute 三层资源把入口设施与路由规则解耦。
NetworkPolicy 网络隔离
前面都在讲"怎么通",这一节讲"怎么不通"。官方 NetworkPolicy 文档定位非常精确:它工作在 OSI 三层/四层(IP 地址与端口,TCP/UDP,可选 SCTP),是"应用为中心"的构造,描述一个 Pod 被允许与哪些网络"实体"通信。判定通信对象有三类标识:其他被允许的 Pod(例外:Pod 不能阻断对自己的访问)、被允许的命名空间、IP 段(例外:Pod 所在节点上的流量总是放行)。
最关键的心智模型是隔离(isolation)与默认放行:默认情况下 Pod 对 ingress 和 egress 都是非隔离的,即全放行。一旦出现任何一条"选中该 Pod 且 policyTypes 包含 Ingress"的策略,该 Pod 对入口流量即被隔离,只有这些策略的 ingress 白名单允许(外加来自本节点)的连接能进来;egress 同理。策略之间不冲突、按方向做并集,顺序无关;而一次连接要被放行,源端 Pod 的 egress 策略与目的端 Pod 的 ingress 策略都必须允许——这是双向白名单模型。
先给生产上最常用的底座:默认拒绝 + 放行 DNS。单独一条 default-deny-egress 会连 DNS 一起掐断(官方文档专门给了 Caution),所以必须显式放行到 DNS 服务的流量:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
spec:
podSelector: {}
policyTypes:
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
带读示例策略时要留意几个官方强调的语法细节:from/to 里一个条目同时写 namespaceSelector 和 podSelector 表示"特定命名空间里的特定 Pod",而拆成两个数组元素则是"或"关系——YAML 层级差一点,语义完全不同,拿不准就用 kubectl describe 看 API server 如何解释。ipBlock 用于集群外 IP(Pod IP 是短暂的,不适合写死),支持 except 排除子段。端口侧支持 endPort 指定范围(v1.25 起 Stable,endPort 必须不小于 port 且两者都必须为数字,且依赖 CNI 支持,否则只作用于单端口)。命名空间不能用名字直接选,要用 selector;控制面会给所有命名空间打不可变标签 kubernetes.io/metadata.name,值即命名空间名,可借此按名定位。
同样重要的是"做不到什么"清单(官方明确列出,截至 Kubernetes 1.37):强制内部流量经过公共网关、任何 TLS 相关能力、按 Kubernetes 身份选节点、按名字选 Service、默认应用到所有命名空间/ Pod 的全局策略、网络策略日志、显式拒绝规则(模型是默认拒绝 + 仅允许)、阻断 loopback 或来自所在节点的流量。需要这些能力就得靠 service mesh、L7 代理或准入控制器。另外文档对生命周期与现有连接的说明也很务实:新策略从创建到插件生效有时间差,Pod 启动瞬间可能完全无网,官方建议用 init container 等待依赖可达;策略变更对已建立的连接是否生效由实现定义,建议不要在中途改会打断现有连接的策略。
你现在应该掌握了什么:NetworkPolicy 是"选中即隔离、白名单并集"的三/四层模型,默认拒绝必须连带放行 DNS;以及它明确不覆盖的那一长串能力清单。
抓包排障思路
官方文档没有专门的"排障"章节,但把前面各页的机制串起来,就得到一条完整的排障决策链。面对"服务不通",按层排查:
# 1. 对象层:Service 是否存在、selector 是否写对、Endpoints 是否就绪
kubectl get svc my-service
kubectl get endpointslices -l kubernetes.io/service-name=my-service
# 2. 后端层:Pod 是否 Ready、标签是否与 selector 精确一致
kubectl get pods -l app.kubernetes.io/name=MyApp --show-labels
# 3. DNS 层:在客户端 Pod 内验证解析
kubectl exec -it client -- nslookup my-service.my-ns.svc.cluster.local
# 4. 连通层:用临时容器或直接 curl 集群 IP 与后端 IP 对照
kubectl run tmp --rm -it --image=busybox:1.28 -- wget -qO- http://my-service.my-ns
# 5. 数据面层:节点上确认 kube-proxy 规则存在(以集群实际模式为准)
iptables -t nat -L KUBE-SERVICES | grep my-service
逐层解读:第 1 步若 EndpointSlice 为空,问题在 selector/标签/Pod 就绪态,与网络无关;第 2 步要警惕标签"看起来一样但差一个字符";第 3 步解析失败看 CoreDNS 日志与 Pod 的 dnsPolicy;第 4 步能直达 Pod IP 但访问 VIP 不通,指向 kube-proxy/规则未同步;第 5 步在 node 上用 tcpdump 定界——在客户端 Pod 所在节点抓 VIP 流量是否被 DNAT、在后端节点抓包是否到达,即可判定问题在"去程转发"还是"回程路由"(CNI 跨节点隧道/路由问题的典型症状是单边通)。对 Java 应用,再加一层 JVM 视角:DNS 缓存(networkaddress.cache.ttl)可能让客户端拿到已下线的旧地址,headless Service 场景下建议显式调低缓存 TTL 并实现失败重连。
你现在应该掌握了什么:排障不是乱试,而是沿"对象 → 后端 → DNS → VIP → 内核规则 → 抓包"的决策链逐层收窄,每一层对应一类根因。
踩坑经验
❌ 错误做法:用环境变量做服务发现,且先部署客户端 Pod 再创建 Service。
✅ 正确做法:需要 Service 信息的 Pod 必须在 Service 创建之后才启动,否则注入的环境变量为空;更稳妥的做法是统一走集群 DNS,彻底规避顺序依赖(官方文档明确:只用 DNS 发现则无此问题)。
❌ 错误做法:手写 nodePort: 30500 却不去了解端口区间划分,与自动分配的端口撞车。
✅ 正确做法:记住默认区间 30000-32767 被官方划分为静态段 30000-30085(手动指定优先用这段)与动态段 30086-32767(自动分配),冲突时控制面会直接报错,自建 LB 暴露端口时优先占静态段。
❌ 错误做法:用 ExternalName 把集群内 HTTP 客户端"代理"到外部域名,然后被 Host 头与 TLS 证书问题折磨。
✅ 正确做法:官方 Caution 已写明——ExternalName 的跳转发生在 DNS 层而非代理层,客户端实际连接的主机名与目标不一致,HTTP 的 Host 头、TLS 的证书匹配都会出错;需要真正代理时改用带 selector 的 Service + 手写 EndpointSlice。
❌ 错误做法:给 namespace 加一条 default-deny-egress 后就宣布"完成零信任",结果所有服务 DNS 解析失败、监控全黑。
✅ 正确做法:官方文档明确警告 default deny egress 会同时阻断 DNS,必须额外放行到集群 DNS 服务的 53 端口流量(示例见 NetworkPolicy 一节),并且评估监控、镜像拉取等系统通道。
❌ 错误做法:以为创建了 Ingress 资源,外部就能访问了;或者集群里装了多个默认 IngressClass。
✅ 正确做法:先确认 Ingress Controller 在运行且 IngressClass 绑定正确;默认 IngressClass 全集群最多一个——有多个时准入控制器会拒绝所有未指定 ingressClassName 的新 Ingress。
❌ 错误做法:沿用旧资料把 kube-proxy 调成 ipvs 模式并号称"性能更好"。
✅ 正确做法:以当前版本官方文档为准——ipvs 模式自 v1.35 弃用、v1.40 起默认禁用、v1.43 移除,官方推荐迁移到 nftables 模式(内核需 5.13+)或继续用增量更新后的 iptables 模式。
最佳实践清单
- 集群上线前先规划三段互不重叠的地址空间(Pod CIDR、Service CIDR
--service-cluster-ip-range、Node 网段),并确认 CNI 插件明确支持 NetworkPolicy 与双栈(如需要)。 - 集群内服务间调用一律走 DNS + ClusterIP Service;不要在应用配置里写 Pod IP;需要客户端侧路由(有状态、长连接、分片)时用 headless Service 自己解析 EndpointSlice。
- NodePort 手动端口占用静态段(默认 30000-30085);对外服务优先 LoadBalancer(合理设置
allocateLoadBalancerNodePorts与loadBalancerClass)或 Gateway,避免暴露 externalIPs(该字段自 v1.36 已弃用)。 - 明确固定 kube-proxy 的 proxy-mode 配置,避免升级时默认模式变化(v1.37 默认为 iptables,未来版本将改为 nftables);关注
sync_proxy_rules_duration_seconds指标再决定是否调minSyncPeriod。 - Ingress 新项目改为评估 Gateway API;存量 Ingress 注意 TLS hosts 与 rules host 必须显式对齐,并为集群指定唯一默认 IngressClass。
- 按"默认拒绝 + 白名单"落地 NetworkPolicy,且每条 deny 策略同步放行 DNS;修改策略时避免打断既有连接。
- 排障遵循分层决策链,把
kubectl get endpointslices、nslookup、节点抓包固化为标准动作,而不是直接重启 Pod 碰运气。
总结
- Kubernetes 网络是"模型由官方定义、实现由插件补齐"的契约体系:CNI 管 Pod 网络与 NetworkPolicy,kube-proxy 管 Service VIP 转发,CoreDNS 管名字,Ingress/Gateway 管入口。
- Service 用稳定 VIP 解耦了动态后端;四种类型层层递进(ClusterIP → NodePort → LoadBalancer,ExternalName 是 DNS 特例),NodePort 端口区间与 LoadBalancer 的实现路径是生产关键细节。
- 后端清单已切换到 EndpointSlice(Endpoints API 自 v1.33 弃用且会截断),转发由每节点的 kube-proxy 以 iptables(默认)/ nftables(未来默认)/ ipvs(已弃用)在内核完成。
- DNS 是集群内发现的事实标准;headless Service(
clusterIP: None)不做平台级负载均衡,把选端逻辑交还给客户端,适合有状态与长连接场景。 - Ingress API 已冻结、官方推荐 Gateway API;Ingress 资源只是声明,必须配合 Ingress Controller 与 IngressClass,且默认 IngressClass 全集群唯一。
- NetworkPolicy 是"选中即隔离、双向白名单"的三/四层模型,默认拒绝必须连带放行 DNS;它明确不做 L7、TLS、日志与显式拒绝,这些交给 service mesh 等上层方案。
参考资料
- Kubernetes 官方文档 - Services, Load Balancing, and Networking(总览):https://kubernetes.io/docs/concepts/services-networking/
- Service:https://kubernetes.io/docs/concepts/services-networking/service/
- Virtual IPs and Service Proxies(kube-proxy 模式):https://kubernetes.io/docs/reference/networking/virtual-ips/
- EndpointSlices:https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/
- DNS for Services and Pods:https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
- Ingress:https://kubernetes.io/docs/concepts/services-networking/ingress/
- Ingress Controllers:https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/
- Gateway API:https://kubernetes.io/docs/concepts/services-networking/gateway/
- Network Policies:https://kubernetes.io/docs/concepts/services-networking/network-policies/
- Cluster Networking:https://kubernetes.io/docs/concepts/cluster-administration/networking/