Gateway 限流与熔断:保障微服务稳定性

好的,没问题!咱们这就开始聊聊 Gateway 限流与熔断,这两个保障微服务稳定的好兄弟。

Gateway 限流与熔断:保障微服务稳定性

各位观众,大家好!今天咱们不聊风花雪月,来点硬核的——Gateway 限流与熔断。话说,微服务架构现在是炙手可热,但同时也带来了新的挑战。想象一下,你辛辛苦苦搭建了一套微服务系统,结果突然遭遇流量洪峰,系统瞬间崩溃,用户体验直线下降,老板脸色铁青……这可不是闹着玩的!

这时候,就需要我们的主角登场了:Gateway。Gateway 就像一个守门员,站在整个系统的最前端,负责接收所有外部请求,并将它们路由到相应的微服务。而限流和熔断,则是 Gateway 的两大法宝,可以有效地防止系统被流量冲垮,保证服务的稳定性。

一、限流:别让流量冲昏了头脑

限流,顾名思义,就是限制流量。就像高速公路收费站一样,控制车辆进入的速度,避免拥堵。在微服务架构中,限流可以防止恶意攻击、爬虫等异常流量涌入,也可以保护后端服务免受过载的压力。

1. 为什么需要限流?

  • 防止 DDoS 攻击: DDoS 攻击会产生大量的无效请求,消耗系统资源,导致服务瘫痪。限流可以有效地过滤掉这些恶意请求。
  • 保护后端服务: 当后端服务处理能力有限时,过多的请求会导致服务响应变慢,甚至崩溃。限流可以保证后端服务在可承受的范围内运行。
  • 防止资源耗尽: 某些接口可能会消耗大量的资源,例如数据库连接、CPU 资源等。限流可以防止这些接口被过度使用,导致资源耗尽。
  • 提升用户体验: 当系统负载过高时,用户体验会受到影响。限流可以保证系统在正常负载下运行,提升用户体验。

2. 常见的限流算法

限流算法有很多种,常见的有:

  • 计数器算法: 这是最简单粗暴的算法。在单位时间内,记录请求的数量,如果超过了设定的阈值,就拒绝后续的请求。

    public class CounterRateLimiter {
        private final int limit; // 允许的请求数量
        private final long period; // 时间窗口,单位毫秒
        private int counter; // 计数器
        private long startTime; // 时间窗口开始时间
    
        public CounterRateLimiter(int limit, long period) {
            this.limit = limit;
            this.period = period;
            this.counter = 0;
            this.startTime = System.currentTimeMillis();
        }
    
        public synchronized boolean allowRequest() {
            long now = System.currentTimeMillis();
            if (now - startTime > period) {
                // 超时,重置计数器
                counter = 0;
                startTime = now;
            }
    
            if (counter < limit) {
                counter++;
                return true; // 允许请求
            } else {
                return false; // 拒绝请求
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            CounterRateLimiter limiter = new CounterRateLimiter(5, 1000); // 每秒最多5个请求
    
            for (int i = 0; i < 10; i++) {
                if (limiter.allowRequest()) {
                    System.out.println("Request " + i + " allowed");
                } else {
                    System.out.println("Request " + i + " rejected");
                }
                Thread.sleep(200); // 模拟请求间隔
            }
        }
    }

    优点: 简单易实现。
    缺点: 存在临界问题。例如,在时间窗口的开始和结束时刻,可能会允许超过阈值的请求。

  • 滑动窗口算法: 相比于计数器算法,滑动窗口算法更加平滑。它将时间窗口划分为多个小窗口,每个小窗口都有一个计数器。每次请求到来时,都会更新相应小窗口的计数器。

    import java.util.LinkedList;
    import java.util.Queue;
    
    public class SlidingWindowRateLimiter {
        private final int limit; // 允许的请求数量
        private final long windowSize; // 时间窗口大小,单位毫秒
        private final Queue<Long> queue; // 请求时间戳队列
    
        public SlidingWindowRateLimiter(int limit, long windowSize) {
            this.limit = limit;
            this.windowSize = windowSize;
            this.queue = new LinkedList<>();
        }
    
        public synchronized boolean allowRequest() {
            long now = System.currentTimeMillis();
    
            // 移除过期请求
            while (!queue.isEmpty() && queue.peek() <= now - windowSize) {
                queue.poll();
            }
    
            if (queue.size() < limit) {
                queue.offer(now);
                return true; // 允许请求
            } else {
                return false; // 拒绝请求
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(5, 1000); // 每秒最多5个请求
    
            for (int i = 0; i < 10; i++) {
                if (limiter.allowRequest()) {
                    System.out.println("Request " + i + " allowed");
                } else {
                    System.out.println("Request " + i + " rejected");
                }
                Thread.sleep(200); // 模拟请求间隔
            }
        }
    }

    优点: 解决了计数器算法的临界问题,更加平滑。
    缺点: 实现相对复杂。

  • 漏桶算法: 漏桶算法就像一个固定容量的桶,请求就像水滴一样注入桶中。桶以恒定的速率漏水,如果请求的速度超过了漏水的速度,桶就会溢出,请求被丢弃。

    public class LeakyBucketRateLimiter {
        private final int capacity; // 桶的容量
        private final double leakRate; // 漏水速率,单位:请求/毫秒
        private int water; // 桶中当前水量
        private long lastLeakTime; // 上次漏水时间
    
        public LeakyBucketRateLimiter(int capacity, double leakRate) {
            this.capacity = capacity;
            this.leakRate = leakRate;
            this.water = 0;
            this.lastLeakTime = System.currentTimeMillis();
        }
    
        public synchronized boolean allowRequest() {
            long now = System.currentTimeMillis();
    
            // 计算漏水
            double leakedWater = (now - lastLeakTime) * leakRate;
            water = Math.max(0, water - (int) leakedWater);
            lastLeakTime = now;
    
            if (water < capacity) {
                water++;
                return true; // 允许请求
            } else {
                return false; // 拒绝请求
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(5, 0.005); // 桶容量为5,漏水速率为每毫秒0.005个请求 (相当于每秒5个请求)
    
            for (int i = 0; i < 10; i++) {
                if (limiter.allowRequest()) {
                    System.out.println("Request " + i + " allowed");
                } else {
                    System.out.println("Request " + i + " rejected");
                }
                Thread.sleep(100); // 模拟请求间隔
            }
        }
    }

    优点: 可以平滑流量,防止突发流量对系统造成冲击。
    缺点: 无法应对短时间内的突发流量。

  • 令牌桶算法: 令牌桶算法就像一个装满令牌的桶,每个请求都需要从桶中获取一个令牌才能通过。桶会以恒定的速率生成令牌,如果桶中没有令牌了,请求就会被拒绝。

    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class TokenBucketRateLimiter {
        private final int capacity; // 桶的容量
        private final double refillRate; // 令牌生成速率,单位:令牌/秒
        private AtomicInteger tokens; // 当前令牌数量
        private final ScheduledExecutorService scheduler;
    
        public TokenBucketRateLimiter(int capacity, double refillRate) {
            this.capacity = capacity;
            this.refillRate = refillRate;
            this.tokens = new AtomicInteger(capacity);
            this.scheduler = Executors.newScheduledThreadPool(1);
    
            // 定期补充令牌
            scheduler.scheduleAtFixedRate(this::refill, 0, (long) (1000 / refillRate), TimeUnit.MILLISECONDS);
        }
    
        private void refill() {
            int newTokens = Math.min(capacity, tokens.get() + 1); // 每次最多补充一个令牌,不能超过桶的容量
            tokens.set(newTokens);
        }
    
        public boolean allowRequest() {
            return tokens.getAndUpdate(t -> Math.max(0, t - 1)) > 0; // 尝试获取一个令牌,如果令牌数量大于0,则允许请求
        }
    
        public void shutdown() {
            scheduler.shutdown();
        }
    
        public static void main(String[] args) throws InterruptedException {
            TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(5, 5); // 桶容量为5,令牌生成速率为每秒5个
    
            for (int i = 0; i < 10; i++) {
                if (limiter.allowRequest()) {
                    System.out.println("Request " + i + " allowed, tokens remaining: " + limiter.tokens.get());
                } else {
                    System.out.println("Request " + i + " rejected, tokens remaining: " + limiter.tokens.get());
                }
                Thread.sleep(100); // 模拟请求间隔
            }
    
            limiter.shutdown(); // 关闭定时任务
        }
    }

    优点: 可以应对突发流量,允许一定程度的 burst。
    缺点: 实现相对复杂。

3. Gateway 中如何实现限流?

现在流行的 Gateway,例如 Spring Cloud Gateway、Kong 等,都提供了限流的功能。你可以通过简单的配置来实现限流。

  • Spring Cloud Gateway:

    Spring Cloud Gateway 提供了 RequestRateLimiterGatewayFilterFactory 过滤器,可以基于 Redis 或其他存储介质实现限流。

    @Configuration
    public class GatewayConfig {
    
        @Bean
        public KeyResolver userKeyResolver() {
            return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
        }
    
        @Bean
        public RedisRateLimiter redisRateLimiter() {
            return new RedisRateLimiter(5, 10); // 允许每秒5个请求,burstCapacity为10
        }
    
        @Bean
        public RouteLocator routes(RouteLocatorBuilder builder, KeyResolver userKeyResolver, RedisRateLimiter redisRateLimiter) {
            return builder.routes()
                    .route("user_route", r -> r.path("/user/**")
                            .filters(f -> f.requestRateLimiter(config -> {
                                config.setRateLimiter(redisRateLimiter);
                                config.setKeyResolver(userKeyResolver);
                            }))
                            .uri("lb://user-service")) // user-service 是你的微服务名称
                    .build();
        }
    }

    这个例子中,我们使用了 Redis 来存储限流信息,并根据 user 参数来区分不同的用户。

  • Kong:

    Kong 提供了 Rate Limiting 插件,可以基于不同的策略来实现限流。

    # 添加 Rate Limiting 插件到 Service
    curl -i -X POST 
      --url http://kong:8001/services/{service_id}/plugins 
      --data "name=rate-limiting" 
      --data "config.minute=5"  
      --data "config.policy=local"
    
    # 或者,添加 Rate Limiting 插件到 Route
    curl -i -X POST 
      --url http://kong:8001/routes/{route_id}/plugins 
      --data "name=rate-limiting" 
      --data "config.minute=5" 
      --data "config.policy=local"

    这个例子中,我们限制了每个用户每分钟最多可以访问 5 次。

二、熔断:保护自己,也保护他人

熔断,来源于电路保护机制。当电路中出现短路时,熔断器会自动断开电路,防止更大的损失。在微服务架构中,熔断是指当某个服务出现故障时,Gateway 会自动切断对该服务的请求,防止故障扩散,保护整个系统的稳定性。

1. 为什么需要熔断?

  • 防止服务雪崩: 当某个服务出现故障时,如果没有熔断机制,大量的请求会涌向该服务,导致服务压力过大,最终崩溃。而该服务的崩溃又会影响到其他依赖于它的服务,最终导致整个系统崩溃,这就是服务雪崩。
  • 快速失败: 当某个服务出现故障时,熔断机制可以快速返回错误,避免用户长时间等待,提升用户体验。
  • 自动恢复: 熔断机制通常会提供自动恢复的功能。当故障服务恢复正常后,Gateway 会逐渐恢复对该服务的请求。

2. 熔断器的状态

熔断器通常有三种状态:

  • Closed(关闭): 这是熔断器的正常状态。所有的请求都会被转发到后端服务。
  • Open(开启): 当后端服务出现故障时,熔断器会进入开启状态。所有的请求都会被直接拒绝,不会转发到后端服务。
  • Half-Open(半开启): 在开启状态一段时间后,熔断器会进入半开启状态。Gateway 会尝试向后端服务发送少量的请求,如果请求成功,则认为服务已经恢复正常,熔断器会回到关闭状态;如果请求失败,则熔断器会回到开启状态。

3. 熔断策略

常见的熔断策略有:

  • 基于错误率: 当错误率超过设定的阈值时,熔断器会进入开启状态。
  • 基于响应时间: 当响应时间超过设定的阈值时,熔断器会进入开启状态。
  • 基于请求数量: 当请求数量超过设定的阈值,并且错误率也超过设定的阈值时,熔断器会进入开启状态。

4. Gateway 中如何实现熔断?

  • Spring Cloud Gateway:

    Spring Cloud Gateway 提供了 Resilience4J 集成,可以方便地实现熔断、限流、重试等功能。

    @Configuration
    public class GatewayConfig {
    
        @Bean
        public Customizer<Resilience4JCircuitBreakerConfigurationBuilder> defaultCustomizer() {
            return builder -> builder.circuitBreakerConfig(CircuitBreakerConfig.custom()
                            .failureRateThreshold(50) // 错误率阈值:50%
                            .waitDurationInOpenState(Duration.ofSeconds(10)) // 熔断器开启状态的持续时间:10秒
                            .slidingWindowSize(10) // 滑动窗口大小:10个请求
                            .build())
                    .timeLimiterConfig(TimeLimiterConfig.custom()
                            .timeoutDuration(Duration.ofSeconds(2)) // 超时时间:2秒
                            .build());
        }
    
        @Bean
        public RouteLocator routes(RouteLocatorBuilder builder) {
            return builder.routes()
                    .route("user_route", r -> r.path("/user/**")
                            .filters(f -> f.circuitBreaker(config -> config.setName("userCircuitBreaker")
                                    .setFallbackUri("forward:/fallback"))) // fallback URI
                            .uri("lb://user-service"))
                    .build();
        }
    
        @GetMapping("/fallback")
        public String fallback() {
            return "User service is unavailable, please try again later.";
        }
    }

    这个例子中,我们使用了 Resilience4J 的 CircuitBreaker 来实现熔断。当 user-service 的错误率超过 50% 时,熔断器会进入开启状态,并跳转到 /fallback 接口。

  • Kong:

    Kong 提供了 Circuit Breaker 插件,可以基于不同的策略来实现熔断。

    # 添加 Circuit Breaker 插件到 Service
    curl -i -X POST 
      --url http://kong:8001/services/{service_id}/plugins 
      --data "name=circuit-breaker" 
      --data "config.tripped=true" 
      --data "config.consecutive_errors=5" 
      --data "config.error_percent=80" 
      --data "config.request_volume=10"
    
    # 或者,添加 Circuit Breaker 插件到 Route
    curl -i -X POST 
      --url http://kong:8001/routes/{route_id}/plugins 
      --data "name=circuit-breaker" 
      --data "config.tripped=true" 
      --data "config.consecutive_errors=5" 
      --data "config.error_percent=80" 
      --data "config.request_volume=10"

    这个例子中,我们配置了当连续出现 5 个错误,或者错误率超过 80%,且请求数量超过 10 个时,熔断器会进入开启状态。

三、总结

特性 作用 常用算法/工具 优点 缺点
限流 控制流量,防止系统过载,保护后端服务 计数器、滑动窗口、漏桶、令牌桶、Spring Cloud Gateway RequestRateLimiter、Kong Rate Limiting 简单易用,保护后端服务,防止资源耗尽,提升用户体验 算法选择需要根据具体场景考虑,可能存在临界问题,无法应对突发流量
熔断 当某个服务出现故障时,自动切断对该服务的请求,防止故障扩散,保护整个系统的稳定性 Resilience4J CircuitBreaker, Kong Circuit Breaker 防止服务雪崩,快速失败,自动恢复,提升用户体验 配置复杂,需要根据服务情况调整阈值,fallback接口需要妥善处理

限流和熔断是保障微服务稳定性的两大法宝。通过合理地配置限流和熔断策略,可以有效地防止系统被流量冲垮,保证服务的可用性。

四、最佳实践

  • 选择合适的限流算法: 根据不同的业务场景选择合适的限流算法。例如,对于需要平滑流量的场景,可以选择漏桶算法或令牌桶算法;对于需要应对突发流量的场景,可以选择令牌桶算法。
  • 设置合理的限流阈值: 限流阈值需要根据后端服务的处理能力来设置。过高的阈值无法起到保护作用,过低的阈值会影响正常用户的访问。
  • 配置合适的熔断策略: 熔断策略需要根据服务的错误率、响应时间等指标来设置。过高的阈值无法及时熔断,过低的阈值会导致误判。
  • 提供 fallback 机制: 当熔断器开启时,需要提供 fallback 机制,例如返回默认值、缓存数据等,避免用户看到错误页面。
  • 监控和告警: 对限流和熔断的指标进行监控和告警,及时发现问题并进行处理。

好了,今天就聊到这里。希望这篇文章能帮助大家更好地理解 Gateway 限流与熔断,保护好你的微服务系统!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注