4. 异常处置核心概念
在算法执行系统中,异常不是“意外”,而是必须被建模、被观测、被处置的一等公民。很多团队把异常处置等同于“加 try-catch”,结果线上仍然频繁出现任务卡死、状态不一致、重复扣款、数据缺口等问题。根本原因在于:没有把异常当作一条完整的生命周期来管理。本节围绕五个核心概念展开:异常定义、异常分类、异常传播、异常恢复与容错机制,帮助大家建立可落地的异常处置心智模型。
一、异常定义:从“报错”到“可判定事件”
在工程语境中,异常定义不能停留在“程序出错了”。一个可处置的异常,必须满足三个条件:可判定、可定位、可决策。可判定是指系统能通过明确规则识别异常发生;可定位是指能关联到具体任务、数据分片、模型版本或调用链;可决策是指处置策略有明确分支,而不是靠人临时拍脑袋。
因此,异常定义应包含结构化字段,例如异常码、异常级别、发生阶段、影响范围、是否可重试、建议动作。下面是一个典型的异常事件结构:
{
"error_code": "ALGO_DATA_TIMEOUT",
"level": "P1",
"stage": "feature_fetch",
"task_id": "task_20240612_0091",
"shard": "shard-17",
"retryable": true,
"suggest_action": "retry_with_backoff",
"occur_time": "2024-06-12T10:23:41+08:00"
}
只有把异常从“字符串日志”升级为“结构化事件”,后续的分类、传播和恢复才有依据。
二、异常分类:决定处置策略的第一道分水岭
异常分类的核心目的不是“归类好看”,而是让不同异常走不同处置通道。最常见的分类维度包括:按来源分为数据异常、算法异常、系统异常、依赖异常;按可恢复性分为可重试、可降级、需人工介入、不可恢复;按影响面分为单任务、单分片、单租户、全局。
其中最关键的是“可重试性”判断。把不可重试异常反复重试,会放大故障;把可重试异常直接判死,会造成不必要的业务损失。下表给出一个实战中常用的分类与处置对照清单:
| 异常类型 | 典型示例 | 可重试性 | 建议处置 |
|---|---|---|---|
| 数据异常 | 字段缺失、格式错误、分布漂移 | 部分可重试 | 校验拦截、隔离脏数据、触发补数 |
| 算法异常 | 除零、溢出、模型输出越界 | 通常不可重试 | 熔断当前分片、回退旧模型、人工复核 |
| 系统异常 | OOM、磁盘满、线程池耗尽 | 可重试但需限流 | 退避重试、资源隔离、扩容 |
| 依赖异常 | 下游超时、限流、连接拒绝 | 可重试 | 指数退避、降级缓存、熔断 |
| 逻辑异常 | 状态机非法迁移、幂等冲突 | 不可重试 | 冻结任务、告警、人工介入 |
三、异常传播:从单点故障到系统雪崩的路径
异常传播描述的是异常如何在调用链、数据流和任务依赖中扩散。算法执行系统通常存在多层调用:调度层 → 执行层 → 数据层 → 模型层 → 下游服务。如果异常在某一层没有被正确拦截或转换,就会向上传播,最终表现为“整个任务失败”甚至“整个集群不可用”。
异常传播有三种典型模式:向上冒泡、横向扩散、延迟爆发。向上冒泡是调用栈逐层抛出;横向扩散是一个分片失败导致重试风暴,拖垮同批次其他任务;延迟爆发是异常被吞掉后,在后续环节以数据不一致的形式出现,排查成本最高。
控制异常传播的关键手段是“边界拦截”和“异常转换”。在每一层边界处,把底层异常转换为上层可理解的异常,并决定是继续抛出、降级还是终止。下面是一个边界拦截的伪代码示例:
def execute_shard(shard):
try:
data = fetch_features(shard)
result = run_model(data)
return result
except DataTimeoutError as e:
raise AlgoStageError(
code="FEATURE_TIMEOUT",
retryable=True,
cause=e
)
except ModelOutputError as e:
log.error("model output invalid", shard=shard)
return fallback_result(shard)
这段代码的价值在于:它没有让底层异常直接穿透到调度层,而是把异常转换成了带有处置语义的上层异常。
四、异常恢复与容错机制:从“能恢复”到“恢复得对”
异常恢复关注的是“异常发生后如何回到正确状态”,容错机制关注的是“系统在部分异常下仍能继续提供服务”。两者互补:恢复是事后动作,容错是事前设计。
常见的恢复策略包括:重试(带指数退避和抖动)、补偿(对已产生副作用进行反向操作)、回滚(回到上一个一致状态)、重放(基于日志或消息重新执行)。容错机制则包括:冗余(多副本)、隔离(舱壁模式)、降级(返回兜底结果)、熔断(快速失败防止雪崩)。
在算法执行场景中,最容易被忽视的是“幂等性”。如果重试或重放不具备幂等性,恢复动作本身就会制造新的数据错误。因此,恢复机制设计必须与幂等键、状态机、去重表配合使用。
五、案例与复盘:一次特征超时引发的连锁故障
某推荐算法任务在晚高峰出现大面积失败。初始异常是特征服务超时,属于依赖异常,理论上可重试。但由于执行层没有做退避和限流,所有分片同时重试,导致特征服务负载进一步升高,超时加剧,最终演变为全局雪崩。复盘发现三个问题:第一,异常分类中未区分“可重试但需限流”;第二,异常传播没有边界拦截,底层超时直接冒泡到调度层;第三,恢复策略只有“立即重试”,没有降级和熔断。
改进方案是:在异常分类中增加“重试需限流”标签;在执行层增加异常转换和退避重试;对特征服务增加熔断和本地缓存降级;对任务增加幂等键,确保重放安全。改进后,同类故障的影响面从“全局失败”缩小到“单分片延迟”,恢复时间从小时级降到分钟级。