动态负载均衡


最近组内有个动态负载均衡的项目,虽然目前开源的网关项目可以满足项目的需求,但是因为网关项目太大,有太多不需要的功能,而这个项目仅仅需要动态负载均衡功能,所以就尝试进行自己开发。

项目功能概述

本项目的定位的是动态负载均衡,主要的功能设计如下

  1. 动态加载
    在不重启的服务(reload)的情况下,能够动态添加 upstream 服务组,动态添加请求到 upstream 服务组的解析规则
  2. 多协议支持
    要能够支持 7 层、4 层负载均衡,7 层要能够支持 http、grpc 协议,4 层要能够支持 tcp、udp 协议
  3. 灵活 upstream 服务组解析规则
    要能够支持通过请求port、 host、uri、header 等规则确定 upstream 服务组

技术选型和可行性论证

技术选型

因为本项目需求应该算是网关项目需求的子集,而目前常用的网关项目,比如 kong、apisix 等都是基于 openresty + go 这样的一个技术栈。

openresty 基于 nginx 的架构,提供非阻塞 + 同步的编程方式来支持丰富的功能开发需求,而 nginx 的架构和性能也是经过无数大型成熟项目的验证,并且本身定位也是作为反向代理来来使用

go 作为云原生时代的热门语言,特别适合这类中间件服务的开发,目前我们项目也考虑 openresty + go 这样一个技术选型

可行性论证

  1. 多协议支持
    openresty 的 lua-nginx-module 模块提供了 7 层负载均衡的编程功能,stream-lua-nginx-module 模块提供了针对 4 层负载均衡的编程功能

  2. 动态加载
    openresty 的 lua-nginx-module 模块和stream-lua-nginx-module 模块均提供了 balancer_by_lua_block 命令,给我们开放了针对负载均衡 server 选择的编程能力

    balancer_by_lua_block

    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;
     }
 }
  1. 灵活 upstream 服务组解析规则
    openresty 提供了丰富的 api (Lua_Nginx_API)使我们可以获取请求 host、port、uri、header,这样我们就可以根据这些信息确定请求upstream 服务组
  2. 无 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

说明

  1. 每个协议占用 1000 个端口范围,每个 Openresty 实例每种协议监听 100 个端口
  2. Openresty 提供 http、grpc、tcp、udp 的协议支持
  3. 我们还为管理接口分配 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 进程初始化调用

}

说明

  1. 如果存在以协议+端口为 key 存储数据,当存在某个端口的解析数据,说明此端口被使用,可以解析;否在端口未被使用,不能解析和访问
  2. 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 服务主要提供一下功能

  1. 管理主机信息,将主机加入集群,或者从集群中删除主机
  2. 协调管理 openresty 实例,通过调用 agent 接口,保证 openresty 实例均匀分布到不同机器上
  3. 管理 openresty 的 upstream 和 upstream 对应的解析等信息
  4. 管理端口分配、分布信息,openresty 实例分布信息

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340