前提
前段時間順利地把整個服務叢集和中介軟體全部從UCloud
遷移到阿里雲,筆者擔任了架構和半個運維的角色。這裡詳細記錄一下通過Nginx
、Consul
、Upsync
實現動態負載均衡和服務平滑釋出的核心知識點和操作步驟,整個體系已經在生產環境中平穩執行。編寫本文使用的虛擬機器系統為CentOS7.x
,虛擬機器的內網IP
為192.168.56.200
。
動態負載均衡的基本原理
一般會通過upstream
配置Nginx
的反向代理池:
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
為:
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
。示意圖大致如下:
Consul安裝和叢集搭建
Consul
是Hashicorp
公司的一個使用Golang
開發的開源專案,它是一個用於服務發現和配置的工具,具備分散式和高度可用特性,並且具有極高的可伸縮性。Consul
主要提供下面的功能:
- 服務發現。
- 執行狀況檢查。
- 服務分塊/服務網格(
Service Segmentation/Service Mesh
)。 - 金鑰/值儲存。
- 多資料中心。
下面是安裝過程:
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
:
下面基於單臺虛擬機器搭建一個偽叢集,關於叢集的一些配置屬性的含義和命令引數的解釋暫時不進行展開。
# 建立叢集資料目錄
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
和埠。
Nginx編譯安裝
直接從官網下載二級制的安裝包並且解壓:
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-upsync-module和nginx_upstream_check_module模組進行編譯
上面做了一個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
後得到的相對比較新的可用版本組合:
- nginx-1.14.2.tar.gz
- xiaokai-wang/nginx_upstream_check_module,使用補丁
check_1.12.1+.patch
- nginx-upsync-module:release:v2.1.2
# 提前把/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 感謝廣州某金融科技公司運維大佬昊哥提供的支援)