Claude Code Recommended: 放弃
在我 k3s 集群上一个活生生的网络 bug 里,排查到第九个小时,Claude Code 给了我一个三选项的问题。第一个选项标着 (Recommended)。是放弃。
这个任务本身到这一步已经很平常了:一个十四步的计划,要把 PR 预览环境接到 Istio Ambient mesh 后面,用 superpowers 的 subagent-driven-development 一个任务一个任务地跑——每个任务一个 implementer subagent,一个 reviewer subagent 检查它的活,我负责在边界上点头。前面十三个任务都是这么过来的,每个都有自己的小仗要打(ArgoCD 的 self-heal 循环跟 istiod 的 webhook 配置打架、sync retry 次数耗尽、一个 waypoint proxy 悄悄要了它 namespace quota 十倍的 CPU),每次都干净利落地解决了,干净到我已经不再期待第十四个会不一样。Task 14 是端到端验证:开一个真实的 PR,发一个带 x-pr-lane: 1 的请求,确认它落到预览 pod 而不是 baseline pod 上。
它没有。不管带不带 header,每个请求都直接去了 baseline。两边都没有任何报错。
排除掉所有显而易见的可能
接下来的排查,从外面看根本不像在排查——看起来就是一长串"这不是问题所在"的清单。istio-cni 在 pod network namespace 里的 iptables REDIRECT 规则:确认在正确拦截出站包,nsenter 进去看过。ztunnel 的 xDS 配置:是对的,waypoint 绑定就摆在 Service 层级的 config dump 里。waypoint 本身:可达,路由已编程,就绪——但三个小时的日志里,一个连接都没接到过。namespace 级和 Service 级的 use-waypoint label 换着试:没变化。Istio 官方 issue 追踪器上一个症状相同的 issue:根因不同,是跨 namespace 的 ambient enrollment,跟这里对不上。宿主机侧的一次 tcpdump 抓到流量落在了 ztunnel 自己的 tunnel 端口上——有那么一阵子,这看起来像是证明了故障就在 ztunnel 自己的路由逻辑里,而不是在它上游——事后证明这个判断是错的,但在凌晨两点,错得并不明显。
排查到一半,凌晨 03:08,Claude Code 试了一个修复,看它失败了,直接说了实话:新抓到的包不只是没能验证之前的判断,反而推翻了它。情况比一小时前看起来的更复杂。它问要不要继续。我四十四分钟没回——我睡着了——回复的时候只写了一个词:继续。
如实汇报
五十分钟之后,04:01,它不再问了,开始汇报。这条消息对发生了什么说得很直接:它已经系统性地过了一遍,用它自己的话说,几乎所有已知、文档化的可能原因,逐一排除。是时候如实汇报了。然后是那个问题,三个选项:
1. 先收尾,把 waypoint L7 路由标记为已知限制(Recommended)
2. 去 Istio 官方渠道求助——短时间内不太可能拿到答案
3. 继续深挖——边际成功率已经在下降,可能就是无解
我问它具体卡在哪。它答得很干脆:这个功能真正的卖点——按请求 header 把一个实时 PR 预览通过 mesh 路由出去——它做不到。是想让它直接收尾标记为已知限制,还是先看看别的选项?
这段对话里没有一句是在演。它检查过的每一个假设,确实都排除了或者验证不了;这轮排除做得很仔细,不是敷衍。真正值得琢磨的不是那个默认选项被标成"Recommended"这件事本身有什么问题——而是:一次仔细、诚实地排除了眼下所有能看见的路径的过程,跟真正排除了所有存在的路径,从来就不是一回事。九个小时扎扎实实的工作,还是收敛到了错误的结论,原因也很普通——第一个小时锚定这次调查的东西,到第九个小时还在锚定它。
一个没有包袱的 subagent
我没有跟它争这个证据,也没有让它在同一条线上继续死磕。我让它开一个 Opus subagent,独立调查这件事。
我给它的指令,跟"开一个 subagent"这个决定本身一样重要:不要继承我对这件事的判断,也不要继承你自己的——如果你觉得上游某一步的验证方法本身做错了,不要照单全收,重新验证一遍。这次调度把前面九个小时提出、测试、标记为已排除或无法确认的每一个假设,整个证据链都交给了这个新 subagent,明确允许它不相信其中任何一个,并给了它谨慎的、可回退的活集群访问权限,让它自己去查。
二十八分钟后它带着答案回来了,而这个答案让人不太舒服,不舒服的地方很具体:第一个小时提出的某个假设,从一开始就是对的。只是它从来没有被真正验证过。
从没重启过的数据面
这个候选是 Cilium 自己的 socket-level load balancing。开了 kube-proxy replacement 之后,Cilium 的 eBPF 数据面会在 istio-cni 那条限定在 pod netns 里的 iptables REDIRECT 规则有机会把原始 ClusterIP 保留成连接的 SO_ORIGINAL_DST 之前,就先把 Service 的 ClusterIP 解析成了具体的 pod IP。等 ztunnel 检查这条被重定向的连接时,它看到的已经是一个 pod IP,不是 Service VIP——而 waypoint 绑定是按 Service 来 key 的,所以永远应用不上。Cilium 自己的 chart 就写着这种情况的修复方法:socketLB.hostNamespaceOnly: true,把这次早期解析限制在 host network namespace 里,pod namespace 里的流量(也就是 istio-cni 重定向的目标)不动。
这个值,其实在九个假设里的第二个那会儿,几小时前就已经设置过了。helm upgrade 跑出去了,cilium-config 这个 ConfigMap 更新了,而之后每一步验证查的就是这个 ConfigMap——因为 Kubernetes 的配置正常情况下就活在 ConfigMap 里,读回来正常情况下就足够当证据了。但这里不行。cilium-agent 只在自己启动的时候读一次这份配置,然后直接把它编译进自己跑的那个 cgroup BPF 程序里——不是控制器循环那种会定时重新读取的东西。而且这份 Helm chart 没有在 DaemonSet 的 pod template 上打任何 config-hash 注解,所以一次只改 values 的变更,会让这个 template 一字不差,完全不会触发任何 rollout。ConfigMap 说的是 true。编译好的数据面,还在跑它启动时那个进程,一直是 full——悄无声息地拒绝了它收到的每一个 waypoint 绑定,任何地方都没留下哪怕一条警告。
这个 subagent 给出的证据不是一个包装成自信的猜测——是三条独立的线索互相印证。cilium-dbg status --verbose 读出来是 Socket LB Coverage: Full,不是 ConfigMap 承诺的 Hostns-only。DaemonSet 自己的历史反过来说明了问题:引入这个设置的那次 Helm revision,比正在跑的 cilium-agent pod 自己的启动时间晚了整整十九个小时,重启次数还是零,controller-revision-hash 还指向一周前的 template。还有一次 live trace,cilium-dbg monitor -t trace-sock,抓到了这次 DNAT 实际发生的地方——Service VIP 被改写成 pod IP,就在发起请求的那个 pod 自己的 cgroup 里面,ztunnel 根本没机会看到原始目的地。
看清楚之后,修复只有一行:
kubectl -n kube-system rollout restart daemonset/cilium
之后 Socket LB Coverage 读出来就是 Hostns-only 了。折腾了一整晚都失败的 header 测试,第一次就路由对了。我回了一个字——做。几分钟后:终于成功了。
不是卡住了,是被锚住了
前九个小时错的不是那个假设。第二个假设从一开始就是对的。错的是把一次 ConfigMap 读取当成了变更已经生效的证据,然后再也没有回头重新检视这一条具体的证据——它已经被归档成"已验证生效"了。那之后的每一个小时,都建立在同一块地基上,再多的小时也不会让人注意到一块已经没人再去看的地基上有条裂缝。这和"不够聪明"或者"太快放弃"是两种不同的失败——这轮排除真的很仔细,一直仔细到它不再检查任何新东西为止。
一个独立的 subagent 解决了这个问题,靠的不是当一个更好的工程师,而是它还不知道哪块证据"应该算是已经定了"。被明确告知不要相信这次读取之后,它去检查了一件没人检查过第二次的事:这次配置变更到底有没有到达真正跑数据面的那个进程,还是只到达了 Kubernetes 存这份配置的那个对象。这两者之间的缝隙,正是这类 bug 藏身的地方——从一个已经决定好"验证过"和"还没验证"这条线画在哪里的调查内部,是看不见的。
小一点的那个修复当晚就落地了,在 values 文件里,给下一个改这个设置的人留了条注释:改动 socketLB 底下任何东西之后,重启 DaemonSet,然后检查真实的数据面,而不是 ConfigMap。另一个修复几个小时后落进了这个仓库自己的 CLAUDE.md 里,足够通用,能在这个具体 bug 之后继续管用——一条规则,禁止在提交之前直接对着一个活的、被 GitOps 管理的资源做改动去测试,因为 ArgoCD 的 self-heal 会把一次没提交的改动悄悄 revert 掉,不留下任何指向原因的线索。这两条其实是同一个教训的两身衣服:搞清楚自己到底在检查什么,别让一次早期的确认,把当初促使你去检查的那个问题给提前结案。
更多推荐



所有评论(0)