Skip to content
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

Merged
merged 30 commits into from
Jan 29, 2021

Conversation

sysulq
Copy link
Contributor

@sysulq sysulq commented Jan 15, 2021

What this PR does / why we need it:

Details can be found in #3211

Pre-submission checklist:

  • Did you explain what problem does this PR solve? Or what new features have been added?
  • [] Have you added corresponding test cases?
  • [] Have you modified the corresponding document?
  • Is this PR backward compatible? If it is not backward compatible, please discuss on the mailing list first

Before

apisix: 1 worker + 200 upstream + no plugin

+ 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.65ms    1.53ms  32.40ms   97.65%
    Req/Sec     5.26k   663.77     8.41k    79.21%
  52974 requests in 5.11s, 211.22MB read
Requests/sec:  10369.65
Transfer/sec:     41.35MB
+ 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.82ms    1.99ms  30.06ms   96.26%
    Req/Sec     5.10k   845.05     6.21k    81.00%
  50759 requests in 5.00s, 202.39MB read
Requests/sec:  10149.88
Transfer/sec:     40.47MB

apisix: 1 worker + 200 upstream + no plugin + ewma

+ 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     2.95ms    1.55ms  27.52ms   93.33%
    Req/Sec     2.84k   490.54     6.22k    88.12%
  28488 requests in 5.10s, 113.60MB read
Requests/sec:   5585.89
Transfer/sec:     22.27MB
+ 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     3.01ms    0.86ms  23.33ms   85.91%
    Req/Sec     2.68k   228.62     3.11k    68.00%
  26713 requests in 5.00s, 106.52MB read
Requests/sec:   5338.53
Transfer/sec:     21.29MB

After

apisix: 1 worker + 200 upstream + no plugin + ewma

+ 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.85ms    0.88ms  21.68ms   94.33%
    Req/Sec     4.48k   562.97     5.48k    68.00%
  44599 requests in 5.00s, 177.83MB read
Requests/sec:   8912.87
Transfer/sec:     35.54MB
+ 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.73ms  693.40us  16.92ms   95.53%
    Req/Sec     4.73k   434.96     5.49k    74.51%
  48052 requests in 5.09s, 191.60MB read
Requests/sec:   9440.28
Transfer/sec:     37.64MB

@membphis
Copy link
Member

nice PR @hnlq715

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)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better style(better performance):

Suggested change
ngx.log(ngx.ERR, string.format("EWMA Balancer failed to lock: %s", tostring(err)))
core.log.error("EWMA Balancer failed to lock: ", err)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

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)))
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

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
Copy link
Member

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

Copy link
Contributor Author

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
Copy link
Member

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:

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

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it

Copy link
Member

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?

Copy link
Contributor Author

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

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.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @hnlq715

Copy link
Contributor Author

@sysulq sysulq Jan 19, 2021

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.

@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


local function lock(upstream)
local _, err = ewma_lock:lock(upstream .. LOCK_KEY)
if err then
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

end
if forcible then
core.log.warn("balancer_ewma_last_touched_at:set valid items forcibly overwritten")
core.log
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why breaking line here?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

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)
Copy link
Contributor

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.

Copy link
Contributor Author

@sysulq sysulq Jan 18, 2021

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

Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Member

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?

return endpoint.host .. ":" .. endpoint.port
end
local filtered_peers
for _, peer in ipairs(peers) do
Copy link
Member

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"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@spacewander
Copy link
Member

And we need a test for "fiter tried servers".

@sysulq
Copy link
Contributor Author

sysulq commented Jan 28, 2021

And we need a test for "fiter tried servers".

@spacewander test case added.

local ewma = shm_ewma:get(upstream) or 0
if lock_err ~= nil then
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

@sysulq sysulq Jan 29, 2021

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]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant empty lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@spacewander
Copy link
Member

The CI was broken. You might need to submit a new commit to trigger it.

@sysulq
Copy link
Contributor Author

sysulq commented Jan 28, 2021

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
Copy link
Member

@spacewander spacewander Jan 28, 2021

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.

Copy link
Contributor Author

@sysulq sysulq Jan 28, 2021

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()
Copy link
Member

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:

local th = assert(ngx.thread.spawn(function(i)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

endpoint = pick_and_score(peers)
local tried_endpoints
if not ctx.balancer_tried_servers then
tried_endpoints = {}
Copy link
Member

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?

Copy link
Contributor Author

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.


if not filtered_peers then
core.log.warn("all endpoints have been retried")
filtered_peers = table_deepcopy(peers)
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

local tried_endpoints
if not ctx.balancer_tried_servers then
tried_endpoints = {}
ctx.balancer_tried_servers = tried_endpoints
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I got it :-)

@sysulq sysulq requested a review from spacewander January 29, 2021 01:06
end

store_stats(upstream, ewma, now)

return ewma
unlock()
Copy link
Member

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?

Copy link
Contributor Author

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.

local ewma = shm_ewma:get(upstream) or 0
if lock_err ~= nil then
Copy link
Member

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?

endpoint = peers[1]
local filtered_peers
for _, peer in ipairs(peers) do
if ctx.balancer_tried_servers then
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return endpoint.host .. ":" .. endpoint.port
end
if not filtered_peers then
core.log.warn("all endpoints have been retried")
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@membphis membphis merged commit ba11e7f into apache:master Jan 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants