From 9204f019a10b15d891045c409e18b988aa82ead7 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 12 Sep 2026 11:56:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E6=95=85=E9=9A=9C=E5=91=8A?= =?UTF-8?q?=E8=AD=A6=E6=8C=89=20invocation=20=E5=88=86=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E5=B4=A9=E6=BA=83=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E6=97=B6=E5=91=8A=E8=AD=A6=E4=B8=8D=E5=86=8D=E8=A2=AB=E6=8A=A4?= =?UTF-8?q?=E6=A0=8F=E6=8C=A1=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 问题(实测) 服务反复崩溃时,故障告警**送不出去**。 链路:告警邮件带 `relay:"summary"`(走免预算通道),而服务端按 `AutoAliasFor(收件人, 主题)` 派生会话别名 —— **主题相同即同一条会话**。于是崩溃 循环下多封告警全落进同一条会话,而网关对会话内**连续**的中继邮件有 `maxRelayHops=5` 的硬上限(防两个 Agent 互相唤醒的正当护栏)→ 第 6 封起返回 403: POST /api/v1/mail/send ... - 403 245B (网关 NRestarts=0,一直在正常服务) 报告只能落 spool,等下次 ExecStartPost `--flush` 才补投。实测 dsh 崩溃循环 (我自己的部署缺陷所致)时的六条告警全部 spooled —— 而**服务反复崩溃正是最需要 告警送达的时刻**。 # 修法 主题带上本次启动的 `INVOCATION_ID` 短号:`[dsh] 桥服务异常终止 #5ce6a845`。 主题变了,别名与会话随之分开,每次崩溃各自可投递。 **护栏不削弱**:它针对的是 agent↔agent 互相唤醒,不是同一个人收多条故障通知。 副作用是崩溃循环产生多条会话而非一条线程 —— 对"服务在崩"这件事,分开计数比合并成 一条更容易发现问题。 `INVOCATION_ID` 缺失时退回 `Date.now()+randomUUID()` 的哈希:宁可每次不同, 也不能因为拿不到标识而退回"主题相同 → 告警又被挡下"。 # 验证(三条,含两个反向对照) 不同 invocation → 主题各不相同(每次崩溃各自成会话) 无 invocation(兜底) → 每次仍各不相同(不会退回同主题) 同一 invocation 重报 → 主题相同(一次崩溃仍是一条会话,不刷屏) 注:第二次验证第一次跑时"失败",是因为**我的测试假设错了** —— 父环境里本来就继承了 `INVOCATION_ID`,两次跑用的是同一个 id。用 `env -u INVOCATION_ID` 才真正走到兜底分支。 --- deploy/service-failure-notify.mjs | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/deploy/service-failure-notify.mjs b/deploy/service-failure-notify.mjs index bded842..d49e750 100644 --- a/deploy/service-failure-notify.mjs +++ b/deploy/service-failure-notify.mjs @@ -58,6 +58,21 @@ function redact(value, env) { .slice(0, 1_500); } +/** + * 本次服务启动的短标识,用于让每封故障通知各自成会话(见 subject 处的注释)。 + * + * 优先用 systemd 的 `INVOCATION_ID`(每次启动唯一)。它缺失时退回随机值 —— + * 宁可每次不同,也不要因为拿不到标识而退回"主题相同 → 告警被护栏挡下"。 + */ +function invocationTag(env) { + const invocation = String(env.INVOCATION_ID || '').trim(); + if (invocation) return invocation.replace(/[^A-Za-z0-9]/g, '').slice(0, 8); + return createHash('sha256') + .update(`${Date.now()}:${randomUUID()}`) + .digest('hex') + .slice(0, 8); +} + function relayKeyFor(env, serviceName) { const invocation = String(env.INVOCATION_ID || '').trim(); if (invocation) return `service-failure:${invocation}`; @@ -97,7 +112,23 @@ export function buildFailurePayload({ return { to: String(env.AGENTMAIL_FAILURE_RECIPIENT || DEFAULT_RECIPIENT), - subject: `[${agentName}] 桥服务异常终止`, + /* + * 主题带上本次启动的 invocation 短号。 + * + * 服务端按 `AutoAliasFor(收件人, 主题)` 派生会话别名,所以**主题相同 = 同一条会话**。 + * 崩溃循环下这一点会咬人:多封告警全落进同一条会话,而网关对会话内**连续**的中继 + * 邮件有 `maxRelayHops=5` 的硬上限(防两个 Agent 互相唤醒的正当护栏)→ 第 6 封起 + * 返回 403,报告只能落 spool,等下次 ExecStartPost `--flush` 才补投。 + * + * 实测就是这样:dsh 崩溃循环时的六条告警全部 spooled,而**服务反复崩溃正是最需要 + * 告警送达的时刻**。 + * + * 带 invocation 让每次崩溃各自成会话,因而每次都能投出去 —— 护栏不受影响 + * (它针对的是 agent↔agent 的互相唤醒,不是同一个人收多条故障通知)。 + * 副作用是崩溃循环会产生多条会话而不是一条线程;对"服务在崩"这件事, + * 分开计数比合并成一条更容易发现问题。 + */ + subject: `[${agentName}] 桥服务异常终止 #${invocationTag(env)}`, body: lines.join('\n'), relay: 'summary', relay_key: relayKeyFor(env, service),