-
Notifications
You must be signed in to change notification settings - Fork 382
1. User Guide开发手册
限流规则有两种存储方式:本地文件存储和集中式zookeeper存储。
- 本地文件存储,放在classpath(ratelimiter-rule.yaml或者ratelimiter-rule.yml或者ratelimiter-rule.json)中或者通过参数配置(ratelimiter.rule.file)指定配置文件的加载路径,比如/etc/app/rule-config.yaml
- 集中式存储,支持配置文件存放在zookeeper中,通过配置zookeeper地址和node path,来指定限流规则存放的路径。默认查询的node path为:/com/eudemon/ratelimit
除此之外,为了使用灵活,限流框架暴露了更加灵活的编程接口,规则可以存储在任意位置,使用者可以自行从自己的存储中心获取规则文本,通过限流框架暴露的接口将规则文本传入,举例如下:
String ruleConfigText = "configs:\n" +
"- appId: app-1\n" +
" limits:\n" +
" - api: /v1/user\n" +
" limit: 100\n" +
" - api: /v1/order\n" +
" limit: 50\n" +
"- appId: app-2\n" +
" limits:\n" +
" - api: /v1/user\n" +
" limit: 50\n" +
" - api: /v1/order\n" +
" limit: 50\n";
/* convert rule text to the intermediate format {@link UniformRuleConfigMapping } */
InputStream inputStream = new ByteArrayInputStream(ruleConfigText.getBytes());
JsonRuleConfigParser parser = new JsonRuleConfigParser();
UniformRuleConfigMapping mapping = parser.parse(inputStream);
/* build rule from {@link UniformRuleConfigMapping }*/
RateLimitRule rule = new UrlRateLimitRule();
rule.rebuildRule(mapping);
/* construct {@link UrlRateLimiter } by rule. */
UrlRateLimiter ratelimiter = new MemoryUrlRateLimiter(rule);`
除了规则存储的数据源可以任意定义之外,规则的格式也可以自行定义,限流框架暴露了更加深度的接口,支持任意格式的规则数据。举例如下:
RateLimitRule rule = new UrlRateLimitRule();
rule.addLimit("app-1", new ApiLimit("/v1/user", 100));
rule.addLimit("app-1", new ApiLimit("/v1/order", 50));
rule.addLimit("app-2", new ApiLimit("/v1/user", 50));
rule.addLimit("app-2", new ApiLimit("/v1/order", 50));
/* construct {@link UrlRateLimiter } by rule. */
UrlRateLimiter ratelimiter = new MemoryUrlRateLimiter(rule);
限流规则支持两种数据格式:yaml和json,举例如下:
configs:
- appId: app-1
limits:
- api: /v1/user
limit: 100
- api: /v1/order
limit: 50
- appId: app-2
limits:
- api: /v1/user
limit: 50
- api: /v1/order
limit: 50
JSON配置举例如下:
{ "configs": [
{
"appId": "app-1",
"limits": [
{
"api": "/v1/user",
"limit": "100"
},
{
"api": "/v1/order",
"limit": "50"
}
]
},
{
"appId": "app-2",
"limits": [
{
"api": "/v1/user",
"limit": "50"
},
{
"api": "/v1/order",
"limit": "50"
}
]
}
]}
具体的配置参数,可以参看com.eudemon.ratelimiter.env.PropertyConstants.java:
public class PropertyConstants {
public static final String PROPERTY_KEY_PREFIX = "ratelimiter";
/* rule format: yaml, json, default is yaml.*/
public static final String PROPERTY_RULE_CONFIG_PARSER =
PROPERTY_KEY_PREFIX + ".rule.config.parser";
/* rule configuration source type: file or zookeeper, default is file.*/
public static final String PROPERTY_RULE_CONFIG_SOURCE =
PROPERTY_KEY_PREFIX + ".rule.config.source";
/* Redis config if needed. */
public static final String PROPERTY_REDIS_ADDRESS = PROPERTY_KEY_PREFIX + ".redis.address";
public static final String PROPERTY_REDIS_MAX_TOTAL = PROPERTY_KEY_PREFIX + ".redis.maxTotal";
public static final String PROPERTY_REDIS_MAX_IDLE = PROPERTY_KEY_PREFIX + ".redis.maxIdle";
public static final String PROPERTY_REDIS_MIN_IDLE = PROPERTY_KEY_PREFIX + ".redis.minIdle";
public static final String PROPERTY_REDIS_MAX_WAIT_MILLIS =
PROPERTY_KEY_PREFIX + ".redis.maxWaitMillis";
public static final String PROPERTY_REDIS_TEST_ON_BORROW =
PROPERTY_KEY_PREFIX + ".redis.testOnBorrow";
public static final String PROPERTY_REDIS_TIMEOUT = PROPERTY_KEY_PREFIX + ".redis.timeout";
/* Zookeeper config if needed. */
public static final String PROPERTY_ZOOKEEPER_ADDRESS =
PROPERTY_KEY_PREFIX + ".zookeeper.address";
public static final String PROPERTY_ZOOKEEPER_RULE_PATH =
PROPERTY_KEY_PREFIX + ".zookeeper.rule.path";
}
对于限流框架需要的参数可以通过多种方式来配置:
可以在系统环境变量中配置,比如:
export ratelimiter.zookeeper.address=127.0.0.1:2181
在启动应用时的,通过jvm参数指定配置,比如:
Java -Dratelimiter.zookeeper.address=127.0.0.1:2181 -jar application.jar
支持yaml格式和properties格式的配置文件,框架加载的默认配置文件名为: ratelimiter-env.yaml or ratelimiter-env.yml or ratelimiter-env.properties 默认的配置文件搜索路径为应用运行的classpath中。当然也可以通过指定文件路径,从指定的位置加载配置文件。
Yaml配置文件举例:
ratelimiter.rule.config.source: zookeeper
ratelimiter.rule.config.parser: yaml
ratelimiter.redis:
address: 127.0.0.1:6379
maxTotal: 100
maxIdle: 100
minIdle: 10
maxWaitMillis: 10
testOnBorrow: true
ratelimiter.zookeeper:
address: 127.0.0.0:2181
rule.path: /com/eudemon/ratelimit
Properties配置文件举例:
ratelimiter.rule.config.source=zookeeper
ratelimiter.rule.config.parser=yaml
ratelimiter.redis.address=127.0.0.1:6379
ratelimiter.redis.maxTotal=100
ratelimiter.redis.maxIdle=100
ratelimiter.redis.minIdle=10
ratelimiter.redis.testOnBorrow=true
ratelimiter.redis.maxWaitMillis=10
ratelimiter.zookeeper.address=127.0.0.1:2181
ratelimiter.zookeeper.rule.path=/com/eudemon/ratelimit
为了方便使用者将框架集成到不同的平台或者系统,适应集成系统的配置方式,框架本身还提供了更加灵活的编程配置方式。举例如下:
基于memory的限流:
ZookeeperConfig zkConfig = new ZookeeperConfig();
zkConfig.setAddress("127.0.0.1:6379");
zkConfig.setPath("/com/eudemon/ratelimit");
UrlRateLimiter ratelimiter = MemoryUrlRateLimiter.builder
.setRuleParserType("yaml")
.setRuleSourceType("zookeeper")
.SetZookeeperConfig(zkConfig)
.build();
基于Redis的分布式限流:
ZookeeperConfig zkConfig = new ZookeeperConfig();
zkConfig.setAddress("127.0.0.1:6379");
zkConfig.setPath("/com/eudemon/ratelimit");
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(200);
RedisConfig redisConfig = new RedisConfig();
redisConfig.setIp("127.0.01:6379");
redisConfig.setTimeout(10);
redisConfig.setPoolConfig(poolConfig);
UrlRateLimiter ratelimiter = DistributedUrlRateLimiter.builder
.ruleParserType("yaml")
.ruleSourceType("zookeeper")
.zookeeper(zkConfig)
.redis(redisConfig)
.build();
框架本身提供了默认的配置,在不做任何配置的情况下都可以使用,默认的配置如下
ratelimiter.rule.config.source=file
ratelimiter.rule.config.parser=yaml
ratelimiter.redis.address=null
ratelimiter.redis.port=6379
ratelimiter.redis.maxTotal=50
ratelimiter.redis.maxIdle=50
ratelimiter.redis.minIdle=20
ratelimiter.redis.maxWaitMillis=10
ratelimiter.redis.testOnBorrow=true
ratelimiter.zookeeper.address=null
ratelimiter.zookeeper.rule.path=/com/eudemon/ratelimit
配置加载顺序与覆盖原则: 框架可以混合使用多种配置方式,如果有某个参数的配置在不同的配置方式中均有配置,则应用下面的优先级覆盖原则,优先级从高到低排列如下: 编程配置>JVM参数>系统环境变量>配置文件>默认
Spring对于java开发来说,已经可以说是标配了。所以这里我们就举例说下,如何在spring开发环境中使用。
如果基于默认配置或者非纯编程配置,在applicationContext.xml中或者其他spring xml配置文件中,像下面一行就可以了:
<bean id="urlRateLimiter" class="com.eudemon.ratelimiter.DistributedUrlRateLimiter" />
但是如果希望纯编程配置或者复用业务系统本身的配置,可以如下构造对象,稍微复杂点:
<bean id="poolConfig" class="org.apache.commons.pool2.impl.GenericObjectPoolConfig">
<property name="maxTotal" value="100" />
<property name="maxIdle" value="50" />
<property name="minIdle" value="20" />
</bean>
<bean id="jedisTaskExecutor" class="com.eudemon.ratelimiter.redis.DefaultJedisTaskExecutor">
<constructor-arg value="127.0.0.1" />
<constructor-arg value="6379" />
<constructor-arg value="10" />
<constructor-arg ref="poolConfig" />
</bean>
<bean id="ruleConfigParser" class="com.eudemon.ratelimiter.rule.parser.YamlRuleConfigParser" />
<bean id="ruleConfigSource" class="com.eudemon.ratelimiter.rule.source.ZookeeperRuleConfigSource">
<constructor-arg value="127.0.0.1:2121" />
<constructor-arg value="/com/eudemon/ratelimit" />
<constructor-arg ref="ruleConfigParser" />
</bean>
<bean id="urlRateLimiter" class="com.eudemon.ratelimiter.DistributedUrlRateLimiter">
<constructor-arg ref="jedisTaskExecutor" />
<constructor-arg ref="ruleConfigSource" />
</bean>
上面稍微有些复杂,也可以使用builder类来构建,如下:
<bean id="poolConfig" class="org.apache.commons.pool2.impl.GenericObjectPoolConfig">
<property name="maxTotal" value="100" />
<property name="maxIdle" value="50" />
<property name="minIdle" value="20" />
</bean>
<bean id="redisConfig" class="com.eudemon.ratelimiter.env.RedisConfig">
<property name="ip" value="127.0.01"/>
<property name="port" value="6379"/>
<property name="timeout" value="10" />
<property name="poolConfig" ref="poolConfig"></property>
</bean>
<bean id="zookeeperConfig" class="com.eudemon.ratelimiter.env.ZookeeperConfig">
<property name="address" value="127.0.0.1:2121"/>
<property name="path" value="/com/eudemon/ratelimit"/>
</bean>
<bean id="urlRateLimiterBuilder" class="com.eudemon.ratelimiter.DistributedUrlRateLimiter.DistributedUrlRateLimiterbuilder">
<property name="redisConfig" ref="redisConfig" />
<property name="zookeeperConfig" ref="zookeeperConfig"/>
<property name="ruleParserType" value="yaml" />
<property name="ruleSourceType" value="zookeeper"></property>
</bean>
<bean id="urlRateLimiter" factory-bean="urlRateLimiterBuilder" factory-method="build" />
举一个列子,如下:
@Configuration
public class AppConfig {
@Bean(name = "urlRateLimiter")
public UrlRateLimiter memoryUrlRateLimiter() {
ZookeeperConfig zkConfig = new ZookeeperConfig();
zkConfig.setAddress("127.0.0.1:6379");
zkConfig.setPath("/com/eudemon/ratelimit");
UrlRateLimiter ratelimiter = MemoryUrlRateLimiter.builder
.ruleParserType("yaml")
.ruleSourceType("zookeeper")
.zookeeper(zkConfig)
.build();
return ratelimiter;
}
}
限流框架采用SPI插件开发模式,使用者在使用框架的时候,在不需要改动框架代码的情况下,可以自行开发插件,替换掉里面的某些实现。可替换组价有:
目前框架支持yaml和json格式的规则配置,用户可以实现这个接口,支持更多的限流规则格式,比如xml格式的。
目前框架支持从本地文件和zookeeper中加载配置文件,用户可以实现这个接口,支持自定义的规则存储方式,比如redis或mysql存储规则。
目前框架实现了基于trie tree数据结构的限流规则增删改查类,可以实现快速的插入一条限流规则,查询某个接口的限流规则,实验证明trie tree这种数据结构非常适合像url这种具有分级目录且目录重复度高的接口格式。用户也可以通过SPI实现RateLimitRule接口,自行实现限流规则增删改查类,比如通过正则表达式来做查询限流规则。
只需要将自动的插件,放到/META-INF/services目录下,文件名称如下:
com.eudemon.ratelimiter.rule.parser.RuleConfigParser
com.eudemon.ratelimiter.rule.source.RuleConfigSource
com.eudemon.ratelimiter.rule.RateLimitRule
文件内容即为用户自定的插件类的路径,比如:
com.xm.thirdparty.XmlRuleConfigParser
备注:SPI的类会优先于配置使用,比如配置文件中配置了ratelimiter.rule.parser=yaml,但是如果用户自定义了支持xml格式的rule格式插件,尽管配置文件配置了使用yaml格式,但基于SPI优先于配置的事先协议,仍然会使用SPI定义的xml格式rule格式插件。
限流功能和服务本身之间的架构模式有以下几种,每种架构模式都有优缺点和局限,需要权衡各种情况,来选择最合适的。
这种方式是在微服务的架构下,有api-gateway的前提下最合理的架构模式。在公司内部没有统一的api-gateway的情况下,我们可以移步其他两种部署模式。单instance部署的api-gateway,我们可以使用基于memory的限流接口,多instance部署的api-gateway集群,我们可以使用基于redis的分布式限流接口。
接口请求过来之后,应用系统会调用限流系统,限流系统告诉接口系统此接口是否有权限,是否超过了限流阈值。 缺点是:需要部署另一个系统,增加了运维成本。而且,如果公司内部很多系统都需要鉴权限流,要么就要针对每个接口系统分别部署一个独立鉴权限流系统,要么就要解决多个接口系统使用同一个鉴权限流系统的多租户问题。
这种架构模式不需要再独立部署服务,减少了运维成本,但鉴权限流代码会跟业务代码有一些耦合,为了解决这个问题,我们将鉴权限流作为独立的jar包开发,并且针对spring项目提供了spring-boot-starter,代码尽量可插拔易集成,能够尽量跟业务代码松耦合。
针对单机的限流,我们可以使用基于memory的限流接口。现在的应用都是集群部署,一方面为了性能,一方面为了高可用,一个调用方对某个接口的访问会分散在多个instance上,如果每个instance单独记录接口调用次数,没法做到服务级别的限流。所以如果要做服务级别的限流,我们可以使用针对Redis的分布式限流。但是,要特别注意的是:集群分布式限流,需要考量性能是否满足接口访问的性能(tps和响应时间)需求,参考benchmar性能测试报告
框架的容错性和性能是我们特别关注的,我们不希望因为框架原因影响接口的成功率和响应时间。所以从以下这几个方面来做保证:
限流框架初始化阶段的任何异常,我们都及时的throw RuntimeException,力求不忽视任何问题,不让问题推迟到运行时暴露。
对于系统性错误,我们及时catch exception,不管是checked还是runtime exception,保证这个框架的任何报错都不会影响到集成系统。
对于需要wait的操作,比如io, 锁,外部调用等,我们都设置了超时时间,用户也可以自定超时时间,保证不会因为无限等待,导致影响业务系统的响应时间。
请参看如下可用性测试报告:
异常测试场景 | 测试结果和表现 | 是否影响业务 |
Zookeeper连接超时 | throw RuntimeException | 否,因为连接zookeeper是在ratelimiter初始化阶段,throw Exception 做到 fail fast. |
Zookeeper访问超时 | 同上 | 否,同上 |
Zookeeper中未配置存储限流规则配置的结点 | 同上 | 否,同上 |
限流规则配置文件不存在 | 限流不起作用 | 否,所有的接口请求都可以正常通过 |
限流规则配置不合法 | throw ConfiguraitonResolveException | 否,在ratelimiter初始化加载配置的阶段,throw RuntimeException做到fail fast. |
ratelimiter类初始化失败之后(错误被catch),继续调用limit接口 | throw NullPointerException | 是,请看后面的建议1&2。 |
Redis连接超时 | throw InternalErrorException | 否,因为throw的是checked exception,需要业务系统捕获 |
Redis访问超时 | 同上 | 否,同上 |
调用limit接口传入的url不合法 | throw InvalidUrlException | 否,同上 |
建议1:在使用ratelimiter时,不要使用lazy load的方式,建议在系统启动初始化时就将ratelimiter类初始化好,这样等到用到的时候,就不需要再加载配置,否则首次调用limit接口会耗时很多。
建议2:建议使用ratelimiter.limit()方法的时候,捕获掉所有异常,如下:
try {
ratelimiter.limit("app1", "http://www.test.com/user/24607172");
} catch (OverloadException e) {
// do business logic
} catch (InvalidUrlException | InternalErrorException e) {
// do business logic
} catch(Exception e) {
// do business logic
}
详细的性能测试请看benchmar性能测试报告
单元测试报告如下: