肖丁,基礎架構研發部工程師,負責 UCloud 公共組件、監控、消息服務等系統,專一於高可用高性能框架、中間件應用。2014年加入 UCloud,前後參與 UCloud 監控平臺改造、內部 DNS 集羣化、DB 服務集羣化、golang 網絡通訊框架、通用 agent 等項目。golang
消息隊列是企業架構演進過程當中不可或缺的部分,其天生的靈活性、可擴展性、消息處理能力能夠方便地幫助企業服務解耦,處理服務峯值壓力。但構建與維護消息隊列是很耗費時間與人力的事情。消息隊列集羣在使用和維護過程當中,常常會遇到主節點性能瓶頸、容量規劃、網絡分區等問題,處理不當則可能致使消息丟失,進而影響服務。算法
一般,咱們將消息功能分爲兩大類:主動消費和被動消費。後端
圖中是兩個被動消費的場景,即消費者首先要和消息隊列產生一個長鏈接。當推送者向消息隊列推送消息的時候,第一種場景是指要輪詢推送給消費者,這是最多見的輪詢負載均衡的方式;第二種是以廣播的形式,如一個消息副本須要傳遞給多個子模塊進行消費。若是沒有消息隊列的應用有些消息從 A 模塊傳遞到 B 模塊在傳遞到 C 模塊,實現一個總體的鏈入的處理。緩存
緊耦合系統:服務 A 依賴服務B的穩定性,當服務 B 異常或者平常升級時,服務 A 的請求將被丟棄。服務器
鬆耦合系統:接入 UMQ 後,服務 A 的消息由 UMQ 緩存,當服務 B 異常時,服務 A 的消息不會丟失,待服務 B 恢復以後再作處理。網絡
無 UMQ 場景下,當服務請求量增長時,服務器壓力與請求量爲線性關係,且一般的作法是作服務的擴容,形成資源的浪費。架構
接入 UMQ 場景後,服務請求量的大幅波動不會影響服務的負載能力,幫助服務平滑處理全部請求。如,當大量消息同時涌進消息隊列時,可能出現後臺服務沒法及時處理全部消息,致使回覆等狀況,這時後端子系統會根據根據用戶的消費能力和消費水平批量篩選部分消息。併發
無 UMQ 場景下,一次請求須要同步等待後端處理,獲得響應後再執行其餘任務,可能會出現延遲等待的狀況;或者輪詢後端任務結果,形成多餘的請求。負載均衡
接入 UMQ 後實現異步通知,用戶不須要同步等待,也不須要額外的輪詢開銷,經過異步的隊列通知,獲取結果集。它更適合後端研發人員使用,是優化後端系統的一種方式。如圖所示,左邊的同步模式和輪詢模式,它們都須要客戶端發送請求後等待服務端的返回,或客戶端發送請求後要不斷地輪詢,這會對後端系統形成負載壓力。異步通知的方式是基於兩個消息隊列,一個進行消費推送,一個進行 response 回執,這有利於實現後端系統負載的減輕。框架
設計消息隊列之初,咱們沒有考慮要本身」造輪子」,而是使用了一些現有的開源方案,如,RabbitMQ。但在搭建過程當中遇到很多問題:總體的集羣須要經過一套自有的共識算法來選出一個 master 節點作全部消息的一次性算法。但網絡波動以後,咱們發現整個消息隊列的集羣會出現腦裂的現象,如,不一樣的隊列會被分配到不一樣的節點中,致使節點數據無法融合在一塊兒,這對消息隊列的設計產生了很大的困擾。未解決此類問題,在 RabbitMQ 腦裂的策略中咱們必須捨棄一部分數據,再將它們合併到一個集羣中等。
隨後,咱們也利用了 Golang 的高併發和 Channel 的一些特性來實現分佈式的消息隊列。
在咱們決定造輪子時,咱們會考慮哪一種方式能最快實現消息隊列,而後咱們肯定了兩種模式,單體模式和微服務模式,以下所示,我對它們的優缺點進行了分析:
架構 v1.0 如圖所示:
其中,Proxy 作揭露層的負載均衡,Broker 實現隊列的核心邏輯,Manager 管理語言數據。初版設計咱們只用了運營訪問的方式。由於咱們已經使用了 Broker 的集羣作隊列的管理,因此整個隊列集羣須要有一套自適應的規則來進行管理全部節點。所以,在 Broker 和 Manager 節點內咱們使用了一次性哈希算法,把不一樣的隊列分配到不一樣的隊列節點上處理。
固然,v1.0上也出現了一些問題,如:咱們無從知道隊列會落在那個隊列節點上等。因而,根據這些問題,咱們作了 v2.0 的改進:
這次改進引入了一個 ULB,它能快速實現後端服務的調度,請求的快速分配等。Controller 可人工控制 Proxy 下發隊列信息到相應節點。底層的分佈式存儲也是 Redis 和 MySQL 的服務,這減輕了底層容量負擔。這個架構目前也存在一些問題,如,Controller 是一個單點,當它失效時,若是沒有及時回覆,隊列集羣會不可控。其次,由於咱們使用的是儲備的 Broker,雖然它有本身的資源限定,但仍是會對總體資源形成浪費。
在 v3.0 的時候咱們最終肯定了架構版本,咱們從新用回原來的 Zookeeper 作 Broker 隊列節點的信息上報和映射關係的處理。而 Proxy 和 Manager 是利用 Zookeeper 的 watch 特性獲取隊列關係的變化和隊列節點的心跳變化,它能秒及地改變下發的策略。
在 Manager 中,咱們把全部隊列的控制規則和 Controller 進行合併,這樣,當主 Manager 失效時,子 Manager 一樣能註冊成功。
而在 Bocker 方面,則用回以前的設計思路,即採用單點的方式,這樣,當某個 Broker 失效時,Manager 會在感知到以後會將整個節點剔除,而後從新分配一個節點。
由於整個內容是用 Golang 製做,因此 Broker 相應的具有了 Golang 的一些特性,如圖所示:
舉例說明:上層模塊(Proxy)將全部消息請求的透傳至 Brocker,Brocker 在協議層作解析,而後將請求放置業務邏輯中,業務邏輯再選擇對應的方法,交由 go routine 進行處理,隨後生成的新隊列節點判斷是否接受消息,而後經過 go channel 層層傳遞至 Subscriber。最後推送的消息則須要利用 Brocker 和 Proxy 之間的鏈接池,它能最大限度提高性能。此外,當 TCP pool 開始服務時,Proxy 會向 Broker 主動申請產生鏈接。
Proxy 與 Brocker 節點的探測:由於咱們沒法直接從鏈接層感知鏈接是否出現異常,因此須要在 receive 和 send 的過程當中感知,隨後再進行鏈接恢復的處理。
因 Proxy 會主動向 Brocker 發起 TCP 鏈接,因此每一個鏈接都會經過 go routine 執行 receive 操做。當 receive 出現異常,Proxy 能感知並退出 go routine,以後從新創建鏈接放入 go routine。反之,當 Brocker 須要探測 Proxy 異常時,它會先從 TCP 鏈接池選出一項鍊接進行消息推送,若是推送失敗,會反向刪除原鏈接池的鏈接並從新選擇鏈接,這兩個過程相輔相成。
跨機房容災是將不一樣用戶分到不一樣的 set 內來作區分,它只能保證部分 set 內的用戶不受影響。當整個數據中心出現問題時,圖中左邊整條鏈路都將失效。
在 UMQ 的設計和架構的演進過程當中,咱們雖然遇到了許多具備挑戰性的問題,但最終仍是克服了困難,併成功利用 UCloud IaaS 服務搭建了微服務架構,保證了服務的高可用,同時利用 golang 完成了 UMQ 核心模塊的實現,實現了高性能。但願這些經驗對你有幫助。