be/src/util/dns_cache.cpp
Line | Count | Source |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | |
18 | | #include "util/dns_cache.h" |
19 | | |
20 | | #include <netdb.h> |
21 | | |
22 | | #include <algorithm> |
23 | | #include <atomic> |
24 | | #include <unordered_set> |
25 | | |
26 | | #include "common/config.h" |
27 | | #include "service/backend_options.h" |
28 | | #include "util/network_util.h" |
29 | | |
30 | | namespace doris { |
31 | | |
32 | 10 | DNSCache::DNSCache() { |
33 | 10 | refresh_thread = std::thread(&DNSCache::_refresh_cache, this); |
34 | 10 | } |
35 | | |
36 | 21 | DNSCache::DNSCache(Resolver resolver) : _resolver(std::move(resolver)) {} |
37 | | |
38 | 27 | DNSCache::~DNSCache() { |
39 | 27 | { |
40 | 27 | std::lock_guard<std::mutex> lk(_cv_mutex); |
41 | 27 | stop_refresh = true; |
42 | 27 | } |
43 | 27 | _cv.notify_all(); |
44 | 27 | if (refresh_thread.joinable()) { |
45 | 6 | refresh_thread.join(); |
46 | 6 | } |
47 | 27 | } |
48 | | |
49 | 148k | Status DNSCache::get(const std::string& hostname, std::string* ip) { |
50 | 148k | bool has_negative_entry = false; |
51 | 148k | { |
52 | 148k | std::shared_lock<std::shared_mutex> lock(mutex); |
53 | 148k | auto it = cache.find(hostname); |
54 | 148k | if (it != cache.end()) { |
55 | 148k | *ip = it->second; |
56 | 148k | return Status::OK(); |
57 | 148k | } |
58 | 24 | auto neg_it = _negative_cache.find(hostname); |
59 | 27 | if (neg_it != _negative_cache.end()) { |
60 | 27 | int32_t ttl = config::dns_cache_negative_ttl_seconds; |
61 | 27 | if (ttl > 0) { |
62 | 26 | auto deadline = neg_it->second + std::chrono::seconds(ttl); |
63 | 26 | if (std::chrono::steady_clock::now() < deadline) { |
64 | | // No stack trace: this is an expected steady state, not an anomaly, and |
65 | | // it is returned once per caller request for as long as the host stays |
66 | | // unresolvable. Capturing a stack here would make Status::Error() log a |
67 | | // WARNING per call (and every caller logs status.to_string() again), |
68 | | // recreating exactly the be.WARNING flood this cache exists to stop. |
69 | 20 | return Status::InternalError<false>( |
70 | 20 | "Hostname {} is in negative DNS cache (recently evicted or " |
71 | 20 | "unresolvable), skipping resolve", |
72 | 20 | hostname); |
73 | 20 | } |
74 | 26 | } |
75 | 7 | has_negative_entry = true; |
76 | 7 | } |
77 | 24 | } |
78 | | |
79 | | // If the host was in the negative cache with an expired (or disabled) TTL, |
80 | | // claim the single-flight retry under unique_lock before the blocking DNS |
81 | | // call. Re-arming the eviction_time to now() makes concurrent callers see |
82 | | // an unexpired entry, bounding retries to one per host per TTL period. |
83 | 7 | if (has_negative_entry) { |
84 | 7 | std::unique_lock<std::shared_mutex> lock(mutex); |
85 | 7 | auto neg_it = _negative_cache.find(hostname); |
86 | 7 | if (neg_it != _negative_cache.end()) { |
87 | 7 | int32_t ttl = config::dns_cache_negative_ttl_seconds; |
88 | 7 | if (ttl <= 0) { |
89 | 1 | _negative_cache.erase(neg_it); |
90 | 6 | } else { |
91 | 6 | auto deadline = neg_it->second + std::chrono::seconds(ttl); |
92 | 6 | if (std::chrono::steady_clock::now() >= deadline) { |
93 | 6 | neg_it->second = std::chrono::steady_clock::now(); |
94 | 6 | } else { |
95 | | // Lost the single-flight race; see above for why this carries no stack. |
96 | 0 | return Status::InternalError<false>( |
97 | 0 | "Hostname {} is in negative DNS cache (recently evicted or " |
98 | 0 | "unresolvable), skipping resolve", |
99 | 0 | hostname); |
100 | 0 | } |
101 | 6 | } |
102 | 7 | } |
103 | 7 | } |
104 | | |
105 | | // First access (or negative TTL expired): resolve and populate the cache. |
106 | | // Consume the IP returned by _update() directly to avoid a second cache |
107 | | // lookup — operator[] under a shared_lock would mutate the map and could |
108 | | // reinsert an empty entry if a concurrent refresh cycle evicted the hostname |
109 | | // between _update() and here. |
110 | 4 | return _update(hostname, nullptr, ip); |
111 | 4 | } |
112 | | |
113 | | // Resolve hostname to IP address, similar to Java's DNSCache.resolveHostname. |
114 | | // If resolution fails, falls back to cached IP if available. |
115 | | // Returns the resolved IP, or cached IP on failure, or empty string if no cache available. |
116 | | // *is_fresh (if non-null) is set to true when DNS returned a live result, false |
117 | | // when the IP comes from the stale cached fallback path. |
118 | 390 | std::string DNSCache::_resolve_hostname(const std::string& hostname, bool* is_fresh) { |
119 | | // Get cached IP first (if any) |
120 | 390 | std::string cached_ip; |
121 | 390 | { |
122 | 390 | std::shared_lock<std::shared_mutex> lock(mutex); |
123 | 390 | auto it = cache.find(hostname); |
124 | 390 | if (it != cache.end()) { |
125 | 348 | cached_ip = it->second; |
126 | 348 | } |
127 | 390 | } |
128 | | |
129 | | // Try to resolve hostname |
130 | 390 | std::string resolved_ip; |
131 | 390 | int gai_err = 0; |
132 | 390 | Status status = |
133 | 390 | _resolver ? _resolver(hostname, resolved_ip, BackendOptions::is_bind_ipv6(), &gai_err) |
134 | 390 | : hostname_to_ip(hostname, resolved_ip, BackendOptions::is_bind_ipv6(), |
135 | 261 | &gai_err); |
136 | | |
137 | 390 | if (!status.ok() || resolved_ip.empty()) { |
138 | 109 | if (is_fresh) { |
139 | 108 | *is_fresh = false; |
140 | 108 | } |
141 | | // EAI_NONAME is the resolver authoritatively answering "this name does not exist", |
142 | | // which is the only evidence that a backend is really gone. Everything else |
143 | | // (EAI_AGAIN = resolver unreachable or timed out, EAI_SYSTEM, EAI_FAIL, ...) means |
144 | | // DNS itself is unhealthy while the host is most likely still up at its last known |
145 | | // address, so those failures must never lead to eviction — otherwise a resolver |
146 | | // outage would wipe every hostname at once and turn a DNS incident into a |
147 | | // cluster-wide RPC outage. |
148 | 109 | const bool authoritative = (gai_err == EAI_NONAME); |
149 | 109 | if (!cached_ip.empty()) { |
150 | | // Only track failure counts for hosts that are currently in the cache. |
151 | | // Hosts that were never cached or have already been evicted are not |
152 | | // tracked, which prevents unbounded growth of failure_count. |
153 | 96 | uint32_t failures = 0; |
154 | 96 | { |
155 | 96 | std::unique_lock<std::shared_mutex> lock(mutex); |
156 | | // Re-check that the host is still cached under the unique_lock: |
157 | | // it may have been evicted by the refresh thread between our |
158 | | // earlier shared_lock read of cached_ip and now (hostname_to_ip |
159 | | // can block for seconds on DNS timeout, widening the window). |
160 | | // Skipping the bump here preserves keys(failure_count) ⊆ keys(cache). |
161 | 96 | if (cache.find(hostname) != cache.end()) { |
162 | 94 | FailureState& state = failure_count[hostname]; |
163 | | // The counter tracks failures of any kind so the throttled log below |
164 | | // stays informative during a resolver outage; only `last_authoritative` |
165 | | // gates eviction. |
166 | 94 | failures = ++state.count; |
167 | 94 | state.last_authoritative = authoritative; |
168 | 94 | } |
169 | 96 | } |
170 | | // Throttle the log: only every N failures or the first failure. |
171 | 96 | if (failures > 0) { |
172 | 94 | int32_t every_n = std::max(1, config::dns_cache_log_every_n_failures); |
173 | 94 | if (failures == 1 || failures % static_cast<uint32_t>(every_n) == 0) { |
174 | 21 | LOG(WARNING) << "Failed to resolve hostname " << hostname |
175 | 21 | << " (consecutive failures: " << failures << ", error: " |
176 | 21 | << (authoritative ? "NXDOMAIN, host is gone" |
177 | 21 | : "transient, DNS unhealthy") |
178 | 21 | << "), use cached ip: " << cached_ip; |
179 | 21 | } |
180 | 94 | } |
181 | 96 | return cached_ip; |
182 | 96 | } else { |
183 | | // Throttle to avoid flooding be.WARNING when callers repeatedly |
184 | | // query an evicted or never-resolvable hostname. This branch |
185 | | // deliberately does not maintain a per-hostname counter (that |
186 | | // would break the keys(failure_count) ⊆ keys(cache) invariant), |
187 | | // so the throttle is a coarse global rate limit shared across |
188 | | // all hostnames hitting this code path. |
189 | 13 | static std::atomic<uint64_t> no_cache_warn_counter {0}; |
190 | 13 | uint64_t n = no_cache_warn_counter.fetch_add(1, std::memory_order_relaxed) + 1; |
191 | 13 | int32_t every_n = std::max(1, config::dns_cache_log_every_n_failures); |
192 | 13 | if (n == 1 || n % static_cast<uint64_t>(every_n) == 0) { |
193 | 2 | LOG(WARNING) << "Failed to resolve hostname " << hostname |
194 | 2 | << ", no cached ip available"; |
195 | 2 | } |
196 | 13 | return ""; |
197 | 13 | } |
198 | 109 | } |
199 | | |
200 | | // Resolution succeeded - clear failure counter for this hostname. |
201 | 281 | if (is_fresh) { |
202 | 281 | *is_fresh = true; |
203 | 281 | } |
204 | 281 | { |
205 | 281 | std::unique_lock<std::shared_mutex> lock(mutex); |
206 | 281 | failure_count.erase(hostname); |
207 | 281 | } |
208 | 281 | return resolved_ip; |
209 | 390 | } |
210 | | |
211 | 13 | void DNSCache::_evict_locked(const std::string& hostname) { |
212 | 13 | cache.erase(hostname); |
213 | 13 | failure_count.erase(hostname); |
214 | 13 | int32_t ttl = config::dns_cache_negative_ttl_seconds; |
215 | 13 | if (ttl > 0) { |
216 | 13 | _negative_cache[hostname] = std::chrono::steady_clock::now(); |
217 | 13 | } |
218 | 13 | } |
219 | | |
220 | 13 | void DNSCache::_remember_unresolvable(const std::string& hostname) { |
221 | 13 | int32_t ttl = config::dns_cache_negative_ttl_seconds; |
222 | 13 | if (ttl <= 0) { |
223 | 8 | return; |
224 | 8 | } |
225 | 5 | std::unique_lock<std::shared_mutex> lock(mutex); |
226 | | // try_emplace, not operator[]: get()'s single-flight path may have just re-armed this |
227 | | // tombstone to now(); overwriting it would be harmless there but would also let two |
228 | | // callers racing on the same host each reset the deadline, loosening the rate limit. |
229 | 5 | _negative_cache.try_emplace(hostname, std::chrono::steady_clock::now()); |
230 | 5 | } |
231 | | |
232 | 2 | void DNSCache::_erase(const std::string& hostname) { |
233 | 2 | std::unique_lock<std::shared_mutex> lock(mutex); |
234 | 2 | _evict_locked(hostname); |
235 | 2 | } |
236 | | |
237 | 13 | bool DNSCache::_erase_if_still_failing(const std::string& hostname, uint32_t threshold) { |
238 | 13 | std::unique_lock<std::shared_mutex> lock(mutex); |
239 | 13 | auto fc_it = failure_count.find(hostname); |
240 | 13 | if (fc_it == failure_count.end() || fc_it->second.count < threshold || |
241 | 13 | !fc_it->second.last_authoritative) { |
242 | | // Either a concurrent successful resolution cleared or reset the counter between |
243 | | // _update() returning and this call — do not erase a now-healthy entry — or the |
244 | | // most recent failure was transient (resolver unreachable) rather than an |
245 | | // authoritative NXDOMAIN, in which case the host is probably still alive. |
246 | 2 | return false; |
247 | 2 | } |
248 | 11 | _evict_locked(hostname); |
249 | 11 | return true; |
250 | 13 | } |
251 | | |
252 | | Status DNSCache::_update(const std::string& hostname, FailureState* out_state, |
253 | 389 | std::string* out_ip) { |
254 | 389 | bool is_fresh = false; |
255 | 389 | std::string real_ip = _resolve_hostname(hostname, &is_fresh); |
256 | 389 | if (real_ip.empty()) { |
257 | 13 | if (out_state) { |
258 | 0 | *out_state = FailureState {}; |
259 | 0 | } |
260 | 13 | if (out_ip) { |
261 | 13 | out_ip->clear(); |
262 | 13 | } |
263 | | // The host could not be resolved and has no cached IP to fall back on, so it never |
264 | | // entered `cache` and will therefore never reach the eviction path that writes a |
265 | | // tombstone. Record one here as well: otherwise every single get() on a hostname |
266 | | // that has never resolved (a typo in the FE, a backend registered before its DNS |
267 | | // record propagated) pays a full blocking getaddrinfo, and many of those calls run |
268 | | // on bthreads where long blocking is especially costly. |
269 | 13 | _remember_unresolvable(hostname); |
270 | 13 | return Status::InternalError<false>( |
271 | 13 | "Failed to resolve hostname {} and no cached ip available", hostname); |
272 | 13 | } |
273 | | |
274 | 376 | std::unique_lock<std::shared_mutex> lock(mutex); |
275 | | // _resolve_hostname may have captured a stale cached_ip before a concurrent |
276 | | // eviction completed. If the host is now in the negative cache we must not |
277 | | // reinsert the stale IP: that would silently undo the eviction and clear the |
278 | | // tombstone, defeating the whole purpose of eviction. Only a fresh DNS |
279 | | // result (is_fresh == true, meaning DNS actually resolved) may override an |
280 | | // eviction — which indicates the backend is genuinely back. |
281 | 376 | if (!is_fresh && _negative_cache.count(hostname)) { |
282 | 1 | if (out_state) { |
283 | 1 | *out_state = FailureState {}; |
284 | 1 | } |
285 | 1 | if (out_ip) { |
286 | 0 | out_ip->clear(); |
287 | 0 | } |
288 | | // No stack trace: like the negative-cache hits in get(), this is an expected |
289 | | // outcome that can repeat on every request while the host stays evicted. |
290 | 1 | return Status::InternalError<false>( |
291 | 1 | "Hostname {} was concurrently evicted; stale-fallback not reinserted", hostname); |
292 | 1 | } |
293 | 375 | auto it = cache.find(hostname); |
294 | 375 | if (it == cache.end() || it->second != real_ip) { |
295 | 29 | cache[hostname] = real_ip; |
296 | 29 | LOG(INFO) << "update hostname " << hostname << "'s ip to " << real_ip; |
297 | 29 | } |
298 | | // DNS resolved successfully — remove any negative cache tombstone so |
299 | | // subsequent get() calls go straight to the main cache. |
300 | 375 | _negative_cache.erase(hostname); |
301 | 375 | if (out_ip) { |
302 | 29 | *out_ip = real_ip; |
303 | 29 | } |
304 | | // Read failure_count under the same lock we already hold, so _refresh_once |
305 | | // does not need a second lock acquisition to decide on eviction. |
306 | 375 | if (out_state) { |
307 | 346 | auto fc_it = failure_count.find(hostname); |
308 | 346 | *out_state = fc_it != failure_count.end() ? fc_it->second : FailureState {}; |
309 | 346 | } |
310 | 375 | return Status::OK(); |
311 | 376 | } |
312 | | |
313 | 353 | void DNSCache::_refresh_once() { |
314 | 353 | std::unordered_set<std::string> keys; |
315 | 353 | { |
316 | 353 | std::shared_lock<std::shared_mutex> lock(mutex); |
317 | 353 | std::transform(cache.begin(), cache.end(), std::inserter(keys, keys.end()), |
318 | 353 | [](const auto& pair) { return pair.first; }); |
319 | 353 | } |
320 | 353 | for (auto& key : keys) { |
321 | | // Each _update() below performs a blocking getaddrinfo, so one cycle over a |
322 | | // cluster whose DNS is timing out can take minutes. Without this check the |
323 | | // destructor's join() would be held up for exactly that long, which would |
324 | | // undo the point of making the wait itself interruptible. |
325 | 348 | if (stop_refresh.load(std::memory_order_acquire)) { |
326 | 1 | break; |
327 | 1 | } |
328 | 347 | FailureState state; |
329 | 347 | Status st = _update(key, &state); |
330 | 347 | if (!st.ok()) { |
331 | | // _update returns an error either when _resolve_hostname returns "" |
332 | | // (no fallback IP) or when a stale fallback was suppressed because |
333 | | // the host was concurrently evicted. Either way, log and move on; |
334 | | // the threshold check below handles the normal eviction path. |
335 | 1 | LOG(WARNING) << "Failed to update DNS cache for hostname " << key << ": " |
336 | 1 | << st.to_string(); |
337 | 1 | } |
338 | | // Evict hostnames that have failed to resolve for too long. |
339 | | // This avoids two pathological symptoms after a backend is dropped |
340 | | // from the cluster and its DNS record is removed: |
341 | | // 1) be.WARNING gets flooded with `failed to get ip from host`. |
342 | | // 2) brpc keeps re-using the stale IP from cache, producing |
343 | | // `Fail to wait EPOLLOUT ... Connection timed out`. |
344 | | // `last_authoritative` keeps this restricted to hosts the resolver has positively |
345 | | // reported as non-existent; a DNS outage yields transient errors for every host at |
346 | | // once and must leave the cache (and the stale-IP fallback) intact. |
347 | 347 | int32_t threshold = config::dns_cache_max_consecutive_failures; |
348 | 347 | if (threshold > 0 && state.last_authoritative && |
349 | 347 | state.count >= static_cast<uint32_t>(threshold)) { |
350 | | // Re-read failure_count under the mutex that also performs the erase |
351 | | // to fence any concurrent success that cleared the counter between |
352 | | // _update() returning and this point. |
353 | 11 | if (_erase_if_still_failing(key, static_cast<uint32_t>(threshold))) { |
354 | 11 | LOG(WARNING) << "Evicting hostname " << key << " from DNS cache after " |
355 | 11 | << state.count << " consecutive resolution failures"; |
356 | 11 | } |
357 | 11 | } |
358 | 347 | } |
359 | 353 | } |
360 | | |
361 | 10 | void DNSCache::_refresh_cache() { |
362 | 275 | while (!stop_refresh) { |
363 | 265 | { |
364 | 265 | std::unique_lock<std::mutex> lk(_cv_mutex); |
365 | | // Wake up either after 1 minute or when the destructor signals stop. |
366 | 526 | _cv.wait_for(lk, std::chrono::minutes(1), [this] { return stop_refresh.load(); }); |
367 | 265 | } |
368 | 265 | if (!stop_refresh) { |
369 | 255 | _refresh_once(); |
370 | 255 | } |
371 | 265 | } |
372 | 10 | } |
373 | | |
374 | | } // end of namespace doris |