-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: ewma use p2c to improve performance #3300
Conversation
b9e1834
to
164dadd
Compare
nice PR @hnlq715 |
164dadd
to
d111944
Compare
apisix/balancer/ewma.lua
Outdated
local _, err = ewma_lock:lock(upstream .. LOCK_KEY) | ||
if err then | ||
if err ~= "timeout" then | ||
ngx.log(ngx.ERR, string.format("EWMA Balancer failed to lock: %s", tostring(err))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better style(better performance):
ngx.log(ngx.ERR, string.format("EWMA Balancer failed to lock: %s", tostring(err))) | |
core.log.error("EWMA Balancer failed to lock: ", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
apisix/balancer/ewma.lua
Outdated
local function unlock() | ||
local ok, err = ewma_lock:unlock() | ||
if not ok then | ||
ngx.log(ngx.ERR, string.format("EWMA Balancer failed to unlock: %s", tostring(err))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
please fix similar points
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
apisix/balancer/ewma.lua
Outdated
return get_or_update_ewma(upstream_name, 0, false) | ||
end | ||
local ewma = ngx.shared.balancer_ewma:get(upstream) or 0 | ||
if lock_err ~= nil then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems wrong, please confirm this code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
ngx.shared.balancer_ewma => shm_ewma
endpoint, backendpoint = peers[a], peers[b] | ||
if score(endpoint) > score(backendpoint) then | ||
endpoint, backendpoint = backendpoint, endpoint | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to sync the tried_endpoint
check from https://github.com/kubernetes/ingress-nginx/blob/a2e77185cc2e91278962f4f1267246c8fefc6e73/rootfs/etc/nginx/lua/balancer/ewma.lua#L180 to our new implementation.
You can take a look at:
apisix/apisix/balancer/roundrobin.lua
Lines 54 to 74 in bbbdf58
if not before_retry then | |
if ctx.balancer_tried_servers then | |
core.tablepool.release("balancer_tried_servers", ctx.balancer_tried_servers) | |
ctx.balancer_tried_servers = nil | |
end | |
return nil | |
end | |
if not ctx.balancer_tried_servers then | |
ctx.balancer_tried_servers = core.tablepool.fetch("balancer_tried_servers", 0, 2) | |
end | |
ctx.balancer_tried_servers[ctx.balancer_server] = true | |
ctx.balancer_tried_servers_count = (ctx.balancer_tried_servers_count or 0) + 1 | |
end | |
} | |
end | |
return _M |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is another topic we could improve the stability, like passive/active healthcheck, tried record & tries count?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to fix this known issue in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems this problem is still not addressed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, filter logic is added, I'm focusing something else in these days.
local function get_or_update_ewma(upstream, rtt, update) | ||
local lock_err = nil | ||
if update then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not locking for get
without update will result in potentially incorrect behaviour. Imagine at line 86 you fetch current ewma
value, then another worker updates it and its last_touched_at
value before you retrieve last_touched_at
at line 92. Then you will end up treating the old ewma as a new one. I'm not sure if in practice it would make big difference, but it is definitely not a correct behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the only way of getting rid of locking for "get" operations is to combine ewma and timestamp in the same value and store under single key. But then you would need to do encoding and decoding every time you set and fetch it. It can be interesting to try that and see the performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ping @hnlq715
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not locking for
get
without update will result in potentially incorrect behaviour. Imagine at line 86 you fetch currentewma
value, then another worker updates it and itslast_touched_at
value before you retrievelast_touched_at
at line 92. Then you will end up treating the old ewma as a new one. I'm not sure if in practice it would make big difference, but it is definitely not a correct behaviour.
@ElvinEfendi You're right, this is not a correct behavior, but maybe a proper solution. In this situation you mentioned, last_touched_at
value should have little difference (almost the same time), and we do not need to lock all get
operation, which is quite heavy.
I think the only way of getting rid of locking for "get" operations is to combine ewma and timestamp in the same value and store under single key. But then you would need to do encoding and decoding every time you set and fetch it. It can be interesting to try that and see the performance.
My first implementation is using worker process cache to store this, and we can simply avoid locking, without frequently encoding and decoding, which have a better performance. But we still have other concerns, details can be found in #3211
And with shared memory, we have to sacrifice between performance and correctness.
I did some benchmark between these two situations:
lock get and update
+ wrk -d 5 -c 16 http://127.0.0.1:9080/hello
Running 5s test @ http://127.0.0.1:9080/hello
2 threads and 16 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.62ms 282.29us 7.64ms 90.45%
Req/Sec 4.98k 511.87 9.62k 97.03%
50013 requests in 5.10s, 199.42MB read
Requests/sec: 9806.24
Transfer/sec: 39.10MB
+ sleep 1
+ wrk -d 5 -c 16 http://127.0.0.1:9080/hello
Running 5s test @ http://127.0.0.1:9080/hello
2 threads and 16 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.69ms 288.94us 7.25ms 89.40%
Req/Sec 4.77k 285.81 5.28k 63.73%
48370 requests in 5.10s, 192.87MB read
Requests/sec: 9484.82
Transfer/sec: 37.82MB
lock update
+ wrk -d 5 -c 16 http://127.0.0.1:9080/hello
Running 5s test @ http://127.0.0.1:9080/hello
2 threads and 16 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.57ms 289.91us 7.23ms 89.61%
Req/Sec 5.14k 584.14 10.43k 96.04%
51652 requests in 5.10s, 205.95MB read
Requests/sec: 10128.09
Transfer/sec: 40.38MB
+ sleep 1
+ wrk -d 5 -c 16 http://127.0.0.1:9080/hello
Running 5s test @ http://127.0.0.1:9080/hello
2 threads and 16 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.55ms 255.99us 6.55ms 89.96%
Req/Sec 5.18k 539.62 9.77k 95.05%
52008 requests in 5.10s, 207.37MB read
Requests/sec: 10198.48
Transfer/sec: 40.66MB
apisix/balancer/ewma.lua
Outdated
|
||
local function lock(upstream) | ||
local _, err = ewma_lock:lock(upstream .. LOCK_KEY) | ||
if err then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can merge these two if conditions to one:
if err and err ~= "timeout" then
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
apisix/balancer/ewma.lua
Outdated
end | ||
if forcible then | ||
core.log.warn("balancer_ewma_last_touched_at:set valid items forcibly overwritten") | ||
core.log |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why breaking line here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the max column number is set to 80
so this line break into two lines by format tool, I fixed it by setting it to 82
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just following the codeing style of APISIX, 100 characters are allowed in one line.
apisix/balancer/ewma.lua
Outdated
local function store_stats(upstream, ewma, now) | ||
local success, err, forcible = shm_last_touched_at:set(upstream, now) | ||
if not success then | ||
core.log.error("balancer_ewma_last_touched_at:set failed ", err) | ||
core.log.warn("shm_last_touched_at:set failed: ", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the error
level is reasonable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error often came out when the dict is full, but everything goes fine because we can evict the old items in the dict.
Refer to ingress-nginx, so maybe warn is a proper level? :-)
https://github.com/kubernetes/ingress-nginx/blob/master/rootfs/etc/nginx/lua/balancer/ewma.lua#L68
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of time, error
level is used in production clusters, the dict full is not a trivial problem, if the level is warn
, i'm afraid the problem will be covered up and the troubleshooting might be difficult.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
endpoint, backendpoint = peers[a], peers[b] | ||
if score(endpoint) > score(backendpoint) then | ||
endpoint, backendpoint = backendpoint, endpoint | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems this problem is still not addressed?
apisix/balancer/ewma.lua
Outdated
return endpoint.host .. ":" .. endpoint.port | ||
end | ||
local filtered_peers | ||
for _, peer in ipairs(peers) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ERROR: apisix/balancer/ewma.lua: line 161: getting the Lua global "ipairs"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
And we need a test for "fiter tried servers". |
@spacewander test case added. |
apisix/balancer/ewma.lua
Outdated
local ewma = shm_ewma:get(upstream) or 0 | ||
if lock_err ~= nil then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why check lock_err
after fetching ewma
? It's so strange we don't check the return value after lock
return. Or you should get ewma
before fetching the lock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need lock when update, and do not lock when get.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't move this block inside the if update then
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to return ewma value for get and failed update operation.
Reconsider this, I think it's ok to move this block, for now we do not use this ewma value in update operation.
--- error_code: 200 | ||
--- no_error_log | ||
[error] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant empty lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
The CI was broken. You might need to submit a new commit to trigger it. |
Seems like the CI is still broken... |
t/node/ewma.t
Outdated
local uri = "http://127.0.0.1:" .. ngx.var.server_port | ||
.. "/ewma" | ||
|
||
--should select the 1980 node, because 1984 is invalid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 1984 is not invalid, you can confirm it via less t/servroot/conf/nginx.conf
.
My bad, even 1984 port is open, it can't handle the request correctly. So it is invalid port.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid misunderstand, I changed it to 9527
t/node/ewma.t
Outdated
--should select the 1980 node, because 1984 is invalid | ||
local ports_count = {} | ||
for i = 1, 12 do | ||
local httpc = http.new() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can speed up the test like this:
Line 309 in b78223c
local th = assert(ngx.thread.spawn(function(i) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
apisix/balancer/ewma.lua
Outdated
endpoint = pick_and_score(peers) | ||
local tried_endpoints | ||
if not ctx.balancer_tried_servers then | ||
tried_endpoints = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems we can save the table allocation of tried_endpoints
and filtered_peers
when not ctx.balancer_tried_servers
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, we called after_balance
in the first time.
apisix/balancer/ewma.lua
Outdated
|
||
if not filtered_peers then | ||
core.log.warn("all endpoints have been retried") | ||
filtered_peers = table_deepcopy(peers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need a test to cover this branch. And since we don't need to copy the peers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
apisix/balancer/ewma.lua
Outdated
local tried_endpoints | ||
if not ctx.balancer_tried_servers then | ||
tried_endpoints = {} | ||
ctx.balancer_tried_servers = tried_endpoints |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already set ctx.balancer_tried_servers
in the after_balancer
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I got it :-)
end | ||
|
||
store_stats(upstream, ewma, now) | ||
|
||
return ewma | ||
unlock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unlock()
's err is not checked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have record the error, when unlock() failed.
apisix/balancer/ewma.lua
Outdated
local ewma = shm_ewma:get(upstream) or 0 | ||
if lock_err ~= nil then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't move this block inside the if update then
?
apisix/balancer/ewma.lua
Outdated
endpoint = peers[1] | ||
local filtered_peers | ||
for _, peer in ipairs(peers) do | ||
if ctx.balancer_tried_servers then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This branch should move outside the for loop. And set the filtered_peers to peers if not in retry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
apisix/balancer/ewma.lua
Outdated
return endpoint.host .. ":" .. endpoint.port | ||
end | ||
if not filtered_peers then | ||
core.log.warn("all endpoints have been retried") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this branch as we already reject this case above:
if ctx.balancer_tried_servers and ctx.balancer_tried_servers_count == nkeys(up_nodes) then
return nil, "all upstream servers tried"
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
What this PR does / why we need it:
Details can be found in #3211
Pre-submission checklist:
Before
apisix: 1 worker + 200 upstream + no plugin
apisix: 1 worker + 200 upstream + no plugin + ewma
After
apisix: 1 worker + 200 upstream + no plugin + ewma