云原生网关高可用演进:基于 Kong DaemonSet 与 Local 流量亲和力的跨节点零绕路改造(含 K3s svclb 伪装源 IP 导致 Local 策略失效的深度复盘)
在跨云及混合云(Cloud & Homelab Edge)部署 Kubernetes (K3s) 集群的场景中,API 网关作为整个系统的流量入口,其部署架构与网络转发策略直接决定了服务的延迟、可用性与安全性。
本文分两部分记录对 Kong Ingress Controller(KIC)的高可用与网络链路改造:
- 第一阶段(基础改造):通过 GitOps 声明式配置完成从 Deployment 到 DaemonSet 的演进,开启
externalTrafficPolicy: Local与 L4 Stream 转发。 - 第二阶段(生产深水区排障与架构修正):上线两个月后,在发送 165k tokens(~500KB JSON Body)的 LLM 长上下文请求时,频繁遭遇 300 秒无输出超时。顺着容器日志与宿主机内核
iptables链计数器一路追查,拮出了第一阶段验证时的一个致命盲区——第一阶段用:31850(NodePort) 验证时绕过了svclb,而生产:80/:443流量经过 K3ssvclb容器时被MASQUERADE篡改源 IP 为 Pod 网段10.42.2.254,导致kube-proxy将公网入站流量误判为集群内部 Pod 流量,externalTrafficPolicy: Local命中率实际为 0%。第七节完整记录了这次从现象到内核规则链的排查、三种修复方案的取舍以及最终的 GitOps 落地校验。
一、 现状盘点与痛点分析
现有的 K3s 业务集群(tencent-dp1-cluster)由跨网络组网的异构节点构成,节点间通过 Tailscale 组网,Flannel VXLAN 跑在 tailscale0 网卡之上:
- 腾讯云节点 (
vm-0-2-debian):云端控制面及国内公网接入节点(公网 IP43.139.214.231DNAT 至网卡eth010.1.0.2,Tailscale IP100.77.64.95)。 - OCI ARM 节点 (
free-arm-vm):海外公有云高算力节点(4C24G ARM,公网 IP134.185.90.98DNAT 至网卡enp0s610.0.0.234,Tailscale IP100.105.130.0),承载 Higress AI Gateway、LiteLLM、Redis 等核心数据面服务。 - 本地 NUC 节点 (
nuc):Homelab x86 计算节点(局域网 IP10.0.1.113,Tailscale IP100.104.150.19)。 - 本地 Radxa 节点 (
radxa-cubie-a7a):Homelab ARM 边缘节点(局域网 IP10.0.1.105,Tailscale IP100.115.214.26),专职运行 CCTV 视频流采集与本地 Agent。
在最初的架构中,网关层面临三个结构性缺陷:
1. 控制面/数据面 Pod 存在单点故障与跨节点漫游
原有的 Kong Ingress Controller 采用 K8s Deployment 单副本部署在腾讯云主节点上。当 OCI 节点或 NUC 节点的入口收到外部请求时,流量必须通过 Flannel VXLAN 跨节点折返回腾讯云节点上的单点 Kong Pod 进行代理,带来显著的跨网延迟,且单 Pod 故障会导致全集群入口瘫痪。
2. externalTrafficPolicy: Cluster 导致的额外 SNAT 与源 IP 丢失
Kong Proxy Service 的默认流量策略为 Cluster。在该模式下,节点收到流量后会按全集群 Endpoints 随机转发到其他节点的 Kong Pod,并强制进行源地址转换(SNAT),不仅增加内部网络跳数,还导致后端服务无法获取客户端真实 IP。
3. 缺乏 L4 (TCP Stream) 传输能力
默认的 Kong 部署仅启用了 HTTP/HTTPS (80/443) 端口代理。对于 Redis (6379)、Kafka (9092) 等四层 TCP 流量,未开启 Nginx Stream 模块,缺乏 L4 TCP 透传能力。
二、 第一阶段:DaemonSet + Local 策略 + L4 Stream 基础改造
1. 改造前拓扑:单点集中 + 跨节点强制绕路
关于 K3s 端口映射的准确说明:
在 K3s 中,type: LoadBalancer的 Service 会同时涉及两层组件:
svclb容器(Klipper-LB DaemonSet):由 K3sservicelb控制器自动在kube-system下生成,通过 CNIhostPort接管宿主机的80 / 443 / 6379 / 9092端口,在svclb容器内部的独立 Network Namespace 中将流量 DNAT 并MASQUERADE到宿主机的 NodePort(31850 / 31324 / 30745 / 30818)。- 宿主机
kube-proxy(内核iptables):管理 NodePort(:31850等)、ClusterIP、ExternalIP 的KUBE-SERVICES/KUBE-NODEPORTS/KUBE-EXT/KUBE-SVL转发链,决定将数据包发往哪个节点的 Kong Pod。
2. 第一阶段的三项改造目标
所有改造通过修改 GitOps 仓库 nvd11/my-argocd-manifests 中的 argocd-apps/kong-controller-app.yaml 由 ArgoCD 自动同步:
- 目标 1:将 Controller 调整为 DaemonSet 模式
设置deployment.daemonset: true,摆脱replicas硬编码限制。Kong Chart 的deployment.yaml模板在daemonset: true时会渲染为DaemonSet并省略replicas字段。 - 目标 2:配置
externalTrafficPolicy: Local
在 Kong Proxy Service 中开启Local流量策略。注意部署顺序:必须先让 DaemonSet 在目标节点全量 Ready,再切换externalTrafficPolicy: Local,否则无本地端点的节点会直接丢弃流量。 - 目标 3:开启 Nginx Stream (TCP 6379 / 9092) 转发能力
在proxy.stream列表中声明containerPort与servicePort。切记不要手动设置env.nginx_stream_listen——Chart 的helpers.tpl中kong.streamListen会根据proxy.stream自动生成KONG_STREAM_LISTEN环境变量,手动注入nginx_stream_listen会被当成裸 nginx 指令写入nginx-kong-stream.conf,触发"listen" directive is not allowed here导致 PodCrashLoopBackOff。
3. 第一阶段的验证与当时留下的“假阳性”伏笔
第一阶段部署后,当时使用了对节点 NodePort :31850 直接发起 HTTP 请求的方式验证 externalTrafficPolicy: Local:
# 当时直接打 NodePort 31850 的测试命令:
$ curl http://100.105.130.0:31850/svc2
$ kubectl logs -n kong-system kong-ingress-controller-kong-rxn4r -c proxy | grep "GET /svc2"
100.115.214.26 - - [02/Aug/2026:17:54:31] "GET /svc2" 307
当时看到:打 OCI 的 :31850 只有 OCI 的 Kong 收到请求,且日志中保留了真实客户端 IP 100.115.214.26,便以为 externalTrafficPolicy: Local 已经全链路生效。
然而,这个测试有一个致命盲区:直接访问 :31850 (NodePort) 绕过了 K3s 的 svclb 容器,而生产域名流量走的是 :80 和 :443 端口!
三、 第二阶段:165k Token 请求 300s 超时引出的真相调查
1. 故障现象:为什么 165k Token 的 LLM 请求会卡死 5 分钟无输出?
2026 年 10 月,跑在家里 Radxa 节点上的 Agent (Yui) 通过公网域名 https://higress.jppwl.asia/v1/chat/completions(DNS A 记录指向 free-arm-vm 公网 IP 134.185.90.98:443)调用部署在 free-arm-vm 上的 Higress AI Gateway 与 higress-gemini-adapter。
当会话上下文累积到 165,047 tokens(HTTP POST Body 约 491 KB)时,Agent 日志连续两次报出 300 秒流超时,第三次重试却在 56 秒内秒回:
# Yui Agent 日志 (~/.hermes-yui/logs/agent.log)
13:29:13 -> 13:34:13 WARNING: Stream stale for 300s (threshold 300s) — no chunks received (attempt 1/3). Killing connection.
13:34:15 -> 13:39:15 WARNING: Stream stale for 300s (threshold 300s) — no chunks received (attempt 2/3). Killing connection.
13:39:21 -> 13:40:17 200 OK (56s, out=57 tokens)
一开始怀疑是 Higress 或上游 Gemini API 处理 165k tokens 太慢。但登上 free-arm-vm 核对 higress-gateway 和 higress-gemini-adapter 的容器日志后,发现了第一个反常事实:
2. 排查第一步:查 higress-gateway 与 kong 访问日志
在 free-arm-vm 上检查 higress-gateway 的 Access Log(/var/log/pods/higress-system_higress-gateway-*/higress-gateway/0.log):
{"start_time":"2026-10-17T13:39:31.896Z","method":"POST","path":"/v1/chat/completions","bytes_received":"491138","response_code":"200","duration":"45745","upstream_service_time":"15016","downstream_remote_address":"10.42.2.252:48106","x_forwarded_for":"10.42.2.254,10.42.2.252"}
两个关键线索直接暴露出来:
- **前两次卡死 300 秒的请求(
13:29:13和13:34:15),在higress-gateway和higress-gemini-adapter的日志里根本不存在!**而第三次成功到达 Higress 的请求,upstream_service_time只用了 15.016 秒。 - 看
higress-gateway其他时间段的日志,发现downstream_remote_address和x_forwarded_for里99% 的请求源 IP 全是10.42.2.254,而且转发给higress-gateway(10.42.2.182) 的上游 Kong Pod IP 竟然在三个节点之间来回跳:10.42.2.252(free-arm-vm本机的 Kong Pod)10.42.1.156(家里nuc节点上的 Kong Pod)10.42.0.23(腾讯云vm-0-2-debian节点上的 Kong Pod)
接着登上家里的 nuc (100.104.150.19) 查看 NUC 上 Kong Pod (10.42.1.156) 的日志,当场抓到了 13:29:14 那次失败请求的尸体:
# NUC 节点 (100.104.150.19) 上的 Kong proxy 日志:
2026/10/17 13:29:14 [warn] 1329#0: *84076 a client request body is buffered to a temporary file /kong_prefix/client_body_temp/0000000203, client: 10.42.2.254, server: kong, request: "POST /v1/chat/completions HTTP/1.1", host: "higress.jppwl.asia", request_id: "0a19450713a000051354af0762469675"
10.42.2.254 - - [17/Oct/2026:13:34:18 +0000] "POST /v1/chat/completions HTTP/1.1" 400 0 "-" "OpenAI/Python 2.24.0" kong_request_id: "0a19450713a000051354af0762469675"
300 秒超时的直接死因水落石出:
- Yui 把 491KB 的 POST 请求发到海外
free-arm-vm(134.185.90.98:443)。 free-arm-vm上的svclb(10.42.2.254) 接到请求后,没有转给free-arm-vm本机的 Kong Pod,而是通过 Flannel over Tailscale 隧道跨洋倒回了国内家里nuc节点上的 Kong Pod (10.42.1.156)!- 因为
Flannel (MTU 1230) over Tailscale (MTU 1280)跨洋传输 491KB 突发大包时发生 TCP 窗口/分片卡死,NUC 上的 Kong 只收到了前半截 Body,一直卡在a client request body is buffered to a temporary file等待客户端发完剩余 Body,根本没往下游higress-gateway转发。 - 直到 300 秒后 Yui 超时断开连接,NUC 上的 Kong 记录 HTTP
400错误。 - 第三次重试时,33% 的随机概率终于命中了
free-arm-vm本机的 Kong Pod (10.42.2.252),本机零跨网转给higress-gateway,15 秒就完成了推理!
3. 排查第二步:为什么 externalTrafficPolicy: Local 和禁用 NUC svclb 都没拦住跨节点转发?
这里有两个核心疑问:
-
我们之前已经通过节点标签
svccontroller.k3s.cattle.io/enablelb=false禁用了 NUC 和 Radxa 上的svclb,为什么流量还能发回 NUC 的 Kong Pod?- 因为
svccontroller.k3s.cattle.io/enablelb=false只是让 NUC 上不再运行svclb入口容器; - 但 Kong DaemonSet 并没有配置
nodeSelector,NUC 上依然跑着kong-ingress-controller-kongPod (10.42.1.156),并被注册在kong-proxyService 的全集群 EndpointSlice 池子里。
- 因为
-
明明
kong-proxyService 已经配置了externalTrafficPolicy: Local,为什么free-arm-vm上的iptables还会把流量按 33% 概率分给 NUC 和腾讯云?
为了查清这个问题,我们在 free-arm-vm 宿主机和 svclb 容器的 Network Namespace 中导出了完整的 iptables -t nat 规则与包计数器。
四、 根因解剖:K3s svclb 的 MASQUERADE 如何在底层架空 externalTrafficPolicy: Local
1. 先理清 Linux VM 宿主机上的三个物理/逻辑实体
在单台 K3s 节点(如 free-arm-vm)上,入站流量涉及三个互相独立的层级:
- 宿主机内核主
iptables实例(由 K3s 内置的kube-proxy和 CNI 维护):掌管宿主机物理网卡的PREROUTING入站转发,内含KUBE-SERVICES、CNI-HOSTPORT-DNAT、KUBE-NODEPORTS、KUBE-EXT-*、KUBE-SVC-*(全局随机链)和KUBE-SVL-*(Local 本地链)。 svclb容器(Pod IP10.42.2.254):拥有独立的 Network Namespace 和容器内独立的iptables实例。它不理解什么是externalTrafficPolicy,只负责把进入容器443端口的包 DNAT 并MASQUERADE到宿主机 NodePort100.105.130.0:31324。Kong Pod容器(Pod IP10.42.2.252):真正运行 Kong (Nginx) 进程的容器,监听8000(HTTP) 和8443(HTTPS)。
2. 跟踪一个 443 公网请求包在 free-arm-vm 上的真实流转路径
在 free-arm-vm 上查看宿主机 PREROUTING 链:
$ sudo iptables -t nat -L PREROUTING -n -v
Chain PREROUTING (policy ACCEPT)
pkts bytes target prot opt in out source destination
5727K 579M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* 第 1 关:kube-proxy Service 入口 */
3041K 359M CNI-HOSTPORT-DNAT all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL /* 第 2 关:svclb hostPort 入口 */
当客户端(源 IP 120.235.167.137)访问 https://higress.jppwl.asia (134.185.90.98:443) 时,OCI 云网关将目标 IP DNAT 为 free-arm-vm 网卡 enp0s6 的内网 IP 10.0.0.234:443 送入虚拟机:
① 第 1 关:宿主机 KUBE-SERVICES 链(因未识别 10.0.0.234 而漏过!)
因为 K3s 使用 --flannel-iface tailscale0 启动,K3s 只把节点的 Tailscale IP (100.105.130.0 和 100.77.64.95) 写进了 Service 的 status.loadBalancer.ingress。因此 KUBE-SERVICES 链里只有匹配 -d 100.105.130.0 的规则:
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 100.105.130.0 /* kong-proxy-tls loadbalancer IP */ tcp dpt:443
而公网请求包的目标 IP 是 10.0.0.234,在第 1 关 KUBE-SERVICES 没匹配上,直接漏到了第 2 关 CNI-HOSTPORT-DNAT!
② 第 2 关:掉进 svclb 容器,源 IP 被 MASQUERADE 篡改为 10.42.2.254
CNI-HOSTPORT-DNAT 不检查目标 IP,只要是 tcp dpt:443 就一律 DNAT 给 svclb 容器 (10.42.2.254:443)。
进入 svclb 容器的 Network Namespace 查看其内部 iptables:
$ sudo nsenter -t <svclb_pid> -n iptables -t nat -S
-A PREROUTING -p tcp -m tcp --dport 443 -j DNAT --to-destination 100.105.130.0:31324
-A POSTROUTING -d 100.105.130.0/32 -p tcp -j MASQUERADE
注意第二行:svclb 在把包转发给宿主机 NodePort 100.105.130.0:31324 时,执行了 -j MASQUERADE,把数据包的源 IP 从客户端真实 IP 120.235.167.137 改写成了 svclb 自己的 Pod IP 10.42.2.254!
③ 第 3 关:回到宿主机 NodePort 31324 (KUBE-EXT 链),被 -s 10.42.0.0/16 规则截胡!
当这个源 IP 已经变成 10.42.2.254 的包到达宿主机 31324 端口,进入 kube-proxy 生成的 KUBE-EXT-EIMG3TRSSUL5W4AV 链时,看当时实测的包计数器:
$ sudo iptables -t nat -L KUBE-EXT-EIMG3TRSSUL5W4AV -n -v
Chain KUBE-EXT-EIMG3TRSSUL5W4AV (4 references)
pkts bytes target prot opt in out source destination
129 7612 KUBE-SVC-EIMG3TRSSUL5W4AV all -- * * 10.42.0.0/16 0.0.0.0/0 /* Rule 1: pod traffic for kong-proxy-tls external destinations */
0 0 KUBE-MARK-MASQ all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match src-type LOCAL
0 0 KUBE-SVC-EIMG3TRSSUL5W4AV all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match src-type LOCAL
0 0 KUBE-SVL-EIMG3TRSSUL5W4AV all -- * * 0.0.0.0/0 0.0.0.0/0 /* Rule 4: externalTrafficPolicy: Local -> 本机 Kong Pod */
看第 1 条规则和第 4 条规则的命中数对比(129 pkts vs 0 pkts):
- Kubernetes
kube-proxy在生成externalTrafficPolicy: Local的入口链KUBE-EXT时,第 1 条规则写死了:如果源 IP 属于集群 Pod CIDR (-s 10.42.0.0/16),说明是集群内部 Pod 访问了 NodePort,直接跳进全局随机链KUBE-SVC! - 而 K3s 的
svclb容器自己的 IP 恰好是10.42.2.254(属于10.42.0.0/16)。 - 结果:所有经过
svclb(:80/:443) 进来的外部流量,因为被svclb换上了10.42.2.254的源 IP,100% 被KUBE-EXT的第 1 条规则当成“集群内部 Pod 流量”截胡,强行送进KUBE-SVC按 33% 概率跨网分发给腾讯云、NUC、OCI 三个 Kong Pod!第 4 条KUBE-SVL(Local 链) 永远是0 pkts!
下图完整还原了为什么第一阶段测 :31850 时命中 KUBE-SVL,而生产走 :443 时却被 svclb 送进 KUBE-SVC 跨洋绕路:
五、 修复方案分析与架构决策
1. 为什么不能靠配置 internalTrafficPolicy: Local 解决?
有人会问:既然 svclb 的源 IP 10.42.2.254 被当成了内部 Pod 流量,那把 Service 的 internalTrafficPolicy 也设为 Local 行不行?
答案是行不通,原因有两点:
internalTrafficPolicy只作用于ClusterIP(10.43.193.202),而svclb转发的目标是宿主机的NodePort (:31324)。在NodePort的KUBE-EXT链中,-s 10.42.0.0/16是硬编码跳向KUBE-SVC的,根本不受internalTrafficPolicy控制。- Chart 模板限制:检查 Kong Helm Chart
2.38.0的templates/_helpers.tpl源码,kong.service模板只渲染了externalTrafficPolicy和externalIPs,并未暴露internalTrafficPolicy字段。
2. 三种可行修复方案的对比
要让 :80 / :443 / :6379 / :9092 的入站流量 100% 命中本节点的 KUBE-SVL (Local 链),共有三种方案:
| 维度 | 方案 1:Service 配置 externalIPs (保留 svclb) | 方案 2:Kong 容器直绑 hostPort + ClusterIP (移除 svclb) | 方案 3:修改各节点 K3s systemd --node-external-ip |
|---|---|---|---|
| 核心原理 | 在 proxy.externalIPs 中显式声明各入口节点的网卡内网 IP 与 Tailscale IP,使入站包在 PREROUTING 第 1 关 (KUBE-SERVICES) 直接跳入 KUBE-EXT -> KUBE-SVL,不再漏入第 2 关的 svclb | 将 proxy.type 改为 ClusterIP 销毁 svclb,由 Kong DaemonSet 自身绑定 hostPort: 80/443/6379/9092 | 在每台宿主机的 /etc/rancher/k3s/config.yaml 中追加网卡内网 IP 到 node-external-ip |
是否保留 svclb | ✅ 保留 | ❌ 移除 | ✅ 保留 |
| 滚动更新影响 | ✅ 零闪断(不涉及 Pod hostPort 端口争用) | ⚠️ DaemonSet 滚动升级时需先杀旧 Pod 释放 hostPort 才能起新 Pod,有 ~10s 闪断 | ⚠️ 需逐台 SSH 改宿主机配置并重启 k3s-agent |
| GitOps 纯粹度 | ✅ 100% GitOps 声明式(单文件统一管控) | ✅ 100% GitOps 声明式 | ❌ 散落在各节点 /etc/ 目录下 |
| 真实源 IP 保留 | ✅ 100% 保留 | ✅ 100% 保留 | ✅ 100% 保留 |
最终决策:选择【方案 1】+ 节点亲和性治理(统一收口至 svccontroller.k3s.cattle.io/enablelb: "true")。
3. 节点拓扑优化:启用 NUC 作为内网网关入口,将 Radxa 彻底移出网关池
在排查过程中,我们还梳理了集群全部 4 个节点的网关职责:
free-arm-vm(OCI):海外公网入口(10.0.0.234)+ Tailscale 入口(100.105.130.0)。vm-0-2-debian(腾讯云):国内公网入口(10.1.0.2)+ Tailscale 入口(100.77.64.95)。nuc(家里 x86 节点):启用svccontroller.k3s.cattle.io/enablelb=true,专门作为家里局域网(10.0.1.113)与 Tailscale(100.104.150.19)的内网网关入口,供家里内网机器(Radxa、NixOS 笔记本、台式机等)走千兆局域网<1ms极速调用 K3s 服务。radxa-cubie-a7a(家里 ARM 边缘板):保持svccontroller.k3s.cattle.io/enablelb=false,并通过在 Kong DaemonSet 上配置nodeSelector: svccontroller.k3s.cattle.io/enablelb: "true",让 Kong DaemonSet 与svclb共用同一个节点标签选择器,自动将 Kong Pod 从 Radxa 节点撤出,彻底释放 Radxa 的内存与 CPU。
顺手解决的 Radxa 节点隐藏故障:
排查时发现 Radxa 节点上的kong和fluent-bitPod 之前一直处于CrashLoopBackOff(报dial tcp 10.43.0.1:443: i/o timeout),且宿主机6379端口还跑着一个 apt 装redis-tools时顺带拉起的空转redis-server.service。检查journalctl -u k3s-agent发现,全志 A733 定制内核5.15.147-8-a733未编译CONFIG_IP_NF_TARGET_REJECT和CONFIG_NFT_FIB_IPV4,导致kube-proxy执行iptables-restore在遇到-j REJECT时整批失败退出,KUBE-SERVICES链为空。通过在 Radxa 上卸载多余的redis-server并配置/usr/local/sbin/iptables-restore-strip-reject将-j REJECT无损转换为-j DROP,Radxa 的KUBE-SERVICES规则链与fluent-bit立即恢复1/1 Running。
六、 最终修复落地与端到端校验
1.修复后的完整目标拓扑
2. GitOps 声明式配置变更 (argocd-apps/kong-controller-app.yaml)
先为 nuc 节点开启 enablelb=true 标签:
$ kubectl label node nuc svccontroller.k3s.cattle.io/enablelb=true --overwrite
更新 nvd11/my-argocd-manifests 中的 argocd-apps/kong-controller-app.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kong-ingress-controller
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: default
source:
repoURL: 'https://charts.konghq.com'
chart: kong
targetRevision: 2.38.0
helm:
values: |
ingressController:
installCRDs: false
env:
database: "off"
# 注意: 不要手动加 nginx_stream_listen! proxy.stream 会自动生成 KONG_STREAM_LISTEN
gateway:
enabled: true
deployment:
# DaemonSet 模式: 每个启用了 LB 的节点 1 个 controller pod
daemonset: true
nodeSelector:
# 仅在启用了 svclb 的节点 (OCI, 腾讯云, NUC) 运行 Kong, 排除 Radxa
svccontroller.k3s.cattle.io/enablelb: "true"
proxy:
# 设置外部流量策略为 Local, 避免跨节点路由, 就近访问本节点的 Controller Pod
externalTrafficPolicy: Local
# 显式声明各节点的网卡内网 IP 与 Tailscale IP, 确保公网 DNAT, 局域网与 Tailscale 流量都在 PREROUTING 第一道关卡直接走本机 KUBE-SVL 本地链
externalIPs:
- 10.0.0.234 # OCI free-arm-vm 网卡内网 IP (公网 134.185.90.98 DNAT 落点)
- 100.105.130.0 # OCI free-arm-vm Tailscale IP
- 10.1.0.2 # 腾讯云 vm-0-2-debian 网卡内网 IP (公网 43.139.214.231 DNAT 落点)
- 100.77.64.95 # 腾讯云 vm-0-2-debian Tailscale IP
- 10.0.1.113 # 本地 NUC 局域网 IP (供家里内网机器直连)
- 100.104.150.19 # 本地 NUC Tailscale IP
# 开启 TCP stream 转发能力 (透传 TCP, 供 Redis/Kafka 等使用)
stream:
- containerPort: 6379
servicePort: 6379
- containerPort: 9092
servicePort: 9092
plugins:
configMaps:
- pluginName: custom-auth
name: kong-plugin-custom-auth
- pluginName: oauth2-forward-auth
name: kong-plugin-oauth2-forward-auth
destination:
name: 'tencent-dp1-cluster'
namespace: kong-system
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
3. 四项硬核校验
校验 1:确认 Pod 拓扑收敛至 3 个网关节点(Radxa 已自动退出)
$ kubectl get pods -n kong-system -o wide
NAME READY STATUS RESTARTS AGE IP NODE
kong-ingress-controller-kong-2h5jl 2/2 Running 0 3m3s 10.42.1.175 nuc
kong-ingress-controller-kong-f2r6g 2/2 Running 0 2m23s 10.42.0.27 vm-0-2-debian
kong-ingress-controller-kong-hqd2b 2/2 Running 0 95s 10.42.2.7 free-arm-vm
校验 2:确认 KUBE-SERVICES 已生成全部 6 个入口 IP 的第一顺位拦截规则
在 free-arm-vm 上检查 KUBE-SERVICES 链,10.0.0.234(OCI 网卡内网 IP)和 10.0.1.113(NUC 局域网 IP)均已出现在 CNI-HOSTPORT-DNAT (svclb) 之前:
$ sudo iptables -t nat -L KUBE-SERVICES -n -v | grep kong-proxy-tls
0 0 KUBE-SVC-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 10.43.193.202 /* kong-proxy-tls cluster IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 10.0.0.234 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 100.105.130.0 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 10.1.0.2 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 100.77.64.95 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 10.0.1.113 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 100.104.150.19 /* kong-proxy-tls external IP */ tcp dpt:443
校验 3:公网访问 https://higress.jppwl.asia(:443)验证 KUBE-SVL 计数器与真实源 IP
从家里 Radxa 发起公网 HTTPS 请求访问 https://higress.jppwl.asia/v1/models,然后查看 free-arm-vm 上的 KUBE-SVL 计数器与 Kong 访问日志:
$ sudo iptables -t nat -L KUBE-SVL-EIMG3TRSSUL5W4AV -n -v
Chain KUBE-SVL-EIMG3TRSSUL5W4AV (1 references)
pkts bytes target prot opt in out source destination
2 120 KUBE-SEP-SXNY44L5LDVPU3XI all -- * * 0.0.0.0/0 0.0.0.0/0 /* kong-proxy-tls -> 10.42.2.7:8443 */
# Kong proxy 访问日志(不再是 10.42.2.254,而是真实的客户端公网 IP 120.235.167.137!):
120.235.167.137 - - [17/Oct/2026:16:59:50 +0000] "GET /v1/models HTTP/2.0" 200 1099 "-" "curl/7.74.0" kong_request_id: "b6680251bdcc067a735e0b4e76873e41"
校验 4:家里内网机器通过局域网 10.0.1.113 直连 NUC Kong 验证
从家里 Radxa (10.0.1.105) 直接请求 NUC 局域网 IP http://10.0.1.113/svc2:
$ curl -s -o /dev/null -w "LAN 10.0.1.113 -> HTTP %{http_code}\n" http://10.0.1.113/svc2
LAN 10.0.1.113 -> HTTP 307
# NUC 节点上的 KUBE-SVL 本地链计数器立即 +1:
$ sudo iptables -t nat -L KUBE-SVL-ZGVTLPLWRODMG3FE -n -v
Chain KUBE-SVL-ZGVTLPLWRODMG3FE (1 references)
pkts bytes target prot opt in out source destination
1 60 KUBE-SEP-PS6KDOXBZVNQZ6VM all -- * * 0.0.0.0/0 0.0.0.0/0 /* kong-proxy -> 10.42.1.175:8000 */
七、 工程经验总结
- 在 K3s + 云厂商 NAT 网卡环境下,只配
externalTrafficPolicy: Local是不够的:- 云主机(如 OCI、腾讯云、AWS)公网 IP 是通过云硬件网关 DNAT 到虚拟机网卡私网 IP(如
10.0.0.234、10.1.0.2)的。 - 如果 K3s 使用
--flannel-iface tailscale0启动,Service 的 LoadBalancer IP 只包含 Tailscale IP(100.x.x.x),不包含网卡私网 IP。 - 这会导致公网入站流量在
PREROUTING第一道关卡KUBE-SERVICES漏匹配,掉进第二道关卡svclb容器被MASQUERADE成 Pod IP10.42.x.254,进而被KUBE-EXT的第 1 条规则(-s 10.42.0.0/16 -j KUBE-SVC)当成内部 Pod 流量全局随机分发,使externalTrafficPolicy: Local完全失效。 - 解法:在 Service 的
externalIPs中显式补齐各入口节点的网卡私网 IP、局域网 IP 与 Tailscale IP,让流量在KUBE-SERVICES第一顺位直接跳入KUBE-EXT -> KUBE-SVL。
- 云主机(如 OCI、腾讯云、AWS)公网 IP 是通过云硬件网关 DNAT 到虚拟机网卡私网 IP(如
- 测试验证必须走真实入口端口(
:80/:443),不能用 NodePort(:31850)代替:- 直接打
:31850绕过了svclb容器,测出来的本地亲和与源 IP 保留在:80/:443上可能完全是另一回事。 - 最可靠的验证指标不是看 HTTP 状态码,而是直接看宿主机
iptables -t nat -L KUBE-SVL-<hash> -n -v的包计数器是否增长,以及 Kong 访问日志中的客户端 IP 是否为真实外部 IP(而非10.42.x.254)。
- 直接打
svclb与 DaemonSet 网关应当共用同一套nodeSelector:- 只用
svccontroller.k3s.cattle.io/enablelb=false禁用边缘节点的svclb只能关掉入口,不能阻止该节点上的 Kong DaemonSet Pod 加入全局 EndpointSlice 分流池。给 Kong DaemonSet 配置相同的svccontroller.k3s.cattle.io/enablelb: "true"才能实现入口与网关实例的严格对齐。# 云原生网关高可用演进:基于 Kong DaemonSet 与 Local 流量亲和力的跨节点零绕路改造(含 K3s svclb 伪装源 IP 导致 Local 策略失效的深度复盘)
- 只用
在跨云及混合云(Cloud & Homelab Edge)部署 Kubernetes (K3s) 集群的场景中,API 网关作为整个系统的流量入口,其部署架构与网络转发策略直接决定了服务的延迟、可用性与安全性。
本文分两部分记录对 Kong Ingress Controller(KIC)的高可用与网络链路改造:
- 第一阶段(基础改造):通过 GitOps 声明式配置完成从 Deployment 到 DaemonSet 的演进,开启
externalTrafficPolicy: Local与 L4 Stream 转发。 - 第二阶段(生产深水区排障与架构修正):上线两个月后,在发送 165k tokens(~500KB JSON Body)的 LLM 长上下文请求时,频繁遭遇 300 秒无输出超时。顺着容器日志与宿主机内核
iptables链计数器一路追查,拮出了第一阶段验证时的一个致命盲区——第一阶段用:31850(NodePort) 验证时绕过了svclb,而生产:80/:443流量经过 K3ssvclb容器时被MASQUERADE篡改源 IP 为 Pod 网段10.42.2.254,导致kube-proxy将公网入站流量误判为集群内部 Pod 流量,externalTrafficPolicy: Local命中率实际为 0%。第七节完整记录了这次从现象到内核规则链的排查、三种修复方案的取舍以及最终的 GitOps 落地校验。
一、 现状盘点与痛点分析
现有的 K3s 业务集群(tencent-dp1-cluster)由跨网络组网的异构节点构成,节点间通过 Tailscale 组网,Flannel VXLAN 跑在 tailscale0 网卡之上:
- 腾讯云节点 (
vm-0-2-debian):云端控制面及国内公网接入节点(公网 IP43.139.214.231DNAT 至网卡eth010.1.0.2,Tailscale IP100.77.64.95)。 - OCI ARM 节点 (
free-arm-vm):海外公有云高算力节点(4C24G ARM,公网 IP134.185.90.98DNAT 至网卡enp0s610.0.0.234,Tailscale IP100.105.130.0),承载 Higress AI Gateway、LiteLLM、Redis 等核心数据面服务。 - 本地 NUC 节点 (
nuc):Homelab x86 计算节点(局域网 IP10.0.1.113,Tailscale IP100.104.150.19)。 - 本地 Radxa 节点 (
radxa-cubie-a7a):Homelab ARM 边缘节点(局域网 IP10.0.1.105,Tailscale IP100.115.214.26),专职运行 CCTV 视频流采集与本地 Agent。
在最初的架构中,网关层面临三个结构性缺陷:
1. 控制面/数据面 Pod 存在单点故障与跨节点漫游
原有的 Kong Ingress Controller 采用 K8s Deployment 单副本部署在腾讯云主节点上。当 OCI 节点或 NUC 节点的入口收到外部请求时,流量必须通过 Flannel VXLAN 跨节点折返回腾讯云节点上的单点 Kong Pod 进行代理,带来显著的跨网延迟,且单 Pod 故障会导致全集群入口瘫痪。
2. externalTrafficPolicy: Cluster 导致的额外 SNAT 与源 IP 丢失
Kong Proxy Service 的默认流量策略为 Cluster。在该模式下,节点收到流量后会按全集群 Endpoints 随机转发到其他节点的 Kong Pod,并强制进行源地址转换(SNAT),不仅增加内部网络跳数,还导致后端服务无法获取客户端真实 IP。
3. 缺乏 L4 (TCP Stream) 传输能力
默认的 Kong 部署仅启用了 HTTP/HTTPS (80/443) 端口代理。对于 Redis (6379)、Kafka (9092) 等四层 TCP 流量,未开启 Nginx Stream 模块,缺乏 L4 TCP 透传能力。
二、 第一阶段:DaemonSet + Local 策略 + L4 Stream 基础改造
1. 改造前拓扑:单点集中 + 跨节点强制绕路
关于 K3s 端口映射的准确说明:
在 K3s 中,type: LoadBalancer的 Service 会同时涉及两层组件:
svclb容器(Klipper-LB DaemonSet):由 K3sservicelb控制器自动在kube-system下生成,通过 CNIhostPort接管宿主机的80 / 443 / 6379 / 9092端口,在svclb容器内部的独立 Network Namespace 中将流量 DNAT 并MASQUERADE到宿主机的 NodePort(31850 / 31324 / 30745 / 30818)。- 宿主机
kube-proxy(内核iptables):管理 NodePort(:31850等)、ClusterIP、ExternalIP 的KUBE-SERVICES/KUBE-NODEPORTS/KUBE-EXT/KUBE-SVL转发链,决定将数据包发往哪个节点的 Kong Pod。
2. 第一阶段的三项改造目标
所有改造通过修改 GitOps 仓库 nvd11/my-argocd-manifests 中的 argocd-apps/kong-controller-app.yaml 由 ArgoCD 自动同步:
- 目标 1:将 Controller 调整为 DaemonSet 模式
设置deployment.daemonset: true,摆脱replicas硬编码限制。Kong Chart 的deployment.yaml模板在daemonset: true时会渲染为DaemonSet并省略replicas字段。 - 目标 2:配置
externalTrafficPolicy: Local
在 Kong Proxy Service 中开启Local流量策略。注意部署顺序:必须先让 DaemonSet 在目标节点全量 Ready,再切换externalTrafficPolicy: Local,否则无本地端点的节点会直接丢弃流量。 - 目标 3:开启 Nginx Stream (TCP 6379 / 9092) 转发能力
在proxy.stream列表中声明containerPort与servicePort。切记不要手动设置env.nginx_stream_listen——Chart 的helpers.tpl中kong.streamListen会根据proxy.stream自动生成KONG_STREAM_LISTEN环境变量,手动注入nginx_stream_listen会被当成裸 nginx 指令写入nginx-kong-stream.conf,触发"listen" directive is not allowed here导致 PodCrashLoopBackOff。
3. 第一阶段的验证与当时留下的“假阳性”伏笔
第一阶段部署后,当时使用了对节点 NodePort :31850 直接发起 HTTP 请求的方式验证 externalTrafficPolicy: Local:
# 当时直接打 NodePort 31850 的测试命令:
$ curl http://100.105.130.0:31850/svc2
$ kubectl logs -n kong-system kong-ingress-controller-kong-rxn4r -c proxy | grep "GET /svc2"
100.115.214.26 - - [02/Aug/2026:17:54:31] "GET /svc2" 307
当时看到:打 OCI 的 :31850 只有 OCI 的 Kong 收到请求,且日志中保留了真实客户端 IP 100.115.214.26,便以为 externalTrafficPolicy: Local 已经全链路生效。
然而,这个测试有一个致命盲区:直接访问 :31850 (NodePort) 绕过了 K3s 的 svclb 容器,而生产域名流量走的是 :80 和 :443 端口!
三、 第二阶段:165k Token 请求 300s 超时引出的真相调查
1. 故障现象:为什么 165k Token 的 LLM 请求会卡死 5 分钟无输出?
2026 年 10 月,跑在家里 Radxa 节点上的 Agent (Yui) 通过公网域名 https://higress.jppwl.asia/v1/chat/completions(DNS A 记录指向 free-arm-vm 公网 IP 134.185.90.98:443)调用部署在 free-arm-vm 上的 Higress AI Gateway 与 higress-gemini-adapter。
当会话上下文累积到 165,047 tokens(HTTP POST Body 约 491 KB)时,Agent 日志连续两次报出 300 秒流超时,第三次重试却在 56 秒内秒回:
# Yui Agent 日志 (~/.hermes-yui/logs/agent.log)
13:29:13 -> 13:34:13 WARNING: Stream stale for 300s (threshold 300s) — no chunks received (attempt 1/3). Killing connection.
13:34:15 -> 13:39:15 WARNING: Stream stale for 300s (threshold 300s) — no chunks received (attempt 2/3). Killing connection.
13:39:21 -> 13:40:17 200 OK (56s, out=57 tokens)
一开始怀疑是 Higress 或上游 Gemini API 处理 165k tokens 太慢。但登上 free-arm-vm 核对 higress-gateway 和 higress-gemini-adapter 的容器日志后,发现了第一个反常事实:
2. 排查第一步:查 higress-gateway 与 kong 访问日志
在 free-arm-vm 上检查 higress-gateway 的 Access Log(/var/log/pods/higress-system_higress-gateway-*/higress-gateway/0.log):
{"start_time":"2026-10-17T13:39:31.896Z","method":"POST","path":"/v1/chat/completions","bytes_received":"491138","response_code":"200","duration":"45745","upstream_service_time":"15016","downstream_remote_address":"10.42.2.252:48106","x_forwarded_for":"10.42.2.254,10.42.2.252"}
两个关键线索直接暴露出来:
- **前两次卡死 300 秒的请求(
13:29:13和13:34:15),在higress-gateway和higress-gemini-adapter的日志里根本不存在!**而第三次成功到达 Higress 的请求,upstream_service_time只用了 15.016 秒。 - 看
higress-gateway其他时间段的日志,发现downstream_remote_address和x_forwarded_for里99% 的请求源 IP 全是10.42.2.254,而且转发给higress-gateway(10.42.2.182) 的上游 Kong Pod IP 竟然在三个节点之间来回跳:10.42.2.252(free-arm-vm本机的 Kong Pod)10.42.1.156(家里nuc节点上的 Kong Pod)10.42.0.23(腾讯云vm-0-2-debian节点上的 Kong Pod)
接着登上家里的 nuc (100.104.150.19) 查看 NUC 上 Kong Pod (10.42.1.156) 的日志,当场抓到了 13:29:14 那次失败请求的尸体:
# NUC 节点 (100.104.150.19) 上的 Kong proxy 日志:
2026/10/17 13:29:14 [warn] 1329#0: *84076 a client request body is buffered to a temporary file /kong_prefix/client_body_temp/0000000203, client: 10.42.2.254, server: kong, request: "POST /v1/chat/completions HTTP/1.1", host: "higress.jppwl.asia", request_id: "0a19450713a000051354af0762469675"
10.42.2.254 - - [17/Oct/2026:13:34:18 +0000] "POST /v1/chat/completions HTTP/1.1" 400 0 "-" "OpenAI/Python 2.24.0" kong_request_id: "0a19450713a000051354af0762469675"
300 秒超时的直接死因水落石出:
- Yui 把 491KB 的 POST 请求发到海外
free-arm-vm(134.185.90.98:443)。 free-arm-vm上的svclb(10.42.2.254) 接到请求后,没有转给free-arm-vm本机的 Kong Pod,而是通过 Flannel over Tailscale 隧道跨洋倒回了国内家里nuc节点上的 Kong Pod (10.42.1.156)!- 因为
Flannel (MTU 1230) over Tailscale (MTU 1280)跨洋传输 491KB 突发大包时发生 TCP 窗口/分片卡死,NUC 上的 Kong 只收到了前半截 Body,一直卡在a client request body is buffered to a temporary file等待客户端发完剩余 Body,根本没往下游higress-gateway转发。 - 直到 300 秒后 Yui 超时断开连接,NUC 上的 Kong 记录 HTTP
400错误。 - 第三次重试时,33% 的随机概率终于命中了
free-arm-vm本机的 Kong Pod (10.42.2.252),本机零跨网转给higress-gateway,15 秒就完成了推理!
3. 排查第二步:为什么 externalTrafficPolicy: Local 和禁用 NUC svclb 都没拦住跨节点转发?
这里有两个核心疑问:
-
我们之前已经通过节点标签
svccontroller.k3s.cattle.io/enablelb=false禁用了 NUC 和 Radxa 上的svclb,为什么流量还能发回 NUC 的 Kong Pod?- 因为
svccontroller.k3s.cattle.io/enablelb=false只是让 NUC 上不再运行svclb入口容器; - 但 Kong DaemonSet 并没有配置
nodeSelector,NUC 上依然跑着kong-ingress-controller-kongPod (10.42.1.156),并被注册在kong-proxyService 的全集群 EndpointSlice 池子里。
- 因为
-
明明
kong-proxyService 已经配置了externalTrafficPolicy: Local,为什么free-arm-vm上的iptables还会把流量按 33% 概率分给 NUC 和腾讯云?
为了查清这个问题,我们在 free-arm-vm 宿主机和 svclb 容器的 Network Namespace 中导出了完整的 iptables -t nat 规则与包计数器。
四、 根因解剖:K3s svclb 的 MASQUERADE 如何在底层架空 externalTrafficPolicy: Local
1. 先理清 Linux VM 宿主机上的三个物理/逻辑实体
在单台 K3s 节点(如 free-arm-vm)上,入站流量涉及三个互相独立的层级:
- 宿主机内核主
iptables实例(由 K3s 内置的kube-proxy和 CNI 维护):掌管宿主机物理网卡的PREROUTING入站转发,内含KUBE-SERVICES、CNI-HOSTPORT-DNAT、KUBE-NODEPORTS、KUBE-EXT-*、KUBE-SVC-*(全局随机链)和KUBE-SVL-*(Local 本地链)。 svclb容器(Pod IP10.42.2.254):拥有独立的 Network Namespace 和容器内独立的iptables实例。它不理解什么是externalTrafficPolicy,只负责把进入容器443端口的包 DNAT 并MASQUERADE到宿主机 NodePort100.105.130.0:31324。Kong Pod容器(Pod IP10.42.2.252):真正运行 Kong (Nginx) 进程的容器,监听8000(HTTP) 和8443(HTTPS)。
2. 跟踪一个 443 公网请求包在 free-arm-vm 上的真实流转路径
在 free-arm-vm 上查看宿主机 PREROUTING 链:
$ sudo iptables -t nat -L PREROUTING -n -v
Chain PREROUTING (policy ACCEPT)
pkts bytes target prot opt in out source destination
5727K 579M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* 第 1 关:kube-proxy Service 入口 */
3041K 359M CNI-HOSTPORT-DNAT all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL /* 第 2 关:svclb hostPort 入口 */
当客户端(源 IP 120.235.167.137)访问 https://higress.jppwl.asia (134.185.90.98:443) 时,OCI 云网关将目标 IP DNAT 为 free-arm-vm 网卡 enp0s6 的内网 IP 10.0.0.234:443 送入虚拟机:
① 第 1 关:宿主机 KUBE-SERVICES 链(因未识别 10.0.0.234 而漏过!)
因为 K3s 使用 --flannel-iface tailscale0 启动,K3s 只把节点的 Tailscale IP (100.105.130.0 和 100.77.64.95) 写进了 Service 的 status.loadBalancer.ingress。因此 KUBE-SERVICES 链里只有匹配 -d 100.105.130.0 的规则:
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 100.105.130.0 /* kong-proxy-tls loadbalancer IP */ tcp dpt:443
而公网请求包的目标 IP 是 10.0.0.234,在第 1 关 KUBE-SERVICES 没匹配上,直接漏到了第 2 关 CNI-HOSTPORT-DNAT!
② 第 2 关:掉进 svclb 容器,源 IP 被 MASQUERADE 篡改为 10.42.2.254
CNI-HOSTPORT-DNAT 不检查目标 IP,只要是 tcp dpt:443 就一律 DNAT 给 svclb 容器 (10.42.2.254:443)。
进入 svclb 容器的 Network Namespace 查看其内部 iptables:
$ sudo nsenter -t <svclb_pid> -n iptables -t nat -S
-A PREROUTING -p tcp -m tcp --dport 443 -j DNAT --to-destination 100.105.130.0:31324
-A POSTROUTING -d 100.105.130.0/32 -p tcp -j MASQUERADE
注意第二行:svclb 在把包转发给宿主机 NodePort 100.105.130.0:31324 时,执行了 -j MASQUERADE,把数据包的源 IP 从客户端真实 IP 120.235.167.137 改写成了 svclb 自己的 Pod IP 10.42.2.254!
③ 第 3 关:回到宿主机 NodePort 31324 (KUBE-EXT 链),被 -s 10.42.0.0/16 规则截胡!
当这个源 IP 已经变成 10.42.2.254 的包到达宿主机 31324 端口,进入 kube-proxy 生成的 KUBE-EXT-EIMG3TRSSUL5W4AV 链时,看当时实测的包计数器:
$ sudo iptables -t nat -L KUBE-EXT-EIMG3TRSSUL5W4AV -n -v
Chain KUBE-EXT-EIMG3TRSSUL5W4AV (4 references)
pkts bytes target prot opt in out source destination
129 7612 KUBE-SVC-EIMG3TRSSUL5W4AV all -- * * 10.42.0.0/16 0.0.0.0/0 /* Rule 1: pod traffic for kong-proxy-tls external destinations */
0 0 KUBE-MARK-MASQ all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match src-type LOCAL
0 0 KUBE-SVC-EIMG3TRSSUL5W4AV all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match src-type LOCAL
0 0 KUBE-SVL-EIMG3TRSSUL5W4AV all -- * * 0.0.0.0/0 0.0.0.0/0 /* Rule 4: externalTrafficPolicy: Local -> 本机 Kong Pod */
看第 1 条规则和第 4 条规则的命中数对比(129 pkts vs 0 pkts):
- Kubernetes
kube-proxy在生成externalTrafficPolicy: Local的入口链KUBE-EXT时,第 1 条规则写死了:如果源 IP 属于集群 Pod CIDR (-s 10.42.0.0/16),说明是集群内部 Pod 访问了 NodePort,直接跳进全局随机链KUBE-SVC! - 而 K3s 的
svclb容器自己的 IP 恰好是10.42.2.254(属于10.42.0.0/16)。 - 结果:所有经过
svclb(:80/:443) 进来的外部流量,因为被svclb换上了10.42.2.254的源 IP,100% 被KUBE-EXT的第 1 条规则当成“集群内部 Pod 流量”截胡,强行送进KUBE-SVC按 33% 概率跨网分发给腾讯云、NUC、OCI 三个 Kong Pod!第 4 条KUBE-SVL(Local 链) 永远是0 pkts!
下图完整还原了为什么第一阶段测 :31850 时命中 KUBE-SVL,而生产走 :443 时却被 svclb 送进 KUBE-SVC 跨洋绕路:
五、 修复方案分析与架构决策
1. 为什么不能靠配置 internalTrafficPolicy: Local 解决?
有人会问:既然 svclb 的源 IP 10.42.2.254 被当成了内部 Pod 流量,那把 Service 的 internalTrafficPolicy 也设为 Local 行不行?
答案是行不通,原因有两点:
internalTrafficPolicy只作用于ClusterIP(10.43.193.202),而svclb转发的目标是宿主机的NodePort (:31324)。在NodePort的KUBE-EXT链中,-s 10.42.0.0/16是硬编码跳向KUBE-SVC的,根本不受internalTrafficPolicy控制。- Chart 模板限制:检查 Kong Helm Chart
2.38.0的templates/_helpers.tpl源码,kong.service模板只渲染了externalTrafficPolicy和externalIPs,并未暴露internalTrafficPolicy字段。
2. 三种可行修复方案的对比
要让 :80 / :443 / :6379 / :9092 的入站流量 100% 命中本节点的 KUBE-SVL (Local 链),共有三种方案:
| 维度 | 方案 1:Service 配置 externalIPs (保留 svclb) | 方案 2:Kong 容器直绑 hostPort + ClusterIP (移除 svclb) | 方案 3:修改各节点 K3s systemd --node-external-ip |
|---|---|---|---|
| 核心原理 | 在 proxy.externalIPs 中显式声明各入口节点的网卡内网 IP 与 Tailscale IP,使入站包在 PREROUTING 第 1 关 (KUBE-SERVICES) 直接跳入 KUBE-EXT -> KUBE-SVL,不再漏入第 2 关的 svclb | 将 proxy.type 改为 ClusterIP 销毁 svclb,由 Kong DaemonSet 自身绑定 hostPort: 80/443/6379/9092 | 在每台宿主机的 /etc/rancher/k3s/config.yaml 中追加网卡内网 IP 到 node-external-ip |
是否保留 svclb | ✅ 保留 | ❌ 移除 | ✅ 保留 |
| 滚动更新影响 | ✅ 零闪断(不涉及 Pod hostPort 端口争用) | ⚠️ DaemonSet 滚动升级时需先杀旧 Pod 释放 hostPort 才能起新 Pod,有 ~10s 闪断 | ⚠️ 需逐台 SSH 改宿主机配置并重启 k3s-agent |
| GitOps 纯粹度 | ✅ 100% GitOps 声明式(单文件统一管控) | ✅ 100% GitOps 声明式 | ❌ 散落在各节点 /etc/ 目录下 |
| 真实源 IP 保留 | ✅ 100% 保留 | ✅ 100% 保留 | ✅ 100% 保留 |
最终决策:选择【方案 1】+ 节点亲和性治理(统一收口至 svccontroller.k3s.cattle.io/enablelb: "true")。
3. 节点拓扑优化:启用 NUC 作为内网网关入口,将 Radxa 彻底移出网关池
在排查过程中,我们还梳理了集群全部 4 个节点的网关职责:
free-arm-vm(OCI):海外公网入口(10.0.0.234)+ Tailscale 入口(100.105.130.0)。vm-0-2-debian(腾讯云):国内公网入口(10.1.0.2)+ Tailscale 入口(100.77.64.95)。nuc(家里 x86 节点):启用svccontroller.k3s.cattle.io/enablelb=true,专门作为家里局域网(10.0.1.113)与 Tailscale(100.104.150.19)的内网网关入口,供家里内网机器(Radxa、NixOS 笔记本、台式机等)走千兆局域网<1ms极速调用 K3s 服务。radxa-cubie-a7a(家里 ARM 边缘板):保持svccontroller.k3s.cattle.io/enablelb=false,并通过在 Kong DaemonSet 上配置nodeSelector: svccontroller.k3s.cattle.io/enablelb: "true",让 Kong DaemonSet 与svclb共用同一个节点标签选择器,自动将 Kong Pod 从 Radxa 节点撤出,彻底释放 Radxa 的内存与 CPU。
顺手解决的 Radxa 节点隐藏故障:
排查时发现 Radxa 节点上的kong和fluent-bitPod 之前一直处于CrashLoopBackOff(报dial tcp 10.43.0.1:443: i/o timeout),且宿主机6379端口还跑着一个 apt 装redis-tools时顺带拉起的空转redis-server.service。检查journalctl -u k3s-agent发现,全志 A733 定制内核5.15.147-8-a733未编译CONFIG_IP_NF_TARGET_REJECT和CONFIG_NFT_FIB_IPV4,导致kube-proxy执行iptables-restore在遇到-j REJECT时整批失败退出,KUBE-SERVICES链为空。通过在 Radxa 上卸载多余的redis-server并配置/usr/local/sbin/iptables-restore-strip-reject将-j REJECT无损转换为-j DROP,Radxa 的KUBE-SERVICES规则链与fluent-bit立即恢复1/1 Running。
六、 最终修复落地与端到端校验
1.修复后的完整目标拓扑
2. GitOps 声明式配置变更 (argocd-apps/kong-controller-app.yaml)
先为 nuc 节点开启 enablelb=true 标签:
$ kubectl label node nuc svccontroller.k3s.cattle.io/enablelb=true --overwrite
更新 nvd11/my-argocd-manifests 中的 argocd-apps/kong-controller-app.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kong-ingress-controller
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: default
source:
repoURL: 'https://charts.konghq.com'
chart: kong
targetRevision: 2.38.0
helm:
values: |
ingressController:
installCRDs: false
env:
database: "off"
# 注意: 不要手动加 nginx_stream_listen! proxy.stream 会自动生成 KONG_STREAM_LISTEN
gateway:
enabled: true
deployment:
# DaemonSet 模式: 每个启用了 LB 的节点 1 个 controller pod
daemonset: true
nodeSelector:
# 仅在启用了 svclb 的节点 (OCI, 腾讯云, NUC) 运行 Kong, 排除 Radxa
svccontroller.k3s.cattle.io/enablelb: "true"
proxy:
# 设置外部流量策略为 Local, 避免跨节点路由, 就近访问本节点的 Controller Pod
externalTrafficPolicy: Local
# 显式声明各节点的网卡内网 IP 与 Tailscale IP, 确保公网 DNAT, 局域网与 Tailscale 流量都在 PREROUTING 第一道关卡直接走本机 KUBE-SVL 本地链
externalIPs:
- 10.0.0.234 # OCI free-arm-vm 网卡内网 IP (公网 134.185.90.98 DNAT 落点)
- 100.105.130.0 # OCI free-arm-vm Tailscale IP
- 10.1.0.2 # 腾讯云 vm-0-2-debian 网卡内网 IP (公网 43.139.214.231 DNAT 落点)
- 100.77.64.95 # 腾讯云 vm-0-2-debian Tailscale IP
- 10.0.1.113 # 本地 NUC 局域网 IP (供家里内网机器直连)
- 100.104.150.19 # 本地 NUC Tailscale IP
# 开启 TCP stream 转发能力 (透传 TCP, 供 Redis/Kafka 等使用)
stream:
- containerPort: 6379
servicePort: 6379
- containerPort: 9092
servicePort: 9092
plugins:
configMaps:
- pluginName: custom-auth
name: kong-plugin-custom-auth
- pluginName: oauth2-forward-auth
name: kong-plugin-oauth2-forward-auth
destination:
name: 'tencent-dp1-cluster'
namespace: kong-system
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
3. 四项硬核校验
校验 1:确认 Pod 拓扑收敛至 3 个网关节点(Radxa 已自动退出)
$ kubectl get pods -n kong-system -o wide
NAME READY STATUS RESTARTS AGE IP NODE
kong-ingress-controller-kong-2h5jl 2/2 Running 0 3m3s 10.42.1.175 nuc
kong-ingress-controller-kong-f2r6g 2/2 Running 0 2m23s 10.42.0.27 vm-0-2-debian
kong-ingress-controller-kong-hqd2b 2/2 Running 0 95s 10.42.2.7 free-arm-vm
校验 2:确认 KUBE-SERVICES 已生成全部 6 个入口 IP 的第一顺位拦截规则
在 free-arm-vm 上检查 KUBE-SERVICES 链,10.0.0.234(OCI 网卡内网 IP)和 10.0.1.113(NUC 局域网 IP)均已出现在 CNI-HOSTPORT-DNAT (svclb) 之前:
$ sudo iptables -t nat -L KUBE-SERVICES -n -v | grep kong-proxy-tls
0 0 KUBE-SVC-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 10.43.193.202 /* kong-proxy-tls cluster IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 10.0.0.234 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 100.105.130.0 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 10.1.0.2 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 100.77.64.95 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 10.0.1.113 /* kong-proxy-tls external IP */ tcp dpt:443
0 0 KUBE-EXT-EIMG3TRSSUL5W4AV tcp -- * * 0.0.0.0/0 100.104.150.19 /* kong-proxy-tls external IP */ tcp dpt:443
校验 3:公网访问 https://higress.jppwl.asia(:443)验证 KUBE-SVL 计数器与真实源 IP
从家里 Radxa 发起公网 HTTPS 请求访问 https://higress.jppwl.asia/v1/models,然后查看 free-arm-vm 上的 KUBE-SVL 计数器与 Kong 访问日志:
$ sudo iptables -t nat -L KUBE-SVL-EIMG3TRSSUL5W4AV -n -v
Chain KUBE-SVL-EIMG3TRSSUL5W4AV (1 references)
pkts bytes target prot opt in out source destination
2 120 KUBE-SEP-SXNY44L5LDVPU3XI all -- * * 0.0.0.0/0 0.0.0.0/0 /* kong-proxy-tls -> 10.42.2.7:8443 */
# Kong proxy 访问日志(不再是 10.42.2.254,而是真实的客户端公网 IP 120.235.167.137!):
120.235.167.137 - - [17/Oct/2026:16:59:50 +0000] "GET /v1/models HTTP/2.0" 200 1099 "-" "curl/7.74.0" kong_request_id: "b6680251bdcc067a735e0b4e76873e41"
校验 4:家里内网机器通过局域网 10.0.1.113 直连 NUC Kong 验证
从家里 Radxa (10.0.1.105) 直接请求 NUC 局域网 IP http://10.0.1.113/svc2:
$ curl -s -o /dev/null -w "LAN 10.0.1.113 -> HTTP %{http_code}\n" http://10.0.1.113/svc2
LAN 10.0.1.113 -> HTTP 307
# NUC 节点上的 KUBE-SVL 本地链计数器立即 +1:
$ sudo iptables -t nat -L KUBE-SVL-ZGVTLPLWRODMG3FE -n -v
Chain KUBE-SVL-ZGVTLPLWRODMG3FE (1 references)
pkts bytes target prot opt in out source destination
1 60 KUBE-SEP-PS6KDOXBZVNQZ6VM all -- * * 0.0.0.0/0 0.0.0.0/0 /* kong-proxy -> 10.42.1.175:8000 */
七、 工程经验总结
- 在 K3s + 云厂商 NAT 网卡环境下,只配
externalTrafficPolicy: Local是不够的:- 云主机(如 OCI、腾讯云、AWS)公网 IP 是通过云硬件网关 DNAT 到虚拟机网卡私网 IP(如
10.0.0.234、10.1.0.2)的。 - 如果 K3s 使用
--flannel-iface tailscale0启动,Service 的 LoadBalancer IP 只包含 Tailscale IP(100.x.x.x),不包含网卡私网 IP。 - 这会导致公网入站流量在
PREROUTING第一道关卡KUBE-SERVICES漏匹配,掉进第二道关卡svclb容器被MASQUERADE成 Pod IP10.42.x.254,进而被KUBE-EXT的第 1 条规则(-s 10.42.0.0/16 -j KUBE-SVC)当成内部 Pod 流量全局随机分发,使externalTrafficPolicy: Local完全失效。 - 解法:在 Service 的
externalIPs中显式补齐各入口节点的网卡私网 IP、局域网 IP 与 Tailscale IP,让流量在KUBE-SERVICES第一顺位直接跳入KUBE-EXT -> KUBE-SVL。
- 云主机(如 OCI、腾讯云、AWS)公网 IP 是通过云硬件网关 DNAT 到虚拟机网卡私网 IP(如
- 测试验证必须走真实入口端口(
:80/:443),不能用 NodePort(:31850)代替:- 直接打
:31850绕过了svclb容器,测出来的本地亲和与源 IP 保留在:80/:443上可能完全是另一回事。 - 最可靠的验证指标不是看 HTTP 状态码,而是直接看宿主机
iptables -t nat -L KUBE-SVL-<hash> -n -v的包计数器是否增长,以及 Kong 访问日志中的客户端 IP 是否为真实外部 IP(而非10.42.x.254)。
- 直接打
svclb与 DaemonSet 网关应当共用同一套nodeSelector:- 只用
svccontroller.k3s.cattle.io/enablelb=false禁用边缘节点的svclb只能关掉入口,不能阻止该节点上的 Kong DaemonSet Pod 加入全局 EndpointSlice 分流池。给 Kong DaemonSet 配置相同的svccontroller.k3s.cattle.io/enablelb: "true"才能实现入口与网关实例的严格对齐。
- 只用
更多推荐

所有评论(0)