背景
随着微服務的流行,服務和服務之間的穩定性變得越來越重要。緩存、降級和限流是保護微服務系統運作穩定性的三大利器。
緩存:提升系統通路速度和增大系統能處理的容量
降級:當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉
限流:解決服務雪崩,級聯服務發生阻塞時,及時熔斷,防止請求堆積消耗占用系統的線程、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自己來實作限流。