最近组内有个动态负载均衡的项目,虽然目前开源的网关项目可以满足项目的需求,但是因为网关项目太大,有太多不需要的功能,而这个项目仅仅需要动态负载均衡功能,所以就尝试进行自己开发。
项目功能概述
本项目的定位的是动态负载均衡,主要的功能设计如下
- 动态加载
在不重启的服务(reload)的情况下,能够动态添加 upstream 服务组,动态添加请求到 upstream 服务组的解析规则 - 多协议支持
要能够支持 7 层、4 层负载均衡,7 层要能够支持 http、grpc 协议,4 层要能够支持 tcp、udp 协议 - 灵活 upstream 服务组解析规则
要能够支持通过请求port、 host、uri、header 等规则确定 upstream 服务组
技术选型和可行性论证
技术选型
因为本项目需求应该算是网关项目需求的子集,而目前常用的网关项目,比如 kong、apisix 等都是基于 openresty + go 这样的一个技术栈。
openresty 基于 nginx 的架构,提供非阻塞 + 同步的编程方式来支持丰富的功能开发需求,而 nginx 的架构和性能也是经过无数大型成熟项目的验证,并且本身定位也是作为反向代理来来使用
go 作为云原生时代的热门语言,特别适合这类中间件服务的开发,目前我们项目也考虑 openresty + go 这样一个技术选型
可行性论证
-
多协议支持
openresty 的 lua-nginx-module 模块提供了 7 层负载均衡的编程功能,stream-lua-nginx-module 模块提供了针对 4 层负载均衡的编程功能 -
动态加载
openresty 的 lua-nginx-module 模块和stream-lua-nginx-module 模块均提供了balancer_by_lua_block
命令,给我们开放了针对负载均衡 server 选择的编程能力syntax: balancer_by_lua_block { lua-script }
context: upstream
This directive runs Lua code as an upstream balancer for any upstream entities defined by the
upstream {}
configuration block.
upstream foo {
server 127.0.0.1;
balancer_by_lua_block {
-- use Lua to do something interesting here
-- as a dynamic balancer
local balancer = require “ngx.balancer”
balancer.set_more_tries(1)
#设置处理请求的 server
local ok, err = balancer.set_current_peer(host, port)
if not ok then
ngx.log(ngx.ERR, “failed to set the current peer: ”, err)
return ngx.exit(500)
end
}
}
server {
location / {
proxy_pass http://foo;
}
}
- 灵活 upstream 服务组解析规则
openresty 提供了丰富的 api (Lua_Nginx_API)使我们可以获取请求 host、port、uri、header,这样我们就可以根据这些信息确定请求upstream 服务组 - 无 reload 运行时动态解析
reload 实现 nginx 在不停止服务的情况下重新加载配置的能力。我们先来看下 nginx 的 reload 过程- master 进程接收到 reload 信号
- master 进程验证配置文件是否正确,如果配置文件正确,添加配置文件里面对新的端口资源的监听
- master 进程启动新的 worker 进程,这些 worker 也拥有 master 新监听的端口 socket 资源,然后开始处理新的请求
- master 进程向旧的 worker 进程发送优雅关闭信息,旧的 worker 优雅关闭
通过以上过程分析,我们知道 reload 过程中很重要的一件事情就是进行新的端口监听,并通过启动新的 worker 进程,来使 worker 进程拥有新的端口 socket 资源。我们很难在不 reload 的情况下进行新的端口监听。
而我们的项目要求在不进行 reload 的情况下,动态配置服务。在这种情况下,我们使用资源预分配来实现在无 reload 情况下动态配置服务,我们会先预分配一批端口资源,在运行过程中动态添加针对端口的解析
技术实现方案
预分配端口资源
通过上面分析,我们采用端口资源预分配的方案,来实现无 reload 动态配置服务,我们会预分配一批端口资源,在运行过程中动态添加针对端口的解析。
我们现将端口资源进行划分,不同的协议(不同层次的负载均衡)分别占用不同的端口范围。我们的端口资源划分如下
- http 协议:80, 10000 - 10999
- grpc 协议:11000 - 11999
- tcp 协议:12000 - 12999
- udp 协议:13000 - 13999
- 管理接口:19000 - 19099
说明
- 每个协议占用 1000 个端口范围,每个 Openresty 实例每种协议监听 100 个端口
- Openresty 提供 http、grpc、tcp、udp 的协议支持
- 我们还为管理接口分配 100 个端口,每个 Openresty 监听一个管理端口,通过管理端口提供动态配置 Openresty 的接口
这样,我们提供如下的 Openresty 配置:
# 7 层负载均衡配置
http {
# 配置 http upstream
upstream http_backend {
server 127.0.0.1;
keepalive 2000;
}
# 配置 http upstream
upstream grpc_backend {
server 127.0.0.1;
keepalive 2000;
}
# 为 http 服务预分配资源
server {
listen 80; #http 服务默认端口,公共 web 服务使用, 根据 host, header, uri 来分配 backend,
listen 10000; # http 服务预分配资源,根据 port 分配给申请方使用,分配方可以根据根据 host, header, uri 来分配 backend
listen 10001;
# ...
listen 10099;
location / {
proxy_pass http://http_backend;
}
}
# 为 grpc 服务预分配资源
server {
listen 11000 http2; # grpc 服务预分配资源,根据 port 分配给申请方使用
listen 11001 http2;
# ...
listen 11099 http2;
location / {
grpc_pass http://grpc_backend;
}
}
}
# 4 层负载均衡配置
stream {
upstream tcp_backend {
server 127.0.0.1;
}
upstream udp_backend {
server 127.0.0.1;
}
# tcp 负载均衡预分配资源
server {
# 不指定协议默认是TCP协议
listen 12000 so_keepalive=on;
listen 12001 so_keepalive=on;
#...
listen 12091 so_keepalive=on;
proxy_pass tcp_backend;
}
# udp 负载均衡预分配资源
server {
listen 13000 udp;
listen 13001 udp;
#...
listen 13099 udp;
proxy_pass udp_backend;
}
}
共享配置数据
Nginx 采用 master + worker 的进程模型,由多个 worker 来共同处理请求,所以多个 worker 进程之间需要共享 upstream 配置数据。我们通过共享内存来在worker 进程之间共享 upstream 配置数据
共享内存配置如下
lua_shared_dict upstream_dict 1m; # 配置共享内存,在不同 work进程之间共享 upstream 信息
init_by_lua_block { # master 进程初始化过程中调用,进行初始化工作,一般进行 lua 模块载入、初始化共享内存
# 初始化 upstream_dict 共享内存,可以从 redis 等存储初始化数据
local upstreamTable = ngx.shared[upstream_dict]
for _, upstreamInfo in pairs(httpUptreams) do
local key = "http_" .. upstreamInfo.port
upstreamTable[key] = upstreamInfo
end
for _, upstreamInfo in pairs(grpcUptreams) do
local key = "grpc_" .. upstreamInfo.port
upstreamTable[key] = upstreamInfo
end
for _, upstreamInfo in pairs(tcpUptreams) do
local key = "tcp_" .. upstreamInfo.port
upstreamTable[key] = upstreamInfo
end
for _, upstreamInfo in pairs(udp_Uptreams) do
local key = "udp_" .. upstreamInfo.port
upstreamTable[key] = upstreamInfo
end
}
init_worker_by_lua_block { # work 进程初始化调用
}
说明
- 如果存在以
协议+端口
为 key 存储数据,当存在某个端口的解析数据,说明此端口被使用,可以解析;否在端口未被使用,不能解析和访问 -
upstreamInfo
为解析配置信息,里面包括了这组解析的信息,包括 upstreams 的 services、负载均衡算法等
动态负载均衡
我们通过 openresty 的提供的 balancer_by_lua_block 指令,来为请求动态选择 server,以实现动态负载均衡功能。我们先从共享内存里面取出来此端口的 upstream 配置信息,然后根据请求的信息和配置信息来为请求选择 server
我们以 http 服务为例,动态负载均衡配置如下
# 7 层负载均衡配置
http {
# 配置 http upstream
upstream http_backend {
server 127.0.0.1;
keepalive 2000; # 需要配置 keepalive https://xiaorui.cc/archives/5970
balancer_by_lua_block {
local balancer = require "ngx.balancer"
local upstreamTable = ngx.shared[upstream_dict]
local port = ngx.var.server_port
local key = "http_" .. port
local uptreamInfo = upstreamTable[key]
if upstreamInfo == nil{
ngx.say("not allow")
ngx.log(ngx.ERR, "port not allow")
return ngx.exit(403)
}
# 根据 host, uri, host 确定 server
local headers = ngx.req.get_headers() # 获取 header 信息,也可以获取其他请求信息,作为选择 uptream 的依据
local uri = ngx.var.request_uri
# 从 upstreamInfo 中选出处理请求的 service
local server
balancer.set_more_tries(1)
#设置处理请求的 server
local ok, err = balancer.set_current_peer(server.host, server.port)
if not ok then
ngx.log(ngx.ERR, “failed to set the current peer: ”, err)
return ngx.exit(500)
end
}
}
# 为 http 服务预分配资源
server {
listen 80; #http 服务默认端口,公共 web 服务使用, 根据 host, header, uri 来分配 backend,
listen 10000; # http 服务预分配资源,根据 port 分配给申请方使用,分配方可以根据根据 host, header, uri 来分配 backend
listen 10001;
# ...
listen 10099;
location / {
proxy_pass http://http_backend;
}
}
}
服务管理接口
我们还需要提供管理接口,来管理端口资源的 upstream 配置数据
http {
# openresty 管理接口
server {
listen 19000;
location =/v1/upstream {
# method = post 新建或者更新 upstream 服务组
# method = delete 删除 upstream 服务组
}
location =/v1/parse {
# method = post 新建或者更新 upstream 的流量解析 规则
# method = delete 删除 upstream 的流量解析规则
}
}
}
服务总体配置信息
根据以上分析,我们的服务配置数据如下所示
lua_shared_dict upstream_dict 1m; # 配置共享内存,在不同 work进程之间共享 upstream 信息
init_by_lua_block { # master 进程初始化过程中调用,进行初始化工作,一般进行 lua 模块载入、初始化共享内存
# 初始化 upstream_dict 共享内存,可以从 redis 等存储初始化数据
local upstreamTable = ngx.shared[upstream_dict]
for _, upstreamInfo in pairs(httpUptreams) do
local key = "http_" .. upstreamInfo.port
upstreamTable[key] = upstreamInfo
end
for _, upstreamInfo in pairs(grpcUptreams) do
local key = "grpc_" .. upstreamInfo.port
upstreamTable[key] = upstreamInfo
end
for _, upstreamInfo in pairs(tcpUptreams) do
local key = "tcp_" .. upstreamInfo.port
upstreamTable[key] = upstreamInfo
end
for _, upstreamInfo in pairs(udp_Uptreams) do
local key = "udp_" .. upstreamInfo.port
upstreamTable[key] = upstreamInfo
end
}
init_worker_by_lua_block { # work 进程初始化调用
}
# 7 层负载均衡配置
http {
# 配置 http upstream
upstream http_backend {
server 127.0.0.1;
keepalive 2000; # 需要配置 keepalive https://xiaorui.cc/archives/5970
balancer_by_lua_block {
local balancer = require "ngx.balancer"
local upstreamTable = ngx.shared[upstream_dict]
local port = ngx.var.server_port
local key = "http_" .. port
local uptreamInfo = upstreamTable[key]
if upstreamInfo == nil{
ngx.say("not allow")
ngx.log(ngx.ERR, "port not allow")
return ngx.exit(403)
}
local headers = ngx.req.get_headers() # 获取 header 信息,也可以获取其他请求信息,作为选择 uptream 的依据
local uri = ngx.var.request_uri
# 根据 host, uri, host 确定 server
balancer.set_more_tries(1)
#设置处理请求的 server
local ok, err = balancer.set_current_peer(host, port)
if not ok then
ngx.log(ngx.ERR, “failed to set the current peer: ”, err)
return ngx.exit(500)
end
}
}
# 配置 http upstream
upstream grpc_backend {
server 127.0.0.1;
keepalive 2000; # 需要配置 keepalive https://xiaorui.cc/archives/5970
balancer_by_lua_block {
# 此处会根据请求 host, port, header, uri 等信息和初始化共享变量的配置,设置处理请求的 server
# use Lua to do something interesting here
# as a dynamic balancer
}
}
# 为 http 服务预分配资源
server {
listen 80; #http 服务默认端口,公共 web 服务使用, 根据 host, header, uri 来分配 backend,
listen 10000; # http 服务预分配资源,根据 port 分配给申请方使用,分配方可以根据根据 host, header, uri 来分配 backend
listen 10001;
# ...
listen 10099;
location / {
proxy_pass http://http_backend;
}
}
# 为 grpc 服务预分配资源
server {
listen 11000 http2; # grpc 服务预分配资源,根据 port 分配给申请方使用
listen 11001 http2;
# ...
listen 11099 http2;
location / {
grpc_pass http://grpc_backend;
}
}
# openresty 管理接口
server {
listen 19000;
location =/v1/upstream {
# method = post 新建或者更新 upstream 服务组
# method = delete 删除 upstream 服务组
}
location =/v1/parse {
# method = post 新建或者更新 upstream 的流量解析 规则
# method = delete 删除 upstream 的流量解析规则
}
}
}
# 4 层负载均衡配置
stream {
upstream tcp_backend {
server 127.0.0.1;
balancer_by_lua_block{
# use Lua to do something interesting here
# as a dynamic balancer
}
}
upstream udp_backend {
server 127.0.0.1;
balancer_by_lua_block{
# use Lua to do something interesting here
# as a dynamic balancer
}
}
# tcp 负载均衡预分配资源
server {
# 不指定协议默认是TCP协议
listen 12000 so_keepalive=on;
listen 12001 so_keepalive=on;
#...
listen 12091 so_keepalive=on;
proxy_pass tcp_backend;
}
# udp 负载均衡预分配资源
server {
listen 13000 udp;
listen 13001 udp;
#...
listen 13099 udp;
proxy_pass udp_backend;
}
}
服务多实例管理
因为我们的端口资源预分配的,既然是预分配,那么就会用耗尽的情况,针对这种情况,我们采用多实例来解决这个问题。当服务资源将要耗尽的时候,我们通过创建新的实例来申请更多的资源
既然是多实例,那我们就需要一个代理服务来管理这些实例,这个代理服务主要提供一下功能
- 代理 openresty 的管理接口
- 管理已经分配和未分配的端口信息
- 新建、删除 openresty 实例
这样,我们的服务架构,将变成如下所示
服务高可用管理
作为一个负载均衡服务,提供高可用功能是非常重要的,所以我们的服务还需提供集群管理功能,并将 Openresty 实例进行备份,然后对 Openresty 实例及其备份实例进行调度,将其均匀分配到集群不同机器上
对 Openresty 进行主备管理,那么主备管理将是一件非常重要的事情。我们采用 keepalived 进行服务实例主备管理。
keepalived基于VRRP协议来实现高可用,主要用作realserver的健康检查以及负载均衡主机和backup主机之间的故障漂移。它主要工作在 ip 层,通过虚拟 ip 和 mac 地址来虚拟出对外提供服务的 ip ,当发证故障需要转移,通过 arp 协议,来通知主机更新各个主机 arp 信息(ip 和 mac 地址对应关系)
既然要提供集群管理功能,所以我们还需要一个 guilder 服务,来对集群进行管理,并提供服务的对外管理接口
到目前为止,服务架构图如下
Guilder 服务主要提供一下功能
- 管理主机信息,将主机加入集群,或者从集群中删除主机
- 协调管理 openresty 实例,通过调用 agent 接口,保证 openresty 实例均匀分布到不同机器上
- 管理 openresty 的 upstream 和 upstream 对应的解析等信息
- 管理端口分配、分布信息,openresty 实例分布信息
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。