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