這次救火討論的是流控,流控可以很簡單,也可以非常復雜,特別是動態流控。我們有一個產品在T國某個運營商遇到了麻煩,這個運營商的母公司是歐洲的運營商,而歐洲的運營商對於產品驗收的苛刻是出了名的,而這次給我們帶來麻煩的就是流控。
這個產品的大致功能就是把訂閱的短消息或者彩信內容發送到戶手機上,就類似於在幾年前火了一段時間的手機報。這個產品的流控有些復雜:
1、首先這套系統部署是集群,假設集群有6台主機,這6台主機的每秒下發的消息量不能超過一個值,假設為10000,為什么有這個要求,是因為下游執行發送消息的短消息網關或彩信網關有流控,你發的快了,下游系統會拒絕你,所以這個總的TPS不能超。
2、這些消息的來源都是來自於某個SP或者CP,每個CP或者SP在簽約平台的時候,它有一個最大每秒發送量,發送量分為彩信、短信以及WAP Push渠道。
3、除了上面的限制以外,CP和SP又分了優先級,如果低先級和高優先級的時候在一起發送的,一定要先等到高優先級的先發送完成。
還有一些規則要求,時間比較久了,我記不是特別清楚了。
這套系統在國內和國外不少地方都上線了,不能說質量多好,也沒有太大的問題,我接手這個產品大概幾個月的時候在這個局點遇到了問題,客戶投訴他們通過線上實際的監控發現2個嚴重的問題,如果不解決,他們會******:
1,這套系統在高峰期間實際下發的每秒流量沒有達到當時他們購買的流量,比如他們購買為10000條/秒,但是他們實際監測發現高峰的時候,可能只有9000條/秒.
2,系統的流控不穩定,當時合同簽署的流控上下波動不能大於正負10%。舉個例子,假設他們有一個最大的SP,在上午10點到11點獨占發送消息,他們設定按200條/秒發送,這時沒有任何其它的SP搶占通道,那么消息發送速率應該是在180~220條/秒,但是實際發送的速率會在這個區間之外。
當時我接手這個產品幾個月時間,對系統的大致實現有了解,但是細節並不是清楚,負責這個產品的小組長找到我求助,不懂也要硬着頭皮上。一開始以為是個小問題,這個產品的開發骨干試了幾種方案過了一周,發現都很難達到客戶的要求,這時客戶和項目經理失去了耐心,開始把問題往高層領導匯報,一旦高層領導知道,后果可想而知。我當時帶着這個產品的小組長以及開發骨干開始做這個問題的攻關,前后總共投入了3周左右的時間(有一半的時間基本上是都是在通宵)。
經過幾天走讀代碼以及實際測試驗證,我發現原來的流控實現方案存在嚴重的缺陷,原來的方案是存儲過程實現上面的流控的流量的分配動作,這個存儲過程每1秒運行一次,每次運行的時候它會把當前需要下發的任務根據總流控、CP以及SP等計算一遍,然后把需要下發的數據加載到Oracle的分區表中,把要發送的速率插入到任務表中,然后集群是每個任務下發的線程從任務表中讀到任務,然后再從分區表中加載上任務要發送的數據,再周而復始的存儲過程不斷計算分配任務、集群每個節點加載任務數據發送數據。
這個產品的最初的開發在2004~2005年左右,最初的設計人員找不到了,但是我猜想為什么用數據庫來解決,是因為如何控制每秒發送的消息在集群下並不好實現,而數據庫存放集中式數據是最簡單的實現方式。這種方案對於大部分運營商對流控的准確性要求沒有那么高時,其實並不是太大的問題,只要系統的負載不是過重,消息能基本准確的發送就可以了。
回到客戶發現的2個問題,可以大致感性的分析原因:
1、因為任務的分配是每秒重新計算一次,計算完成以后,下發線程要再從分區表中加載數據,這都需要時間,即數據不是立即下發,會導致之前分配的速率執行時間被拉長了,所以很難達到總容量
2、流控的不准確性:單個線程發送的流控算法優化問題,我會在后面再討論這個問題
針對第一個問題,我們最后設計了一個流控中心的應用,拋棄了原來通過數據庫進行任務的分配的邏輯,其它所有發送消息的應用通過Netty連接到流控中心,當前任務以什么速率發送完全以流控中心的指標為准,而流控中心它的計算邏輯全在內存中實現。流控中心和消息發送應用之間雙向通信,當下發的速率要調整的時候,流控中心可以把速率主動的下發給消息中心。當時因為時間緊,我們並沒有用專門的機器來部署流控中心,而是把所有的發送消息的主機在啟動的時候把自己的IP地址寫到數據庫的一張表中,然后在這張表中最先插入數據的那台機器就兼容來流控,順便發送消息。因為流控並不是很耗性能,所以即使發送消息對性能影響不大。如果負責流控的機器掛了的話,再由心跳機制把那台掛了的機器刪除掉,這時其它發送消息的節點再重新連接到新的負責流控的主機上。
看到這,大家可能覺得這招很土,其實當時我想過用JGroup來做集群的管理,但是如果你在生產環境用過JGroup的話,就會發現這玩意太復雜了,我當時在很多項目都被坑過。那時也沒有見過其它什么更好的集群通信的開源組件了,加上項目時間緊,我們按更加穩妥的方案實現。
而針對第二個問題,難度相對要小一些。原來的方案采用了一個定時器,這個定時器每秒運行一次,每次運行的時候把一個Semaphore置成需要發送的TPS,每個發送線程在發送之前accquire,如果能取到就發送,取不到就阻塞,然后再下一個1秒的時候再把這個信號量Release到發送的TPS。這么實現會導致發送的在1秒內不平均,比如說:我要一秒發送30條消息,有可能在1/3秒的時候就把消息都發完了,然后在剩下的2/3秒什么也沒發,這樣如果在下游采樣統計TPS的周期不是按1秒來計算,而是按1/6計算的時候,明顯發送速率就不穩定。
改進以后的流控算法是參考了一個兄弟產品的方案,稱之為滑動窗口流控算法,因為畫圖比較花時間,我簡單描述一下。這個算法將1秒分成10個小窗口,還是以上面每秒發送30條為例,在第一個小窗口時,我需要發送3條消息,到第2個小窗口時我只需要累計發送2*3=6條消息,到第3個小窗口時,我只需要累計發送3*3=9條消息,這個算法的好處是發送的消息速率更加平滑,將下游發送的速率的波動給抹平掉。這個算法還有一個好處,不像上面一個算法要有定時器不斷的清零,而只需要很簡單的獲取當前系統時間-系統啟動時間,就可以算出來當前處在哪個發送窗口,系統的開銷不僅小而且更准確。
經過上面的2個優化以后,達到了預期的目標,后面這個產品的基線版本的流控算法又重新進行了設計,后面的隨筆再說后來怎么優化的。1年以后的基線版本優化以后,這個產品的小組長離開了公司,這個產品的開發骨干去了海外常駐,也沒有了聯系。但是寫到這,就想到3個人像個落魄鬼一樣在公司不分晝夜的討論方案、改代碼、測試…