基於 OpenResty 的動態服務路由方案

2019 年 5 月 11 日,OpenResty 社區聯合又拍雲,舉辦 OpenResty × Open Talk 全國巡迴沙龍武漢站,又拍雲首席佈道師在活動上作了《 基於 OpenResty 的動態服務路由方案 》的分享。html

OpenResty x Open Talk 全國巡迴沙龍是由 OpenResty 社區、又拍雲發起,邀請業內資深的 OpenResty 技術專家,分享 OpenResty 實戰經驗,增進 OpenResty 使用者的交流與學習,推進 OpenResty 開源項目的發展。活動已前後在深圳、北京、武漢舉辦,後續還將陸續在上海、廣州、杭州等城市巡迴舉辦。node

邵海楊,又拍雲首席佈道師,運維總監,資深系統運維架構師,多年 CDN 行業架構設計、運維開發、團隊管理相關經驗,精通 Linux 系統及嵌入式系統,互聯網高性能架構設計、CDN 加速、KVM 虛擬化及 OpenStack 雲平臺的研究,目前專一於容器及虛擬化技術在又拍雲的私有云實踐。git

 

如下是分享全文:github

 

今天和你們介紹一個基於 ngx_lua 的動態服務路由解決方案,它是整個容器化過程當中的組件,容器化在服務路由上有很大的挑戰,又拍雲經過本身的方案來實現了,而且已經穩定運行了三年左右。目前這個方案已經開源,若是你們後續也碰到同樣的問題,能夠直接使用這個方案。算法

服務 zero down-time 更新

在更新服務時,如何能作到讓服務不斷掉呢?又拍雲作服務更新的時候,是不容許有失敗的,若是由於咱們的更新失敗致使請求失敗,即便請求很是少,口碑上也會很差,並且若是形成了事故,是要賠錢的。這也是咱們作動態服務路由的重要緣由。數據庫

服務路由主要包括如下幾個部分:後端

  • 服務註冊是指服務提供者在起來時,去服務發現註冊,以代表提它提供的服務、端口、IP是多少,服務名是什麼等;
  • 服務發現是集中管理服務的地方,記錄了有哪些服務,它們在哪些地方;
  • 負載均衡,因爲有不少一樣的容器提供了一樣的服務,須要考慮怎麼在這些容器裏作負載均衡。

服務發現有不少方案,可是它們的應用場景和語言都不太同樣。Zookeeper 是一個比較老牌的開源項目,相對比較成熟,但對資源的要求比較高,是咱們最先使用的一個方案,包括咱們如今的 kafka、消息隊列都是依賴 Zookeeper;etcd 和 Consul 是後起之秀,K8S 是依賴 etcd 的,etcd 在容器編排裏面是依賴的;又拍雲在服務註冊和發現環節用了 Consul ,它是一站式的技術站,部署、可視化、維護等環節都比較方便,它不但支持 KV 存儲,還有原生的服務監控、多數據中心、DNS 功能等。緩存

負載均衡也有不少方案, LVS 有一個優點是在作完前面兩層後,若是性能很差能夠再加一個 LVS,由於它在四層,更加底層,不會破壞原來的網絡結構,可是它的擴展很是難。HA_PROXY 和 Nginx 各有千秋,HA_PROXY 對 HTTP 頭部解析消耗的 CPU 更少,若是作純轉發,如 WAF 可使用 HA_PROXY,HA_PROXY 大概佔 CPU 10% 左右 ,而 Nginx 作純頭部轉發基本上是佔 CPU 20%-25%,可是 Nginx 可擴展性更強,Nginx 能夠作 TCP、UDP、HTTP 三種協議的轉發和負載均衡,可是 HA_PROXY 只支持 TCP、HTTP。 HA_PROXY 最大的變化是它已經用 lua 重構,後續的發展也會與 lua 緊密結合,這至關因而又多了一種能力,它們也在擁抱 K8S 的生態圈。咱們的方案是選擇了 Nginx ,由於它專一於作 HTTP ,擴展性好,支持 TCP。網絡

如上圖,咱們把 Nginx 和 Consul 放在一張圖裏。爲了突出服務,這裏把一些跟服務不太相關的都省略掉了。咱們基於 Mesos、Docker、 Marathon 作了服務管理。其中有一個特殊的服務是 Registrator,它會經過 Docker API 在每一個物理機上起一個容器,經過 Docker API,把容器的狀態定時的彙報給 Consul。上面的 Nginx 作負載均衡,由於咱們的服務目前都是基於 Nginx 直接到容器裏面。架構

Consul 裏的服務如何更新到 Nginx

在前面的圖裏,Nginx 到容器、服務註冊到配置文件都沒有問題,可是從 Consul 到 Nginx 會出現問題,由於 Consul 有全部的信息,可是這些信息如何通知給 Nginx 呢?一個新的服務起來,或者是一個服務掛掉,這些信息 Consul 知道後怎麼讓 Nginx 把這些有問題的服務刪掉,再把一些新寫的服務加進去,這就是咱們要解決的問題。

這裏的問題就是 Consul 裏的服務如何更新到 Nginx,若是解決了這個問題,Nginx +Consul+Registrator 的模式就圓滿了。目前也有不少方案能夠來解決這個問題:

一、方案一:Consul_template

監聽 Consul 裏的 key,觸發執行一個腳本,利用這個特性的服務,服務發生變更,會根據預先配置好的模板從新生成配置,這個就是最後要執行的一個腳本。

上圖是一個例子,有模板生成 upstream.conf,中間都是未來要被渲染的一些變量,若是 K/v 發生變更,模板化生成一份真實的配置文件,而後再執行一個本地的命令,Nginx -s reload,從新生成配置文件,Reload 一下,這樣新的服務就生效了。

固然 Reload 也會有一些缺點:

  • 第一,若是頻繁 Reload 會有性能損耗;
  • 第二,舊進程長時間處於 shutting down 狀態,若是鏈接裏有長鏈接,舊的進程會一直處於中間進程,這個時間是不定的,你不知道到底何時Reload真正完成;
  • 第三,進程內緩存失效,咱們會把數據庫的一些信息,一些代碼所有緩存進本地,這樣緩存就所有失效了;
  • 最重要的一點是與設計初衷不符,它設計的初衷是方便運維不影響當前的請求,就至關於拿 Docker 作虛擬機用同樣走歪了,走歪了以後極可能會碰到不少奇怪的坑,因此當時沒有用這個方案。

二、方案二:內部 NDS 方案

DNS 的方案也是比較經常使用的,好比把以前是一個 IP 地址的 Server,如今改爲一個域名,只要把它解析掉一批 IP 就行了,這個聽起來已經很完美了,並且 Consul 自己支持DNS,咱們也不用維護另外的 DNS 了,只要把這個 ID 換成域名就行了。

可是咱們感受使用 DNS 方案還不如作 Reload,緣由是

  • 第一,多了一層 DNS 解析時間,增長了額外的處理時間;
  • 第二,DNS 緩存,這是最主要的緣由,由於緩存的存在沒辦法當即把一臺有問題的機器切掉,若是須要緩解這個問題,就要把緩存設得短一點,但這樣解析次數就多了。
  • 第三,端口號會改變,物理機通常會配置同一個端口,在 Docker 裏也能夠這麼作,但對於一些對網絡不是很敏感的應用,好比一些強 CPU 的應用,咱們會直接把容器的網絡用橋接的方式鏈接起來,而這時候端口是隨機分配的,可能每一個容器分配的都不同,因此不可行。

咱們想要的是經過 HTTP 接口,動態修改 Nginx 的上游服務列表,咱們找到了現成的方案,叫 ngx_http_dyups_module。

三、方案三:ngx_http_dyups_module

ngx_http_dyups_module 能夠經過 GET 接口查詢當前的一些信息;POST 能夠更新上游;也能經過 Delete 刪除上游。

上圖是一個例子,這個例子有三個請求:

  • 第一個,給 8080 這個服務端口發了請求以後,發現後面根本就沒有任何的上游服務,因此它就 502 了;
  • 第二個,經過一個 Curl 的請求把兩個服務地址給加進來;
  • 第三個,從新訪問,第三條指令跟第一條指令是如出一轍,由於第二條已經把服務加進來了,因此這是一個正常的輸出。

在這個過程裏沒有任何 Reload 的操做,也沒有改配置,它就完成了一個功能。

這個模塊寫得很是好,可是咱們用了一段時間後把它下掉了,主要緣由不是由於它很差,而是咱們結合了一些自身的狀況,發現了一些問題:

  • 第一,致使依賴 Nginx 自己的負載均衡算法。若是咱們內部用 Ngx_lua 寫得比較多,用了這個模塊以後,會致使咱們很是依賴 C 模塊,也就是自身的一些負載均衡算法,咱們有本身特有的需求,好比「本機優先」,優先訪問本機的服務,這樣聽起來比較奇怪的負載均衡,若是要作這些事情,咱們就要改 C 代碼;
  • 第二,二次開發效率低,C 的開發效率遠不及 Lua;
  • 第三,純 lua 的方案沒法使用,咱們作這樣一個方案並非有一個項目能用就好了,而最好是其餘項目均可以用。


動態負載均衡 Slardar 特性

基於以上這些緣由,咱們開始造本身的輪子。

這個輪子有四個部分:

  • 第一個部分,是最基礎的 Nginx,咱們但願用一些原生的指令和重試的策略;
  • 第二部分,是 lua 的模塊;
  • 第三部分,是 lua_resty_checkups,這是咱們 lua 版的管理模塊,實現了動態的upstream 管理,這個模塊實現了大概 30% 的功能,並且還有一些主動的健康檢查功能,它的代碼量大概是 1500 行左右,若是是 C 模塊估計至少有 1 萬行;
  • 第四部分,是 luasocket,千萬不能在 Nginx 在處理請求的時候用。

一、lua-resty-checkups

簡單介紹下 lua_resty_checkups 這個模板,它有幾個功能:

  • 第一,是動態 upstream 管理,基於共享內存實現 worker 間同步;
  • 第二,是被動健康檢查,這個是 Nginx 自身的一個特性;
  • 第三,是主動健康檢查,這個模塊會主動給後端發心跳包,能夠定時,15 秒發一次,檢查後端的服務是否是存活。咱們還能夠有一些個性化的檢查,好比 heratbeat 定時給上游發送心跳包檢測服務是否存活;
  • 第四,是負載均衡算法,本地優先可節約內網流量等。

二、服務區分

以 Host 區分服務:好比上圖兩個 curl 往同一個地址去發,這二者之間是不同的。

三、請求流程

簡單介紹下請求的流程,它能夠分爲三個部分,最上面是接收請求,會加載一個 worker 代碼,worker 代碼執行完根據 host 找對應的列表,而後把這個請求代理給服務端。

四、動態 upstream 更新

這個跟 dyups 的 C 模塊同樣,也是經過 HTTP 接口來動態更新 upstream 列表,加完後能夠在管理頁面看到剛加進去的兩個服務,這裏會有 server 地址、一些健康檢查的消息、狀態變動的時間,以及它失敗的次數,下圖是一次主動健康檢查的一個記錄。

爲何會有主動健康檢查呢?你們平時用的就是一些被動的健康檢查,也就是請求發出去以後失敗了才知道失敗了,主動的檢查是發心跳包,在請求以前就能夠知道服務是否是出問題了。

五、動態 lua 加載

動態 lua 加載在作遊戲的時候會常常用到。一開始程序裏面跑了一些 lua 的代碼,給後端的程序作參數轉化和作兼容,好比有一個小調整不樂意去改,就拿前面的路由去作,首先能夠對請求作改寫,由於我能夠拿到整個請求,它的請求體能夠作任意的事情。

此外,咱們還能夠跟一些權限控制結合,作一些簡單的參數檢查。據咱們的統計,咱們至少有 10% 是重複請求,若是這些重複請求都去執行就是無謂的消耗,咱們會返 304,表示結果跟以前的同樣,能夠直接用以前的結果。在返 304 的同時,若是咱們須要後端的服務去判斷,會把整個請求收下來,而後再日後面發,至關於內網帶寬要增長一些,這樣其實已經節省了帶寬,能夠不日後面發了。

這是一個動態負載加載的例子,若是把這段代碼推到 Slardar 裏面,它會執行,若是進行一個刪除操做,它會返 403,便可以當即經過這個代碼禁掉這個操做,那還有什麼功能呢?你能夠想象到的功能均可以作,並且這個過程是動態的,若是代碼加載,也能夠從狀態頁裏看到它的信息。

動態負載均衡 Slardar 實現

前面介紹都是 Slardar 的特性,接下來簡單介紹一下實現過程,一共分爲三個部分: 動態 upstream 管理、負載均衡和動態 lua 代碼加載。

一、動態 upstream 管理

啓動時經過 luasocket 從 consul 加載配置文件,服務若是沒有任何理由的掛了,掛了以後你剛起來時,你怎麼知道剛剛怎麼了呢?因此得有一個方式去固化這些東西,而咱們選的是 consul,因此它啓動的時候必須從 consul 加載,啓動以後要監聽管理的端口,接收 upstream 更新指令,還要啓動一個定時器,這個定時器作 worker 間的同步,定時從共享內存看一下有沒有更新,有更新就能夠同步在本身的 worker 裏。

這是一個簡單的流程圖,最開始的時候從 consul 加載,在完成 fork 後到了 worker 進程,也就是剛剛初始化加載的那些 worker 都有了,另一部分啓動定時器,一旦有更新就會進入到這個裏面。


二、負載均衡

負載均衡咱們主要用到了 balance_by_lua_*,一個請求過來,經過 upstream 的 C 模塊把這個請求往這裏發,如圖是配置文件,剛剛也有一個相似的,就是在這裏寫了地址。經過 balance_by_lua_* 指令,咱們會把它攔到這個文件裏,就能夠在這個 lua 文件裏用 lua 代碼選一個,這就是自身的一個 checkups 的選擇的過程。

上圖是大概的流程,能夠先看下邊部分,一開始的時候,checkups.select_peer 是咱們的模塊,而後根據這個 host 再到當前的 peer 就跳出去了,這就實現了用 lua 控制。上面部分是要知道它是成功仍是失敗的,若是它失敗了,要對這個狀態進行反饋。

三、動態 lua 加載

這個主要是用到 lua 的三個函數,分別是 loadfile、loadstring 和 setfenv。loadfile 是加載本地的 lua 代碼,loadstring 是從 consul 或 HTTP 請求 body 加載代碼,setfenv 設置代碼的執行環境,經過這三個函數就能夠加載,具體的實踐細節這裏就再也不介紹。

四、動態負載均衡 Slardar 的優點

這就是咱們造的輪子,主要用到 lua-resty-checkups 的模塊和 balance_by_lua_* ,它有如下的優點:

  • 純 lua 實現,不依賴第三方 C 模塊,所以二次開發很是高效,減小維護負擔;
  • 能夠用 Nginx 原生的 proxy_*,由於咱們只在請求的選 peer 的那個階段作,peer 選完以後,發數據的那個階段是直接走 Nginx 本身的指令,因此它能夠用到 Nginx 原生的 proxy_* 指令;
  • 它適用於幾乎任何的 ngx_lua 項目,可同時知足純 lua 方案與 C 方案。

在微服務架構裏,Slardar 能作什麼

咱們目前也在把以前的一些服務改形成微服務模式。微服務其實就是源於一個比較大的服務,把它拆分紅一些小的服務,它的擴容跟遷移也不同,微服務的擴容能夠只擴容其中一部分,擴容多少能夠根據需求。

咱們如今正在嘗試一個方案,這個方案背景是咱們有作圖的需求,作圖這個功能有不少,好比說美化、縮略、水印等,若是要對作圖的服務進行優化是很是困難的,由於它功能太多了,若是咱們把它拆成微服務就不同了,好比上圖虛線上面的是咱們如今的服務,這個是微服務的一個網關,下面是一些小的服務。好比說美化,它的運算比較複雜,耗 CPU 比較多,咱們確定選擇一些 CPU 比較好的機器;用 GPU 來作縮略圖,這個性能可能提升幾十倍;最後是一箇中規中矩的作圖,那就普通的一些就夠了。

還有一些比較偏門的,好比說梯度,可能只要保證服務能夠用就好了,經過這個微服務的路由,咱們根據後面的區分把以前的一個服務,以及它的參數拆成三個小的服務,這樣經過三個步驟能夠完成一個作圖的服務。

固然咱們在嘗試這個方案其實也有不少的問題,好比一個服務原來用一個程序就能夠作了,如今變成了三個,勢必內網的帶寬要增長了,中間的圖片要被導來導去,這個怎麼辦呢?咱們如今想到的辦法就是作一些本地優先的調度策略,即作完以後,本地有一些水印的,那就優先用本地的。

最後套用大師的一句話:Talk is cheap,Show me the code。目前咱們已經將 Sladar 項目開源,項目地址是: 。

 

演講視頻及PPT:

基於 OpenResty 的動態服務路由方案

相關文章
相關標籤/搜索