使用redis在SpringCloud gateway中进行速率限制
目前,Spring Cloud Gateway是仅次于Spring Cloud Netflix的第二个最受欢迎的Spring Cloud项目(就GitHub上的星数而言)。它是作为Spring Cloud系列中Zuul代理的继任者而创建的。该项目提供了用于微服务体系结构的API网关,并基于反应式Netty和Project Reactor构建。它旨在提供一种简单但有效的方法来路由到API并解决诸如安全性,监视/度量和弹性之类的普遍关注的问题。 Spring Cloud Gateway为您提供了许多功能和配置选项。今天,我将集中讨论网关配置的一个非常有趣的方面- 速率限制。速率限制器可以定义为一种控制在网络上发送或接收的流量速率的方法。我们还可以定义几种类型的速率限制。Spring Cloud Gateway当前提供了Request Rate Limiter,它负责将每个用户每秒限制为N个请求。与Spring Cloud Gateway一起使用时RequestRateLimiter,我们可能会利用Redis。Spring Cloud实现使用令牌桶算法做限速。该算法具有集中式存储桶主机,您可以在其中对每个请求获取令牌,然后将更多的令牌缓慢滴入存储桶。如果存储桶为空,则拒绝该请求。
1.依存关系
我们将针对较高流量下的速率限制测试示例应用程序。首先,我们需要包括一些依赖项。当然,需要Spring Cloud Gateway启动器。为了使用Redis处理速率限制器,我们还需要向spring-boot-starter-data-redis-reactive启动器添加依赖项。其他依赖关系用于测试目的。mockserver测试容器内提供的模块。它负责模拟目标服务。反过来,该库在测试期间mockserver-client-java用于与mockserver容器集成。最后一个库junit-benchmarks用于基准测试方法并同时运行测试。
org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-data-redis-reactive org.projectlombok lombok org.springframework.boot spring-boot-starter-test test org.testcontainers mockserver 1.12.3 test org.mock-server mockserver-client-java 3.10.8 test com.carrotsearch junit-benchmarks 0.7.2 test
该示例应用程序基于Spring Boot构建,2.2.1.RELEASE并使用Spring Cloud Hoxton.RC2Release Train。
org.springframework.boot spring-boot-starter-parent 2.2.1.RELEASE UTF-8 UTF-8 11 Hoxton.RC2 org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import
2.实施
请求速率限制是通过使用称为的Spring Cloud Gateway组件实现的GatewayFilter。该过滤器的每个实例都由一个特定的工厂构造。过滤器当然负责在发送下游请求之前或之后修改请求和响应。当前,有30个可用的内置 网关过滤器工厂。 在GatewayFilter有一个可选的keyResolver参数和参数特定于速率限制器的实现(在这种情况下使用的Redis的实施方案)。参数keyResolver是实现KeyResolver接口的bean 。它允许您应用不同的策略来导出限制请求的密钥。参数keyResolver是一个实现KeyResolver接口。它允许您应用不同的策略来导出限制请求的密钥。遵循Spring Cloud Gateway文档:
的默认实现KeyResolver是,PrincipalNameKeyResolver它Principal从ServerWebExchange和调用检索Principal.getName()。默认情况下,如果KeyResolver找不到密钥,则请求将被拒绝。可以使用spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key(true或false)和spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code属性来调整此行为。
由于我们已经讨论了速率限制的一些理论方面,因此我们可以继续进行实施。首先,让我们定义主类和非常简单的KeyResolverbean,它始终等于一个。
@SpringBootApplicationpublic class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } @Bean KeyResolver userKeyResolver() { return exchange -> Mono.just("1"); }}
假设我们具有以下配置,并且目标应用程序在端口上运行,8091我们可以执行一些测试调用。您可以设置两个属性以自定义过程。该redis-rate-limiter.replenishRate决定每秒用户多少个请求被允许发送,没有任何下降的请求。这是令牌桶被填充的速率。第二个属性redis-rate-limiter.burstCapacity是允许用户在一秒钟内执行的最大请求数。这是令牌桶可以容纳的令牌数。将此值设置为零将阻止所有请求。
server: port: ${PORT:8085} spring: application: name: gateway-service redis: host: 192.168.99.100 port: 6379 cloud: gateway: routes: - id: account-service uri: http://localhost:8091 predicates: - Path=/account/** filters: - RewritePath=/account/(?.*), /${path} - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20
现在,如果您呼叫网关公开的端点,则会收到以下响应。它包括一些特定的标头,其前缀为x-ratelimit。标头x-ratelimit-burst-capacity指示burstCapacity值,x-ratelimit-replenish-rate指示replenishRate值,最重要,指示x-ratelimit-remaining您可能在下一秒内发送的请求数。
如果您超出了允许的请求数量,Spring Cloud Gateway将返回带有code的响应HTTP 429 - Too Many Requests,并且不会处理传入的请求。
3.测试
我们有一个Spring Boot测试,该测试使用了Testcontainers提供的两个Docker容器: MockServer和Redis。因为暴露端口是动态生成的,所以我们需要@BeforeClass在运行测试之前在方法中设置网关属性。在init方法内部,我们还用于MockServerClient在模拟服务器容器上定义模拟服务。我们的测试方法在六个线程中同时运行,并重复了600次。
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)@RunWith(SpringRunner.class)public class GatewayRateLimiterTest { private static final Logger LOGGER = LoggerFactory.getLogger(GatewayRateLimiterTest.class); @Rule public TestRule benchmarkRun = new BenchmarkRule(); @ClassRule public static MockServerContainer mockServer = new MockServerContainer(); @ClassRule public static GenericContainer redis = new GenericContainer("redis:5.0.6").withExposedPorts(6379); @Autowired TestRestTemplate template; @BeforeClass public static void init() { System.setProperty("spring.cloud.gateway.routes[0].id", "account-service"); System.setProperty("spring.cloud.gateway.routes[0].uri", "" + mockServer.getServerPort()); System.setProperty("spring.cloud.gateway.routes[0].predicates[0]", "Path=/account/**"); System.setProperty("spring.cloud.gateway.routes[0].filters[0]", "RewritePath=/account/(?.*), /${path}"); System.setProperty("spring.cloud.gateway.routes[0].filters[1].name", "RequestRateLimiter"); System.setProperty("spring.cloud.gateway.routes[0].filters[1].args.redis-rate-limiter.replenishRate", "10"); System.setProperty("spring.cloud.gateway.routes[0].filters[1].args.redis-rate-limiter.burstCapacity", "20"); System.setProperty("spring.redis.host", "192.168.99.100"); System.setProperty("spring.redis.port", "" + redis.getMappedPort(6379)); new MockServerClient(mockServer.getContainerIpAddress(), mockServer.getServerPort()) .when(HttpRequest.request() .withPath("/1")) .respond(response() .withBody("{"id":1,"number":"1234567890"}") .withHeader("Content-Type", "application/json")); } @Test @BenchmarkOptions(warmupRounds = 0, concurrency = 6, benchmarkRounds = 600) public void testAccountService() { ResponseEntity r = template.exchange("/account/{id}", HttpMethod.GET, null, Account.class, 1); LOGGER.info("Received: status->{}, payload->{}, remaining->{}", r.getStatusCodeValue(), r.getBody(), r.getHeaders().get("X-RateLimit-Remaining")); } }
让我们看一下测试结果。启动网关后,用户可以在一秒钟内发送最多20个请求。超过此值后,它将开始返回HTTP 429。
删除某些传入请求后,网关将在下一秒开始接受它们。但是这一次它只允许处理10个请求,这等于replenishRate参数值。