在一個後臺系統中,流量控制屬於基礎組件的功能,其實,在好久以前的通信時代,流量控制就已經很是成熟了,在路由器交換機上面幾乎都有全面的流量控制的解決方案,像QoS這類流量整形的方案,都已是在網絡模型的各個層來進行流量的控制和分發了,能夠按照通道,按照端口,IP,MAC,業務類型等各個維度對流量進行整形和控制,好比讓語音類的這種高優先級的流量優先經過,而視頻聊天這種丟了幾幀數據其實沒什麼影響的低優先級流量慢點經過。前端
對於流量控制,在中後臺系統中,通常分紅兩個類型吧,一種是對鏈接數進行控制,保證一個機器有可控的鏈接數,一種是對真實流量的控制,保證機器能經過的流量有多少。golang
流量控制和緩存同樣,其實是對後端的服務起到一個保護的做用,不至於把後端的服務擊穿,不一樣的地方在於緩存保護的主要是讀操做,若是用緩存來保護寫操做的話,也是一個異步的過程,像下面這個圖同樣。算法
而流量控制主要保護的就是寫操做了,保證後端的服務別被寫請求給擊穿,目前咱們之因此不多見到流量控制的服務了,主要由於你們優化方向已經朝其餘兩個方向上來作了。後端
一是優化了後端的服務,把後端服務變成了一個能夠動態擴展的集羣,如今都是說要彈性擴展嘛,因此把優化放到後端服務器上去了,讓後端的服務器可以承載更多的寫流量,流量控制的東西相比就少了,並且從用戶體驗上來講,後端服務器可以承載更大的流量也能夠保證數據的實時性更好,不會產生數據的延遲,因此擴展後端的集羣規模成了一個優化方向。就像下圖這樣,後端的DB變成了一個集羣來保證寫規模的擴大。緩存
另一個優化方向就是引入了消息隊列這個東西,實際上消息隊列就是一個升級版本的流量控制系統,雖然它沒有用到流量控制的這些個算法,可是它達到的目前和流控其實差不太多,效果還更好,因此如今一旦出現後端扛不住寫的狀況,都在中間加上一個消息隊列來解決,一是解決了寫入流量的可控,二是還把系統給解耦了,一箭雙鵰。服務器
正由於上面的兩個緣由,如今關注流量控制的人變少了。但有時候,若是後端的服務不能抗住寫的壓力,而且也沒有足夠的資源去部署一個消息隊列的話(由於消息隊列的部署是須要單獨的服務器的,仍是有成本上的考慮),那麼作一個簡單的流控系統也基本能知足要求。微信
在本文中,基於鏈接的流量控制就不是咱們討論的範圍了,那個比較簡單一點。網絡
咱們所說的流量控制,你們比較瞭解的通常分紅兩種算法,一種是漏桶算法,一種是令牌桶算法,咱們這裏並不去深究這兩種算法的區別,這個能夠在網上很容易找到兩種算法的定義和算法描述,這兩種流控策略都是來源於路由器的IP層流量控制的算法,咱們從另一個角度來看看流控,咱們只借用這些算法的思想,從需求開始,本身一步一步設計一個流控系統。多線程
首先,拿到一個流量控制的需求,需求是入口流量是5MB/s,可是峯值流量是100MB/s,出口的流量要控制在50MB/s之內,數據還不能丟棄,如何來實現這個系統。異步
第一感受應該就是下圖這個樣子,中間有一個內存的FIFO隊列,寫入方不停的往這個隊列裏面寫入數據,而另外一端不停的讀取這個FIFO,而後把流量分發到後端上去,這樣就完成了數據不能丟棄這個需求,很像前面的那個消息隊列,可是慢着,要是前端的寫入流量一直保持在峯值的話,那麼這內存也爆了,因此除了內存的FIFO之外,還須要一個文件的FIFO來保證在一直是峯值的狀況下保證數據的不丟失,你要是問要是硬盤滿了怎麼辦,那我只能呵呵了,固然,也不是沒有解決辦法,把服務設計成多機模式嘛,這不在本文的討論範圍內。
總之,按照上圖的設計方法,基本能夠知足數據不丟的狀況了,對於FIFO的實現方式,能夠有不少種,一種是本身開鏈表,兩個指針一頭一尾,一邊寫一邊讀,若是兩邊都是多線程的話,鎖的設計須要特別注意,儘可能減小鎖的消耗。還有若是是使用想golang這樣的帶channel的語言,那麼直接丟到channel裏面也行,不過這樣就是內存不太可控,若是某一個時間段上的數據包都特別大的話,容易形成總體內存的飆升,看具體場景和硬件資源吧,這裏就不在贅述了,那麼,接下來就考慮流控了。
既然須要流量控制,那麼就是發送端在發送數據的時候得知道我如今這個數據能不能發,能發的話能夠全發出去仍是隻能發一部分,最簡單的辦法就是有個總體的流量控制器,每次發送端發送數據的時候都去詢問一下這裏流量控制器,如今有多少配額,我能用多少,結構圖以下圖所示,發送端去詢問流量控制器,而後拿到一個發送的配額,按照這個配額進行發送。
如今FIFO隊列也有了,流量控制器也有了,可是最關鍵的就是流量控制器如何工做的呢?接下來就要設計這個流量控制器了。
對於流控算法的設計,由於是配額制的,因此咱們首先得有一個配額的產生機制,好比需求裏面說的50MB/s,那就是每秒能夠產生50MB的配額,這個簡單,你把配額當作一個池子,每秒往池子里加50MB就好了,一旦池子滿了,就不加了嘛,這裏說的是每秒50MB,實際上加的時候能夠按照毫秒來,好比每10毫秒往池子裏面加0.5MB,這個用一個線程循環的線程就能夠完成。
Quota=0 while(1){ sleep(10MS) ; Quota+=0.5;if(Quota>=50){continue;} }
若是是golang的話,也能夠把這一部分交給channel來作,寫滿了就阻塞在channel上了,就像下面這樣:
QuotaChannel:=make([]int,100) for{ QuotaChannel<-5 time.Sleep(time.Millisecond*10) }
配額產生搞定了,那麼配額的消耗就是這個的反操做嘛,代碼就不寫了,可是若是是像前面那樣使用一個變量的話,那麼讀寫都要加鎖,而用golang的channel的話,就不用加鎖了,看上去後面的效率更高,但實際上差很少,由於golang的channel在實現上也是加了鎖的,並且鎖的粒度還比較大,因此用channel並無什麼效率上的提高。
整個流控算法實現完之後,就是下圖這樣樣子了。
上面實現了一個簡單的流控算法,咱們沒有去深究令牌桶和漏桶算法的具體實現方式,只是按照咱們本身的思想去設計了一個流控算法,實際上和令牌桶的思想基本是一致的,你們能夠去具體的看看令牌桶,再在算法上作一些優化。
通常的流控設計是跑在TCP層上的,就是能限制TCP層的流量,這樣有一個好處就是當你要發送的一個數據包大於50MB的時候,也可使用這個流控模型,由於在TCP的鏈接上是能夠分包進行發送的,能夠拆成屢次進行發送,這沒有什麼問題。
若是你的應用是跑在HTTP協議上的,如今不少語言都集成了HTTP的包,直接調用POST請求的API就能夠發送數據了,這時候若是出現大於50MB的數據包,就沒法進行拆包發送了,出現這種狀況,就須要根據實際的業務要求來進行修改和優化了。
若是隻是偶發狀況而且後端服務也能夠忍受的話,那就忍了吧,若是不是偶發狀況或者後端服務徹底不能忍,那你就別用語言自帶的HTTP包了,發送的時候本身創建TCP鏈接本身發送吧,這種改造也不是很複雜。
這篇文章說到的是一個流控的模型,借鑑了令牌桶和漏桶的一些算法,但不要覺得流控就是這樣的,令牌桶的模型,只是路由器中對IP報文進行流控的一種方式,在路由器上,還有TCP滑動窗口的流控方式,底層還有MAC層的流控方式等等。
實際上在TCP層以上是無法作到比較準確的流控的,由於一些協議的開銷,TCP自動重傳的開銷,這個流控器都監測不到,根本就無法準確的流控,只能說是作作簡單的流量限制,就拿本文說的例子來講,需求說是須要50MB/s的速度,這是一秒的速度,準確來說需求方是但願50MB的數據在一秒鐘之內均勻的發送出去,而咱們的這個流控模型根本沒法作到這一點,咱們來了一個數據,若是發現池子夠大,那麼50MB的數據直接就發走了,只是下一次發送的時候咱們還須要等0.5秒而已,而均勻的端到端的流控,通常採用的都是TCP的滑動窗口調整來實現,這已經超過本文要描述的了,若是你們感興趣能夠深刻去研究一下路由器的流控方式,包括滑動窗口流控啊,MAC層流控【802.1Qbb】啊。
通信領域仍是有不少很牛逼的算法的,只是如今通信領域有些沒落了,互聯網起來了,作個高可用,分佈式就很牛叉同樣,實際上和通信領域的許多東西比起來就是渣渣啊。就如同本文,寫了這麼多,可能在路由器芯片的功能介紹文檔上,就是一行,呵呵。
若是你以爲不錯,歡迎轉發給更多人看到,也歡迎關注個人公衆號,主要聊聊搜索,推薦,廣告技術,還有瞎扯。。文章會在這裏首先發出來:)掃描或者搜索微信號XJJ267或者搜索西加加語言就行