Files
2026-07-13 12:32:21 +08:00

253 lines
6.5 KiB
Lua

--------------------------------------------------------------------------
-- ewma balancer algorithm
--
-- Original Authors: Shiv Nagarajan & Scott Francis
-- Accessed: March 12, 2018
-- Inspiration drawn from:
-- https://github.com/twitter/finagle/blob/1bc837c4feafc0096e43c0e98516a8e1c50c4421
-- /finagle-core/src/main/scala/com/twitter/finagle/loadbalancer/PeakEwma.scala
local balancers = require "kong.runloop.balancer.balancers"
local pairs = pairs
local ipairs = ipairs
local math = math
local math_exp = math.exp
local ngx_now = ngx.now
local ngx_log = ngx.log
local ngx_WARN = ngx.WARN
local ngx_DEBUG = ngx.DEBUG
local table_nkeys = table.nkeys
local table_clear = table.clear
local table_insert = table.insert
local DECAY_TIME = 10 -- this value is in seconds
local PICK_SET_SIZE = 2
local new_addresses = {}
local ewma = {}
ewma.__index = ewma
local function decay_ewma(ewma, last_touched_at, rtt, now)
local td = now - last_touched_at
td = (td > 0) and td or 0
local weight = math_exp(-td / DECAY_TIME)
ewma = ewma * weight + rtt * (1.0 - weight)
return ewma
end
-- slow_start_ewma is something we use to avoid sending too many requests
-- to the newly introduced endpoints. We currently use average ewma values
-- of existing endpoints.
local function calculate_slow_start_ewma(self)
local total_ewma = 0
local address_count = 0
for _, target in ipairs(self.balancer.targets) do
for _, address in ipairs(target.addresses) do
if address.available then
local ewma = self.ewma[address] or 0
address_count = address_count + 1
total_ewma = total_ewma + ewma
end
end
end
if address_count == 0 then
ngx_log(ngx_DEBUG, "no ewma value exists for the endpoints")
return nil
end
self.address_count = address_count
return total_ewma / address_count
end
function ewma:afterHostUpdate()
table_clear(new_addresses)
for _, target in ipairs(self.balancer.targets) do
for _, address in ipairs(target.addresses) do
if address.available then
new_addresses[address] = true
end
end
end
local ewma = self.ewma
local ewma_last_touched_at = self.ewma_last_touched_at
for address, _ in pairs(ewma) do
if not new_addresses[address] then
ewma[address] = nil
ewma_last_touched_at[address] = nil
end
end
local slow_start_ewma = calculate_slow_start_ewma(self)
if slow_start_ewma == nil then
return
end
local now = ngx_now()
for address, _ in pairs(new_addresses) do
if not ewma[address] then
ewma[address] = slow_start_ewma
ewma_last_touched_at[address] = now
end
end
end
local function get_or_update_ewma(self, address, rtt, update)
local ewma = self.ewma[address] or 0
local now = ngx_now()
local last_touched_at = self.ewma_last_touched_at[address] or 0
ewma = decay_ewma(ewma, last_touched_at, rtt, now)
if update then
self.ewma_last_touched_at[address] = now
self.ewma[address] = ewma
end
return ewma
end
function ewma:afterBalance(_, handle)
local ngx_var = ngx.var
local response_time = tonumber(ngx_var.upstream_response_time) or 0
local connect_time = tonumber(ngx_var.upstream_connect_time) or 0
local rtt = connect_time + response_time
local upstream = ngx_var.upstream_addr
local address = handle.address
if upstream then
ngx_log(ngx_DEBUG, "ewma after balancer rtt: ", rtt)
return get_or_update_ewma(self, address, rtt, true)
end
return nil, "no upstream addr found"
end
local function pick_and_score(self, addresses, k)
local lowest_score_index = 1
local lowest_score = get_or_update_ewma(self, addresses[lowest_score_index], 0, false) / addresses[lowest_score_index].weight
for i = 2, k do
local new_score = get_or_update_ewma(self, addresses[i], 0, false) / addresses[i].weight
if new_score < lowest_score then
lowest_score_index = i
lowest_score = new_score
end
end
return addresses[lowest_score_index], lowest_score
end
function ewma:getPeer(cache_only, handle)
if handle then
-- existing handle, so it's a retry
handle.retryCount = handle.retryCount + 1
-- keep track of failed addresses
handle.failedAddresses = handle.failedAddresses or setmetatable({}, {__mode = "k"})
handle.failedAddresses[handle.address] = true
else
handle = {
failedAddresses = setmetatable({}, {__mode = "k"}),
retryCount = 0,
}
end
if not self.balancer.healthy then
return nil, balancers.errors.ERR_BALANCER_UNHEALTHY
end
-- select first address
local address
for addr, ewma in pairs(self.ewma) do
if ewma ~= nil then
address = addr
break
end
end
if address == nil then
-- No peers are available
return nil, balancers.errors.ERR_NO_PEERS_AVAILABLE, nil
end
local address_count = self.address_count
local ip, port, host
while true do
-- retry end
if address_count > 1 then
local k = (address_count < PICK_SET_SIZE) and address_count or PICK_SET_SIZE
local filtered_addresses = {}
for addr, ewma in pairs(self.ewma) do
if not handle.failedAddresses[addr] then
table_insert(filtered_addresses, addr)
end
end
local filtered_addresses_num = table_nkeys(filtered_addresses)
if filtered_addresses_num == 0 then
ngx_log(ngx_WARN, "all endpoints have been retried")
return nil, balancers.errors.ERR_NO_PEERS_AVAILABLE
end
local score
if filtered_addresses_num > 1 then
k = filtered_addresses_num > k and filtered_addresses_num or k
address, score = pick_and_score(self, filtered_addresses, k)
else
address = filtered_addresses[1]
score = get_or_update_ewma(self, filtered_addresses[1], 0, false)
end
ngx_log(ngx_DEBUG, "get ewma score: ", score)
end
-- check the address returned, and get an IP
ip, port, host = balancers.getAddressPeer(address, cache_only)
if ip then
-- success, exit
handle.address = address
break
end
handle.failedAddresses[address] = true
if port ~= balancers.errors.ERR_DNS_UPDATED then
-- an unknown error
break
end
end
return ip, port, host, handle
end
function ewma.new(opts)
assert(type(opts) == "table", "Expected an options table, but got: "..type(opts))
local balancer = opts.balancer
local self = setmetatable({
ewma = {},
ewma_last_touched_at = {},
balancer = balancer,
address_count = 0,
}, ewma)
self:afterHostUpdate()
ngx_log(ngx_DEBUG, "latency balancer created")
return self
end
return ewma