feat(utils): make DNS resolver cache TTL configurable via RUSTFS_DNS_CACHE_TTL_SECS (#4604)

* feat(utils): make DNS resolver cache TTL configurable via RUSTFS_DNS_CACHE_TTL_SECS

The resolver cache in crates/utils/src/net.rs had a hardcoded 300s TTL with
no way to tune or disable it. On orchestrated networks (Docker Swarm,
Kubernetes) a peer's DNS name is its only durable identity while its container
IP changes on reschedule, and the platform DNS always serves the current
answer. A fixed 300s application-side cache can keep a member dialing a dead IP
for up to five minutes after a peer restarts. Rust has no runtime DNS cache, so
on musl images this was the only cache in the stack and the only one that could
not be turned off.

Read the TTL from RUSTFS_DNS_CACHE_TTL_SECS (u64, default 300), matching the
existing RUSTFS_INTERNODE_* transport knobs. `0` disables caching entirely so
every get_host_ip consults the system resolver. The resolved value is logged
once at first use, removing an invisible cache from the triage surface. Change
is backward-compatible and confined to net.rs.

Also drop the two per-lookup info! logs on the resolve path: they would flood
the log when caching is disabled and added hot-path noise otherwise.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(helm): derive mTLS secret names from chart fullname (#4601)

* fix(helm): mtls certificate for server and client hardcode

* fix hardcode issue in deployment

* update typo yaml file

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com>
This commit is contained in:
houseme
2026-07-09 11:26:06 +08:00
committed by GitHub
parent e0eee89a3b
commit 073628e8be
4 changed files with 66 additions and 12 deletions
+2
View File
@@ -45,6 +45,8 @@ nonexisted = "nonexisted"
consts = "consts"
# Swift API - company/product names
Hashi = "Hashi" # HashiCorp
# Accept alternate spelling used in parser/XML comments.
unparseable = "unparseable"
[files]
extend-exclude = []
+60 -8
View File
@@ -48,7 +48,39 @@ impl DnsCacheEntry {
}
static DNS_CACHE: LazyLock<Mutex<HashMap<String, DnsCacheEntry>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
const DNS_CACHE_TTL: Duration = Duration::from_secs(300); // 5 minutes
/// Environment variable to tune the DNS resolver cache TTL, in seconds.
///
/// Rust has no runtime-level DNS cache or global TTL knob (unlike the JVM's
/// `networkaddress.cache.ttl`), so this application cache is the only cache in the stack on
/// musl-based images. On orchestrated networks (Docker Swarm, Kubernetes) a peer's DNS name is
/// its only durable identity while its IP changes on reschedule; a fixed cache can keep a member
/// dialing a dead IP after a peer restarts. Making the TTL tunable removes an invisible,
/// unconfigurable cache between rustfs and the platform DNS.
///
/// `0` disables caching entirely (every `get_host_ip` consults the system resolver).
const ENV_DNS_CACHE_TTL_SECS: &str = "RUSTFS_DNS_CACHE_TTL_SECS";
const DEFAULT_DNS_CACHE_TTL_SECS: u64 = 300; // 5 minutes
/// Resolved DNS cache TTL, read once from `RUSTFS_DNS_CACHE_TTL_SECS` at first use.
///
/// A value of `Duration::ZERO` means caching is disabled. The configured value is logged once so
/// operators debugging cluster membership on dynamic networks can rule the cache in or out from
/// the logs alone.
static DNS_CACHE_TTL: LazyLock<Duration> = LazyLock::new(|| {
let secs = crate::envs::get_env_u64(ENV_DNS_CACHE_TTL_SECS, DEFAULT_DNS_CACHE_TTL_SECS);
if secs == 0 {
info!("DNS resolver cache disabled ({ENV_DNS_CACHE_TTL_SECS}=0); every lookup consults the system resolver");
} else {
info!("DNS resolver cache TTL set to {secs}s ({ENV_DNS_CACHE_TTL_SECS})");
}
Duration::from_secs(secs)
});
/// Whether the DNS resolver cache is enabled (TTL greater than zero).
fn dns_cache_enabled() -> bool {
!DNS_CACHE_TTL.is_zero()
}
type DynDnsResolver = dyn Fn(&str) -> std::io::Result<HashSet<IpAddr>> + Send + Sync + 'static;
static CUSTOM_DNS_RESOLVER: LazyLock<RwLock<Option<Arc<DynDnsResolver>>>> = LazyLock::new(|| RwLock::new(None));
@@ -219,34 +251,35 @@ fn get_local_ips_with_fallback() -> Vec<IpAddr> {
pub async fn get_host_ip(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
match host {
Host::Domain(domain) => {
// The cache is bypassed entirely when a custom resolver is installed or when the TTL
// is configured to 0 (disabled), in which case every lookup consults the resolver.
let cache_enabled = dns_cache_enabled() && !has_custom_dns_resolver();
// Check cache first
if !has_custom_dns_resolver()
if cache_enabled
&& let Ok(mut cache) = DNS_CACHE.lock()
&& let Some(entry) = cache.get(domain)
{
if !entry.is_expired(DNS_CACHE_TTL) {
if !entry.is_expired(*DNS_CACHE_TTL) {
return Ok(entry.ips.clone());
}
// Remove expired entry
cache.remove(domain);
}
info!("Cache miss for domain {domain}, querying system resolver.");
// Fallback to standard resolution when DNS resolver is not available
match resolve_domain(domain) {
Ok(ips) => {
if !has_custom_dns_resolver() {
if cache_enabled {
// Cache the result
if let Ok(mut cache) = DNS_CACHE.lock() {
cache.insert(domain.to_string(), DnsCacheEntry::new(ips.clone()));
// Limit cache size to prevent memory bloat
if cache.len() > 1000 {
cache.retain(|_, v| !v.is_expired(DNS_CACHE_TTL));
cache.retain(|_, v| !v.is_expired(*DNS_CACHE_TTL));
}
}
}
info!("System query for domain {domain}: {:?}", ips);
Ok(ips)
}
Err(err) => {
@@ -575,6 +608,25 @@ mod test {
assert!(get_host_ip(invalid_host).await.is_err());
}
#[test]
fn test_dns_cache_entry_expiry() {
let ips: HashSet<IpAddr> = [IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))].into_iter().collect();
// A freshly cached entry is not expired under a normal TTL.
let fresh = DnsCacheEntry::new(ips.clone());
assert!(!fresh.is_expired(Duration::from_secs(300)));
// An entry whose age exceeds the TTL is expired; a longer TTL keeps it valid.
let aged = DnsCacheEntry {
ips,
cached_at: Instant::now()
.checked_sub(Duration::from_secs(600))
.expect("instant in range"),
};
assert!(aged.is_expired(Duration::from_secs(300)));
assert!(!aged.is_expired(Duration::from_secs(900)));
}
#[test]
fn test_get_available_port() {
let port1 = get_available_port();
+2 -2
View File
@@ -156,7 +156,7 @@ spec:
{{- if .Values.mtls.enabled }}
- name: server-cert
secret:
secretName: rustfs-server-tls
secretName: {{ include "rustfs.fullname" . }}-server-tls
items:
- key: tls.crt
path: rustfs_cert.pem
@@ -166,7 +166,7 @@ spec:
path: ca.crt
- name: client-cert
secret:
secretName: rustfs-client-tls
secretName: {{ include "rustfs.fullname" . }}-client-tls
items:
- key: tls.crt
path: client_cert.pem
+2 -2
View File
@@ -178,7 +178,7 @@ spec:
{{- if .Values.mtls.enabled }}
- name: server-cert
secret:
secretName: rustfs-server-tls
secretName: {{ include "rustfs.fullname" . }}-server-tls
items:
- key: tls.crt
path: rustfs_cert.pem
@@ -188,7 +188,7 @@ spec:
path: ca.crt
- name: client-cert
secret:
secretName: rustfs-client-tls
secretName: {{ include "rustfs.fullname" . }}-client-tls
items:
- key: tls.crt
path: client_cert.pem