三层限流:为高并发系统保驾护航

文章目录

  • 前言
  • 一、第一层限流:Nginx 层面的 IP 限流
  • 二、第二层限流:Gateway 对用户层级的限流
  • 三、第三层限流:微服务限流
    • 分布式限流和单机限流的优缺点:
    • 1、RateLimiter的使用
    • 2、Hystrix的使用
    • 3、Redis+lua脚本
    • 4、使用Sentinel
  • 四、关于为何同时使用 Nginx 和 Spring Cloud Gateway
  • 总结

前言

在高并发网络环境中,确保系统的可用性、稳定性以及防范恶意流量攻击至关重要。为此,在我们的项目中构建了三层限流设计。

第一层是 Nginx 层面的 IP 限流,借助 Nginx 的 http_limit_req_module 模块,依据用户 IP 设限,这是抵御恶意 IP 的 DDoS 攻击、阻挡大量非法请求深入系统的首道防线。

第二层为 gateway 针对用户层级的限流,通过用户的唯一标识如 user_id,控制每个用户在单位时间内的请求数量,确保公平,避免单一用户过度占用资源而影响他人体验。

第三层则是微服务限流,每个微服务运用如 Google 的 Guava RateLimiter 等技术限流,防止服务过载影响系统稳定,且各服务根据自身能力和业务需求独立设定限流阈值。

三层限流的结构图如下:
三层流程结构

一、第一层限流:Nginx 层面的 IP 限流

Nginx 的 http_limit_req_module 模块是我们构建的第一道坚固防线。 其工作原理基于定义一个明确的“速率”值,用于对单位时间内的请求数量进行严格的限制。比如说,您可以设定每分钟只处理 100 个请求。当某个客户端的请求速率超过预先设定的限制时,Nginx 就会地将这些请求放入一个专门的队列中,等待后续的处理。然而,如果队列中的请求数量过多,或者等待处理的时间超出了可接受的范围,那么这些请求将会被果断丢弃。

在配置方面,首先需要在 Nginx 的 http 块中使用 limit_req_zone 指令来定义限制速率的区域。例如,如果我们决定根据客户端的 IP 进行限制,限流20MB,每秒允许处理1000个请求,配置如下:

http http {   
 limit_req_zone $binary_remote_addr zone=perip:20m rate=1000r/s; 
 ...
} 

这里,$binary_remote_addr 代表客户端的 IP 地址,zone=totalLimit:20m 定义了一个名为 totalLimit 的存储区域,其大小为 20M,用于存储每个 IP 的状态信息,而 rate=10r/s 则清晰地设定了每秒 1000 个请求的限制速率。

接下来,在需要应用这个限制的 server 块或 location 块中,使用 limit_req 指令来设定这个限制。例如:

json server {    
	location / {        
	limit_req zone=totalLimit burst=1000 nodelay;
    ...
	}
}

​ 在上述配置中,zone=totalLimit 明确表示应用之前定义的 totalLimit 区域,而 burst=1000 则意味着允许在短时间内超过定义的速率,最多累积 1000 个请求等待处理。 通过调整 rateburstnodelay 等配置参数,我们能够根据不同的业务需求灵活定制限流策略。比如,假设我们的业务主要面向个人用户,并且大部分时间请求量相对稳定,但在某些特定的高峰期会出现请求量的突然增加。在这种情况下,我们可能会选择设定一个适中的 rate ,并同时允许一定数量的 burst ,以在保障系统稳定性的同时,最大程度地优化用户体验。

二、第二层限流:Gateway 对用户层级的限流

为了进一步增强系统的限流效果和精细化管理,我们在 API 网关层面也实施了限流策略。 首先,在 Spring Cloud Gateway 中,通过在配置文件中明确地定义限流规则,我们能够基于 user_id 这一关键标识,精准地控制每个用户在单位时间内所能发送的请求数量。以下是一个针对 user_id 进行限流的配置示例:

spring:
  cloud:
    gateway:
      routes:
      - id: user_route
        uri: http://mybackend.com
        predicates:
        - Path=/api/**
        filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 10
            redis-rate-limiter.burstCapacity: 20
            key-resolver: "#{@userIdResolver}"

在这个配置中,redis-rate-limiter.replenishRate 定义了每秒可以处理的请求数量,而 redis-rate-limiter.burstCapacity 则设定了可以接受的突发请求数量。同时,key-resolver 用于明确如何从请求中准确获取 user_id

为了实现从请求中提取 user_id ,我们需要实现一个 KeyResolver 接口。以下是一个使用 Java 语言实现的示例代码,展示了如何从请求的查询参数中获取 user_id

1、引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>3.3.1</version>
</dependency>

2、配置获取用户id的方法

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Configuration
public class RateLimiterConfiguration
    
    @Bean
    KeyResolver userIdKeyResolver(){
    	return Mono.just(
            exchange.getRequest().getQueryParams().getFirst("user_id")
        );
	}
}

3、配置c配置文件中的过滤配置

spring:
  cloud:
    gateway:
        filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 1000     # 令牌桶每秒填充平均速率
            redis-rate-limiter.burstCapacity: 2000     # 令牌桶的上限
            key-resolver: "#{@userIdResolver}"        # 使用spel表达式从spring容器中获取Bean对象

网关层面限流的好处众多,通过这样的设计,极大地简化了整个系统的工作流程和管理复杂度。 网关层面实现限流机制带来了诸多显著的好处。

首先,它能够为所有的应用程序提供一个统一的流量控制管理,使得我们能够在一个集中的位置对所有服务的流量进行有效的监管和控制,而无需在每个服务中分别进行复杂的配置。

其次,限流机制能够有效地防止任何一个服务由于过大的流量冲击而陷入崩溃的境地,从而显著增强了系统的稳定性。

再者,它能够有效地对系统中的每个用户的请求进行精确控制,避免了某些用户过度占用系统资源而对其他用户的体验造成不良影响。

最后,对于可预期的高流量请求场景,我们能够在网关层面迅速而灵活地进行调整和应对,保障系统的正常运行。

三、第三层限流:微服务限流

在每个微服务内部,每个微服务都能够根据自身的处理能力和独特的业务需求,独立设定限流阈值。这种个性化的设置确保了每个微服务在面对不同的负载情况时,都能够保持稳定的性能和可靠的服务质量,从而有效地防止了某个服务的过载对整个系统的稳定性产生不利影响。

在这层中有多种技术来实现微服务限流,可以采用,如 Google 的 Guava RateLimiter、SpringCloud的Hystrix等框架来实施单机限流策略,也可以采用 阿里的Sentinel、或者自己用Redis+lua脚本实现分布式限流对微服务多个实例统一限流。

分布式限流和单机限流的优缺点:

分布式限流的优点:

  1. 全局一致:确保整个系统限流策略统一,维持稳定。
  2. 适应高并发:处理大规模、高流量场景,保障系统稳定。
  3. 弹性扩展:随系统规模变化能灵活调整限流策略。
  4. 精准控制:依据系统整体情况精确限流。

**分布式限流的缺点: **

  1. 复杂:实现和维护难度大,涉及协调和同步问题。

  2. 有性能开销:节点通信带来一定性能损失。

  3. 依赖外部组件:增加系统依赖和故障点。

单机限流的优点:

  1. 简单:实现逻辑简单,无需复杂协调机制。

  2. 低开销:无节点通信,性能影响小。

  3. 独立:限流策略不受其他节点干扰。

单机限流的缺点:

  1. 局限:只能处理单机器流量。
  2. 缺乏协调:不同机器策略难统一,影响系统稳定。
  3. 难扩展:无法直接用于多机环境。

1、RateLimiter的使用

RateLimiter使用起来比较简单,代码示例如下

 public static void main(String[] args) {
        // 创建一个每秒放入5个令牌的RateLimiter
        RateLimiter limiter = RateLimiter.create(100.0);

        for (int i = 0; i < 10; i++) {
            // 请求一个令牌
            limiter.acquire();
            System.out.println("处理请求: " + i);
        }
    }

这段代码创建了一个RateLimiter,它每秒产生100个令牌。在一个循环中,我们通过acquire()方法从RateLimiter获取令牌。如果令牌不够,acquire()会阻塞,直到获取到令牌

2、Hystrix的使用

Hystrix的限流是基于线程池的所以在配置文件里设置hystrix线程池的核心线程数就可实现限流

hystrix:
  threadpool:
    default:
      coreSize: 200 #并发执行的最大线程数,默认10
      maxQueueSize: 1000 #BlockingQueue的最大队列数,默认值-1
      queueSizeRejectionThreshold: 800 #即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝,默认值5

3、Redis+lua脚本

limit.lua

local count
- 获取调用脚本时传入的第一个key值(用作限流的 key)
count = redis.call('get',KEYS[1])
-- 获取调用脚本时传入的第一个参数值(限流大小)
if count and tonumber(count) > tonumber(ARGV[1]) then
    return count;
end
    count = redis.call('incr',KEYS[1])
if tonumber(count) == 1 then
    --从第一次调用开始限流,设置对应key的过期时间
    redis.call('expire',KEYS[1],ARGV[2])
end
return count;

大致的代码逻辑,实际使用建议封装成一个注解来使用

	DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
    redisScript.setResultType(Number.class);
    ClassPathResource classPathResource = new ClassPathResource(LIMIT_LUA_PATH);
    try {
        classPathResource.getInputStream();//探测资源是否存在
        redisScript.setScriptSource(new ResourceScriptSource(classPathResource));
    } catch (IOException e) {
        logger.error("未找到文件:{}", LIMIT_LUA_PATH);
    }	
	List result = stringRedisTemplate.execute(redisScript, keyList, 					String.valueOf(value),String.valueOf(time));
 


		Object result = stringRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
 		if (result == null) {
            //降级
        } 
		Integer count = Integer.valueOf(result.toString());
        if (count <= limitCount) {
            //执行业务逻辑
        } else {
            //降级
        }

4、使用Sentinel

Sentinel 是阿里出品的一个功能强大的分布式限流框架,具体使用可以参考下面的官方文档

quick-start | Sentinel (sentinelguard.io)

四、关于为何同时使用 Nginx 和 Spring Cloud Gateway

首先,我们的项目主要基于 Spring Boot 和 Spring Cloud 进行开发,而 Spring Cloud Gateway 能够与这些技术实现无缝的集成。这种紧密的集成特性不仅减少了我们在处理不同组件之间兼容性问题上所花费的时间和精力,还极大地提高了开发和维护的效率。

其次,Spring Cloud Gateway 支持非阻塞的方式来处理请求,这一特性在处理高并发请求时表现出了显著的优势,是 Nginx 所无法提供的。非阻塞的处理方式能够更高效地利用系统资源,提升系统的整体性能和响应速度。

再者,Spring Cloud Gateway 允许我们通过动态的编程方式来定义路由规则,与 Nginx 相对静态的配置方式形成了鲜明的对比。这种动态定义路由规则的能力使我们能够更加灵活地应对复杂多变的业务需求和系统架构调整,为系统的持续演进提供了有力的支持。

最后,Spring Cloud Gateway 还集成了 Spring Cloud 的服务发现功能,以及与 Spring Cloud 集群紧密配合的断路、降级和限流等机制。这些集成的功能共同强化了微服务架构的健壮性和可靠性,确保系统在面对各种异常情况和高负载场景时,依然能够保持稳定的运行状态,为用户提供持续、优质的服务。

综上所述,虽然 Nginx 已经在网络服务领域展现出了强大的实力,但在我们特定的项目架构中,Spring Cloud Gateway 凭借其与现有技术栈的高度适配性、独特的功能特性以及对微服务架构的全面支持,更加符合我们的项目需求。

总结

综上所述,这三层限流设计相互协作、相辅相成,从不同的层面和角度全方位地保障了系统在高并发场景下的稳定运行。

  • 31
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知北游z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值