Skip to content

fix: the problem of high cpu and memory usage (#9015)#9016

Merged
spacewander merged 12 commits into
apache:masterfrom
juststillthinking:fix-high-cpu-and-memory-usage
Mar 23, 2023
Merged

fix: the problem of high cpu and memory usage (#9015)#9016
spacewander merged 12 commits into
apache:masterfrom
juststillthinking:fix-high-cpu-and-memory-usage

Conversation

@juststillthinking

Copy link
Copy Markdown
Contributor

Description

cocurrent requests after reload or service discovery 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

Fixes #9015

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible (If not, please discuss on the APISIX mailing list first)

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
@moonming

moonming commented Mar 7, 2023

Copy link
Copy Markdown
Member

please add test cases for this PR

@spacewander

Copy link
Copy Markdown
Member

@kingluo kingluo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A healthcheck_parent.create_checker = true guard is enough. And I suggest removing obsolete code.

Comment thread apisix/upstream.lua

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we add create_checker guard, cancel_clean_handler will not fail anymore, so no need to do pcall?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just for code robustness

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just for code robustness

It's not necessary.


core_tab.remove(item.clean_handlers, pos)
if fire then
if not fire then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we add create_checker guard, fire will not be nil, so no need to check nil here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just for code robustness

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just for code robustness

ditto.

Comment thread apisix/upstream.lua
@@ -115,8 +122,11 @@ local function create_checker(upstream)
end

if healthcheck_parent.checker then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

@juststillthinking juststillthinking Mar 7, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

@kingluo kingluo Mar 7, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@monkeyDluffy6017 You're wrong. Sometimes reading code is not enough, and you need to debug it to guarantee your point.

self.values[pre_index] = res

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)
EOF

error.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_upstream

Override:

update_upstream

error.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_upstream

error.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,

filter_upstream(upstream.value, upstream)

value.parent = parent

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removing this code will result in a lot of ci errors, consider dealing with it in a separate PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@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.

apisix/apisix/upstream.lua

Lines 272 to 284 in c159f04

local new_up_conf = core.table.clone(up_conf)
core.log.info("discover new upstream from ", up_conf.service_name, ", type ",
up_conf.discovery_type, ": ",
core.json.delay_encode(new_up_conf, true))
local parent = up_conf.parent
if parent.value.upstream then
-- the up_conf comes from route or service
parent.value.upstream = new_up_conf
else
parent.value = new_up_conf
end
up_conf = new_up_conf

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It looks like that the ci has covered this, so after removed this code, the ci failed:

=== TEST 2: create new checker when nodes change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, so keep that code, although it's only for service discovery but not for normal upstream configuration update from etcd.

@juststillthinking

juststillthinking commented Mar 7, 2023

Copy link
Copy Markdown
Contributor Author

please add test cases for this PR

It is difficult to reproduce this issue and may not be suitable for integration into CI @moonming

@moonming

moonming commented Mar 7, 2023

Copy link
Copy Markdown
Member

please add test cases for this PR

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?

@juststillthinking

juststillthinking commented Mar 7, 2023

Copy link
Copy Markdown
Contributor Author

please add test cases for this PR

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 moonming left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We first reproduce a problem before we can fix it. So, without a test case, I don't think the bug is fixed

@juststillthinking

Copy link
Copy Markdown
Contributor Author

@moonming The test is case is added

Comment thread apisix/core/config_util.lua Outdated
if f then
f(item)
else
log.error("call cancel_clean_handler error, f is nil")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can there be a more clear error message?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment thread apisix/upstream.lua
healthcheck_parent.checker_idx =
core.config_util.add_clean_handler(healthcheck_parent, release_checker)

upstream.is_creating_checker = nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should also reset it when this function exits with nil

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment thread t/node/healthcheck.t Outdated


=== TEST 20: set route(two healthy upstream nodes)
--- config

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

https://github.com/apache/apisix/blob/master/CONTRIBUTING.md#check-code-style-and-test-case-style

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@juststillthinking

Copy link
Copy Markdown
Contributor Author

@moonming test case is added, please check

@spacewander
spacewander merged commit 8197e03 into apache:master Mar 23, 2023
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.

bug: high cpu and memory usage

5 participants