动态负载均衡

本文介绍了如何针对特定需求开发一个轻量级的动态负载均衡项目,重点讨论了技术选型,包括openresty+go的组合、多协议支持、动态加载、协议解析规则和资源预分配。文章详细阐述了管理接口、服务配置、多实例管理和高可用性的实现策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


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

项目功能概述

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

  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 实例分布信息
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值