• 动态负载均衡



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

    项目功能概述

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

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    共享配置数据

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    说明

    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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    服务管理接口

    我们还需要提供管理接口,来管理端口资源的 upstream 配置数据

    http {
        # openresty 管理接口
        server {
            listen 19000;
            location =/v1/upstream {
                # method = post 新建或者更新 upstream 服务组
                # method = delete 删除 upstream 服务组
            }
    
            location =/v1/parse {
                # method = post 新建或者更新 upstream 的流量解析 规则
                # method = delete 删除 upstream 的流量解析规则
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    服务总体配置信息

    根据以上分析,我们的服务配置数据如下所示

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145

    服务多实例管理

    因为我们的端口资源预分配的,既然是预分配,那么就会用耗尽的情况,针对这种情况,我们采用多实例来解决这个问题。当服务资源将要耗尽的时候,我们通过创建新的实例来申请更多的资源

    既然是多实例,那我们就需要一个代理服务来管理这些实例,这个代理服务主要提供一下功能

    • 代理 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 实例分布信息
  • 相关阅读:
    Rust权威指南之泛型、trait和生命周期
    【目标检测】6、SSD
    重磅 | 思特威获得ISO 26262:2018汽车功能安全ASIL D流程认证证书
    【Web前端】一文带你吃透CSS(中篇)
    【PAT(甲级)】1047 Student List for Course(超时问题)
    关于我用iVX沉浸式体验了一把0代码创建飞机大战这件事
    机械臂抓取的产业落地进展与思考
    Docker学习笔记
    PHP8中调换数组中的键值和元素值-PHP8知识详解
    偷梁换柱“Windows 11安装包”竟成了恶意程序?
  • 原文地址:https://blog.csdn.net/math_chao/article/details/126510327