一次失败引发的思考
2026年6月10日,jianfei-wechat v4的调度器出现了一个奇怪的现象:微信发布任务在凌晨3点全部失败,但凌晨4点再跑就成功了。
排查日志发现,失败原因是微信API返回了"接口调用频率超限"。但我们的推送频率明明很低,一天才推两三篇,怎么会超限?
进一步追查,发现是另一个项目的微信小程序在凌晨3点跑了一个批量同步任务,把当天的调用配额全用光了。等到凌晨4点,配额重置,推送就成功了。
这件事让我意识到:错误不是异常,是常态。一个健壮的系统,不是"不犯错",而是"错了能恢复"。
错误处理不是事后补救,而是设计时就该考虑的核心能力。
错误的四种类型
不同类型的错误,需要不同的处理策略。把所有错误混为一谈,要么过度重试导致雪崩,要么简单放弃丢失数据。
根据错误的可恢复性,可以分为四类:
第一类:临时性错误(Transient)
这类错误是环境问题,等一会儿就好了。比如网络抖动、服务过载、限流。
特征:重试大概率能成功,等待时间越长,成功率越高。
处理策略:指数退避重试。第一次失败等1秒,第二次等2秒,第三次等4秒,直到达到上限。不要立即重试,会加重服务端负担。
第二类:永久性错误(Permanent)
这类错误是业务问题,重试也不会成功。比如权限不足、资源不存在、内容违规。
特征:重试没有任何意义,需要人工干预或修改逻辑。
处理策略:记录错误详情,标记为"失败待人工处理",不要自动重试。自动重试只会浪费资源,可能还会触发更严格的限流。
第三类:依赖性错误(Dependency)
这类错误是下游服务的问题,上游服务本身没问题。比如数据库挂了、第三方API不可用、消息队列堵了。
特征:错误不在当前服务,但影响当前服务。当前服务无法独立恢复。
处理策略:降级或熔断。如果下游服务不可用,当前服务提供降级方案(比如返回缓存数据、显示友好的错误页面),或者直接熔断,快速失败,不让请求堆积。
第四类:状态性错误(State)
这类错误是状态不一致导致的。比如重复推送、订单已支付但库存未扣减、任务执行到一半崩溃。
特征:不是某个API调用失败,而是系统状态不对。重试可能修复,也可能让情况更糟。
处理策略:补偿或回滚。先检测当前状态,判断应该补偿(补做缺失的步骤)还是回滚(撤销已完成的步骤)。状态性错误是最复杂的,需要完整的事务日志或事件溯源才能处理。
四种错误处理策略
对应四种错误类型,有四种处理策略:重试、降级、熔断、补偿。
策略一:重试(Retry)
重试是最简单的错误处理策略,但也是最容易滥用的。
正确用法:
- 只对临时性错误重试,不要对永久性错误重试
- 使用指数退避,避免立即重试
- 设置重试上限,避免无限重试
- 重试前检查错误类型,不是所有错误都值得重试
实际案例:小红书发布
小红书没有官方API,用的是第三方接口,经常返回"操作频繁,请稍后再试"。我们的重试策略是:
def publish_to_xiaohongshu_with_retry(article, max_retries=5):
for attempt in range(max_retries):
result = publish_to_xiaohongshu(article)
if result["status"] == "success":
return result
if result["error_code"] == "RATE_LIMIT":
wait_time = 600 * (2 ** attempt) # 10分钟、20分钟、40分钟...
logger.info(f"Rate limited, waiting {wait_time}s before retry")
time.sleep(wait_time)
elif result["error_code"] == "CONTENT_REJECTED":
logger.error("Content rejected, no retry")
return result # 永久性错误,不重试
else:
logger.error(f"Unknown error: {result['error']}")
return result
return {"status": "failed", "error": "Max retries exceeded"}
注意两点:一是区分了"限流"(临时性)和"内容违规"(永久性),只有限流才重试;二是指数退避,避免频繁重试触发更严格的限流。
常见错误:
- 对所有异常都重试,包括权限错误、参数错误
- 立即重试,导致服务端压力更大
- 无限重试,导致任务永远无法完成
策略二:降级(Degradation)
当依赖的服务不可用时,提供一个简化但可用的替代方案,这就是降级。
降级的层次:
- 功能降级:核心功能可用,非核心功能关闭。比如推荐系统挂了,显示默认推荐;搜索服务挂了,只支持精确匹配。
- 数据降级:实时数据不可用,用缓存数据代替。比如数据库挂了,从缓存读数据,虽然可能不是最新,但至少能响应用户。
- 体验降级:正常流程不可用,提示用户稍后再试。比如支付服务挂了,显示"当前支付人数过多,请稍后再试",而不是显示500错误页面。
实际案例:内容中台的质量检查
内容中台有一个质量检查模块,会调用AI服务检查文章的逻辑连贯性、事实准确性。如果AI服务不可用,有两种选择:
- 不降级:质量检查失败,文章无法发布
- 降级:跳过AI检查,只做基础的格式检查,允许文章发布
我们选择了降级。因为AI检查是"加分项",不是"必需项"。AI服务挂了不应该影响正常发布流程。
实现方式:
def quality_check(article):
try:
ai_result = ai_service.check(article.content, timeout=30)
return {
"status": "pass" if ai_result["score"] > 0.7 else "fail",
"score": ai_result["score"],
"details": ai_result["details"]
}
except AIServiceTimeout:
logger.warning("AI service timeout, falling back to basic check")
return {
"status": "pass", # 降级:默认通过
"score": None,
"details": "AI service unavailable, basic check only",
"degraded": True
}
except AIServiceError as e:
logger.error(f"AI service error: {e}")
return {
"status": "pass", # 降级:默认通过
"score": None,
"details": f"AI service error: {e}",
"degraded": True
}
降级的代价:
降级意味着服务质量下降。在AI服务不可用时,可能放过一些质量不高的文章。这是权衡:宁可发布质量稍差的文章,也不要阻塞整个发布流程。
策略三:熔断(Circuit Breaker)
熔断是更激进的降级。当错误率超过阈值时,不再尝试调用服务,直接返回失败,直到一段时间后再尝试恢复。
熔断的状态机:
- 关闭(Closed):正常状态,请求正常转发
- 打开(Open):熔断状态,请求直接失败,不转发
- 半开(Half-Open):试探状态,允许少量请求通过,测试服务是否恢复
状态转换规则:
- 关闭 → 打开:错误率超过阈值(比如50%),或连续失败次数超过阈值(比如10次)
- 打开 → 半开:经过一段时间(比如30秒),尝试恢复
- 半开 → 关闭:试探请求成功,服务已恢复
- 半开 → 打开:试探请求失败,服务未恢复,继续熔断
实际案例:多平台发布的熔断保护
jianfei-wechat v4同时发布到微信、小红书、微博、博客四个平台。如果微博API不稳定,频繁超时,会影响整个发布流程的响应时间。
熔断策略:
class CircuitBreaker:
def __init__(self, failure_threshold=5, timeout=60):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.timeout = timeout
self.state = "closed"
self.last_failure_time = None
def call(self, func, *args, **kwargs):
if self.state == "open":
if time.time() - self.last_failure_time > self.timeout:
self.state = "half-open"
else:
raise CircuitBreakerOpen("Circuit breaker is open")
try:
result = func(*args, **kwargs)
if self.state == "half-open":
self.state = "closed"
self.failure_count = 0
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
raise
# 使用
weibo_circuit = CircuitBreaker(failure_threshold=3, timeout=300)
def publish_to_weibo(article):
return weibo_circuit.call(weibo_api.publish, article)
熔断的好处:
- 快速失败:不用等待超时,直接返回错误
- 保护下游:减少对故障服务的请求,给它恢复的时间
- 防止雪崩:避免故障扩散到整个系统
策略四:补偿(Compensation)
补偿是最复杂的错误处理策略,用于处理状态性错误。
场景:跨平台发布时,微信推送成功了,但小红书推送失败。这时候系统处于不一致状态:微信有草稿,小红书没有。
补偿的方向:
- 向前补偿:继续完成未完成的步骤。比如小红书推送失败,等一会儿重试,最终让小红书也发布成功。
- 向后补偿:撤销已完成的步骤。比如微信推送成功后,发现小红书无法推送,撤销微信的草稿,让系统回到"未发布"状态。
选择原则:
- 如果操作可重试,优先向前补偿
- 如果操作不可逆,只能向前补偿
- 如果操作可撤销,且撤销代价小于重试,选择向后补偿
实际案例:跨平台发布的补偿机制
我们的策略是:向前补偿优先,向后补偿兜底。
def publish_to_all_platforms(article):
results = {}
# 阶段1:推送到各平台
for platform in article.platforms:
result = publish_to_platform(platform, article)
results[platform] = result
if result["status"] == "failed":
log_error(platform, result["error"])
# 阶段2:检查结果,决定补偿策略
success_count = sum(1 for r in results.values() if r["status"] == "success")
if success_count == len(article.platforms):
# 全部成功,无需补偿
return {"status": "success"}
elif success_count == 0:
# 全部失败,标记为失败
return {"status": "failed"}
else:
# 部分成功,向前补偿
for platform, result in results.items():
if result["status"] == "failed":
schedule_retry(platform, article, delay=600) # 10分钟后重试
return {"status": "partial_success", "results": results}
补偿的挑战:
- 补偿逻辑复杂:需要记录每个步骤的状态,判断应该向前还是向后
- 补偿可能失败:补偿本身也是操作,也可能失败,需要递归处理
- 补偿可能产生副作用:撤销操作可能影响其他业务(比如撤销微信草稿,会影响已预览的用户)
错误处理的架构设计
错误处理不是代码层面的技巧,而是架构层面的设计。
设计原则一:错误要显式化
不要用异常控制流程,要用返回值。异常应该是"异常",不是"正常逻辑的一部分"。
反例:
try:
article = get_article(article_id)
except ArticleNotFound:
return "文章不存在"
正例:
result = get_article(article_id)
if result["status"] == "not_found":
return "文章不存在"
显式化的好处是:错误处理逻辑清晰,不会被遗漏。异常容易被catch-all捕获,掩盖了真正的错误。
设计原则二:错误要分类
不同类型的错误,不同的处理方式。用错误码或错误类型区分,不要用字符串匹配。
反例:
if "rate limit" in error_message:
retry()
正例:
if error.code == ErrorCode.RATE_LIMIT:
retry()
分类的好处是:处理逻辑稳定,不会因为错误消息变化而失效。
设计原则三:错误要隔离
一个组件的错误,不应该影响其他组件。用熔断或降级隔离故障域。
反例:
# 微信推送失败,整个发布流程终止
wechat_result = publish_to_wechat(article)
xiaohongshu_result = publish_to_xiaohongshu(article) # 如果微信失败,这行不执行
正例:
# 各平台独立执行
results = parallel_map(publish_to_platform, platforms)
隔离的好处是:局部故障不影响全局,系统整体可用性更高。
设计原则四:错误要可观测
错误发生时,要记录足够的信息:时间、错误类型、上下文、堆栈。这些信息是排查问题的关键。
但不要只记录日志。日志是给人看的,系统需要的是指标(Metrics)和告警(Alerts)。
推荐的观测体系:
- 错误率:每分钟的错误次数,超过阈值告警
- 错误类型分布:各类错误的比例,帮助定位问题
- 错误影响范围:影响了多少用户、多少任务
- 错误恢复时间:从错误发生到恢复的时间,衡量系统的自愈能力
总结
错误处理不是防御性编程的附属品,而是系统韧性的核心设计。
四种错误类型,对应四种处理策略:
- 临时性错误 → 重试(指数退避)
- 永久性错误 → 记录详情,人工处理
- 依赖性错误 → 降级或熔断
- 状态性错误 → 补偿或回滚
错误处理的设计原则:
- 显式化:用返回值代替异常控制流程
- 分类:用错误码区分类型,不依赖字符串匹配
- 隔离:一个组件的故障不影响其他组件
- 可观测:记录日志、指标、告警,快速定位问题
一个健壮的系统,不是"不犯错",而是"错了能恢复"。错误处理的能力,决定了系统的韧性。
承认错误是常态,设计错误处理机制,比追求"零错误"更现实,也更有效。
文章信息
- 写作时间:2026-06-17
- 字数:约3200字
- 状态:草稿