1. 程式人生 > >通過Nginx、Consul、Upsync實現動態負載均衡和服務平滑釋出

通過Nginx、Consul、Upsync實現動態負載均衡和服務平滑釋出

## 前提 前段時間順利地把整個服務叢集和中介軟體全部從`UCloud`遷移到阿里雲,筆者擔任了架構和半個運維的角色。這裡詳細記錄一下通過`Nginx`、`Consul`、`Upsync`實現動態負載均衡和服務平滑釋出的核心知識點和操作步驟,整個體系已經在生產環境中平穩執行。編寫本文使用的虛擬機器系統為`CentOS7.x`,虛擬機器的內網`IP`為`192.168.56.200`。 ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-1.png) ## 動態負載均衡的基本原理 一般會通過`upstream`配置`Nginx`的反向代理池: ```shell http { upstream upstream_server{ server 127.0.0.1:8081; server 127.0.0.1:8082; } server { listen 80; server_name localhost; location / { proxy_pass http://upstream_server; } } } ``` 現在假如`8081`埠的服務例項掛了需要剔除,那麼需要修改`upstream`為: ```shell upstream upstream_server{ # 新增down標記該埠的服務例項不參與負載 server 127.0.0.1:8081 down; server 127.0.0.1:8082; } ``` 並且通過`nginx -s reload`重新載入配置,該`upstream`配置才會生效。我們知道,服務釋出時候重啟過程中是處於不可用狀態,正確的服務釋出過程應該是: - 把該服務從對應的`upstream`剔除,一般是置為`down`,告知`Nginx`服務`upstream`配置變更,需要通過`nginx -s reload`進行過載。 - 服務構建、部署和重啟。 - 通過探活指令碼感知服務對應的埠能夠訪問,把該服務從對應的`upstream`中拉起,一般是把`down`去掉,告知`Nginx`服務`upstream`配置變更,需要通過`nginx -s reload`進行過載。 上面的步驟一則涉及到`upstream`配置,二則需要`Nginx`重新載入配置(`nginx -s reload`),顯得比較笨重,在高負載的情況下重新啟動`Nginx`並重新載入配置會進一步增加系統的負載並可能暫時降低效能。 所以,可以考慮使用分散式快取把`upstream`配置存放在快取服務中,然後`Nginx`直接從這個快取服務中讀取`upstream`的配置,這樣如果有`upstream`的配置變更就可以直接修改快取服務中對應的屬性,而`Nginx`服務也不需要`reload`。在實戰中,這裡提到的快取服務就選用了`Consul`,`Nginx`讀取快取中的配置屬性選用了新浪微博提供的`Nginx`的`C`語言模組`nginx-upsync-module`。示意圖大致如下: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-2.png) ## Consul安裝和叢集搭建 `Consul`是`Hashicorp`公司的一個使用`Golang`開發的開源專案,它是一個用於服務發現和配置的工具,具備分散式和高度可用特性,並且具有極高的可伸縮性。`Consul`主要提供下面的功能: - 服務發現。 - 執行狀況檢查。 - 服務分塊/服務網格(`Service Segmentation/Service Mesh`)。 - 金鑰/值儲存。 - 多資料中心。 下面是安裝過程: ```shell mkdir /data/consul cd /data/consul wget https://releases.hashicorp.com/consul/1.7.3/consul_1.7.3_linux_amd64.zip # 注意解壓後只有一個consul執行檔案 unzip consul_1.7.3_linux_amd64.zip ``` 解壓完成後,使用命令`nohup /data/consul/consul agent -server -data-dir=/tmp/consul -bootstrap -ui -advertise=192.168.56.200 -client=192.168.56.200 > /dev/null 2>&1 &`即可後臺啟動單機的`Consul`服務。啟動`Consul`例項後,訪問`http://192.168.56.200:8500/`即可開啟其後臺管理`UI`: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-3.png) 下面基於單臺虛擬機器搭建一個偽叢集,**關於叢集的一些配置屬性的含義和命令引數的解釋暫時不進行展開**。 ```shell # 建立叢集資料目錄 mkdir /data/consul/node1 /data/consul/node2 /data/consul/node3 # 建立叢集日誌目錄 mkdir /data/consul/node1/logs /data/consul/node2/logs /data/consul/node3/logs ``` 在`/data/consul/node1`目錄新增`consul_conf.json`檔案,內容如下: ```json { "datacenter": "es8-dc", "data_dir": "/data/consul/node1", "log_file": "/data/consul/node1/consul.log", "log_level": "INFO", "server": true, "node_name": "node1", "ui": true, "bind_addr": "192.168.56.200", "client_addr": "192.168.56.200", "advertise_addr": "192.168.56.200", "bootstrap_expect": 3, "ports":{ "http": 8510, "dns": 8610, "server": 8310, "serf_lan": 8311, "serf_wan": 8312 } } ``` 在`/data/consul/node2`目錄新增`consul_conf.json`檔案,內容如下: ```json { "datacenter": "es8-dc", "data_dir": "/data/consul/node2", "log_file": "/data/consul/node2/consul.log", "log_level": "INFO", "server": true, "node_name": "node2", "ui": true, "bind_addr": "192.168.56.200", "client_addr": "192.168.56.200", "advertise_addr": "192.168.56.200", "bootstrap_expect": 3, "ports":{ "http": 8520, "dns": 8620, "server": 8320, "serf_lan": 8321, "serf_wan": 8322 } } ``` 在`/data/consul/node3`目錄新增`consul_conf.json`檔案,內容如下: ```json { "datacenter": "es8-dc", "data_dir": "/data/consul/node3", "log_file": "/data/consul/node3/consul.log", "log_level": "INFO", "server": true, "node_name": "node3", "ui": true, "bind_addr": "192.168.56.200", "client_addr": "192.168.56.200", "advertise_addr": "192.168.56.200", "bootstrap_expect": 3, "ports":{ "http": 8530, "dns": 8630, "server": 8330, "serf_lan": 8331, "serf_wan": 8332 } } ``` 新建一個叢集啟動指令碼: ```shell cd /data/consul touch service.sh # /data/consul/service.sh內容如下: nohup /data/consul/consul agent -config-file=/data/consul/node1/consul_conf.json > /dev/null 2>&1 & sleep 10 nohup /data/consul/consul agent -config-file=/data/consul/node2/consul_conf.json -retry-join=192.168.56.200:8311 > /dev/null 2>&1 & sleep 10 nohup /data/consul/consul agent -config-file=/data/consul/node3/consul_conf.json -retry-join=192.168.56.200:8311 > /dev/null 2>&1 & ``` 如果叢集啟動成功,觀察節點1中的日誌如下: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-4.png) 通過節點1的`HTTP`端點訪問後臺管理頁面如下(可見當前的節點1被標記了一顆紅色的星星,說明當前節點1是`Leader`節點): ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-5.png) 至此,`Consul`單機偽叢集搭建完成(其實分散式叢集的搭建大同小異,注意叢集節點所在的機器需要開放使用到的埠的訪問許可權),由於`Consul`使用`Raft`作為共識演算法,該演算法是**強領導者模型,也就是隻有`Leader`節點可以進行寫操作**,因此接下來的操作都需要使用節點1的`HTTP`端點,就是`192.168.56.200:8510`。 > 重點筆記:如果`Consul`叢集重啟或者重新選舉,`Leader`節點有可能發生更變,外部使用的時候建議把`Leader`節點的`HTTP`端點抽離到可動態更新的配置項中或者動態獲取`Leader`節點的`IP`和埠。 ## Nginx編譯安裝 直接從官網下載二級制的安裝包並且解壓: ```shell mkdir /data/nginx cd /data/nginx wget http://nginx.org/download/nginx-1.18.0.tar.gz tar -zxvf nginx-1.18.0.tar.gz ``` 解壓後的所有原始檔在`/data/nginx/nginx-1.18.0`目錄下,編譯之前需要安裝`pcre-devel`、`zlib-devel`依賴: ```shell yum -y install pcre-devel yum install -y zlib-devel ``` 編譯命令如下: ```shell cd /data/nginx/nginx-1.18.0 ./configure --prefix=/data/nginx ``` 如果`./configure`執行過程不出現問題,那麼結果如下: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-6.png) 接著執行`make`: ```shell cd /data/nginx/nginx-1.18.0 make ``` 如果`make`執行過程不出現問題,那麼結果如下: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-7.png) 最後,如果是首次安裝,可以執行`make install`進行安裝(實際上只是拷貝編譯好的檔案到`--prefix`指定的路徑下): ```shell cd /data/nginx/nginx-1.18.0 make install ``` ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-8.png) `make install`執行完畢後,`/data/nginx`目錄下新增了數個資料夾: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-9.png) 其中,`Nginx`啟動程式在`sbin`目錄下,`logs`是其日誌目錄,`conf`是其配置檔案所在的目錄。嘗試啟動一下`Nginx`: ```shell /data/nginx/sbin/nginx ``` 然後訪問虛擬機器的`80`埠,從而驗證`Nginx`已經正常啟動: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-10.png) ### 通過nginx-upsync-module和nginx_upstream_check_module模組進行編譯 上面做了一個`Nginx`極簡的編譯過程,實際上,在做動態負載均衡的時候需要新增`nginx-upsync-module`和`nginx_upstream_check_module`兩個模組,兩個模組必須提前下載原始碼,並且在編譯`Nginx`過程中需要指定兩個模組的物理路徑: ```shell mkdir /data/nginx/modules cd /data/nginx/modules # 這裡是Github的資源,不能用wget下載,具體是: nginx-upsync-module需要下載release裡面的最新版本:v2.1.2 nginx_upstream_check_module需要下載整個專案的原始碼,主要用到靠近當前版本的補丁,使用patch命令進行補丁升級 ``` ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-11.png) ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-12.png) 下載完成後分別(解壓)放在`/data/nginx/modules`目錄下: ```shell ll /data/nginx/modules drwxr-xr-x. 6 root root 4096 Nov 3 2019 nginx_upstream_check_module-master drwxrwxr-x. 5 root root 93 Dec 18 00:56 nginx-upsync-module-2.1.2 ``` 編譯前,還要先安裝一些前置依賴元件: ```shell yum -y install libpcre3 libpcre3-dev ruby zlib1g-dev patch ``` 接下來開始編譯安裝`Nginx`: ```shell cd /data/nginx/nginx-1.18.0 patch -p1 < /data/nginx/modules/nginx_upstream_check_module-master/check_1.16.1+.patch ./configure --prefix=/data/nginx --add-module=/data/nginx/modules/nginx_upstream_check_module-master --add-module=/data/nginx/modules/nginx-upsync-module-2.1.2 make make install ``` 上面的編譯和安裝過程無論怎麼調整,都會出現部分依賴缺失導致`make`異常,估計是這兩個模組並不支援太高版本的`Nginx`。(生產上用了一個版本比較低的`OpenResty`,這裡想復原一下使用相對新版本`Nginx`的踩坑過程)於是嘗試降級進行編譯,下面是參考多個`Issue`後得到的相對比較新的可用版本組合: - [nginx-1.14.2.tar.gz](http://nginx.org/download/nginx-1.14.2.tar.gz) - [xiaokai-wang/nginx_upstream_check_module](https://github.com/xiaokai-wang/nginx_upstream_check_module),使用補丁`check_1.12.1+.patch` - [nginx-upsync-module:release:v2.1.2](https://github.com/weibocom/nginx-upsync-module/releases/tag/v2.1.2) ```shell # 提前把/data/nginx下除了之前下載過的modules目錄外的所有檔案刪除 cd /data/nginx wget http://nginx.org/download/nginx-1.14.2.tar.gz tar -zxvf nginx-1.14.2.tar.gz ``` 開始編譯安裝: ```shell cd /data/nginx/nginx-1.14.2 patch -p1 < /data/nginx/modules/nginx_upstream_check_module-master/check_1.12.1+.patch ./configure --prefix=/data/nginx --add-module=/data/nginx/modules/nginx_upstream_check_module-master --add-module=/data/nginx/modules/nginx-upsync-module-2.1.2 make && make install ``` 安裝完成後通過`/data/nginx/sbin/nginx`命令啟動即可。 ### 啟用動態負載均和健康檢查 首先編寫一個簡易的`HTTP`服務,因為`Java`比較重量級,這裡選用`Golang`,程式碼如下: ```go package main import ( "flag" "fmt" "net/http" ) func main() { var host string var port int flag.StringVar(&host, "h", "127.0.0.1", "IP地址") flag.IntVar(&port, "p", 9000, "埠") flag.Parse() address := fmt.Sprintf("%s:%d", host, port) http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) { _, _ = fmt.Fprintln(writer, fmt.Sprintf("%s by %s", "pong", address)) }) http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { _, _ = fmt.Fprintln(writer, fmt.Sprintf("%s by %s", "hello world", address)) }) err := http.ListenAndServe(address, nil) if nil != err { panic(err) } } ``` 編譯: ```shell cd src set GOARCH=amd64 set GOOS=linux go build -o ../bin/app app.go ``` 這樣子在專案的`bin`目錄下就得到一個`Linux`下可執行的二級制檔案`app`,分別在埠`9000`和`9001`啟動兩個服務例項: ```shell # 記得先給app檔案的執行許可權chmod 773 app nohup ./app -p 9000 >
/dev/null 2>&1 & nohup ./app -p 9001 >/dev/null 2>&1 & ``` ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-14.png) 修改一下`Nginx`的配置,新增`upstream`: ```shell # /data/nginx/conf/nginx.conf部分片段 http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; upstream app { # 這裡是consul的leader節點的HTTP端點 upsync 192.168.56.200:8510/v1/kv/upstreams/app/ upsync_timeout=6m upsync_interval=500ms upsync_type=consul strong_dependency=off; # consul訪問不了的時候的備用配置 upsync_dump_path /data/nginx/app.conf; # 這裡是為了相容Nginx的語法檢查 include /data/nginx/app.conf; # 下面三個配置是健康檢查的配置 check interval=1000 rise=2 fall=2 timeout=3000 type=http default_down=false; check_http_send "HEAD / HTTP/1.0\r\n\r\n"; check_http_expect_alive http_2xx http_3xx; } server { listen 80; server_name localhost; location / { proxy_pass http://app; } # 健康檢查 - 檢視負載均衡的列表 location /upstream_list { upstream_show; } # 健康檢查 - 檢視負載均衡的狀態 location /upstream_status { check_status; access_log off; } } } # /data/nginx/app.conf server 127.0.0.1:9000 weight=1 fail_timeout=10 max_fails=3; server 127.0.0.1:9001 weight=1 fail_timeout=10 max_fails=3; ``` 手動新增兩個`HTTP`服務進去`Consul`中: ```shell curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000 curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9001 ``` ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-15.png) 最後重新載入`Nginx`的配置即可。 ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-16.png) ## 動態負載均衡測試 前置工作準備好,現在嘗試動態負載均衡,先從`Consul`下線`9000`埠的服務例項: ```shell curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":1}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000 ``` ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-17.png) 可見負載均衡的列表中,`9000`埠的服務例項已經置為`down`,此時瘋狂請求`http://192.168.56.200`,只輸出`hello world by 127.0.0.1:9001`,可見`9000`埠的服務例項已經不再參與負載。重新上線`9000`埠的服務例項: ```shell curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":0}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000 ``` 再瘋狂請求`http://192.168.56.200`,發現`hello world by 127.0.0.1:9000`和`hello world by 127.0.0.1:9001`交替輸出。到此可以驗證動態負載均衡是成功的。此時再測試一下服務健康監測,通過`kill -9`隨機殺掉其中一個服務例項,然後觀察`/upstream_status`端點: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-18.png) 瘋狂請求`http://192.168.56.200`,只輸出`hello world by 127.0.0.1:9001`,可見`9000`埠的服務例項已經不再參與負載,但是檢視`Consul`中`9000`埠的服務例項的配置,並沒有標記為`down`,可見是`nginx_upstream_check_module`為我們過濾了異常的節點,讓這些節點不再參與負載。 總的來說,這個相對完善的動態負載均衡功能需要`nginx_upstream_check_module`和`nginx-upsync-module`共同協作才能完成。 ## 服務平滑釋出 服務平滑釋出依賴於前面花大量時間分析的動態負載均衡功能。筆者所在的團隊比較小,所以選用了阿里雲的雲效作為產研管理平臺,通過裡面的流水線功能實現了服務平滑釋出,下面是其中一個服務的生產環境部署的流水線: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-19.png) 其實平滑釋出和平臺的關係不大,整體的步驟大概如下: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202006/n-c-u-20.png) 步驟比較多,並且涉及到大量的`shell`指令碼,這裡不把詳細的指令碼內容列出,簡單列出一下每一步的操作(注意某些步驟之間可以插入合理的`sleep n`保證前一步執行完畢): - 程式碼掃描、單元測試等等。 - 程式碼構建,生成構建後的壓縮包。 - 壓縮包上傳到伺服器`X`中,解壓到對應的目錄。 - 向`Consul`傳送指令,把當前釋出的`X_IP:PORT`的負載配置更新為`down=1`。 - `stop`服務`X_IP:PORT`。 - `start`服務`X_IP:PORT`。 - 檢查服務`X_IP:PORT`的健康狀態(可以設定一個時間週期例如120秒內每10秒檢查一次),如果啟動失敗,則直接中斷返回,確保還有另一個正常的舊節點參與負載,並且人工介入處理。 - 向`Consul`傳送指令,把當前釋出的`X_IP:PORT`的負載配置更新為`down=0`。 上面的流程是通過`hard code`完成,對於不同的伺服器,只需要新增一個釋出流程節點並且改動一個`IP`的佔位符即可,不需要對`Nginx`進行配置重新載入。筆者所在的平臺流量不大,目前每個服務部署兩個節點就能滿足生產需要,試想一下,如果要實現動態擴容,應該怎麼構建流水線? ## 小結 服務平滑釋出是`CI/CD`中比較重要的一個環節,而動態負載均衡則是服務平滑釋出的基礎。雖然現在很多雲平臺都提供了十分便捷的持續整合工具,但是在使用這些工具和配置流程的時候,最好能夠理解背後的基本原理,這樣才能在工具不適用的時候或者出現問題的時時候,迅速地作出判斷和響應。 參考資料: - [nginx-upsync-module](https://github.com/weibocom/nginx-upsync-module) - [Nginx docs](https://nginx.org/en/docs) - [Consul docs](https://www.consul.io/docs) (本文完 c-7-d e-a-20200613 感謝廣州某金融科技公司運維大佬昊哥提供的