歡迎光臨
每天分享高質量文章

Nginx動態路由的新姿勢:使用Go取代lua

[Envoy] -> [Nginx] -(2)–> [Service endpoint]”],[20,”\n”,”36:0″],[20,” \\”],[20,”\n”,”36:0″],[20,” (1) \\ (redis proto)”],[20,”\n”,”36:0″],[20,” \\”],[20,”\n”,”36:0″],[20,” -> [Go router]”],[20,”\n”,”36:0″],[20,”\n\n這個呼叫從公網進入, 觸發一個Envoy 節點, 然後到一個Nginx節點.Nginx 節點(1) 詢問路由器將請求送至何處。 然後Nginx節點(2)將請求送至指定的服務端點。 \n\n\n”],[20,”\n”,”text-indent:\”1\””],[20,”實現”],[20,”\n”,”32:1″],[20,”我們在Go中建立了一個庫來管理由Sidecar或Hashicorp的Memberlist庫支援的一致性雜湊。我們稱之為Ringman庫。然後,我們將該庫強制接入Redeo庫支援的Redis協議請求的服務中。\n \n這種方案只需要兩個Redis命令:GET和SELECT。我們選擇實現一些用於除錯的的命令,其中包括INFO,可以用您想要的任何伺服器狀態進行回覆。在兩個必需的命令中,我們可以放心地忽略SELECT,這是用由於選擇Redis DB以用於任何後續呼叫。我們只接受它,什麼也不做。GET讓所有的工作都很容易實現。以下是透過Redis和Redeo為Ringman端點提供服務的完整功能。 Nginx會傳遞它接收到的URL,然後從雜湊環中傳回端點。\n \n\n\n\nsrv.HandleFunc(\”get\”, func(out *redeo.Responder, req *redeo.Request) error {“],[20,”\n”,”36:0″],[20,”\tif len(req.Args) != 1 {“],[20,”\n”,”36:0″],[20,”\t\treturn req.WrongNumberOfArgs()”],[20,”\n”,”36:0″],[20,”\t}”],[20,”\n”,”36:0″],[20,”\tnode, err := ringman.GetNode(req.Args[0])”],[20,”\n”,”36:0″],[20,”\tif err != nil {“],[20,”\n”,”36:0″],[20,”\t\tlog.Errorf(\”Error fetching key ‘%s’: %s\”, req.Args[0], err)”],[20,”\n”,”36:0″],[20,”\t\treturn err”],[20,”\n”,”36:0″],[20,”\t}”],[20,”\n\n”,”36:0″],[20,”\tout.WriteString(node)”],[20,”\n”,”36:0″],[20,”\treturn nil”],[20,”\n”,”36:0″],[20,”})”],[20,”\n”,”36:0″],[20,”\n這是Nginx使用以下配置呼叫:\n\n# NGiNX configuration for Go router proxy.”],[20,”\n”,”36:0″],[20,”# Relies on the ngx_http_redis, nginx-eval modules,”],[20,”\n”,”36:0″],[20,”# and http_stub_status modules.”],[20,”\n\n”,”36:0″],[20,”error_log /dev/stderr;”],[20,”\n”,”36:0″],[20,”pid /tmp/nginx.pid;”],[20,”\n”,”36:0″],[20,”daemon off;”],[20,”\n\n”,”36:0″],[20,”worker_processes 1;”],[20,”\n\n”,”36:0″],[20,”events {“],[20,”\n”,”36:0″],[20,” worker_connections 1024;”],[20,”\n”,”36:0″],[20,”}”],[20,”\n\n”,”36:0″],[20,”http {“],[20,”\n”,”36:0″],[20,” access_log /dev/stdout;”],[20,”\n\n”,”36:0″],[20,” include mime.types;”],[20,”\n”,”36:0″],[20,” default_type application/octet-stream;”],[20,”\n\n”,”36:0″],[20,” sendfile off;”],[20,”\n”,”36:0″],[20,” keepalive_timeout 65;”],[20,”\n\n”,”36:0″],[20,” upstream redis_servers {“],[20,”\n”,”36:0″],[20,” keepalive 10;”],[20,”\n\n”,”36:0″],[20,” # Local (on-box) instance of our Go router”],[20,”\n”,”36:0″],[20,” server services.nitro.us:10109;”],[20,”\n”,”36:0″],[20,” }”],[20,”\n\n”,”36:0″],[20,” server {“],[20,”\n”,”36:0″],[20,” listen 8010;”],[20,”\n”,”36:0″],[20,” server_name localhost;”],[20,”\n\n”,”36:0″],[20,” resolver 127.0.0.1;”],[20,”\n\n”,”36:0″],[20,” # Grab the filename/path and then rewrite to /proxy. Can’t do the”],[20,”\n”,”36:0″],[20,” # eval in this block because it can’t handle a regex path.”],[20,”\n”,”36:0″],[20,” location ~* /documents/(.*) {“],[20,”\n”,”36:0″],[20,” set $key $1;”],[20,”\n\n”,”36:0″],[20,” rewrite ^ /proxy;”],[20,”\n”,”36:0″],[20,” }”],[20,”\n\n”,”36:0″],[20,” # Take the $key we set, do the Redis lookup and then set”],[20,”\n”,”36:0″],[20,” # $target_host as the return value. Finally, proxy_pass”],[20,”\n”,”36:0″],[20,” # to the URL formed from the pieces.”],[20,”\n”,”36:0″],[20,” location /proxy {“],[20,”\n”,”36:0″],[20,” eval $target_host {“],[20,”\n”,”36:0″],[20,” set $redis_key $key;”],[20,”\n”,”36:0″],[20,” redis_pass redis_servers;”],[20,”\n”,”36:0″],[20,” }”],[20,”\n\n”,”36:0″],[20,” #add_essay-header \”X-Debug-Proxy\” \”$uri — $key — $target_host\”;”],[20,”\n\n”,”36:0″],[20,” proxy_pass \”http://$target_host/documents/$key?$args\”;”],[20,”\n”,”36:0″],[20,” }”],[20,”\n\n”,”36:0″],[20,” # Used to health check the service and to report basic statistics”],[20,”\n”,”36:0″],[20,” # on the current load of the proxy service.”],[20,”\n”,”36:0″],[20,” location ~ ^/(status|health)$ {“],[20,”\n”,”36:0″],[20,” stub_status on;”],[20,”\n”,”36:0″],[20,” access_log off;”],[20,”\n”,”36:0″],[20,” allow 10.0.0.0/8; # Allow anyone on private network”],[20,”\n”,”36:0″],[20,” allow 172.16.0.0/12; # Allow anyone on Docker bridge network”],[20,”\n”,”36:0″],[20,” allow 127.0.0.0/8; # Allow localhost”],[20,”\n”,”36:0″],[20,” deny all;”],[20,”\n”,”36:0″],[20,” }”],[20,”\n\n”,”36:0″],[20,” error_page 500 502 503 504 /50x.html;”],[20,”\n”,”36:0″],[20,” location = /50x.html {“],[20,”\n”,”36:0″],[20,” root html;”],[20,”\n”,”36:0″],[20,” }”],[20,”\n”,”36:0″],[20,” }”],[20,”\n”,”36:0″],[20,”}”],[20,”\n”,”36:0″],[20,”我們呼叫Nginx和容器裡的路由,讓他們在同樣的host上執行,這樣我們就可以在其中實現較低成本的呼叫。\n \n以下是我們建立的Nginx:\n./configure –add-module=plugins/nginx-eval-module \\”],[20,”\n”,”36:0″],[20,”     –add-module=plugins/ngx_http_redis \\”],[20,”\n”,”36:0″],[20,”     –with-cpu-opt=generic \\”],[20,”\n”,”36:0″],[20,”     –with-http_stub_status_module \\”],[20,”\n”,”36:0″],[20,”     –with-cc-opt=\”-static -static-libgcc\” \\”],[20,”\n”,”36:0″],[20,”     –with-ld-opt=\”-static\” \\”],[20,”\n”,”36:0″],[20,”     –with-cpu-opt=generic”],[20,”\n”,”36:0″],[20,” “],[20,”\n”,”36:0″],[20,”make -j8″],[20,”\n”,”36:0″],[20,”\n”],[20,”\n”,”text-indent:\”1\””],[20,”效能”],[20,”\n”,”32:1″],[20,”我們在自有環境中進行了細緻的效能測試, 我們看到,透過Redis協議從Nginx到Go路由器的平均響應時間大約為0.2-0.3ms。由於來自上游服務的響應時間的中值大約為70毫秒,所以這是可以忽略的延遲。\n一個更複雜的Nginx配置大概能夠做更複雜的錯誤處理。服務一年後的可靠性非常好,效能一直很穩定。\n\n\n結束語”],[20,”\n”,”32:1″],[20,”\n如果您有類似需求,則可以復用大部分元件。只需按照上面的連結到實際的原始碼。如果您有興趣直接向Ringman新增對K8或Mesos的支援,我們會非常歡迎。\n \n這個解決方案聽起來有點駭客,不過它最終成為我們基礎設施的重要補充。希望它能幫助別人解決類似的問題。”]]” style=”font-family: -webkit-standard;”/>

導語: 在Nitro 中, 我們需要一款專業的負載均衡器。 經過一番研究之後,Mihai Todor和我使用Go構建了基於Nginx、Redis 協議的路由器解決方案,其中nginx負責所有繁重工作,路由器本身並不承載流量。 這個解決方案過去一年在生產環境中執行順暢。 以下是我們所做的工作以及我們為什麼那樣做。 

為什麼

 

我們正在構建的新服務將位於負載均衡池之後,負責執行代價很高的計算任務,正因如此,我們需要做本地快取。 為了快取最佳化, 我們想嘗試將相同資源的請求傳送到同一主機上(如果這臺主機是可用的)。

    

解決這個問題有很多現有方案,以下是一個不完全的清單串列:

  • 利用cookie維護黏性session

  • 利用Header 

  • 基於源IP的黏性 

  • HTTP重定向到正確實體

 

這個服務在每個頁面載入時將會被觸發多次, 因此出於效能的考慮, HTTP重定向方式並不可行。 如果所有的入站請求都透過同樣的負載均衡器,那麼剩下的幾種解決方案都可以正常工作。 另一方面, 如果你的前端是一個負載均衡器池, 你需要能夠在它們之間共享狀態或實現複雜的路由邏輯。 我們對當前需要在負載均衡器之間共享狀態變更的設計並沒有興趣,因此我們為這個服務選擇了更複雜的路由邏輯。 

 

我們的架構


瞭解一下我們的設計架構也許能夠幫你更好的理解我們的意圖。 

 

我們擁有一組前端負載均衡器,這些服務的實體被部署在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的支援,我們會非常歡迎。

 

這個解決方案聽起來有點駭客,不過它最終成為我們基礎設施的重要補充。希望它能幫助別人解決類似的問題。

本文作者Karl Mathias由王賀翻譯。轉載譯文請註明出處,技術原創及架構實踐文章,歡迎透過公眾號選單「聯絡我們」進行投稿。

相關閱讀:


Service Mesh利器:NGINX將支援gRPC

快報 | Nginx在Web伺服器市場份額達到33.3%,而Apache則低於50%


活動預告:

6 月 1 ~ 2 日,GIAC 全球網際網路架構大會將於深圳舉行。GIAC 是高可用架構技術社群推出的面向架構師、技術負責人及高階技術從業人員的技術架構大會。今年的 GIAC 已經有騰訊、阿裡巴巴、百度、今日頭條、科大訊飛、新浪微博、小米、美圖、Oracle、鏈家、唯品會、京東、餓了麼、美團點評、羅輯思維、ofo、曠視LinkedInPivotal等公司專家出席。


本期 GIAC 大會上,部分精彩的議題如下:

參加 GIAC,盤點2018最新技術。點選“閱讀原文”瞭解大會更多詳情

贊(0)

分享創造快樂