把管理台收回内网,执行器心跳全断了:前缀收敛的连带损伤
删掉一条 /tsp 的 path,管理台 404 是预期,四十分钟后定时任务全停。日志里 99% 的请求是心跳。前缀收敛的写法、割接顺序与验证矩阵。
安全整改清单里有一条:公网只保留业务必需的入口,管理台一律收回内网。清单上写着"调度中心",我把它翻译成一条 Ingress 的改动——把 /tsp 这条 path 删掉,只留内网入口。
改完管理台确实打不开了,404,符合预期。四十分钟后同事来问:今天的定时任务一个都没跑。
我第一反应是巧合,去看了任务日志,最后触发时间是改配置前十分钟。再看执行器列表,几十台全部"离线"。执行器和调度中心之间是执行器主动上报,上报地址就是那条被我删掉的路径。管理台不能登录只是表面,真正被打断的是心跳。
一、先确认"打不开"和"不工作"是两件事
管理台的 404 是我要的结果,所以看到 404 时我并没有去查。这是我这次最大的判断失误:同一条路径上跑着两种完全不同的流量,一种是人的点击,一种是机器的心跳。把整段路径关掉,等于把两种流量一起切断。
判断方法不复杂:看访问日志里的请求方法、路径、UA 和频率。人的点击是 GET、分布散、白天为主;心跳是 POST、路径固定、频率整齐。
# 从 ingress 访问日志里统计 /tsp 下的真实请求分布
kubectl -n ingress-nginx logs deploy/ingress-nginx-controller --since=24h \
| grep -o '"POST /tsp[^"]*"\|"GET /tsp[^"]*"' \
| sort | uniq -c | sort -rn | head -10
结果非常集中:
4088 "POST /tsp/api/registry HTTP/1.1"
17 "GET /tsp/auth/login HTTP/1.1"
6 "GET /tsp/jobinfo HTTP/1.1"
近六千行日志里,/tsp 下 99.4% 的请求是 POST /tsp/api/registry,其余是人工登录。也就是说,我完全可以只保留 /tsp/api,把管理面关掉,既满足整改要求,又不断心跳。
这个数据事后看是决定性的,但当时我没有先做这一步。顺序错了:先改,再查。正确的顺序是先查清"谁在用",再决定改哪一段。
二、前缀收敛时最容易切错的地方
把这条件改对,需要同时理解三件事:匹配类型、路径边界、上游的上下文路径。
| 写法 | 含义 | 对 /tsp/api/registry 的效果 |
|---|---|---|
path: /tsp(Prefix) |
前缀匹配 | 匹配,但管理台路径也一起放开 |
path: /tsp/api(Prefix) |
前缀匹配 | 匹配,管理台 404 |
path: /tsp/api/(带尾斜杠) |
前缀匹配 | 匹配 /tsp/api/registry,但 /tsp/api 本身不匹配,容易漏掉探测请求 |
path: /tsp/toLogin(Exact) |
精确匹配 | 不匹配,且会漏掉其他管理路径 |
我们最终用的是 Prefix /tsp/api。这里有个细节:Prefix 匹配是按路径段边界的,/tsp/api 不会匹配 /tsp/apixxx,但会匹配 /tsp/api/anything,所以不用担心误放开相邻路径。
上游这边还有一个坑:调度中心的应用上下文路径本身就是 /tsp,容器内监听 /tsp/*。如果我在 Ingress 上做 rewrite 剥掉前缀,后端就收不到 /tsp 开头,全部 404。所以这条规则必须"只匹配、不改写":
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: scheduler-api
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host: example.com
http:
paths:
- path: /tsp/api
pathType: Prefix
backend:
service:
name: scheduler-admin
port:
number: 9120
对比一下错误的写法,错在哪很直观:如果加了 nginx.ingress.kubernetes.io/rewrite-target: /$2 这类注解,就必须在 path 里写正则分组,而 Prefix 类型不接受正则,两者不能混用。混用的结果是规则不生效、返回 404,但 Ingress 对象状态是健康的。
三、割接顺序:先加后减
这次之所以出现四十分钟的空白,是因为我做了"删一条、留一条"的原子替换。正确的做法是先加新规则、观察心跳、再删旧规则:
# 1) 先建只放 /tsp/api 的新 Ingress,旧 /tsp 暂不动
kubectl apply -f ingress-scheduler-api.yaml
# 2) 观察心跳是否仍然进来(新旧规则同时生效时,请求仍会命中旧规则)
kubectl -n ingress-nginx logs deploy/ingress-nginx-controller --since=5m \
| grep -c 'POST /tsp/api/registry'
# 3) 心跳正常后,收掉旧规则里的 /tsp
kubectl -n $NS get ingress main-site -o yaml > /tmp/ingress-main-before.yaml # 先留回退件
kubectl -n $NS patch ingress main-site --type=json \
-p='[{"op":"remove","path":"/spec/rules/0/http/paths/8"}]'
第 2 步有个容易自欺的地方:新旧两条规则都在时,心跳命中的是哪一条,日志里看不出来。所以我改成"先把旧规则的 path 改成一个不会命中的值"再观察——这样能确认新规则真的在工作,而不是旧规则还在兜底。
回退件一定要在改之前留。我这次的回退是把 Ingress 的 path 从 /tsp/api 改回 /tsp,一条 patch 的事,但前提是有人把改前的 YAML 存下来了。
四、验证不能只看状态码
改完之后的验证我列了一张矩阵,逐条打勾:
for p in /tsp/auth/login /tsp/jobinfo /tsp/api/jobinfo /tsp/; do
printf '%-22s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://example.com$p)"
done
| 路径 | 期望 | 实际 | 说明 |
|---|---|---|---|
/tsp |
404 | 404 | 管理台入口关闭 |
/tsp/auth/login |
404 | 404 | 登录页不可达 |
/tsp/jobinfo |
404 | 404 | 任务列表不可达 |
/tsp/api/jobinfo |
200 | 200 | 接口层放行,返回 89 字节 JSON |
/tsp/api/registry |
200 | 200 | 心跳通道 |
最后一行的"200"需要换一种验证方式,因为它是 POST 且需要请求体,直接 curl 会返回参数错误。我用的是观察日志计数:grep -c 'POST /tsp/api/registry',每分钟稳定 17 次。这个数字比状态码更可靠——它证明的是"执行器在持续上报",而不是"网关愿意转发"。
顺带记一个假故障:/tsp/toLogin 返回 404 不是配置错。这个版本的调度中心真实登录页是 /tsp/auth/login,/tsp/toLogin 是应用自带的 404 模板页,状态码 404 但内容是一段 HTML。我一开始拿这个路径当验收项,白查了半小时。
五、为什么 IP 白名单方案走不通
整改方案里还有一个备选:保留 /tsp 公网可达,但只允许执行器所在网段的 IP。这个方案听起来更精确,实际不可行。
原因是来源 IP 在这一层已经被改写。公网流量先经过入口网关,网关做 SNAT 之后再转发进来,Ingress 看到的来源地址是网关的内部地址,不是真实客户端。日志里同一时刻的所有请求来源都是同一个内部地址,用它做白名单等于对所有人开放。
# 看日志里的来源地址分布
kubectl -n ingress-nginx logs deploy/ingress-nginx-controller --since=1h \
| grep 'POST /tsp/api/registry' | awk '{print $1}' | sort | uniq -c
输出只有两三个地址,且都是集群内部网段——这些是网关节点,不是执行器。想在应用层按 IP 限制,就得先把真实来源透传下来(X-Forwarded-For 一路保留),而这需要改入口网关、改 Ingress 配置、还要验证伪造风险,成本远高于"只放开 /tsp/api"。
六、改完之后补的一条规矩
这件事之后我给自己加了一条:任何"关闭某个入口"的变更,先统计该入口最近 24 小时的请求分布,按路径+方法分组,看有没有非人类的流量。 人的流量关掉是需求,机器的流量关掉是故障。
具体到操作上,就是改之前先跑一遍上面那条 grep | sort | uniq -c,把结果贴进变更单。这个动作三十秒,能挡住绝大部分"关一个入口、断一条链路"的事故。
小结
- 一条路径上可能同时跑着人的点击和机器的心跳,关入口前先按路径+方法统计请求分布。
- 前缀收敛用
Prefix /tsp/api,不要加尾斜杠,也不要用 rewrite 剥前缀——上游的上下文路径就是/tsp。 - 割接顺序是先加新规则、确认新规则真的在承载流量、再删旧规则,回退件在改之前留。
- 心跳类通道用日志计数验证(每分钟次数)比用状态码可靠,POST 接口直接 curl 会返回参数错误。
- 来源 IP 白名单在 SNAT 之后不可行,日志里的来源只有网关地址,别在这条路上耗时间。
正在加载评论…