错误隔离

3 min read
Zekari
系统设计可靠性工程软件工程弹性系统

一个环节失败,整个系统瘫痪

支付服务挂了。用户无法下单。 推荐引擎挂了。整个首页白屏。 评论功能挂了。文章都无法打开。

这些场景很常见。一个功能失败,拖垮整个系统。

问题不在于支付服务会失败——所有服务都会失败。问题在于,为什么支付失败会导致整个下单流程卡死?为什么推荐引擎的问题会让首页完全不可用?

没有隔离。

失败像病毒一样传播。一个组件的错误,蔓延到调用它的组件,再蔓延到更上层的组件。最终,整个系统都被感染。

💡 Click the maximize icon to view in fullscreen

隔离的本质

错误隔离不是防止失败,而是限制失败的范围

一个服务挂了,只影响那个服务本身。不影响调用它的服务,不影响整个系统。

这需要三个前提:

1. 快速失败

不要等待一个失败的服务慢慢超时。给它一个明确的时间限制。2秒、5秒,超过就立即放弃。

// ❌ 没有超时,可能永远等待
async function callPaymentService() {
  return await paymentAPI.charge()
}

// ✅ 2秒超时,快速失败
async function callPaymentService() {
  return await Promise.race([
    paymentAPI.charge(),
    timeout(2000)
  ])
}

等待 30 秒才发现失败,这 30 秒里,调用方被阻塞,资源被占用,其他请求排队等待。快速失败,把损失控制在最小。

2. 断路器(Circuit Breaker)

如果一个服务已经失败了 10 次,第 11 次调用它大概率还会失败。与其继续尝试,不如直接跳过。

断路器会监控失败率。当失败率超过阈值,断路器打开,所有请求直接返回失败,不再调用下游服务。

过一段时间,断路器会尝试放一个请求过去,看看服务是否恢复。如果恢复了,逐渐恢复流量。如果还是失败,继续阻断。

class CircuitBreaker {
  constructor(threshold = 5) {
    this.failureCount = 0
    this.threshold = threshold
    this.state = 'CLOSED' // CLOSED, OPEN, HALF_OPEN
  }

  async call(fn) {
    if (this.state === 'OPEN') {
      throw new Error('Circuit breaker is OPEN')
    }

    try {
      const result = await fn()
      this.onSuccess()
      return result
    } catch (error) {
      this.onFailure()
      throw error
    }
  }

  onSuccess() {
    this.failureCount = 0
    this.state = 'CLOSED'
  }

  onFailure() {
    this.failureCount++
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN'
      setTimeout(() => this.state = 'HALF_OPEN', 60000)
    }
  }
}

这不是放弃,而是止损。给失败的服务时间恢复,而不是继续给它压力。

3. 优雅降级

支付服务挂了,不代表用户不能浏览商品。推荐引擎挂了,可以显示默认内容。评论功能挂了,文章依然可以阅读。

核心功能保持可用,非核心功能降级或隐藏。

async function renderPage() {
  // 核心内容:必须成功
  const article = await getArticle()

  // 次要功能:失败后降级
  let recommendations = []
  try {
    recommendations = await getRecommendations()
  } catch (error) {
    // 降级:使用默认推荐
    recommendations = getDefaultRecommendations()
  }

  // 可选功能:失败后隐藏
  let comments = null
  try {
    comments = await getComments()
  } catch (error) {
    // 降级:不显示评论区
    comments = null
  }

  return render({ article, recommendations, comments })
}

完美的体验固然好,但不完美的体验胜过完全不可用。

资源隔离:不同功能使用独立资源池

一艘船被分成多个舱室。其中一个舱室漏水,其他舱室仍然密封。船不会沉。

在系统设计中,舱壁模式意味着:不同的功能使用独立的资源。

// ❌ 所有请求共享一个连接池
const pool = createConnectionPool({ max: 10 })

async function handleUserRequest() {
  return await pool.query('SELECT * FROM users')
}

async function handleReportRequest() {
  return await pool.query('SELECT * FROM analytics')
}

// 如果报表查询很慢,占满了连接池,
// 用户请求也会被阻塞
// ✅ 不同功能使用独立连接池
const userPool = createConnectionPool({ max: 8 })
const reportPool = createConnectionPool({ max: 2 })

async function handleUserRequest() {
  return await userPool.query('SELECT * FROM users')
}

async function handleReportRequest() {
  return await reportPool.query('SELECT * FROM analytics')
}

// 报表查询再慢,也不会影响用户查询

关键资源(CPU、内存、连接、线程)都可以分舱。一个功能出问题,不会拖累其他功能。

隔离的代价

错误隔离不是免费的。

你需要更多的监控。要知道哪个服务失败了,失败率多高,何时恢复。

你需要更复杂的逻辑。要处理降级,要管理断路器状态,要区分不同级别的失败。

你需要接受不完美。系统不再是"全有或全无",而是"能用多少算多少"。

但这个代价是值得的。

因为在分布式系统中,失败是常态。网络会断,服务会挂,依赖会超时。

你无法消除失败,但你可以控制失败的影响范围。

隔离让系统变得有韧性。即使部分组件失败,整体仍然可用。这种可用性,比完美的性能更重要。

不只是技术

错误隔离的思想不局限于代码。

团队协作也需要隔离。一个项目延期,不应该阻塞其他所有项目。一个人请假,不应该导致整个团队停摆。

人际关系也需要隔离。一次争吵,不应该毁掉整段友谊。一个误解,不应该推翻所有信任。

隔离不是冷漠,而是保护。

它承认:失败会发生。错误会出现。但我们可以限制它们的破坏力。

一个环节出问题,不代表全盘崩溃。系统仍然可以运转,只是稍微降级而已。

接受不完美

追求完美的系统,是一种执念。

每个功能都必须可用,每个服务都必须在线,每个请求都必须成功。

但现实不是这样。完美是脆弱的。任何一个环节出错,整个系统就崩溃。

错误隔离是在说:我们不需要完美,我们需要韧性。

支付功能挂了?用户还可以浏览。 推荐引擎挂了?用户还可以搜索。 评论功能挂了?用户还可以阅读。

系统依然在运行。也许不是最佳状态,但它还活着。

在一个充满不确定性的世界里,能够在部分失败的情况下继续运转,本身就是一种成功。


在什么情况下,错误隔离反而会降低系统可靠性?

如何判断一个功能是"核心功能"还是"可降级功能"?这个边界如何划定?

断路器的阈值如何设置?失败多少次打开?恢复时间多长?不同场景有什么不同?

如果所有下游服务都失败了,系统还能提供什么价值?降级的底线在哪里?

错误隔离与用户体验的权衡:什么时候应该选择"完全失败并明确告知",而不是"部分降级"?

在团队协作中,如何实现"错误隔离"?如何避免一个人的失误影响整个团队?


相关阅读:

博客内链

Related Posts

Articles you might also find interesting

幂等性检查

1 min read

在不确定的系统中,幂等性检查是对重复的容忍,是对稳定性的追求,也是对失败的预期与接纳

系统设计分布式系统
Read More

文档标准是成本计算的前提

3 min read

API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。

API文档
Read More

CRUD 操作

2 min read

四个字母背后,是数据的生命周期,是权限的边界,也是系统设计的基础逻辑

系统设计软件工程
Read More

实现幂等性处理,忽略已处理的任务

3 min read

在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解

系统设计并发控制
Read More

单例模式管理 Redis 连接

5 min read

连接不是技术细节,而是系统与外部世界的第一次握手,是可靠性的起点

系统设计后端架构
Read More

监控观察期法

3 min read

部署不是结束,而是验证的开始。修复代码只是假设,监控数据才是证明。48小时观察期:让错误主动暴露,让数据证明修复。

系统设计监控
Read More

监听 Redis 连接事件 - 让不可见的脆弱变得可见

4 min read

连接看起来应该是透明的。但当它断开时,你才意识到透明不等于可靠。监听不是多余,而是对脆弱性的承认。

系统设计Redis
Read More

指数退避超时 - 防止无限重试循环

3 min read

失败后立即重试是本能。但有些失败,需要时间来消化。指数退避不是逃避失败,而是尊重失败。

系统设计可靠性工程
Read More

管理后台需要两次设计

3 min read

第一次设计回答"发生了什么",第二次设计回答"我能做什么"。在第一次就试图解决所有问题,结果是功能很多但都不够深入。

系统设计API 设计
Read More

告警分级与响应时间

2 min read

不是所有问题都需要立即响应。RPC失败会在凌晨3点叫醒人。安全事件每15分钟检查一次。支付成功只记录,不告警。系统的响应时间应该匹配问题的紧急程度。

系统设计监控
Read More

API 测试各种边界情况

2 min read

边界情况是系统最脆弱的地方,也是最容易被忽略的地方。测试边界情况不是为了追求完美,而是为了理解系统的真实边界。

API测试
Read More

BullMQ 队列

3 min read

队列不是技术选型,而是对时间的承认,对顺序的尊重,对不确定性的应对

系统设计异步处理
Read More

BullMQ Worker

2 min read

Worker 的本质是对时间的重新分配,是对主线的解放,也是对专注的追求

系统设计异步处理
Read More

配置不会自动同步

2 min read

视频生成任务永远pending,代码完美部署,队列正确配置。问题不在代码,在于配置的独立性被低估。静默失败比错误更危险。

部署配置管理
Read More

数据库参数国际化:从 13 个迁移学到的设计原则

3 min read

数据不该懂语言。当数据库参数嵌入中文标签时,系统的边界就被语言限制了。这篇文章从 13 个参数对齐迁移中提炼出设计原则——国际化不是功能,是系统设计的底层约束。

数据库国际化
Read More

Stripe Webhook中的防御性编程

2 min read

三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。

Web开发系统设计
Read More

让文档跟着代码走

2 min read

文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。

文档软件工程
Read More

双重验证:Stripe生产模式的防御性切换

7 min read

从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。

系统设计Stripe
Read More

端到端 Postback 模拟测试

2 min read

真实的测试不是模拟完美的流程,而是重现真实世界的混乱。Postback 测试的价值在于发现系统在不确定性中的表现。

测试API
Read More

在运行的系统上生长新功能

3 min read

扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。

系统设计架构
Read More

缺失值的级联效应

3 min read

一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。

系统设计防御性编程
Read More

Props Drilling

3 min read

数据在组件树中层层传递,每一层都不需要它,却必须经手。这不是技术债,是架构对真实需求的误解。

React组件设计
Read More

队列生产者实例的工厂函数

4 min read

工厂函数不是设计模式的炫技,而是对重复的拒绝,对集中管理的追求,对变化的准备

系统设计设计模式
Read More

资源不会消失,只会泄露

2 min read

在积分系统中,用户的钱不会凭空消失,但会因为两个时间窗口而泄露:并发请求之间的竞争,和回调永不到达的沉默。

系统设计并发控制
Read More

RPC函数的原子化处理

1 min read

当一个远程函数做太多事情,失败就变得难以理解

分布式系统RPC
Read More