Skip to content

c-ares: make qcache_max_ttl configurable - #45073

Merged
yanavlasov merged 11 commits into
envoyproxy:mainfrom
andy-fong:cares-qcache-max-ttl
Jul 1, 2026
Merged

c-ares: make qcache_max_ttl configurable#45073
yanavlasov merged 11 commits into
envoyproxy:mainfrom
andy-fong:cares-qcache-max-ttl

Conversation

@andy-fong

Copy link
Copy Markdown
Contributor

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.

Signed-off-by: Andy Fong <andy.fong@solo.io>
@repokitteh-read-only

Copy link
Copy Markdown

CC @envoyproxy/runtime-guard-changes: FYI only for changes made to (source/common/runtime/runtime_features.cc).
CC @envoyproxy/api-shepherds: Your approval is needed for changes made to (api/envoy/|docs/root/api-docs/).
envoyproxy/api-shepherds assignee is @markdroth
CC @envoyproxy/api-watchers: FYI only for changes made to (api/envoy/|docs/root/api-docs/).

🐱

Caused by: #45073 was opened by andy-fong.

see: more, trace.

@andy-fong

Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor

cc @yanavlasov

@markdroth

Copy link
Copy Markdown
Contributor

/lgtm api

@tyxia

tyxia commented May 22, 2026

Copy link
Copy Markdown
Member

/assign @yanavlasov

andy-fong added 3 commits May 26, 2026 11:03
…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>
@andy-fong

Copy link
Copy Markdown
Contributor Author

random tests or download issue keep failing the precheck. I just use the opportunity to merge with main to kick the tests.

@andy-fong

Copy link
Copy Markdown
Contributor Author

Latest fail is a segfault:

FAIL: //test/extensions/filters/http/ext_proc:ext_proc_integration_test (shard 4 of 8) (Segmentation fault) (see /build/bazel_root/base/execroot/envoy/bazel-out/k8-opt/testlogs/test/extensions/filters/http/ext_proc/ext_proc_integration_test/shard_4_of_8/test.log)

not completely sure it's related to this change.

@andy-fong

Copy link
Copy Markdown
Contributor Author

ext_proc_integration_test

This test runs fine locally.

@andy-fong

Copy link
Copy Markdown
Contributor Author

/retest

@andy-fong

Copy link
Copy Markdown
Contributor Author

All tests passed for the first time!

@yanavlasov

Copy link
Copy Markdown
Contributor

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment thread source/extensions/network/dns_resolver/cares/dns_impl.cc
Comment on lines +709 to +726
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;

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.

high

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;

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.

switched to use operator[] and ignored the data race comment because it's not true.

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.

emm, why the data race issue is not true? Considering dns filter might call createDnsResolver() in worker threads concurrently (

resolver_ = std::make_unique<DnsFilterResolver>(
), then accessing resolver_map_ needs to be mutex protected, right?

@yanjunxiang-google yanjunxiang-google Aug 7, 2026

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.

#46577 to revert this PR. PTAL @andy-fong . Please feel free to let me if you think there is no race issue here.

Comment thread source/extensions/network/dns_resolver/cares/dns_impl.cc
Comment on lines 32 to 34

absl::flat_hash_map<std::size_t, std::weak_ptr<Network::DnsResolver>> resolver_map_;
};

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.

medium

The resolver_map_ member variable added to DnsClusterFactory is completely unused and represents dead code. It should be removed.

};

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.

removed.

Comment on lines +73 to +80
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;
}

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.

medium

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);
}

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.

This function is actually not needed, just directly called mutable_qcache_max_ttl()->set_value()

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

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>
@andy-fong

Copy link
Copy Markdown
Contributor Author

Thank you @yanavlasov, I have updated the PR with the 3 changes, I replied to the gemini review comment. The test failures are unrelated:

  • CircuitBreaker Test core dump
  • Distroless image download timed out from the registry

…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>
@botengyao

Copy link
Copy Markdown
Member

ping @yanavlasov for a further look.

@yanavlasov
yanavlasov enabled auto-merge (squash) July 1, 2026 22:53
@yanavlasov
yanavlasov merged commit 740ae0d into envoyproxy:main Jul 1, 2026
29 checks passed
@andy-fong

andy-fong commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@yanavlasov Thank you for merging this PR but I just found this issue:
#42836 (closed as not planed) that relies on the fact that when there is any c-ares config, it will create a resolver per cluster to workaround the problem. My change here by default will prevent that workaround from working unless the user turns it off with reloadable feature.

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:

  // .. note::
  //   While the c-ares library defaults to 3 attempts, Envoy's default (if this field is unset) is 4 attempts.
  //   This adjustment was made to maintain the previous behavior after users reported an increase in DNS resolution times.
  //
  google.protobuf.UInt32Value query_tries = 7 [(validate.rules).uint32 = {gte: 1}];

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?

yanjunxiang-google added a commit to yanjunxiang-google/envoy that referenced this pull request Aug 7, 2026
Signed-off-by: Yanjun Xiang <yanjunxiang@google.com>
@AntonKanug

Copy link
Copy Markdown
Contributor

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.

yanjunxiang-google pushed a commit that referenced this pull request Aug 13, 2026
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>
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.

8 participants