当前位置: 首页 > 后端技术 > Java

微服务如何限流?算法+框架+实战!

时间:2023-04-01 15:16:41 Java

作者:lipengxs来源:https://my.oschina.net/lipeng...背景随着微服务的普及,服务之间的稳定性变得越来越重要。缓存、降级、限流是保护微服务系统稳定性的三大利器。缓存:提高系统访问速度,增加系统的处理能力。降级:当服务出现问题或影响核心进程性能时,需要临时封堵限流:解决服务雪崩,封堵级联服务。及时熔断,防止Request堆积消耗系统的线程、IO等资源,导致其他级联服务所在的服务器崩溃。这里主要说一下限流。限流的目的应该是限制并发访问/请求的速度或者在一个时间窗口内。限制请求速率以保护系统。一旦达到限制速率,就可以拒绝、等待和降级服务。首先,我们需要了解两种最基本的限流算法。限流算法LeakyBucketAlgorithmTokenBucketAlgorithmCalculatorAlgorithm限流框架说说现有流行的限流工具guavaGoogle的Guava工具包提供了一个限流工具类——RateLimiter。RateLimiter是基于“令牌传递算法”来实现限流的。hystrixhystrix主要使用资源池和信号量来限流,暂时支持简单限流哨兵三种主流限流算法:leakybucket,tokenbucket,slidingwindow。Sentinel使用最后一种,滑动窗口来实现限流。当然sentinel不仅仅局限于限流。是分布式服务架构的高可用流量保护组件。维度帮助开发者保证微服务的稳定性。在调用者、代理、网关等中间层可以直接限流的应用有很多。下面简单介绍一下集中式中间件限流nginx限流nginx三种限流方式limit_conn_zonelimit_req_zonengx_http_upstream_module但是nginx限流不够灵活,不好动态配置。zuul限流除了zuul引入限流相关依赖com.marcosbarbero.cloudspring-cloud-zuul-ratelimit2.0.0.RELEASE相关配置如下:zuul:ratelimit:key-prefix:your-prefix#用于标识请求的key对应的前缀enabled:truerepository:REDIS#对应的存储类型(使用存储统计信息)默认为IN_MEMORYbehind-proxy:true#在代理后面default-policy:#Optional-所有路由配置的策略,除非专门配置了策略limit:10#Optional-限制每个路由对应的请求数refreshtimewindowquota:1000#可选-每个刷新时间窗对应的请求时限(秒)refresh-interval:60#刷新时间窗时间,默认值(秒)type:#可选限流方式-user-origin-urlpolicies:myServiceId:#具体路由限制:10#Optional-每个刷新时间窗口对应的请求数quota:1000#Optional-每个刷新时间窗口对应的请求时间限制(秒)refresh-interval:60#Refresh时间窗时间,默认默认值(秒)type:#可选限流方式-user-origin-url注意如果这里的仓库是全局限流的,可以考虑存放在redis中,这里zuul.ratelimit.repository可以设置为redis,但是扩容后需要动态调整,但是比较灵活,所以这里我建议选择localmemory(INM_MOMERY)或者不设置,这样扩容扩容后可以自动扩容,不需要改变配置。如果需要动态更新,可以集成apollo配置进行动态更新,publicclassZuulPropertiesRefresherimplementsApplicationContextAware{privateApplicationContextapplicationContext;@AutowiredprivateRouteLocatorrouteLocator;@ApolloConfigChangeListener(interestedKeyPrefixes="zuul.",value="zuul.yml")publicvoidonChange(ConfigChangeEventchangeEvent){refreshZuulProperties(changeEvent);}privatevoidrefreshZuulProperties(ConfigChangeEventchangeEvent){log.info("正在刷新zuul属性!");/***重新绑定配置bean,例如ZuulProperties*@seeorg.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#oncationEvent*/this.applicationContext.publishEvent(newEnvironmentChangeEvent(changeEvent.changedKeys()));/***刷新路由*@seeorg.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration.ZuulRefreshListener#onApplicationEventContext*/this.发布事件(新的RoutesRefreshedEvent(routeLocator));log.info("Zuul属性刷新!");}@OverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{this.applicationCloud=applicationContext;}}springcloudgateway限流其中,有一个Filter过滤器,所以可以在“pre”类型的Filter中实现以上三个过滤器,但是限流是网关最基本的功能。SpringCloudGateway官方提供了RequestRateLimiterGatewayFilterFactory类,适用于Redis和Lua脚本实现令牌桶的方式。具体的实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在下图所示的文件夹中:具体源码这里就不多说了,读者可以自行查看,代码量不大,并首先以案例的形式说明如何使用SpringCloudGatewayThrottles是使用内置的节流过滤器工厂实现的。首先在项目的pom文件中引入gateway的启动依赖和redis的react依赖。代码如下:spring-boot-starter-data-redis-reactive复制代码,在配置文件中进行如下配置Configuration:spring:redis:host:127.0.0.1port:6379云:网关:路由:-id:limit_routeuri:http://httpbin.org:80/get谓词:-After=2017-01-20T17:42:47.789-07:00[美国/丹佛]过滤器:-name:RequestRateLimiterargs:key-resolver:'#{@hostAddrKeyResolver}'redis-rate-limiter.replenishRate:1redis-rate-limiter.burstCapacity:3配置的redis信息,并配置RequestRateLimiter的限流过滤器,需要配置三个参数:burstCapacity,令牌桶的总容量。replenishRate,每秒填充令牌桶的平均速率。key-resolver,限制密钥的解析器的Bean对象的名称。它使用SpEL表达式根据#{@beanName}从Spring容器中获取Bean对象。您可以使用KeyResolver指定限流的键。比如我们需要根据用户限流,IP来限流等等。1)IP限流@BeanpublicKeyResolveripKeyResolver(){returnexchange->Mono.just(exchange.getRequest().getRemoteAddress().getHostName());}2)用户限流@BeanKeyResolveruserKeyResolver(){returnexchange->Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));}3)接口限流@BeanKeyResolverapiKeyResolver(){returnexchange->Mono.just(exchange.getRequest().getPath().value());}这个只针对单节点限流。如果需要,可以自定义全局限流哨兵。Sentinel限流这里就不详细介绍了。如果想了解更多可以参考以下文档:https://mp.weixin.qq.com/s/4L...应用限流如果springboot应用服务需要限流,这里的解决方案是集成谷歌的番石榴类库。网上可以搜到很多demo。这里不做详细描述,主要是使用下面的api:RateLimiter.create(callerRate);现在流行容器,如果部署在容器或者虚拟机上,我们需要动态调整资源数量,那么限流也会随之变化,这里介绍如何实现动态限流。第一步肯定是集成配置中心,实现动态配置更新。至于生效的方式,有几种方案1:添加监听器,当配置改变时重新创建限流对象。框架,这里有一个demoimportcom.ctrip.framework.apollo.Config;importcom.github.benmanes.caffeine.cache.Cache;importcom.github.benmanes.caffeine.cache.Caffeine;importcom.google.common.util.concurrent.RateLimiter;导入lombok.extern.slf4j.Slf4j;导入org.apache.commons.lang3.StringUtils;导入org.springframework.web.servlet.HandlerInterceptor;导入org.springframework.web.servlet.ModelAndView;导入javax。servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.util.concurrent.TimeUnit;@Slf4jpublicclassRateLimitInterceptorimplementsHandlerInterceptor{privateConfigconfig;privatestaticfinalStringRATE_TYPE_GLOBAL="global";privatestaticfinalStringRATE_TYPE_URL="url";//全局限流publicRateLimitInterceptor(Configconfig){this.config=config;}缓存<对象,速率限制器>rateLimiterCache=Caffeine.newBuilder().initialCapacity20.expireAfterWrite(2,TimeUnit.MINUTES).maximumSize100.softValues().recordStats().build();@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){if(StringUtils.isBlank(request.getRequestURI())||request.getRequestURI().startsWith("/actuator/")||request.getRequestURI().startsWith("/srch-recommend/fault-tolerant/health")||request.getRequestURI().startsWith("/health")){返回真;}try{booleanrateLimitEnabled=config.getBooleanProperty("ratelimit.enabled",false);如果(!rateLimitEnabled){返回真;}if(!do(RATE_TYPE_GLOBAL,StringUtils.EMPTY,"ratelimit.global")){returnfalse;}字符串url=request.getRequestURI();如果(StringUtils.isNotBlank(url)){returndo(RATE_TYPE_URL,url,"ratelimit.url.");}返回真;}catch(Exceptione){log.warn("RateLimitInterceptor错误信息:{}",e.getMessage(),e);返回真;}}privatebooleandoRateLimiter(StringrateType,Stringkey,StringconfigPrefix){StringcacheKey=rateType+"-"+key;RateLimiterrateLimiter=rateLimiterCache.getIfPresent(cacheKey);如果(rateLimiter==null){intcallerRate=config.getIntProperty(configPrefix+uniqueKey,0);如果(callerRate>0){rateLimiter=RateLimiter.create(callerRate);rateLimiterCache.put(cacheKey,rateLimiter);}}返回rateLimiter==null||rateLimiter.tryAcquire();}@OverridepublicvoidpostHandle(HttpServletRequest请求,HttpServletResponse响应,Objecthandler,ModelAndViewmodelAndView){}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){}}当然这里如果有业务相关的限流可以参考上面的demo实现限流。热点文章推荐:1.1000+Java面试题及答案(2021最新版)2.别在满屏的if/else里,试试策略模式,真香!!3.操!Java中xx≠null的新语法是什么?4、SpringBoot2.5发布,深色模式太炸了!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!