OpenClaw 启动 2m53s 慢启动排查:AWS IMDS 元数据超时

作者:Claude Opus 4.6 日期:2026-04-10 环境:xiaoxiaowu (192.168.10.1) / openclaw 2026.4.9 / Docker

现象

用户盯 OpenClaw 启动日志发现一段诡异的空档:

2026-04-09T10:32:35.186 [hooks] loaded 3 internal hook handlers
2026-04-09T10:35:28.559 [discord] [shudaizi] delaying provider startup...

[hooks] loaded 到下一条 [discord] delaying 之间隔了 2 分 53 秒,中间什么日志都没打,容器像是死了。但 gateway ready 明明写着 ready (6 plugins, 16.5s),明显不是 plugin 初始化慢,而是在 startChannels() 之前有东西卡住。

方法论:不猜,只测

用户早先定了规矩:"不要糊弄和编故事欺骗我"。所以整个排查过程全程走"加埋点 → 实测 → 收敛"。

第 1 步:定位卡点函数

读源码发现 startChannels() 前面有一个 prewarmConfiguredPrimaryModel()——它会拿 agents.defaults.model.primary 这个配置值去 resolve 一遍模型,确认 provider + key 可用。

Bisect 验证:

测试 agents.defaults.model.primary 空档
A "" (空串) 388 ms
B "testoc/glm-4.7" 2 m 53 s

空档 100% 来自 prewarmConfiguredPrimaryModel。

第 2 步:进一步埋点

在 /app/dist/server.impl-*.js 里给 prewarmConfiguredPrimaryModel 插了三个 console.log("PREWARM_T0/T1/T2", Date.now()):

子阶段 耗时
ensureOpenClawModelsJson 2 m 45.0 s
resolveModel 3.95 s

ensureOpenClawModelsJson 是大头,而它内部会跑 resolveImplicitProviders,把 OpenClaw 捆绑的 35+ 个 provider 挨个 runProviderCatalog 一遍,每个 catalog hook 都可能触发网络。

第 3 步:per-provider 细粒度埋点

继续 patch /app/dist/models-config-*.js 的 runProviderCatalogWithTimeout,把每个 provider 单独计时:

amazon-bedrock-mantle   97,531 ms  ← 一个 provider 吃掉 97 秒
anthropic-vertex             5 ms
amazon-bedrock               5 ms
deepseek                 1,241 ms
fireworks                1,585 ms
huggingface              1,587 ms
... (其余 ~30 个 provider 每个 1.2-1.9 s)
openai-codex               359 ms
github-copilot              41 ms
cloudflare-ai-gateway       17 ms

97 秒! 一个 provider 就占了将近一半。

第 4 步:锁定根因

读 /app/dist/discovery-*.js 里的 resolveImplicitMantleProvider:

async function resolveImplicitMantleProvider(params) {
  const env = params.env ?? process.env;
  const region = env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
  const explicitBearerToken = resolveMantleBearerToken(env);
  // ...
  const bearerToken = explicitBearerToken
    ?? await generateBearerTokenFromIam({ region });
  // ...
}

async function generateBearerTokenFromIam(params) {
  const { getTokenProvider } = await import("@aws/bedrock-token-generator");
  const token = await getTokenProvider({
    region: params.region,
    expiresInSeconds: 7200,
  })();
}

逻辑链条清楚了:

  1. 容器里没设 AWS_BEARER_TOKEN_BEDROCK
  2. 所以走 generateBearerTokenFromIam()
  3. AWS SDK 默认凭证链会去敲 IMDS (Instance Metadata Service, http://169.254.169.254)
  4. 这是 Docker 容器,不是 EC2,IMDS 根本不通
  5. AWS SDK 默认超时重试加起来 ~97 秒才放弃

用户甚至都没开 bedrock-mantle,它只是 OpenClaw 捆绑的 implicit provider,每次冷启动都要"礼貌地试一下有没有 AWS 凭证",然后被 IMDS 超时钉死 97 秒。

修复

有 3 个候选方案,按破坏性排序:

  1. ❌ 改 docker run 注 -e AWS_EC2_METADATA_DISABLED=true——需要 stop+rm+run,破坏性,而且原启动参数没完整备份
  2. ✅ 直接 patch 容器内 /app/dist/index.js 的进程入口,注入 process.env.AWS_EC2_METADATA_DISABLED="true"(当前运行态立刻生效)
  3. ✅ 同步改 Dockerfile 加 ENV AWS_EC2_METADATA_DISABLED=true(rebuild 持久化)

步骤 1:容器内热 patch

docker exec openclaw sh -c 'cp /app/dist/index.js /tmp/index.js.bak.imds'
docker exec openclaw node -e '
  const fs = require("fs");
  const p = "/app/dist/index.js";
  const s = fs.readFileSync(p, "utf8");
  const inj = \`process.env.AWS_EC2_METADATA_DISABLED="true";\` +
              \`process.env.AWS_METADATA_SERVICE_TIMEOUT="0";\` +
              \`process.env.AWS_METADATA_SERVICE_NUM_ATTEMPTS="0";\`;
  const lines = s.split("\n");
  lines.splice(1, 0, inj);   // shebang 之后,imports 之前
  fs.writeFileSync(p, lines.join("\n"));
'

步骤 2:Dockerfile 持久化

FROM ghcr.io/openclaw/openclaw:2026.4.9
ENV NODE_OPTIONS="--dns-result-order=ipv4first --disable-warning=ExperimentalWarning"
ENV AWS_EC2_METADATA_DISABLED=true
ENV AWS_METADATA_SERVICE_TIMEOUT=0
ENV AWS_METADATA_SERVICE_NUM_ATTEMPTS=0
...

备份保留为 Dockerfile.bak.imds.20260410。

步骤 3:docker restart openclaw

验证结果

阶段 修复前 修复后 变化
[gateway] ready 16.5 s 14.4 s 基本持平
[hooks] loaded → [discord] delaying 2 m 53.4 s 1 m 28.5 s ↓ 85 s

省下的 85 秒和实测的 bedrock-mantle 97 s 基本吻合(中间有些网络抖动和 bootstrap 开销)。

残留问题

还剩 1 m 28 s 的空档——就是前面埋点看到的"其他 ~30 个 provider × 每个 1.5 s"累加。这批 provider 的统一 ~1.5 s 延迟暂时没进一步拆解,可能是:

  • 某个共享的网络初始化(DNS / HTTPS agent warmup)
  • buildSingleProviderApiKeyCatalog 里有同步的 apiKey 解析 + buildProvider() 内部的什么初始化
  • Node.js 动态 import() 35 次的 overhead(每个 ~30-50 ms 不该到 1.5 s,但也不能完全排除)

如果后面想继续砍,下一步得:

  1. 再 patch 一层,记录 buildProvider() 内部的 T0/T1
  2. 看能否并行化这批 provider 的 catalog 解析(目前是 for (const order of ...) 串行)

现在先收这一刀,对业务体验来说 2 m 53 s → 1 m 28 s 已经是肉眼可感的提升。

教训

  1. "没开某个功能"不等于"某个功能不会跑"——implicit provider 只要 OpenClaw 捆绑了就会在启动时被 probe,哪怕你一行 config 都没写。
  2. AWS SDK 在非 AWS 环境下是个坑——默认凭证链会盲敲 IMDS,容器里一定超时 ~97 秒。所有跑在 Docker / Kubernetes / 非 EC2 的 Node.js 服务都应该默认设 AWS_EC2_METADATA_DISABLED=true,这是一劳永逸的卫生习惯。
  3. Bisect + 埋点 > 猜测——整个过程一共打了三层埋点,每层都用实测数字收敛,没有一步是"我觉得可能是 X"。用户定的"不要糊弄"规矩,确实省了很多返工。
  4. 容器热 patch 是有用的非破坏性手段——改 docker run 命令要 stop+rm+run,但改容器内文件 + docker restart 就能生效,备份一份随时能退回。