c-ares: make qcache_max_ttl configurable - #45073
Conversation
Signed-off-by: Andy Fong <andy.fong@solo.io>
|
CC @envoyproxy/runtime-guard-changes: FYI only for changes made to |
|
There are some past issues in the past that are closed as not planned related to this problem I am trying to solve:
While the correct solution is probably to deduplicate the DNS lookups across clusters or add native DNS cache. They are probably too big of an effort. I know DNS cache is available in DFP but does not look like it can be easily applied to the DNS lookup used by DNS clusters. There is also the new Hickory DNS resolver that seems to support caching but it's still new and I have not tried that. This is kind of a middle of the ground approach to reduce the DNS lookup from envoy in a large env. It's not ideal that I put the resolver_map_ under the DNSResolver class but only really c-ares impl uses that but that's the simplest route without a lot of changes. Open to any suggestion. |
…tl-main Signed-off-by: Andy Fong <andy.fong@solo.io>
|
cc @yanavlasov |
|
/lgtm api |
|
/assign @yanavlasov |
…tl-main Signed-off-by: Andy Fong <andy.fong@solo.io>
…tl-main Signed-off-by: Andy Fong <andy.fong@solo.io>
…tl-main Signed-off-by: Andy Fong <andy.fong@solo.io>
|
random tests or download issue keep failing the precheck. I just use the opportunity to merge with main to kick the tests. |
|
Latest fail is a segfault: not completely sure it's related to this change. |
This test runs fine locally. |
|
/retest |
|
All tests passed for the first time! |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a configurable maximum TTL (qcache_max_ttl) for the c-ares DNS resolver cache and implements sharing of DNSResolver instances with identical configurations, controlled by a runtime feature flag. The reviewer's feedback highlights critical thread-safety issues, noting that the shared resolver_map_ in CaresDnsResolverFactory is accessed concurrently without synchronization and should be guarded by mutex_. Additionally, the reviewer points out unused dead code in DnsClusterFactory and suggests simplifying a helper function in the test suite using idiomatic Protobuf APIs.
| auto resolver = std::make_shared<Network::DnsResolverImpl>( | ||
| cares, dispatcher, csv_or_error.value(), api.rootScope()); | ||
| if (Runtime::runtimeFeatureEnabled("envoy.restart_features.shared_cares_dns_resolver")) { | ||
| // clean up any nil resolver in the map so it doesn't keep growing | ||
| auto original_size = resolver_map_.size(); | ||
| absl::erase_if( | ||
| resolver_map_, | ||
| [](const std::pair<const std::size_t, std::weak_ptr<Network::DnsResolver>>& entry) { | ||
| return entry.second.lock() == nullptr; | ||
| }); | ||
| if (resolver_map_.size() < original_size) { | ||
| ENVOY_LOG(trace, "cleaned up {} entries in resolver_map_", | ||
| original_size - resolver_map_.size()); | ||
| } | ||
| resolver_map_.emplace(key, resolver); | ||
| ENVOY_LOG(trace, "resolver_map_ size after adding: {}", resolver_map_.size()); | ||
| } | ||
| return resolver; |
There was a problem hiding this comment.
Guard the insertion and cleanup of resolver_map_ with mutex_ to prevent data races. Also, prefer using operator[] or insert_or_assign instead of emplace to ensure the map is correctly updated if the key already exists (even if expired).
auto resolver = std::make_shared<Network::DnsResolverImpl>(
cares, dispatcher, csv_or_error.value(), api.rootScope());
if (Runtime::runtimeFeatureEnabled("envoy.restart_features.shared_cares_dns_resolver")) {
absl::MutexLock lock(&mutex_);
// clean up any nil resolver in the map so it doesn't keep growing
auto original_size = resolver_map_.size();
absl::erase_if(
resolver_map_,
[](const std::pair<const std::size_t, std::weak_ptr<Network::DnsResolver>>& entry) {
return entry.second.lock() == nullptr;
});
if (resolver_map_.size() < original_size) {
ENVOY_LOG(trace, "cleaned up {} entries in resolver_map_",
original_size - resolver_map_.size());
}
resolver_map_[key] = resolver;
ENVOY_LOG(trace, "resolver_map_ size after adding: {}", resolver_map_.size());
}
return resolver;There was a problem hiding this comment.
switched to use operator[] and ignored the data race comment because it's not true.
There was a problem hiding this comment.
emm, why the data race issue is not true? Considering dns filter might call createDnsResolver() in worker threads concurrently (
), then accessing resolver_map_ needs to be mutex protected, right?There was a problem hiding this comment.
#46577 to revert this PR. PTAL @andy-fong . Please feel free to let me if you think there is no race issue here.
|
|
||
| absl::flat_hash_map<std::size_t, std::weak_ptr<Network::DnsResolver>> resolver_map_; | ||
| }; |
| bool setQcacheMaxTtl( | ||
| envoy::extensions::network::dns_resolver::cares::v3::CaresDnsResolverConfig& config, | ||
| uint32_t value) { | ||
| auto qcache_max_ttl = std::make_unique<Protobuf::UInt32Value>(); | ||
| qcache_max_ttl->set_value(value); | ||
| config.set_allocated_qcache_max_ttl(qcache_max_ttl.release()); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Simplify setQcacheMaxTtl by using the idiomatic Protobuf mutable_qcache_max_ttl() helper instead of manual memory allocation and release(). Also, change the return type to void since the return value is always true and is ignored by callers.
void setQcacheMaxTtl(
envoy::extensions::network::dns_resolver::cares::v3::CaresDnsResolverConfig& config,
uint32_t value) {
config.mutable_qcache_max_ttl()->set_value(value);
}There was a problem hiding this comment.
This function is actually not needed, just directly called mutable_qcache_max_ttl()->set_value()
yanavlasov
left a comment
There was a problem hiding this comment.
The thread safety comments were hallucinated by Gemini. I have closed them. Please address a couple of remaining nits and it should be good to go. Thanks.
/wait
…tl-main Signed-off-by: Andy Fong <andy.fong@solo.io>
Signed-off-by: Andy Fong <andy.fong@solo.io>
…dy exists but not expired Signed-off-by: Andy Fong <andy.fong@solo.io>
|
Thank you @yanavlasov, I have updated the PR with the 3 changes, I replied to the gemini review comment. The test failures are unrelated:
|
…tl-main Signed-off-by: Andy Fong <andy.fong@solo.io>
…tl-main Signed-off-by: Andy Fong <andy.fong@solo.io>
…tl-main Signed-off-by: Andy Fong <andy.fong@solo.io>
|
ping @yanavlasov for a further look. |
|
@yanavlasov Thank you for merging this PR but I just found this issue: Another issue we found was that with shared resolvers and query_tries set to 1 and ~400 clusters for DNS lookup on start up, some DNS look up will timeout due to the kernel buffer being filled and dropping the udp packet. It only recovers on the next DNS lookup. Setting query_tries to 2 or disabling the shared resolver feature will prevent the DNS lookup timeout. This behavior is not observed by the default shared resolvers because the default query_tries is set to 4: Since even c-ares defaults to 3 tries, not sure how common for users to explicitly set it to 1. What do you think about these 2 issues? I saw that 1.39 just released, what is the best cause of action? |
Signed-off-by: Yanjun Xiang <yanjunxiang@google.com>
|
Flagging that I saw issues where two workers shared the same DNS resolver, corrupting its socket event state and causing a worker to spin at 100% CPU. Disabling resolver sharing resolved it. |
Commit Message: c-ares: disable shared resolver by default Additional Description: DnsFilter would call createDnsResolver() from a workerthread that would cause a race when sharing dns resolvers. Turn off this feature until the race is fixed. Relates to #46577 and #45073 Risk Level: low Testing: Docs Changes: Release Notes: Platform Specific Features: Signed-off-by: Andy Fong <andy.fong@solo.io>
Commit Message: c-ares: make qcache_max_ttl configurable
Additional Description:
expose the qcache_max_ttl setting and share DNSResolver if cares config is the same so the qcache can be shared. runtime guard "envoy.restart_features.shared_cares_dns_resolver" is set to true by default to enable sharing DNSResolver if cares config of the clusters are the same. Set to false to disable this behavior.
AI is used to generate the tests and write some of the comment but I did review and tune/modify all the tests added. All other code are hand written.
Risk Level: Low
Testing: unit test and manual test using tcpdump to verify multiple clusters with the same host (different port) only generate 1 dns lookup most of the time.
Docs Changes: None
Release Notes: Added qcache_max_ttl field to CaresDnsResolverConfig
Platform Specific Features: None
[Optional Runtime guard:] "envoy.restart_features.shared_cares_dns_resolver" is set to true by default to enable sharing DNSResolver if cares config of the clusters are the same. Set to false to disable this behavior.