原創聲明:本文系作者原創,謝絕個人、媒體、公衆號或網站未經授權轉載,違者追究其法律責任。
任何團隊面臨着業務的增長,服務的路由場景及流量控制需求都會越來越複雜。通常會涉及到接入層路由、流量控制和應用服務層路由、甚至數據庫路由等。而負載均衡又是另一個熱門的技術話題,可以用在服務端各層。本文主要講解分佈式系統中接入層的動態路由以及圍繞的負載均衡。
一、業務需要
業務早期,業務需求和發展規劃簡單,接入層用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