项目实战 # 后端 # nodejs已发布

高并发下的服务治理:限流、熔断与降级原理及实战

在后端系统中,高并发并不只是请求数量增加的问题。真正棘手的是,当流量突然上涨、下游服务响应变慢或者某个核心组件发生故障时,系统能否控制故障范围,避免一个局部问题演变成整个系统不可用...

2026.03.0721 分钟阅读
高并发下的服务治理:限流、熔断与降级原理及实战
高并发下的服务治理:限流、熔断与降级原理及实战 · Cover

在后端系统中,高并发并不只是请求数量增加的问题。真正棘手的是,当流量突然上涨、下游服务响应变慢或者某个核心组件发生故障时,系统能否控制故障范围,避免一个局部问题演变成整个系统不可用。

例如,一个电商系统在促销期间突然迎来大量请求,订单服务需要调用库存服务、优惠券服务和支付服务。如果库存服务响应时间从 50ms 增长到 5s,大量订单请求就会持续占用线程、连接和内存。随着请求不断堆积,订单服务本身也可能出现超时,最终导致整个业务链路瘫痪。

面对这类问题,通常需要三种重要的服务治理手段:限流、熔断和降级。

它们解决的是不同层面的问题:

  • 限流:控制进入系统的请求数量,避免系统被流量压垮。
  • 熔断:当下游服务持续异常时,暂时停止调用,防止故障扩散。
  • 降级:当系统资源不足或部分功能不可用时,主动牺牲非核心功能,优先保证核心业务。

三者并不是相互替代的关系。在生产环境中,它们通常需要配合使用,并与超时控制、并发隔离、负载均衡、缓存和监控告警共同构成完整的服务治理体系。

一、为什么高并发会导致系统雪崩

1. 从请求堆积到系统崩溃

假设一个接口平均处理时间为 100ms,那么在理想情况下,一个持续处理请求的执行单元每秒可以完成约 10 个请求。如果系统中有 100 个能够并行工作的执行单元,理论处理能力大约是每秒 1,000 个请求。

这里仅仅是一个简化模型,实际吞吐量还会受到 CPU、数据库连接池、锁竞争、网络和下游服务等因素影响。

当流量达到每秒 800 个请求时,系统可能仍然运行正常。但如果流量突然上升到每秒 3,000 个请求,超过处理能力的请求就会逐渐积压。

积压可能引发一系列连锁反应:

  1. 请求等待时间增加。
  2. 请求占用的内存和连接不断增加。
  3. 数据库连接池被占满。
  4. 请求超时后,客户端或上游重新发起请求。
  5. 重试进一步增加流量。
  6. 服务处理能力继续下降,最终发生雪崩。

尤其需要注意,超时并不一定意味着请求已经停止执行。例如,HTTP 客户端已经超时,但底层数据库查询仍然在运行。如果系统缺乏正确的取消机制,重试反而可能让原来的工作量翻倍。

因此,高并发治理的目标不只是提升处理能力,更重要的是在超过系统承载能力时,能够主动控制负载和故障传播。

2. 为什么增加服务器不一定能解决问题

假设订单服务部署了 10 个实例,每个实例能够处理 500 QPS,那么理论总处理能力是 5,000 QPS。

但是,如果所有请求最终都要访问一个每秒只能处理 1,000 次查询的数据库,那么整个系统仍然受到数据库处理能力的限制。

             用户请求
                |
                v
          负载均衡器
                |
       +--------+--------+
       |        |        |
       v        v        v
    订单服务  订单服务  订单服务
       |        |        |
       +--------+--------+
                |
                v
           数据库
          1000 QPS

服务实例增加后,数据库可能承受更多并发查询,反而更快达到瓶颈。

这说明高并发问题往往不是某一个服务处理得慢,而是整个调用链路中存在容量不匹配。服务治理必须从请求入口、应用服务、线程或连接池、数据库和下游依赖等多个层面考虑。

二、限流:控制进入系统的流量

限流(Rate Limiting)是指限制一定时间内允许通过的请求数量,超出限制的请求根据策略拒绝、延迟处理或进入排队系统。

限流的核心不是让每个请求都成功,而是在系统资源有限时,让系统保持可用。

1. 常见的限流算法

固定窗口计数器

将时间划分成固定窗口,例如每分钟一个窗口,每个窗口最多允许 1,000 次请求。

实现简单,使用 Redis 计数器就能完成。但是它存在窗口边界问题:如果客户端在一分钟结束前发送 1,000 次请求,在下一分钟开始后立即再发送 1,000 次请求,那么极短时间内可能出现 2,000 次请求。

滑动窗口

滑动窗口将时间范围划分为更细的区间,统计最近一段时间内的请求数量,降低固定窗口边界带来的突发流量问题。

它可以使用 Redis Sorted Set 等结构实现,但会增加存储和计算成本。实际实现还需要注意原子性、键过期、并发竞争以及请求量很大时的内存消耗。

令牌桶

令牌桶以固定速率生成令牌。请求到达时需要消耗令牌,存在令牌时放行,否则拒绝或等待。

例如,令牌桶每秒补充 100 个令牌,桶容量为 200,那么系统可以长期维持每秒 100 个请求左右,同时允许短时间内消耗积累的令牌来吸收一定的突发流量。

漏桶

漏桶以固定速率处理请求,超出处理速度的请求进入桶中等待。如果桶满了,新请求就会被拒绝。

漏桶适合需要平滑请求速率的场景,但排队会增加延迟,因此必须设置队列容量和最大等待时间。

算法优点局限适用场景
固定窗口简单、开销低存在窗口边界突发普通接口配额
滑动窗口统计更平滑实现和存储成本较高严格时间段限额
令牌桶允许可控突发流量需要维护令牌状态API 网关、接口限流
漏桶平滑处理速率排队可能增加延迟流量整形、任务处理

实际项目不一定需要自己实现所有算法。很多场景下,使用成熟的 API 网关、反向代理或限流组件会更可靠。

2. 基于 Redis 实现分布式限流

单实例限流可以直接使用内存计数器。但在多实例部署时,每个实例的计数器彼此独立,无法直接形成全局限额。

例如,服务部署了 5 个实例,每个实例都允许每分钟 1,000 次请求,那么整个集群的实际额度可能达到每分钟 5,000 次。

Redis 可以作为多个实例共享的计数存储。为了保证计数和过期设置的原子性,可以使用 Lua 脚本。

以下示例采用固定窗口计数器,限制某个用户每分钟最多调用 100 次接口。

安装依赖:

npm install ioredis

实现限流服务:

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);

const FIXED_WINDOW_LIMIT_SCRIPT = `
local current = redis.call('INCR', KEYS[1])

if current == 1 then
  redis.call('EXPIRE', KEYS[1], ARGV[1])
end

local ttl = redis.call('TTL', KEYS[1])

return { current, ttl }
`;

export async function isAllowed(
  userId: string,
  limit = 100,
  windowSeconds = 60,
): Promise<boolean> {
  const key = `rate_limit:user:${userId}`;

  const result = await redis.eval(
    FIXED_WINDOW_LIMIT_SCRIPT,
    1,
    key,
    windowSeconds,
  ) as [number, number];

  const [count] = result;

  return count <= limit;
}

调用示例:

const allowed = await isAllowed('user-123');

if (!allowed) {
  throw new Error('请求过于频繁,请稍后重试');
}

这里的关键是使用 Lua 脚本将计数和首次设置过期时间放在同一个原子操作中执行,避免多个请求竞争时出现计数器过期时间设置不一致的问题。

需要注意,这个示例仍然存在固定窗口算法的边界突发问题。另外,生产环境应根据业务确定限流维度,例如:

  • 用户 ID:限制单个用户的调用频率。
  • IP 地址:防止单个来源持续占用资源。
  • API 路径:为不同接口设置不同额度。
  • 租户 ID:避免某个租户挤占其他租户的资源。
  • 全局服务:限制整个集群的总流量。

对于匿名请求,可以考虑 IP 限流,但应注意代理转发头的可信配置,不能无条件相信客户端自行提交的 X-Forwarded-For。

3. 在 NestJS 中实现限流守卫

NestJS 可以通过 Guard 在请求进入业务方法之前完成限流检查。

下面使用前面的 Redis 限流函数实现一个简单的守卫:

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  HttpException,
  HttpStatus,
} from '@nestjs/common';

@Injectable()
export class RateLimitGuard implements CanActivate {
  async canActivate(
    context: ExecutionContext,
  ): Promise<boolean> {
    const request = context.switchToHttp().getRequest();

    // 示例:优先使用已经认证的用户 ID
    const userId = request.user?.id;

    if (!userId) {
      throw new HttpException(
        'Unauthorized',
        HttpStatus.UNAUTHORIZED,
      );
    }

    const allowed = await isAllowed(
      String(userId),
      100,
      60,
    );

    if (!allowed) {
      throw new HttpException(
        'Too Many Requests',
        HttpStatus.TOO_MANY_REQUESTS,
      );
    }

    return true;
  }
}

在控制器中使用:

import {
  Controller,
  Get,
  UseGuards,
} from '@nestjs/common';

@Controller('orders')
export class OrderController {
  @Get()
  @UseGuards(RateLimitGuard)
  findAll() {
    return {
      data: [],
    };
  }
}

这里省略了 Redis 客户端的依赖注入和异常封装,实际项目建议将 Redis 封装成 NestJS Provider,并通过配置管理限流阈值。

当限流触发时,通常返回 HTTP 429 Too Many Requests,也可以根据业务返回合理的重试建议。不要把所有限流失败都包装成 HTTP 500,否则会干扰客户端重试策略和监控告警。

还要考虑 Redis 本身不可用时怎么办:对登录、支付等高风险接口,通常不能因为限流存储故障就直接放开所有流量;而某些低风险接口可能允许短时间降级为本地限流。这个决策需要根据安全要求和可用性目标确定。

三、熔断:阻止故障沿调用链扩散

熔断(Circuit Breaker)的思想类似电路中的保险装置。

当下游服务持续失败或响应异常时,调用方暂时停止向它发送请求,避免大量失败调用继续消耗资源。经过一定时间后,再尝试少量请求,判断服务是否恢复。

1. 熔断器的三种状态

一个典型的熔断器有三种状态:

              正常调用
                 |
                 v
          +-------------+
          |   CLOSED    |
          |  关闭状态   |
          +-------------+
                 |
       失败率超过阈值
                 |
                 v
          +-------------+
          |    OPEN     |
          |  打开状态   |
          +-------------+
                 |
           等待恢复时间
                 |
                 v
          +-------------+
          | HALF-OPEN   |
          | 半开状态    |
          +-------------+
            |         |
        探测成功    探测失败
            |         |
            v         v
         CLOSED      OPEN

CLOSED(关闭):正常放行请求,同时统计调用成功率、失败率和响应时间。

OPEN(打开):快速拒绝调用,不再访问故障服务,从而释放资源并阻止故障继续传播。

HALF-OPEN(半开):等待一定时间后,只允许少量探测请求通过。如果探测成功,说明下游可能已经恢复;如果仍然失败,就重新打开熔断器。

这里的“关闭”和“打开”容易理解反:CLOSED 表示熔断器关闭、允许正常调用;OPEN 表示熔断器打开、拒绝调用。

2. 为什么超时和熔断需要配合

假设订单服务调用库存服务,每次调用都要等待 5 秒才能超时。

当库存服务故障时,如果每个订单请求都继续等待 5 秒,那么大量请求会占用连接、内存以及其他资源。

熔断可以让后续请求快速失败,但它不能替代超时控制。

合理的调用流程通常是:

收到订单请求
     |
     v
检查熔断器状态
     |
     +---- OPEN ----> 快速失败或降级
     |
     v
设置调用超时
     |
     v
调用库存服务
     |
     +---- 成功 ----> 返回结果
     |
     +---- 失败 ----> 记录异常并更新熔断状态

超时限制单次调用的等待时间;熔断限制故障期间的调用频率;降级则决定失败后给业务返回什么结果。

三者共同工作,才能有效降低故障扩散的风险。

3. 使用 opossum 实现熔断

在 Node.js 中,可以使用 opossum 实现熔断器,而不必手动维护所有状态转换规则。

安装依赖:

npm install opossum
npm install -D @types/opossum

创建一个库存服务调用封装:

import CircuitBreaker from 'opossum';

async function queryInventory(productId: string) {
  const controller = new AbortController();

  const timer = setTimeout(() => {
    controller.abort();
  }, 1000);

  try {
    const response = await fetch(
      `http://inventory-service/products/${productId}`,
      {
        signal: controller.signal,
      },
    );

    if (!response.ok) {
      throw new Error(
        `Inventory service status: ${response.status}`,
      );
    }

    return await response.json();
  } finally {
    clearTimeout(timer);
  }
}

const breaker = new CircuitBreaker(queryInventory, {
  timeout: 1200,
  errorThresholdPercentage: 50,
  resetTimeout: 5000,
  volumeThreshold: 10,
});

breaker.on('open', () => {
  console.warn('库存服务熔断器已打开');
});

breaker.on('halfOpen', () => {
  console.info('库存服务进入半开状态');
});

breaker.on('close', () => {
  console.info('库存服务熔断器已关闭');
});

export async function getInventory(productId: string) {
  return breaker.fire(productId);
}

这个示例中:

  • timeout:熔断器等待单次调用的最长时间。
  • errorThresholdPercentage:用于触发熔断的错误率阈值。
  • resetTimeout:打开状态持续一段时间后,允许进入半开状态。
  • volumeThreshold:开始根据错误率触发熔断前,要求积累的最小请求量。

这些参数是示例值,不是所有系统都适用的推荐配置。生产环境需要根据请求量、业务延迟目标和故障模式进行调整。

代码还通过 AbortController 尝试取消超时的 HTTP 请求。但需要注意:取消 HTTP 请求不一定能够撤销下游已经执行的业务操作。对于写操作,仍然需要考虑幂等性。

4. 熔断应该统计哪些错误

不能简单地把所有异常都视为下游故障。

例如:

  • 下游连接失败:通常应该计入失败。
  • 下游持续超时:通常应该计入失败。
  • 下游返回 503:通常应该计入失败。
  • 用户传入非法参数导致 400:通常不应该触发熔断。
  • 用户没有权限导致 403:通常不应该触发熔断。
  • 调用方主动取消请求:是否计入失败,需要根据场景判断。

如果把业务参数错误也纳入熔断统计,就可能出现下游本身健康,却因为大量非法请求导致熔断的情况。

另外,低流量服务不能仅凭一两个失败就轻易熔断。熔断器应考虑最小采样量、统计窗口和失败比例,避免误判。

四、降级:牺牲非核心功能,保障核心业务

降级(Degradation)是指在系统压力过大或依赖服务不可用时,主动减少部分功能或返回简化结果,以保障核心业务继续运行。

它与熔断有明显区别:熔断负责控制是否继续调用故障依赖,降级负责决定业务如何继续响应。

例如,商品详情页面通常包含:

  • 商品基本信息。
  • 实时库存。
  • 商品推荐。
  • 用户评价。
  • 浏览量统计。

当推荐服务出现故障时,商品基本信息仍然可以正常返回。此时可以跳过推荐服务,返回一个没有推荐商品的结果,而不必让整个商品详情接口失败。

1. 常见的降级策略

返回缓存数据

对于商品信息、配置、榜单等允许一定延迟的数据,可以在实时服务不可用时返回缓存中的旧数据。

关闭非核心功能

例如关闭商品推荐、浏览记录、非关键统计和部分装饰性内容。

返回默认值

例如推荐列表返回空数组,非关键统计返回默认值。

切换到备用实现

例如主搜索服务不可用时,切换到简单数据库查询或备用搜索服务,但需要评估备用路径的容量,避免将故障转移到另一个系统。

异步化处理

将非关键操作写入队列,后续通过 BullMQ、Kafka 等机制异步完成。例如,订单创建成功后,将统计事件发送到消息系统,而不必等待统计逻辑完成。

需要注意,降级不能破坏核心业务的数据一致性。支付金额、订单状态、库存扣减等关键逻辑不能为了快速返回就直接跳过。

2. 在 NestJS 中实现业务降级

下面以商品详情为例:商品主信息是核心数据,推荐列表属于非核心数据。

import {
  Injectable,
  Logger,
} from '@nestjs/common';

@Injectable()
export class ProductService {
  private readonly logger = new Logger(
    ProductService.name,
  );

  constructor(
    private readonly recommendationService:
      RecommendationService,

    private readonly productRepository:
      ProductRepository,
  ) {}

  async getProductDetail(productId: string) {
    const product =
      await this.productRepository.findById(productId);

    if (!product) {
      return null;
    }

    let recommendations = [];

    try {
      recommendations =
        await this.recommendationService.getRecommendations(
          productId,
        );
    } catch (error) {
      this.logger.warn(
        `推荐服务不可用,启用降级策略: ${productId}`,
      );

      // 推荐属于非核心功能,失败时返回空列表
      recommendations = [];
    }

    return {
      product,
      recommendations,
      degraded: recommendations.length === 0,
    };
  }
}

这个示例体现了最基本的降级思路,但它还不够完善:如果推荐服务一直超时,每次请求仍然会等待推荐服务失败后才返回。

因此,实际系统应该在推荐调用上设置合理的超时,并在持续失败时配合熔断器,避免每次请求都重复等待。

另外,recommendations.length === 0 不一定代表发生了降级,因为推荐服务也可能正常返回空列表。生产环境最好显式记录降级原因,而不是根据业务结果猜测是否降级。

3. 使用缓存实现数据降级

对于商品详情等允许短暂过期的数据,可以先查询缓存;当实时数据源不可用时,尝试返回缓存中的旧数据。

async function getProductWithFallback(
  productId: string,
) {
  try {
    const product =
      await productService.fetchLatest(productId);

    await redis.set(
      `product:cache:${productId}`,
      JSON.stringify(product),
      'EX',
      300,
    );

    return {
      data: product,
      source: 'origin',
    };
  } catch (error) {
    const cached = await redis.get(
      `product:cache:${productId}`,
    );

    if (cached) {
      return {
        data: JSON.parse(cached),
        source: 'cache',
        degraded: true,
      };
    }

    throw error;
  }
}

这段代码展示的是基本的缓存兜底策略。实际实现时,还需要注意:

  • 先为源站请求设置超时。
  • 明确哪些数据允许返回旧值。
  • 区分缓存未命中和缓存服务异常。
  • 防止缓存反序列化错误导致整个兜底路径失败。
  • 对旧数据的可接受时长进行限制。
  • 避免缓存也不可用时,所有请求继续无保护地打向源站。

尤其需要注意:如果 Redis 本身就是当前故障点,不能假设读取缓存一定成功。缓存兜底也应该有自己的错误处理和资源边界。

五、把限流、熔断和降级组合起来

限流、熔断和降级解决不同的问题,真正的服务治理通常需要组合使用。

假设订单服务依赖库存服务和推荐服务:

用户请求
    |
    v
  限流
    |
    v
  订单服务
    |
    +------------------+
    |                  |
    v                  v
库存服务调用        推荐服务调用
    |                  |
    v                  v
  熔断保护           熔断保护
    |                  |
    v                  v
核心业务判断        推荐结果
    |                  |
    +---------+--------+
              |
              v
          返回响应

当流量过大时,限流首先控制进入系统的请求数量。

当库存服务持续超时或返回错误时,熔断器停止继续调用故障依赖。对于无法确认库存状态的订单请求,应该返回明确的错误或进入安全的待处理流程,而不是随意假定库存充足。

当推荐服务不可用时,系统可以使用降级策略返回空推荐列表,保证商品主信息和其他核心业务仍然可用。

可以总结为:

机制主要解决的问题典型触发条件常见处理方式
限流请求量超过承载能力QPS、配额超限拒绝请求、排队
熔断下游依赖持续异常超时率、失败率过高暂停调用、半开探测
降级完整业务功能无法稳定提供资源紧张、依赖故障返回缓存、关闭非核心功能
超时单次调用等待时间过长超过延迟预算终止等待、取消请求
隔离一个依赖耗尽共享资源并发过高、连接池耗尽限制并发、独立资源池

这里还有一个重要的原则:服务降级并不意味着所有异常都应该返回 HTTP 200。对用户来说,返回一个明确的业务失败结果,通常比伪造成功更安全。

六、实战:构建一个具备基本治理能力的 NestJS 接口

下面通过一个简化的订单查询接口,将几种机制组合起来。

假设订单服务需要调用库存服务,要求:

  1. 每个用户每分钟最多查询 100 次。
  2. 库存服务调用有明确的超时时间。
  3. 库存服务持续异常时触发熔断。
  4. 商品推荐失败时返回空列表。
  5. 核心订单数据不能伪造。

前面的代码片段可以整理为以下结构:

src/
├── common/
│   ├── guards/
│   │   └── rate-limit.guard.ts
│   └── redis/
│       └── redis.service.ts
├── orders/
│   ├── orders.controller.ts
│   ├── orders.service.ts
│   └── orders.module.ts
├── inventory/
│   └── inventory.service.ts
└── recommendations/
    └── recommendations.service.ts

控制器负责 HTTP 接口和请求入口,Guard 负责限流,业务服务负责组织调用关系,下游客户端封装超时和熔断,非核心功能由业务服务执行降级。

例如:

@Injectable()
export class OrdersService {
  constructor(
    private readonly inventoryService:
      InventoryService,

    private readonly recommendationService:
      RecommendationService,

    private readonly orderRepository:
      OrderRepository,
  ) {}

  async getOrderDetail(orderId: string) {
    // 核心业务数据不能随意降级为虚假成功
    const order =
      await this.orderRepository.findById(orderId);

    if (!order) {
      return null;
    }

    let inventory;
    let inventoryUnavailable = false;

    try {
      inventory =
        await this.inventoryService.getInventory(
          order.productId,
        );
    } catch (error) {
      inventoryUnavailable = true;
    }

    let recommendations = [];
    let recommendationDegraded = false;

    try {
      recommendations =
        await this.recommendationService.getRecommendations(
          order.productId,
        );
    } catch (error) {
      recommendationDegraded = true;
    }

    return {
      order,
      inventory,
      inventoryUnavailable,
      recommendations,
      recommendationDegraded,
    };
  }
}

这个例子演示了业务降级的边界,但还要根据实际业务完善。

例如,如果库存信息是订单确认流程的必要条件,就不能把库存不可用当成库存充足;应该返回明确的暂不可用结果,或者将业务转入安全的待处理状态。

此外,示例为了突出服务治理而简化了错误映射、日志、监控和类型定义。生产环境中应使用统一的异常处理机制,避免把内部异常信息直接暴露给客户端。

七、高并发服务治理中容易忽略的问题

1. 重试可能放大故障

重试可以提高瞬时故障下的成功率,但在服务已经过载时,无限制重试会产生重试风暴。

例如,某个请求失败后重试 3 次,如果链路中有多个服务分别执行重试,实际调用量可能呈倍数增长。

因此,重试策略应该具备:

  • 最大重试次数。
  • 指数退避和随机抖动。
  • 整体请求截止时间。
  • 只对适合重试的错误执行重试。
  • 对非幂等写操作设置幂等保护。

尤其要避免在每一层都独立进行大量重试。通常应该由明确的一层负责重试策略,并为整个调用链设置总的时间预算。

2. 限流不能只看 QPS

相同的 QPS,在不同的请求类型下可能带来完全不同的系统压力。

查询缓存的接口可能每秒处理几千次请求都很轻松;但一个需要执行多条 SQL、调用多个下游服务的接口,即使每秒只有几十次请求,也可能耗尽数据库连接池。

因此,限流规则可以综合考虑请求频率、并发量、请求成本和用户等级。

对于耗时较长的接口,并发限制可能比单纯的 QPS 限制更有效。

3. 熔断器不是分布式全局开关

大多数熔断器默认维护的是进程内状态。

如果部署了 10 个实例,每个实例都有自己的熔断器,那么某个实例可能已经打开熔断器,另一个实例却仍然在继续调用故障下游。

这并不一定是错误:本地熔断状态可以避免额外引入分布式协调成本。但需要清楚,它保护的是各实例的调用路径,而不是天然形成整个集群统一的熔断状态。

如果确实需要全局控制,可以结合 API 网关、共享状态或集中式流量治理平台,但必须评估额外的延迟、复杂度和单点风险。

4. 监控指标比单纯的异常日志更重要

生产环境至少应该关注:

  • 请求量和拒绝率。
  • 请求延迟的 P95、P99。
  • HTTP 429、5xx 的数量和比例。
  • 下游调用成功率与超时率。
  • 熔断器各状态的持续时间。
  • 降级发生的次数和原因。
  • Redis 延迟、连接池使用情况。
  • 数据库连接池和连接等待时间。
  • CPU、内存、事件循环延迟。

例如,如果发现请求量没有明显增加,但 P99 延迟持续升高,同时数据库连接池逐渐被占满,就需要进一步排查数据库查询、锁竞争和连接管理,而不是简单地继续增加应用实例。

5. 降级策略应该提前设计

不能等到生产事故发生时,才临时决定哪些功能可以关闭。

在设计业务时,就应该明确:

  • 哪些功能属于核心业务。
  • 哪些功能允许暂时关闭。
  • 哪些数据允许返回缓存旧值。
  • 哪些错误必须明确返回给客户端。
  • 哪些操作可以异步化。
  • 依赖故障时业务应进入什么状态。

这也是服务治理与单纯的异常捕获之间的重要区别。异常捕获解决的是程序如何处理错误,服务治理则决定系统在资源不足或依赖异常时应该如何保持整体可用。

总结

高并发场景下,服务治理的目标不是保证所有请求永远成功,而是让系统在流量超出承载能力或下游出现故障时,仍然能够控制影响范围,优先保障核心业务。

限流负责控制进入系统的请求量,熔断负责隔离持续异常的下游依赖,降级负责在不影响核心业务安全的前提下减少非必要功能。超时、并发隔离、缓存、异步队列和监控则为这些机制提供必要的基础能力。

对于 Node.js 和 NestJS 项目,可以从 Redis 分布式限流、下游调用超时、成熟熔断库以及明确的业务降级策略开始,再根据实际流量、故障模式和监控数据逐步完善。

最后需要记住:限流是控制流量,熔断是隔离故障,降级是调整服务能力。真正可靠的高并发系统,不是永远不出故障,而是出现故障时仍然知道哪些请求应该放行、哪些调用应该停止,以及哪些业务必须继续保证正确性。

#后端 #nodejs
ZZC

ZZC

写代码,也写一些生活。