前段時間順利地把整個服務集羣和中間件所有從UCloud
遷移到阿里雲,筆者擔任了架構和半個運維的角色。這裏詳細記錄一下經過Nginx
、Consul
、Upsync
實現動態負載均衡和服務平滑發佈的核心知識點和操做步驟,整個體系已經在生產環境中平穩運行。編寫本文使用的虛擬機系統爲CentOS7.x
,虛擬機的內網IP
爲192.168.56.200
。node
通常會經過upstream
配置Nginx
的反向代理池:linux
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
爲:nginx
upstream upstream_server{ # 添加down標記該端口的服務實例不參與負載 server 127.0.0.1:8081 down; server 127.0.0.1:8082; }
而且經過nginx -s reload
從新加載配置,該upstream
配置纔會生效。咱們知道,服務發佈時候重啓過程當中是處於不可用狀態,正確的服務發佈過程應該是:git
upstream
剔除,通常是置爲down
,告知Nginx
服務upstream
配置變動,須要經過nginx -s reload
進行重載。upstream
中拉起,通常是把down
去掉,告知Nginx
服務upstream
配置變動,須要經過nginx -s reload
進行重載。上面的步驟一則涉及到upstream
配置,二則須要Nginx
從新加載配置(nginx -s reload
),顯得比較笨重,在高負載的狀況下從新啓動Nginx
並從新加載配置會進一步增長系統的負載並可能暫時下降性能。github
因此,能夠考慮使用分佈式緩存把upstream
配置存放在緩存服務中,而後Nginx
直接從這個緩存服務中讀取upstream
的配置,這樣若是有upstream
的配置變動就能夠直接修改緩存服務中對應的屬性,而Nginx
服務也不須要reload
。在實戰中,這裏提到的緩存服務就選用了Consul
,Nginx
讀取緩存中的配置屬性選用了新浪微博提供的Nginx
的C
語言模塊nginx-upsync-module
。示意圖大體以下:算法
Consul
是Hashicorp
公司的一個使用Golang
開發的開源項目,它是一個用於服務發現和配置的工具,具有分佈式和高度可用特性,而且具備極高的可伸縮性。Consul
主要提供下面的功能:shell
Service Segmentation/Service Mesh
)。下面是安裝過程:json
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
:bootstrap
下面基於單臺虛擬機搭建一個僞集羣,關於集羣的一些配置屬性的含義和命令參數的解釋暫時不進行展開。緩存
# 建立集羣數據目錄 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
文件,內容以下:
{ "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
文件,內容以下:
{ "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
文件,內容以下:
{ "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 } }
新建一個集羣啓動腳本:
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中的日誌以下:
經過節點1的HTTP
端點訪問後臺管理頁面以下(可見當前的節點1被標記了一顆紅色的星星,說明當前節點1是Leader
節點):
至此,Consul
單機僞集羣搭建完成(其實分佈式集羣的搭建大同小異,注意集羣節點所在的機器須要開放使用到的端口的訪問權限),因爲Consul
使用Raft
做爲共識算法,該算法是強領導者模型,也就是隻有Leader
節點能夠進行寫操做,所以接下來的操做都須要使用節點1的HTTP
端點,就是192.168.56.200:8510
。
重點筆記:若是
Consul
集羣重啓或者從新選舉,Leader
節點有可能發生更變,外部使用的時候建議把Leader
節點的HTTP
端點抽離到可動態更新的配置項中或者動態獲取Leader
節點的IP
和端口。
直接從官網下載二級制的安裝包而且解壓:
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
依賴:
yum -y install pcre-devel yum install -y zlib-devel
編譯命令以下:
cd /data/nginx/nginx-1.18.0 ./configure --prefix=/data/nginx
若是./configure
執行過程不出現問題,那麼結果以下:
接着執行make
:
cd /data/nginx/nginx-1.18.0 make
若是make
執行過程不出現問題,那麼結果以下:
最後,若是是首次安裝,能夠執行make install
進行安裝(實際上只是拷貝編譯好的文件到--prefix
指定的路徑下):
cd /data/nginx/nginx-1.18.0 make install
make install
執行完畢後,/data/nginx
目錄下新增了數個文件夾:
其中,Nginx
啓動程序在sbin
目錄下,logs
是其日誌目錄,conf
是其配置文件所在的目錄。嘗試啓動一下Nginx
:
/data/nginx/sbin/nginx
而後訪問虛擬機的80
端口,從而驗證Nginx
已經正常啓動:
上面作了一個Nginx
極簡的編譯過程,實際上,在作動態負載均衡的時候須要添加nginx-upsync-module
和nginx_upstream_check_module
兩個模塊,兩個模塊必須提早下載源碼,而且在編譯Nginx
過程當中須要指定兩個模塊的物理路徑:
mkdir /data/nginx/modules cd /data/nginx/modules # 這裏是Github的資源,不能用wget下載,具體是: nginx-upsync-module須要下載release裏面的最新版本:v2.1.2 nginx_upstream_check_module須要下載整個項目的源碼,主要用到靠近當前版本的補丁,使用patch命令進行補丁升級
下載完成後分別(解壓)放在/data/nginx/modules
目錄下:
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
編譯前,還要先安裝一些前置依賴組件:
yum -y install libpcre3 libpcre3-dev ruby zlib1g-dev patch
接下來開始編譯安裝Nginx
:
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
後獲得的相對比較新的可用版本組合:
check_1.12.1+.patch
# 提早把/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
開始編譯安裝:
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
,代碼以下:
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) } }
編譯:
cd src set GOARCH=amd64 set GOOS=linux go build -o ../bin/app app.go
這樣子在項目的bin
目錄下就獲得一個Linux
下可執行的二級制文件app
,分別在端口9000
和9001
啓動兩個服務實例:
# 記得先給app文件的執行權限chmod 773 app nohup ./app -p 9000 >/dev/null 2>&1 & nohup ./app -p 9001 >/dev/null 2>&1 &
修改一下Nginx
的配置,添加upstream
:
# /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
中:
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
最後從新加載Nginx
的配置便可。
前置工做準備好,如今嘗試動態負載均衡,先從Consul
下線9000
端口的服務實例:
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
可見負載均衡的列表中,9000
端口的服務實例已經置爲down
,此時瘋狂請求http://192.168.56.200
,只輸出hello world by 127.0.0.1:9001
,可見9000
端口的服務實例已經再也不參與負載。從新上線9000
端口的服務實例:
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
端點:
瘋狂請求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
共同協做才能完成。
服務平滑發佈依賴於前面花大量時間分析的動態負載均衡功能。筆者所在的團隊比較小,因此選用了阿里雲的雲效做爲產研管理平臺,經過裏面的流水線功能實現了服務平滑發佈,下面是其中一個服務的生產環境部署的流水線:
其實平滑發佈和平臺的關係不大,總體的步驟大概以下:
步驟比較多,而且涉及到大量的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
中比較重要的一個環節,而動態負載均衡則是服務平滑發佈的基礎。雖然如今不少雲平臺都提供了十分便捷的持續集成工具,可是在使用這些工具和配置流程的時候,最好可以理解背後的基本原理,這樣才能在工具不適用的時候或者出現問題的時時候,迅速地做出判斷和響應。
參考資料:
(本文完 c-7-d e-a-20200613 感謝廣州某金融科技公司運維大佬昊哥提供的支持)