fix: the problem of high cpu and memory usage (#9015)#9016
Conversation
cocurrent requests after reload or update the upstream nodes will cause high cpu and memory usage, the checker created by healthcheck.new in create_checker won't be released if the program crashes after cancel_clean_handler failed
|
please add test cases for this PR |
|
|
||
| if healthcheck_parent.checker then | ||
| core.config_util.cancel_clean_handler(healthcheck_parent, | ||
| local ok, err = pcall(core.config_util.cancel_clean_handler, healthcheck_parent, |
There was a problem hiding this comment.
Since we add create_checker guard, cancel_clean_handler will not fail anymore, so no need to do pcall?
There was a problem hiding this comment.
Just for code robustness
There was a problem hiding this comment.
Just for code robustness
It's not necessary.
|
|
||
| core_tab.remove(item.clean_handlers, pos) | ||
| if fire then | ||
| if not fire then |
There was a problem hiding this comment.
Since we add create_checker guard, fire will not be nil, so no need to check nil here?
There was a problem hiding this comment.
Just for code robustness
There was a problem hiding this comment.
Just for code robustness
ditto.
| @@ -115,8 +122,11 @@ local function create_checker(upstream) | |||
| end | |||
|
|
|||
| if healthcheck_parent.checker then | |||
There was a problem hiding this comment.
Except for this parallel checker creation bug (due to the non-atomic fact of checker new() and add_target()), each resource update from etcd will replace the old value with a brand new one, here healthcheck_parent (the resource itself) will have only one checker at most anytime, so should we delete the obsolete code of checker conditions handling?
There was a problem hiding this comment.
each resource update from etcd will replace the old value with a brand new one
I checked the code, updating the resource won't replace the old value with a brand new one
https://github.com/apache/apisix/blob/master/apisix/core/config_etcd.lua#L472
There was a problem hiding this comment.
@monkeyDluffy6017 You're wrong. Sometimes reading code is not enough, and you need to debug it to guarantee your point.
apisix/apisix/core/config_etcd.lua
Line 472 in d8fd367
Set up a breakpoint:
cat > /usr/local/apisix/plugin_inspect_hooks.lua <<EOF
dbg.set_hook("apisix/core/config_etcd.lua", 472, nil, function(info)
core.log.warn("res: ", tostring(info.vals.res))
return false
end)
EOFerror.log:
2023/03/07 16:19:33 [warn] 2472696#2472696: *110 [lua] init.lua:68: setup_hooks(): set hooks: err: true, hooks: ["apisix/core/config_etcd.lua#472"], context: ngx.timer
Create a test upstream:
update_upstream() {
curl http://127.0.0.1:9180/apisix/admin/upstreams/1 \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"nodes":[
{
"host":"httpbin.org",
"port":80,
"weight":1
},
{
"host":"postman-echo.com",
"port":80,
"weight":1
}
]
}'
}
update_upstreamOverride:
update_upstreamerror.log:
2023/03/07 16:20:09 [warn] 2472696#2472696: *35 [lua] [string "local dbg = require("apisix.inspect.dbg")..."]:256: res: table: 0x7f5c62fede28, context: ngx.timer
Override again:
update_upstreamerror.log:
2023/03/07 16:20:25 [warn] 2472696#2472696: *35 [lua] [string "local dbg = require("apisix.inspect.dbg")..."]:256: res: table: 0x7f5c680bbe88, context: ngx.timer
Note that the table is different: old: 0x7f5c62fede28, new: 0x7f5c680bbe88.
The parent of an upstream is itself,
Line 564 in d8fd367
Line 510 in d8fd367
So, each time the upstream gets updated, the parent is new, so it's impossible to enter into cases where the parent is the same but with a different checker, except this parallel create_checker bug, and since we do the bugfix, then no need to keep such code anymore. It works for all cases, including route with inline upstream or service with inline upstream.
@spacewander @moonming @membphis I think we should remove such obsolete code to make our code base lint.
There was a problem hiding this comment.
I see what you mean, except for this concurrent scenario, because the upstream object changes every time, so there can't be a healthcheck_parent.checker
There was a problem hiding this comment.
Removing this code will result in a lot of ci errors, consider dealing with it in a separate PR
There was a problem hiding this comment.
@monkeyDluffy6017 @spacewander @moonming @membphis
I take back what I concluded. That code is not dead code, although it's not for concurrent checker creation guard, it's necessary for service discovery.
Each time the discovery result changes, the upstream (inlined in route/service or separate upstream) will do a shallow copy and reserves its parent, so the parent (route/service/upstream) may have a previous checker.
Lines 272 to 284 in c159f04
There was a problem hiding this comment.
It looks like that the ci has covered this, so after removed this code, the ci failed:
apisix/t/node/healthcheck-discovery.t
Line 110 in c159f04
There was a problem hiding this comment.
Yes, so keep that code, although it's only for service discovery but not for normal upstream configuration update from etcd.
It is difficult to reproduce this issue and may not be suitable for integration into CI @moonming |
How do you ensure the fix is correct if you can not reproduce it? |
We can see the error logs through a long test, but the CI also needs to focus on efficiency, as it may not be suitable for this type of test |
moonming
left a comment
There was a problem hiding this comment.
We first reproduce a problem before we can fix it. So, without a test case, I don't think the bug is fixed
|
@moonming The test is case is added |
| if f then | ||
| f(item) | ||
| else | ||
| log.error("call cancel_clean_handler error, f is nil") |
There was a problem hiding this comment.
Can there be a more clear error message?
| healthcheck_parent.checker_idx = | ||
| core.config_util.add_clean_handler(healthcheck_parent, release_checker) | ||
|
|
||
| upstream.is_creating_checker = nil |
There was a problem hiding this comment.
We should also reset it when this function exits with nil
|
|
||
|
|
||
| === TEST 20: set route(two healthy upstream nodes) | ||
| --- config |
There was a problem hiding this comment.
When the test file is too large, for example > 800 lines, you should split it to a new file. Please take a look at t/plugin/limit-conn.t and t/plugin/limit-conn2.t.
|
@moonming test case is added, please check |
Description
cocurrent requests after reload or service discovery will cause high cpu and memory usage, the checker created by
healthcheck.newin create_checker won't be released if the program crashes aftercancel_clean_handlerfailedFixes #9015
Checklist