作者: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="https://www.cnblogs.com/javastack/p/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 重磅發布,黑暗模式太炸了!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/327743.html
標籤:Java
上一篇:簡單談談Java泛型
