Consistent hashing to a subset of nodes. It works like consistent hash,
but instead of mapping to a single node, we map to a subset of nodes.
This commit is contained in:
parent
29118750be
commit
60b983503b
17 changed files with 434 additions and 17 deletions
|
|
@ -5,6 +5,7 @@ local dns_util = require("util.dns")
|
|||
local configuration = require("configuration")
|
||||
local round_robin = require("balancer.round_robin")
|
||||
local chash = require("balancer.chash")
|
||||
local chashsubset = require("balancer.chashsubset")
|
||||
local sticky = require("balancer.sticky")
|
||||
local ewma = require("balancer.ewma")
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ local DEFAULT_LB_ALG = "round_robin"
|
|||
local IMPLEMENTATIONS = {
|
||||
round_robin = round_robin,
|
||||
chash = chash,
|
||||
chashsubset = chashsubset,
|
||||
sticky = sticky,
|
||||
ewma = ewma,
|
||||
}
|
||||
|
|
@ -29,8 +31,12 @@ local function get_implementation(backend)
|
|||
|
||||
if backend["sessionAffinityConfig"] and backend["sessionAffinityConfig"]["name"] == "cookie" then
|
||||
name = "sticky"
|
||||
elseif backend["upstream-hash-by"] then
|
||||
name = "chash"
|
||||
elseif backend["upstreamHashByConfig"] and backend["upstreamHashByConfig"]["upstream-hash-by"] then
|
||||
if backend["upstreamHashByConfig"]["upstream-hash-by-subset"] then
|
||||
name = "chashsubset"
|
||||
else
|
||||
name = "chash"
|
||||
end
|
||||
end
|
||||
|
||||
local implementation = IMPLEMENTATIONS[name]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function _M.new(self, backend)
|
|||
local nodes = util.get_nodes(backend.endpoints)
|
||||
local o = {
|
||||
instance = self.factory:new(nodes),
|
||||
hash_by = backend["upstream-hash-by"],
|
||||
hash_by = backend["upstreamHashByConfig"]["upstream-hash-by"],
|
||||
traffic_shaping_policy = backend.trafficShapingPolicy,
|
||||
alternative_backends = backend.alternativeBackends,
|
||||
}
|
||||
|
|
|
|||
84
rootfs/etc/nginx/lua/balancer/chashsubset.lua
Normal file
84
rootfs/etc/nginx/lua/balancer/chashsubset.lua
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
-- Consistent hashing to a subset of nodes. Instead of returning the same node
|
||||
-- always, we return the same subset always.
|
||||
|
||||
local resty_chash = require("resty.chash")
|
||||
local util = require("util")
|
||||
|
||||
local _M = { name = "chashsubset" }
|
||||
|
||||
local function build_subset_map(backend)
|
||||
local endpoints = {}
|
||||
local subset_map = {}
|
||||
local subsets = {}
|
||||
local subset_size = backend["upstreamHashByConfig"]["upstream-hash-by-subset-size"]
|
||||
|
||||
for _, endpoint in pairs(backend.endpoints) do
|
||||
table.insert(endpoints, endpoint)
|
||||
end
|
||||
|
||||
local set_count = math.ceil(#endpoints/subset_size)
|
||||
local node_count = set_count * subset_size
|
||||
|
||||
-- if we don't have enough endpoints, we reuse endpoints in the last set to
|
||||
-- keep the same number on all of them.
|
||||
local j = 1
|
||||
for _ = #endpoints+1, node_count do
|
||||
table.insert(endpoints, endpoints[j])
|
||||
j = j+1
|
||||
end
|
||||
|
||||
local k = 1
|
||||
for i = 1, set_count do
|
||||
local subset = {}
|
||||
local subset_id = "set" .. tostring(i)
|
||||
for _ = 1, subset_size do
|
||||
table.insert(subset, endpoints[k])
|
||||
k = k+1
|
||||
end
|
||||
subsets[subset_id] = subset
|
||||
subset_map[subset_id] = 1
|
||||
end
|
||||
|
||||
return subset_map, subsets
|
||||
end
|
||||
|
||||
function _M.new(self, backend)
|
||||
local subset_map, subsets = build_subset_map(backend)
|
||||
|
||||
local o = {
|
||||
instance = resty_chash:new(subset_map),
|
||||
hash_by = backend["upstreamHashByConfig"]["upstream-hash-by"],
|
||||
subsets = subsets,
|
||||
current_endpoints = backend.endpoints
|
||||
}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
function _M.balance(self)
|
||||
local key = util.lua_ngx_var(self.hash_by)
|
||||
local subset_id = self.instance:find(key)
|
||||
local endpoints = self.subsets[subset_id]
|
||||
local endpoint = endpoints[math.random(#endpoints)]
|
||||
return endpoint.address .. ":" .. endpoint.port
|
||||
end
|
||||
|
||||
function _M.sync(self, backend)
|
||||
local subset_map
|
||||
|
||||
local changed = not util.deep_compare(self.current_endpoints, backend.endpoints)
|
||||
if not changed then
|
||||
return
|
||||
end
|
||||
|
||||
self.current_endpoints = backend.endpoints
|
||||
|
||||
subset_map, self.subsets = build_subset_map(backend)
|
||||
|
||||
self.instance:reinit(subset_map)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
@ -16,7 +16,7 @@ describe("Balancer chash", function()
|
|||
end
|
||||
|
||||
local backend = {
|
||||
name = "my-dummy-backend", ["upstream-hash-by"] = "$request_uri",
|
||||
name = "my-dummy-backend", upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri" },
|
||||
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } }
|
||||
}
|
||||
local instance = balancer_chash:new(backend)
|
||||
|
|
|
|||
82
rootfs/etc/nginx/lua/test/balancer/chashsubset_test.lua
Normal file
82
rootfs/etc/nginx/lua/test/balancer/chashsubset_test.lua
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
|
||||
local function get_test_backend(n_endpoints)
|
||||
local backend = {
|
||||
name = "my-dummy-backend",
|
||||
["upstreamHashByConfig"] = {
|
||||
["upstream-hash-by"] = "$request_uri",
|
||||
["upstream-hash-by-subset"] = true,
|
||||
["upstream-hash-by-subset-size"] = 3
|
||||
},
|
||||
endpoints = {}
|
||||
}
|
||||
|
||||
for i = 1, n_endpoints do
|
||||
backend.endpoints[i] = { address = "10.184.7." .. tostring(i), port = "8080", maxFails = 0, failTimeout = 0 }
|
||||
end
|
||||
|
||||
return backend
|
||||
end
|
||||
|
||||
describe("Balancer chash subset", function()
|
||||
local balancer_chashsubset = require("balancer.chashsubset")
|
||||
|
||||
describe("balance()", function()
|
||||
it("returns peers from the same subset", function()
|
||||
_G.ngx = { var = { request_uri = "/alma/armud" }}
|
||||
|
||||
local backend = get_test_backend(9)
|
||||
|
||||
local instance = balancer_chashsubset:new(backend)
|
||||
|
||||
instance:sync(backend)
|
||||
|
||||
local first_node = instance:balance()
|
||||
local subset_id
|
||||
local endpoint_strings
|
||||
|
||||
local function has_value (tab, val)
|
||||
for _, value in ipairs(tab) do
|
||||
if value == val then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
for id, endpoints in pairs(instance["subsets"]) do
|
||||
endpoint_strings = {}
|
||||
for _, endpoint in pairs(endpoints) do
|
||||
local endpoint_string = endpoint.address .. ":" .. endpoint.port
|
||||
table.insert(endpoint_strings, endpoint_string)
|
||||
if first_node == endpoint_string then
|
||||
-- found the set of first_node
|
||||
subset_id = id
|
||||
end
|
||||
end
|
||||
if subset_id then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- multiple calls to balance must return nodes from the same subset
|
||||
for i = 0, 10 do
|
||||
assert.True(has_value(endpoint_strings, instance:balance()))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
describe("new(backend)", function()
|
||||
it("fills last subset correctly", function()
|
||||
_G.ngx = { var = { request_uri = "/alma/armud" }}
|
||||
|
||||
local backend = get_test_backend(7)
|
||||
|
||||
local instance = balancer_chashsubset:new(backend)
|
||||
|
||||
instance:sync(backend)
|
||||
for id, endpoints in pairs(instance["subsets"]) do
|
||||
assert.are.equal(#endpoints, 3)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -32,7 +32,10 @@ local function reset_backends()
|
|||
sessionAffinityConfig = { name = "", cookieSessionAffinity = { name = "", hash = "" } },
|
||||
},
|
||||
{ name = "my-dummy-app-1", ["load-balance"] = "round_robin", },
|
||||
{ name = "my-dummy-app-2", ["load-balance"] = "round_robin", ["upstream-hash-by"] = "$request_uri", },
|
||||
{
|
||||
name = "my-dummy-app-2", ["load-balance"] = "chash",
|
||||
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", },
|
||||
},
|
||||
{
|
||||
name = "my-dummy-app-3", ["load-balance"] = "ewma",
|
||||
sessionAffinityConfig = { name = "cookie", cookieSessionAffinity = { name = "route", hash = "sha1" } }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue