一.為什么要流量整形(削峰填谷)
流量沖擊(高並發情況下帶來的突發流量):
上游調用方(push)不限速,很可能會把下游壓垮 eg:上游發起下單操作,下游完成秒殺業務邏輯(庫存檢查,庫存枷鎖,余額檢查,余額枷鎖,訂單生成,余額扣減,庫存扣減,生成流水,余額解鎖,庫存解鎖)
上游業務簡單,每秒發起了10000個請求,下游業務復雜,每秒只能處理2000個請求,上游不限速的下單,導致下游系統被壓垮,引發雪崩。
常見的優化方案有兩種:
1)上游隊列緩沖(put阻塞),限速發送
2)下游隊列緩沖(定時或者批量拉取pull,可以起到削平流量),限速執行
如果上游發送流量過大,MQ提供拉模式確實可以起到下游自我保護的作用,會不會導致消息在MQ中堆積?
答:下游MQ-client拉取消息,消息接收方能夠批量獲取消息,需要下游消息接收方進行優化(提供批處理,比如批量寫),否則整體吞吐量低,也會造成mq堆積
二.高並發系統保護策略
1.緩存:
緩存不單單能夠提升系統訪問速度、提高並發訪問量,也是保護數據庫、保護系統的有效方式。大型網站一般主要是“讀”,先走DB再走緩存。在大型“寫”系統中,先走緩存,再走DB,對DB進行批處理操作。(累積一些數據,批量寫入;內存里面的緩存隊列,mq像是一種緩存隊列)
2.降級:
根據服務器壓力,指定某些服務或者頁面的級別(需求不同,降級策略不同),以此釋放服務器資源,保證核心任務的正常運行
根據服務方式:可以拒接服務,可以延遲服務,也有時候可以隨機服務。
根據服務范圍:可以砍掉某個功能,可以砍掉某些模塊。
主要的目的就是提供有損服務,以保證服務正常運行。
3.限流:
限制系統的輸入和輸出流量已達到保護系統的目的。
一般來說系統的吞吐量是可以被測算的,一旦達到閾值,就需要限制流量。比如:延遲處理,拒絕處理,部分拒絕處理等等
實際場景中常用的限流策略:
- Nginx前端限流
按照一定的規則如IP、賬號、調用邏輯等在Nginx層面做限流
- 業務應用系統限流
1、客戶端限流(驗證碼;獲取動態請求路徑pathvariable,到達接口地址隱藏的效果)
2、服務端限流(redis限速器,延遲隊列)
- 數據庫限流
數據庫鏈接池化,Mysql(如max_connections)、Redis(如tcp-backlog)都會有類似的限制連接數的配置。 //backlog:待辦事項列表
三.限流算法:
1.計數器算法
@1使用JUC工具包下的AtomicInteger或Semaphore:
public class AtomicIntegerDemo { private static AtomicInteger count = new AtomicInteger(0); public static void exec() { if (count.get() >= 5) { System.out.println("請求用戶過多,請稍后在試!"+System.currentTimeMillis()/1000); } else { count.incrementAndGet();//或者單純的increment try { //處理核心邏輯 TimeUnit.SECONDS.sleep(1); System.out.println("--"+System.currentTimeMillis()/1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { count.decrementAndGet(); } } } }
public class SemaphoreDemo { private static Semaphore semphore = new Semaphore(50);//限流閾值50 public static void exec() { if(semphore.getQueueLength()>100){ //獲取等待中的請求數 System.out.println("當前等待排隊的任務數大於100,請稍候再試..."); } try { semphore.acquire(); // 處理核心邏輯 TimeUnit.SECONDS.sleep(1); System.out.println("--" + System.currentTimeMillis() / 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { semphore.release();//釋放信號量,給等待隊列 } } }
semaphore相對Atomic優點:如果是瞬時的高並發,可以使請求在阻塞隊列中排隊,而不是馬上拒絕請求,從而達到一個流量削峰的目的。
@2使用redis:
將用戶id+請求路徑(對接口限流)做key,將訪問次數count做value,加入過期時間,原理同AtomicInteger
還可以基於redis的列表,用戶id做key,value為訪問路徑,並設置過期時間,當list的長度大於閾值,拒絕 //rpushx(如果list不存在,插入失敗),rpush,expire,llen等命令
2.漏桶(leaky bucket)
漏桶算法的主要概念如下:
-
任意速率水滴流入漏桶
- 固定容量的漏桶,按照固定速率流出水滴
-
如果流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的。
- 漏桶為空,則無水滴可留
通過它,突發流量可以被整形以便為網絡提供一個穩定的流量。 漏桶算法比較好實現,在單機系統中可以使用隊列來實現
這里有兩個變量,一個是桶的大小,支持流量突發增多時可以存多少的水(burst),另一個是水桶漏洞!!的大小(rate)。
漏桶算法對於存在突發特性的流量來說缺乏效率.
3.令牌桶(Token Bucket)
令牌桶算法的原理是系統會以一個恆定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務。 當桶滿時,新添加的令牌被丟棄或拒絕。
令牌桶算法基本可以用下面的幾個概念來描述:
- 令牌將按照固定的速率被放入令牌桶中。比如每秒放10個。
- 令牌桶容量固定,超出容量的令牌被丟棄
- 當一個n個字節大小的數據包(請求,突發流量)到達,將從桶中刪除m個令牌,接着數據包被發送到網絡上(上游發送至下游)。
- 如果桶中的令牌不足m個,則不會刪除令牌,且該數據包將被限流(要么丟棄,要么緩沖區等待)。
核心:令牌算法是根據向桶中放令牌的速率去控制輸出的速率
漏桶和令牌桶的比較
令牌桶可以在運行時控制和調整數據處理的速率,並很好的處理某時的突發流量。
提升數據整體處理速度:放令牌的頻率增加(令牌桶),提高漏洞大小(漏桶)
降低整體數據處理速度:增加每次獲取令牌的個數(請求大小,多少)或者放令牌的頻率減小(令牌桶)。
整體而言,令牌桶算法更優,但是實現更為復雜一些。
限流實戰效果
- 生產環境背景
1、服務商接口所能提供的服務上限是400條/s
2、業務方調用服務方QPS可能達到800/s,1200/s,或者更高
3、當服務商接口訪問頻率超過400/s時,超過的量將拒絕服務,業務方丟失數據
4、業務方為多節點布置(分布式),但調用的是同一個服務商接口 - 限流策略
1、使用guava 的RateLimtier(令牌桶實現者),但是只能用於單機,分布式不可控
2、使用DelayQueue的過程相對較麻煩,耗時可能比較長,而且達不到精准限流的效果
3、使用redis的計數器,精准限流,編寫簡單,適用於分布式,ok
應用級限流:
應用配置:
對於一個應用系統來說一定會有極限並發/請求數,即總有一個TPS/QPS閥值
Tomcat,其Connector 其中一種配置有如下幾個參數:
acceptCount:如果Tomcat的線程都忙於響應,新來的連接會進入隊列排隊,如果超出排隊大小,則拒絕連接;
maxConnections: 瞬時最大連接數,超出的會排隊等待;
maxThreads:Tomcat能啟動用來處理請求的最大線程數,如果請求處理量一直遠遠大於最大線程數則可能會僵死。
這里對nginx限流做了解:
Nginx自身有的請求限制模塊、流量限制模塊(基於令牌桶算法),可以方便的控制令牌速率,自定義調節限流,實現基本的限流控制。
vi /export/servers/nginx/conf/nginx.conf limit_zone one $binary_remote_addr 20m; limit_req_zone $binary_remote_addr zone=req_one:20m rate=12r/s; limit_conn one 10; limit_req zone=req_one burst=120;
可以使用池化技術來限制總資源數:連接池、線程池。比如分配給每個應用的數據庫連接是100,那么本應用最多可以使用100個資源,超出了可以等待或者拋異常。
參考:
https://blog.csdn.net/syc001/article/details/72841951
https://www.cnblogs.com/softidea/p/6229543.html
https://www.cnblogs.com/mr-amazing/p/4935672.html