導語: 在Nitro 中, 咱們須要一款專業的負載均衡器。 通過一番研究以後,Mihai Todor和我使用Go構建了基於Nginx、Redis 協議的路由器解決方案,其中nginx負責全部繁重工做,路由器自己並不承載流量。 這個解決方案過去一年在生產環境中運行順暢。 如下是咱們所作的工做以及咱們爲何那樣作。 前端
爲何nginx
咱們正在構建的新服務將位於負載均衡池以後,負責執行代價很高的計算任務,正因如此,咱們須要作本地緩存。 爲了緩存優化, 咱們想嘗試將相同資源的請求發送到同一主機上(若是這臺主機是可用的)。redis
解決這個問題有不少現有方案,如下是一個不徹底的清單列表:緩存
利用cookie維護黏性session服務器
利用Header cookie
基於源IP的黏性 session
HTTP重定向到正確實例架構
這個服務在每一個頁面加載時將會被觸發屢次, 所以出於性能的考慮, HTTP重定向方式並不可行。 若是全部的入站請求都經過一樣的負載均衡器,那麼剩下的幾種解決方案均可以正常工做。 另外一方面, 若是你的前端是一個負載均衡器池, 你須要可以在它們之間共享狀態或實現複雜的路由邏輯。 咱們對當前須要在負載均衡器之間共享狀態變動的設計並無興趣,所以咱們爲這個服務選擇了更復雜的路由邏輯。 負載均衡
咱們的架構ide
瞭解一下咱們的設計架構也許可以幫你更好的理解咱們的意圖。
咱們擁有一組前端負載均衡器,這些服務的實例被部署在Mesos, 以便根據服務規模和資源可用性進行進出控制。 將主機和端口號列表放入負載均衡器中不是問題,這已經成爲咱們平臺的核心。
由於一切都在Mesos上運行, 而且咱們擁一種簡單的方式定義和部署服務,因此添加任何新服務都很簡單。
在Mesos之上, 咱們在每處都運行着基於gossip的Sidecar來管理服務發現。 咱們的前端負載均衡器是由Lyft的Envoy組成 , 它背後由Sidecar的Envoy集成支持。 這能知足大部分服務的需求。 Envoy主機運行在專用實例上, 但全部的服務都根據須要, 在主機之間遷移,由Mesos和Sigualarity調度器執行。
仍在考慮中的Mesos服務節點將擁有基於磁盤的本地緩存。
設計
看着這個問題咱們下了決定,咱們着實想要一種一致性哈稀環。 咱們可讓節點根據須要控制進出,只有那些節點所服務的請求才會被從新路由。 剩下的全部節點將繼續服務於任何公開的會話。 咱們能夠很簡單地經過Sidecar數據來支持一致性哈稀環 (你能夠用Mesos 或k8s代替) 。 Sidecar健康檢查節點, 咱們能夠靠這些健康檢查節點判斷它們在Sidecar中是否工做正常。
而後,咱們須要某種一致性哈稀方法將流量導入到正確的節點中。它須要接收每個請求, 識別問題資源, 而後將請求傳遞給其餘已經準備處理該資源的服務實例。
固然, 資源識別能夠簡單的經過URL處理,而且任何負載均衡器可以將他們分開來處理簡單的路由。 因此咱們只須要將他們與一致性哈稀關聯起來,對此咱們已經有一種解決方案。
你能夠在nginx用lua那樣作, 也可在HAproxy中用lua 。 在Nitro裏, 咱們沒有一我的是Lua 專家,而且顯然沒有庫可以實現咱們的須要。 理想狀況下, 路由邏輯將在Go中實現, Go在咱們的技術棧中是一門關鍵語言而且獲得了很好的支持。
Nginx有着豐富的生態環境, 跳脫常規的思路還引起了一些頗有趣的nginx插件。 這些插件中首選插件Valery Kholodko的nginx-eval-module。 這個插件容許你從nginx到一個端點生成一個調用,而且將返回的結果評估爲nginx的變量。 在其餘可能的做用中, 這個插件的意義在於它容許您動態地決定哪一個端點應該接收代理傳遞。 這就是咱們想要作的。 你從Ngnix到某個地方生成一個調用, 獲取一個結果後, 你能夠根據返回的結果值生成路由決策。 你可使用HTTP服務實現該請求的接收方。 該服務僅返回目標服務器端點的主機名和端口號的字符串。 這個服務始終保持一致性哈希,而且告知Nginx 每一個請求流量路由的位置 , 可是生成一個單獨的HTTP請求,仍然有些笨重。 整個預期的回覆內容將會是字符串10.10.10.5:23453。 經過HTTP,咱們會在兩個方向傳遞頭部信息,這將大大超出響應正文的大小。
因而我開始研究Nginx支持的其餘協議, 發現memcache協議和redis協議它都支持。其中,對Go服務最友好的支持是Redis協議。因此那就是咱們改進的方向。Nginx 中有兩個Redis模塊,有一個適合經過nginx-eval-module 使用。 實現Redis Go語言最好的庫是Redeo。Rodeo實現了一個極其簡單的處理機制,很是相似於go標準庫中的http包。 任何redis協議命令將會包含一個handler函數,而且它的寫法很是簡單。 相比Nginx插件,它可以處理更新版本的redis協議。 因而, 我摒棄了個人C技能,並補充了Nginx插件以使用最新的Redis協議編碼。
因而, 咱們最新的解決方案是:
這個調用從公網進入, 觸發一個Envoy 節點, 而後到一個Nginx節點.Nginx 節點(1) 詢問路由器將請求送至何處。 而後Nginx節點(2)將請求送至指定的服務端點。
實現
咱們在Go中創建了一個庫來管理由Sidecar或Hashicorp的Memberlist庫支持的一致性哈希。咱們稱之爲Ringman庫。而後,咱們將該庫強制接入Redeo庫支持的Redis協議請求的服務中。
這種方案只須要兩個Redis命令:GET和SELECT。咱們選擇實現一些用於調試的的命令,其中包括INFO,能夠用您想要的任何服務器狀態進行回覆。在兩個必需的命令中,咱們能夠放心地忽略SELECT,這是用因爲選擇Redis DB以用於任何後續調用。咱們只接受它,什麼也不作。GET讓全部的工做都很容易實現。如下是經過Redis和Redeo爲Ringman端點提供服務的完整功能。 Nginx會傳遞它接收到的URL,而後從哈希環中返回端點。
這是Nginx使用如下配置調用:
咱們調用Nginx和容器裏的路由,讓他們在一樣的host上運行,這樣咱們就能夠在其中實現較低成本的調用。
如下是咱們創建的Nginx:
性能
咱們在自有環境中進行了細緻的性能測試, 咱們看到,經過Redis協議從Nginx到Go路由器的平均響應時間大約爲0.2-0.3ms。因爲來自上游服務的響應時間的中值大約爲70毫秒,因此這是能夠忽略的延遲。
一個更復雜的Nginx配置大概可以作更復雜的錯誤處理。服務運行了一年多可靠性很是好,性能一直很穩定。
結束語
若是您有相似需求,則能夠複用大部分組件。只需按照上面的連接到實際的源代碼。若是您有興趣直接向Ringman添加對K8或Mesos的支持,咱們會很是歡迎。
這個解決方案聽起來有點黑客,不過它最終成爲咱們基礎設施的重要補充。但願它能幫助別人解決相似的問題。