把 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)的具体来源依产品而异,上述伪代码未在真实接口上实测,落地前需对着实际返回体核对字段名。
排查顺序(可复现)
- 把原始枚举全文贴出来,确认目标值是否与
FAILED并列;别用 tsc 的字符串联合类型猜测。 - 看返回体除顶层状态外,是否还有详情字段(如 VIAS 的
status)、事件列表或updated_at。 - 对比身份标识:
pod_ipvsendpoint.ip、实例 id、job_name,判断这条状态属于当前实例还是快照/旧实例。 - 复现过期场景:构造一条旧快照记录,在恢复后立刻触发一次探测,观察是否会走进失败分支。
- 修分支前先修判型:把
ABNORMAL从失败分支摘出,改为“读详情 + 校验身份 + 未超时才继续等待”。