接入層控制:nginx動態流量路由和負載均衡

原創聲明:本文系作者原創,謝絕個人、媒體、公衆號或網站未經授權轉載,違者追究其法律責任。

任何團隊面臨着業務的增長,服務的路由場景及流量控制需求都會越來越複雜。通常會涉及到接入層路由、流量控制和應用服務層路由、甚至數據庫路由等。而負載均衡又是另一個熱門的技術話題,可以用在服務端各層。本文主要講解分佈式系統中接入層的動態路由以及圍繞的負載均衡。

一、業務需要

業務早期,業務需求和發展規劃簡單,接入層用nginx+tomcat web 的簡單結構就能滿足需求:

這種結構下,nginx層流量控制和負載均衡沒有特別複雜的要求,一般使用location、upstream、if、rewrite、reload等簡單的nginx指令,實現部署或請求轉發等功能。例如:

location ^~ /static/ {

root /webroot/static/;

}

 

location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {

root /webroot/res/;

}

 

upstream backend_aaa {

server 10.4.232.110:8080 weight=10;

}

 

location /abc {

proxy_pass http://backend_aaa; #路由到backend_bbb集羣

}

 

location / {

proxy_pass http://tomcat:8080/

}

 

這種使用方法的優點是運行時性能高,接近原生nginx,缺點是功能受限、操作不精細、容易出錯,以及轉發規則固定,只能靜態分流實現等。特別的,比如使用reload命令,業務初期,大部分情況類似如下使用:

#!/bin/bash

ps -fe | grep nginx | grep -v grep

if [ $? -ne 0 ] then

sudo /usr/servers/nginx/sbin/nginx

echo "nginx start"

else

sudo /usr/servers/nginx/sbin/nginx -s reload

echo "nginx reload"

fi

 

而reload是有一定損耗的。如果你需要長連接支持的話,那麼當reload nginx時長連接所在worker進程會進行優雅退出,並當該worker進程上的所有連接都釋放時,進程才真正退出(表現爲worker進程處於worker process is shutting down)。因此,如果能做到不reload就能動態更改upstream,那麼就更完美。如京東張開濤 "億級流量架構"書籍中提到的,社區版Nginx目前有三個選擇實現不用reload也能動態更改upstream:Tengine的Dyups模塊、微博的Upsync和使用OpenResty的balancer_by_lua。微博使用Upsync+Consul實現動態負載均衡,而又拍雲使用其開源的slardar(Consul + balancer_by_lua)實現動態負載均衡。

 

隨着業務高速發展及用戶量的增加,接入層的路由、流量控制等需求會變得越來越複雜,引用美團"Oceanus:美團HTTP流量定製化路由的實踐"文中講到的,複雜的接入層路由通常會有類似如下的常見場景:

 

  • 團購秒殺要靈活控制壓測流量,實現線上服務單節點、各機房、各地域等多維度的壓測。
  • 外賣業務要做流量隔離,把北方地域的流量轉發到分組a,南方地域的流量轉發到分組b。
  • 酒旅業務要對App新版本進行灰度,讓千分之一的用戶試用新版本,其他用戶訪問老版本。
  • QA部門要通過請求的自定義參數指定轉發分組,構建穩定且高可用的測試環境。

 

因此,依靠原生的nginx指令很難精細的支持某些業務場景下的分流控制需求,所以很難作爲解決公司級分流框架的有效手段。針對它們所存在的不足,定製高可擴展的動態分流框架就是一個很必要的定位,能實現動態支持各種業務場景的分流需求。

 

二、動態流量控制方案

動態流量處理中, consul+consul-template是一個開源的動態流量方案,這也是《億級流量網站架構核心技術》中推薦的。在原有方案上,我對其細節做了一些重構,下圖是它的實施架構:

大致過程:

1.upstream服務啓動。我們可以通過管理後臺手動或者改造server自動註冊服務到consul。

2.在nginx機器上部署並啓動consul-template 作爲nginx agent,通過長輪詢監聽consul服務變更。

3.consul-template監聽到變更後,動態修改upstream列表。

4.consul-template修改完upstream列表後,調用nginx重新加載nginx upstream配置(reload)。

5.nginx轉發請求到新的upstream server;

接下來我們講解各個節點的實施細節。

 

2.1 consul 及consul-template使用

以Consul1.2.3和Consul-template v0.19.5爲例

consul

  • ./consul agent -server -bootstrap-expect 1-data-dir /tmp/consul -bind 0.0.0.0 -client 0.0.0.0 是consul啓動命令。data-dir指定agent的狀態存儲位置,bind指定集羣通信的地址,client指定客戶端通信的地址(如consul-template與consul通信)。啓動時還可以使用-ui-dir ./ui/指定Consul Web UI目錄,實現通過Web UI管理consul,然後訪問如http://127.0.0.1:8500即可看到控制界面。

 

  • 到consul中註冊服務:

curl -X PUT http://127.0.0.1:8500/v1/catalog/register -d'{"Datacenter": "dc_south", "Node":"tomcat", "Address":"192.168.1.1","Service": {"Id" :"192.168.1.1:8080", "Service": "oilcard_tomcat","tags": ["dev"], "Port": 8080}}'

curl -X PUT http://127.0.0.1:8500/v1/catalog/register -d'{"Datacenter": "dc_north", "Node":"tomcat", "Address":"192.168.1.2","Service": {"Id" :"192.168.1.1:8090", "Service": "oilcard_tomcat","tags": ["dev"], "Port": 8090}}'

Datacenter指定數據中心,Address指定服務IP,Service.Id指定服務唯一標識,Service.Service指定服務分組,Service.tags指定服務標籤(如測試環境、預發環境等),Service.Port指定服務端口。

 

  • consul中摘除服務:

curl -X PUThttp://127.0.0.1:8500/v1/catalog/deregister -d '{"Datacenter":"dc_south", "Node": "tomcat", "ServiceID" :"192.168.1.1:8080"}'

 

  • consul發現服務:

curl http://127.0.0.1:8500/v1/catalog/service/oilcard_tomcat

上面是常見的consul api,更多可參考: https://www.consul.io/docs/agent/http.html

 

consul-template

  • template配置

consul-template nginx代理機器上添加一份consul-template模板 oilcard.tomcat.ctmpl。

upstream oilcard_tomcat {

server 127.0.0.1:1111;

{{range service"dev.item_jd_tomcat@dc1"}}

server {{.Address}}:{{.Port}} weight=1;

{{end}}

}

service指定格式爲:標籤.服務@數據中心,然後通過循環輸出Address和Port,從而生成nginx upstream配置。

consul-template模板語言使用可以多參考官方文檔

 

  • 啓動consul-template

./consul-template -consul 127.0.0.1:8500 -template ./oilcard.tomcat.ctmpl:/usr/servers/nginx/conf/domains/oilcard.tomcat.conf: "./restart.sh" 2>&1 >/opt/consul-template.log & ;

使用consul指定consul服務器客戶端通信地址,template格式是“配置模板:目標配置文件:腳本/命令”,即通過配置模板更新目標配置文件,然後調用腳本重啓nginx。使用nginx include指將/usr/servers/nginx/conf/domains/oilcard.tomcat.conf包含到nginx.conf配置文件。restart.sh腳本:

#!/bin/bash

ps -fe | grep nginx | grep -v grep

if [ $? -ne 0 ]

then

sudo /usr/servers/nginx/sbin/nginx

echo "nginx start"

else

sudo /usr/servers/nginx/sbin/nginx -s reload

echo "nginx reload"

fi

 

  • java程序自動註冊到consul中

讓程序自動註冊到consul中能讓服務很好的達到高可用和質量檢測,當然也可以通過consul admin手工註冊管理。

本文的自動註冊使用Spring Boot+Consul Java Client實現,代碼細節如下:

<dependency>

<groupId>com.orbitz.consul</groupId>

<artifactId>consul-client</artifactId>

<version>0.12.8</version>

</dependency>

如下spring boot啓動代碼是進行服務註冊與摘除:

public static void main(String[] args) {

SpringApplication.run(Bootstrap.class, args);

Consul consul = Consul.builder().withHostAndPort(HostAndPort.fromString ("192.168.61.129:8500")).build();

final AgentClient agentClient = consul.agentClient();

String service = "oilcard_tomcat";

String address = "192.168.1.1";

String tag = "dev";

int port= 8090;

final String serviceId = address + ":" + port;

ImmutableRegistration.Builder builder = ImmutableRegistration.builder();

builder.id(serviceId).name(service).address(address).port(port).addTags(tag);

agentClient.register(builder.build());

//JVM停止時摘除服務

Runtime.getRuntime().addShutdownHook(new Thread() {

 

@Override

publicvoid run() {

agentClient.deregister(serviceId);

}

});

}

啓動後進行服務註冊,然後在JVM停止時進行服務摘除。

到此我們就實現了動態upstream負載均衡,upstream服務啓動後自動註冊到nginx,upstream服務停止時,自動從nginx上摘除。

通過consul+consul-template方式,每次配置變更都需要reload nginx,如本文開始所述,reload耗損性能,接下來我們使用consul + balancer_by_lua 實現動態upstream。

 

consul+openresty

使用consul註冊服務,使用openresty balancer_by_lua實現無reload動態負載均衡,架構如下:

1.通過upstream server啓動/停止時註冊服務,或者通過consul管理後臺註冊服務。

2.nginx啓動時調用init_by_lua,啓動時拉取配置,並更新到共享字典來存儲upstream列表;然後通過init_worker_by_lua啓動定時器,定期去consul拉取配置並實時更新到共享字典。

3.balancer_by_lua使用共享字典存儲的upstream列表進行動態負載均衡。

 

將更新、獲取upstream 抽象成dyna_upstreams模塊:

local http = require("socket.http")

local ltn12 = require("ltn12")

local cjson = require "cjson"

local function update_upstreams()

local resp = {}

http.request{

url="http://192.168.61.100:8500/v1/catalog/service/oilcard_tomcat",

sink = ltn12.sink.table(resp)

}

 

resp = table.concat(resp)

resp =cjson.decode(resp)

 

local upstreams = {{ip="127.0.0.1", port=1111}}

for i, vin ipairs(resp) do

upstreams[i+1] = {ip=v.Address, port=v.ServicePort}

end

ngx.shared.upstream_list:set("item_jd_tomcat", cjson.encode (upstreams))

end

end

 

local function get_upstreams()

local upstreams_str = ngx.shared.upstream_list:get("item_jd_tomcat")

end

 

local _M = {

update_upstreams = update_upstreams,

get_upstreams = get_upstreams

}

 

update_upstreams用於更新upstream列表,get_upstreams用於返回upstream列表,注意因爲luasocket是阻塞API這可能會阻塞我們的服務,使用時要慎重。

 

init_*_by_lua配置

 

#存儲upstream列表的共享字典

lua_shared_dict upstream_list 10m;

 

#Nginx Master進程加載配置文件時執行,用於第一次初始化配置

 

init_by_lua_block {

localdyna_upstreams = require "dyna_upstreams";

dyna_upstreams.update_upstreams();

}

 

#Nginx Worker進程調度,使用ngx.timer.at定時拉取配置

init_worker_by_lua_block {

local dyna_upstreams = require"dyna_upstreams";

local handle = nil;

handle =function ()

--TODO:控制每次只有一個worker執行

dyna_upstreams.update_upstreams();

ngx.timer.at(5, handle);

end

 

ngx.timer.at(5, handle);

}

 

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

consul template可以長輪詢拉取,然後調用HTTP API推送到nginx上,對於拉取的配置,除了放在內存裏,請考慮在本地文件系統中存儲一份,在網絡出問題時作爲備份使用。

 

oilcard.tomcat.conf upstream配置balancer_by_lua

upstream oilcard_tomcat{

server 0.0.0.1; #佔位server

balancer_by_lua_block {

local balancer = require "ngx.balancer"

local dyna_upstreams = require "dyna_upstreams";

local upstreams = dyna_upstreams.get_upstreams();

local ip_port = upstreams[math.random(1,table.getn(upstreams)) ]

ngx.log(ngx.ERR, "current :=============", math.random(1,table.getn (upstreams)))

balancer.set_current_peer(ip_port.ip, ip_port.port)

}

}

獲取upstream列表,實現自己的負載均衡算法,通過ngx.balancer API進行動態設置本次upstream server。通過balancer_by_lua除可以實現動態負載均衡外,還可以實現個性化負載均衡算法。

最後,記得使用lua-resty-upstream-healthcheck模塊進行健康檢查。

 

三、總結

本文一開始講述了動態流量及路由控制的業務必要性,接着以開源的consul+consul-template+nginx+openresty+lua爲例,講解了具體怎樣實施動態路由方案。穩定的接入層方案很需要時間不斷來檢驗。希望本文幫助到你。

 

注:開源的consul+consul-template方案主要從《億級流量網站架構核心技術》中借鑑而來,特此申明。

 

參考:

1.https://tech.meituan.com/Oceanus_Custom_Traffic_Routing.html

2.https://mp.weixin.qq.com/s/O9LELywlYUkSrdcNaooNAw?

3.https://www.consul.io/api/index.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章