OpenResty 動態負載均衡

OpenResty 動態負載均衡

在之前提到的OpenResty/Nginx的負載均衡當中,當服務器啟動之後,upstream中的上游服務器就是固定死的了,做不到動態的變更。這裡面說到的變更,其實更多指的是增加機器。因為當上遊服務器不可用時,upstream會自動將服務器摘除,但是當新增服務器時,upstream就做不到了。傳統的負載均衡辦法,就是能是修改配置,然後重啟服務。下面介紹一下動態負載均衡的方式,一種是通過動態重啟服務;另外一種是通過代碼的方式動態拉取服務器列表。

Consul

Consul是一個分佈式服務註冊與發現系統。這裡面使用Consul來管理上游服務器,當服務器啟動時將其註冊到註冊中心去,當服務關閉時從註冊中心列表中剔除。這裡面需要注意一點的是:當上遊服務器關閉時,Consul本身不會自動從列表中剔除,而是需要在服務器關閉前主動向Consul發起刪除服務。

Consul有以下特性:

  • 服務註冊:服務提供者可以通過HTTP或DNS的方式,將服務註冊到Consul中去。
  • 服務發現:服務消費者可以通過HTTP或DNS的方式,獲取服務的IP和PORT。
  • 故障檢測:支持如TCP/HTTP等方式的健康檢查機制,當服務出現故障時摘除服務。
  • K/V存儲:使用key-value實現配置中心,使用HTTP長輪詢實現變更配置。
  • 多數據中心:支持多數據中心,可以按照數據中心註冊和發現服務。可以支持只消費本地機房的服務,多機房可以做到異地容災。
  • Raft算法:Consul的一致性算法。

通過Consul可以獲取到upstream中的上游服務器列表,下面要做的事情就是生成upstream中的模板了。這裡就需要用到Consul-templete,它可以使用HTTP長輪詢實現變更觸發和配置更改。從而可以根據Consul服務器列表動態生成配置文件,然後去重新啟動OpenResty/Nginx即可。

Consul+Consul-templete 方式

Consul+Consul-templete 就如上面所說的,是一種監聽服務器列表變更,然後動態生成upstream模板,重啟服務器。

Consul-Server

筆者使用的是MAC,下面所進行的操作都是基於MAC系統的。首先需要安裝Consul如下:

brew install consul

安裝完成之後,可以通過如下命令啟動Consul服務:

consul agent -dev

啟動完成之後,可以通過如下地址:localhost:8500/ui。訪問Consul的Web界面:

OpenResty 動態負載均衡

  • 註冊服務

可以使用HTTP的方式向Consul註冊一個服務:

curl -X PUT http://localhost:8500/v1/agent/service/register -d '
{
"ID": "moguhu_server1",
"Name": "moguhu_server",
"Tags": ["dev"],
"Address": "127.0.0.1",
"Port": 8081
}
'
  • ID:代表要註冊的服務的唯一標識
  • Name:表示一組服務的名稱
  • Tags:服務標籤,可以用於區分開發、測試環境
  • Address:服務的地址
  • Port:服務的端口

Consul-template

Consul-template的作用是生成upstream配置模板,安裝命令如下:

brew install consul-template

然後在nginx.conf同級目錄下創建moguhu_server.ctmpl

upstream moguhu_server {
{{range service "dev.moguhu_server@dc1"}}
server {{.Address}}:{{.Port}};
{{end}}
}

重啟OpenResty腳本如下:reboot.sh

ps -ef | grep nginx | grep -v grep
if [ $? -ne 0 ]
then
sudo ~/software/openresty/nginx/sbin/nginx -p ~/hugege/code-sublime/01-zhtest -c ~/hugege/code-sublime/01-zhtest/config/nginx.conf
echo "OpenResty start..."
else
sudo ~/software/openresty/nginx/sbin/nginx -p ~/hugege/code-sublime/01-zhtest -c ~/hugege/code-sublime/01-zhtest/config/nginx.conf -s reload
echo "Openresty restart..."
fi

然後nginx.conf配置如下:

include /Users/xuefeihu/hugege/code-sublime/01-zhtest/config/moguhu_server.conf;
...
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
proxy_pass http://moguhu_server;
}
}

上游服務器

上游服務器upstream中使用的是Spring Boot實現的,其核心代碼如下所示:

@Configuration
public class ConsulConfiguration {
@Value("${server.port}")
private int port;
@Value("${spring.application.name}")
private String serviceId;
@PostConstruct
public void init() {
// 參數完全對應HTTP API
ImmutableRegistration.Builder builder = ImmutableRegistration.builder()
.id(serviceId)
.name("moguhu_server")
.address("127.0.0.1")
.port(port)
.addTags("dev");
// 向Consul註冊服務
Consul consul = Consul.builder().withHostAndPort(HostAndPort.fromString("127.0.0.1:8500")).build();
final AgentClient agentClient = consul.agentClient();
agentClient.register(builder.build());
//註冊shutdown hook,停掉應用時從Consul摘除服務
Runtime.getRuntime().addShutdownHook(new Thread(() -> agentClient.deregister(serviceId)));
}
}

筆者在實驗時,Consul版本的問題,造成在JVM停止時,沒有執行刪除服務的操作。因此附上下面的pom依賴

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.orbitz.consul</groupId>
<artifactId>consul-client</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.10.0</version>
</dependency>
</dependencies>

測試驗證

1、啟動Consul

consul agent -dev

2、啟動Consul-template

consul-template -consul-addr 127.0.0.1:8500 -template \
"/Users/xuefeihu/hugege/code-sublime/01-zhtest/config/moguhu_server.ctmpl:\
/Users/xuefeihu/hugege/code-sublime/01-zhtest/config/moguhu_server.conf:\
sh /Users/xuefeihu/hugege/code-sublime/01-zhtest/config/reboot.sh"

3、啟動2臺upstream服務器

然後你會發現在nginx.conf的同級目錄下生成了moguhu_server.conf文件,內容如下:

upstream real_server {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}

當手動停掉一臺服務器時,配置又會變更為如下:

upstream real_server {
server 127.0.0.1:8081;
}

此時reboot.sh腳本會自動觸發執行,如下所示:

OpenResty 動態負載均衡

Consul+Lua 方式

上面的方式實現動態負載均衡在配置較多的時候會有一些問題,比如配置較多時,OpenResty重啟的速度就會變慢。所以通過Lua腳本的方式可以規避掉重啟這一步驟。

使用Lua實現時,與上面的組件相比Consul-templete就不需要了。通過Consul的http://127.0.0.1:8500/v1/catalog/service/moguhu_server接口就可以獲取到服務的列表,如下所示:

[
{
"ID": "5d452e6b-71e0-67ae-5169-c7e6342da53b",
"Node": "Jacks-MacBook-Air.local",
"Address": "127.0.0.1",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "127.0.0.1",
"wan": "127.0.0.1"
},
"NodeMeta": {
"consul-network-segment": ""
},
"ServiceID": "csi",
"ServiceName": "moguhu_server",
"ServiceTags": [
"dev"
],
"ServiceAddress": "127.0.0.1",
"ServiceMeta": {},
"ServicePort": 8081,
"ServiceEnableTagOverride": false,
"CreateIndex": 10,
"ModifyIndex": 10
},
{
"ID": "5d452e6b-71e0-67ae-5169-c7e6342da53b",
"Node": "Jacks-MacBook-Air.local",
"Address": "127.0.0.1",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "127.0.0.1",
"wan": "127.0.0.1"
},
"NodeMeta": {
"consul-network-segment": ""
},
"ServiceID": "oc",
"ServiceName": "moguhu_server",
"ServiceTags": [
"dev"
],
"ServiceAddress": "127.0.0.1",
"ServiceMeta": {},
"ServicePort": 8082,
"ServiceEnableTagOverride": false,
"CreateIndex": 12,
"ModifyIndex": 12
}
]

這一方式當中主要就是OpenResty裡面的相關配置。

OpenResty 配置

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/moguhu_server", sink = ltn12.sink.table(resp)
}
local resp = cjson.decode(resp)
local upstreams = {}
for i, v in ipairs(resp) do
upstreams[i+1] = {ip=v.Address, port=v.ServicePort}
end
ngx.shared.upstream_list:set("moguhu_server", cjson.encode(upstreams))
end
function _M:get_upstreams()
local upstreams_str = ngx.shared.upstream_list:get("moguhu_server")
return upstreams_str
end
return _M

nginx.conf

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 moguhu_server {
server 0.0.0.1; #佔位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))];
ngx.log(ngx.ERR, "current :=============", math.random(1, table.getn(tmp_upstreams)));
balancer.set_current_peer(ip_port.ip, ip_port.port);
}
}
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
proxy_pass http://moguhu_server;
}
}

上面通過balancer_by_lua_block去動態的設置了,upstream的服務器列表。然後啟動OpenResty就可以了。


參考:《億級流量網站架構核心技術》

相關推薦

推薦中...