天天看點

通過 Consul+OpenResty 實作無reload動态負載均衡

【轉載請注明出處】: https://www.jianshu.com/p/bee45550781e

動态Nginx負載均衡的配置,可以通過Consul+Consul-Template方式,但是這種方案有個缺點:每次發現配置變更都需要reload Nginx,而reload是有一定損耗的。而且,如果你需要長連接配接支援的話,那麼當reload時Nginx長連接配接所在worker程序會進行優雅退出,并當該worker程序上的所有連接配接都釋放時,程序才真正退出(表現為worker程序處于worker process is shutting down)。是以,如果能做到不reload就能動态更改upstream,那麼就完美了。

目前的開源解決方法有3種:

1、Tengine的Dyups子產品

2、微網誌的

Upsync子產品

+Consul

3、使用OpenResty的balancer_by_lua。

這裡使用的是Consul+OpenResty 來實作動态負載均衡。Consul的安裝這裡将不再介紹。

OpenResty簡介

OpenResty 是一個基于 Nginx 與 Lua 的高性能 Web 平台,其内部內建了大量精良的 Lua 庫、第三方子產品以及大多數的依賴項。用于友善地搭建能夠處理超高并發、擴充性極高的動态 Web 應用、Web 服務和動态網關。

Lua簡介

Lua是一個簡潔、輕量、可擴充的程式設計語言,其設計目的是為了嵌入應用程式中,進而為應用程式提供靈活的擴充和定制功能。Lua由标準C編寫而成,代碼簡潔優美,幾乎在所有作業系統和平台上都可以編譯,運作。

如何做到動态呢?

正常使用Nginx作為API網關, 需要如下配置, 當加後端執行個體時, reload一下Nginx, 使其生效

upstream backend {
    server 192.168.0.1;
    server 192.168.0.2;
}           

那麼隻要讓upstream變成動态可程式設計就OK了, 當新增後端執行個體時, 無需reload, 自動生效。

在OpenResty通過長輪訓和版本号及時擷取Consul的kv store變化。Consul提供了time_wait和修改版本号概念,如果Consul發現該kv沒有變化就會hang住這個請求5分鐘,在這5分鐘内如果有任何變化都會及時傳回結果。通過比較版本号我們就知道是逾時了還是kv的确被修改了。

Consul的node和service也支援阻塞查詢,相對來說用service更好一點,畢竟支援服務的健康檢查。阻塞api和kv一樣,加一個index就好了。

OpenResty安裝

筆者使用的是MAC,下面重點介紹MAC系統上的操作。

Mac

brew tap openresty/brew
brew install openresty           

Linux

sudo yum install yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

sudo yum install openresty

#指令行工具 resty
sudo yum install openresty-resty           

Mac OS系統上安裝完之後Nginx的配置檔案在目錄

/usr/local/etc/openresty

,啟動指令在目錄

/usr/local/opt/openresty/nginx/sbin

啟動

openresty           

在浏覽器輸入

localhost

openresty -h           

到這裡OpenResty就已經安裝好了。

重新開機

openresty -s reload           

安裝Lua的包管理器LuaRocks

LuaRocks是Lua子產品的包管理器,安裝LuaRocks之後可以通過LuaRocks來快速安裝Lua的子產品,官網位址是

https://luarocks.org/

進入OpenResty的安裝目錄

/usr/local/opt/openresty

可以看到已經安裝了luajit,也是OpenResty預設使用的lua環境。

下載下傳LuaRocks安裝包

http://luarocks.github.io/luarocks/releases/luarocks-3.0.4.tar.gz

,解壓之後進入LuaRocks源碼目錄編譯安裝到OpenResty的安裝目錄。

./configure --prefix=/usr/local/opt/openresty/luajit --with-lua=/usr/local/opt/openresty/luajit --lua-suffix=luajit --with-lua-include=/usr/local/opt/openresty/luajit/include/luajit-2.1
make build
make install           

安裝完LuaRocks之後,可以看到LuaRocks的執行指令在目錄

/usr/local/opt/openresty/luajit

安裝完LuaRocks之後就可以安裝搭建環境需要的依賴包了。

./luarocks install luasocket           

修改配置檔案

在Consul注冊好服務之後通過Consul的

http://127.0.0.1:8500/v1/catalog/service/api_tomcat2

接口就可以擷取到服務的清單。這裡不再講述Consul的服務注冊。

在預設配置檔案目錄

/usr/local/etc/openresty

建立一個

servers

檔案夾來放新的配置檔案,建立

lualib

檔案夾來放lua腳本,修改配置檔案

nginx.conf

,添加

include servers/*.conf;

lualib

檔案夾下建立腳本

upstreams.lua

local http = require "socket.http"
local ltn12 = require "ltn12"
local cjson = require "cjson"

local _M = {}

_M._VERSION="0.1"

function _M:update_upstreams()
    local resp = {}

    http.request{
        url = "http://127.0.0.1:8500/v1/catalog/service/api_tomcat2", sink = ltn12.sink.table(resp)
    }
       
        local resp = table.concat(resp);
    local resp = cjson.decode(resp);

    local upstreams = {}
    for i, v in ipairs(resp) do
         upstreams[i] = {ip=v.Address, port=v.ServicePort}
       end
        
      ngx.shared.upstream_list:set("api_tomcat2", cjson.encode(upstreams))
end

function _M:get_upstreams()
   local upstreams_str = ngx.shared.upstream_list:get("api_tomcat2");
   local tmp_upstreams = cjson.decode(upstreams_str);
   return tmp_upstreams;
end

return _M           

過luasockets查詢Consul來發現服務,update_upstreams用于更新upstream清單,get_upstreams用于傳回upstream清單,此處可以考慮worker程序級别的緩存,減少因為json的反序列化造成的性能開銷。

還要注意使用的luasocket是阻塞API,這可能會阻塞我們的服務,使用時要慎重。

建立檔案

test_openresty.conf

lua_package_path "/usr/local/etc/openresty/lualib/?.lua;;";
lua_package_cpath "/usr/local/etc/openresty/lualib/?.so;;";

lua_shared_dict upstream_list 10m;

# 第一次初始化
init_by_lua_block {
    local upstreams = require "upstreams";
    upstreams.update_upstreams();
}

# 定時拉取配置
init_worker_by_lua_block {
    local upstreams = require "upstreams";
    local handle = nil;

    handle = function ()
        --TODO:控制每次隻有一個worker執行
        upstreams.update_upstreams();
        ngx.timer.at(5, handle);
    end
    ngx.timer.at(5, handle);
}

upstream api_server {
    server 0.0.0.1 down; #占位server

    balancer_by_lua_block {
        local balancer = require "ngx.balancer";
        local upstreams = require "upstreams";    
        local tmp_upstreams = upstreams.get_upstreams();
        local ip_port = tmp_upstreams[math.random(1, table.getn(tmp_upstreams))];
        balancer.set_current_peer(ip_port.ip, ip_port.port);
    }
}

server {
    listen       8000;
    server_name  localhost;
    charset utf-8;
    location / {
         proxy_pass http://api_server;
         access_log  /usr/local/etc/openresty/logs/api.log  main;
    }
}           

init_worker_by_lua是每個Nginx Worker程序都會執行的代碼,是以實際實作時可考慮使用鎖機制,保證一次隻有一個人處理配置拉取。另外ngx.timer.at是定時輪詢,不是走的長輪詢,有一定的時延。有個解決方案,是在Nginx上暴露HTTP API,通過主動推送的方式解決。

Agent可以長輪詢拉取,然後調用HTTPAPI推送到Nginx上,Agent可以部署在Nginx本機或者遠端。

對于拉取的配置,除了放在記憶體裡,請考慮在本地檔案系統中存儲一份,在網絡出問題時作為托底。

擷取upstream清單,實作自己的負載均衡算法,通過ngx.balancer API進行動态設定本次upstream server。通過balancer_by_lua除可以實作動态負載均衡外,還可以實作個性化負載均衡算法。

可以使用lua-resty-upstream-healthcheck子產品進行健康檢查,後面會單獨介紹。

到這裡,其實還有一個問題沒有解決掉,雖然upstream中的占位server是下線的,但是nginx在檢測upstream清單中server的健康狀态的時候是會去檢測這個占位server的,最好的方式還是在啟動之後把它徹底從upstream清單中給移除掉。