Skip to content

Commit

Permalink
perf(clustering) improve calculate hash performance by utilizing stri…
Browse files Browse the repository at this point in the history
…ng buffer and tablepool
  • Loading branch information
bungle committed Jul 8, 2022
1 parent 1ad3b5d commit 66543c3
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 110 deletions.
209 changes: 113 additions & 96 deletions kong/clustering/config_helper.lua
Original file line number Diff line number Diff line change
@@ -1,132 +1,147 @@
local constants = require("kong.constants")
local declarative = require("kong.db.declarative")
local tablepool = require("tablepool")
local isempty = require("table.isempty")
local isarray = require("table.isarray")
local nkeys = require("table.nkeys")
local buffer = require("string.buffer")


local tostring = tostring
local assert = assert
local type = type
local error = error
local pairs = pairs
local ipairs = ipairs
local concat = table.concat
local sort = table.sort

local isempty = require("table.isempty")
local isarray = require("table.isarray")
local nkeys = require("table.nkeys")
local new_tab = require("table.new")

local yield = require("kong.tools.utils").yield
local fetch_table = tablepool.fetch
local release_table = tablepool.release


local ngx_log = ngx.log
local ngx_null = ngx.null
local ngx_md5 = ngx.md5
local ngx_md5_bin = ngx.md5_bin


local ngx_DEBUG = ngx.DEBUG


local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH
local _log_prefix = "[clustering] "


local _M = {}


local function to_sorted_string(value)
local function to_sorted_string(value, o)
yield(true)

if value == ngx_null then
return "/null/"
end

local t = type(value)
if t == "string" or t == "number" then
return value
end

if t == "boolean" then
return tostring(value)
if #o > 1000000 then
o:set(ngx_md5_bin(o:tostring()))
end

if t == "table" then
if isempty(value) then
return "{}"
end

if isarray(value) then
local count = #value
if count == 1 then
return to_sorted_string(value[1])
end

if count == 2 then
return to_sorted_string(value[1]) .. ";" ..
to_sorted_string(value[2])
end

if count == 3 then
return to_sorted_string(value[1]) .. ";" ..
to_sorted_string(value[2]) .. ";" ..
to_sorted_string(value[3])
end

if count == 4 then
return to_sorted_string(value[1]) .. ";" ..
to_sorted_string(value[2]) .. ";" ..
to_sorted_string(value[3]) .. ";" ..
to_sorted_string(value[4])
end

if count == 5 then
return to_sorted_string(value[1]) .. ";" ..
to_sorted_string(value[2]) .. ";" ..
to_sorted_string(value[3]) .. ";" ..
to_sorted_string(value[4]) .. ";" ..
to_sorted_string(value[5])
end

local i = 0
local o = new_tab(count < 100 and count or 100, 0)
for j = 1, count do
i = i + 1
o[i] = to_sorted_string(value[j])
if value == ngx_null then
o:put("/null/")

else
local t = type(value)
if t == "string" or t == "number" then
o:put(value)

elseif t == "boolean" then
o:put(tostring(value))

elseif t == "table" then
if isempty(value) then
o:put("{}")

elseif isarray(value) then
local count = #value
if count == 1 then
to_sorted_string(value[1], o)
elseif count == 2 then
to_sorted_string(value[1], o)
o:put(";")
to_sorted_string(value[2], o)

elseif count == 3 then
to_sorted_string(value[1], o)
o:put(";")
to_sorted_string(value[2], o)
o:put(";")
to_sorted_string(value[3], o)

elseif count == 4 then
to_sorted_string(value[1], o)
o:put(";")
to_sorted_string(value[2], o)
o:put(";")
to_sorted_string(value[3], o)
o:put(";")
to_sorted_string(value[4], o)

elseif count == 5 then
to_sorted_string(value[1], o)
o:put(";")
to_sorted_string(value[2], o)
o:put(";")
to_sorted_string(value[3], o)
o:put(";")
to_sorted_string(value[4], o)
o:put(";")
to_sorted_string(value[5], o)

else
for i = 1, count do
to_sorted_string(value[i], o)
o:put(";")
end
end

if j % 100 == 0 then
i = 1
o[i] = ngx_md5_bin(concat(o, ";", 1, 100))
else
local count = nkeys(value)
local keys = fetch_table("hash-calc", count, 0)
local i = 0
for k in pairs(value) do
i = i + 1
keys[i] = k
end
end

return ngx_md5_bin(concat(o, ";", 1, i))
sort(keys)

else
local count = nkeys(value)
local keys = new_tab(count, 0)
local i = 0
for k in pairs(value) do
i = i + 1
keys[i] = k
end

sort(keys)
for i = 1, count do
o:put(keys[i])
o:put(":")
to_sorted_string(value[keys[i]], o)
o:put(";")
end

local o = new_tab(count, 0)
for i = 1, count do
o[i] = keys[i] .. ":" .. to_sorted_string(value[keys[i]])
release_table("hash-calc", keys)
end

value = concat(o, ";", 1, count)

return #value > 512 and ngx_md5_bin(value) or value
end -- isarray
else
error("invalid type to be sorted (JSON types are supported)")
end
end
end

end -- table
local function calculate_hash(input, o)
if input == nil then
return DECLARATIVE_EMPTY_CONFIG_HASH
end

error("invalid type to be sorted (JSON types are supported)")
o:reset()
to_sorted_string(input, o)
return ngx_md5(o:tostring())
end


local function calculate_config_hash(config_table)
local o = buffer.new()
if type(config_table) ~= "table" then
local config_hash = ngx_md5(to_sorted_string(config_table))
local config_hash = calculate_hash(config_table, o)
return config_hash, { config = config_hash, }
end

Expand All @@ -136,23 +151,25 @@ local function calculate_config_hash(config_table)
local upstreams = config_table.upstreams
local targets = config_table.targets

local routes_hash = routes and ngx_md5(to_sorted_string(routes)) or DECLARATIVE_EMPTY_CONFIG_HASH
local services_hash = services and ngx_md5(to_sorted_string(services)) or DECLARATIVE_EMPTY_CONFIG_HASH
local plugins_hash = plugins and ngx_md5(to_sorted_string(plugins)) or DECLARATIVE_EMPTY_CONFIG_HASH
local upstreams_hash = upstreams and ngx_md5(to_sorted_string(upstreams)) or DECLARATIVE_EMPTY_CONFIG_HASH
local targets_hash = targets and ngx_md5(to_sorted_string(targets)) or DECLARATIVE_EMPTY_CONFIG_HASH
local routes_hash = calculate_hash(routes, o)
local services_hash = calculate_hash(services, o)
local plugins_hash = calculate_hash(plugins, o)
local upstreams_hash = calculate_hash(upstreams, o)
local targets_hash = calculate_hash(targets, o)

config_table.routes = nil
config_table.services = nil
config_table.plugins = nil
config_table.upstreams = nil
config_table.targets = nil

local config_hash = ngx_md5(to_sorted_string(config_table) .. routes_hash
.. services_hash
.. plugins_hash
.. upstreams_hash
.. targets_hash)
local rest_hash = calculate_hash(config_table, o)
local config_hash = ngx_md5(routes_hash ..
services_hash ..
plugins_hash ..
upstreams_hash ..
targets_hash ..
rest_hash)

config_table.routes = routes
config_table.services = services
Expand Down
29 changes: 15 additions & 14 deletions spec/01-unit/19-hybrid/02-clustering_spec.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
local calculate_config_hash = require("kong.clustering.config_helper").calculate_config_hash


local DECLARATIVE_EMPTY_CONFIG_HASH = require("kong.constants").DECLARATIVE_EMPTY_CONFIG_HASH


describe("kong.clustering", function()
describe(".calculate_config_hash()", function()
it("calculating hash for nil errors", function()
local pok = pcall(calculate_config_hash, nil)
assert.falsy(pok)
it("calculating hash for nil", function()
local hash = calculate_config_hash(nil)
assert.equal(DECLARATIVE_EMPTY_CONFIG_HASH, hash)
end)

it("calculates hash for null", function()
Expand Down Expand Up @@ -167,13 +170,13 @@ describe("kong.clustering", function()
for _ = 1, 10 do
local hash = calculate_config_hash(value)
assert.is_string(hash)
assert.equal("aaf38faf0b5851d711027bb4d812d50d", hash)
assert.equal("2da3c26e4dbd6dd6ea3ab60a3dd7fb3e", hash)
end

for _ = 1, 10 do
local hash = calculate_config_hash(value)
assert.is_string(hash)
assert.equal("aaf38faf0b5851d711027bb4d812d50d", hash)
assert.equal("2da3c26e4dbd6dd6ea3ab60a3dd7fb3e", hash)
end
end)

Expand Down Expand Up @@ -204,24 +207,22 @@ describe("kong.clustering", function()
for _ = 1, 10 do
local hash = calculate_config_hash(value)
assert.is_string(hash)
assert.equal("cb83c48d5b2932d1bc9d13672b433365", hash)
assert.equal("675ff4b6b1c8cefa5198284110786ad0", hash)
assert.equal(h, hash)
end
end)

describe("granular hashes", function()
local DECLARATIVE_EMPTY_CONFIG_HASH = require("kong.constants").DECLARATIVE_EMPTY_CONFIG_HASH

it("filled with empty hash values for missing config fields", function()
local value = {}

for _ = 1, 10 do
local hash, hashes = calculate_config_hash(value)
assert.is_string(hash)
assert.equal("aaf38faf0b5851d711027bb4d812d50d", hash)
assert.equal("2da3c26e4dbd6dd6ea3ab60a3dd7fb3e", hash)
assert.is_table(hashes)
assert.same({
config = "aaf38faf0b5851d711027bb4d812d50d",
config = "2da3c26e4dbd6dd6ea3ab60a3dd7fb3e",
routes = DECLARATIVE_EMPTY_CONFIG_HASH,
services = DECLARATIVE_EMPTY_CONFIG_HASH,
plugins = DECLARATIVE_EMPTY_CONFIG_HASH,
Expand All @@ -241,10 +242,10 @@ describe("kong.clustering", function()
for _ = 1, 10 do
local hash, hashes = calculate_config_hash(value)
assert.is_string(hash)
assert.equal("768533baebe6e0d46de8d5f8a0c05bf0", hash)
assert.equal("81aac97a81ad03c0cf0b36663806488e", hash)
assert.is_table(hashes)
assert.same({
config = "768533baebe6e0d46de8d5f8a0c05bf0",
config = "81aac97a81ad03c0cf0b36663806488e",
routes = "99914b932bd37a50b983c5e7c90ae93b",
services = "99914b932bd37a50b983c5e7c90ae93b",
plugins = "99914b932bd37a50b983c5e7c90ae93b",
Expand All @@ -261,10 +262,10 @@ describe("kong.clustering", function()
for _ = 1, 10 do
local hash, hashes = calculate_config_hash(value)
assert.is_string(hash)
assert.equal("6c5fb69169a0fabb24dcfa3a5d7a14b0", hash)
assert.equal("4d60dd9998ea4314039da5ca2f1db96f", hash)
assert.is_table(hashes)
assert.same({
config = "6c5fb69169a0fabb24dcfa3a5d7a14b0",
config = "4d60dd9998ea4314039da5ca2f1db96f",
routes = DECLARATIVE_EMPTY_CONFIG_HASH,
services = DECLARATIVE_EMPTY_CONFIG_HASH,
plugins = DECLARATIVE_EMPTY_CONFIG_HASH,
Expand Down

0 comments on commit 66543c3

Please sign in to comment.