基於 OpenResty 的動態服務路由方案
2019 年 5 月 11 日,OpenResty 社群聯合又拍雲,舉辦 OpenResty × Open Talk 全國巡迴沙龍武漢站,又拍雲首席佈道師在活動上做了《 基於 OpenResty 的動態服務路由方案 》的分享。
OpenResty x Open Talk 全國巡迴沙龍是由 OpenResty 社群、又拍雲發起,邀請業內資深的 OpenResty 技術專家,分享 OpenResty 實戰經驗,增進 OpenResty 使用者的交流與學習,推動 OpenResty 開源專案的發展。活動已先後在深圳、北京、武漢舉辦,後續還將陸續在上海、廣州、杭州等城市巡迴舉辦。
邵海楊,又拍雲首席佈道師,運維總監,資深系統運維架構師,多年 CDN 行業架構設計、運維開發、團隊管理相關經驗,精通 Linux 系統及嵌入式系統,網際網路高效能架構設計、CDN 加速、KVM 虛擬化及 OpenStack 雲平臺的研究,目前專注於容器及虛擬化技術在又拍雲的私有云實踐。
以下是分享全文:
今天和大家介紹一個基於 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 的模式就圓滿了。目前也有很多方案可以來解決這個問題:
1、方案一:Consul_template
監聽 Consul 裡的 key,觸發執行一個指令碼,利用這個特性的服務,服務發生變動,會根據預先配置好的模板重新生成配置,這個就是最後要執行的一個指令碼。
上圖是一個例子,有模板生成 upstream.conf,中間都是將來要被渲染的一些變數,如果 K/v 發生變動,模板化生成一份真實的配置檔案,然後再執行一個本地的命令,Nginx -s reload,重新生成配置檔案,Reload 一下,這樣新的服務就生效了。
當然 Reload 也會有一些缺點:
- 第一,如果頻繁 Reload 會有效能損耗;
- 第二,舊程序長時間處於 shutting down 狀態,如果連線裡有長連線,舊的程序會一直處於中間程序,這個時間是不定的,你不知道到底什麼時候Reload真正完成;
- 第三,程序內快取失效,我們會把資料庫的一些資訊,一些程式碼全部快取進本地,這樣快取就全部失效了;
- 最重要的一點是與設計初衷不符,它設計的初衷是方便運維不影響當前的請求,就相當於拿 Docker 做虛擬機器用一樣走歪了,走歪了之後很可能會碰到很多奇怪的坑,所以當時沒有用這個方案。
2、方案二:內部 NDS 方案
DNS 的方案也是比較常用的,比如把之前是一個 IP 地址的 Server,現在改成一個域名,只要把它解析掉一批 IP 就好了,這個聽起來已經很完美了,而且 Consul 本身支援DNS,我們也不用維護另外的 DNS 了,只要把這個 ID 換成域名就好了。
但是我們感覺使用 DNS 方案還不如做 Reload,原因是
- 第一,多了一層 DNS 解析時間,增加了額外的處理時間;
- 第二,DNS 快取,這是最主要的原因,因為快取的存在沒辦法立即把一臺有問題的機器切掉,如果需要緩解這個問題,就要把快取設得短一點,但這樣解析次數就多了。
- 第三,埠號會改變,物理機一般會配置同一個埠,在 Docker 裡也可以這麼做,但對於一些對網路不是很敏感的應用,比如一些強 CPU 的應用,我們會直接把容器的網路用橋接的方式連線起來,而這時候埠是隨機分配的,可能每個容器分配的都不一樣,所以不可行。
我們想要的是通過 HTTP 介面,動態修改 Nginx 的上游服務列表,我們找到了現成的方案,叫 ngx_http_dyups_module。
3、方案三: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 在處理請求的時候用。
1、lua-resty-checkups
簡單介紹下 lua_resty_checkups 這個模板,它有幾個功能:
- 第一,是動態 upstream 管理,基於共享記憶體實現 worker 間同步;
- 第二,是被動健康檢查,這個是 Nginx 自身的一個特性;
- 第三,是主動健康檢查,這個模組會主動給後端發心跳包,可以定時,15 秒發一次,檢查後端的服務是不是存活。我們還可以有一些個性化的檢查,比如 heratbeat 定時給上游傳送心跳包檢測服務是否存活;
- 第四,是負載均衡演算法,本地優先可節約內網流量等。
2、服務區分
以 Host 區分服務:比如上圖兩個 curl 往同一個地址去發,這兩者之間是不一樣的。
3、請求流程
簡單介紹下請求的流程,它可以分為三個部分,最上面是接收請求,會載入一個 worker 程式碼,worker 程式碼執行完根據 host 找對應的列表,然後把這個請求代理給服務端。
4、動態 upstream 更新
這個跟 dyups 的 C 模組一樣,也是通過 HTTP 介面來動態更新 upstream 列表,加完後可以在管理頁面看到剛加進去的兩個服務,這裡會有 server 地址、一些健康檢查的訊息、狀態變更的時間,以及它失敗的次數,下圖是一次主動健康檢查的一個記錄。
為什麼會有主動健康檢查呢?大家平時用的就是一些被動的健康檢查,也就是請求發出去之後失敗了才知道失敗了,主動的檢查是發心跳包,在請求之前就可以知道服務是不是出問題了。
5、動態 lua 載入
動態 lua 載入在做遊戲的時候會經常用到。一開始程式裡面跑了一些 lua 的程式碼,給後端的程式做引數轉化和做相容,比如有一個小調整不樂意去改,就拿前面的路由去做,首先可以對請求做改寫,因為我可以拿到整個請求,它的請求體可以做任意的事情。
此外,我們還可以跟一些許可權控制結合,做一些簡單的引數檢查。據我們的統計,我們至少有 10% 是重複請求,如果這些重複請求都去執行就是無謂的消耗,我們會返 304,表示結果跟之前的一樣,可以直接用之前的結果。在返 304 的同時,如果我們需要後端的服務去判斷,會把整個請求收下來,然後再往後面發,相當於內網頻寬要增加一些,這樣其實已經節省了頻寬,可以不往後面發了。
這是一個動態負載載入的例子,如果把這段程式碼推到 Slardar 裡面,它會執行,如果進行一個刪除操作,它會返 403,即可以立即通過這個程式碼禁掉這個操作,那還有什麼功能呢?你可以想象到的功能都可以做,而且這個過程是動態的,如果程式碼載入,也可以從狀態頁裡看到它的資訊。
動態負載均衡 Slardar 實現
前面介紹都是 Slardar 的特性,接下來簡單介紹一下實現過程,一共分為三個部分: 動態 upstream 管理、負載均衡和動態 lua 程式碼載入。
1、動態 upstream 管理
啟動時通過 luasocket 從 consul 載入配置檔案,服務如果沒有任何理由的掛了,掛了之後你剛起來時,你怎麼知道剛剛怎麼了呢?所以得有一個方式去固化這些東西,而我們選的是 consul,所以它啟動的時候必須從 consul 載入,啟動之後要監聽管理的埠,接收 upstream 更新指令,還要啟動一個定時器,這個定時器做 worker 間的同步,定時從共享記憶體看一下有沒有更新,有更新就可以同步在自己的 worker 裡。
這是一個簡單的流程圖,最開始的時候從 consul 載入,在完成 fork 後到了 worker 程序,也就是剛剛初始化載入的那些 worker 都有了,另外一部分啟動定時器,一旦有更新就會進入到這個裡面。
2、負載均衡
負載均衡我們主要用到了 balance_by_lua_*,一個請求過來,通過 upstream 的 C 模組把這個請求往這裡發,如圖是配置檔案,剛剛也有一個類似的,就是在這裡寫了地址。通過 balance_by_lua_* 指令,我們會把它攔到這個檔案裡,就可以在這個 lua 檔案裡用 lua 程式碼選一個,這就是自身的一個 checkups 的選擇的過程。
上圖是大概的流程,可以先看下邊部分,一開始的時候,checkups.select_peer 是我們的模組,然後根據這個 host 再到當前的 peer 就跳出去了,這就實現了用 lua 控制。上面部分是要知道它是成功還是失敗的,如果它失敗了,要對這個狀態進行反饋。
3、動態 lua 載入
這個主要是用到 lua 的三個函式,分別是 loadfile、loadstring 和 setfenv。loadfile 是載入本地的 lua 程式碼,loadstring 是從 consul 或 HTTP 請求 body 載入程式碼,setfenv 設定程式碼的執行環境,通過這三個函式就可以載入,具體的實踐細節這裡就不再介紹。
4、動態負載均衡 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 專案開源,專案地址是:https://github.com/upyun/slardar 。
演講視訊及PPT:
基於 OpenResty 的動態服務路由