全栈杂谈第八期 详解接口的自限流

178 阅读9分钟

上一期我们谈到了如何实现对于接口访问流量的控制和管理,但是这种仅适用于一般的接口限流,较为不灵活。碰上不规律的突发性高流量总是有滞后性,因此我们本期来聊一聊接口的自限流,看看如何让接口限流变的更加灵活。

自适应限流是一种动态调整流量控制策略的机制,其核心目标是通过结合多维度的实时监控指标,让系统能够在高负载情况下保持稳定,同时尽可能发挥系统的最大吞吐能力。相比于传统固定限流策略,自适应限流更加智能和灵活,能够根据系统状态实时调整限流参数,从而避免因流量突增导致的系统崩溃。

自适应限流的关键要素

  1. 多维度监控指标
    • Load(系统负载):监控当前服务器或应用的负载,避免过载。
    • CPU 使用率:识别资源瓶颈,动态调整限流。
    • 平均响应时间(RT):通过响应时间反映系统的处理能力和压力。
    • 入口 QPS(每秒请求数):衡量系统当前的流量压力。
    • 并发线程数:确保不会因线程数过多导致资源争抢。
  2. 动态调整机制
    • 基于当前系统的状态指标,计算出系统的最大可承载流量。
    • 动态调整限流策略,如令牌桶容量、滑动窗口阈值等。
  3. 实时监控与反馈
    • 实时采集数据并进行分析。
    • 快速响应流量变化,并反馈调整限流策略。

类似实现思路

  • 自适应自旋锁:根据线程争用状态调整自旋时间,避免资源浪费。
  • Kubernetes 动态扩容:根据 Pod 的负载或指标动态增加或减少副本数。
  • 动态线程池调整:根据任务队列长度或处理时间动态调整线程池大小。

自适应限流的实现思路

为了实现自适应限流,我们可以采用以下步骤:

  1. 定义监控指标
    • 收集系统的实时负载、CPU 使用率、RT、QPS 等指标。
  2. 阈值计算
    • 根据监控指标计算当前的系统承载能力,例如:
      • 最大允许 QPS = 1000 × (1 - CPU 使用率/100)
      • 动态调整的并发线程数 = (最大线程数 - 活跃线程数) × 系数。
  3. 动态调整限流策略
    • 如果系统负载过高,则收紧流量(降低 QPS)。
    • 如果系统负载较低,则适当放宽流量。
  4. 执行限流
    • 根据动态计算的阈值执行令牌桶或滑动窗口限流。

简单Demo

这里我们写一个非常基础的demo,来实现一个简单的自限流算法

我们采用根据系统的一些性能指标来做简要的限流依据

系统监控类

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;

/**
 * 系统指标类
 * 
 * 该类提供了获取系统负载和CPU使用率的方法
 */
public class SystemMetrics {
    
    private final OperatingSystemMXBean osBean;

    /**
     * 构造函数
     * 
     * 初始化OperatingSystemMXBean实例以获取系统指标
     */
    public SystemMetrics() {
        this.osBean = ManagementFactory.getOperatingSystemMXBean();
    }

    /**
     * 获取系统负载平均值
     * 
     * @return 返回系统的负载平均值
     */
    public double getSystemLoad() {
        return osBean.getSystemLoadAverage();
    }

    /**
     * 获取CPU使用率
     * 
     * @return 返回CPU使用率百分比,如果不支持则返回-1
     */
    public double getCpuUsage() {
        if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
            return ((com.sun.management.OperatingSystemMXBean) osBean).getSystemCpuLoad() * 100;
        }
        return -1; // 不支持获取 CPU 使用率
    }
}

自适应限流类

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 自适应速率限制器类
 * 
 * 该类根据系统的CPU使用率和系统负载动态调整请求的限流阈值
 */
public class AdaptiveRateLimiter {
    private final SystemMetrics metrics;
    private final AtomicInteger maxQps; // 当前允许的最大 QPS

    /**
     * 构造函数
     * 
     * 初始化系统指标和最大QPS
     * 
     * @param initialQps 初始的最大QPS值
     */
    public AdaptiveRateLimiter(int initialQps) {
        this.metrics = new SystemMetrics();
        this.maxQps = new AtomicInteger(initialQps);
    }

    /**
     * 动态调整限流阈值
     * 
     * 根据CPU使用率和系统负载动态调整最大QPS
     */
    public void adjustQps() {
        double cpuUsage = metrics.getCpuUsage();
        double systemLoad = metrics.getSystemLoad();

        // 简单逻辑:基于 CPU 使用率和系统负载动态计算 QPS
        if (cpuUsage > 80 || systemLoad > 1.0) {
            maxQps.set(Math.max(maxQps.get() - 10, 10)); // 降低 QPS,最小为 10
        } else if (cpuUsage < 50 && systemLoad < 0.5) {
            maxQps.set(maxQps.get() + 10); // 增加 QPS
        }

        System.out.println("Adjusted QPS: " + maxQps.get());
    }

    /**
     * 尝试获取请求许可
     * 
     * @return 如果成功获取许可则返回true,否则返回false
     */
    public boolean tryAcquire() {
        // 模拟请求限流逻辑
        if (maxQps.get() <= 0) return false;
        maxQps.decrementAndGet();
        return true;
    }

    /**
     * 释放请求许可
     */
    public void release() {
        maxQps.incrementAndGet();
    }
}

测试main方法

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(100); // 初始最大 QPS 为 100

        // 定时调整 QPS
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(limiter::adjustQps, 0, 1, TimeUnit.SECONDS);
    
        // 模拟请求处理
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                if (limiter.tryAcquire()) {
                    System.out.println(Thread.currentThread().getName() + " processed");
                    limiter.release();
                } else {
                    System.out.println(Thread.currentThread().getName() + " rejected");
                }
            }).start();
    
            try {
                Thread.sleep(10); // 模拟请求间隔
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    
        scheduler.shutdown();
    }

}

运行结果

Snipaste_2024-12-17_17-31-12.png

Snipaste_2024-12-17_17-30-39.png

从上述这两个运行截图我们也能看到,随着线程的不断增加,qps也在动态调整。当然这也是一个很简单的不成熟的demo,仅仅是为了简单演示一下其核心思想。

阿里的Sentinel

在高并发、复杂的系统中,限流是保障系统稳定性的重要手段。传统限流方案通常基于系统负载(load)进行限流调节,但这种方式存在显著的延迟性问题。Sentinel 提出的自适应限流方案,为我们提供了全新的思路:

  • 基于负载作为启动控制流量的条件,并非直接作为调整的核心指标。
  • 通过系统的处理能力(如请求响应时间 RT 和当前请求速率)来决定允许通过的流量

为什么传统基于负载(load)的限流方式存在问题?

长期以来,传统限流方案的核心思想是基于硬指标(如 load1)来判断系统是否过载:

  1. 当系统的负载高于某个阈值时,减少流量进入或直接禁止流量。
  2. 当负载恢复到合理水平时,再逐步放开流量。

这种基于负载的限流方式存在两个主要问题:

  • 延迟性问题

    • 系统负载(load)本质上是系统状态的一个“结果”,它反映了过去一段时间的系统压力,而非当前状态。
    • 例如,当系统负载过高时,触发限流,然而实际效果(即负载的变化)需要一定的时间才能观察到。通常这个过程至少需要 1 秒甚至更长。
    • 同样,当负载开始好转时,恢复流量的操作也会有滞后性,导致系统恢复的速度较慢。

    这种延迟性带来的直接后果是:

    • 调节动作无法立刻生效,容易导致限流决策不及时。
    • 系统负载曲线会出现抖动,无法快速恢复到稳定状态。
  • 资源浪费问题

    • 负载高的情况不一定意味着系统无法处理更多请求。
    • 在负载较高但系统能够处理更多请求时,传统限流方案依然会严格限制流量,导致系统的吞吐率降低,浪费了宝贵的处理能力。

真实场景下的问题分析

假设我们遇到这样一个场景:

  • 下游服务不稳定:下游应用出现故障,导致请求的响应时间(RT)大幅增加,系统的负载也随之升高。
  • 下游恢复后:随着下游应用恢复,RT 降低,系统实际上具备更强的处理能力。

此时,传统限流方案依然会根据“高负载”的指标限制流量,即便系统已经具备快速恢复的条件,但流量的恢复仍然会较慢。这种设计导致系统无法在第一时间释放吞吐能力,恢复曲线呈现出明显的滞后和抖动。

Sentinel 自适应限流的核心思想

Sentinel 的设计受到了 TCP BBR 算法的启发,突破了传统基于“硬指标”的限流思维,提出了一种基于系统实际处理能力的自适应限流策略。其核心思想如下:

  1. 启动条件基于系统负载(load1)

    • 负载是触发流量控制的一个初始条件,但不是调整限流的核心依据。
    • 当系统负载超过阈值时,启动限流逻辑,进入自适应调节状态。
  2. 核心依据是系统的处理能力

    • 请求响应时间(RT):反映系统处理单个请求的耗时。
    • 请求速率(当前 QPS):反映系统正在处理的请求量。
    • Sentinel 根据这些实时指标来评估系统的处理能力,并动态调整允许通过的流量。
  3. 目标:最大化系统吞吐量

    • Sentinel 的限流目标并非使负载降低到某个特定阈值,而是在保证系统不被拖垮的前提下,尽可能提高系统的吞吐量。

    • 调节的依据是当前系统“能够处理的请求”和“允许进来的请求”之间的平衡,而非单纯依赖系统负载。

Sentinel 自适应限流的优势

  1. 解决延迟性问题
    • 由于限流决策基于实时请求指标(RT 和 QPS),而非系统负载这种间接指标,能够更快速地做出限流或恢复的决策。
    • 避免了传统限流方式因负载指标滞后导致的抖动和恢复慢的问题。
  2. 更高的吞吐率
    • 通过动态评估系统的实际处理能力,Sentinel 能够在系统负载较高但仍有余力时,尽可能提高通过的流量。
    • 避免了资源浪费,系统能够达到最大吞吐率。
  3. 平滑的限流调整
    • Sentinel 的自适应限流是一个动态调整的过程,能够平滑地调节流量的通过率,避免了突增或突降带来的系统冲击。

总结

自适应限流突破了传统限流的固有思维,通过更实时、更精准的流量调控,达到了系统稳定性与吞吐量的双重优化。Sentinel 的自适应限流实践为高并发场景下的系统保护提供了强有力的解决方案,同时也为其他限流机制带来了启示:通过实时监控系统的真实处理能力,动态平衡请求与资源,才能最大程度地发挥系统性能

欢迎关注公众号:“全栈开发指南针”

这里是技术潮流的风向标,也是你代码旅程的导航仪!🚀

Let’s code and have fun! 🎉