
上图中是一个水坝泄洪的图,那么,对于软件系统,如何使用最方便的可编程的方式增加服务限流能力呢?
下面我结合一个常规的springCloud项目实践了一把,希望他山之石可以攻玉。
简单使用jmeter,压20个并发,访问 列表查询接口 /worksheet/findInfo, 对应的服务崩溃。【apprun,common】
架构复杂度的一个种类是: 保护API和服务端点免受攻击,
比如:拒绝服务,级联失败,或者 超额使用资源。
限流是一种技术,来控制API或者服务的消费速度,在分布式系统中,没有比集中式的配置和管理API的消费速度更好的选择,
只有这些请求在限定的速度内访问,才能保证API的正常,更多的将会产生Http的 请求频繁错误。
交互模型图:

SpringCloudGateway是一个简单和轻量级的组件,也是一种管理限制API的消费速度有效的方式。
springCloudGateway的限流模型:

当前企业600人,按照两倍估算,即1200人使用,高频接口秒并发限制为20, 即有20个人同时使用同一个接口操作数据。

需要增加限流和熔断的点:
| 组件 | 增加限制 | 业务说明 |
|---|---|---|
| openresty | 限流,熔断 【统一】 | 保证流量再nginx的处理阈值,参考数据:5W/S |
| gateway | 限流,熔断 【统一】 | 保证每个API的访问速度在20/S 峰值40 ; |
| apprun | 高频接口限流,每个接口统一分类定制熔断逻辑 | 限流可以复用封装的组件,熔断采用最简单的hystix ; |
| devops | 高频接口限流,每个接口统一分类定制熔断逻辑 | 限流可以复用封装的组件,熔断采用最简单的hystix ; |
| common | 高频接口限流,每个接口统一分类定制熔断逻辑,feign定制熔断逻辑 | 限流可以复用封装的组件,熔断采用最简单的hystix ; |
| job | 高频接口限流,每个接口统一分类定制熔断逻辑,feign定制熔断逻辑 | 限流可以复用封装的组件,熔断采用最简单的hystix ; |
网关做整体限制,接口由业务来增加限流。
RequestRateLimiter GatewayFilter工厂使用了RateLimiter实现来决定当前的并发请求是否允许处理,
如果不能处理,默认返回状态码 429 - 太多请求;
这个过滤器采用了可选的KeyResolver参数和对于速度限制的特殊参数,下面会介绍。
keyResolver是一个实体实现了KeyResolver接口,配置指向一个bean的名字,
使用SpEL表达式。 #{@myKeyResolver} 是一个SPEL表达式指向了一个叫做myKeyResolver的bean,下面展示了 KeyResolver接口;
public interface KeyResolver {
Mono<String> resolve(ServerWebExchange exchange);
}
keyResolver接口是的插件策略驱动请求限制,再未来的里程碑版本,将会由一些KeyResolver的实现。
默认实现KeyResolver的类是 PrincipalNameKeyResolver, 会接受ServerWebExchange的Principal参数, 并且会调用 Principal.getName()方法。
默认的,如果KeyResolver没有找到key, 请求会被拒绝,你可以配置这个行为。
spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key=true
spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code=xxxx
注意: RequestRateLimiter没有配置短注解,下面的例子是非法的。
spring.cloud.gateway.routes[0].filters[0]=RequestRateLimiter=2, 2, #{@userkeyresolver}
Redis实现是基于Stripe . 它需要使用 spring-boot-starter-data-redis-reactive 这个starter ;
算法使用的是令牌桶。
| key | 业务含义 | 用途 |
|---|---|---|
| redis-rate-limiter.replenishRate | 一个用户每秒多少请求数,不包含丢弃的请求,这个速度就是令牌桶的数量。 | 补充速度 |
| redis-rate-limiter.burstCapacity | 用户每秒允许最大的请求数量,这个令牌数量就是令牌桶可以持有的数量,设置为0标识阻塞所有请求 | 突增容量 |
| redis-rate-limiter.requestedTokens | 单个请求消耗多少令牌,这个数量就是从令牌桶中每个请求获取令牌的数量,默认是1 | 请求消耗令牌数量 |
如果你把 replenishRate 和 burstCapacity值设置为一样,则完成了一个稳定的速度设置。
临时突增流量可以允许设置 burstCapacity > replenishRate ,
这种场景下,RateLimiter需要允许一些时间在 burstCapacity和 replenishRate 之间 。
两种连续的徒增会导致丢弃请求,下面的例子配置了一个 redis-rate-limit.
速度限制在1个请求每秒, replenishRate=1, requestedTokens=60,burstCapacity=60 ;
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
上面的配置补充令牌的速度是10, 突增容量是20,但是在下一秒,只有10个请求是可以进入的;
下面的例子配置了一个KeyResolver。简单的从请求参数中获取user(在生产环境不推荐使用),
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
你也可以定义自己的RateLImiter,作为一个bean,实现RateLimiter接口即可,
在下面的配置中。你可以引用一个bean通过名字,使用SpEL表达式。
下面的例子定义了一个rateLimite并且使用自定义的KeyResolver.
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
rate-limiter: "#{@myRateLimiter}"
key-resolver: "#{@userKeyResolver}"
对所有的请求,限制如下。
| key | value | 设置值原因 |
|---|---|---|
| replenishRate | 20 | 每个用户每秒处理请求速度 为20 |
| burstCapacity | 40 | 40,每秒处理请求数量突增容量 ; |
| requestedTokens | 1 | 每个连接耗费1个令牌; |
源代码分析: RequestRateLimiterGatewayFilterFactory
public GatewayFilter apply(Config config) {
KeyResolver resolver = getOrDefault(config.keyResolver, defaultKeyResolver);
RateLimiter<Object> limiter = getOrDefault(config.rateLimiter, defaultRateLimiter);
boolean denyEmpty = getOrDefault(config.denyEmptyKey, this.denyEmptyKey);
HttpStatusHolder emptyKeyStatus = HttpStatusHolder
.parse(getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));
return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> {
if (EMPTY_KEY.equals(key)) {
if (denyEmpty) {
setResponseStatus(exchange, emptyKeyStatus);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
String routeId = config.getRouteId();
if (routeId == null) {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
routeId = route.getId();
}
return limiter.isAllowed(routeId, key).flatMap(response -> {
for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
}
if (response.isAllowed()) {
return chain.filter(exchange);
}
setResponseStatus(exchange, config.getStatusCode());
return exchange.getResponse().setComplete();
});
});
}
处理流程如下:

单个路由的限流配置:
spring:
cloud:
gateway:
routes:
- id: account-service
uri: http://localhost:8090
predicates:
- Path=/account/**
filters:
- RewritePath=/account/(?<path>.*), /$\{path}
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 60
redis-rate-limiter.requestedTokens: 15
重写429的返回值。
package com.zengame.cycube.api.gateway.rest.aspect;
import cn.hutool.json.JSONUtil;
import com.zengame.cycube.api.lib.common.bean.R;
import com.zengame.cycube.api.lib.common.util.UUIDUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.stream.Stream;
/**
* 魔方自定义限流
* @author Carter.li
* @createtime 2022/8/1 17:30
*/
@Slf4j
@Component
public class CubeRequestLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {
private final RateLimiter redisRateLimiter;
private final KeyResolver keyResolver;
private final boolean denyEmptyKey = true;
private static final String EMPTY_KEY = "____EMPTY_KEY__";
public CubeRequestLimiterGatewayFilterFactory(RateLimiter redisRateLimiter, KeyResolver keyResolver) {
super(redisRateLimiter, keyResolver);
this.redisRateLimiter = redisRateLimiter;
this.keyResolver = keyResolver;
}
@Override
public GatewayFilter apply(Config config) {
KeyResolver resolver = getOrDefault(config.getKeyResolver(), keyResolver);
RateLimiter<Object> limiter = getOrDefault(config.getRateLimiter(), redisRateLimiter);
boolean denyEmpty = getOrDefault(config.getDenyEmptyKey(), this.denyEmptyKey);
return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> {
if (EMPTY_KEY.equals(key)) {
if (denyEmpty) {
return TokenCheckGatewayFilterFactory.generateJson(exchange, R.error(9998, "请求key为空"));
}
return chain.filter(exchange);
}
String routeId = config.getRouteId();
if (routeId == null) {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
routeId = route.getId();
}
return limiter.isAllowed(routeId, key).flatMap(response -> {
for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
}
if (response.isAllowed()) {
return chain.filter(exchange);
}
R<String> r = R.error(9998, "请求太频繁");
r.setData(key);
r.setGuid("请控制请求速度");
r.setTraceId(Stream.of(exchange.getRequest().getHeaders().getFirst("requestId"), exchange.getRequest().getQueryParams().getFirst("requestId")).filter(StringUtils::isNotBlank).findFirst().orElse(UUIDUtils.uuid()));
log.warn("too many requests: {}", JSONUtil.toJsonStr(r));
return TokenCheckGatewayFilterFactory.generateJson(exchange, r);
});
});
}
private <T> T getOrDefault(T configValue, T defaultValue) {
return (configValue != null) ? configValue : defaultValue;
}
}
jmeter脚本
线程配置:

接口配置:

经过测试,对高频接口增加了限流能力,而且限流能力是可以设定的。
在网关添加了最低限度的保护限流策略。
企业用户数量有限,可以使用最小的资源满足软件系统的需求;
原创不易,关注诚可贵,转发价更高!转载请注明出处,让我们互通有无,共同进步,欢迎沟通交流。
我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel
我是一名决定学习Ruby和RubyonRails的ASP.NETMVC开发人员。我已经有所了解并在RoR上创建了一个网站。在ASP.NETMVC上开发,我一直使用三层架构:数据层、业务层和UI(或表示)层。尝试在RubyonRails应用程序中使用这种方法,我发现没有关于它的信息(或者也许我只是找不到它?)。也许有人可以建议我如何在RubyonRails上创建或使用三层架构?附言我使用ruby1.9.3和RubyonRails3.2.3。 最佳答案 我建议在制作RoR应用程序时遵循RubyonRails(RoR)风格。Rails
我的rails3.1.6应用程序中有一个自定义访问器方法,它为一个属性分配一个值,即使该值不存在。my_attr属性是一个序列化的哈希,除非为空白,否则应与给定值合并指定了值,在这种情况下,它将当前值设置为空值。(添加了检查以确保值是它们应该的值,但为简洁起见被删除,因为它们不是我的问题的一部分。)我的setter定义为:defmy_attr=(new_val)cur_val=read_attribute(:my_attr)#storecurrentvalue#makesureweareworkingwithahash,andresetvalueifablankvalueisgiven
有没有办法在liquidtemplate中输出(用于调试/信息目的)可用对象和对象属性??也就是说,假设我正在使用jekyll站点生成工具,并且我在我的index.html模板中(据我所知,这是一个液体模板)。它可能看起来像这样{%forpostinsite.posts%}{{post.date|date_to_string}}»{{post.title}}{%endfor%}是否有任何我可以使用的模板标签会告诉我/输出名为post的变量在此模板(以及其他模板)中可用。此外,是否有任何模板标签可以告诉我post对象具有键date、title、url、摘录、永久链接等
我尝试在我的应用中只使用:symbols作为关键词。我尝试在:symbol=>logic或string=>UI/languagespecific之间做出严格的决定但我也得到了每个JSON的一些“值”(即选项等),因为JSON中没有:symbols,所以我调用的所有哈希都具有“with_indifferent_access”属性。但是:数组是否有相同的东西?像那样a=['std','elliptic',:cubic].with_indifferent_accessa.include?:std=>true?编辑:将rails添加到标签 最佳答案
我尝试用Ruby设计一个基于Web的应用程序。我开发了一个简单的核心应用程序,在没有框架和数据库的情况下在六边形架构中实现DCI范例。核心六边形中有小六边形和网络,数据库,日志等适配器。每个六边形都在没有数据库和框架的情况下自行运行。在这种方法中,我如何提供与数据库模型和实体类的关系作为独立于数据库的关系。我想在将来将框架从Rails更改为Sinatra或数据库。事实上,我如何在这个核心Hexagon中实现完全隔离的rails和mongodb的数据库适配器或框架适配器。有什么想法吗? 最佳答案 ROM呢?(Ruby对象映射器)。还有
我进行了一些谷歌搜索,似乎缺少用于jRuby的IDE。我读过TextMate和Sublime,但它们不提供调试或代码完成功能。有人可以提出建议吗(或者这项技术还处于起步阶段)? 最佳答案 有几个选项;我更喜欢JetBrains'IntelliJ(RubyMine).AptanahasanEclipseplugin.NetBeansusedtohaveofficialsupport,不确定currentstate是什么是。 关于ruby-哪些IDE可用于jRuby?,我们在StackOve
我将guard与rspec和cucumber一起使用。要连续运行选定的规范,我只需使用focus标记来确定我要处理的内容。但问题是,如果没有带有该标签的规范,我想运行所有规范。我该怎么做?注意::我知道所有RSpec选项。因此,请仅在阅读问题后回复。 最佳答案 我通过以下配置实现了您描述的行为:#torunonlyspecificspecs,add:focustothespec#describe"foo",:focusdo#OR#it"shouldfoo",:focusdoconfig.treat_symbols_as_metada
我从事Rails已有一段时间,并且刚刚开始深入研究Ruby元编程,Rails从中获得了强大的力量。我真的想不通这个,这让我发疯。Controller中的实例变量如何提供给Rails中的View(与View共享)?我知道它背后有一些元编程魔法,但我无法弄明白。在此先感谢您的所有帮助。 最佳答案 更新:原来接受的答案是错误的我现在将它留在下面以证明我错了。在获得足够多的反对票后,我决定研究这实际上是如何工作的。我最初的回答是在我对Rails还很陌生之后写的,并且是基于我使用过的其他MVC库(特别是:CodeIgniter)的工作方式的假
对于最近的一个项目,我有几个View是这样的代码:这在开发模式下工作得很好......我将它推出到生产模式并且它爆炸了,说count不是Array的有效方法。我将每个实例都改为使用Array#length,它似乎可以正常工作。1)这种行为差异的原因是什么?2)我应该注意开发模式和生产模式之间的任何其他令人兴奋的差异吗?道德:确保您的生产托管环境使用与本地开发环境相同的Ruby版本。:)谢谢汤姆 最佳答案 count方法仅在Ruby1.9及更高版本中可用。我建议您使用与服务器相同版本的Ruby以避免此类问题-1.9中发生了很多变化