Kubernetes里面跑服务,最容易被低估的两块坑就是:服务怎么暴露,DNS怎么解析。十次排查疑难网络问题,有七次跟这两件事有关。很多同学能把Deployment和Service文件写得规规矩矩,集群里apply一下就发现应用跑起来了,可一旦要对外提供访问,或者要用域名把服务调通,就开始抓瞎。服务暴露解决的是"外部怎么找到你",CoreDNS解决的是"内部怎么按名字找到你"。两者分开看都不算难,但连在一起就很容易踩坑:域名解析到了Service的ClusterIP,却连不上后端;NodePort端口明明开了,外部就是访问不了;Ingress配好了域名,curl过去永远404。这篇文章把我自己在生产环境里调试这层链路时用到的方法、踩过的坑、以及验证过的方案整理出来,适合正在做集群运维、需要调试服务访问链路的同学,也适合那些被域名不通问题折磨了一天还没头绪的人。1. 服务暴露这件事,为什么比想象中复杂1.1 先搞明白Service提供的四种访问入口Kubernetes里的Pod IP是动态的,Pod一重建IP就变,你不能让调用方一直去跟一个会变的IP打交道。Service的作用就是给一组Pod提供一个稳定的访问入口,这个入口可以是一个虚拟IP,也可以是Node上的一个端口,或者是一个外部负载均衡器。Service类型主要有四种,各自的定位和适用场景完全不同:Service类型访问方式适用场景典型问题ClusterIP集群内部虚拟IP服务间调用、内部API集群外部无法直接访问NodePortNodeIP + 静态端口临时对外暴露、小规模场景端口范围受限,多服务难管理LoadBalancer依赖云厂商LB公有云上的生产环境绑定云资源,本地环境无法使用ExternalNameDNS别名把外部服务伪装成集群内服务只能做域名映射,不能做端口转发很多人上来就选NodePort,因为它最简单,改一行就能让外部访问。但等服务数量超过十个,就发现NodePort方案几乎无法维护:端口经常冲突,健康检查不好做,HTTPS证书也没地方挂。LoadBalancer在本地开发环境根本没法用,因为本地没有LB资源可创建。所以在我的实践经验里,ClusterIP是默认选择,所有集群内部服务之间通信都用它;需要对外暴露的业务,上面加一层Ingress,把域名、路径、证书都统一收口到Ingress里管理;NodePort通常只在调试、应急、或者没有Ingress Controller的简单环境里用。1.2 从实际场景决定用哪种暴露方式选型不能靠"感觉哪个好用",要回到业务场景去分析。我见过不少团队把Ingress当成唯一解法,结果内部服务之间也走Ingress,绕了一圈让流量在外网打转,既增加延迟又扩大暴露面。我的建议是分三类来思考。第一类是纯内部调用,Pod之间、微服务之间的接口调用,用ClusterIP加Service域名,让CoreDNS去做服务发现;第二类是面向集群外部用户的服务,比如Web应用、开放API,用Ingress统一入口,一张域名表管所有服务;第三类是调试场景,临时把某个Pod端口映射出来看看效果,用NodePort最直接。这里还有一个容易忽略的细节:Ingress本身不是Kubernetes自带的功能,它只是一个资源对象,真正干活的是Ingress Controller。集群里必须有nginx这类Ingress Controller在运行,Ingress规则才会生效。如果没有Controller,你apply一个Ingress下去,日志里不会有任何报错,它就像被默默忽略了一样,这也是很多初学者百思不得其解的地方。2. CoreDNS是怎么让服务名变成IP的2.1 Service域名的标准格式与解析规则Kubernetes集群内默认安装了CoreDNS,它负责把Service域名解析成ClusterIP。Service域名的标准格式是:<service-name>.<namespace>.svc.<cluster-domain>集群域名默认是cluster.local,所以一个叫demo-svc、部署在default命名空间下的服务,完整域名是demo-svc.default.svc.cluster.local。在同一个命名空间里,你可以偷懒只写demo-svc,CoreDNS能根据Pod的search域补齐。但跨命名空间访问时,必须写完整域名,比如从pay命名空间访问default命名空间的服务,就得写demo-svc.default.svc.cluster.local。无头服务(headless service)的解析规则和普通Service不一样。普通Service解析返回ClusterIP,无头Service直接把所有后端Pod IP返回给调用方,格式是<pod-podName>.<service-name>.<namespace>.svc.cluster.local。如果你在业务里用到StatefulSet,每个Pod都有稳定域名,很多数据库集群就是靠这个特性组建的。2.2 三种DNS策略和search域的影响Kubernetes中的Pod默认使用ClusterFirst策略,意思是Pod内DNS查询优先交给CoreDNS处理,CoreDNS解析不了的域名再转发给上游DNS。除此之外还有Default和None,Default直接使用Node节点的/etc/resolv.conf配置,None则完全自定义dnsConfig。实际调试时,最影响理解的是Pod内/etc/resolv.conf里自动注入的search域,长这样:search default.svc.cluster.local svc.cluster.local cluster.local nameserver 10.96.0.10 options ndots:5这里的10.96.0.10是CoreDNS的Service ClusterIP,Pod内所有DNS查询都发给它。ndots:5表示域名里点的数量少于5个时,会先尝试拼接search域。这意味着你访问demo-svc,实际执行的是demo-svc.default.svc.cluster.local.、demo-svc.svc.cluster.local.、demo-svc.cluster.local.、demo-svc.依次查询。带来的副作用是,如果你在代码里访问一个外部短域名,比如api.internal.com(只有3个点),它也会先拿default.svc.cluster.local去拼接一次,白白多几次DNS查询。所以代码里访问外部域名,尽量写成带最后一个点号的FQDN形式,能省掉不必要的search域尝试。2.3 用Corefile自定义域名解析CoreDNS的核心配置在一个叫Corefile的配置块里,存放在kube-system命名空间下名为coredns的ConfigMap中。默认配置里有一段kubernetes cluster.local in-addr.arpa ip6.arpa,说明只会解析cluster.local这个域。如果业务里需要让集群内Pod访问一个自定义域名,比如内网某个中台系统platform.internal,不需要改每台Pod的hosts文件,直接在CoreDNS的ConfigMap里加hosts插件配置就行:apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .:53 { errors health kubernetes cluster.local in-addr.arpa ip6.arpa { pods verified fallthrough in-addr.arpa ip6.arpa } hosts { 10.0.0.5 platform.internal fallthrough } forward . /etc/resolv.conf }修改ConfigMap后需要滚动重启CoreDNS的Pod才能生效,命令是kubectl -n kube-system rollout restart deployment coredns。这里注意fallthrough关键字必须写上,否则自定义域名查不到后会直接返回NXDOMAIN,不会继续去kubernetes插件或上游DNS查。我最初就是漏了这个,导致集群原有Service域名解析全部失败。3. 从零做一遍:服务暴露与域名访问完整实验3.1 准备一个示例应用和一个ClusterIP服务理论讲再多,不如亲手跑一遍。我用一个最简单的Nginx应用做演示,先创建Deployment,再创建一个ClusterIP类型Service:apiVersion: apps/v1 kind: Deployment metadata: name: demo namespace: default spec: replicas: 2 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: containers: - name: demo image: nginx:1.25 ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: demo-svc spec: selector: app: demo ports: - port: 80 targetPort: 80 type: ClusterIP保存为demo.yaml后执行kubectl apply -f demo.yaml。Service的selector必须和Pod的标签匹配,这里都是app: demo。查看Service详情时,第一件事是看Endpoints列表是否有真实Pod地址:kubectl get svc demo-svc kubectl get endpoints demo-svc如果Endpoints为空,说明Selector没匹配上Pod,这时候Service本身是正常的,但你访问它一定超时。我建议养成习惯:Service创建完之后顺手看一眼Endpoints,能少踩很多坑。3.2 用NodePort把服务暴露到集群外部接下来把服务暴露到外部。我不建议把原有Service的类型直接改成NodePort,而是新建一个独立Service,这样内网访问和外部访问互不影响。NodePort端口的合法范围默认是30000-32767,可以手动指定,也可以让K8s随机分配:apiVersion: v1 kind: Service metadata: name: demo-nodeport spec: selector: app: demo type: NodePort ports: - port: 80 targetPort: 80 nodePort: 30080apply之后,外部访问地址就是任意一台Node节点的IP:30080。本地起实验环境时只有一个节点,直接访问本机IP即可。多Node场景下所有Node的30080端口都会转发到Pod,前提是基础网络放行了这段端口。我在云环境里遇到过几次NodePort端口明明监听正常,外部就是访问不通,十有八九是安全组没放行。所以配置完记得先在集群内节点上curl一下localhost:30080,排除掉服务本身的问题,再去找网络放行的问题。3.3 用Ingress给服务挂上域名生产环境里想让多个服务共享同一组对外IP和端口,就得用Ingress。先把Ingress Controller部署好,然后创建一条Ingress规则:apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress spec: ingressClassName: nginx rules: - host: demo.example.com http: paths: - path: / pathType: Prefix backend: service: name: demo-svc port: number: 80Ingress里的serviceName指向ClusterIP服务,不需要指向NodePort服务。Ingress Controller收到请求后,自己会直接转发到Service对应的后端Pod。这一点很多人理解反了,以为Ingress还要经过NodePort,实际上Ingress Controller位于集群内,直接访问Service ClusterIP就能完成转发。测试时先把demo.example.com这个测试域名解析到Ingress Controller所在节点的IP上,最简单的办法是改本机的hosts文件,然后浏览器访问http://demo.example.com。如果返回Nginx默认页面,说明整条链路已经通了。3.4 验证整个访问链路链路通不通,不能只测一次。我习惯从内到外分三步验证:先在Pod内部用短域名访问一次,确认CoreDNS解析正常;再在节点上通过NodePort访问一次,确认kube-proxy转发正常;最后在外部通过Ingress域名访问一次,确认入口规则生效。Pod内访问短域名的方法:kubectl run -it --rm --image=busybox:1.28 curl-test --restart=Never -- sh进入容器后执行wget -q -O- demo-svc.default.svc.cluster.local或者直接短域名demo-svc。之所以用busybox:1.28,是因为新版busybox砍掉了nslookup功能,调试DNS会不太顺手。这个镜像版本在很多排障场景下都好使。4. 一次DNS查询的背后,数据包是怎么走的4.1 从应用进程到CoreDNS的解析流程当Pod里的应用访问demo-svc.default.svc.cluster.local时,流程是这样的:应用发起解析请求,进程读取本地/etc/resolv.conf,把查询发送给nameserver,也就是CoreDNS的ClusterIP。CoreDNS收到请求后,会看这个域名是否属于cluster.local域,如果是,就通过kubernetes插件向kube-apiserver查询对应Service的ClusterIP和服务端口,然后把结果返回给Pod里的应用。这里有一个关键细节:CoreDNS返回的不是Pod IP,而是Service的ClusterIP。应用再去连接这个ClusterIP,流量才会被kube-proxy转发到后端的某个Pod上。所以DNS解析成功不代表访问成功,解析到ClusterIP只是第一步,后面还有一层端口转发的逻辑要验证。我见过一个案例:应用日志里一直报域名解析失败,但手动在容器里nslookup这个域名明明能解析成功。后来发现应用自己的DNS客户端配置了缓存,缓存了旧的错误结果。所以排查问题时优先清缓存、重启进程,而不是反复修改CoreDNS配置。4.2 集群内部的流量转发链路ClusterIP的流量转发依赖kube-proxy。在Linux内核的iptables或ipvs规则中,访问ClusterIP的流量会被改写目标地址,转发给某个健康的后端Pod。ipvs模式下这种转发是轮询的,性能更好;iptables模式下每条规则都要逐条匹配,Service数量几百个时会有明显的匹配成本。从这个链路可以看出,任意一环出问题都会表现为"域名解析正常但请求失败":Service的Endpoints为空、Pod健康检查失败、kube-proxy规则异常、网络插件策略拦截,每一种都可能。网上经常有人遇到类似问题后就到处问为什么连不上,实际上最该做的第一件事是kubectl get endpoints,先确认后端Pod列表是否存在。4.3 外部域名和内部域名为什么不能混着配有个危险配置我特别想提醒大家:不要在CoreDNS里把互联网域名硬解析成内网地址。比如为了测试某个外部API,在Corefile的hosts里把api.real-service.com指向了内网IP,当时看起来一切正常,但等到这个外部服务上线后,全集群的Pod仍然会命中这个hosts记录,导致线上事故。Corefile里的hosts配置优先级很高,一旦写上域名映射,集群内所有Pod都会采用这条记录,不会再往上游DNS去查。如果确实要模拟某个外部域名,建议用独立的测试域,例如fake-api.internal.test,不要污染真实域名。这也是一个值得写进规范的好习惯。5. 排障实录:七天里踩过的那些坑5.1 常见故障现象速查表现象大概率原因排查动作Pod里nslookup Service名超时CoreDNS副本挂了或网络原因kubectl -n kube-system get pods -l k8s-app=kube-dns解析返回NXDOMAINnameserver上CoreDNS配置缺失、搜索域不对检查Corefile、查看resolv.conf域名解析正常但curl超时Service Endpoints为空kubectl get endpoints <svc>Service IP存在但通过NodePort不通节点安全组没放行在节点上curl localhost测试Ingress返回404路径不匹配或Controller未部署查看Ingress Controller日志短域名可访问,完整域名失败search域配置被覆盖cat /etc/resolv.conf这张表是我平时排查顺序的浓缩版:先确认CoreDNS状态,再确认Pods的resolv.conf,然后检查Service Endpoints,最后才怀疑网络规则。5.2 排查DNS解析问题的标准流程如果遇到Pod之间解析失败,我会按下面的顺序执行,每一步都有明确产出,不会盲目操作。第一步,确认CoreDNS自身状态:kubectl -n kube-system get pods -l k8s-app=kube-dns kubectl -n kube-system logs deployment/coredns --tail=30第二步,用一个临时Pod做端到端测试,避免在生产容器里装额外工具:kubectl run -it --rm --image=busybox:1.28 dnstest --restart=Never -- sh nslookup demo-svc.default.svc.cluster.local第三步,检查Service和对应的Endpoints:kubectl get svc -n default kubectl get endpoints -n default第四步,检查CoreDNS配置是否被改坏:kubectl -n kube-system get configmap coredns -o yaml这套流程走下来,90%的DNS问题都能定位到具体环节。如果CoreDNS日志里有大量SERVFAIL,通常意味着上游DNS不可达,检查Corefile里forward .指向的目标地址是否正常。5.3 我坚持的几个好习惯做服务暴露和域名访问调试久了,我总结出三个值得固化的习惯。一是所有Service创建完立即看Endpoints,不把隐患留到后面;二是所有域名规则改动都走版本化管理,CoreDNS的ConfigMap改动必须有记录,否则改坏了很难回滚;三是在代码和配置里统一用FQDN格式,避免依赖search域补全,既减少DNS查询次数,也让日志里的访问目标更加清晰。另外,CoreDNS在高并发场景下容易被流量打穿,我自己就遇到过Pod内部频繁调用导致CoreDNS容器CPU报警的情况。缓解办法是调整CoreDNS副本数和资源限制,当前生产上常用NodeLocal DNSCache方案把DNS缓存下放到每个Node,减少上游请求压力。这个方案配置起来有点工作量,但对大规模集群非常值。最后再分享一个小技巧:遇到奇怪的域名访问问题,先看Pod里/etc/resolv.conf是怎么写的,再在Pod里手动解析一遍目标域名,配合CoreDNS日志交叉比对。大多数问题不是配置本身错了,而是你还没看到真正的现象。服务暴露和域名访问这条链路,本质上就是一条从客户端到Pod的路径,把每一跳都验证一遍,问题自然无处可藏。
Kubernetes里面跑服务,最容易被低估的两块坑就是:服务怎么暴露,DNS怎么解析。十次排查疑难网络问题,有七次跟这两件事有关。很多同学能把Deployment和Service文件写得规规矩矩,集群里apply一下就发现应用跑起来了,可一旦要对外提供访问,或者要用域名把服务调通,就开始抓瞎。服务暴露解决的是"外部怎么找到你",CoreDNS解决的是"内部怎么按名字找到你"。两者分开看都不算难,但连在一起就很容易踩坑:域名解析到了Service的ClusterIP,却连不上后端;NodePort端口明明开了,外部就是访问不了;Ingress配好了域名,curl过去永远404。这篇文章把我自己在生产环境里调试这层链路时用到的方法、踩过的坑、以及验证过的方案整理出来,适合正在做集群运维、需要调试服务访问链路的同学,也适合那些被域名不通问题折磨了一天还没头绪的人。1. 服务暴露这件事,为什么比想象中复杂1.1 先搞明白Service提供的四种访问入口Kubernetes里的Pod IP是动态的,Pod一重建IP就变,你不能让调用方一直去跟一个会变的IP打交道。Service的作用就是给一组Pod提供一个稳定的访问入口,这个入口可以是一个虚拟IP,也可以是Node上的一个端口,或者是一个外部负载均衡器。Service类型主要有四种,各自的定位和适用场景完全不同:Service类型访问方式适用场景典型问题ClusterIP集群内部虚拟IP服务间调用、内部API集群外部无法直接访问NodePortNodeIP + 静态端口临时对外暴露、小规模场景端口范围受限,多服务难管理LoadBalancer依赖云厂商LB公有云上的生产环境绑定云资源,本地环境无法使用ExternalNameDNS别名把外部服务伪装成集群内服务只能做域名映射,不能做端口转发很多人上来就选NodePort,因为它最简单,改一行就能让外部访问。但等服务数量超过十个,就发现NodePort方案几乎无法维护:端口经常冲突,健康检查不好做,HTTPS证书也没地方挂。LoadBalancer在本地开发环境根本没法用,因为本地没有LB资源可创建。所以在我的实践经验里,ClusterIP是默认选择,所有集群内部服务之间通信都用它;需要对外暴露的业务,上面加一层Ingress,把域名、路径、证书都统一收口到Ingress里管理;NodePort通常只在调试、应急、或者没有Ingress Controller的简单环境里用。1.2 从实际场景决定用哪种暴露方式选型不能靠"感觉哪个好用",要回到业务场景去分析。我见过不少团队把Ingress当成唯一解法,结果内部服务之间也走Ingress,绕了一圈让流量在外网打转,既增加延迟又扩大暴露面。我的建议是分三类来思考。第一类是纯内部调用,Pod之间、微服务之间的接口调用,用ClusterIP加Service域名,让CoreDNS去做服务发现;第二类是面向集群外部用户的服务,比如Web应用、开放API,用Ingress统一入口,一张域名表管所有服务;第三类是调试场景,临时把某个Pod端口映射出来看看效果,用NodePort最直接。这里还有一个容易忽略的细节:Ingress本身不是Kubernetes自带的功能,它只是一个资源对象,真正干活的是Ingress Controller。集群里必须有nginx这类Ingress Controller在运行,Ingress规则才会生效。如果没有Controller,你apply一个Ingress下去,日志里不会有任何报错,它就像被默默忽略了一样,这也是很多初学者百思不得其解的地方。2. CoreDNS是怎么让服务名变成IP的2.1 Service域名的标准格式与解析规则Kubernetes集群内默认安装了CoreDNS,它负责把Service域名解析成ClusterIP。Service域名的标准格式是:<service-name>.<namespace>.svc.<cluster-domain>集群域名默认是cluster.local,所以一个叫demo-svc、部署在default命名空间下的服务,完整域名是demo-svc.default.svc.cluster.local。在同一个命名空间里,你可以偷懒只写demo-svc,CoreDNS能根据Pod的search域补齐。但跨命名空间访问时,必须写完整域名,比如从pay命名空间访问default命名空间的服务,就得写demo-svc.default.svc.cluster.local。无头服务(headless service)的解析规则和普通Service不一样。普通Service解析返回ClusterIP,无头Service直接把所有后端Pod IP返回给调用方,格式是<pod-podName>.<service-name>.<namespace>.svc.cluster.local。如果你在业务里用到StatefulSet,每个Pod都有稳定域名,很多数据库集群就是靠这个特性组建的。2.2 三种DNS策略和search域的影响Kubernetes中的Pod默认使用ClusterFirst策略,意思是Pod内DNS查询优先交给CoreDNS处理,CoreDNS解析不了的域名再转发给上游DNS。除此之外还有Default和None,Default直接使用Node节点的/etc/resolv.conf配置,None则完全自定义dnsConfig。实际调试时,最影响理解的是Pod内/etc/resolv.conf里自动注入的search域,长这样:search default.svc.cluster.local svc.cluster.local cluster.local nameserver 10.96.0.10 options ndots:5这里的10.96.0.10是CoreDNS的Service ClusterIP,Pod内所有DNS查询都发给它。ndots:5表示域名里点的数量少于5个时,会先尝试拼接search域。这意味着你访问demo-svc,实际执行的是demo-svc.default.svc.cluster.local.、demo-svc.svc.cluster.local.、demo-svc.cluster.local.、demo-svc.依次查询。带来的副作用是,如果你在代码里访问一个外部短域名,比如api.internal.com(只有3个点),它也会先拿default.svc.cluster.local去拼接一次,白白多几次DNS查询。所以代码里访问外部域名,尽量写成带最后一个点号的FQDN形式,能省掉不必要的search域尝试。2.3 用Corefile自定义域名解析CoreDNS的核心配置在一个叫Corefile的配置块里,存放在kube-system命名空间下名为coredns的ConfigMap中。默认配置里有一段kubernetes cluster.local in-addr.arpa ip6.arpa,说明只会解析cluster.local这个域。如果业务里需要让集群内Pod访问一个自定义域名,比如内网某个中台系统platform.internal,不需要改每台Pod的hosts文件,直接在CoreDNS的ConfigMap里加hosts插件配置就行:apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .:53 { errors health kubernetes cluster.local in-addr.arpa ip6.arpa { pods verified fallthrough in-addr.arpa ip6.arpa } hosts { 10.0.0.5 platform.internal fallthrough } forward . /etc/resolv.conf }修改ConfigMap后需要滚动重启CoreDNS的Pod才能生效,命令是kubectl -n kube-system rollout restart deployment coredns。这里注意fallthrough关键字必须写上,否则自定义域名查不到后会直接返回NXDOMAIN,不会继续去kubernetes插件或上游DNS查。我最初就是漏了这个,导致集群原有Service域名解析全部失败。3. 从零做一遍:服务暴露与域名访问完整实验3.1 准备一个示例应用和一个ClusterIP服务理论讲再多,不如亲手跑一遍。我用一个最简单的Nginx应用做演示,先创建Deployment,再创建一个ClusterIP类型Service:apiVersion: apps/v1 kind: Deployment metadata: name: demo namespace: default spec: replicas: 2 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: containers: - name: demo image: nginx:1.25 ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: demo-svc spec: selector: app: demo ports: - port: 80 targetPort: 80 type: ClusterIP保存为demo.yaml后执行kubectl apply -f demo.yaml。Service的selector必须和Pod的标签匹配,这里都是app: demo。查看Service详情时,第一件事是看Endpoints列表是否有真实Pod地址:kubectl get svc demo-svc kubectl get endpoints demo-svc如果Endpoints为空,说明Selector没匹配上Pod,这时候Service本身是正常的,但你访问它一定超时。我建议养成习惯:Service创建完之后顺手看一眼Endpoints,能少踩很多坑。3.2 用NodePort把服务暴露到集群外部接下来把服务暴露到外部。我不建议把原有Service的类型直接改成NodePort,而是新建一个独立Service,这样内网访问和外部访问互不影响。NodePort端口的合法范围默认是30000-32767,可以手动指定,也可以让K8s随机分配:apiVersion: v1 kind: Service metadata: name: demo-nodeport spec: selector: app: demo type: NodePort ports: - port: 80 targetPort: 80 nodePort: 30080apply之后,外部访问地址就是任意一台Node节点的IP:30080。本地起实验环境时只有一个节点,直接访问本机IP即可。多Node场景下所有Node的30080端口都会转发到Pod,前提是基础网络放行了这段端口。我在云环境里遇到过几次NodePort端口明明监听正常,外部就是访问不通,十有八九是安全组没放行。所以配置完记得先在集群内节点上curl一下localhost:30080,排除掉服务本身的问题,再去找网络放行的问题。3.3 用Ingress给服务挂上域名生产环境里想让多个服务共享同一组对外IP和端口,就得用Ingress。先把Ingress Controller部署好,然后创建一条Ingress规则:apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress spec: ingressClassName: nginx rules: - host: demo.example.com http: paths: - path: / pathType: Prefix backend: service: name: demo-svc port: number: 80Ingress里的serviceName指向ClusterIP服务,不需要指向NodePort服务。Ingress Controller收到请求后,自己会直接转发到Service对应的后端Pod。这一点很多人理解反了,以为Ingress还要经过NodePort,实际上Ingress Controller位于集群内,直接访问Service ClusterIP就能完成转发。测试时先把demo.example.com这个测试域名解析到Ingress Controller所在节点的IP上,最简单的办法是改本机的hosts文件,然后浏览器访问http://demo.example.com。如果返回Nginx默认页面,说明整条链路已经通了。3.4 验证整个访问链路链路通不通,不能只测一次。我习惯从内到外分三步验证:先在Pod内部用短域名访问一次,确认CoreDNS解析正常;再在节点上通过NodePort访问一次,确认kube-proxy转发正常;最后在外部通过Ingress域名访问一次,确认入口规则生效。Pod内访问短域名的方法:kubectl run -it --rm --image=busybox:1.28 curl-test --restart=Never -- sh进入容器后执行wget -q -O- demo-svc.default.svc.cluster.local或者直接短域名demo-svc。之所以用busybox:1.28,是因为新版busybox砍掉了nslookup功能,调试DNS会不太顺手。这个镜像版本在很多排障场景下都好使。4. 一次DNS查询的背后,数据包是怎么走的4.1 从应用进程到CoreDNS的解析流程当Pod里的应用访问demo-svc.default.svc.cluster.local时,流程是这样的:应用发起解析请求,进程读取本地/etc/resolv.conf,把查询发送给nameserver,也就是CoreDNS的ClusterIP。CoreDNS收到请求后,会看这个域名是否属于cluster.local域,如果是,就通过kubernetes插件向kube-apiserver查询对应Service的ClusterIP和服务端口,然后把结果返回给Pod里的应用。这里有一个关键细节:CoreDNS返回的不是Pod IP,而是Service的ClusterIP。应用再去连接这个ClusterIP,流量才会被kube-proxy转发到后端的某个Pod上。所以DNS解析成功不代表访问成功,解析到ClusterIP只是第一步,后面还有一层端口转发的逻辑要验证。我见过一个案例:应用日志里一直报域名解析失败,但手动在容器里nslookup这个域名明明能解析成功。后来发现应用自己的DNS客户端配置了缓存,缓存了旧的错误结果。所以排查问题时优先清缓存、重启进程,而不是反复修改CoreDNS配置。4.2 集群内部的流量转发链路ClusterIP的流量转发依赖kube-proxy。在Linux内核的iptables或ipvs规则中,访问ClusterIP的流量会被改写目标地址,转发给某个健康的后端Pod。ipvs模式下这种转发是轮询的,性能更好;iptables模式下每条规则都要逐条匹配,Service数量几百个时会有明显的匹配成本。从这个链路可以看出,任意一环出问题都会表现为"域名解析正常但请求失败":Service的Endpoints为空、Pod健康检查失败、kube-proxy规则异常、网络插件策略拦截,每一种都可能。网上经常有人遇到类似问题后就到处问为什么连不上,实际上最该做的第一件事是kubectl get endpoints,先确认后端Pod列表是否存在。4.3 外部域名和内部域名为什么不能混着配有个危险配置我特别想提醒大家:不要在CoreDNS里把互联网域名硬解析成内网地址。比如为了测试某个外部API,在Corefile的hosts里把api.real-service.com指向了内网IP,当时看起来一切正常,但等到这个外部服务上线后,全集群的Pod仍然会命中这个hosts记录,导致线上事故。Corefile里的hosts配置优先级很高,一旦写上域名映射,集群内所有Pod都会采用这条记录,不会再往上游DNS去查。如果确实要模拟某个外部域名,建议用独立的测试域,例如fake-api.internal.test,不要污染真实域名。这也是一个值得写进规范的好习惯。5. 排障实录:七天里踩过的那些坑5.1 常见故障现象速查表现象大概率原因排查动作Pod里nslookup Service名超时CoreDNS副本挂了或网络原因kubectl -n kube-system get pods -l k8s-app=kube-dns解析返回NXDOMAINnameserver上CoreDNS配置缺失、搜索域不对检查Corefile、查看resolv.conf域名解析正常但curl超时Service Endpoints为空kubectl get endpoints <svc>Service IP存在但通过NodePort不通节点安全组没放行在节点上curl localhost测试Ingress返回404路径不匹配或Controller未部署查看Ingress Controller日志短域名可访问,完整域名失败search域配置被覆盖cat /etc/resolv.conf这张表是我平时排查顺序的浓缩版:先确认CoreDNS状态,再确认Pods的resolv.conf,然后检查Service Endpoints,最后才怀疑网络规则。5.2 排查DNS解析问题的标准流程如果遇到Pod之间解析失败,我会按下面的顺序执行,每一步都有明确产出,不会盲目操作。第一步,确认CoreDNS自身状态:kubectl -n kube-system get pods -l k8s-app=kube-dns kubectl -n kube-system logs deployment/coredns --tail=30第二步,用一个临时Pod做端到端测试,避免在生产容器里装额外工具:kubectl run -it --rm --image=busybox:1.28 dnstest --restart=Never -- sh nslookup demo-svc.default.svc.cluster.local第三步,检查Service和对应的Endpoints:kubectl get svc -n default kubectl get endpoints -n default第四步,检查CoreDNS配置是否被改坏:kubectl -n kube-system get configmap coredns -o yaml这套流程走下来,90%的DNS问题都能定位到具体环节。如果CoreDNS日志里有大量SERVFAIL,通常意味着上游DNS不可达,检查Corefile里forward .指向的目标地址是否正常。5.3 我坚持的几个好习惯做服务暴露和域名访问调试久了,我总结出三个值得固化的习惯。一是所有Service创建完立即看Endpoints,不把隐患留到后面;二是所有域名规则改动都走版本化管理,CoreDNS的ConfigMap改动必须有记录,否则改坏了很难回滚;三是在代码和配置里统一用FQDN格式,避免依赖search域补全,既减少DNS查询次数,也让日志里的访问目标更加清晰。另外,CoreDNS在高并发场景下容易被流量打穿,我自己就遇到过Pod内部频繁调用导致CoreDNS容器CPU报警的情况。缓解办法是调整CoreDNS副本数和资源限制,当前生产上常用NodeLocal DNSCache方案把DNS缓存下放到每个Node,减少上游请求压力。这个方案配置起来有点工作量,但对大规模集群非常值。最后再分享一个小技巧:遇到奇怪的域名访问问题,先看Pod里/etc/resolv.conf是怎么写的,再在Pod里手动解析一遍目标域名,配合CoreDNS日志交叉比对。大多数问题不是配置本身错了,而是你还没看到真正的现象。服务暴露和域名访问这条链路,本质上就是一条从客户端到Pod的路径,把每一跳都验证一遍,问题自然无处可藏。