作者:lipengxs

起源:https://my.oschina.net/lipeng...

背景

随着微服务的风行,服务和服务之间的稳定性变得越来越重要。缓存、降级和限流是爱护微服务零碎运行稳定性的三大利器。

  • 缓存:晋升零碎访问速度和增大零碎能解决的容量
  • 降级:当服务出问题或者影响到外围流程的性能则须要临时屏蔽掉
  • 限流:解决服务雪崩,级联服务产生阻塞时,及时熔断,避免申请沉积耗费占用零碎的线程、IO等资源,造成其余级联服务所在服务器的解体

这里咱们次要说一下限流,限流的目标该当是通过对并发拜访/申请进行限速或者一个工夫窗口内的的申请进行限速来爱护零碎,一旦达到限度速率就能够拒绝服务、期待、降级。 首先,咱们须要去理解最根本的两种限流算法。

限流算法

  • 漏桶算法
  • 令牌桶算法
  • 计算器算法

限流框架

上面说一下现有风行的限流工具

guava

Google的Guava工具包中就提供了一个限流工具类——RateLimiter。

RateLimiter是基于“令牌通算法”来实现限流的。

hystrix

hystrix次要是通过资源池以及信号量来限流,临时能反对简略的限流

sentinel

限流比拟支流的三种算法:漏桶,令牌桶,滑动窗口。而Sentinel采纳的是最初一种,滑动窗口来实现限流的。当然sentinel不仅仅局限于限流,它是一个面向分布式服务架构的高可用流量防护组件,次要以流量为切入点,从限流、流量整形、熔断降级、零碎负载爱护、热点防护等多个维度来帮忙开发者保障微服务的稳定性。

限流实战

有很多利用都是能够间接在调用端、代理、网关等中间层进行限流,上面简略介绍下集中中间件限流形式

nginx限流

nginx限流形式有三种

  • limit_conn_zone
  • limit_req_zone
  • ngx_http_upstream_module

然而nginx限流不够灵便,不好动静配置。

zuul限流

除了zuul引入限流相干依赖

<dependency>     <groupid>com.marcosbarbero.cloud</groupid>     <artifactid>spring-cloud-zuul-ratelimit</artifactid>     <version>2.0.0.RELEASE</version></dependency>

相干配置如下:

zuul:    ratelimit:        key-prefix: your-prefix  #对利用来标识申请的key的前缀        enabled: true        repository: REDIS  #对应存储类型(用来存储统计信息)默认是IN_MEMORY        behind-proxy: true  #代理之后        default-policy: #可选 - 针对所有的路由配置的策略,除非特地配置了policies             limit: 10 #可选 - 每个刷新工夫窗口对应的申请数量限度             quota: 1000 #可选-  每个刷新工夫窗口对应的申请工夫限度(秒)              refresh-interval: 60 # 刷新工夫窗口的工夫,默认值 (秒)               type: #可选 限流形式                    - user                    - origin                    - url          policies:                myServiceId: #特定的路由                      limit: 10 #可选- 每个刷新工夫窗口对应的申请数量限度                      quota: 1000 #可选-  每个刷新工夫窗口对应的申请工夫限度(秒)                      refresh-interval: 60 # 刷新工夫窗口的工夫,默认值 (秒)                      type: #可选 限流形式                          - user                          - origin                          - url

留神这里的仓库如果是针对全局限流,那么能够思考存到redis中,这里的zuul.ratelimit.repository能够设置为redis,然而如果扩容后则须要动静调整,不过灵便,所以这里我倡议还是抉择本地内存(INM_MOMERY)或者不设置,这样伸缩容后能够主动扩大,不必变更配置,

如果须要动静更新,能够集成apollo配置进行动静更新,

public class ZuulPropertiesRefresher implements ApplicationContextAware {    private ApplicationContext applicationContext;    @Autowired    private RouteLocator routeLocator;    @ApolloConfigChangeListener(interestedKeyPrefixes = "zuul.",value="zuul.yml")    public void onChange(ConfigChangeEvent changeEvent) {        refreshZuulProperties(changeEvent);    }    private void refreshZuulProperties(ConfigChangeEvent changeEvent) {        log.info("Refreshing zuul properties!");        /**         * rebind configuration beans, e.g. ZuulProperties         * @see org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent         */        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));        /**         * refresh routes         * @see org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration.ZuulRefreshListener#onApplicationEvent         */        this.applicationContext.publishEvent(new RoutesRefreshedEvent(routeLocator));        log.info("Zuul properties refreshed!");    }    @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        this.applicationContext = applicationContext;    }}

springcloud gateway限流

在Spring Cloud Gateway中,有Filter过滤器,因而能够在“pre”类型的Filter中自行实现上述三种过滤器。

然而限流作为网关最根本的性能,Spring Cloud Gateway官网就提供了RequestRateLimiterGatewayFilterFactory这个类,实用Redis和lua脚本实现了令牌桶的形式。

具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:

具体源码不打算在这里讲述,读者能够自行查看,代码量较少,先以案例的模式来解说如何在Spring Cloud Gateway中应用内置的限流过滤器工厂来实现限流。

首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:

 <dependency>    <groupid>org.springframework.cloud</groupid>    <artifactid>spring-cloud-starter-gateway</artifactid></dependency><dependency>    <groupid>org.springframework.boot</groupid>    <artifatid>spring-boot-starter-data-redis-reactive</artifatid></dependency>

复制代码在配置文件中做以下的配置:

spring:  redis:    host: 127.0.0.1    port: 6379  cloud:    gateway:      routes:      - id: limit_route        uri: http://httpbin.org:80/get        predicates:        - After=2017-01-20T17:42:47.789-07:00[America/Denver]        filters:        - name: RequestRateLimiter          args:            key-resolver: '#{@hostAddrKeyResolver}'            redis-rate-limiter.replenishRate: 1            redis-rate-limiter.burstCapacity: 3

配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器须要配置三个参数:

  • burstCapacity,令牌桶总容量。
  • replenishRate,令牌桶每秒填充均匀速率。
  • key-resolver,用于限流的键的解析器的 Bean 对象的名字。它应用 SpEL 表达式依据#{@beanName}从 Spring 容器中获取 Bean 对象。

能够通过KeyResolver来指定限流的Key,比方咱们须要依据用户来做限流,IP来做限流等等。

1)IP限流

@Beanpublic KeyResolver ipKeyResolver() {    return exchange -&gt; Mono.just(exchange.getRequest().getRemoteAddress().getHostName());}

2)用户限流

@BeanKeyResolver userKeyResolver() {    return exchange -&gt; Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));}

3)接口限流

@BeanKeyResolver apiKeyResolver() {    return exchange -&gt; Mono.just(exchange.getRequest().getPath().value());}

这里只是针对单节点限流,如果须要能够自定义全局限流

sentinel 限流

sentinel限流这里不做详细描述,大家想理解能够参考上面文档:https://mp.weixin.qq.com/s/4L...

利用限流

这里springboot应用服务须要限流的话,这里给的计划是集成google的guava类库,大家在网上能搜寻到很多demo,我这里不做详细描述,次要是上面api的应用:

 RateLimiter.create(callerRate);

当初容器比拟火,当初如果部署在容器或者虚拟机上,咱们须要动静调整资源数后,那么限流也会跟着变动,这里说一下如何实现动静限流。第一步必定是集成配置核心实现配置动静更新,至于说失效形式有几种 计划一: 减少监听器,当配置变动时从新创立限流对象

计划二: 限流对象定时创立,这里引入了利用缓存框架,上面给个demo

import com.ctrip.framework.apollo.Config;import com.github.benmanes.caffeine.cache.Cache;import com.github.benmanes.caffeine.cache.Caffeine;import com.google.common.util.concurrent.RateLimiter;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.concurrent.TimeUnit;@Slf4jpublic class RateLimitInterceptor implements HandlerInterceptor {    private Config config;    private static final String RATE_TYPE_GLOBAL = "global";    private static final String RATE_TYPE_URL = "url";    //全局限流    public RateLimitInterceptor(Config config) {        this.config = config;    }    Cache<object, ratelimiter> rateLimiterCache = Caffeine.newBuilder()            .initialCapacity20            .expireAfterWrite(2, TimeUnit.MINUTES)            .maximumSize100            .softValues()            .recordStats()            .build();    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {        if (StringUtils.isBlank(request.getRequestURI()) || request.getRequestURI().startsWith("/actuator/")                || request.getRequestURI().startsWith("/srch-recommend/fault-tolerant/health")||request.getRequestURI().startsWith("/health")) {            return true;        }         try {            boolean rateLimitEnabled=config.getBooleanProperty("ratelimit.enabled", false);            if(!rateLimitEnabled){                return true;            }            if (!do(RATE_TYPE_GLOBAL, StringUtils.EMPTY, "ratelimit.global")) {                return false;            }            String url = request.getRequestURI();            if (StringUtils.isNotBlank(url)) {                return do(RATE_TYPE_URL, url, "ratelimit.url.");            }            return true;        } catch (Exception e) {            log.warn("RateLimitInterceptor error message:{}", e.getMessage(), e);            return true;        }    }    private boolean doRateLimiter(String rateType, String key, String configPrefix) {        String cacheKey = rateType + "-" + key;        RateLimiter rateLimiter = rateLimiterCache.getIfPresent(cacheKey);        if (rateLimiter == null) {            int callerRate = config.getIntProperty(configPrefix + uniqueKey, 0);            if (callerRate &gt; 0) {                rateLimiter = RateLimiter.create(callerRate);                rateLimiterCache.put(cacheKey, rateLimiter);            }        }        return rateLimiter == null || rateLimiter.tryAcquire();    }    @Override    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,                           ModelAndView modelAndView) {    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {    }}

当然这里如果有业务相干的限流能够依据参考下面的demo本人来实现限流。

近期热文举荐:

1.1,000+ 道 Java面试题及答案整顿(2021最新版)

2.别在再满屏的 if/ else 了,试试策略模式,真香!!

3.卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.5 重磅公布,光明模式太炸了!

5.《Java开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞+转发哦!