fix memory leaking in dfp#31433
Conversation
Signed-off-by: doujiang24 <doujiang24@gmail.com>
Signed-off-by: doujiang24 <doujiang24@gmail.com>
1be5a50 to
e199002
Compare
|
Assigning Alyssa as codeowner |
alyssawilk
left a comment
There was a problem hiding this comment.
thanks for tackling this. I'm not terribly familiar with this area of code so tagging @zuercher as well. My Thoughts below
/wait
| std::unique_ptr<Upstream::HostVector>& hosts_added) { | ||
| const Extensions::Common::DynamicForwardProxy::DnsHostInfoSharedPtr& host_info) { | ||
| Upstream::LogicalHostSharedPtr emplaced_host; | ||
| bool founded = false; |
There was a problem hiding this comment.
okay, will fix it later :)
| dummy_lb_endpoint_); | ||
| // will add host into priorityState again, with the new address. | ||
| } | ||
| updatePriorityState(hosts, {}); |
There was a problem hiding this comment.
do we actually need to do the add/remove here? I don't think we really use priority for DFP, and the address update grabs a lock and updates (I think) globally so I'm not sure if the update is necessary. strict DNS appears to simply do the priority update on host addition and skip on resolution change.
One nice way to test e2e would be to do an e2e test where we start with a bad resolved IP via cache and a short DNS refresh timeout, and verify that the update makes it to the worker threads (crib off of test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc tests for cache files)
There was a problem hiding this comment.
I don't think we really use priority for DFP
yep, I do think so too. can we remove the priority entirely? if yes, it would be much simpler.
do we actually need to do the add/remove here
yes, I do think so, we have to keep tracking the correct address in priority, otherwise,
when host is TTL expired, the old(before change) address will leaking in priority.
One nice way to test e2e would be to do an e2e test
okay, I will have a try later, after we figure out the proper way to fix it.
There was a problem hiding this comment.
the different between logical DNS and strict DNS is that - strict DNS could remove all of the IPs from priority, but logic DNS could not.
since there is only the latest IP in logic DNS when removing the expired host:
There was a problem hiding this comment.
I worry a little about the double-call to updatePriorityState -- I think it means theres's a brief period with no host in the priority set. Perhaps that's ok.
An alternative might be to check for the existing host but always create a new one for the updated IP and then make a single call to updatePriorityState with the new/old host.
There was a problem hiding this comment.
yeah if it's not super complicated it'd be nice to fix updatePriortySet properly so it handles a remove/add of a single host.
There was a problem hiding this comment.
okay, already updated to a single call to updatePriorityState.
There was a problem hiding this comment.
@zuercher can you do first pass here? I didn't expect the single call to updatePriorityState to require an add/remove of host instead of updateAddress and I'm not really sure which is better
There was a problem hiding this comment.
hm, not sure what happened to @zuercher on this review, but I'm wondering if it's possible to do an update while not replacing the host, still doing the address resolution change option
/wait-any
There was a problem hiding this comment.
Oh, nope, seems hard to reach it, it needs two logicalHost, sine we want a single call to updatePriorityState
|
|
||
| for (const auto& host : hosts_added) { | ||
| mutable_cross_priority_host_map_->insert({addressToString(host->address()), host}); | ||
| ENVOY_LOG(debug, "cross_priority host map, adding: {}", addressToString(host->address())); |
There was a problem hiding this comment.
Based on your problem description, it looks like the problem is that when the host is initially resolved into IP_A, IP_A is inserted into the mutable_cross_priority_host_map_ here. And later on if the host is resolved into IP_B, then IP_B is inserted here. However, the above (Line 841) mutable_cross_priority_host_map_->erase(addressToString(host->address())) for IP_A is somehow not called.
Intuitively, I would think when the DNS resolving returns a different address, MainPrioritySetImpl::updateCrossPriorityHostMap() will be called with both host_added and host_removed set. But, it looks like host_removed is empty here. Could you please share which code path is causing that?
There was a problem hiding this comment.
Based on your problem description, it looks like the problem is that when the host is initially resolved into IP_A, IP_A is inserted into the mutable_cross_priority_host_map_ here. And later on if the host is resolved into IP_B, then IP_B is inserted here. However, the above (Line 841) mutable_cross_priority_host_map_->erase(addressToString(host->address())) for IP_A is somehow not called.
yep, that's it.
Intuitively, I would think when the DNS resolving returns a different address, MainPrioritySetImpl::updateCrossPriorityHostMap() will be called with both host_added and host_removed set. But, it looks like host_removed is empty here. Could you please share which code path is causing that?
Nope, in the old implementation, it simply do the priority update on host addition and skip on host subtraction, similar to strict DNS, as @alyssawilk said.
And I think that't the problem.
since it only remove the latest IP in logic DNS when removing an expired host:
There was a problem hiding this comment.
really I don't think having an IP based host map makes sense for DFP hosts, as we can have many "unique" hosts refered to by the same address. Pinging @wbpcode for thoughts - if it still lived in the LB classes I'd nix it for the DFP cluster but that's not an easy option nowadays.
Signed-off-by: doujiang24 <doujiang24@gmail.com>
Signed-off-by: doujiang24 <doujiang24@gmail.com>
Signed-off-by: doujiang24 <doujiang24@gmail.com>
|
/retest |
|
/retest |
|
the mobile (compile-time-options) job is passing - there was a ci glitch (since fixed) that meant it didnt always complete |
|
ping @zuercher |
zuercher
left a comment
There was a problem hiding this comment.
I think this seems reasonable. I suppose the old version had an efficiency when many hosts were added or removed at once. If that's a concern we could lift the loop in startPreInit to happen under lock.
|
Thanks @zuercher
I think it might be fine, since it only happens while cluster initize, means low rate usually. |
| } | ||
| hosts_added->emplace_back(emplaced_host); | ||
| ASSERT(hosts_added.size() > 0); | ||
| updatePriorityState(hosts_added, hosts_removed); |
There was a problem hiding this comment.
Actually looking through this code belatedly, doesn't updatePriorityState cause updateHosts cause drainOrCloseConnPools(host, ConnectionPool::DrainBehavior::DrainAndDelete) on the "old host"? I think this could be a pretty bad latency hit where we want to lame-duck drain rather than force-remove drain.
There was a problem hiding this comment.
Oh, I see the problem.
But, I think it might be reasonable to force-remove drain.
When dns resolve result changes, force-remove drain the old host should be expected?
The root cause is the dfp cluster use logical DNS mode instead of strict dns mode, it may lead to host change even when dns resolve result not changes.
I haven't found a simple solution yet.
Maybe we can recommend people to use the new sub_cluster_config?
Or, any suggestions?
There was a problem hiding this comment.
DrainExistingConnections would maybe be OK, but DrainAndDelete is too aggressive IMO as it will force-close connections.
Either way I'm thinking we should probably roll this back as I don't think the prior leak had caused many problems in production and (I think) this ended up being an unexpected non-flag-guarded behavioral change.
I am also OK writing a test to verify if this resulted in the behavioral change I think it did before we go that route but @yanavlasov @ggreenway for their thoughts.
There was a problem hiding this comment.
This bug is first reported in #30999, and it leads to a large leak, according to the profile data.
But, according @rbtz-openai's update, brotli decompression is the root cause for the large leak.
I also agree that it just leaking in extremely case, with very low rate.
Revert this patch might be a proper choice, since it's hard to keep the old behaviour to fix this extremely low rate leak.
Maybe @rbtz-openai could reconfirm that this dfp leak could be ignored in your production case?
For "DrainAndDelete will force-close connections":
After reading the comments of DrainExistingConnections/DrainAndDelete, and the code of drainConnections(drain_behavior), I didn't find force-close connections.
I think I missed it somewhere. Would you please give a guideline? i.e. some keywords of the code.
Thanks very much~
There was a problem hiding this comment.
Yeah so I added tests (#32710) and while I'm not sure why (having looked at the code) we're not force-closing, it does appear we're not force closing. I'll do a little more investigation before pushing for rollback. Thanks for looking into this as well!
There was a problem hiding this comment.
Ah ok, figured out what's wrong with my test.
OK so this PR both did fix the memory leak described, but also result in a functional change to connection lifetimes.
By the Envoy change guidelines I think it should not have landed without flag protection (because data plane change). The particular change is problematic for Envoy Mobile. Given the memory leak is long standing and hadn't been largely I'm inclined to roll back to prior (buggy) state than keep this (also buggy for E-M) state. We could also flag guard the change if there's anyone who wants the memory leak fix and doesn't mind the connectivity change?
cc @yanavlasov @RyanTheOptimist for second opinions.
for context, this fixes the leak in the mutable_cross_priority_host_map_ as described but does so by updating the host in worker threads, which results in an (new, undesirable for E-M) connection pool drain and fresh non-warmed-CWND connection being used for subsequent streams.
Ideally we'd have a solution which handled both but until we have a solution the question is which bug to prefer. I'm inclined to stick with the old one but would also be amenable into making it a flag-guarded "behavioral change + fix". Unclear if the correct behavior for a server is to drain on DNS refresh - it does do a better job of respecting DNS TTL but can also result in a lot of connection churn and suboptimal bandwidth.
There was a problem hiding this comment.
Yeah, I agree with @alyssawilk that since this included a non-flag protected behavior change (especially since it broke mobile), we should roll this back. We can go ahead and roll forward with flag protection, of course, but we should return to a stable state first, I think.
There was a problem hiding this comment.
okay, I see. Rolling back does make sense. If someone really need this fix, we could create a new PR with a flag guard.
Adding some DFP tests of what happens on DNS resolution. Prior to #31433 both these tests passed, as we didn't drain the host when we re-resolved the IP. Now the second test fails, as the host is drained (and the newly resolved IP doesn't work by design). Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Commit Message:
fix #30999
mutable_cross_priority_host_map_logical_host_will be updated to IP B.mutable_cross_priority_host_map_, IP A is leaking inmutable_cross_priority_host_map_.this PR do fix the leaking issue, but I'm not sure if it is a proper fix.
Additional Description:
Risk Level:
Testing:
Docs Changes:
Release Notes:
Platform Specific Features:
[Optional Runtime guard:]
[Optional Fixes #Issue]
[Optional Fixes commit #PR or SHA]
[Optional Deprecated:]
[Optional API Considerations:]