作者:lipengxs
來源:https://my.oschina.net/lipengxs/blog/4733443
背景
隨着微服務的流行,服務和服務之間的穩定性變得越來越重要。緩存、降級和限流是保護微服務系統運行穩定性的三大利器。
- 緩存:提升系統訪問速度和增大系統能處理的容量
- 降級:當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉
- 限流:解決服務雪崩,級聯服務發生阻塞時,及時熔斷,防止請求堆積消耗占用系統的線程、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限流
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
2)用戶限流
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
3)接口限流
@Bean
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
這里只是針對單節點限流,如果需要可以自定義全局限流
sentinel 限流
sentinel限流這里不做詳細描述,大家想了解可以參考下面文檔:https://mp.weixin.qq.com/s/4LjnzDg9uNQIJML6MIriEg
應用限流
這里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;
@Slf4j
public 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 > 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 重磅發布,黑暗模式太炸了!
覺得不錯,別忘了隨手點贊+轉發哦!