背景
在今天,基於SOA的架構已經大行其道。伴隨着架構的SOA化,相關聯的服務熔斷、降級、限流等思想,也在各種技術講座中頻繁出現。本文將結合Netflix開源的Hystrix框架,對這些思想做一個梳理。
伴隨着業務複雜性的提高,系統的不斷拆分,一個面向用戶端的API,其內部的RPC調用層層嵌套,調用鏈條可能會非常長。這會造成以下幾個問題:
API接口可用性降低
引用Hystrix官方的一個例子,假設tomcat對外提供的一個application,其內部依賴了30個服務,每個服務的可用性都很高,爲99.99%。那整個applicatiion的可用性就是:99.99%的30次方 = 99.7%,即0.3%的失敗率。
這也就意味着,每1億個請求,有30萬個失敗;按時間來算,就是每個月的故障時間超過2小時。
服務熔斷
爲了解決上述問題,服務熔斷的思想被提出來。類似現實世界中的“保險絲“,當某個異常條件被觸發,直接熔斷整個服務,而不是一直等到此服務超時。
熔斷的觸發條件可以依據不同的場景有所不同,比如統計一個時間窗口內失敗的調用次數。
服務降級
有了熔斷,就得有降級。所謂降級,就是當某個服務熔斷之後,服務器將不再被調用,此時客戶端可以自己準備一個本地的fallback回調,返回一個缺省值。
這樣做,雖然服務水平下降,但好歹可用,比直接掛掉要強,當然這也要看適合的業務場景。
關於Hystrix中fallback的使用,此處不詳述,參見官網。
項目搭建
需求:搭建一套分佈式rpc遠程通訊案例:比如訂單服務調用會員服務實現服務隔離,防止雪崩效應案例
訂單工程
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
<version>1.5.12</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.12</version>
</dependency>
</dependencies>
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private MemberService memberService;
@RequestMapping("/orderIndex")
public Object orderIndex() throws InterruptedException {
JSONObject member = memberService.getMember();
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",訂單服務調用會員服務:member:" + member);
return member;
}
@RequestMapping("/orderIndexHystrix")
public Object orderIndexHystrix() throws InterruptedException {
return new OrderHystrixCommand(memberService).execute();
}
@RequestMapping("/orderIndexHystrix2")
public Object orderIndexHystrix2() throws InterruptedException {
return new OrderHystrixCommand2(memberService).execute();
}
@RequestMapping("/findOrderIndex")
public Object findIndex() {
System.out.println("當前線程:" + Thread.currentThread().getName() + ",findOrderIndex");
return "findOrderIndex";
}
}
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private MemberService memberService;
@RequestMapping("/orderIndex")
public Object orderIndex() throws InterruptedException {
JSONObject member = memberService.getMember();
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",訂單服務調用會員服務:member:" + member);
return member;
}
@RequestMapping("/orderIndexHystrix")
public Object orderIndexHystrix() throws InterruptedException {
return new OrderHystrixCommand(memberService).execute();
}
@RequestMapping("/orderIndexHystrix2")
public Object orderIndexHystrix2() throws InterruptedException {
return new OrderHystrixCommand2(memberService).execute();
}
@RequestMapping("/findOrderIndex")
public Object findIndex() {
System.out.println("當前線程:" + Thread.currentThread().getName() + ",findOrderIndex");
return "findOrderIndex";
}
}
@Service
public class MemberService {
public JSONObject getMember() {
JSONObject result = HttpClientUtils.httpGet("http://127.0.0.1:8081/member/memberIndex");
return result;
}
}
public class HttpClientUtils {
private static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); // 日誌記錄
private static RequestConfig requestConfig = null;
static {
// 設置請求和傳輸超時時間
requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build();
}
/**
* post請求傳輸json參數
*
* @param url
* url地址
* @param json
* 參數
* @return
*/
public static JSONObject httpPost(String url, JSONObject jsonParam) {
// post請求返回結果
CloseableHttpClient httpClient = HttpClients.createDefault();
JSONObject jsonResult = null;
HttpPost httpPost = new HttpPost(url);
// 設置請求和傳輸超時時間
httpPost.setConfig(requestConfig);
try {
if (null != jsonParam) {
// 解決中文亂碼問題
StringEntity entity = new StringEntity(jsonParam.toString(), "utf-8");
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
CloseableHttpResponse result = httpClient.execute(httpPost);
// 請求發送成功,並得到響應
if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
String str = "";
try {
// 讀取服務器返回過來的json字符串數據
str = EntityUtils.toString(result.getEntity(), "utf-8");
// 把json字符串轉換成json對象
jsonResult = JSONObject.parseObject(str);
} catch (Exception e) {
logger.error("post請求提交失敗:" + url, e);
}
}
} catch (IOException e) {
logger.error("post請求提交失敗:" + url, e);
} finally {
httpPost.releaseConnection();
}
return jsonResult;
}
/**
* post請求傳輸String參數 例如:name=Jack&sex=1&type=2
* Content-type:application/x-www-form-urlencoded
*
* @param url
* url地址
* @param strParam
* 參數
* @return
*/
public static JSONObject httpPost(String url, String strParam) {
// post請求返回結果
CloseableHttpClient httpClient = HttpClients.createDefault();
JSONObject jsonResult = null;
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
try {
if (null != strParam) {
// 解決中文亂碼問題
StringEntity entity = new StringEntity(strParam, "utf-8");
entity.setContentEncoding("UTF-8");
entity.setContentType("application/x-www-form-urlencoded");
httpPost.setEntity(entity);
}
CloseableHttpResponse result = httpClient.execute(httpPost);
// 請求發送成功,並得到響應
if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
String str = "";
try {
// 讀取服務器返回過來的json字符串數據
str = EntityUtils.toString(result.getEntity(), "utf-8");
// 把json字符串轉換成json對象
jsonResult = JSONObject.parseObject(str);
} catch (Exception e) {
logger.error("post請求提交失敗:" + url, e);
}
}
} catch (IOException e) {
logger.error("post請求提交失敗:" + url, e);
} finally {
httpPost.releaseConnection();
}
return jsonResult;
}
/**
* 發送get請求
*
* @param url
* 路徑
* @return
*/
public static JSONObject httpGet(String url) {
// get請求返回結果
JSONObject jsonResult = null;
CloseableHttpClient client = HttpClients.createDefault();
// 發送get請求
HttpGet request = new HttpGet(url);
request.setConfig(requestConfig);
try {
CloseableHttpResponse response = client.execute(request);
// 請求發送成功,並得到響應
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// 讀取服務器返回過來的json字符串數據
HttpEntity entity = response.getEntity();
String strResult = EntityUtils.toString(entity, "utf-8");
// 把json字符串轉換成json對象
jsonResult = JSONObject.parseObject(strResult);
} else {
logger.error("get請求提交失敗:" + url);
}
} catch (IOException e) {
logger.error("get請求提交失敗:" + url, e);
} finally {
request.releaseConnection();
}
return jsonResult;
}
}
會員工程
@RestController
@RequestMapping("/member")
public class MemberController {
@RequestMapping("/memberIndex")
public Object memberIndex() throws InterruptedException {
Map<String, Object> hashMap = new HashMap<String, Object>();
hashMap.put("code", 200);
hashMap.put("msg", "memberIndex");
Thread.sleep(1500);
return hashMap;
}
}
Hystrix簡介
使用Hystrix實現服務隔離
Hystrix 是一個微服務關於服務保護的框架,是Netflix開源的一款針對分佈式系統的延遲和容錯解決框架,目的是用來隔離分佈式服務故障。它提供線程和信號量隔離,以減少不同服務之間資源競爭帶來的相互影響;提供優雅降級機制;提供熔斷機制使得服務可以快速失敗,而不是一直阻塞等待服務響應,並能從中快速恢復。Hystrix通過這些機制來阻止級聯失敗並保證系統彈性、可用。
什麼是服務隔離
當大多數人在使用Tomcat時,多個HTTP服務會共享一個線程池,假設其中一個HTTP服務訪問的數據庫響應非常慢,這將造成服務響應時間延遲增加,大多數線程阻塞等待數據響應返回,導致整個Tomcat線程池都被該服務佔用,甚至拖垮整個Tomcat。因此,如果我們能把不同HTTP服務隔離到不同的線程池,則某個HTTP服務的線程池滿了也不會對其他服務造成災難性故障。這就需要線程隔離或者信號量隔離來實現了。
使用線程隔離或信號隔離的目的是爲不同的服務分配一定的資源,當自己的資源用完,直接返回失敗而不是佔用別人的資源。
Hystrix實現服務隔離兩種方案
Hystrix的資源隔離策略有兩種,分別爲:線程池和信號量。
線程池方式
1、 使用線程池隔離可以完全隔離第三方應用,請求線程可以快速放回。 2、 請求線程可以繼續接受新的請求,如果出現問題線程池隔離是獨立的不會影響其他應用。
3、 當失敗的應用再次變得可用時,線程池將清理並可立即恢復,而不需要一個長時間的恢復。
4、 獨立的線程池提高了併發性
缺點:
線程池隔離的主要缺點是它們增加計算開銷(CPU)。每個命令的執行涉及到排隊、調度和上 下文切換都是在一個單獨的線程上運行的。
public class OrderHystrixCommand extends HystrixCommand {
@Autowired
private MemberService memberService;
/**
* @param group
*/
public OrderHystrixCommand(MemberService memberService) {
super(setter());
this.memberService = memberService;
}
protected JSONObject run() throws Exception {
JSONObject member = memberService.getMember();
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",訂單服務調用會員服務:member:" + member);
return member;
}
private static Setter setter() {
// 服務分組
HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("members");
// 服務標識
HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("member");
// 線程池名稱
HystrixThreadPoolKey threadPoolKey = HystrixThreadPoolKey.Factory.asKey("member-pool");
// #####################################################
// 線程池配置 線程池大小爲10,線程存活時間15秒 隊列等待的閾值爲100,超過100執行拒絕策略
HystrixThreadPoolProperties.Setter threadPoolProperties = HystrixThreadPoolProperties.Setter().withCoreSize(10)
.withKeepAliveTimeMinutes(15).withQueueSizeRejectionThreshold(100);
// ########################################################
// 命令屬性配置Hystrix 開啓超時
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
// 採用線程池方式實現服務隔離
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
// 禁止
.withExecutionTimeoutEnabled(false);
return HystrixCommand.Setter.withGroupKey(groupKey).andCommandKey(commandKey).andThreadPoolKey(threadPoolKey)
.andThreadPoolPropertiesDefaults(threadPoolProperties).andCommandPropertiesDefaults(commandProperties);
}
@Override
protected JSONObject getFallback() {
// 如果Hystrix發生熔斷,當前服務不可用,直接執行Fallback方法
System.out.println("系統錯誤!");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 500);
jsonObject.put("msg", "系統錯誤!");
return jsonObject;
}
}
信號量
使用一個原子計數器(或信號量)來記錄當前有多少個線程在運行,當請求進來時先判斷計數 器的數值,若超過設置的最大線程個數則拒絕該請求,若不超過則通行,這時候計數器+1,請求返 回成功後計數器-1。
與線程池隔離最大不同在於執行依賴代碼的線程依然是請求線程
tips:信號量的大小可以動態調整, 線程池大小不可以
public class OrderHystrixCommand2 extends HystrixCommand {
@Autowired
private MemberService memberService;
/**
* @param group
*/
public OrderHystrixCommand2(MemberService memberService) {
super(setter());
this.memberService = memberService;
}
protected JSONObject run() throws Exception {
// Thread.sleep(500);
// System.out.println("orderIndex線程名稱" +
// Thread.currentThread().getName());
// System.out.println("success");
JSONObject member = memberService.getMember();
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",訂單服務調用會員服務:member:" + member);
return member;
}
private static Setter setter() {
// 服務分組
HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("members");
// 命令屬性配置 採用信號量模式
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
// 使用一個原子計數器(或信號量)來記錄當前有多少個線程在運行,當請求進來時先判斷計數
// 器的數值,若超過設置的最大線程個數則拒絕該請求,若不超過則通行,這時候計數器+1,請求返 回成功後計數器-1。
.withExecutionIsolationSemaphoreMaxConcurrentRequests(50);
return HystrixCommand.Setter.withGroupKey(groupKey).andCommandPropertiesDefaults(commandProperties);
}
@Override
protected JSONObject getFallback() {
// 如果Hystrix發生熔斷,當前服務不可用,直接執行Fallback方法
System.out.println("系統錯誤!");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 500);
jsonObject.put("msg", "系統錯誤!");
return jsonObject;
}
}
應用場景
線程池隔離:
1、 第三方應用或者接口
2、 併發量大
信號量隔離:
1、 內部應用或者中間件(redis)
2、 併發需求不大
在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。緩存的目的是提升系統訪問速度和增大系統能處理的容量,可謂是抗高併發流量的銀彈;而降級是當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉,待高峯或者問題解決後再打開;而有些場景並不能用緩存和降級來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(評論的最後幾頁),因此需有一種手段來限制這些場景的併發/請求量,即限流。
爲什麼要互聯網項目要限流
互聯網雪崩效應解決方案
服務降級: 在高併發的情況, 防止用戶一直等待,直接返回一個友好的錯誤提示給客戶端。
服務熔斷:在高併發的情況,一旦達到服務最大的承受極限,直接拒絕訪問,使用服務降級。
服務隔離: 使用服務隔離方式解決服務雪崩效應
服務限流: 在高併發的情況,一旦服務承受不了使用服務限流機制(計時器(滑動窗口計數)、漏桶算法、令牌桶(Restlimite))
高併發限流解決方案
高併發限流解決方案限流算法(令牌桶、漏桶、計數器)、應用層解決限流(Nginx)
限流算法
常見的限流算法有:令牌桶、漏桶。計數器也可以進行粗暴限流實現。
計數器
它是限流算法中最簡單最容易的一種算法,比如我們要求某一個接口,1分鐘內的請求不能超過10次,我們可以在開始時設置一個計數器,每次請求,該計數器+1;如果該計數器的值大於10並且與第一次請求的時間間隔在1分鐘內,那麼說明請求過多,如果該請求與第一次請求的時間間隔大於1分鐘,並且該計數器的值還在限流範圍內,那麼重置該計數器
public class LimitService {
private int limtCount = 60;// 限制最大訪問的容量
AtomicInteger atomicInteger = new AtomicInteger(0); // 每秒鐘 實際請求的數量
private long start = System.currentTimeMillis();// 獲取當前系統時間
private int interval = 60;// 間隔時間60秒
public boolean acquire() {
long newTime = System.currentTimeMillis();
if (newTime > (start + interval)) {
// 判斷是否是一個週期
start = newTime;
atomicInteger.set(0); // 清理爲0
return true;
}
atomicInteger.incrementAndGet();// i++;
return atomicInteger.get() <= limtCount;
}
static LimitService limitService = new LimitService();
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i < 100; i++) {
final int tempI = i;
newCachedThreadPool.execute(new Runnable() {
public void run() {
if (limitService.acquire()) {
System.out.println("你沒有被限流,可以正常訪問邏輯 i:" + tempI);
} else {
System.out.println("你已經被限流呢 i:" + tempI);
}
}
});
}
}
}
滑動窗口計數
滑動窗口計數有很多使用場景,比如說限流防止系統雪崩。相比計數實現,滑動窗口實現會更加平滑,能自動消除毛刺。
滑動窗口原理是在每次有訪問進來時,先判斷前 N 個單位時間內的總訪問量是否超過了設置的閾值,並對當前時間片上的請求數 +1。
令牌桶算法
令牌桶算法是一個存放固定容量令牌的桶,按照固定速率往桶裏添加令牌。令牌桶算法的描述如下:
假設限制2r/s,則按照500毫秒的固定速率往桶中添加令牌;
桶中最多存放b個令牌,當桶滿時,新添加的令牌被丟棄或拒絕;
當一個n個字節大小的數據包到達,將從桶中刪除n個令牌,接着數據包被髮送到網絡上;
如果桶中的令牌不足n個,則不會刪除令牌,且該數據包將被限流(要麼丟棄,要麼緩衝區等待)。
使用RateLimiter實現令牌桶限流
RateLimiter是guava提供的基於令牌桶算法的實現類,可以非常簡單的完成限流特技,並且根據系統的實際情況來調整生成token的速率。
通常可應用於搶購限流防止沖垮系統;限制某接口、服務單位時間內的訪問量,譬如一些第三方服務會對用戶訪問量進行限制;限制網速,單位時間內只允許上傳下載多少字節等。
下面來看一些簡單的實踐,需要先引入guava的maven依賴。
/**
- 功能說明:使用RateLimiter 實現令牌桶算法
*/
@RestController
public class IndexController {
@Autowired
private OrderService orderService;
// 解釋:1.0 表示 每秒中生成1個令牌存放在桶中
RateLimiter rateLimiter = RateLimiter.create(1.0);
// 下單請求
@RequestMapping("/order")
public String order() {
// 1.限流判斷
// 如果在500秒內 沒有獲取不到令牌的話,則會一直等待
System.out.println("生成令牌等待時間:" + rateLimiter.acquire());
boolean acquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!acquire) {
System.out.println("你在怎麼搶,也搶不到,因爲會一直等待的,你先放棄吧!");
return "你在怎麼搶,也搶不到,因爲會一直等待的,你先放棄吧!";
}
// 2.如果沒有達到限流的要求,直接調用訂單接口
boolean isOrderAdd = orderService.addOrder();
if (isOrderAdd) {
return "恭喜您,搶購成功!";
}
return "搶購失敗!";
}
}
漏桶算法
漏桶作爲計量工具(The Leaky Bucket Algorithm as a Meter)時,可以用於流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下:
一個固定容量的漏桶,按照常量固定速率流出水滴;
如果桶是空的,則不需流出水滴;
可以以任意速率流入水滴到漏桶;
如果流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的。
令牌桶和漏桶對比:
令牌桶是按照固定速率往桶中添加令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數減爲零時則拒絕新的請求;
漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;
令牌桶限制的是平均流入速率(允許突發請求,只要有令牌就可以處理,支持一次拿3個令牌,4個令牌),並允許一定程度突發流量;
漏桶限制的是常量流出速率(即流出速率是一個固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),從而平滑突發流入速率;
令牌桶允許一定程度的突發,而漏桶主要目的是平滑流入速率;
兩個算法實現可以一樣,但是方向是相反的,對於相同的參數得到的限流效果是一樣的。
另外有時候我們還使用計數器來進行限流,主要用來限制總併發數,比如數據庫連接池、線程池、秒殺的併發數;只要全局總請求數或者一定時間段的總請求數設定的閥值則進行限流,是簡單粗暴的總數量限流,而不是平均速率限流。
應用級限流
限流總併發/連接/請求數
對於一個應用系統來說一定會有極限併發/請求數,即總有一個TPS/QPS閥值,如果超了閥值則系統就會不響應用戶請求或響應的非常慢,因此我們最好進行過載保護,防止大量請求涌入擊垮系統。
如果你使用過Tomcat,其Connector其中一種配置有如下幾個參數:
acceptCount:如果Tomcat的線程都忙於響應,新來的連接會進入隊列排隊,如果超出排隊大小,則拒絕連接;
maxConnections:瞬時最大連接數,超出的會排隊等待;
maxThreads:Tomcat能啓動用來處理請求的最大線程數,如果請求處理量一直遠遠大於最大線程數則可能會僵死。
詳細的配置請參考官方文檔。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都會有類似的限制連接數的配置。
限流總資源數
如果有的資源是稀缺資源(如數據庫連接、線程),而且可能有多個系統都會去使用它,那麼需要限制應用;可以使用池化技術來限制總資源數:連接池、線程池。比如分配給每個應用的數據庫連接是100,那麼本應用最多可以使用100個資源,超出了可以等待或者拋異常。
限流某個接口的總併發/請求數
如果接口可能會有突發訪問情況,但又擔心訪問量太大造成崩潰,如搶購業務;這個時候就需要限制這個接口的總併發/請求數總請求數了;因爲粒度比較細,可以爲每個接口都設置相應的閥值。可以使用Java中的AtomicLong進行限流:
適合對業務無損的服務或者需要過載保護的服務進行限流,如搶購業務,超出了大小要麼讓用戶排隊,要麼告訴用戶沒貨了,對用戶來說是可以接受的。而一些開放平臺也會限制用戶調用某個接口的試用請求量,也可以用這種計數器方式實現。這種方式也是簡單粗暴的限流,沒有平滑處理,需要根據實際情況選擇使用;
限流某個接口的時間窗請求數
即一個時間窗口內的請求數,如想限制某個接口/服務每秒/每分鐘/每天的請求數/調用量。如一些基礎服務會被很多其他系統調用,比如商品詳情頁服務會調用基礎商品服務調用,但是怕因爲更新量比較大將基礎服務打掛,這時我們要對每秒/每分鐘的調用量進行限速;一種實現方式如下所示:
平滑限流某個接口的請求數
之前的限流方式都不能很好地應對突發請求,即瞬間請求可能都被允許從而導致一些問題;因此在一些場景中需要對突發請求進行整形,整形爲平均速率請求處理(比如5r/s,則每隔200毫秒處理一個請求,平滑了速率)。這個時候有兩種算法滿足我們的場景:令牌桶和漏桶算法。Guava框架提供了令牌桶算法實現,可直接拿來使用。
Guava RateLimiter提供了令牌桶算法實現:平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。
接入層限流
接入層通常指請求流量的入口,該層的主要目的有:負載均衡、非法請求過濾、請求聚合、緩存、降級、限流、A/B測試、服務質量監控等等,可以參考筆者寫的《使用Nginx+Lua(OpenResty)開發高性能Web應用》。
對於Nginx接入層限流可以使用Nginx自帶了兩個模塊:連接數限流模塊ngx_http_limit_conn_module和漏桶算法實現的請求限流模塊ngx_http_limit_req_module。還可以使用OpenResty提供的Lua限流模塊lua-resty-limit-traffic進行更復雜的限流場景。
limit_conn用來對某個KEY對應的總的網絡連接數進行限流,可以按照如IP、域名維度進行限流。limit_req用來對某個KEY對應的請求的平均速率進行限流,並有兩種用法:平滑模式(delay)和允許突發模式(nodelay)。
ngx_http_limit_conn_module
limit_conn是對某個KEY對應的總的網絡連接數進行限流。可以按照IP來限制IP維度的總連接數,或者按照服務域名來限制某個域名的總連接數。但是記住不是每一個請求連接都會被計數器統計,只有那些被Nginx處理的且已經讀取了整個請求頭的請求連接纔會被計數器統計。