1. 先搞明白:Kubernetes里的服务暴露到底解决什么问题
1.1 为什么 Pod 的 IP 不能直接拿来用
我见过不少刚接触 Kubernetes 的同学,第一个直觉就是“容器有 IP,我直接访问这个 IP 不就行了”。实际上在 k8s 集群里,直接拿 Pod IP 当访问入口基本等于自己给自己挖坑。
原因特别直接:Pod 是“一次性”的。某个业务服务跑在三个副本上,你记下了其中一个 Pod 的 IP,结果这台节点出问题,或镜像重新调度,Pod 被删除重建后 IP 就变了。你没法把一个写死在配置文件里的 IP 指向新 Pod。更麻烦的是,Deployment 滚动更新场景下,旧 Pod 正在被销毁、新 Pod 还没 Ready,这中间谁会去处理“把请求从旧 IP 切到新 IP”这件事?答案是没有,除非你做服务暴露。
另一个现实问题是,一个服务往往不止一两个副本。客户端如果只能拿到一个后端 IP,就做不到负载均衡,某个副本压力大了也没办法自动分摊。所以在 Kubernetes 里,我们几乎永远不直接跟 Pod IP 打交道,而是通过 Service 这类抽象层,把一个稳定的访问入口固定下来,让它自己去维护背后一组 Pod 的映射关系。
那为什么这个标题里要同时提“服务暴露”和“CoreDNS 域名访问”呢?因为 Service 提供的稳定入口,本质上是一个虚拟 IP(ClusterIP)或某个节点端口。但真实业务里,谁愿意去记一串数字?我们更喜欢用域名,比如order-service、api-gateway。把这些域名解析到 Service 入口的,正是 CoreDNS。
从这个角度看,服务暴露和域名访问是绑定的。你先把流量接到了 Service 上,再让 CoreDNS 把域名解析到 Service,整个链路才算完整。
1.2 三种暴露方式的适用场景对比
Kubernetes 里暴露服务最常见的四种方式,很多文章只会列概念。我按实际使用频率给你梳理一遍,并强调各自适合什么场景。
首先是 ClusterIP。这是默认类型,Service 会拿到一个集群内部的虚拟 IP,只有集群内的 Pod、节点上的进程能访问它。ClusterIP 不是给外部用户用的,而是给集群内部服务做互相调用用的。比如你的前端 Pod 要访问后端 API,这个后端 API 的 Service 就设为 ClusterIP,前端直接通过域名backend.default.svc.cluster.local来访问。这里有个容易混淆的点:ClusterIP 虽然叫虚拟 IP,但它在集群内是真实可路由的,kube-proxy 会负责把访问 ClusterIP 的数据包转发到某个后端 Pod。
然后是 NodePort。它在每个节点上开放一个高位端口(默认范围 30000-32767),外部请求打到任意节点的这个端口,kube-proxy 会转发到对应 Service。NodePort 的尴尬之处在于:你得知道节点 IP,还得处理端口冲突。一个集群几十上百个服务,如果每个都用 NodePort,端口管理就是噩梦。所以它适合临时调试、小规模测试,或者作为某些场景下的兜底方案,不适合作为正式的对外接入方案。
接下来是 LoadBalancer。这是云环境下的标准做法。Service 定义为 LoadBalancer,云厂商的控制器会自动创建一个负载均衡器实例,分配一个公网或内网 IP,然后把流量转发到集群节点端口。这里要注意,LoadBalancer 本质上也是构建在 NodePort 之上的,云厂商的实现通常是把节点端口挂到负载均衡器后端,所以三者的关系是层层递进,而不是互相独立。它的好处是“一句话就能用”,坏处是依赖云厂商环境,而且每个 Service 一个负载均衡器,成本不低。
最灵活的是 Ingress。它不是一种新的 Service 类型,而是一个 API 对象,相当于一个七层反向代理网关,根据域名和路径把请求路由到不同的 Service。Ingress 有一个明显的优势:多个服务共享一个入口 IP,不同域名走不通的路由规则,证书也能在入口层统一管理。你可以把它类比成 Nginx 里的 server 配置,只是这份配置是声明式的,由 Ingress Controller 去执行。
那怎么选?一句话总结:集群内部互相调用优先用 ClusterIP,测试或应急用 NodePort,云上对外服务优先 LoadBalancer,要按域名和路径分流就上 Ingress。
1.3 从 Service 到端点的流量转发过程
搞清楚 Service 的流量转发,是为了后面调试排查时不至于一头雾水。
Service 创建后,控制面里的 kube-controller-manager 或对应的 Endpoints Controller 会持续观察这个 Service 匹配到的 Pod。如果 Pod 正常 Ready,它的 IP 和端口就会被写进 Endpoints。如果 Pod 异常或被删除,它的 IP 会被移出 Endpoints。这一层是 Service 可用性的核心。
数据包到达 Service 后,谁来做转发?NodePort 和 ClusterIP 模式下,节点上的 kube-proxy 负责。kube-proxy 有三种实现模式:userspace 模式基本淘汰;iptables 模式是最常见的默认模式,通过创建大量 iptables 规则把 ClusterIP 的访问转到某个随机选中的 Pod IP;IPVS 模式则在内核里维护一组虚拟服务器,负载均衡能力更强,规则更清晰,适合规模较大的集群。
这些规则不是凭空生成的。kube-proxy 会 watch Service 和 Endpoints 的变化,任何一次变动,它都会把规则同步到底层。但这也带来了一个感性的问题:在 iptables 模式下,如果规则量巨大,数据包匹配链路会很长,可能在转发延迟上有一定影响。这也是为什么很多人建议称为“大规模集群 + 严格控制网络延迟”的场景下,优先用 IPVS 模式。
理解了转发链路,再学域名解析就顺了:CoreDNS 会把 Service 的域名解析到 ClusterIP,数据包实际上访问的是这个 ClusterIP。你如果直接 dig 域名拿到的是 ClusterIP,再往下走到 kube-proxy 这条链路,才到具体的 Pod。
2. CoreDNS 集群内域名解析:核心原理拆解
2.1 Kubernetes 和 CoreDNS 是怎么配合工作的
CoreDNS 是 Kubernetes 1.13 之后官方推荐的集群内 DNS 实现,替代了早期的 kube-dns。它本质上是一个用 Go 写的、插件化的 DNS 服务器,但它很聪明地接入了 Kubernetes 的 API watch 机制。
CoreDNS 在集群里通常以 Deployment 方式运行,一般两个副本,通过 Servicekube-dns暴露给其他 Pod。注意这个细节:虽然 CoreDNS 是实际工作的组件,但暴露出的 Service 名仍然叫kube-dns,这样做是为了向下兼容,因为很多早期配置文件里写死了kube-dns这个 DNS Service 地址。
每个 Pod 启动后,kubelet 会给它注入一个/etc/resolv.conf,里面指向 CoreDNS 的 ClusterIP。所以 Pod 内任何 DNS 查询请求,本质上都会发到 CoreDNS,再由 CoreDNS 决定怎么应答。
CoreDNS 通过内置的kubernetes插件来监听 Service、Pod、Namespace 等资源的变化。当你在集群里创建一个名为pay-service的 Service,CoreDNS 会立刻知道这个对象存在,并且能响应pay-service.default.svc.cluster.local的解析。注意,CoreDNS 不是把域名规则配置成静态文件的,而是动态从 k8s API 拉取,所以新服务上线后不用去改 DNS 配置,这一点非常关键。
2.2 服务名的组件规则
Kubernetes 里的完整域名格式是:
<service-name>.<namespace>.svc.<cluster-domain>比如一个服务叫user-service,部署在prod命名空间,集群域名默认是cluster.local,那么完整域名就是:
user-service.prod.svc.cluster.local在这个格式里,svc是固定字段,表示这是 Service 类型记录;cluster.local是集群默认域名,可以改,但一般没人动。如果你在一个 Pod 内访问user-service,DNS 会走一次搜索域补齐:kubelet 生成的resolv.conf里通常有类似search default.svc.cluster.local svc.cluster.local cluster.local的配置,这样当你写短域名user-service时,CoreDNS 会依次尝试补齐为user-service.default.svc.cluster.local等。
但跨命名空间时就要写全名。访问prod下的user-service,你不能只写user-service,否则解析到的是当前命名空间的同名服务,如果没有就会失败。必须用user-service.prod.svc.cluster.local或者简写成user-service.prod.svc,后者也能被 CoreDNS 正确响应。
很多人在“为什么我在 A 命名空间访问不到 B 命名空间的同名服务”上花费了大量时间,其实根因就是短名解析只在当前命名空间生效。我建议所有跨命名空间访问,一律使用全名或至少带上命名空间,不要在yaml里写裸服务名。
2.3 有头服务与无头服务的解析差异
前面说的都是普通 Service,它有 ClusterIP。CoreDNS 在解析这类域名时返回的是虚拟 IP。但还有一种是 headless Service,你在spec里显式把clusterIP设为None,它不分配虚拟 IP,也不做负载均衡。
headless Service 的解析结果会是一组 Pod IP 列表。客户端拿到这组 IP 后,自己决定连接哪一个。最常见的用途是:
- StatefulSet 的场景,每个 Pod 都有独立身份,需要直连到具体某个 Pod;
- 需要应用层自定义负载均衡,比如数据库客户端、消息队列客户端自己处理主从选择和重连;
- 服务发现需要拿到真实端点而不是代理 IP。
举个例子,如果你部署了一个三节点的数据库 StatefulSet,对应的 headless Service 叫db,命名空间data,那么解析db.data.svc.cluster.local时,CoreDNS 会返回三个 A 记录,分别对应三个 Pod 的 IP。你甚至可以进一步解析pod0.db.data.svc.cluster.local或 Pod 名字到具体的那个 Pod。
这里补一个很多文章没讲清楚的细节:普通 Service 动态解析到 ClusterIP,静态且负载由 kube-proxy 负责;headless Service 解析到多个 IP,负载由调用方自己实现。两者在 DNS 层面都能工作,但语义完全不同,错误使用会导致应用层行为异常。
3. 实战操作:从部署 CoreDNS 到跨命名空间域名访问
3.1 先验证 CoreDNS 是否正常干活
在接触任何域名解析问题之前,第一步永远是确认 CoreDNS 本身是否健康。你可以执行下面几条命令快速检查:
kubectl get pods -n kube-system -l k8s-app=kube-dns kubectl logs -n kube-system deployment/coredns --tail=20 kubectl get svc -n kube-system kube-dns正常情况下,你会看到两个 CoreDNS Pod 处于 Running 状态,日志里没有连续报错;kube-dnsService 有一个 ClusterIP。如果 Pod 一直 CrashLoopBackOff,多半是 CoreDNS 配置或网络插件的问题。
CoreDNS 的配置通过 ConfigMapcoredns管理,保存在kube-system命名空间。默认配置大概长这样:
apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .:53 { errors health { lameduck 5s } ready kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa ttl 30 } prometheus :9153 forward . /etc/resolv.conf cache 30 }我解释一下这里面几个关键插件:
errors:记录错误日志,排错时必须打开;health:暴露健康检查端点,供 kubelet 探活;kubernetes:核心插件,负责把集群内 Service 和 Pod 域名翻译成解析记录;forward:把集群外的域名(比如baidu.com)转发到上游 DNS,也就是节点/etc/resolv.conf里配置的 DNS;cache:缓存,默认 TTL 30 秒。
如果你要验证 CoreDNS 的解析是否正常,最直接的方法是在任意 Pod 里执行:
kubectl run temp --image=busybox:1.36 --rm -it --restart=Never -- nslookup kubernetes.default.svc.cluster.local如果返回了10.96.0.1之类的 ClusterIP,说明 CoreDNS 工作正常。这里要注意,busybox的nslookup输出格式比较旧,但足够判断解析是否成功。
另外,有的网络环境会默认在 Pod 里屏蔽到 CoreDNS 的流量,或者有 NetworkPolicy 不允许跨命名空间访问,这些因素会让 CoreDNS 解析超时。检查健康端点也是一个办法:
curl http://<coredns-pod-ip>:8181/health返回OK说明进程健康。
3.2 一个完整的跨命名空间访问示例
理论说再多,不如亲手搭一遍。我通常在测试环境干这样一套流程,几百字就能验证核心链路。
第一步,创建两个命名空间:
kubectl create namespace dev kubectl create namespace sit第二步,在dev下部署一个简单的 HTTP 服务。我们用一个测试镜像nginx,副本数 2:
apiVersion: apps/v1 kind: Deployment metadata: name: hello-service namespace: dev spec: replicas: 2 selector: matchLabels: app: hello template: metadata: labels: app: hello spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80第三步,创建 Service:
apiVersion: v1 kind: Service metadata: name: hello-service namespace: dev spec: selector: app: hello ports: - port: 80 targetPort: 80 protocol: TCP type: ClusterIP第四步,在sit命名空间放一个测试 Pod:
kubectl run test-pod --image=busybox:1.36 -n sit --rm -it --restart=Never -- sh在那个 Pod 里,依次执行:
nslookup hello-service.dev.svc.cluster.local wget -O- http://hello-service.dev.svc.cluster.local如果跨命名空间访问正常,你会看到hello-service的 ClusterIP,同时能拉取到 Nginx 默认页面。如果在这里解析失败,那问题几乎集中在网络策略、CoreDNS 集群内解析规则、命名空间级别隔离这几种可能上。
同命名空间访问更简单。如果你在dev里也放一个 Pod,直接访问http://hello-service就能通,因为搜索域自动补齐了命名空间。跨命名空间访问则必须写完整名字,这是实践中最容易踩的坑。
3.3 给 Service 配置自定义域名解析
默认的域名规则满足 90% 场景,但有些人希望内部域名更短,或者希望某些固定域名指向特定 Service,比如内部文档站点就叫docs.internal。这个需求可以通过 CoreDNS 的rewrite插件或hosts插件实现。
先说简单的hosts方式。你可以在 CoreDNS 的 Corefile 中增加一段:
.:53 { hosts { 10.96.12.34 docs.internal fallthrough } kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa } forward . /etc/resolv.conf }10.96.12.34是你某个 Service 的 ClusterIP。这样集群内任何 Pod 解析docs.internal都能拿到这个地址。但这样做有一个不好的地方:如果 Service 被重建,ClusterIP 换了,这个配置就得手动改。所以我会优先用rewrite来做规则转换。
比如,我想把api.internal这个域名映射成api-gateway.default.svc.cluster.local,配置这样写:
.:53 { rewrite name api.internal api-gateway.default.svc.cluster.local kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure } forward . /etc/resolv.conf }这样api.internal在 CoreDNS 里其实是被改写成标准域名,然后走kubernetes插件完成解析。好处是它跟着 Service 的 ClusterIP 动态变化,不会漂移。改完 ConfigMap 后,CoreDNS 的配置大概需要几十秒生效;如果想要立即生效,也可以重启 CoreDNS:
kubectl -n kube-system rollout restart deployment/coredns这类自定义域名规则适合内部工具、统一网关入口等场景,但它只影响集群内部解析,外部用户是感知不到的。
4. 域名访问排查链路:从解析失败到业务恢复的完整过程
4.1 各种解析测试工具该怎么用
排查 DNS 问题第一步,是选对工具。nslookup、dig、getent、ping这四个命令很多人混用,但它们的观察角度完全不一样。
nslookup适合快速看某个域名是否能解析,能返回 A 记录和 TTL,但在复杂问题诊断时信息不够全。dig是更详尽的查询工具,能显示查询耗时、查询的 DNS 服务器、返回的权威记录、完整应答报文。凡是深入排查,我都建议用dig。
getent hosts则和普通应用层库函数走同一套getaddrinfo调用链,它返回的结果会受/etc/nsswitch.conf影响,如果你的环境里还有/etc/hosts配置,这个工具测出来的结果可能和dig不一致,但这恰恰就是实际应用看到的结果。
ping呢?它能看域名能不能解析成 IP,也能看目标 IP 通不通,但它依赖 ICMP,很多 Pod 或防火墙会屏蔽 ICMP,所以“ping 不通”不等于服务不可用,“ping 通”也不能证明业务端口正常。
我把常用测试命令整理成这样一个对照表方便你回顾:
| 命令 | 观察重点 | 适合场景 | 返回内容和区别 |
|---|---|---|---|
| nslookup | DNS 解析是否成功 | 快速检查 | 返回服务器地址、域名对应 IP |
| dig | DNS 查询全链路 | 深层次排查 | 显示查询时间、应答服务器、详细记录、TTL |
| getent hosts | 应用实际解析结果 | 模拟应用行为 | 受/etc/hosts和/etc/nsswitch.conf影响 |
| ping | 解析 + ICMP 连通性 | 测试网络通断 | 受 ICMP 策略影响,不能单独作为判断依据 |
| curl/wget | 业务连通性 | 最终业务验证 | 验证 HTTP/TCP 端口及服务响应是否符合预期 |
还有一点,在 Pod 里测试时要注意镜像是否内置这些工具。busybox自带nslookup和wget,但没有dig。要用dig的话,可以用netshoot镜像,它集成了几乎所有网络排错工具,或者直接跑一个装了dnsutils的容器:
kubectl run dns-test --image=ubuntu:22.04 --rm -it --restart=Never -- bash apt update && apt install -y dnsutils iputils-ping curl4.2 典型故障链路推演
我拿一个很常见的案例来说:帮忙排查某系统里 Pod A 无法访问 Pod B 的域名backend.default.svc.cluster.local,现象是超时或 502。
第一步,先进 Pod A 里确认问题面:
kubectl exec -it <pod-a> -- curl -v http://backend.default.svc.cluster.local如果报Could not resolve host,问题基本在 DNS 解析环节。开始用nslookup试:
nslookup backend.default.svc.cluster.local返回server can't find ...或超时,说明 CoreDNS 没有正常应答。这时要检查 CoreDNS Pod 状态:
kubectl get pods -n kube-system -l k8s-app=kube-dns如果 CoreDNS 状态正常,接着看 CoreDNS 日志:
kubectl logs -n kube-system deployment/coredns --tail=50日志里如果有i/o timeout或connection refused,多半是 CoreDNS 到 Kubernetes API 的连接有问题,或者网络插件工作异常。这时可以检查 CoreDNS 的 DNS 查询性能:直接在 coredns Pod 上执行dig @127.0.0.1 backend.default.svc.cluster.local,注意这里的@127.0.0.1是指 CoreDNS 自己监听的地址。
如果解析没问题,换curl再试。如果解析成功但连接失败或超时,问题就从 DNS 转到 Service 转发链路。检查 Service 对应的 Endpoints 是否非空:
kubectl get endpoints backend -n default kubectl describe svc backend -n default如果 Endpoints 全是空的,说明后端 Pod 的标签没有匹配上 Service 的 selector,或者 Pod 未 Ready。如果 Endpoints 正常,但访问依然失败,则要看 kube-proxy 的规则是否同步。
在 iptables 模式下,可以用:
iptables -t nat -L -n | grep <cluster-ip>确认有没有对应的规则;在 IPVS 模式下用:
ipvsadm -ln | grep <cluster-ip>如果规则缺失,重启 kube-proxy 或者触发一次 Service 更新通常能临时恢复,但根因要深入查 kube-proxy 与 API Server 的连接状态。
这一步也展示了排查的核心逻辑:先分层。不要一上来就怀疑网络,先分“DNS 层”、“Service 转发层”、“后端 Pod 健康层”三块,一层层排除。
4.3 缓存、TTL 和搜索域对排查的影响
DNS 查询结果不是实时的,很多人在排错时被缓存坑过。CoreDNS 默认对解析结果做缓存,TTL 可以在 Corefile 里配置。而 kubelet 和操作系统层面也可能有自己的缓存。一些云环境里的容器网络方案甚至会做额外的 DNS 加速或缓存。
如果你刚重建了一个 Service,还在旧的 ClusterIP 上做测试,得到的可能是一个长时间缓存的解析结果。这时应该等待 TTL 过期,或直接使用dig查权威 DNS 服务器来绕过缓存:
dig @<coredns-clusterip> backend.default.svc.cluster.local +noall +answer如果直接向 CoreDNS 查询得到的就是旧 IP,而 API 里 Service 已经更新,可能要检查 CoreDNS 与 API Server 的同步是否正常,或者 CoreDNS 的内部缓存有没有异常。
还有一类问题非常隐蔽:Pod 的/etc/resolv.conf中search域配置异常。kubelet 会根据 Pod 所在命名空间生成search域列表。正常情况下,一个default命名空间的 Pod 会这样:
nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5ndots:5是一个很容易忽略的点。当查询的域名中“点”的数量小于 5 时,系统会先尝试用search域拼接出来的域名去查询,然后再查原域名。这意味着查询my-service时,实际可能会先发起多次 DNS 查询。如果业务代码里配置的内网域名带了不一定合法的点,解析就会因为 ndots 规则产生额外延迟。
某个业务的内部域名如果是backend.v1,点数为 1,小于 5,系统会先拼上search域去问 CoreDNS,结果显示不存在,最后才查原域名。这个额外负担在大规模集群里会被放大。与之相比,直接用全名backend.v1.default.svc.cluster.local能减少这一步开销,也是我推荐在 Pod 间通信时多直接用标准全域名的一个原因。
5. 过程中的一些体会和常见误区
5.1 Pod 内的 DNS 策略影响比你想的大
Kubernetes 的 Pod DNS 策略有ClusterFirst、Default、None、ClusterFirstWithHostNet四种。大多数人用的默认值是ClusterFirst,这意味着 Pod 里的/etc/resolv.conf指向 CoreDNS。
但如果你把 spec 里的hostNetwork设为true,直接使用宿主机网络,那 DNS 策略不改成ClusterFirstWithHostNet的话,Pod 会继承宿主机的/etc/resolv.conf,根本不会走集群 CoreDNS。这个问题在调试一些网络插件、监控组件时特别常见,症状就是 Pod 里能访问外网,但访问不到其他 Service 的短域名。
还有一种场景:在kubectl run临时测试时,如果镜像默认禁用了 DNS 工具,你容易被“解析超时”这类假象误导。我建议准备一个固定的排错镜像,里面装好dig、curl、ip、iptables,遇到问题直接启动,省得每次装工具浪费时间。
5.2 外部访问不要硬套内部解析逻辑
很多人会把集群内域名解析的思维带到集群外部:在开发机、跳板机上,也想直接解析my-service.default.svc.cluster.local。除非你在外部专门配置了转发,否则这是做不到的。Kubernetes 的集群域名天生只在集群内生效,外部用户访问服务走的是 NodePort、LoadBalancer 或 Ingress,而不是集群 DNS。
这引出一个设计建议:区分好“集群内部域名”和“外部接入域名”两套体系。内部服务间的调用,通通走 CoreDNS 解析的 Service 域名;外部流量的入口,则统一收敛到 Ingress 或 LoadBalancer 层。这样的好处是,你在做网络策略、安全审计时,只需要关注关键入口点,而不是让每个 Service 都暴露到外部。
5.3 想清楚再改 CoreDNS 配置
CoreDNS 是整个集群所有 Pod 的 DNS 依赖,改它的配置要非常谨慎。我见过有人在 Corefile 里加了一段错误的forward配置,结果集群内所有解析都开始超时。原因很简单:Corefile 的语法或者插件顺序有问题,CoreDNS 启动失败,而 DNS 又是集群的关键依赖,一个组件挂了几乎影响所有业务。
我建议每次修改 ConfigMap 前先做好备份,修改后观察 CoreDNS 的日志和探活状态,一旦异常立刻回滚。另外,给 CoreDNS 的水平自动扩缩容和资源配额留足余量。集群规模增长后,CoreDNS 的 QPS 会明显上升,如果没有设置资源限额,它可能在流量高峰时被 OOMKill,造成大面积解析失败。
最后再分享一个技巧:如果你需要快速查看某个域名当前解析到哪个 IP,又不想进 Pod 太多次,可以临时跑一个一次性的解析容器:
kubectl run dns-query --image=busybox:1.36 --rm -it --restart=Never -- nslookup <service-name>.<namespace>.svc.cluster.local这种方式在写 YAML 之前先确认目标服务的域名,能提前避免很多配置错误。毕竟,域名访问的根本逻辑就是:Service 提供稳定入口,CoreDNS 负责把名称翻译成入口地址,而你把资源定义写对了,剩下的链路整体就比较稳了。