253 lines
6.5 KiB
Lua
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
|