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 <algorithm> |
21 | | #include <atomic> |
22 | | #include <unordered_set> |
23 | | |
24 | | #include "common/config.h" |
25 | | #include "service/backend_options.h" |
26 | | #include "util/network_util.h" |
27 | | |
28 | | namespace doris { |
29 | | |
30 | 11 | DNSCache::DNSCache() { |
31 | 11 | refresh_thread = std::thread(&DNSCache::_refresh_cache, this); |
32 | 11 | } |
33 | | |
34 | 10 | DNSCache::DNSCache(Resolver resolver) : _resolver(std::move(resolver)) {} |
35 | | |
36 | 17 | DNSCache::~DNSCache() { |
37 | 17 | { |
38 | 17 | std::lock_guard<std::mutex> lk(_cv_mutex); |
39 | 17 | stop_refresh = true; |
40 | 17 | } |
41 | 17 | _cv.notify_all(); |
42 | 17 | if (refresh_thread.joinable()) { |
43 | 7 | refresh_thread.join(); |
44 | 7 | } |
45 | 17 | } |
46 | | |
47 | 150k | Status DNSCache::get(const std::string& hostname, std::string* ip) { |
48 | 150k | bool expired_negative = false; |
49 | 150k | { |
50 | 150k | std::shared_lock<std::shared_mutex> lock(mutex); |
51 | 150k | auto it = cache.find(hostname); |
52 | 151k | if (it != cache.end()) { |
53 | 151k | *ip = it->second; |
54 | 151k | return Status::OK(); |
55 | 151k | } |
56 | | // Return a cheap error for recently-evicted hosts to avoid repeated |
57 | | // blocking getaddrinfo calls during the transition after a backend is |
58 | | // dropped. The entry expires after dns_cache_negative_ttl_seconds; |
59 | | // once expired the next caller falls through and retries the resolve. |
60 | 18.4E | auto neg_it = _negative_cache.find(hostname); |
61 | 18.4E | if (neg_it != _negative_cache.end()) { |
62 | 13 | if (std::chrono::steady_clock::now() < neg_it->second) { |
63 | 12 | return Status::InternalError( |
64 | 12 | "Hostname {} is in negative DNS cache (recently evicted), skipping resolve", |
65 | 12 | hostname); |
66 | 12 | } |
67 | 1 | expired_negative = true; // TTL passed; allow one retry |
68 | 1 | } |
69 | 18.4E | } |
70 | | // First access (or negative TTL expired): resolve and populate the cache. |
71 | | // Consume the IP returned by _update() directly to avoid a second cache |
72 | | // lookup — operator[] under a shared_lock would mutate the map and could |
73 | | // reinsert an empty entry if a concurrent refresh cycle evicted the hostname |
74 | | // between _update() and here. |
75 | 18.4E | Status st = _update(hostname, nullptr, ip); |
76 | 18.4E | if (!st.ok() && expired_negative) { |
77 | | // DNS is still failing after the backoff window. Re-arm the negative |
78 | | // cache so the retry rate stays bounded at one attempt per TTL period — |
79 | | // otherwise every concurrent caller blocks on a full DNS timeout each time. |
80 | 1 | int32_t ttl = config::dns_cache_negative_ttl_seconds; |
81 | 1 | if (ttl > 0) { |
82 | 1 | std::unique_lock<std::shared_mutex> lock(mutex); |
83 | 1 | _negative_cache[hostname] = |
84 | 1 | std::chrono::steady_clock::now() + std::chrono::seconds(ttl); |
85 | 1 | } |
86 | 1 | } |
87 | 18.4E | return st; |
88 | 18.4E | } |
89 | | |
90 | | // Resolve hostname to IP address, similar to Java's DNSCache.resolveHostname. |
91 | | // If resolution fails, falls back to cached IP if available. |
92 | | // Returns the resolved IP, or cached IP on failure, or empty string if no cache available. |
93 | | // *is_fresh (if non-null) is set to true when DNS returned a live result, false |
94 | | // when the IP comes from the stale cached fallback path. |
95 | 285 | std::string DNSCache::_resolve_hostname(const std::string& hostname, bool* is_fresh) { |
96 | | // Get cached IP first (if any) |
97 | 285 | std::string cached_ip; |
98 | 285 | { |
99 | 285 | std::shared_lock<std::shared_mutex> lock(mutex); |
100 | 285 | auto it = cache.find(hostname); |
101 | 285 | if (it != cache.end()) { |
102 | 260 | cached_ip = it->second; |
103 | 260 | } |
104 | 285 | } |
105 | | |
106 | | // Try to resolve hostname |
107 | 285 | std::string resolved_ip; |
108 | 285 | Status status = _resolver |
109 | 285 | ? _resolver(hostname, resolved_ip, BackendOptions::is_bind_ipv6()) |
110 | 285 | : hostname_to_ip(hostname, resolved_ip, BackendOptions::is_bind_ipv6()); |
111 | | |
112 | 285 | if (!status.ok() || resolved_ip.empty()) { |
113 | 24 | if (is_fresh) *is_fresh = false; |
114 | 24 | if (!cached_ip.empty()) { |
115 | | // Only track failure counts for hosts that are currently in the cache. |
116 | | // Hosts that were never cached or have already been evicted are not |
117 | | // tracked, which prevents unbounded growth of failure_count. |
118 | 16 | uint32_t failures = 0; |
119 | 16 | { |
120 | 16 | std::unique_lock<std::shared_mutex> lock(mutex); |
121 | | // Re-check that the host is still cached under the unique_lock: |
122 | | // it may have been evicted by the refresh thread between our |
123 | | // earlier shared_lock read of cached_ip and now (hostname_to_ip |
124 | | // can block for seconds on DNS timeout, widening the window). |
125 | | // Skipping the bump here preserves keys(failure_count) ⊆ keys(cache). |
126 | 16 | if (cache.find(hostname) != cache.end()) { |
127 | 14 | failures = ++failure_count[hostname]; |
128 | 14 | } |
129 | 16 | } |
130 | | // Throttle the log: only every N failures or the first failure. |
131 | 16 | if (failures > 0) { |
132 | 14 | int32_t every_n = std::max(1, config::dns_cache_log_every_n_failures); |
133 | 14 | if (failures == 1 || failures % static_cast<uint32_t>(every_n) == 0) { |
134 | 7 | LOG(WARNING) << "Failed to resolve hostname " << hostname |
135 | 7 | << " (consecutive failures: " << failures |
136 | 7 | << "), use cached ip: " << cached_ip; |
137 | 7 | } |
138 | 14 | } |
139 | 16 | return cached_ip; |
140 | 16 | } else { |
141 | | // Throttle to avoid flooding be.WARNING when callers repeatedly |
142 | | // query an evicted or never-resolvable hostname. This branch |
143 | | // deliberately does not maintain a per-hostname counter (that |
144 | | // would break the keys(failure_count) ⊆ keys(cache) invariant), |
145 | | // so the throttle is a coarse global rate limit shared across |
146 | | // all hostnames hitting this code path. |
147 | 8 | static std::atomic<uint64_t> no_cache_warn_counter {0}; |
148 | 8 | uint64_t n = no_cache_warn_counter.fetch_add(1, std::memory_order_relaxed) + 1; |
149 | 8 | int32_t every_n = std::max(1, config::dns_cache_log_every_n_failures); |
150 | 8 | if (n == 1 || n % static_cast<uint64_t>(every_n) == 0) { |
151 | 1 | LOG(WARNING) << "Failed to resolve hostname " << hostname |
152 | 1 | << ", no cached ip available"; |
153 | 1 | } |
154 | 8 | return ""; |
155 | 8 | } |
156 | 24 | } |
157 | | |
158 | | // Resolution succeeded - clear failure counter for this hostname. |
159 | 261 | if (is_fresh) *is_fresh = true; |
160 | 261 | { |
161 | 261 | std::unique_lock<std::shared_mutex> lock(mutex); |
162 | 261 | failure_count.erase(hostname); |
163 | 261 | } |
164 | 261 | return resolved_ip; |
165 | 285 | } |
166 | | |
167 | 2 | void DNSCache::_erase(const std::string& hostname) { |
168 | 2 | std::unique_lock<std::shared_mutex> lock(mutex); |
169 | 2 | cache.erase(hostname); |
170 | 2 | failure_count.erase(hostname); |
171 | 2 | int32_t ttl = config::dns_cache_negative_ttl_seconds; |
172 | 2 | if (ttl > 0) { |
173 | 2 | _negative_cache[hostname] = |
174 | 2 | std::chrono::steady_clock::now() + std::chrono::seconds(ttl); |
175 | 2 | } |
176 | 2 | } |
177 | | |
178 | 7 | bool DNSCache::_erase_if_still_failing(const std::string& hostname, uint32_t threshold) { |
179 | 7 | std::unique_lock<std::shared_mutex> lock(mutex); |
180 | 7 | auto fc_it = failure_count.find(hostname); |
181 | 7 | if (fc_it == failure_count.end() || fc_it->second < threshold) { |
182 | | // A concurrent successful resolution cleared or reset the counter between |
183 | | // _update() returning and this call — do not erase a now-healthy entry. |
184 | 1 | return false; |
185 | 1 | } |
186 | 6 | cache.erase(hostname); |
187 | 6 | failure_count.erase(hostname); |
188 | 6 | int32_t ttl = config::dns_cache_negative_ttl_seconds; |
189 | 6 | if (ttl > 0) { |
190 | 6 | _negative_cache[hostname] = |
191 | 6 | std::chrono::steady_clock::now() + std::chrono::seconds(ttl); |
192 | 6 | } |
193 | 6 | return true; |
194 | 7 | } |
195 | | |
196 | 284 | Status DNSCache::_update(const std::string& hostname, uint32_t* out_failures, std::string* out_ip) { |
197 | 284 | bool is_fresh = false; |
198 | 284 | std::string real_ip = _resolve_hostname(hostname, &is_fresh); |
199 | 284 | if (real_ip.empty()) { |
200 | 8 | if (out_failures) *out_failures = 0; |
201 | 8 | if (out_ip) out_ip->clear(); |
202 | 8 | return Status::InternalError("Failed to resolve hostname {} and no cached ip available", |
203 | 8 | hostname); |
204 | 8 | } |
205 | | |
206 | 276 | std::unique_lock<std::shared_mutex> lock(mutex); |
207 | | // _resolve_hostname may have captured a stale cached_ip before a concurrent |
208 | | // eviction completed. If the host is now in the negative cache we must not |
209 | | // reinsert the stale IP: that would silently undo the eviction and clear the |
210 | | // tombstone, defeating the whole purpose of eviction. Only a fresh DNS |
211 | | // result (is_fresh == true, meaning DNS actually resolved) may override an |
212 | | // eviction — which indicates the backend is genuinely back. |
213 | 276 | if (!is_fresh && _negative_cache.count(hostname)) { |
214 | 1 | if (out_failures) *out_failures = 0; |
215 | 1 | if (out_ip) out_ip->clear(); |
216 | 1 | return Status::InternalError( |
217 | 1 | "Hostname {} was concurrently evicted; stale-fallback not reinserted", hostname); |
218 | 1 | } |
219 | 275 | auto it = cache.find(hostname); |
220 | 275 | if (it == cache.end() || it->second != real_ip) { |
221 | 17 | cache[hostname] = real_ip; |
222 | 17 | LOG(INFO) << "update hostname " << hostname << "'s ip to " << real_ip; |
223 | 17 | } |
224 | | // DNS resolved successfully — remove any negative cache tombstone so |
225 | | // subsequent get() calls go straight to the main cache. |
226 | 275 | _negative_cache.erase(hostname); |
227 | 275 | if (out_ip) *out_ip = real_ip; |
228 | | // Read failure_count under the same lock we already hold, so _refresh_once |
229 | | // does not need a second lock acquisition to decide on eviction. |
230 | 275 | if (out_failures) { |
231 | 258 | auto fc_it = failure_count.find(hostname); |
232 | 258 | *out_failures = fc_it != failure_count.end() ? fc_it->second : 0; |
233 | 258 | } |
234 | 275 | return Status::OK(); |
235 | 276 | } |
236 | | |
237 | 263 | void DNSCache::_refresh_once() { |
238 | | // Purge expired negative-cache entries to prevent unbounded map growth. |
239 | 263 | { |
240 | 263 | std::unique_lock<std::shared_mutex> lock(mutex); |
241 | 263 | auto now = std::chrono::steady_clock::now(); |
242 | 263 | for (auto it = _negative_cache.begin(); it != _negative_cache.end();) { |
243 | 0 | it = (now >= it->second) ? _negative_cache.erase(it) : std::next(it); |
244 | 0 | } |
245 | 263 | } |
246 | | |
247 | 263 | std::unordered_set<std::string> keys; |
248 | 263 | { |
249 | 263 | std::shared_lock<std::shared_mutex> lock(mutex); |
250 | 263 | std::transform(cache.begin(), cache.end(), std::inserter(keys, keys.end()), |
251 | 263 | [](const auto& pair) { return pair.first; }); |
252 | 263 | } |
253 | 263 | for (auto& key : keys) { |
254 | 259 | uint32_t failures = 0; |
255 | 259 | Status st = _update(key, &failures); |
256 | 259 | if (!st.ok()) { |
257 | | // _update returns an error either when _resolve_hostname returns "" |
258 | | // (no fallback IP) or when a stale fallback was suppressed because |
259 | | // the host was concurrently evicted. Either way, log and move on; |
260 | | // the threshold check below handles the normal eviction path. |
261 | 1 | LOG(WARNING) << "Failed to update DNS cache for hostname " << key << ": " |
262 | 1 | << st.to_string(); |
263 | 1 | } |
264 | | // Evict hostnames that have failed to resolve for too long. |
265 | | // This avoids two pathological symptoms after a backend is dropped |
266 | | // from the cluster and its DNS record is removed: |
267 | | // 1) be.WARNING gets flooded with `failed to get ip from host`. |
268 | | // 2) brpc keeps re-using the stale IP from cache, producing |
269 | | // `Fail to wait EPOLLOUT ... Connection timed out`. |
270 | 259 | int32_t threshold = config::dns_cache_max_consecutive_failures; |
271 | 259 | if (threshold > 0 && failures >= static_cast<uint32_t>(threshold)) { |
272 | | // Re-read failure_count under the mutex that also performs the erase |
273 | | // to fence any concurrent success that cleared the counter between |
274 | | // _update() returning and this point. |
275 | 6 | if (_erase_if_still_failing(key, static_cast<uint32_t>(threshold))) { |
276 | 6 | LOG(WARNING) << "Evicting hostname " << key << " from DNS cache after " |
277 | 6 | << failures << " consecutive resolution failures"; |
278 | 6 | } |
279 | 6 | } |
280 | 259 | } |
281 | 263 | } |
282 | | |
283 | 11 | void DNSCache::_refresh_cache() { |
284 | 269 | while (!stop_refresh) { |
285 | 258 | { |
286 | 258 | std::unique_lock<std::mutex> lk(_cv_mutex); |
287 | | // Wake up either after 1 minute or when the destructor signals stop. |
288 | 512 | _cv.wait_for(lk, std::chrono::minutes(1), [this] { return stop_refresh.load(); }); |
289 | 258 | } |
290 | 258 | if (!stop_refresh) { |
291 | 247 | _refresh_once(); |
292 | 247 | } |
293 | 258 | } |
294 | 11 | } |
295 | | |
296 | | } // end of namespace doris |