OpenResty基于ID负载均衡

一、前言:为什么需要"基于 ID"的负载均衡?

在传统轮询(round-robin)负载均衡下:

  • 用户 A 的请求可能被分发到 Tomcat-1
  • 下次请求却到了 Tomcat-2

这会导致严重问题:

  • 本地缓存失效(如用户会话、订单状态)
  • 数据库连接池浪费
  • 无法实现"同 ID 同实例"业务逻辑

解决方案:基于用户 ID、订单 ID 等关键字段做哈希路由

本文将教你用 OpenResty 实现高性能、可扩展、支持动态扩缩容的 ID 维度负载均衡。


二、核心方案对比

方案 原理 优点 缺点
ip_hash 按客户端 IP 哈希 简单 移动端 NAT 下所有用户同 IP
Cookie Sticky Set-Cookie 绑定实例 通用 依赖浏览器,API 不友好
一致性哈希(推荐) 按指定 ID 哈希 精准、扩容影响小 需提取 ID

结论一致性哈希 + 业务 ID = 最佳实践


三、OpenResty 实现一致性哈希负载均衡

3.1 基础配置:静态 upstream

假设后端有 3 个 Tomcat 实例:

Lua 复制代码
upstream backend_servers {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

3.2 Lua 实现 ID 提取 + 哈希路由

Lua 复制代码
location /api/ {
    access_by_lua_block {
        -- 1. 从请求中提取 ID(支持多种方式)
        local id = nil

        -- 方式1: 从 URL 路径 /api/user/1001
        if ngx.var.uri:match("/api/user/(%d+)") then
            id = ngx.var.uri:match("/api/user/(%d+)")
        end

        -- 方式2: 从 GET 参数 ?userId=1001
        if not id then
            local args = ngx.req.get_uri_args()
            id = args.userId or args.orderId
        end

        -- 方式3: 从 POST JSON body(需读取 body)
        if not id and ngx.req.get_method() ~= "GET" then
            ngx.req.read_body()
            local body = ngx.req.get_body_data()
            if body then
                local cjson = require "cjson.safe"
                local data = cjson.decode(body)
                id = data and (data.userId or data.orderId)
            end
        end

        -- 2. 若未提取到 ID,回退到轮询
        if not id then
            return
        end

        -- 3. 一致性哈希选择 upstream 节点
        local servers = {
            "192.168.1.10:8080",
            "192.168.1.11:8080",
            "192.168.1.12:8080"
        }

        -- 使用 CRC32 哈希(高效且分布均匀)
        local crc32 = require "ngx.crc32"
        local hash = crc32.str(id)
        local index = hash % #servers + 1
        local target = servers[index]

        -- 4. 设置代理地址
        ngx.var.backend_target = target
    }

    # 动态 proxy_pass
    set $backend_target "";
    proxy_pass http://$backend_target;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

关键点

  • 使用 ngx.crc32(OpenResty 内置,高性能)
  • 支持多来源 ID 提取
  • 无 ID 时自动降级轮询

四、进阶:使用 resty.chash 实现标准一致性哈希

OpenResty 官方提供更强大的模块 `lua-resty-chash`,支持虚拟节点,减少扩容抖动。

4.1 安装(通常已内置)

bash 复制代码
opm get openresty/chash

4.2 配置示例

Lua 复制代码
http {
    lua_shared_dict chash_dict 1m;  # 共享内存存储环

    init_worker_by_lua_block {
        local chash = require "resty.chash"
        local nodes = {
            { host = "192.168.1.10", port = 8080, weight = 100 },
            { host = "192.168.1.11", port = 8080, weight = 100 },
            { host = "192.168.1.12", port = 8080, weight = 100 }
        }

        -- 创建一致性哈希环(带 160 个虚拟节点)
        local ring = chash.new(nodes, { dict = "chash_dict", replicas = 160 })
        package.loaded.ring = ring  -- 全局共享
    }

    server {
        location /api/ {
            content_by_lua_block {
                local id = ngx.var.arg_userId or "default"

                local ring = package.loaded.ring
                local host, port = ring:lookup(id)

                if host then
                    local res = ngx.location.capture("/proxy/" .. host .. "/" .. port, {
                        method = ngx.req.get_method(),
                        body = ngx.req.get_body_data(),
                        args = ngx.var.args
                    })
                    ngx.status = res.status
                    ngx.print(res.body)
                else
                    ngx.exit(502)
                end
            }
        }

        # 内部 location 用于代理
        location ~ "^/proxy/([^/]+)/(.+)$" {
            internal;
            proxy_pass http://$1:$2;
        }
    }
}

🔑 优势

  • 扩容时仅 1/N 的 key 重新映射(N=节点数)
  • 支持权重(weight)
  • 虚拟节点提升分布均匀性

五、ID 提取策略建议

场景 推荐提取方式
RESTful API 从 URI 路径(如 /user/{id}
Web 表单 从 POST 参数
JSON API 从 body 解析(需 read_body
移动端 从 Header(如 X-User-ID
Lua 复制代码
-- 通用 ID 提取函数(封装)
local function extract_id()
    -- 优先级:Header > Path > Query > Body
    local id = ngx.req.get_headers()["x-user-id"]
    if id then return id end

    if ngx.var.uri:match("/api/[^/]+/(%d+)") then
        return ngx.var.uri:match("/api/[^/]+/(%d+)")
    end

    local args = ngx.req.get_uri_args()
    id = args.userId or args.orderId
    if id then return id end

    if ngx.req.get_method() ~= "GET" then
        ngx.req.read_body()
        local body = ngx.req.get_body_data()
        if body then
            local cjson = require "cjson.safe"
            local data = cjson.decode(body)
            return data and (data.userId or data.orderId)
        end
    end

    return nil
end

六、生产环境注意事项

✅ 必做配置

Lua 复制代码
# nginx.conf
http {
    # 防止 body 过大
    client_max_body_size 1m;

    # 超时设置
    proxy_connect_timeout 1s;
    proxy_send_timeout 2s;
    proxy_read_timeout 2s;

    # resolver(若使用域名)
    resolver 8.8.8.8 valid=30s;
}

⚠️ 扩容影响评估

  • 使用普通哈希(crc32 % N):扩容后 100% 请求重分布
  • 使用一致性哈希:扩容后仅 ~33%(3 节点 → 4 节点)重分布

💡 建议直接使用 resty.chash,避免数据抖动


七、监控与调试

日志记录路由结果

Lua 复制代码
ngx.log(ngx.INFO, "Route user_id=", id, " to ", target)

暴露调试接口

Lua 复制代码
location /debug/route {
    content_by_lua_block {
        local id = ngx.var.arg_id
        if not id then
            ngx.say("Usage: ?id=123")
            return
        end

        -- 执行路由逻辑...
        ngx.say("Routed to: ", target)
    }
}

八、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
難釋懷1 天前
OpenResty-CJSON工具类
junit·openresty
難釋懷1 天前
OpenResty封装http工具
http·junit·openresty
he___H1 天前
Nginx+lua+openresty
nginx·lua·openresty
cool32001 天前
二进制基于kubeasz部署 K8s 1.34.x 高可用集群实战指南-第二章:HAProxy + Keepalived负载均衡高可用配置(2-4)
容器·k8s·负载均衡
刘~浪地球1 天前
Nginx + Tomcat 整合实战(三):负载均衡与集群部署
nginx·tomcat·负载均衡
人间打气筒(Ada)1 天前
「码动四季·开源同行」golang:负载均衡如何提高系统可用性?
算法·golang·开源·go·负载均衡·负载均衡算法
无名-CODING2 天前
SpringCloud 服务调用与负载均衡:OpenFeign 极简使用教程
spring·spring cloud·负载均衡
wydaicls3 天前
什么时候触发负载均衡(kernel 6.12)
运维·负载均衡
難釋懷3 天前
OpenResty查询Tomcat
tomcat·firefox·openresty