Service 有地址,Ingress 却 502:Endpoints 为空的排查路径
Pod 是 Running 1/1,日志干净,直连 Pod IP 也通,域名访问却 502。ingress-nginx 的 upstream 指向一个已回收的 IP,根因是 Service 的 selector 和 Pod 标签根本不是一回事。
新服务上线,域名挂了 Ingress,kubectl get pod 显示 Running 1/1,容器日志里干干净净——一条请求都没有。
浏览器访问,502 Bad Gateway。这个 502 是从 ingress-nginx 返回的,页面上的 nginx 字样很明确。也就是说请求到了网关,但网关往后转发那一步失败了。
我当时的第一反应是「Pod 没起来」,看了一眼是 Running;第二个反应是「端口不对」,检查了 containerPort: 8080,也对。然后就僵在那儿了,因为应用侧完全没有日志可看——它压根没收到请求。
一、现象:502 但后端「看起来正常」
把当时掌握的信息列一下,能看出矛盾点:
| 检查项 | 结果 | 判断 |
|---|---|---|
| Pod 状态 | Running 1/1 |
容器活着 |
| 容器日志 | 无请求记录 | 请求没进来 |
| Ingress 状态 | ADDRESS 有值 |
网关工作 |
curl pod-ip:8080/health |
200 OK | 应用本身正常 |
| 浏览器访问域名 | 502 | 网关到 Pod 这一段断了 |
第四行是最能定性的:直连 Pod IP 通,走域名不通。这说明问题不在应用,而在「Service → Endpoints」这层间接引用上。
在 K8s 里,Ingress 从来不直接连 Pod。它连的是 Service,Service 再通过 Endpoints 找到具体的 Pod IP。中间任何一环断了,表现出来都是 502 或 503。
二、502 是网关给的,先确认它说了什么
ingress-nginx 的 502 有几类固定文案,看一眼就能缩小范围。最有效的手段是打开错误日志:
# 找到 ingress-nginx 的 pod
kubectl -n ingress-nginx get pods -l app.kubernetes.io/name=ingress-nginx
# 盯着错误日志复现一次
kubectl -n ingress-nginx logs -f deploy/ingress-nginx-controller --tail=50 | grep -i error
复现时打出来的关键行是:
[error] 32#32: *8721 connect() failed (111: Connection refused) while connecting to upstream,
client: 10.0.1.77, server: api.ispt.top,
request: "GET /api/orders HTTP/2.0",
upstream: "http://10.244.3.18:8080/api/orders"
Connection refused 加上一个 10.244.3.18:8080 的 upstream 地址。注意这个 IP:它不是我这个新服务的 Pod IP。
我把新 Pod 的 IP 打出来对比,是 10.244.5.42。网关转发到的是一个已经被回收的旧 Pod IP。
这就是 502 的经典成因:Endpoints 里存着过期的 IP,或者干脆没有 IP。
三、查 Endpoints:空的就是没接上
Service 只是一个虚拟 IP 加一组选择规则,真正被转发的是 Endpoints(新版本里叫 EndpointSlice)。它为空,网关就无处可转。
kubectl -n prod get svc order-api -o wide
kubectl -n prod get endpoints order-api
kubectl -n prod get endpointslices -l kubernetes.io/service-name=order-api
# 没 endpoints 时,看 Service 的选择器到底选中了谁
kubectl -n prod get pods -l "$(kubectl -n prod get svc order-api \
-o jsonpath='{range $k,$v := .spec.selector}{$k}={$v},{end}' | sed 's/,$//')"
当时 get endpoints 输出的是 ENDPOINTS: <none>。而按 selector 查 Pod,返回的是 No resources found。
原因就在这儿:Service 的 selector 是 app: order-api,我 Deployment 里写的标签是 app.kubernetes.io/name: order-api。两个标签完全不是一回事,selector 一个都没选中。
# Service:选择器写错
spec:
selector:
app: order-api
# Deployment:Pod 实际带的标签
metadata:
labels:
app.kubernetes.io/name: order-api
改法很简单,统一到一套标签规范上。我倾向用 app.kubernetes.io/* 这套标准标签,因为 kubectl 和大部分工具默认认它:
# Deployment
spec:
selector:
matchLabels:
app.kubernetes.io/name: order-api
template:
metadata:
labels:
app.kubernetes.io/name: order-api
app.kubernetes.io/instance: order-api-prod
# Service
spec:
selector:
app.kubernetes.io/name: order-api
改完 kubectl apply,Endpoints 立刻出现。但这里有个坑:Service 的 selector 可以随时改,Deployment 的 spec.selector 是不可变的,要改只能删了重建。所以我给新服务的模板里把标签一次定死,避免以后再动。
四、Endpoints 有 IP 但仍然是 502 的三种情况
标签对不上是最容易发现的。如果 Endpoints 里有 IP,502 还有三种成因:
| 现象 | 原因 | 验证命令 |
|---|---|---|
| upstream 是旧 Pod IP | Endpoints 未及时同步,或 readiness 抖动 | 对比 endpoints 与 get pods -o wide 的 IP |
| IP 对但仍 refused | targetPort 与容器实际监听端口不一致 |
kubectl exec -- curl localhost:<port> |
| 间歇性 502 | 部分 Pod readiness 未就绪却被摘入 | kubectl get endpointslices -o yaml 看 ready 字段 |
第二种最常见。 port 是 Service 暴露的端口,targetPort 才是容器里的端口,两个可以不同。写 targetPort: http 时,它引用的是容器 ports 里 name: http 那一项;名字写错或者没定义,Endpoints 里就会是空端口或者干脆不生成:
spec:
ports:
- name: http
port: 80
targetPort: http # 对应容器 ports 里 name: http
selector:
app.kubernetes.io/name: order-api
---
containers:
- name: app
ports:
- name: http
containerPort: 8080
这套写法比硬编码 targetPort: 8080 好维护,但前提是容器里的 name 必须严格一致。我吃过一次亏:把 name: http 改成了 name: web,忘了同步 Service,Endpoints 里就只剩下 port: 80 而 targetPort 解析不出来,网关直接 502。
第三种是间歇性的,最难查。Endpoints 里会保留未就绪的 Pod,但标记 ready: false,网关默认不转发给它们。如果 readiness 探针本身写得太严(比如依赖一个不稳定下游),Pod 会频繁在「就绪/不就绪」之间抖动,表现出来就是「十个请求里两三个 502」。
五、readiness 自己把 Pod 摘掉了
还有一类更隐蔽:Endpoints 一开始是有的,过几分钟变空,然后 Pod 被重启。这种情况要去查探针。
kubectl -n prod describe pod order-api-7d9f8c6b5-x2vmn
Events 里如果有:
Warning Unhealthy Readiness probe failed: Get "http://10.244.5.42:8080/healthz": context deadline exceeded
就说明探针在超时。常见原因是三条:
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5 # 太短:应用还没加载完配置
periodSeconds: 5
timeoutSeconds: 1 # 太短:一次 GC 抖动就超时
failureThreshold: 3
timeoutSeconds: 1 在 JVM 应用上非常危险——一次 Young GC 或者首次请求的类加载,就可能超过 1 秒。我把它放宽到 3,failureThreshold 保持 3,initialDelaySeconds 按应用实际启动时间(这个服务冷启动 22 秒,设成 30)。
探针的语义要分清,配混了会出现「Pod 活着但不接流量」或者「Pod 有问题还在接流量」:
| 探针 | 失败后动作 | 适合检查什么 |
|---|---|---|
livenessProbe |
重启容器 | 进程死锁、无法自愈 |
readinessProbe |
从 Endpoints 摘除 | 依赖不可用、启动未完成 |
startupProbe |
重启容器(启动期内不查 liveness) | 慢启动应用 |
/healthz 这个接口也要注意别写成「什么都查」:如果它去 ping 数据库、ping Redis、ping 下游,任何一个抖动都会让所有 Pod 同时被摘掉,等于自己制造一次全站故障。我的做法是 /healthz 只检查进程自身(返回 200 就行),依赖健康交给业务层的熔断降级。
六、把排查固化成一条命令
这个问题修完之后,我把排查顺序写成了脚本,下次直接跑:
#!/bin/bash
# k8s-svc-diag.sh <namespace> <service>
NS=$1; SVC=$2
echo "== Service =="
kubectl -n "$NS" get svc "$SVC" -o wide
echo "== Selector =="
SEL=$(kubectl -n "$NS" get svc "$SVC" -o jsonpath='{.spec.selector}' \
| tr -d '{}"' | tr ',' '\n' | paste -sd, -)
echo "$SEL"
echo "== 被选中的 Pod =="
kubectl -n "$NS" get pods -l "$SEL" -o wide
echo "== Endpoints =="
kubectl -n "$NS" get endpoints "$SVC"
echo "== EndpointSlice ready 状态 =="
kubectl -n "$NS" get endpointslices -l "kubernetes.io/service-name=$SVC" \
-o jsonpath='{range .items[*]}{range .endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}{end}'
输出里只要「被选中的 Pod」为空,或者 Endpoints 是 <none>,就不用再往下查应用了,问题 100% 在标签或端口上。这条判断规则省掉了我大量时间——以前总是先去翻应用日志,而应用根本没收到请求。
小结
- 502 是网关在「连不上后端」时给的,应用日志干净恰恰说明请求没到,别在应用里找。
- 先看 ingress-nginx 的错误日志,
upstream后面的 IP 和端口能直接指出问题层级。 - Endpoints 为空九成是 Service 的 selector 和 Pod 标签对不上,
kubectl get pods -l <selector>一查就露。 port和targetPort是两回事,用命名端口时容器和 Service 的名字必须完全一致。- readiness 探针的
timeoutSeconds别设 1 秒,JVM 一次 GC 就能让它超时,/healthz也不要串上下游依赖。 - 把「selector → Pod → Endpoints」固化成一条诊断脚本,比每次凭经验猜快得多。
正在加载评论…