Fission CI 故障排查模式:错误字符串速查、NetworkPolicy 机制与 kind-ci Helm 开关
Fission CI 故障排查模式:错误字符串速查、NetworkPolicy 机制与 kind-ci Helm 开关
导读
本文是一份面向 Fission(Kubernetes 上的 Serverless 函数平台)贡献者与 CI 维护者的故障排查速查手册,整理自仓库中沉淀的 debug-ci 经验:Fission CI 反复出现的少数几类故障模式,以及它们背后的 NetworkPolicy 与 CI-only Helm 开关机制。读完本文,你将能够:从一条 CI 错误字符串直接对应到根因与修复路径(而不是通读整份日志);理解 networkPolicy.enabled=true 的 kind-ci 环境里 pod 标签、命名空间选择器与 router 内部监听器白名单的运作细节;掌握通过 kind-ci skaffold profile 以两步 JSONPatch 模式开启 CI-only Helm 特性的标准方法。
一、先认清"已知红标":这些失败不用追查
Fission 仓库维护者在长期实践中确认了两类故意常红的检查项,遇到它们不要花时间排查:
| 已知红标 | 说明 |
|---|---|
| License Compliance(FOSSA) | 在 main 分支上持续失败,报 "11 issues found",属于策略性常红状态 |
| 需要本地 Docker daemon 的测试 | 例如 pkg/storagesvc/client 下的 TestS3StorageService——本地没有 dockerd 时的失败纯粹是环境问题,与代码无关 |
原则:匹配到错误字符串后再读完整日志。本文的结构正是为这一原则服务——先用速查表定位,再深入机制。
关联参考文档:fission-build-pipeline.md 与 fission-integration-test-quirks.md。
二、症状 → 根因速查表(Symptom → Cause)
2.1 网络 / 连通性
| 症状 | 根因 | 更多 |
|---|---|---|
dial tcp <ip>:<port>: i/o timeout(Fission pod → storagesvc) |
NetworkPolicy 丢弃了请求,或 pod 标签与策略选择器不匹配 | 见下文"NetworkPolicy" |
调用方 pod 日志中出现 dial tcp <router-internal-clusterIP>:8889: i/o timeout |
调用 router 内部监听器的 fission-bundle 头不在白名单里 | 见下文"Router internal-listener allowlist" |
portless: waiting for route "router.fission": ... not ready within 60s(或其他 *.fission 路由) |
进程内 port-forward 找不到 Service 背后 Ready 的 pod——部署不健康或 FISSION_NAMESPACE 错误。请检查 Service 的 pods(没有 kubectl forward 可查) |
fission-integration-test-quirks.md |
公共路由上 /fission-function/... 返回 404 |
请求打到了公共监听器;监听器拆分后这些路由已迁移到 router-internal.fission(请使用 f.Router(t) / f.RouterInternalBaseURL()) |
fission-integration-test-quirks.md |
raw.githubusercontent.com/... 返回 404 Not Found |
测试数据 URL 上游失效,不是回归 | — |
2.2 文件系统 / 权限
| 症状 | 根因 | 更多 |
|---|---|---|
/packages/... 上的 permission denied / openfdat …: permission denied |
共享卷上的跨容器 UID 不匹配;0o750 会阻断 fetcher 访问,需要 0o755 |
fission-build-pipeline.md |
no files found for globs: [/packages/<deploy>/*] |
Deploy 目录为空或 fetcher 无法读取 | fission-build-pipeline.md |
failed to get source directory info: stat /packages/<path>: no such file or directory |
构建脚本从未写入该路径;流水线在更早环节已中断 | fission-build-pipeline.md |
2.3 构建流水线
| 症状 | 根因 | 更多 |
|---|---|---|
package "<pkg>" never reached build status "succeeded" |
buildermgr → builder → fetcher 的某一步失败 | fission-build-pipeline.md |
Error uploading deployment package: ... |
fetcher 的 UploadHandler 失败——被包裹的错误才是真正原因 | fission-build-pipeline.md |
builder 日志中 bufio.Scanner: token too long |
构建脚本某行超过 64 KB(pip 进度条常触发) | fission-build-pipeline.md |
error: invalid buildCommand: contains shell metacharacter(HTTP 400) |
2026-05 之后 BuildCommand 白名单拒绝 shell 元字符;请把 && 之类改写进脚本 shebang 主体 |
pkg/builder/builder.go 的 resolveBuildCommand |
CreateEnv(Builder=...) 之后出现 dial tcp <env-svc>:8000: i/o timeout |
builder/runtime pod 就绪竞态 | fission-integration-test-quirks.md |
2.4 Lint / Go module
| 症状 | 根因 |
|---|---|
printf: non-constant format string in call to ... (govet) |
调用方把 fmt.Sprintf(...) 当作 printf 辅助函数的格式参数传入。应直接传 format, args...,或用 "%s", str 包裹动态字符串 |
github.com/docker/docker 报 invalid version: unknown revision v29.x.y |
Docker 用 docker-vX.Y.Z 前缀重新打了该 import 路径的 tag,代理未收录。将 docker/cli 升到 v29.x——其传递依赖 moby/moby/v2 会让 docker/docker 整体从 go.mod 消失 |
go: github.com/foo/bar@vX: invalid version |
module 路径迁移或被 retract。到上游 releases/latest 与真实 git tags 核对 |
三、网络类故障深入:NetworkPolicy
当 Fission pod 无法访问另一个 Fission 服务、症状是 dial tcp <ip>:<port>: i/o timeout 时,NetworkPolicy 是第一怀疑对象。
关键背景:networkPolicy.enabled=true 只出现在 kind-ci skaffold profile 中(skaffold.yaml 的 base setValues 中默认是 "false")。因此一个策略 bug 可以通过单测和 helm lint,只在集成测试环节暴露。
kubectl get networkpolicies -n fission
kubectl describe networkpolicy <name> -n fission
3.1 pod 标签才是唯一真相
from: [{ podSelector: ... }] 规则匹配的是 pod 标签,而不是 Service/Deployment 名字。控制器与其创建的 worker pod 使用不同的标签约定:
| Pod 类型 | 标签 |
|---|---|
buildermgr / executor 控制器 |
svc=buildermgr / svc=executor |
router 控制器 |
svc=router, application=fission-router |
storagesvc |
svc=storagesvc, application=fission-storage |
| 每个 env 的 builder pod(builder + fetcher sidecar) | owner=buildermgr, envName=<env>, envNamespace=<ns>, envResourceVersion=<rv> |
| Function pod | executorType=poolmgr|newdeploy|container, functionName=<name>, executorInstanceId=<id>, managed=<bool> |
关键规则:要选中 worker pod 里的 fetcher sidecar,选择器必须匹配 owner=buildermgr(env-builder pod)和 executorType in [...](function pod)——而不是 svc=*(控制器不直接访问 storagesvc)。
这些标签常量定义在源码中:
- 函数 pod 标签:
pkg/apis/core/v1/const.go(FUNCTION_NAME、EXECUTOR_TYPE等); - env-builder pod 标签:由
pkg/buildermgr/environment_reconciler.go的getLabels()生成,返回envName、envNamespace、envResourceVersion与owner=buildermgr(LABEL_DEPLOYMENT_OWNER: BUILDER_MGR),并有environment_reconciler_test.go中的用例佐证。
3.2 跨命名空间 vs 同命名空间选择器
storagesvc 位于 fission 命名空间;env-builder 与 function pod 位于用户命名空间(测试中为 default)。
podSelector 规则只匹配策略自身命名空间内的 pod,除非与 namespaceSelector 配对:
- from:
- namespaceSelector: {} # 匹配任何命名空间中带标签的 pod
podSelector:
matchLabels: { owner: buildermgr }
对于同命名空间规则(调用方与目标都在 fission 内),单独使用 podSelector 即可——它会隐式限定在策略所属命名空间内。
不要"为了显式"而加上 namespaceSelector.matchLabels.kubernetes.io/metadata.name: <ns>:该标签由某个 admission 插件自动填充,并非在所有 k8s 版本/发行版上都保证启用,插件关闭时规则会静默失效。
规范示例见 charts/fission-all/templates/networkpolicy.yaml 对应的各组件模板(如 charts/fission-all/templates/buildermgr/networkpolicy-builderpods.yaml、charts/fission-all/templates/executor/networkpolicy-functionpods.yaml)。
3.3 Router 内部监听器白名单:每个新内部客户端都要登记
charts/fission-all/templates/router/networkpolicy.yaml 将到内部监听器(端口 8889,提供 /fission-function/...)的入站流量显式限制在一组源 pod 标签白名单内:svc: kubewatcher | timer | mqtrigger | mqtrigger-keda | canaryconfig | executor | buildermgr | mcp(还有按条件启用的 svc: workflow 与 svc: router)。
任何新的调用内部监听器的 fission-bundle 头,都必须加入该 from 块,否则其签名请求会被静默丢弃——只在集成测试中表现为调用方 pod 日志(而非 router 的)里的 dial tcp <router-internal-clusterIP>:8889: i/o timeout。
这类是同命名空间规则,所以单独使用 podSelector 是正确的。
在 fission 命名空间内运行的调用方(例如 MCP)的 pod 日志位于 CI 的 kind-logs-<run>-<ver> 制品中,而不在 default 作用域的诊断转储里。
3.4 CNI 到底有没有在强制?
kindnet 从 kind v0.27 / k8s 1.30+ 开始强制实施 NetworkPolicy。在更老的 kind 或未装附加组件的 EKS 上,策略会被 API 接受但从不执行。
若策略看起来正确但行为不变:
kubectl get pods -n kube-system -o name | grep -E 'kindnet|cilium|calico|weave'
四、构建流水线故障深入
4.1 BuildCommand 的 shell 元字符白名单
速查表中 error: invalid buildCommand: contains shell metacharacter(HTTP 400)对应源码实现 pkg/builder/builder.go 的 resolveBuildCommand:
- 空字符串回退到
defaultBuildCommand(/build,见builder.go),不带参数; - 非空字符串按空白切分,第一个 token 为可执行文件,其余为参数;
- 输入中任何位置出现 shell 元字符即被拒绝——常量定义在
builder.go:";|&$()<>\n\r"`。
设计动机:os/exec 不会调用 shell,这些字符在任何合法构建命令中都不该出现;前置拒绝可以封死命令注入面(即使下游调用方将来用 sh -c 包裹)。resolveBuildCommand 的用例覆盖在 pkg/builder/builder_test.go(含空串回退与元字符拒绝)。
实战改写方式:把原本写在 BuildCommand 里的 &&、; 等 shell 语法下沉到构建脚本的 shebang 主体中(例如把命令序列写进 build.sh 的 #!/bin/sh 之后),BuildCommand 只保留脚本路径与简单参数。
4.2 /packages 共享卷权限模型
env-builder pod 是两个容器共享 /packages 这个 emptyDir:
builder——env 镜像(如python-builder),通常是 root(UID 0);fetcher——Fission 的 sidecar,UID 10001(values.yaml的podSpec.securityContext.runAsUser)。
构建脚本(builder 侧)写入部署产物,fetcher 再读回上传。陷阱:父目录若被创建为 0o750(rwxr-x---),属主是 root,fetcher(UID 10001、不同组)无法遍历,产生三类症状(权限拒绝、stat 目录不存在、glob 找不到文件——最后一种是因为 fetcher 能 stat 目录但列不出内容)。
修复:/packages 根下的目录必须世界可读——目录用 0o755(文件继承 umask,0o644 即可;只有目录遍历位关键)。该模式由 pkg/utils/zip.go 的 Unarchive(os.MkdirAll(..., os.ModeDir|0o755))强制,构建脚本的 cp -r ${SRC_PKG} ${DEPLOY_PKG} 会保留权限,因此 Unarchive 的模式会传导下去。
判断路径是否被 fetcher sidecar 可达的通用法则:
/packages/...上 → 目录必须0o755;- 用户本地文件系统(CLI 工具:
dump.go、package util、support resources)→0o700/0o600即可; - 由节点级日志采集读取的日志卷 →
0o755。
为什么不把两个容器跑成同一 UID:env-builder 镜像在上游预构建,许多以 root 身份安装依赖;强制非 root 会破坏用户环境。控制面 pod 以 UID 10001 运行以缩小爆炸半径,于是 UID 不对称是既定事实,只能靠世界可读目录。单测无法模拟 UID 隔离,这一层只能由集成套件把关。
4.3 日志分别在哪里
下载的 go-integration-logs-* 制品中,每个测试的诊断目录包含:
| 文件 | 来源 |
|---|---|
logs-<env>-<rv>-<hash>-builder.log |
builder 容器——env 镜像里的 /builder 二进制,记录 ===== START ===== / ===== END ===== 标记之间的构建脚本输出 |
logs-<env>-<rv>-<hash>-fetcher.log |
env-builder pod 的 fetcher sidecar,处理取包(输入)与上传(输出) |
logs-poolmgr-<fn>-*-fetcher.log / logs-newdeploy-<fn>-*-fetcher.log |
function pod 的 fetcher sidecar,只在特化(specialise)时下载,从不上传 |
logs-poolmgr-<fn>-*-<env-runtime>.log |
用户函数的运行时容器 |
控制器日志(buildermgr、executor、router、storagesvc)来自 kind-logs-<runId>-v<k8s> 制品下 fission/ 命名空间的 pod 日志——只有怀疑是 step-1 控制器问题时才下载。
阅读 builder 日志:成功路径为 builder received request → building source package → START → 脚本输出 → END → build request complete。失败形态:error starting cmd(exec.Command 启动前就失败——脚本权限/PATH 问题)、error waiting for cmd(进程非零退出,读 START/END 之间)、没有 END 标记(捕获被截断:bufio.Scanner 撞上 64 KB 行上限,或 pod 在构建中途被杀)。
4.4 哪个二进制实际在跑(env 镜像不随 PR 重建)
二进制有两种进入集群的方式:
- 控制面镜像——
make skaffold-deploy(goreleaser)按 PR 构建:fission-bundle(多头:buildermgr、executor、router、storagesvc、kubewatcher、timer、mqtrigger、canary、webhook 等)、fetcher、pre-upgrade-checks、reporter(见skaffold.yaml的 artifacts 列表); - 环境镜像——由 env 维护者预先构建并发布到 GHCR,独立于本仓库:
python-builder、node-builder-22、go-builder-1.23等,每个都是<语言基础镜像> + 内嵌 /builder 二进制 + 构建脚本。
要点:这些镜像里的 /builder 二进制是在镜像构建推送时编译的,不是来自当前 PR 的源码。 因此修改 pkg/builder/builder.go 不会改变 CI 集成测试中 env-builder pod 的行为,直到镜像被重新发布。若集成测试表现出旧行为而单测通过、lint 干净,几乎可以断定是这个原因——用 builder pod 日志中的 caller":"builder/builder.go:NN" 字段与本地行号对照确认。
pkg/builder 的 Go 测试(builder_test.go)跑在 make test-run(隔离的包逻辑),不在集成任务中。真正在集群里验证 cmd/builder/pkg/builder 改动的途径:写一个避开 env 镜像的 Go 单测/集成测试;用打过补丁的二进制构建本地 env-builder 镜像(Dockerfile 在独立的 fission/environments 仓库)并在测试 Environment CR 中覆盖 Builder.Image;或把行为搬进 pkg/buildermgr(控制器在 fission-bundle 内,随 PR 重建)。
五、CI-only Helm 特性:kind-ci skaffold profile 两步模式
有些 chart 特性对用户默认关闭,但 CI 应该开启以覆盖代码路径。仓库通过 kind-ci profile 翻转它们(完整 patch 列表见 skaffold.yaml):
- base
setValues中声明但默认关闭:canaryDeployment.enabled: "false"、podMonitor.enabled: "false"、serviceMonitor.enabled: "false"、networkPolicy.enabled: "false"(2026-05)、storagesvc.archivePruner.interval: "60"、mcp.enabled: "true"+mcp.allowInsecure: "true"(2026-06,写在 basesetValues中,使 MCP 头在每个 kind profile 都运行); kind-ciprofile 中通过 JSONPatch 翻转为开:canaryDeployment.enabled: true、podMonitor.enabled: true、serviceMonitor.enabled: true、storagesvc.archivePruner.interval: 1、networkPolicy.enabled: true等。
两步标准模式(skaffold 的 JSONPatch replace 操作要求路径已存在):
- 在 base
manifests.helm.releases[0].setValues映射里以 chart 默认值声明该值(例如networkPolicy.enabled: "false"); - 在
kind-ciprofile 中添加replacepatch:
- op: replace
path: /manifests/helm/releases/0/setValues/networkPolicy.enabled
value: true
跳过第 1 步会导致渲染失败:报 path does not exist。路径使用点分隔的键段,因为 base 映射就是这样组织的。若该特性也需要在旧版本上测试,同步镜像到 kind-ci-old profile。
验证渲染:
helm lint charts/fission-all
helm template charts/fission-all --set networkPolicy.enabled=true | sed -n '/^kind: NetworkPolicy/,/^---/p' | head -50
helm template charts/fission-all --set networkPolicy.enabled=false | grep -c 'kind: NetworkPolicy' # 0
如果该特性对用户可调,还要把它加进 charts/fission-all/values.yaml;如果是纯内部 CI 旋钮,skaffold patch 就足够了。
六、附:集成测试快速排查清单
以下清单摘自配套文档 fission-integration-test-quirks.md,配合本文速查表使用:
- 单独重跑——
go test -tags=integration -run TestFooBar -v -count=1 ./test/integration/suites/common/...。单独通过而套件中失败 ⇒ 并行负载问题(更多 pod、更多 kube-proxy 延迟); - 读诊断转储——
$LOG_DIR/<sanitized-test-name>/下有events.yaml、pods.yaml、容器日志、CR yaml。caller":"…"字段告诉你实际跑的是哪个二进制版本; - 构建失败查
Status.BuildLog——记录在packages.yaml以及WaitForPackageBuildSucceeded的失败消息里; - 逐级排查分层竞态修复——builder-pod 测试出现
dial tcp …:8000: i/o timeout,多半需要更长的 settle 时间,而不是新机制(CreateEnv内置的就绪等待链包含WaitForBuilderReady、waitForRuntimePoolReady、EndpointSlice 就绪 + 5 秒 settle,以及WaitForPackageBuildSucceeded对瞬时超时的 3 次重试,见pkg/buildermgr/common.go附近的实现); - FIXME 下跳过的测试——
TestIdleObjectsReaper当前被跳过,不解决并行 CI 的 fsvc-TTL 刷新问题(commit0fc53807)前不要重新启用。
七、核心要点回顾
- 先匹配错误字符串,再读完整日志——本文速查表覆盖了 Fission CI 反复出现的全部主要故障模式;
dial tcp …: i/o timeout优先怀疑 NetworkPolicy,而策略只对 pod 标签生效;kind-ci环境里网络策略是开启的,单测/helm lint拦不住策略 bug;- router 内部监听器(8889)采用显式白名单,新增的 fission-bundle 内部客户端必须同步登记到
charts/fission-all/templates/router/networkpolicy.yaml的from块; /packages共享卷目录必须0o755,否则 UID 10001 的 fetcher 无法读取构建产物;- env 镜像不随 PR 重建——改
pkg/builder/builder.go不会立即改变集成测试中的 builder 行为,核对日志caller行号是确认手段; - CI-only Helm 特性走"base 声明默认值 + kind-ci JSONPatch replace"两步模式,路径缺失会导致渲染失败。