Service 通、Pod 正常,为什么外部总 503?Ingress 端口映射的隐藏陷阱
场景:微服务上线后 Pod Running、Service 正常、集群内 curl 200,但外部域名访问一直 503 路径:坐标(503 现象 + 内部验证)→ 分层(Service→Ingress→Controller→集群四层排查)→ 路径(三板斧使用场景)→ 定位(三个最常走的弯路)→ 标点(修复 + 验证 + Check-list) K8s 版本:v1.25,Ingress-Nginx Controller v1.8
上篇我们聊了 Service 内部不通的排查——Endpoints 有 IP 却超时,kube-proxy 的 iptables 规则没刷进去。这次从 Service 往上走一层,看看 Ingress 的问题。
新服务上线,团队长出一口气——Pod 全 Running,curl 本地 200。接着上线验证:https://api.example.com/orders → 503 Service Unavailable。
"Service 端口写错了吧?"改了 3 遍端口号,问题依旧。又加了 rewrite-target annotation、改了 Service type: LoadBalancer、甚至怀疑 DNS 配错了——全白费。
不是 Pod 挂了,不是 Service 端口不对——是 Ingress 规则里的 port 引用错了 Service 的端口号。Service 的 port 是 80、targetPort 是 8080,但 Ingress backend 写的是 8080——Ingress Controller 拿到这个配置直接懵了。
如果你去搜这个问题,大部分文章会说"检查 Ingress 的 port"。但很少有人告诉你:Ingress 的 backend.service.port.number 指的是 Service 的 port,不是 targetPort。这两个概念在 K8s 里是完全不同的两个字段,配错了 Controller 不会报 Error——只会在日志里写一个 Warning,然后默默 fallback。小流量场景可能"碰巧通了",真正上量就崩。
坐标 — Pod 通,Service 通,那谁不通?
排错的第一步不是去看 Ingress——是先确认地基是稳的。很多人在外部 503 后直接去翻 Ingress YAML,发现端口写对了(比如写的 80),但问题根本不是端口号——是 Pod 根本没跑起来。所以不管你觉得问题在哪,先验 Pod 和 Service。

从截图可以看到完整验证链:
- Pod Running,日志正常启动 Tomcat,监听 8080
- Service ClusterIP 10.96.1.5:80 正常分配
- 集群内 curl http://orders:80/actuator/health → 200
- Endpoints 绑定 10.1.0.15:8080,Service port 80 → targetPort 8080 映射正确
现在我们知道 Pod 和 Service 都正常。那为什么外网走 Ingress 就 503?
这涉及到 K8s 的流量路径:外部请求 → Ingress Controller → Ingress 规则匹配 → Service → Pod。Pod 本身没问题,Service 也没问题,那问题出在 Ingress 这个环节。继续往上查。
分层 — Ingress 排查四层顺序
Ingress(Ingress — K8s 的路由规则资源,定义外部 HTTP/HTTPS 流量如何转发到集群内的 Service)只定义了路由规则,是一个声明式配置。真正干活的是 Ingress Controller(Ingress-Nginx Controller — K8s 生态最流行的 Ingress 实现,将 Ingress 资源转换为 Nginx 配置并热加载)。
一条外部请求到达 Ingress Controller 的完整路径:
Client → Ingress Controller(LB/NodePort)
→ 匹配 Ingress 规则(Host + Path)
→ 转发到 Service(ClusterIP:Port)
→ 转发到 Pod(PodIP:targetPort)
所以排查顺序应该是:Service → Ingress → Ingress Controller → 集群

第 1 层:Service 端口确认
先把 Service 的端口定义看仔细。很多人用 kubectl get svc 瞟一眼就过去了,但 port 和 targetPort 的区别就在这里:
kubectl get svc orders -o yaml | grep -A 8 ports
ports:
- name: http
port: 80 # ← Service 暴露的端口
protocol: TCP
targetPort: 8080 # ← 容器实际监听的端口
为什么 K8s 要区分 port 和 targetPort?这不是冗余设计——port 是 Service 在集群网络层的抽象端口,targetPort 是 Pod 容器层的实际端口。解耦的意义在于:你可以改变 Pod 的容器端口(比如从 8080 改成 9090),只改 Service 的 targetPort 而不影响调用方——调用方仍然用 svc:port 访问,K8s 内部做 DNAT 转换。
这个区别对于 Ingress 至关重要,因为 Ingress 引用的是 Service 的 port,不是 targetPort。
第 2 层:Ingress 资源配置
kubectl get ingress orders-ingress -o yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: orders-ingress
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: api.example.com
http:
paths:
- path: /orders
pathType: Prefix
backend:
service:
name: orders
port:
number: 8080 # ← 问题在这里!
port.number: 8080——看起来没错啊,Pod 就是监听 8080 的。但 Ingress 的 backend.service.port.number 引用的是 Service 的 port(80),不是 Pod 监听的 targetPort(8080)。
为什么这么设计?Ingress Controller 的转发逻辑是:
Ingress → Service(ClusterIP:port)→ targetPort → Pod
Ingress 只需要知道 Service 的入口端口(port),具体的 Pod 端口(targetPort)由 Service 负责映射。让 Ingress 绕过 Service 直接引用 targetPort 等于跳过了 Service 层的抽象,违背了分层设计。
再查 Ingress 的 Events 有没有线索:
kubectl describe ingress orders-ingress

Events: <none>
Events 什么也没有。Ingress 资源通过 K8s API 校验了——YAML 格式正确、引用的 Service 存在、字段类型没错——但在语义层面(port 号匹配 Service 的哪个字段)不做校验。这是一个静默失败场景:资源创建成功,Ingress Controller 加载配置时才发现问题。
这里有一个关键的设计决策:为什么不把这个校验放在 Admission Webhook 里?
Ingress-Nginx 确实有一个 ValidatingAdmissionWebhook,但它只做结构校验——检查必需字段、IngressClass 是否存在、annotation 格式。它不做运行时校验——它不可能知道你的 Service 的 port 是多少,因为 admission webhook 在资源持久化前触发,此时 Ingress Controller 还没开始处理这个资源。
所以端口号不对这种错误,只有 Ingress Controller 在 sync loop 里加载配置时才能发现。这也是为什么必须看 Controller 日志——Events 和 API 校验都不会告诉你。
第 3 层:Ingress Controller 日志
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50

W0315 10:25:12.345678 1 controller.go:1234]
Service "orders" port 8080 not found.
Falling back to first available port (80).
Controller 报了一个 Warning(不是 Error)。这里又是一个隐蔽的设计:Ingress Controller 为了保证"配置已经提交了,尽可能让流量走通",实现了 fallback 逻辑——找不到指定端口时,选第一个可用端口。
这个行为在很多场景是"好心办坏事": - 单端口 Service:fallback 到 80 端口恰好对了,于是"配错了也能通" - 多端口 Service:fallback 到第一个端口,如果你的 Ingress 需要第二个端口,就 503 了 - 加上多 path 多 host:fallback 逻辑的端口和 path 交叉组合,结果完全不可预测
所以你会遇到一个诡异的现象:在小流量测试时 Ingress 偶尔能通,上线后大面积 503。不是流量大了崩了——是 fallback 行为在流量少的时候"碰巧"工作,多路由多端口时全面失效。
这也是为什么我强调第 3 层(Controller 日志)是 Ingress 排查必看的一层——Events 是空的,Ingress YAML 看起来正常,只有 Controller 日志告诉你"端口 8080 找不到,我在 fallback"。
第 4 层:集群全局检查
最后确认集群层面的 Ingress 能力是正常的:
kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 30d
# ValidatingWebhookConfiguration 是否拦截
kubectl get validatingwebhookconfigurations
NAME WEBHOOKS AGE
ingress-nginx-admission 1 30d
IngressClass(IngressClass — K8s v1.18+ 引入的资源,用于声明集群中有哪些 Ingress Controller 实现。一个集群可能有多个 Controller,Ingress 通过 ingressClassName 字段选择用哪个)是 K8s v1.18+ 的标准化机制。如果你发现 Ingress 资源创建了但 Controller 完全不处理,先查 IngressClass:
# Ingress 的 ingressClassName 是否匹配集群中的 IngressClass
kubectl get ingress orders-ingress -o yaml | grep ingressClassName
# IngressClass 的 controller 字段是否匹配实际部署的 Controller
kubectl get ingressclass nginx -o yaml | grep controller
不匹配的表现更隐蔽——Ingress YAML 完全正常、Events 为空、Controller 日志也没报错——只是 Controller 根本不 watch 这个 Ingress 资源。
路径 — Ingress 排查三板斧
结论前置:三个命令覆盖 Ingress 问题 90% 场景,按顺序执行。每个命令解决一个特定层面的问题,不要跳步。

第 1 斧:看 Ingress 完整 YAML
什么时候用:任何时候排查 Ingress 第一步。先确认你看到的 Ingress 规则是不是你以为的那样。
看哪里:spec.rules[].http.paths[].backend.service.port.number
异常信号:port.number != Service 的 port(用 kubectl get svc <name> -o yaml | grep port 对比)
第 2 斧:看 Ingress Controller 日志
什么时候用:Ingress YAML 看起来正常但访问仍然 503。
看哪里:搜 W 开头的 Warning 行,搜 not found / falling back / error syncing
异常信号:任何谈到 port、endpoint、backend 的警告行——Ingress Controller 很少报 Error,大部分问题以 Warning 形式出现
第 3 斧:看 Controller 实际路由表 什么时候用:前两斧检查完,还是不确定 Ingress 规则是否被加载。 看什么:Ingress Controller 内部生成的 Nginx 配置文件。命令直达底层——不管 Ingress YAML 怎么写,Controller 到底用没用你的规则,cat 出来就知道了。
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
cat /etc/nginx/nginx.conf | grep <service-name>
异常信号:没有输出 → 规则没刷进去。有输出但端口不对 → 规则刷进去了但 backend 不对。
这三板斧的顺序不是随便排的——每一斧覆盖一个层面:YAML 看意图(你配了什么)、日志看过程(Controller 怎么处理你的配置)、路由表看结果(实际生效了什么)。从意图到结果,三层验证完才叫真正查完。
定位 — 三个最常走的弯路
Ingress 503 的根因其实很简单——端口号引用错了。但为什么简单的问题能卡团队几天?因为大部分人走的路都是错的。
❌ 弯路 1:加 rewrite-target Annotation(浪费时间:2-4 小时)
"外网访问路径跟 Pod 内路径对不上?加 rewrite-target。"这是大部分人的第一反应。
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
这个 annotation 确实能解决路径重写的问题——比如你的 Ingress path 是 /orders/api 但 Pod 内只有 /api,rewrite-target 把前缀截掉。但它不解决端口问题。端口不对,rewrite 再多也没用——Ingress Controller 连你的 Service 端口都找不到,路径重写在路由匹配之后才执行,路由匹配都没过。
为什么大家第一反应改这个?因为大部分人第一次接触 Ingress 问题时,网上的教程都在教 rewrite。但教程教的场景是你路径写错了,不是你端口写错了。
成本:加了没用 → 搜其他 annotation 换着加 → 反复试了 2-4 小时 → 问题依旧。
❌ 弯路 2:改 Service type: LoadBalancer(浪费金钱:额外的 LB 费用)
"外网不通?那给 Service 加个 LoadBalancer 试试。"
结果:LoadBalancer 确实能通(直接走 Service 80 端口),但 Ingress 还是 503。因为你绕过了 Ingress 但没有修 Ingress 的问题。
这里有一个隐蔽的成本:你为同一组 Pod 开了两个入口——一个走 Ingress(还是坏的),一个走 LoadBalancer(通了)。多花的云厂商 LB 费用不说,流量路径变成了"部分请求走 Ingress → 503,部分请求走 LB → 200",排查起来更混乱了。
成本:花 30 分钟改 Service type → 发现还是 503 → 又花 1 小时排查为什么"LB 通但 Ingress 不通" → 2 倍时间。
❌ 弯路 3:怀疑 DNS,改 /etc/hosts(浪费时间:1-2 小时)
"域名解析有问题吧?我加个 hosts 强制解析。"
测了 dig 发现 IP 正确。又白忙一场。DNS 解析到 Ingress Controller 的 LoadBalancer IP 再正常不过——但流量到 Controller 后,Controller 找不到后端 Service 端口,照样 503。
这个误判的逻辑其实合理——外部访问第一跳是 DNS。但 DNS 检查基本 1 分钟就能验证完,大部分人花在"我还是加个 hosts 试试"和"从 hosts 切回来"的反复上。
成本:1 分钟能验证的事,花了 1-2 小时在反复确认 DNS。
✅ 正确排查路径(5 分钟)
先查 Ingress YAML 的 backend.service.port,再确认这个端口号等于 Service 的 port(不是 targetPort)。
# 1 分钟:看 Service 的端口定义
kubectl get svc orders -o yaml | grep -A 5 ports
# → port: 80, targetPort: 8080
# 1 分钟:看 Ingress 的 backend.port
kubectl get ingress orders-ingress -o yaml | grep -A 10 http
# → port.number: 8080 ← 应该写 80!
# 2 分钟:确认 Ingress Controller 日志
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=20 | grep -i "not found\|fall"
# 1 分钟:修复 + 验证
总时间:5 分钟。
三条弯路加起来浪费 5-8 小时,正确路径 5 分钟。这就是为什么我说排查 Ingress 不是缺命令——是缺排查顺序。Ingress backend 引用的是 Service 的 port(80),不是 targetPort(8080)。
这两个概念在哪看?kubectl get svc -o wide 只显示 Service port,不显示 targetPort。必须用 -o yaml 才能看到完整的端口映射。很多人不看 -o yaml,只瞟了一眼 kubectl get svc 的输出就下结论——这也是为什么 Service 那个字段被称为"隐蔽陷阱"。
标点 — 修复 + 验证 + Check-list
修复

修复只需要把 port.number: 8080 改成 port.number: 80:
kubectl edit ingress orders-ingress
# 找到 port.number: 8080,改为 80
# 保存退出,Ingress Controller 自动热加载
Ingress-Nginx Controller 默认配置下,Ingress 资源变更后大约 1-3 秒内完成配置 reload。
验证
修复后完整验证链条——不要只测一个 curl 就认为修好了:
# 1. 确认 Ingress Controller 已完成 reload(日志不再出现端口错误)
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=10 | grep -i "port"
# 2. 确认路由表已更新
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
cat /etc/nginx/nginx.conf | grep "proxy_pass.*orders"
# 3. 确认 Ingress 的 ADDRESS 列已分配
kubectl get ingress orders-ingress -w
NAME HOSTS ADDRESS PORTS AGE
orders-ingress api.example.com 192.168.1.100 80 12m
# 4. 外部访问验证
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/orders
200
# 5. 路径完整性验证(多个 path 时逐个测)
curl -s https://api.example.com/orders/health
{"status":"UP"}
curl -s https://api.example.com/orders/api/v1/products
[...]
前两步验证 Ingress Controller 端配置生效,后两步验证流量从外到内完整走通。
金句
"K8s 不是没有告诉你问题在哪——Ingress Controller 日志已经说了端口找不到,只是你没去看。排查的终点不是修好了,是把排查路径写成 check-list。"
Ingress 故障通用排查 Check-list
| # | 检查项 | 命令 | 异常信号 |
|---|---|---|---|
| 1 | Service port 和 targetPort 分别是什么 | kubectl get svc <name> -o yaml \| grep -A 5 ports |
port!= targetPort(正常,但要确认你用哪个) |
| 2 | Ingress backend.port 等于 Service 的 port(非 targetPort) | kubectl get ingress <name> -o yaml \| grep -A 10 http |
port.number != Service 的 port |
| 3 | Ingress 引用的 Service 存在且名字拼写正确 | kubectl get svc <name> |
"NotFound" Error |
| 4 | IngressClass 匹配 Controller | kubectl get ingressclass |
无 IngressClass / 不匹配 |
| 5 | Ingress Controller Pod 运行正常 | kubectl get pods -n ingress-nginx |
Pod CrashLoopBackOff / Pending |
| 6 | Ingress Controller 日志无 port/endpoint 报错 | kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50 |
"not found" / "falling back" / "error syncing" |
| 7 | Controller 实际路由表已更新 | kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf \| grep <service-name> |
无输出 → 规则未加载 |
| 8 | TLS Secret 存在(如有 TLS 配置) | kubectl get secret <tls-name> |
"NotFound" Error |
| 9 | 集群内 Service 可访问 | kubectl run test --rm -it --image curlimages/curl -- curl -s http://<svc-name>:<port>/health |
超时 / 连接拒绝 |
| 10 | DNS 解析到 Ingress Controller 的公网地址 | dig api.example.com +short |
解析到错误 IP / 超时 |
每一行的"异常信号"列是快速判断依据——你不需要记住每行命令的输出长什么样,只需要知道什么是"正常的",什么是"有问题"的。
下篇预告
下篇我们聊 跨 Namespace 服务调用——Pod A 在 Namespace A,Pod B 在 Namespace B,curl 过去要么超时要么连接拒绝——是 DNS 解析问题还是 NetworkPolicy 在挡?
🔗 个人博客:https://opencao.cn 📺 公众号:Ai拆代码的曹操 🌟 知识星球:Ai拆代码的曹操
附:完整命令清单
# ========== 第 1 层:Service ==========
kubectl get svc <name> -o wide
kubectl get endpoints <name>
kubectl get svc <name> -o yaml | grep -A 5 ports
# ========== 第 2 层:Ingress ==========
kubectl get ingress <name> -o yaml
kubectl describe ingress <name>
kubectl get events --field-selector involvedObject.kind=Ingress
# ========== 第 3 层:Ingress Controller ==========
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=100
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
cat /etc/nginx/nginx.conf | grep <service-name>
# ========== 第 4 层:集群全局 ==========
kubectl get ingressclass
kubectl get validatingwebhookconfigurations
kubectl get ingressclass nginx -o yaml | grep controller
# ========== 修复后验证 ==========
kubectl get ingress <name> -w
curl -s -o /dev/null -w "%{http_code}" https://<domain>/<path>