精华内容
下载资源
问答
  • 行业资料-电子功用-改进电弧电流转移型交流故障限流器及限流方法.pdf
  • 电子政务-交流限流起动异步电动机.zip
  • 电子政务-交流可调速限流起动异步电动机.zip
  • 电子政务-快速限流保护交流电子调压器.zip
  • 限流型保护电路

    2021-02-03 14:19:00
    限流型保护电路、电子技术,开发板制作交流
  • 基于柔性直流电网的故障特征,结合国内外研究成果,从交流限流、换流器限流以及直流侧限流3个方面分析了柔性直流电网各类限流技术和方法的原理及性能,对相关限流技术和方法进行了仿真测试和比较。基于对比分析...
  • Spring Cloud Gateway 限流操作

    万次阅读 2018-07-23 16:00:42
    开发高并发系统时有三把利器用来保护系统:缓存、降级和限流,API网关作为所有请求的入口,请求量大,我们可以通过对并发访问的请求进行限速来保护系统的可用性。 常用的限流算法比如有令牌桶算法,漏桶算法,...

    开发高并发系统时有三把利器用来保护系统:缓存、降级和限流,API网关作为所有请求的入口,请求量大,我们可以通过对并发访问的请求进行限速来保护系统的可用性。

    常用的限流算法比如有令牌桶算法,漏桶算法,计数器算法等,在Zuul中我们可以自己去实现限流的功能(Zuul中如何限流在我的书《Spring Cloud微服务-全栈技术与案例解析》中有详细讲解),Spring Cloud Gateway的出现本身就是用来替代Zuul的,要想替代那肯定得有强大的功能,除了性能上的优势之外,Spring Cloud Gateway还提供了很多新功能,比如今天我们要讲的限流操作,使用起来非常简单,今天我们就来学习在如何在Spring Cloud Gateway中进行限流操作。

    目前限流提供了基于Redis的实现,我们需要增加对应的依赖:

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

    可以通过KeyResolver来指定限流的Key,比如我们需要根据用户来做限流,IP来做限流等等。

    IP限流

    @Bean
    public KeyResolver ipKeyResolver() {
    	return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
    

    通过exchange对象可以获取到请求信息,这边用了HostName,如果你想根据用户来做限流的话这边可以获取当前请求的用户ID或者用户名就可以了,比如:

    用户限流
    使用这种方式限流,请求路径中必须携带userId参数

    @Bean
    KeyResolver userKeyResolver() {
    	return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
    }
    

    接口限流
    获取请求地址的uri作为限流key

    @Bean
    KeyResolver apiKeyResolver() {
    	return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }
    

    然后配置限流的过滤器信息:

    server:
      port: 8084
    spring:
      redis:
        host: 127.0.0.1
        port: 6379
      cloud:
        gateway:
          routes:
          - id: fsh-house
            uri: lb://fsh-house
            predicates:
            - Path=/house/**
            filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@ipKeyResolver}"
    
    • filter名称必须是RequestRateLimiter
    • redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求
    • redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数
    • key-resolver:使用SpEL按名称引用bean

    可以访问接口进行测试,这时候Redis中会有对应的数据:

    127.0.0.1:6379> keys *
    1) "request_rate_limiter.{localhost}.timestamp"
    2) "request_rate_limiter.{localhost}.tokens"
    

    大括号中就是我们的限流Key,这边是IP,本地的就是localhost

    • timestamp:存储的是当前时间的秒数,也就是System.currentTimeMillis() / 1000或者Instant.now().getEpochSecond()
    • tokens:存储的是当前这秒钟的对应的可用的令牌数量

    Spring Cloud Gateway目前提供的限流还是相对比较简单的,在实际中我们的限流策略会有很多种情况,比如:

    • 每个接口的限流数量不同,可以通过配置中心动态调整
    • 超过的流量被拒绝后可以返回固定的格式给调用方
    • 对某个服务进行整体限流(这个大家可以思考下用Spring Cloud Gateway如何实现,其实很简单)

    当然我们也可以通过重新RedisRateLimiter来实现自己的限流策略,这个我们后面再进行介绍。

    限流源码

    // routeId也就是我们的fsh-house,id就是限流的key,也就是localhost。
    public Mono<Response> isAllowed(String routeId, String id) {
        // 会判断RedisRateLimiter是否初始化了
    	if (!this.initialized.get()) {
    		throw new IllegalStateException("RedisRateLimiter is not initialized");
    	}
        // 获取routeId对应的限流配置
        Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);
    
        if (routeConfig == null) {
    		throw new IllegalArgumentException("No Configuration found for route " + routeId);
        }
    
        // 允许用户每秒做多少次请求
        int replenishRate = routeConfig.getReplenishRate();
    
        // 令牌桶的容量,允许在一秒钟内完成的最大请求数
        int burstCapacity = routeConfig.getBurstCapacity();
    
    	try {
            // 限流key的名称(request_rate_limiter.{localhost}.timestamp,request_rate_limiter.{localhost}.tokens)
    		List<String> keys = getKeys(id);
    
    
    		// The arguments to the LUA script. time() returns unixtime in seconds.
    		List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
    				Instant.now().getEpochSecond() + "", "1");
    		// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
            // 执行LUA脚本
    		Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
    				// .log("redisratelimiter", Level.FINER);
    		return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
    				.reduce(new ArrayList<Long>(), (longs, l) -> {
    					longs.addAll(l);
    					return longs;
    				}) .map(results -> {
    					boolean allowed = results.get(0) == 1L;
    					Long tokensLeft = results.get(1);
    
    					Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));
    
    					if (log.isDebugEnabled()) {
    						log.debug("response: " + response);
    					}
    					return response;
    				});
    	}
    	catch (Exception e) {
    		log.error("Error determining if user allowed from redis", e);
    	}
    	return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
    }
    

    LUA脚本在:
    WX20180715-150447@2x.png

    local tokens_key = KEYS[1]
    local timestamp_key = KEYS[2]
    --redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
    
    local rate = tonumber(ARGV[1])
    local capacity = tonumber(ARGV[2])
    local now = tonumber(ARGV[3])
    local requested = tonumber(ARGV[4])
    
    local fill_time = capacity/rate
    local ttl = math.floor(fill_time*2)
    
    --redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
    --redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
    --redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
    --redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
    --redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
    --redis.log(redis.LOG_WARNING, "ttl " .. ttl)
    
    local last_tokens = tonumber(redis.call("get", tokens_key))
    if last_tokens == nil then
      last_tokens = capacity
    end
    --redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
    
    local last_refreshed = tonumber(redis.call("get", timestamp_key))
    if last_refreshed == nil then
      last_refreshed = 0
    end
    --redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
    
    local delta = math.max(0, now-last_refreshed)
    local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
    local allowed = filled_tokens >= requested
    local new_tokens = filled_tokens
    local allowed_num = 0
    if allowed then
      new_tokens = filled_tokens - requested
      allowed_num = 1
    end
    
    --redis.log(redis.LOG_WARNING, "delta " .. delta)
    --redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
    --redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
    --redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
    
    redis.call("setex", tokens_key, ttl, new_tokens)
    redis.call("setex", timestamp_key, ttl, now)
    
    return { allowed_num, new_tokens }
    
    

    欢迎加入我的知识星球,一起交流技术,免费学习猿天地的课程(http://cxytiandi.com/course)

    PS:目前星球中正在星主的带领下组队学习Spring Cloud,等你哦!

    微信扫码加入猿天地知识星球

    猿天地

    展开全文
  • 为了降低交流系统短路故障给基于模块化多电平换流器的统一潮流控制器(MMC-UPFC)带来的大电流冲击风险,保障电力电子设备的安全运行,提出了一种基于分裂电感的限流式MMC-UPFC。通过对桥臂电感的分裂设计和串联变压器...
  • Spring Cloud Gateway 结合配置中心限流

    千次阅读 2018-08-20 09:37:52
    上篇文章我讲过复杂的限流场景可以通过扩展RedisRateLimiter来实现自己的限流策略。 假设你领导给你安排了一个任务,具体需求如下: - 针对具体的接口做限流 - 不同接口限流的力度可以不同 - 可以动态调整限流...

    前言

    上篇文章我讲过复杂的限流场景可以通过扩展RedisRateLimiter来实现自己的限流策略。

    假设你领导给你安排了一个任务,具体需求如下:

    • 针对具体的接口做限流
    • 不同接口限流的力度可以不同
    • 可以动态调整限流配置,实时生效

    如果你接到上面的任务,你会怎么去设计+实现呢?

    每个人看待问题的角度不同,自然思考出来的方案也不同,正所谓条条大路通罗马,能到达目的地的路那就是一条好路。

    如何分析需求

    下面我给出我的实现方式,仅供各位参考,大牛请忽略。

    具体问题具体分析,针对需求点,分别去做分析。

    需求一 “如何针对具体的接口做限流” 这个在上篇文章中也有讲过,只需要让KeyResolver返回的是接口的URI即可,这样限流的维度那就是对这个接口进行限流。

    需求二 “不同接口限流的力度可以不同” 这个通过配置的方式明显实现不了,配置中的replenishRate和burstCapacity都是配置死的,如果要做成动态的那么必须的自己通过扩展RedisRateLimiter来实现。

    前提是必须有一个配置列表,这个配置列表就是每个接口对应的限流数值。有了这个配置我们就可以通过请求的接口获取这个接口对应的限流值。

    需求三“可以动态调整限流配置,实时生效” 这个的话也比较容易,无论你是存文件,存数据库,存缓存只要每次都去读取,必然是实时生效的,但是性能问题我们不得不考虑啊。

    存文件,读取文件,耗IO,主要是不方便修改
    存数据库,可以通过web界面去修改,也可以直接改数据库,每次都要查询,性能不行
    存分布式缓存(redis),性能比数据库有提高

    对比下来肯定是缓存是最优的方案,还有更好的方案吗?
    有,结合配置中心来做,我这边用自己的配置中心(https://github.com/yinjihuan/smconf)来讲解,换成其他的配置中心也是一样的思路。

    配置中心的优点在于它本来就是用来存储配置的,配置在项目启动时加载完毕,当有修改时推送更新,每次读取都在本地对象中,性能好。

    具体方案有了之后我们就可以开始撸代码了,但是你有想过这么多接口的限流值怎么初始化吗?手动一个个去加?

    不同的服务维护的小组不同,当然也有可能是一个小组维护,从设计者的角度来思考,应该把设置的权利交给用户,交给我们的接口开发者,每个接口能够承受多少并发让用户来定,你的职责就是在网关进行限流。当然在公司中具体的限制量也不一定会由开发人员来定哈,这个得根据压测结果,做最好的调整。

    话不多说-开始撸码

    首先我们定义自己的RedisRateLimiter,复制源码稍微改造下即可, 这边只贴核心代码。

    public class CustomRedisRateLimiter extends AbstractRateLimiter<CustomRedisRateLimiter.Config>
    		implements ApplicationContextAware {
    
    	public static final String CONFIGURATION_PROPERTY_NAME = "custom-redis-rate-limiter";
    	public static final String REDIS_SCRIPT_NAME = "redisRequestRateLimiterScript";
    	public static final String REMAINING_HEADER = "X-RateLimit-Remaining";
    	public static final String REPLENISH_RATE_HEADER = "X-RateLimit-Replenish-Rate";
    	public static final String BURST_CAPACITY_HEADER = "X-RateLimit-Burst-Capacity";
    
    	public CustomRedisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate, RedisScript<List<Long>> script,
    			Validator validator) {
    		super(Config.class, CONFIGURATION_PROPERTY_NAME, validator);
    		this.redisTemplate = redisTemplate;
    		this.script = script;
    		initialized.compareAndSet(false, true);
    	}
    
    	public CustomRedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) {
    		super(Config.class, CONFIGURATION_PROPERTY_NAME, null);
    		this.defaultConfig = new Config().setReplenishRate(defaultReplenishRate).setBurstCapacity(defaultBurstCapacity);
    	}
    
    	// 限流配置
    	private RateLimitConf rateLimitConf;
    
    	@Override
    	@SuppressWarnings("unchecked")
    	public void setApplicationContext(ApplicationContext context) throws BeansException {
    		// 加载配置
    		this.rateLimitConf = context.getBean(RateLimitConf.class);	
    	}
    
    
    	/**
    	 * This uses a basic token bucket algorithm and relies on the fact that
    	 * Redis scripts execute atomically. No other operations can run between
    	 * fetching the count and writing the new count.
    	 */
    	@Override
    	@SuppressWarnings("unchecked")
    	public Mono<Response> isAllowed(String routeId, String id) {
    		if (!this.initialized.get()) {
    			throw new IllegalStateException("RedisRateLimiter is not initialized");
    		}
    
    		//Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);
    		
    		if (rateLimitConf == null) {
    			throw new IllegalArgumentException("No Configuration found for route " + routeId);
    		}
    		Map<String,Integer> routeConfig = rateLimitConf.getLimitMap();
    
    		// Key的格式:服务名称.接口URI.类型
    		String replenishRateKey = routeId + "." + id + ".replenishRate";
    		int replenishRate = routeConfig.get(replenishRateKey) == null ? routeConfig.get("default.replenishRate") : routeConfig.get(replenishRateKey);
    		
    		String burstCapacityKey = routeId + "." + id + ".burstCapacity";
    		int burstCapacity = routeConfig.get(burstCapacityKey) == null ? routeConfig.get("default.burstCapacity") : routeConfig.get(burstCapacityKey);
    			
    		try {
    			List<String> keys = getKeys(id);
    
    			// The arguments to the LUA script. time() returns unixtime in
    			// seconds.
    			List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
    					Instant.now().getEpochSecond() + "", "1");
    			// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
    			Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
    			// .log("redisratelimiter", Level.FINER);
    			return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
    					.reduce(new ArrayList<Long>(), (longs, l) -> {
    						longs.addAll(l);
    						return longs;
    					}).map(results -> {
    						boolean allowed = results.get(0) == 1L;
    						Long tokensLeft = results.get(1);
    
    						Response response = new Response(allowed, getHeaders(replenishRate, burstCapacity, tokensLeft));
    
    						if (log.isDebugEnabled()) {
    							log.debug("response: " + response);
    						}
    						return response;
    					});
    		} catch (Exception e) {
    			/*
    			 * We don't want a hard dependency on Redis to allow traffic. Make
    			 * sure to set an alert so you know if this is happening too much.
    			 * Stripe's observed failure rate is 0.01%.
    			 */
    			log.error("Error determining if user allowed from redis", e);
    		}
    		return Mono.just(new Response(true, getHeaders(replenishRate, burstCapacity, -1L)));
    	}
    
    	public HashMap<String, String> getHeaders(Integer replenishRate, Integer burstCapacity, Long tokensLeft) {
    		HashMap<String, String> headers = new HashMap<>();
    		headers.put(this.remainingHeader, tokensLeft.toString());
    		headers.put(this.replenishRateHeader, String.valueOf(replenishRate));
    		headers.put(this.burstCapacityHeader, String.valueOf(burstCapacity));
    		return headers;
    	}
    
    }
    

    需要在setApplicationContext中加载我们的配置类,配置类的定义如下:

    @CxytianDiConf(system="fangjia-gateway")
    public class RateLimitConf {
    	// 限流配置
    	@ConfField(value = "limitMap")
    	private Map<String, Integer> limitMap = new HashMap<String, Integer>(){{
    		put("default.replenishRate", 100);
    		put("default.burstCapacity", 1000);
    	}};
    	public void setLimitMap(Map<String, Integer> limitMap) {
    		this.limitMap = limitMap;
    	}
    	public Map<String, Integer> getLimitMap() {
    		return limitMap;
    	}
    }
    
    

    所有的接口对应的限流信息都在map中,有默认值,如果没有对应的配置就用默认的值对接口进行限流。

    isAllowed方法中通过‘服务名称.接口URI.类型’组成一个Key, 通过这个Key去Map中获取对应的值。

    类型的作用主要是用来区分replenishRate和burstCapacity两个值。

    接下来就是配置CustomRedisRateLimiter:

    
    @Bean
    @Primary
    public CustomRedisRateLimiter customRedisRateLimiter(
                      ReactiveRedisTemplate<String, String> redisTemplate,	 
                      @Qualifier(CustomRedisRateLimiter.REDIS_SCRIPT_NAME)  RedisScript<List<Long>> redisScript,
    		          Validator validator) {
    	return new CustomRedisRateLimiter(redisTemplate, redisScript, validator);
    }
    

    网关这边的逻辑已经实现好了,接下来就是需要在具体的服务中自定义注解,然后将限流的参数初始化到我们的配置中心就可以了。

    定义注解

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ApiRateLimit {
    	
    	/**
    	 * 速率
    	 * @return
    	 */
    	int replenishRate() default 100;
    	
    	/**
    	 * 容积
    	 * @return
    	 */
    	int burstCapacity() default 1000;
    	
    }
    
    

    启动监听器,读取注解,初始化配置

    /**
     * 初始化API网关需要进行并发限制的API
     * @author yinjihuan
     *
     */
    public class InitGatewayApiLimitRateListener implements ApplicationListener<ApplicationReadyEvent> {
    
    	// Controller包路径
    	private String controllerPath;
    
    	private RateLimitConf rateLimitConf;
    	
    	private ConfInit confInit;
    	
    	private String applicationName;
    	
    	public InitGatewayApiLimitRateListener(String controllerPath) {
    		this.controllerPath = controllerPath;
    	}
    
    	@Override
    	public void onApplicationEvent(ApplicationReadyEvent event) {
    		this.rateLimitConf = event.getApplicationContext().getBean(RateLimitConf.class);
    		this.confInit = event.getApplicationContext().getBean(ConfInit.class);
    		this.applicationName = event.getApplicationContext().getEnvironment().getProperty("spring.application.name");
    		try {
    			initLimitRateAPI();
    		} catch (Exception e) {
    			throw new RuntimeException("初始化需要进行并发限制的API异常", e);
    		}
    	}
    	
    	/**
    	 * 初始化需要进行并发限制的API
    	 * @throws IOException
    	 * @throws ClassNotFoundException
    	 */
    	private void initLimitRateAPI() throws IOException, ClassNotFoundException {
    		Map<String, Integer> limitMap = rateLimitConf.getLimitMap();
    		ClasspathPackageScannerUtils scan = new ClasspathPackageScannerUtils(this.controllerPath);
    		List<String> classList = scan.getFullyQualifiedClassNameList();
    		for (String clazz : classList) {
    			Class<?> clz = Class.forName(clazz);
    			if (!clz.isAnnotationPresent(RestController.class)) {
    				continue;
    			}
    			Method[] methods = clz.getDeclaredMethods();
    			for (Method method : methods) {
    				if (method.isAnnotationPresent(ApiRateLimit.class)) {
    					ApiRateLimit apiRateLimit = method.getAnnotation(ApiRateLimit.class);
    					String replenishRateKey = applicationName + "." + getApiUri(clz, method) + ".replenishRate";
    					String burstCapacityKey = applicationName + "." + getApiUri(clz, method) + ".burstCapacity";
    					limitMap.put(replenishRateKey, apiRateLimit.replenishRate());
    					limitMap.put(burstCapacityKey, apiRateLimit.burstCapacity());
    				}
    			}
    		}
    		rateLimitConf.setLimitMap(limitMap);
    		// 初始化值到配置中心
    		confInit.init(rateLimitConf);
    	}
    
    	 private String getApiUri(Class<?> clz, Method method) {
    	        StringBuilder uri = new StringBuilder();
    	        uri.append(clz.getAnnotation(RequestMapping.class).value()[0]);
    	        if (method.isAnnotationPresent(GetMapping.class)) {
    	            uri.append(method.getAnnotation(GetMapping.class).value()[0]);
    	        } else if (method.isAnnotationPresent(PostMapping.class)) {
    	            uri.append(method.getAnnotation(PostMapping.class).value()[0]);
    	        } else if (method.isAnnotationPresent(RequestMapping.class)) {
    	            uri.append(method.getAnnotation(RequestMapping.class).value()[0]);
    	        }
    	        return uri.toString();
    	 }
    }
    

    配置监听器

    SpringApplication application = new SpringApplication(FshHouseServiceApplication.class);
    application.addListeners(new InitGatewayApiLimitRateListener("com.fangjia.fsh.house.controller"));
    context = application.run(args);
    

    最后使用就很简单了,只需要增加注解就可以了

    @ApiRateLimit(replenishRate=10, burstCapacity=100)
    @GetMapping("/data")
    public HouseInfo getData(@RequestParam("name") String name) {
    	return new HouseInfo(1L, "上海", "虹口", "东体小区");
    }
    

    我这边只是给大家提供一种去实现的思路,也许大家还有更好的方案。

    我觉得只要不让每个开发都去关心这种非业务性质的功能,那就可以了,都在框架层面处理掉。当然实现原理可以跟大家分享下,会用很好,既会用又了解原理那就更好了。

    新书购买:单本75折包邮

    WechatIMG48.jpeg

    欢迎加入我的知识星球,一起交流技术,免费学习猿天地的课程(http://cxytiandi.com/course)

    PS:目前星球中正在星主的带领下组队学习Spring Cloud,等你哦!

    微信扫码加入猿天地知识星球

    猿天地

    展开全文
  • 微服务接口限流的设计、思考

    千次阅读 2018-11-29 10:13:59
    服务治理本身的概念比较大,包括鉴权、限流、降级、熔断、监控告警等等,本文聚焦于限流,根据笔者的实战经验,分享一些对微服务接口限流的思考。 本文试图讲清楚以下问题,如果您对限流也有类似的疑问或对某一话题...

    微服务拆分之后,系统之间的调用关系错综复杂,平台的整体复杂熵升高,出错的概率、debug 问题的难度都高了好几个数量级。所以,服务治理便成了微服务的一个技术重点。服务治理本身的概念比较大,包括鉴权、限流、降级、熔断、监控告警等等,本文聚焦于限流,根据笔者的实战经验,分享一些对微服务接口限流的思考。

    本文试图讲清楚以下问题,如果您对限流也有类似的疑问或对某一话题感兴趣,欢迎阅读本文。

    1. 微服务架构中没有接口限流,可能会遇到哪些问题?

    2. 针对微服务接口限流,如何选择合适的限流算法?

    3. 如何根据场景和性能要求权衡选择单机还是分布式限流?

    4. 如何根据业务需求灵活的选择不同的限流熔断机制?

    5. 如何对接口选择合适的限流时间粒度和最大限流值?

    6. 如何验证微服务接口限流功能的有效性和正确性?

    7. 如何打造高度容错、高 TPS、低延迟的限流框架?

    文章的最后,还顺带介绍了笔者开源的限流框架: ratelimiter4j,欢迎大家交流使用。

    1微服务接口限流的背景

    在应对秒杀、大促、双 11、618 等高性能压力的场景时,限流已经成为了标配技术解决方案,为保证系统的平稳运行起到了关键性的作用。不管应用场景是哪种,限流无非就是针对超过预期的流量,通过预先设定的限流规则选择性的对某些请求进行限流“熔断”。限于篇幅和作者的经验能力,本文主要讲微服务架构下,服务接口的限流。

    对于微服务来说,特别是一些中台微服务,其接口请求可能来自很多系统,例如用户服务的接口会被很多内部系统调用,比如 CRM, 促销系统等。对于服务于众多调用系统和应对海量接口请求的微服务来说,接口限流除了应对上面提到的一些大促秒杀场景之外,在下面一些场景中也发挥着很大的作用。

    作为提供接口服务的微服务系统,我们是无法限制调用方如何来使用我们的接口的,我们曾经就遇到过有一些调用方多线程并发跑 job 来请求我们的接口,也遇到到一些因为调用方的代码 bug 或者业务上面的突发流量,导致来自这个调用方的接口请求数量突增,过度争用服务线程资源,而来自其他调用方的接口请求因此来不及响应而排队等待,微服务整体的请求响应时间变长甚至超时。所以为了防止接口被过度调用,需要对每个调用方进行细粒度的访问限流。

    除了对调用者的访问频率进行限制外,我们有的时候还需要对某些接口的访问频率做限制。比如一些慢接口,可能因为逻辑复杂,处理时间会比较长,如果对慢接口的访问频率不加限制,过多的慢接口请求会一直占用服务的线程资源不释放,导致无法响应其他接口请求,影响微服务系统整体的吞吐量和接口响应时间,甚至引起大量的接口超时。除了慢接口,有些核心接口,因为一旦异常访问对业务的影响比较大,除了做调用鉴权之外,还需要做非预期异常流量的限流。

    综上所述,我们不仅仅需要针对大促秒杀场景的粗粒度的微服务接口限流功能:比如限制微服务集群单台机器每秒请求次数,我们还需要针对不同调用方甚至不同接口进行更加细粒度限流:比如限制 A 调用方对某个服务的某个的接口的每秒最大请求次数。

    2微服务接口限流的背景

    限流中的“流”字该如何解读呢?要限制的指标到底是什么?不同的场景对“流”的定义也是不同的,可以是网络流量,带宽,每秒处理的事务数 (TPS),每秒请求数 (hits per second),并发请求数,甚至还可能是业务上的某个指标,比如用户在某段时间内允许的最多请求短信验证码次数。

    从保证系统稳定可用的角度考量,对于微服务系统来说,最好的一个限流指标是:并发请求数。通过限制并发处理的请求数目,可以限制任何时刻都不会有过多的请求在消耗资源,比如:我们通过配置 web 容器中 servlet worker 线程数目为 200,则任何时刻最多都只有 200 个请求在处理,超过的请求都会被阻塞排队。

    上一节讲到,我们为了解决调用方对服务资源的过度争用问题,还需要针对不同调用方甚至不同接口做细粒度限流,所以,我们除了需要对系统整体的并发请求数做限制之外,还需要对每个调用方甚至不同接口的并发请求数做限制。但是,要想合理的设置某个调用方的最大允许并发数是比较困难的,这个值很难通过监控统计来获取,太小容易误杀,太大又起不了作用。所以我们还需要其他限流指标。

    对比 TPS 和 hits per second 的两个指标,我们选择使用 hits per second 作为限流指标。因为,对 TPS 的限流实际上是无法做的,TPS 表示每秒处理事务数,事务的开始是接收到接口请求,事务的结束是处理完成返回,所以有一定的时间跨度,如果事务开始限流计数器加一,事务结束限流计数器减一,则就等同于并发限流。而如果把事务请求接收作为计数时间点,则就退化为按照 hits per second 来做限流,而如果把事务结束作为计数时间点,则计数器的数值并不能代表系统当下以及接下来的系统访问压力。

    对 hits per second 的限流是否是一个有效的限流指标呢?答案是肯定的,这个值是可观察可统计的,所以方便配置限流规则,而且这个值在一定程度上反应系统当前和接下来的性能压力,对于这一指标的限流确实也可以达到限制对系统资源的使用。

    有了流的定义之后,我们接下来看几种常用的限流算法:固定时间窗口,滑动时间窗口,令牌桶算法,漏桶算法以及他们的改进版本。

    3固定、滑动时间窗口限流算法

    基于固定时间窗口的限流算法是非常简单的。首先需要选定一个时间起点,之后每次接口请求到来都累加计数器,如果在当前时间窗口内,根据限流规则(比如每秒钟最大允许 100 次接口请求),累加访问次数超过限流值,则限流熔断拒绝接口请求。当进入下一个时间窗口之后,计数器清零重新计数。

    这种基于固定时间窗口的限流算法的缺点在于:限流策略过于粗略,无法应对两个时间窗口临界时间内的突发流量。我们举一个例子:假设我们限流规则为每秒钟不超过 100 次接口请求,第一个 1s 时间窗口内,100 次接口请求都集中在最后的 10ms 内,在第二个 1s 的时间窗口内,100 次接口请求都集中在最开始的 10ms 内,虽然两个时间窗口内流量都符合限流要求 (<=100 个请求),但在两个时间窗口临界的 20ms 内会集中有 200 次接口请求,如果不做限流,集中在这 20ms 内的 200 次请求就有可能压垮系统,如图 -1:

    滑动时间窗口算法是对固定时间窗口算法的一种改进,流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。对比固定时间窗口限流算法,滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,对内存的占用会比较多。滑动时间窗口的算法模型如下:

    滑动窗口记录的时间点 list = (t_1, t_2, …t_k),时间窗口大小为 1 秒,起点是 list 中最小的时间点。当 t_m 时刻新的请求到来时,我们通过以下步骤来更新滑动时间窗口并判断是否限流熔断:

    STEP 1: 检查接口请求的时间 t_m 是否在当前的时间窗口 [t_start, t_start+1 秒) 内。如果是,则跳转到 STEP 3,否则跳转到 STEP 2.

    STEP 2: 向后滑动时间窗口,将时间窗口的起点 t_start 更新为 list 中的第二小时间点,并将最小的时间点从 list 中删除。然后,跳转到 STEP 1。

    STEP 3: 判断当前时间窗口内的接口请求数是否小于最大允许的接口请求限流值,即判断: list.size < max_hits_limit,如果小于,则说明没有超过限流值,允许接口请求,并将此接口请求的访问时间放入到时间窗口内,否则直接执行限流熔断。

    滑动时间窗口限流算法可以部分解决固定时间窗口的临界问题,上面的例子通过滑动时间窗口算法整形之后,第一个 1 秒的时间窗口的 100 次请求都会通过,第二个时间窗口最开始 10ms 内的 100 个请求会被限流熔断。

    即便滑动时间窗口限流算法可以保证任意时间窗口内接口请求次数都不会超过最大限流值,但是仍然不能防止在细时间粒度上面访问过于集中的问题,比如上面举的例子,第一个 1s 的时间窗口内 100 次请求都集中在最后 10ms 中。也就是说,基于时间窗口的限流算法,不管是固定时间窗口还是滑动时间窗口,只能在选定的时间粒度上限流,对选定时间粒度内的更加细粒度的访问频率不做限制。

    为了应对上面的问题,对于时间窗口限流算法,还有很多改进版本,比如:

    多层次限流,我们可以对同一个接口设置多条限流规则,除了 1 秒不超过 100 次之外,我们还可以设置 100ms 不超过 20 次 (这里需要设置的比 10 次大一些),两条规则同时限制,流量会更加平滑。除此之外,还有针对滑动时间窗口限流算法空间复杂度大的改进算法,限于篇幅,这里就不展开详说了。

    4令牌桶、漏桶限流算法

    上面我们讲了两种基于时间窗口的限流算法:固定时间窗口和滑动时间窗口算法,两种限流算法都无法应对细时间粒度的突发流量,对流量的整形效果在细时间粒度上不够平滑。本节介绍两种更加平滑的限流算法:令牌桶算法和漏桶算法,在某些场景下,这两种算法会优于时间窗口算法成为首选。实际上令牌桶和漏桶算法的算法思想大体类似,可以把漏桶算法作为令牌桶限流算法的改进版本,所以我们以介绍令牌桶算法为主。

    我们先来看下最基础未经过改进的令牌桶算法:

    1. 接口限制 t 秒内最大访问次数为 n,则每隔 t/n 秒会放一个 token 到桶中;

    2. 桶中最多可以存放 b 个 token,如果 token 到达时令牌桶已经满了,那么这个 token 会被丢弃;

    3. 接口请求会先从令牌桶中取 token,拿到 token 则处理接口请求,拿不到 token 则执行限流。

    令牌桶算法看似比较复杂,每间隔固定时间都要放 token 到桶中,但并不需要专门起一个线程来做这件事情。每次在取 token 之前,根据上次放入 token 的时间戳和现在的时间戳,计算出这段时间需要放多少 token 进去,一次性放进去,所以在实现上面也并没有太大难度。

    漏桶算法稍微不同与令牌桶算法的一点是:对于取令牌的频率也有限制,要按照 t/n 固定的速度来取令牌,所以可以看出漏桶算法对流量的整形效果更加好,流量更加平滑,任何突发流量都会被限流。因为令牌桶大小为 b,所以是可以应对突发流量的。当然,对于令牌桶算法,还有很多其他改进算法,比如:

    1. 预热桶

    2. 一次性放入多个令牌

    3. 支持一次性取多个令牌

    对比基于时间窗口的限流算法,令牌桶和漏桶算法对流量整形效果比时间窗口算法要好很多,但是并不是整形效果越好就越合适,对于没有提前预热的令牌桶,如果做否决式限流,会导致误杀很多请求。上述算法中当 n 比较小时,比如 50,间隔 20ms 才会向桶中放入一个令牌,而接口的访问在 1s 内可能随机性很强,这就会出现:尽管从曲线上看对最大访问频率的限制很有效,流量在细时间粒度上面都很平滑,但是误杀了很多本不应该拒绝的接口请求。

    所以令牌桶和漏桶算法比较适合阻塞式限流,比如一些后台 job 类的限流,超过了最大访问频率之后,请求并不会被拒绝,而是会被阻塞到有令牌后再继续执行。对于像微服务接口这种对响应时间比较敏感的限流场景,会比较适合选择基于时间窗口的否决式限流算法,其中滑动时间窗口限流算法空间复杂度较高,内存占用会比较多,所以对比来看,尽管固定时间窗口算法处理临界突发流量的能力较差,但实现简单,而简单带来了好的性能和不容易出错,所以固定时间窗口算法也不失是一个好的微服务接口限流算法。

    5限流算法分布式改造: 分布式限流算法

    相对于单机限流算法,分布式限流算法的是指: 算法可以分布式部署在多台机器上面,多台机器协同提供限流功能,可以对同一接口或者服务做限流。分布式限流算法相较于单机的限流算法,最大的区别就是接口请求计数器需要中心化存储,比如我们开源限流项目 ratelimiter4j 就是基于 Redis 中心计数器来实现分布式限流算法。

    分布式限流算法在引入 Redis 中心计数器这个独立的系统之后,系统的复杂度一下子高了很多,因为要解决一些分布式系统的共性技术问题:

    1. 数据一致性问题  

    接口限流过程包含三步操作:

    Step 1:“读”当前的接口访问计数 n;

    Step 2:”判断”是否限流;

    Step 3:“写”接口计数 n+1, if 接口限流验证通过

    在并发情况下,这 3 步 CAS 操作 (compare and swap) 存在 race condition。在多线程环境下,可以通过线程的加锁或者 concurrent 开发包中的 Atomic 原子对象来实现。在分布式情况下,思路也是类似的,可以通过分布式锁,来保证同一时间段只有一个进程在访问,但是引入分布式锁需要引入新的系统和维护锁的代码,代价较大,为了简单,我们选择另一种思路:借助 Redis 单线程工作模式 +Lua 脚本完美的支持了上述操作的原子性。限于篇幅,不展开代码讨论,详细可以参看开源项目 ratelimiter4j.

    2. 超时问题  

    对于 Redis 的各种异常情况,我们处理起来并不是很难,catch 住,封装为统一的 exception,向上抛,或者吞掉。但是如果 Redis 访问超时,会严重影响接口的响应时间甚至导致接口响应超时,这个副作用是不能接受的。所以在我们访问 Redis 时需要设置合理的超时时间,一旦超时,判定为限流失效,继续执行接口逻辑。Redis 访问超时时间的设置既不能太大也不能太小,太大可能会影响到接口的响应时间,太小可能会导致太多的限流失效。我们可以通过压测或者线上监控,获取到 Redis 访问时间分布情况,再结合服务接口可以容忍的限流延迟时间,权衡设置一个较合理的超时时间。

    3. 性能问题  

    分布式限流算法的性能瓶颈主要在中心计数器 Redis,从我们开源的 ratelimiter4j 压测数据来看,在没有做 Redis sharding 的情况下,基于单实例 Redis 的分布式限流算法的性能要远远低于基于内存的单机限流算法,基于我们的压测环境,单机限流算法可以达到 200 万 TPS,而分布式限流算法只能做到 5 万 TPS。所以,在应用分布式限流算法时,一定要考量限流算法的性能是否满足应用场景,如果微服务接口的 TPS 已经超过了限流框架本身的 TPS,则限流功能会成为性能瓶颈影响接口本身的性能。

    除了 TPS 之外,网络延迟也是一个需要特别考虑的问题,特别是如果中心计数器与限流服务跨机房跨城市部署,之间的网络延迟将会非常大,严重影响微服务接口的响应时间。

    6如何选择单机限流还是分布式限流

    首先需要说明一下:这里所说的单机限流和分布式限流与之前提到的单机限流算法和分布式限流算法并不是一个概念!为了提高服务的性能和可用性,微服务都会多实例集群部署,所谓单机限流是指:独立的对集群中的每台实例进行接口限流,比如限制每台实例接口访问的频率为最大 1000 次 / 秒,单机限流一般使用单机限流算法;所谓的分布式限流是指:提供服务级的限流,限制对微服务集群的访问频率,比如限制 A 调用方每分钟最多请求 1 万次“用户服务”,分布式限流既可以使用单机限流算法也可以使用分布式限流算法。

    单机限流的初衷是防止突发流量压垮服务器,所以比较适合针对并发做限制。分布式限流适合做细粒度限流或者访问配额,不同的调用方对不同的接口执行不同的限流规则,所以比较适合针对 hits per second 限流。从保证系统可用性的角度来说,单机限流更具优势,从防止某调用方过度竞争服务资源来说,分布式限流更加适合。

    分布式限流与微服务之间常见的部署架构有以下几种:

    1. 在接入层(api-gateway)集成限流功能  

    这种集成方式是在微服务架构下,有 api-gateway 的前提下,最合理的架构模式。如果 api-gateway 是单实例部署,使用单机限流算法即可。如果 api-gateway 是多实例部署,为了做到服务级别的限流就必须使用分布式限流算法。

    2. 限流功能封装为 RPC 服务  

    当微服务接收到接口请求之后,会先通过限流服务暴露的 RPC 接口来查询接口请求是否超过限流阈值。这种架构模式,需要部署一个限流服务,增加了运维成本。这种部署架构,性能瓶颈会出现在微服务与限流服务之间的 RPC 通信上,即便单机限流算法可以做到 200 万 TPS,但经过 RPC 框架之后,做到 10 万 TPS 的请求限流就已经不错了。

    3. 限流功能集成在微服务系统内  

    这种架构模式不需要再独立部署服务,减少了运维成本,但限流代码会跟业务代码有一些耦合,不过,可以将限流功能集成在切面层,尽量跟业务代码解耦。如果做服务级的分布式限流,必须使用分布式限流算法,如果是针对每台微服务实例进行单机限流,使用单机限流算法就可以。

    7针对不同业务使用不同限流熔断策略

    这里所讲的熔断策略,就是当接口达到限流上限之后,如何来处理接口请求的问题。前面也有提到过一些限流熔断策略了,所谓否决式限流就是超过最大允许访问频率之后就拒绝请求,比如返回 HTTP status code 429 等,所谓阻塞式限流就是超过最大允许访问频率之后就排队请求。除此之外,还有其他一些限流熔断策略,比如:记录日志,发送告警,服务降级等等。

    同一个系统对于不同的调用方也有可能有不同的限流熔断策略,比如对响应时间敏感的调用方,我们可能采用直接拒绝的熔断策略,对于像后台 job 这样对响应时间不敏感的调用方,我们可能采用阻塞排队处理的熔断策略。

    我们再来看下其他熔断策略的一些应用场景:比如限流功能刚刚上线,为了验证限流算法的有效性及其限流规则的合理性,确保不误杀请求,可以先采用日志记录 + 告警的限流熔断策略,通过分析日志判定限流功能正常工作后,再进一步升级为其他限流熔断策略。

    不同的熔断策略对于选择限流算法也是有影响的,比如令牌桶和漏桶算法就比较适合阻塞式限流熔断场景,如果是否决式的限流熔断场景就比较适合选择基于时间窗口的限流算法。

    8如何配置合理的限流规则

    限流规则包含三个部分:时间粒度,接口粒度,最大限流值。限流规则设置是否合理直接影响到限流是否合理有效。

    对于限流时间粒度的选择,我们既可以选择 1 秒钟不超过 1000 次,也可以选择 10 毫秒不超过 10 次,还可以选择 1 分钟不超过 6 万次,虽然看起这几种限流规则都是等价的,但过大的时间粒度会达不到限流的效果,比如限制 1 分钟不超过 6 万次,就有可能 6 万次请求都集中在某一秒内;相反,过小的时间粒度会削足适履导致误杀很多本不应该限流的请求,因为接口访问在细时间粒度上随机性很大。所以,尽管越细的时间粒度限流整形效果越好,流量曲线越平滑,但也并不是越细越合适。

    对于访问量巨大的接口限流,比如秒杀,双十一,这些场景下流量可能都集中在几秒内,TPS 会非常大,几万甚至几十万,需要选择相对小的限流时间粒度。相反,如果接口 TPS 很小,建议使用大一点的时间粒度,比如限制 1 分钟内接口的调用次数不超过 1000 次,如果换算成:一秒钟不超过 16 次,这样的限制就有点不合理,即便一秒内超过 16 次,也并没有理由就拒绝接口请求,因为对于我们系统的处理能力来说,16 次 / 秒的请求频率太微不足道了。即便 1000 次请求都集中在 1 分钟内的某一秒内,也并不会影响到系统的稳定性,所以 1 秒钟 16 次的限制意义不大。

    除了时间粒度之外,还需要根据不同的限流需求选择不同接口粒度,比如:

    1)限制微服务每个实例接口调用频率

    2)限制微服务集群整体的访问频率

    2)限制某个调用方对某个服务的调用频率

    3)限制某个调用方对某个服务的某个接口的访问频率

    4)限制某服务的某个接口的访问频率

    5)限制某服务的某类接口的访问频率

    对于最大允许访问频率的设置,需要结合性能压测数据、业务预期流量、线上监控数据来综合设置,最大允许访问频率不大于压测 TPS,不小于业务预期流量,并且参考线上监控数据。

    9如何配置合理的限流规则

    这里所说的有效性包含两个方面:限流算法的有效性和限流规则的有效性。在大促,秒杀,或者其他异常流量到来之前,我们需要事先通过实验来验证限流功能的有效性,用数据来证明限流功能确实能够拦截非预期的异常流量。否则,就有可能会因为限流算法的选择不够合适或者限流规则设置不合理,导致真正超预期流量到来的时候,限流不能起到保护服务的作用,超出预期的异常流量压垮系统。

    如何测试限流功能正确有效呢?尽管可以通过模拟流量或者线上流量回放等手段来测试,但是最有效的测试方法还是:通过导流的方式将流量集中到一小组机器上做真实场景的测试。对于测试结果,我们至少需要记录每个请求的如下信息:对应接口,请求时间点,限流结果 (通过还是熔断),然后根据记录的数据绘制成如下图表:

    从图表中,我们可以一目了然的了解限流前与限流后的流量情况,可以清晰的看到限流规则和算法对流量的整形是否合理有效。

    除了事先验证之外,我们还需要时刻监控限流的工作情况,实时了解限流功能是否运行正常。一旦发生限流异常,能够在不重启服务的情况下,做到热更新限流配置:包括开启关闭限流功能,调整限流规则,更换限流算法等等。

    10高容错高性能开源限流框架:ratelimiter4j

    ratelimiter4j 是一个高性能高容错易集成的限流框架, 从功能的角度来看限流功能的实现并不复杂,而非功能性的需求是系统开发的技术难点:

    1)低延迟:不能或者较小的影响接口本身的响应时间

    每个微服务接口请求都需要检查是否超过了限定的访问频率,无疑会增加接口的响应时间,而响应时间对于微服务接口来说,是一个非常关注的性能指标,所以让限流延迟尽可能小,是我们在开发 ratelimiter4j 限流框架时特别考虑的。

    2)高度容错:限流框架的异常不影响微服务的可用性

    接入限流本身是为了提供系统的可用性稳定性,不能因为限流本身的异常反过来影响到微服务的可用性,这个副作用是不能接受的。比如分布式限流算法依赖的 Redis 挂掉了,限流操作无法进行,这个时候业务接口也要能继续正常服务。

    3)高 TPS:限流框架的 TPS 至少要大于微服务本身的接口 TPS

    对于大规模服务来说,接口访问频率比较高,几万甚至几十万的 TPS,限流框架支持的 TPS 至少要高于服务本身的 TPS,否则就会因为限流本身的性能问题反过来拖垮服务。

    目前 ratelimiter4j 框架将限流规则组织成 trie  tree 数据结构,可以实现快速查询请求对应的接口限流规则,实验证明 trie tree 这种数据结构非常适合像 url 这种具有分级目录且目录重复度高的接口格式。

    针对分布式限流,目前 ratelimiter4j 压测得到的结果在响应时间可以接受的范围内最大支持 5 万 TPS,高并发对 TPS 的影响并不敏感,瓶颈主要在 Redis 中心计数器,接下来会通过改进算法及其中心计数器支持 sharding 的方式来优化性能。

    ratelimiter4j GitHub 地址:https://github.com/wangzheng0822/ratelimiter4j

     

     

     

    展开全文
  • 近日,STMicroelectronics为高效功率转换器提供限流芯片——STIL02-P5 和STIL04-P5。在很多的交流/支流电源转换中,都采用桥式电路。但是桥式电路会产生瞬间电流,这部分电流对功率转换不起作用,同时,常常会在PCB...
  • 本文是源码分析 Sentinel 系列的第十三篇,已经非常详细的介绍了 Sentinel 的架构体系、滑动窗口、调用链上下文、限流、熔断的实现原理,相信各位读者朋友们对Sentinel有一个较为体系化的认知了,这个时候是该开始...

    本文是源码分析 Sentinel 系列的第十三篇,已经非常详细的介绍了 Sentinel 的架构体系、滑动窗口、调用链上下文、限流、熔断的实现原理,相信各位读者朋友们对Sentinel有一个较为体系化的认知了,这个时候是该开始如何在生产环境进行运用了。

    本文将以 Dubbo 服务调用为案例剖析场景,尝试对官方提供的 Dubbo 适配器做一个研究学习并对此做出自己的评价,抛出我的观点,期待与大家共同探讨,交流。

    一个 Dubbo RPC 的简易调用过程如下图所示:
    在这里插入图片描述
    消费者会维护一个服务提供者列表,然后再发一起一个服务调用的时候会首先根据负载均衡算法从中选择一个服务提供者,然后发起 RPC 调用,在请求真实发送之前会依次通过客户端设置的过滤器链(Filter),然后经过网络传输到到达服务提供者,并执行完服务提供者端的 Filter,最后进入到业务逻辑执行并返回结果。

    Sentinel 与 Dubbo 的整合就是利用了 Dubbo 的 Filter 机制,为 Dubbo 提供对应的 过滤器,无缝实现限流、熔断等功能,做到业务无感知,即业务代码无需使用 Sentinel 相关的 API。

    接下来请大家带着在 Dubbo 中如何使用限流、熔断方面来看官方给出的解决方案。

    思考题:在看接下来的内容之前,建议大家稍作停顿,思考一下,在服务调用模型中,限流、熔断通常在哪个端做比较合适。

    1、从消费端来看限流与熔断

    在这里插入图片描述从消费端的视角,虽然提供了服务端的负载均衡,但从客户端不管是向192.168.1.3还是向192.168.1.4发送RPC调用,都会经过同一个 Sentinel Dubbo Filter。这个看似简单明了,但这是我们接下来思考的最基本最核心的点。

    我们先来看看官方提供的 Dubbo 适配器的核心实现:
    SentinelDubboConsumerFilter#invoke
    在这里插入图片描述
    消费端这边使用到了两个资源名称,一个是接口级别,例如 com.demo.service.UserService,另外一是方法级别,例如 com.demo.servcie.UserServce#findUser(Ljava.lang.String)。
    定义了两个资源后,Sentinel 会使用滑动窗口机制,为上述两个资源收集实时的调用信息,为后续的限流、熔断提供数据依据。

    限流规则是依附于具体某一个项目的,例如如下图所示:
    在这里插入图片描述限流、熔断都是根据资源级别,如果需要对消费端的调用进行限流的话,就需要为这两个资源配置对应的限流规则,如果不配置则默认通过,表示不限流。

    1.1 服务调用端(消费方)是否需要配置限流规则

    在 dubbo 的服务调用场景中,在消费端设置限流的规则的话,这个调用链是针对整个集群所有服务提供者的,例如当前集群中包含3个服务提供者,每个服务提供者用于1000tps的服务能力,那消费端的限流,应该配置的限流TPS应该为3000tps,如果设置为1000tps,则无法完整的利用服务端的能力,基于这样的情况,通常消费端无需配置限流规则。

    那是不是说消费端就没必要配置限流规则呢?其实也不是,有如下这个场景,例如调用第三方外部的计费类服务接口,对方通常为特定的账户等级设置对应的TPS上限,如果超过该调用频率就会抛出错误,这种情况还是需要设置限流规则,确保消费端以不超过要求进行调用,避免业务异常。

    1.2 服务调用端(消费方)是否需要配置熔断

    引入熔断的目的是避免服务端单节点响应慢而导致这个服务不稳定,例如示例中有3个服务提供者,如果192.168.1.3的服务提供者由于触发了BUG导致响应时间大大增加,导致发往该服务提供者的请求大概率超时,在这样的情况下希望在接下来某段时间内消费方发往这这个服务提供者的请求快速熔断降级,返回错误,由客户端重试其他服务提供者。其实现效果如下:
    在这里插入图片描述
    当前的 Sentinel 默认是否能满足上述的需求呢?

    我们以 Sentinel 基于异常比例熔断策略来进行阐述,如果资源的调用异常比例超过一定值是会触发降级熔断,抛出 DegradeException 异常。

    由于总共只有三个服务提供者,其中发往192.168.1.3的请求大概率会由于超时失败,则异常比例会超过设置的熔断降级规则,会触发降级,造成的效果是整个服务调用都会发送熔断降级,即调用192.168.1.4,5两个请求都不会被熔断,造成整个服务调用不可用,与期望不一致。即还是会出现一个节点的不稳定而导致整个服务不稳定的情况。

    其造成的根本原因是因为其资源的定义并没有包含服务提供者的信息,改进的初步方案:

    1. 在过滤器中再定义一个资源,加上服务提供的IP与端口号,例如 SphU.entry(“com.d.s.UserService@ip:port”),对单个服务提供者进行单独收集调用信息,并且需要提供一可配置的项,用来标记该类型的资源在做熔断判断可使用某一个资源的配置,例如配置为 com.d.s.UserService,表示用这个配置规则来判断熔断。
    2. 在熔断规则判断的时候,该资源使用被引用资源的熔断规则进行判断。

    最后来解答一下,熔断规则通常只需要配置在调用方即可。

    2、从服务来看限流与熔断

    由于服务端看限流与熔断就比较简单,因为服务端与客户端存在一个非常大的区别是客户端存在负载均衡机制,一个消费端对于同一资源来说,会调用多个服务提供者,而服务提供者对于一个资源来就是其自身,故限流规则,熔断规则都是针对个体,其复杂度降低。

    为了知识体系的完备性,我们来看一下 Sentinel Dubbo 在服务端的适配器的实现。

    SentinelDubboProviderFilter#invoke
    在这里插入图片描述
    这里有二个关键点:

    1. 使用了 ContextUtil 的 entry 方法,定义本次调用的上下文环境名称为:resourceName,默认为接口名与方法名、参数列表,例如 com.d.s.UserServce#findUser(Ljava.lang.String),源头为消费端的应用名称。
    2. 定义两个资源,这里与消费端相同,就不做重复解读。

    关于这个 ContextUtil 的 entry 方法非常关键,因为 Sentinel 中数据节点的统计是以 ContextName 为维度的。

    例如想对一个应用所有的操作 redis 操作统一设置为一个资源名,redisOpsResource,即想控制该应用整体的 redis 操作 tps,其场景如下:
    在这里插入图片描述
    例如初衷设计为 opsReisTotal 的整个 tps 为 500,例如从UserService#findser链路的访问 redis tps 为 400,而从 Order#createOrder 链路访问 redis tps 为 400,此时 redis 的整体 tps 已经达到了 800 tps,但你会发现并不会触发限流,因为对资源 RredisOpResource 的调用信息统计是以 context name 为维度的,不同的 context name 互不影响,从而无法做到全局控制。

    3、总结

    本文结合 Sentinel 官方对于 Dubbo 提供的适配器并加以理解,提出了如下观点,欢迎大家留言探讨,相互交流,共同进步。

    1. 限流规则根据不同的使用场景可以在客户端、服务端配置。
    2. 熔断规则通常在服务调用方配置即可。
    3. Sentinel 目前的熔断还实现的比较简单,无法解决集群中因为某一个节点的访问慢而触发熔断,并使服务稳定的能力。
    4. Sentienl 的实时统计是以调用上下文(Context Name),即 ContextUtil.entry 方法定义的上下文为维度的,这点非常关键,不然容易踩坑。

    好了,本文就介绍到这里了,您的点赞是对我持续输出高质量文章最大的鼓励。

    欢迎加笔者微信号(dingwpmz),加群探讨,笔者优质专栏目录:
    1、源码分析RocketMQ专栏(40篇+)
    2、源码分析Sentinel专栏(12篇+)
    3、源码分析Dubbo专栏(28篇+)
    4、源码分析Mybatis专栏
    5、源码分析Netty专栏(18篇+)
    6、源码分析JUC专栏
    7、源码分析Elasticjob专栏
    8、Elasticsearch专栏(20篇+)
    9、源码分析MyCat专栏

    展开全文
  • 有效防止高温失灵—PTC热敏电阻用作LED限流器、电子技术,开发板制作交流
  • 一般对外暴露的系统,在促销或者黑客攻击时会涌来大量的请求,为了保护系统不被瞬间到来的高并发流量给打垮, 就需要限流 . 本文主要阐述如何用nginx 来实现限流. 听说 Hystrix 也可以, 各位有兴趣可以去研究哈 . 2....
  • 模糊控制限流软启动器设计,金立,王梅,交流异步电机直接起动有许多弊端,通过分析传统PID控制存在的不足,讲述了一种将模糊控制和PID结合的异步电机软启动控制方式,并基
  • 摘要:介绍了一种适用于固态限流器的多输出高压隔离谐振式电源,给出了主电路拓扑结构,分析了其工作原理,并用PSpice对其进行了仿真验证,最后给出了实验结果。 理论分析和实验结果证明,负载谐振模式使负载电流...
  • Sentinel实现熔断与限流

    千次阅读 2020-06-07 22:59:27
    限流和降级是一个非常重要的手段,具体实施方法可以归纳为八字箴言,分别是限流,降级,熔断和隔离。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 官网地址:...
  • Sentinel: 使用注解限流

    千次阅读 2019-03-12 13:07:33
    在前面我们对Sentinel做了一个详细的介绍,可以...欢迎加入我的知识星球,一起交流技术,免费学习猿天地的课程( http://cxytiandi.com/course) PS:目前星球中正在星主的带领下组队学习Sentinel,等你哦!
  • 为此,基于系统复频域模型提出一种基于直流线路限流电抗电压积分值的多端直流环网故障快速检测方法。首先,详细分析了多端环状柔性直流配电网中各元件的时域和复频域等效模型。在此基础上,给出了限流电抗电压的计算...
  • 德力西RN 型高压限流熔断器pdf,德力西RN型高压限流熔断器:本产品使用于户内交流50Hz,额定电压6~35kV系统中作为电力设备及电力线路的过载或短路保护。
  • Sentinel 触发限流的实现类为 FlowSlot。我们再来简单思考一下,要实现触发限流,至少需要完成如下几件事情: 收集实时调用信息。 设置触发限流规则 根据限流规则与调用信息来决定是否对请求进行限流等。 如何收集...
  • 25、zuul之多维度限流

    千次阅读 2019-03-12 09:46:10
    对请求的目标URL进行限流(例如:某个URL每分钟只允许调用多少次) 对客户端的访问IP进行限流(例如:某个IP每分钟只允许请求多少次) 对某些特定用户或者用户组进行限流(例如:非VIP用户限制每分钟只...
  • 针对电力系统的短路电流限制问题,研究了一种新型桥式高温超导故障限流...使用PSpice电路仿真软件对限流器的限流性能进行仿真分析,与无限流器时的仿真数据相比,该限流器反应速度快,限流效果明显,尤其第1个短路电流
  • 基于Step-Down PWM电源管理芯片的PFM限流比较器电路设计、电子技术,开发板制作交流
  • 德力西RN1 型高压限流熔断器pdf,德力西RN1型高压限流熔断器:本产品使用于户内交流50Hz,额定电压6~35kV系统中作为电力设备及电力线路的过载或短路保护。
  • spring cloud gateway 限流的实现与原理

    千次阅读 2019-03-26 14:37:54
    在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。 常见的限流方式,比如Hystrix适用线程池隔离,超过线程池的负载,走熔断的逻辑...
  • 2.算法-手写滑动窗口限流

    千次阅读 2020-04-03 00:33:07
    限流 滑动窗口 手写滑动窗口限流 手写滑动窗口限流 public class slidingWindows { /** * 时间窗口 **/ private Long window; /** * 窗口的size 用于计算总的流量上限 **/ private Integer size = 2000; ...
  • MOS管限流

    千次阅读 2013-08-19 10:05:43
  • 冰河带你使用Nginx实现负载均衡、限流、缓存、黑白名单和灰度发布,建议收藏!!
  • 高效的 Linux 限流神器Trickle

    千次阅读 2015-08-10 09:09:11
    Trickle 是个非常小巧实用的 Linux 命令限流工具 Trickle 可以限制 Linux 命令行工具的上传和下载流量。在跨地域文件传输或者备份时非常有用,因为外网带宽往往会比较贵。 或者你想备份进程或者下载进程不对同机器...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 74,554
精华内容 29,821
关键字:

交流什么限流