Skip to content

1. User Guide开发手册

wangzheng0822 edited this page Jul 26, 2018 · 4 revisions

如何配置限流规则

限流规则有两种存储方式:本地文件存储和集中式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";
}

对于限流框架需要的参数可以通过多种方式来配置:

1) 通过环境变量配置

可以在系统环境变量中配置,比如:

export ratelimiter.zookeeper.address=127.0.0.1:2181

2) 通过JVM参数配置

在启动应用时的,通过jvm参数指定配置,比如:

Java -Dratelimiter.zookeeper.address=127.0.0.1:2181 -jar application.jar

3) 通过文件配置

支持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

4) 编程配置

为了方便使用者将框架集成到不同的平台或者系统,适应集成系统的配置方式,框架本身还提供了更加灵活的编程配置方式。举例如下:

基于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();

5) 默认配置

框架本身提供了默认的配置,在不做任何配置的情况下都可以使用,默认的配置如下

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环境下如何使用

Spring对于java开发来说,已经可以说是标配了。所以这里我们就举例说下,如何在spring开发环境中使用。

1) 基于xml的配置方式

如果基于默认配置或者非纯编程配置,在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" />

2) 基于@Configuration编程的配置方式

举一个列子,如下:

@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插件二次开发

限流框架采用SPI插件开发模式,使用者在使用框架的时候,在不需要改动框架代码的情况下,可以自行开发插件,替换掉里面的某些实现。可替换组价有:

1) 限流规则配置格式:com.eudemon.ratelimiter.rule.parser.RuleConfigParser

目前框架支持yaml和json格式的规则配置,用户可以实现这个接口,支持更多的限流规则格式,比如xml格式的。

2) 限流规则配置存储:com.eudemon.ratelimiter.rule.source.RuleConfigSource

目前框架支持从本地文件和zookeeper中加载配置文件,用户可以实现这个接口,支持自定义的规则存储方式,比如redis或mysql存储规则。

3) 限流规则内存数据结构:com.eudemon.ratelimiter.rule.RateLimitRule

目前框架实现了基于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格式插件。

应用场景与集成部署方式

限流功能和服务本身之间的架构模式有以下几种,每种架构模式都有优缺点和局限,需要权衡各种情况,来选择最合适的。

1) 在接入层(API-GATEWAY)集成鉴权限流功能;

这种方式是在微服务的架构下,有api-gateway的前提下最合理的架构模式。在公司内部没有统一的api-gateway的情况下,我们可以移步其他两种部署模式。单instance部署的api-gateway,我们可以使用基于memory的限流接口,多instance部署的api-gateway集群,我们可以使用基于redis的分布式限流接口。

2) 包裹限流框架作为独立服务系统,通过远程调用的方式与应用系统交互;

接口请求过来之后,应用系统会调用限流系统,限流系统告诉接口系统此接口是否有权限,是否超过了限流阈值。 缺点是:需要部署另一个系统,增加了运维成本。而且,如果公司内部很多系统都需要鉴权限流,要么就要针对每个接口系统分别部署一个独立鉴权限流系统,要么就要解决多个接口系统使用同一个鉴权限流系统的多租户问题。

3) 将鉴权限流作为独立的jar包,集成到应用系统内;

这种架构模式不需要再独立部署服务,减少了运维成本,但鉴权限流代码会跟业务代码有一些耦合,为了解决这个问题,我们将鉴权限流作为独立的jar包开发,并且针对spring项目提供了spring-boot-starter,代码尽量可插拔易集成,能够尽量跟业务代码松耦合。

针对单机的限流,我们可以使用基于memory的限流接口。现在的应用都是集群部署,一方面为了性能,一方面为了高可用,一个调用方对某个接口的访问会分散在多个instance上,如果每个instance单独记录接口调用次数,没法做到服务级别的限流。所以如果要做服务级别的限流,我们可以使用针对Redis的分布式限流。但是,要特别注意的是:集群分布式限流,需要考量性能是否满足接口访问的性能(tps和响应时间)需求,参考benchmar性能测试报告

容错机制及可用性

框架的容错性和性能是我们特别关注的,我们不希望因为框架原因影响接口的成功率和响应时间。所以从以下这几个方面来做保证:

1) 启动出错fail fast

限流框架初始化阶段的任何异常,我们都及时的throw RuntimeException,力求不忽视任何问题,不让问题推迟到运行时暴露。

2) 运行时高度容错

对于系统性错误,我们及时catch exception,不管是checked还是runtime exception,保证这个框架的任何报错都不会影响到集成系统。

3) 超时时间设置

对于需要wait的操作,比如io, 锁,外部调用等,我们都设置了超时时间,用户也可以自定超时时间,保证不会因为无限等待,导致影响业务系统的响应时间。

<4) 异常情况下可用性测试

请参看如下可用性测试报告:

异常测试场景 测试结果和表现 是否影响业务
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
}

5) 全面的性能测试

详细的性能测试请看benchmar性能测试报告

6) 单元测试(覆盖率>60%)

单元测试报告如下: unit tests