perf(self-hosted): acr update 共享一次 install 取代 23 個 worker 各裝 324MB

壓測 2026-06-12 真因:每個 worker 各 pnpm install ~324MB node_modules(23× 重複,
全是 hono+wrangler)→ 好幾分鐘。改成 tarball root 裝一次(hono+wrangler+tier2 額外
zod/mcp-sdk/yaml),各 worker 靠 node 往上 resolve(dry-run 驗證 tier1+tier2 都 bundle 成功)。
207MB×1 取代 324MB×23。疊加 manifest 跳過 → 第二次 update 近乎瞬間。
共享失敗自動退回各 worker 自裝(不破壞既有路徑)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-13 14:45:30 +08:00
parent f336fb2fca
commit b778086f4a
+38 -6
View File
@@ -153,6 +153,35 @@ export async function downloadAndDeploy(
return { implemented: true, message: `部署物中找不到任何 wrangler.tomlroot=${root})。` }; return { implemented: true, message: `部署物中找不到任何 wrangler.tomlroot=${root})。` };
} }
// 2.5 共享依賴:23 個 component worker 的 runtime dep 全是 hono、devDep 全含 wrangler
// 舊版每個 worker 各 install 一份 ~324MB node_modules23× 重複,壓測 2026-06-12 慢的真因)。
// 改成在 tarball root 裝「一次」hono+wranglercomponent 目錄靠 node 往上 resolve(已驗證可行)。
// → 23×4.4s install 變 1×17s。失敗不致命:退回各 worker 自裝(runWranglerDeploy 仍有 fallback)。
let sharedBin = '';
try {
process.stdout.write(chalk.gray(' → 安裝共享部署依賴(一次,取代每個 worker 各裝)...'));
// 含全部 worker 的 runtime depstier1 component 只要 honotier2 cypher/registry/mcp/kbdb
// 另需 zod / @hono/zod-openapi / @modelcontextprotocol/sdk / js-yaml / yaml)→ 全裝 root
// 各 worker 往上 resolveesbuild bundle 找得到。漏一個會讓該 worker deploy 失敗,故寧可多列。
writeFileSync(
join(root, 'package.json'),
JSON.stringify({ name: 'arcrun-deploy-shared', private: true, type: 'module',
dependencies: {
hono: '^4.7.0', wrangler: '^4.0.0', zod: '^3.23.0',
'@hono/zod-openapi': '^0.18.0', '@modelcontextprotocol/sdk': '^1.0.0',
'js-yaml': '^4.1.0', yaml: '^2.4.0',
} }),
);
execFileSync('npm', ['install', '--no-audit', '--no-fund'],
{ cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
sharedBin = join(root, 'node_modules', '.bin', 'wrangler');
console.log(existsSync(sharedBin) ? chalk.green(' ✓') : chalk.yellow(' ⚠ 退回各 worker 自裝'));
if (!existsSync(sharedBin)) sharedBin = '';
} catch (e) {
const tail = (e as { stderr?: Buffer }).stderr?.toString().trim().split('\n').slice(-2).join(' | ').slice(0, 200) ?? '';
console.log(chalk.yellow(` ⚠ 共享安裝失敗,退回各 worker 自裝${tail ? `${tail}` : ''}`));
}
// 3. 對每個 worker:注入 KV id+ cypher WORKER_SUBDOMAIN)→ wrangler deploy。tier1 先 tier2 後。 // 3. 對每個 worker:注入 KV id+ cypher WORKER_SUBDOMAIN)→ wrangler deploy。tier1 先 tier2 後。
// 逐 worker 串流進度(每個含 pnpm install + wrangler deploy,沉默會讓人以為卡住—— // 逐 worker 串流進度(每個含 pnpm install + wrangler deploy,沉默會讓人以為卡住——
// 壓測 2026-06-11 richblack 觀察:「D1 ✓」後停很久其實在這個迴圈靜默部署 20+ worker)。 // 壓測 2026-06-11 richblack 觀察:「D1 ✓」後停很久其實在這個迴圈靜默部署 20+ worker)。
@@ -178,7 +207,7 @@ export async function downloadAndDeploy(
console.log(chalk.gray(' ⊘ 未變動,跳過')); console.log(chalk.gray(' ⊘ 未變動,跳過'));
continue; continue;
} }
runWranglerDeploy(dir, ctx); runWranglerDeploy(dir, ctx, sharedBin);
manifest[label] = hash; // 只在成功後記錄 → 失敗者下次必重試 manifest[label] = hash; // 只在成功後記錄 → 失敗者下次必重試
saveManifest(manifest); saveManifest(manifest);
deployed++; deployed++;
@@ -400,16 +429,19 @@ export function stripOfficialOnlyBindings(toml: string): string {
return out.join('\n'); return out.join('\n');
} }
/** 在 worker 目錄跑 wrangler deploy(用用戶的 CF token + account)。*/ /** 在 worker 目錄跑 wrangler deploy(用用戶的 CF token + account)。
function runWranglerDeploy(dir: string, ctx: DeployContext): void { * sharedBinroot 共享 wrangler binary 路徑(見 downloadAndDeploy 2.5)。有則用它且**跳過本地 install**
// 先裝依賴(cypher-executor/registry 是 TSwrangler 內建 esbuild bundle 需 node_modules * deps 從 root node_modules 往上 resolve);空字串則退回舊行為(各 worker 自裝)。*/
if (existsSync(join(dir, 'package.json'))) { function runWranglerDeploy(dir: string, ctx: DeployContext, sharedBin = ''): void {
if (!sharedBin && existsSync(join(dir, 'package.json'))) {
// fallback:共享安裝失敗時才走這條,各 worker 自裝
const installer = existsSync(join(dir, 'pnpm-lock.yaml')) const installer = existsSync(join(dir, 'pnpm-lock.yaml'))
? ['pnpm', 'install', '--frozen-lockfile'] ? ['pnpm', 'install', '--frozen-lockfile']
: ['npm', 'install', '--no-audit', '--no-fund']; : ['npm', 'install', '--no-audit', '--no-fund'];
runStep(installer[0], installer.slice(1), dir, process.env); runStep(installer[0], installer.slice(1), dir, process.env);
} }
runStep('wrangler', ['deploy'], dir, { const wranglerCmd = sharedBin || 'wrangler';
runStep(wranglerCmd, ['deploy'], dir, {
...process.env, ...process.env,
CLOUDFLARE_API_TOKEN: ctx.apiToken, CLOUDFLARE_API_TOKEN: ctx.apiToken,
CLOUDFLARE_ACCOUNT_ID: ctx.accountId, CLOUDFLARE_ACCOUNT_ID: ctx.accountId,