SpringCloud Alibaba系列——14Sentinel简介及基本应用((上)

老老老JR老北
发布于 2022-8-18 17:27
浏览
0收藏

作者 | 一起撸Java
来源 |今日头条

学习目标

  1. Sentinel是什么?它的作用
  2. 你了解哪些限流算法
  3. Sentinel的限流规则有哪些
  4. Sentinel的限流策略
  5. Sentinel的限流模
  6. 熔断与限流的区别
  7. Sentinel 降级熔断策略有哪些
    第1章 限流
    1.1 概述与作用
    随着微服务的流行,服务和服务之间的稳定性变得越来越重要。缓存、降级和限流是保护微服务系统运行稳定性的三大利器。

缓存:提升系统访问速度和增大系统能处理的容量 降级:当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉 ​ 限流:解决服务雪崩,级联服务发生阻塞时,及时熔断,防止请求堆积消耗占用系统的线程、IO等资源,造成其他级联服务所在服务器的崩溃

这里我们说一下限流,限流的目的应当是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率就可以拒绝服务、等待、降级。

1.2 限流算法
限流算法常用的几种实现方式有如下四种:计数器、滑动窗口、漏桶和令牌桶

1.2.1 计数器

固定窗口

1、思想:计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现。SpringCloud Alibaba系列——14Sentinel简介及基本应用((上)-鸿蒙开发者社区2、问题:这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题,如下图:SpringCloud Alibaba系列——14Sentinel简介及基本应用((上)-鸿蒙开发者社区假设1min内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最后1秒和下一个周期的开始1秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,但是整体上2秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。

3、代码实现

定义异常

public class BlockException extends Exception {}

 

定义接口

public interface RateLimit {    boolean canPass() throws BlockException;}

 

定义实现类

/** * CounterRateLimit * 计算器限流 * */public class CounterRateLimit implements RateLimit , Runnable {    /**     * 阈值     */    private Integer limitCount;    /**     * 当前通过请求数     */    private AtomicInteger passCount;    /**     * 统计时间间隔     */    private long period;    private TimeUnit timeUnit;    private ScheduledExecutorService scheduledExecutorService;     public CounterRateLimit(Integer limitCount) {        this(limitCount, 1000, TimeUnit.MILLISECONDS);    }     public CounterRateLimit(Integer limitCount, long period, TimeUnit timeUnit) {        this.limitCount = limitCount;        this.period = period;        this.timeUnit = timeUnit;        passCount = new AtomicInteger(1);        this.startResetTask();    }     @Override    public boolean canPass() throws BlockException {        if (passCount.incrementAndGet() > limitCount) {            throw new BlockException();        }        return true;    }     private void startResetTask() {        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();        scheduledExecutorService.scheduleAtFixedRate(this, 0, period, timeUnit);    }     @Override    public void run() {        passCount.set(1);    }     public static void main(String[] args) throws BlockException, InterruptedException {        CounterRateLimit counterRateLimit = new CounterRateLimit(10, 2000, TimeUnit.MILLISECONDS);        for (int i = 1; i < 10000000; i++) {            Thread.sleep(200);            if (counterRateLimit.canPass()) {                System.out.println("我是第" + i + "个请求");            }        }    }}

 

1.2.2 滑动窗口

Sentinel底层实现方式

1、思想:固定窗口存在临界值问题,要解决这种临界值问题,显然只用一个窗口是解决不了问题的。假设我们仍然设定1分钟内允许通过的请求是100个,但是在这里我们需要把1分钟的时间分成多格,假设分成5格(格数越多,流量过渡越平滑),每格窗口的时间大小是12秒,每过12秒,就将窗口向前移动一格。为了便于理解,可以看下图SpringCloud Alibaba系列——14Sentinel简介及基本应用((上)-鸿蒙开发者社区图中将窗口划为5份,每个小窗口中的数字表示在这个窗口中请求数,所以通过观察上图,可知在当前时间快(12秒)允许通过的请求数应该是10而不是100(只要超过10就会被限流),因为我们最终统计请求数时是需要把当前窗口的值进行累加,进而得到当前请求数来判断是不是需要进行限流。

此算法可以很好地解决固定窗口算法的临界问题。

2、代码实现

/** * SlidingWindowRateLimit * 滑动窗口限流 */@Slf4jpublic class SlidingWindowRateLimit implements RateLimit, Runnable {     /**     * 阈值     */    private Integer limitCount;     /**     * 当前通过的请求数     */    private AtomicInteger passCount;     /**     * 窗口数     */    private Integer windowSize;     /**     * 每个窗口时间间隔大小     */    private long windowPeriod;    private TimeUnit timeUnit;      private Window[] windows;    private volatile Integer windowIndex = 0;     private Lock lock = new ReentrantLock();    public SlidingWindowRateLimit(Integer limitCount) {        // 默认统计qps, 窗口大小5        this(limitCount, 5, 200, TimeUnit.MILLISECONDS);    }     /**     * 统计总时间 = windowSize * windowPeriod     */    public SlidingWindowRateLimit(Integer limitCount, Integer windowSize, Integer windowPeriod, TimeUnit timeUnit) {        this.limitCount = limitCount;        this.windowSize = windowSize;        this.windowPeriod = windowPeriod;        this.timeUnit = timeUnit;        this.passCount = new AtomicInteger(0);        this.initWindows(windowSize);        this.startResetTask();    }     @Override    public boolean canPass() throws BlockException {        lock.lock();        if (passCount.get() > limitCount) {            throw new BlockException();        }        windows[windowIndex].passCount.incrementAndGet();        passCount.incrementAndGet();        lock.unlock();        return true;    }     private void initWindows(Integer windowSize) {        windows = new Window[windowSize];        for (int i = 0; i < windowSize; i++) {            windows[i] = new Window();        }    }     private ScheduledExecutorService scheduledExecutorService;    private void startResetTask() {        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();        scheduledExecutorService.scheduleAtFixedRate(this, windowPeriod, windowPeriod, timeUnit);    }     @Override    public void run() {        // 获取当前窗口索引        Integer curIndex = (windowIndex + 1) % windowSize;//        log.info("curIndex = {}", curIndex);        // 重置当前窗口索引通过数量,并获取上一次通过数量        Integer count = windows[curIndex].passCount.getAndSet(0);        windowIndex = curIndex;        // 总通过数量 减去 当前窗口上次通过数量        passCount.addAndGet(-count);//        log.info("curOldCount = {}, passCount = {}, windows = {}", count, passCount, windows);    }     @Data    class Window {        private AtomicInteger passCount;        public Window() {            this.passCount = new AtomicInteger(0);        }    }     public static void main(String[] args) throws BlockException, InterruptedException {        SlidingWindowRateLimit slidingWindowRateLimit = new SlidingWindowRateLimit(100);        for(int i=1;i<10000000;i++){//            Thread.sleep(10);            if(slidingWindowRateLimit.canPass()){                System.out.println("我是第"+i+"个请求");            }        }     }}

 

1.2.3 漏桶算法
1、思想:漏桶算法是首先想象有一个木桶,桶的容量是固定的。当有请求到来时先放到木桶中,处理请求的worker以固定的速度从木桶中取出请求进行相应。如果木桶已经满了,直接返回请求频率超限的错误码或者页面SpringCloud Alibaba系列——14Sentinel简介及基本应用((上)-鸿蒙开发者社区2、适用场景:漏桶算法是流量最均匀的限流实现方式,一般用于流量“整形”。例如保护数据库的限流,先把对数据库的访问加入到木桶中,worker再以db能够承受的qps从木桶中取出请求,去访问数据库。

3、问题:木桶流入请求的速率是不固定的,但是流出的速率是恒定的。这样的话能保护系统资源不被打满,但是面对突发流量时会有大量请求失败,不适合电商抢购和微博出现热点事件等场景的限流。

4、代码实现

/** * LeakyBucketRateLimit * 漏桶算法 */@Slf4jpublic class LeakyBucketRateLimit implements RateLimit, Runnable {     /**     * 出口限制qps     */    private Integer limitSecond;    /**     * 漏桶队列     */    private BlockingQueue<Thread> leakyBucket;     private ScheduledExecutorService scheduledExecutorService;     public LeakyBucketRateLimit(Integer bucketSize, Integer limitSecond) {        this.limitSecond = limitSecond;        this.leakyBucket = new LinkedBlockingDeque<>(bucketSize);         scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();        long interval = (1000 * 1000 * 1000) / limitSecond;//周期秒。TimeUnit.NANOSECONDS(毫微秒,1秒=1000*1000*1000毫微秒        scheduledExecutorService.scheduleAtFixedRate(this, 0, interval, TimeUnit.NANOSECONDS);    }     @Override    public boolean canPass() throws BlockException {        if (leakyBucket.remainingCapacity() == 0) {            throw new BlockException();        }        leakyBucket.offer(Thread.currentThread());        LockSupport.park();        return true;    }     @Override    public void run() {        Thread thread = leakyBucket.poll();        if (Objects.nonNull(thread)) {            LockSupport.unpark(thread);        }    }    public static void main(String[] args) throws BlockException, InterruptedException {        LeakyBucketRateLimit leakyBucketRateLimit = new LeakyBucketRateLimit(100,10);        for(int i=1;i<10000000;i++){            if(leakyBucketRateLimit.canPass()){                System.out.println("我是第"+i+"个请求");            }        }    }}

 

1.2.4 令牌桶算法
1、思想:令牌桶是反向的"漏桶",它是以恒定的速度往木桶里加入令牌,木桶满了则不再加入令牌。服务收到请求时尝试从木桶中取出一个令牌,如果能够得到令牌则继续执行后续的业务逻辑。如果没有得到令牌,直接返回访问频率超限的错误码或页面等,不继续执行后续的业务逻辑。  SpringCloud Alibaba系列——14Sentinel简介及基本应用((上)-鸿蒙开发者社区2、适用场景:适合电商抢购或者微博出现热点事件这种场景,因为在限流的同时可以应对一定的突发流量。如果采用漏桶那样的均匀速度处理请求的算法,在发生热点时间的时候,会造成大量的用户无法访问,对用户体验的损害比较大。

3、代码实现

/** * TokenBucketRateLimit * 令牌桶 */@Slf4jpublic class TokenBucketRateLimit implements RateLimit, Runnable {     /**     * token 生成 速率 (每秒)     */    private Integer tokenLimitSecond;     /**     * 令牌桶队列     */    private BlockingQueue<String /* token */> tokenBucket;     private static final String TOKEN = "__token__";     private ScheduledExecutorService scheduledExecutorService;     public TokenBucketRateLimit(Integer bucketSize, Integer tokenLimitSecond) {        this.tokenLimitSecond = tokenLimitSecond;        this.tokenBucket = new LinkedBlockingDeque<>(bucketSize);         scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();        long interval = (1000 * 1000 * 1000) / tokenLimitSecond;        scheduledExecutorService.scheduleAtFixedRate(this, 0, interval, TimeUnit.NANOSECONDS);    }     @Override    public boolean canPass() throws BlockException {        String token = tokenBucket.poll();        if (StringUtils.isEmpty(token)) {            throw new BlockException();        }        return true;    }    @Override    public void run() {        if (tokenBucket.remainingCapacity() == 0) {            return;        }        tokenBucket.offer(TOKEN);    }}

 

4、guava的实现

  • 引包
<dependency>    <groupId>com.google.guava</groupId>    <artifactId>guava</artifactId>    <version>22.0</version></dependency>

 

代码举例

public class RateLimiterDemo {    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");    //线程数    private static final int THREAD_COUNT = 25;     public static void main(String[] args) {        RateLimiterDemo rateLimiterDemo = new RateLimiterDemo();        rateLimiterDemo.testRateLimiter1();    }     public void testRateLimiter1() {        //令牌桶算法        RateLimiter rateLimiter=RateLimiter.create(1); //TPS=1        Thread[] ts = new Thread[THREAD_COUNT];        for (int i = 0; i < THREAD_COUNT; i++) {            ts[i] = new Thread(new RateLimiterThread(rateLimiter), "RateLimiterThread-" + i);        }        for (int i = 0; i < THREAD_COUNT; i++) {            ts[i].start();        }        for(;;) {        }    }     public class RateLimiterThread implements Runnable {        private RateLimiter rateLimiter;        public RateLimiterThread(RateLimiter rateLimiter) {            this.rateLimiter = rateLimiter;        }        @Override        public void run() {            rateLimiter.acquire(1);            System.out.println(Thread.currentThread().getName() + "获取到了令牌,时间 = " + FORMATTER.format(new Date()));        }    }}

 

1.2.5 算法比较
SpringCloud Alibaba系列——14Sentinel简介及基本应用((上)-鸿蒙开发者社区

分类
已于2022-8-18 17:27:31修改
收藏
回复
举报
回复
    相关推荐