编程 把 ABNORMAL 归进失败分支之后:重复建实例、误告警和一次 nodemanager 自杀

2026-11-01 00:03:35

把 ABNORMAL 归进失败分支之后:重复建实例、误告警和一次 nodemanager 自杀

部署编排服务改成事件驱动后,状态解析里写了这么一行:

if (status === 'FAILED' || status === 'ABNORMAL') return handleFailure();

上线两周,三个后果:

  • 一个 pod 短暂进入 ABNORMAL,handleFailure() 走销毁重建,同一 deployment 下实例数涨到 3 个,直到人工清理;
  • 运维事件流里 Abnormal 被映射成业务失败,告警每次维修窗口都响一遍;
  • 履约侧把异常单回调的 is_final=false 当成失败终态,订单被提前关闭。

根因都一样:ABNORMAL 在多数枚举里是并列于 FAILED 的独立值,代表“有异常但状态未定”,不是失败终态。

三份云 API 枚举里的 ABNORMAL

华为云 ModelArts — ListInferDeploymentPods

GET /v2/{project_id}/services/{id}/deployments/{deployment_name}/instances/{name}/pods

Query 参数 status 可选,多状态用英文逗号连接、不能有空格,默认不过滤。取值范围 7 种:RUNNING、PENDING、SUCCEEDED、FAILED、ABNORMAL、UNKNOWN、DELETED;返回体 data[].status 取值相同,响应含 current/pages/size/total。FAILED 与 ABNORMAL 在同一枚举里并列,这本身就否定了二者同义。

华为云 VIAS — 获取作业列表

GET /v2/{project_id}/tasks

task_state 枚举:PENDING|RECOVERING|STARTING|UPGRADING|CREATE_FAILED|START_FAILED|RUNNING|STOPPING|STOPPED|ABNORMAL|SUCCEEDED|FAILED|DELETING|FREEZING|FROZEN|SCHEDULING|LACK_OF_RESOURCES。响应里 state 是枚举值,status 是“作业状态的详情信息,仅部分状态会有详情信息”,最大 4096 字符——这就是“状态 + 子状态”两段式结构。LACK_OF_RESOURCES 的存在说明部分状态是可自愈的等待态。page_size 最大 1000,默认 10。

百度智能云 BCC — 查询预期事件列表

serverEventStatus:Inquiring、Processing、Executing、Executed、Abnormal、DisConfirm。运维事件(维修/重启)的 Abnormal 表示事件处理异常,与 Executed(已执行)、DisConfirm(未确认)分属不同分支,不是业务失败。

同类用法也出现在业务域:淘宝开放平台 tmall.cityretail.fulfill.abnormal.center.abnormal.status.change(同城零售履约异常单处理结果回调,入参含 is_final、abnormal_order_id、abnormal_accept_id)——ABNORMAL 是“异常单”这种有生命周期、可关闭的对象;马帮 ERP 的 action=do-order-abnormal(订单异常)用 abnormal_label_name 的 000/999 表示动作成功或失败。ABNORMAL 是跨产品通用的枚举值,不属于某家私有定义。

一次具体的翻车:MindIE-Motor PR #334

标题即事故清单:修复未排干的 metric 请求导致 checkpoint 失败 & 快照恢复后过期的 abnormal status 导致 nodemanager 自杀 & 快照实例 register 被拒绝。

第二条是本文主题:快照恢复后,缓存里留着上一次的 abnormal status,nodemanager 探测 server 状态时读到它,判定自己异常并退出。修复方式不是给 ABNORMAL 加分支,而是加身份校验:用 pod_ip 与 endpoint.ip 是否一致判断该状态是否过期。

同一 PR 的第三条是同源问题:快照恢复过快时,controller 还未把同 job_name 的旧实例置为 inactive,快照实例的 register 被拒,但 register 仍返回 ok,实例不再重试。非快照实例只要 HTTP 请求发通即视为成功(防重复 register 的门禁),快照实例新增 register_after_restore,只有真正成功才返回 ok。

结论很直接:状态值不带时间戳和身份校验时,缓存里的旧 ABNORMAL 会被当成本次故障。

另一条提醒:通用错误码不做特殊判断

腾讯云开发 CloudBase 错误码文档

表中含 ENV_ABNORMAL、FUNCTIONS_STATUS_ABNORMAL、SERVICE_LB_STATUS_ABNORMAL。文档末尾明确写着:DATABASE_REQUEST_FAILED、STORAGE_REQUEST_FAIL、FUNCTIONS_EXECUTE_FAIL 分别为通用的数据库、云存储、函数请求错误,无特殊语义,未来将细化出独立特征语义的错误码,请避免基于该错误码进行特殊逻辑判断。同理,*_ABNORMAL 这类聚合码只能用来“进去看详情”,不能当作一个可判定的结果。

判据:先分清终态、聚合态、瞬时态

类别典型取值能否终止流程处理
终态SUCCEEDED、FAILED、DELETED、STOPPED、CREATE_FAILED、START_FAILED、Executed是成功即完结;失败才重建/告警
聚合态ABNORMAL、UNKNOWN、Abnormal否必须带子状态/事件/时间戳再判
瞬时态PENDING、STARTING、RECOVERING、SCHEDULING、STOPPING、LACK_OF_RESOURCES、Inquiring、Processing、Executing否按退避重试,不动作
type Kind = 'terminal' | 'aggregate' | 'transient';

const KIND: Record = {
RUNNING: 'transient', PENDING: 'transient', SCHEDULING: 'transient',
RECOVERING: 'transient', STARTING: 'transient', UPGRADING: 'transient',
STOPPING: 'transient', DELETING: 'transient', FREEZING: 'transient',
LACK_OF_RESOURCES: 'transient',
SUCCEEDED: 'terminal', FAILED: 'terminal', DELETED: 'terminal',
STOPPED: 'terminal', CREATE_FAILED: 'terminal', START_FAILED: 'terminal',
ABNORMAL: 'aggregate', UNKNOWN: 'aggregate',
};

function decide(s: Sample) {
// 未登记的取值一律按聚合态处理:先不动作,去查详情
const kind = KIND[s.status] ?? 'aggregate';

if (kind === 'transient') return { action: 'wait', retryAfter: s.nextPoll };
if (kind === 'terminal') {
return { action: s.status === 'SUCCEEDED' ? 'done' : 'recreate-and-alert' };
}

// 聚合态:两段判断
if (s.detail ?? s.event) return { action: 'route-by-detail', detail: s.detail ?? s.event };
if (s.observedAt - s.statusUpdatedAt < STALE_MS) return { action: 'wait' }; // 新鲜,继续观察
// 过期状态:不猜,先用身份比对确认它是不是本次的
return { action: 'stale-status', hint: 'compare pod_ip vs endpoint.ip' };
}

STALE_MS 的阈值和各字段(detail、statusUpdatedAt)的具体来源依产品而异,上述伪代码未在真实接口上实测,落地前需对着实际返回体核对字段名。

排查顺序(可复现)

  1. 把原始枚举全文贴出来,确认目标值是否与 FAILED 并列;别用 tsc 的字符串联合类型猜测。
  2. 看返回体除顶层状态外,是否还有详情字段(如 VIAS 的 status)、事件列表或 updated_at。
  3. 对比身份标识:pod_ip vs endpoint.ip、实例 id、job_name,判断这条状态属于当前实例还是快照/旧实例。
  4. 复现过期场景:构造一条旧快照记录,在恢复后立刻触发一次探测,观察是否会走进失败分支。
  5. 修分支前先修判型:把 ABNORMAL 从失败分支摘出,改为“读详情 + 校验身份 + 未超时才继续等待”。

推荐文章

程序员茄子在线接单