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.
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) |
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.
ditto
please fix similar points
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.
it seems wrong, please confirm this code
There was a problem hiding this comment.
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.
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
There was a problem hiding this comment.
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.
It would be better to fix this known issue in this PR.
There was a problem hiding this comment.
It seems this problem is still not addressed?
There was a problem hiding this comment.
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.
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.
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.
Not locking for
getwithout update will result in potentially incorrect behaviour. Imagine at line 86 you fetch currentewmavalue, then another worker updates it and itslast_touched_atvalue before you retrievelast_touched_atat 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.
We can merge these two if conditions to one:
if err and err ~= "timeout" then
end
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.
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.
Just following the codeing style of APISIX, 100 characters are allowed in one line.
apisix/balancer/ewma.lua
Outdated
| 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.
I think the error level is reasonable.
There was a problem hiding this comment.
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.
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.
| endpoint, backendpoint = peers[a], peers[b] | ||
| if score(endpoint) > score(backendpoint) then | ||
| endpoint, backendpoint = backendpoint, endpoint | ||
| end |
There was a problem hiding this comment.
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.
ERROR: apisix/balancer/ewma.lua: line 161: getting the Lua global "ipairs"
|
And we need a test for "fiter tried servers". |
@spacewander test case added. |
apisix/balancer/ewma.lua
Outdated
| end | ||
|
|
||
| local ewma = shm_ewma:get(upstream) or 0 | ||
| if lock_err ~= nil then |
There was a problem hiding this comment.
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.
We need lock when update, and do not lock when get.
There was a problem hiding this comment.
Why don't move this block inside the if update then?
There was a problem hiding this comment.
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] | ||
|
|
|
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.
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.
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.
You can speed up the test like this:
Line 309 in b78223c
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.
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.
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.
Need a test to cover this branch. And since we don't need to copy the peers?
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.
We already set ctx.balancer_tried_servers in the after_balancer?
| store_stats(upstream, ewma, now) | ||
|
|
||
| return ewma | ||
| unlock() |
There was a problem hiding this comment.
The unlock()'s err is not checked?
There was a problem hiding this comment.
We have record the error, when unlock() failed.
apisix/balancer/ewma.lua
Outdated
| end | ||
|
|
||
| local ewma = shm_ewma:get(upstream) or 0 | ||
| if lock_err ~= nil then |
There was a problem hiding this comment.
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.
This branch should move outside the for loop. And set the filtered_peers to peers if not in retry.
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.
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
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