feat: Add multi-target load balancing support

Fixes #47.
- Added lua-resty-balancer to dependencies (Dockerfile & install.sh).
- Added 'targets' field to the Host model to hold additional targets.
- Updated the UI to allow inputting additional targets (IP:port).
- Updated targetinfo.lua to parse the additional targets and load balance between them and the primary target using resty.balancer.round_robin.
This commit is contained in:
2026-07-21 00:48:43 -04:00
parent 21e295615b
commit 4321826dc8
6 changed files with 53 additions and 0 deletions
+1
View File
@@ -123,6 +123,7 @@ apt-get install -y nodejs openresty
echo "==> Lua modules"
luarocks install lua-resty-auto-ssl
luarocks install luasocket
luarocks install lua-resty-balancer
# CIDR matcher for the per-host IP allow/deny lists (hostfeatures.lua).
# resty.limit.req is bundled with OpenResty, so no rock is needed for it.
luarocks install lua-resty-ipmatcher
+40
View File
@@ -62,6 +62,7 @@ function M.get(ngx, domain, targetInfo)
local json = require "cjson"
local redis = require "resty.redis"
local round_robin = require "resty.balancer.round_robin"
if not domain then
return nil, 499
@@ -95,6 +96,45 @@ function M.get(ngx, domain, targetInfo)
return nil, 406
end
-- Load balancing
local target_list = {}
table.insert(target_list, res["ip"] .. ":" .. tostring(res["targetPort"]))
if res["targets"] and res["targets"] ~= "" and res["targets"] ~= "[]" then
local decodeOk, decodedTargets = pcall(json.decode, res["targets"])
if decodeOk and type(decodedTargets) == "table" then
for _, t in ipairs(decodedTargets) do
table.insert(target_list, t)
end
end
end
if #target_list > 1 then
if not M.host_balancers then M.host_balancers = {} end
local cache_key = domain .. "_" .. (res["updated_on"] or "0")
if not M.host_balancers[domain] or M.host_balancers[domain].key ~= cache_key then
local b = round_robin:new()
local nodes = {}
for _, t in ipairs(target_list) do
nodes[t] = 1
end
b:reinit(nodes)
M.host_balancers[domain] = { b = b, key = cache_key }
end
local peer = M.host_balancers[domain].b:find()
if peer then
local colon = peer:find(":")
if colon then
res["ip"] = peer:sub(1, colon - 1)
res["targetPort"] = peer:sub(colon + 1)
else
res["ip"] = peer
end
end
end
ngx.ctx.targetInfo = res
-- Remember which host this target was resolved for, so the reuse guard at
-- the top can tell a genuine cache hit from a coalesced request for a