This commit is contained in:
+139
@@ -89,6 +89,10 @@ const REPORT_INTERVAL: Duration = Duration::from_secs(30);
|
||||
const FAILOVER_AFTER: u64 = 5;
|
||||
/// Сколько доменов помнить для панели.
|
||||
const STATS_CAP: usize = 256;
|
||||
/// Сколько последних запросов держать для панели. Двести — это примерно
|
||||
/// полминуты жизни машины под нагрузкой и несколько часов простоя, то есть
|
||||
/// ровно то окно, в котором ещё имеет смысл спрашивать «а что сейчас было».
|
||||
const RECENT_CAP: usize = 200;
|
||||
/// Потолок одновременных TCP-запросов. По TCP ходят единицы клиентов, но
|
||||
/// принимать их без счёта — способ остаться без дескрипторов.
|
||||
const MAX_TCP: usize = 32;
|
||||
@@ -419,6 +423,112 @@ impl Attempts {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── лента последних запросов ──────────────────────────────── */
|
||||
|
||||
/// Что резолвер сделал с запросом.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Verdict {
|
||||
/// Отсечён по списку, наверх не пошёл.
|
||||
Blocked,
|
||||
/// Ответ взят из своего кэша.
|
||||
Cached,
|
||||
/// Ушёл вышестоящему серверу.
|
||||
Upstream,
|
||||
}
|
||||
|
||||
/// Одна запись ленты.
|
||||
///
|
||||
/// Имя лежит прямо в структуре массивом, а не отдельной строкой в куче. Кольцо
|
||||
/// заводится один раз при запуске, и запись в него не трогает распределитель
|
||||
/// памяти — иначе на каждый запрос в системе приходилось бы выделение и
|
||||
/// освобождение, а горячий путь резолвера построен ровно на том, чтобы их не
|
||||
/// было. Ценой идут постоянные 56 КБ, что на фоне кэша ответов незаметно.
|
||||
struct Seen {
|
||||
name: [u8; MAX_NAME],
|
||||
len: u8,
|
||||
qtype: u16,
|
||||
verdict: Verdict,
|
||||
at: Instant,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RecentQuery {
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub verdict: Verdict,
|
||||
pub ago_secs: u64,
|
||||
}
|
||||
|
||||
/// Кольцо: доходит до предела и начинает затирать самое старое.
|
||||
///
|
||||
/// Живёт только в памяти и умирает вместе со службой. На диск не попадает
|
||||
/// намеренно — это перечень всего, что человек открывал, и хранить такое
|
||||
/// дольше, чем нужно для ответа на вопрос «что сейчас произошло», незачем.
|
||||
#[derive(Default)]
|
||||
struct Recent {
|
||||
items: Vec<Seen>,
|
||||
/// Куда писать следующую запись; она же — самая старая, когда кольцо полно.
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl Recent {
|
||||
fn record(&mut self, name: &str, qtype: u16, verdict: Verdict, at: Instant) {
|
||||
if self.items.len() < RECENT_CAP {
|
||||
self.items.push(Seen {
|
||||
name: [0; MAX_NAME],
|
||||
len: 0,
|
||||
qtype: 0,
|
||||
verdict: Verdict::Upstream,
|
||||
at,
|
||||
});
|
||||
}
|
||||
// Пишем на месте, а не собираем запись целиком и не двигаем её: имя
|
||||
// копируется ровно на свою длину, а не на все 255 байт массива.
|
||||
let slot = &mut self.items[self.next];
|
||||
let len = name.len().min(MAX_NAME);
|
||||
slot.name[..len].copy_from_slice(&name.as_bytes()[..len]);
|
||||
slot.len = len as u8;
|
||||
slot.qtype = qtype;
|
||||
slot.verdict = verdict;
|
||||
slot.at = at;
|
||||
|
||||
self.next = (self.next + 1) % RECENT_CAP;
|
||||
}
|
||||
|
||||
/// Отдаёт ленту от свежего к старому — в том порядке, в каком её читают.
|
||||
fn snapshot(&self, now: Instant) -> Vec<RecentQuery> {
|
||||
let n = self.items.len();
|
||||
(0..n)
|
||||
.map(|k| {
|
||||
let slot = &self.items[(self.next + n - 1 - k) % n];
|
||||
RecentQuery {
|
||||
name: String::from_utf8_lossy(&slot.name[..slot.len as usize]).into_owned(),
|
||||
kind: type_name(slot.qtype),
|
||||
verdict: slot.verdict,
|
||||
ago_secs: now.saturating_duration_since(slot.at).as_secs(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Название типа записи для панели. Считается только на запрос из панели, на
|
||||
/// горячем пути хранится сырое число.
|
||||
fn type_name(qtype: u16) -> String {
|
||||
match qtype {
|
||||
TYPE_A => "A".to_string(),
|
||||
TYPE_AAAA => "AAAA".to_string(),
|
||||
5 => "CNAME".to_string(),
|
||||
12 => "PTR".to_string(),
|
||||
15 => "MX".to_string(),
|
||||
16 => "TXT".to_string(),
|
||||
33 => "SRV".to_string(),
|
||||
65 => "HTTPS".to_string(),
|
||||
other => format!("#{other}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Счётчики для панели. Обновляются на горячем пути, поэтому атомарные:
|
||||
/// блокировка ради статистики — худшая из возможных причин ждать.
|
||||
#[derive(Default)]
|
||||
@@ -441,6 +551,7 @@ pub struct DnsStatus {
|
||||
pub timeouts: u64,
|
||||
pub error: Option<String>,
|
||||
pub attempts: Vec<BlockedDomain>,
|
||||
pub recent: Vec<RecentQuery>,
|
||||
}
|
||||
|
||||
/* ── общее состояние ───────────────────────────────────────── */
|
||||
@@ -450,6 +561,7 @@ pub struct Dns {
|
||||
blocklist: watch::Sender<Arc<Blocklist>>,
|
||||
counters: Arc<Counters>,
|
||||
attempts: Arc<Mutex<Attempts>>,
|
||||
recent: Arc<Mutex<Recent>>,
|
||||
/// Строка с адресом вышестоящего сервера либо причина, почему резолвер не
|
||||
/// работает. Панель должна показывать это раньше, чем родитель заметит, что
|
||||
/// блокировка молчит.
|
||||
@@ -463,6 +575,7 @@ impl Dns {
|
||||
blocklist,
|
||||
counters: Arc::default(),
|
||||
attempts: Arc::default(),
|
||||
recent: Arc::default(),
|
||||
state: Mutex::new((false, String::new(), None)),
|
||||
}
|
||||
}
|
||||
@@ -486,6 +599,11 @@ impl Dns {
|
||||
if let Ok(mut s) = self.state.lock() {
|
||||
*s = (false, String::new(), None);
|
||||
}
|
||||
// Лента уходит вместе с резолвером: выключенная блокировка не должна
|
||||
// оставлять после себя перечень того, что человек открывал.
|
||||
if let Ok(mut recent) = self.recent.lock() {
|
||||
*recent = Recent::default();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> DnsStatus {
|
||||
@@ -512,6 +630,12 @@ impl Dns {
|
||||
.map(|s| s.clone())
|
||||
.unwrap_or((false, String::new(), None));
|
||||
|
||||
let recent = self
|
||||
.recent
|
||||
.lock()
|
||||
.map(|r| r.snapshot(now))
|
||||
.unwrap_or_default();
|
||||
|
||||
DnsStatus {
|
||||
listening,
|
||||
upstream,
|
||||
@@ -521,6 +645,7 @@ impl Dns {
|
||||
timeouts: self.counters.timeouts.load(Ordering::Relaxed),
|
||||
error,
|
||||
attempts,
|
||||
recent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -550,6 +675,7 @@ struct Resolver {
|
||||
blocklist: watch::Receiver<Arc<Blocklist>>,
|
||||
counters: Arc<Counters>,
|
||||
attempts: Arc<Mutex<Attempts>>,
|
||||
recent: Arc<Mutex<Recent>>,
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
@@ -557,6 +683,14 @@ impl Resolver {
|
||||
self.upstreams[self.active.load(Ordering::Relaxed) % self.upstreams.len()]
|
||||
}
|
||||
|
||||
/// Заносит запрос в ленту последних. Кольцо заведено заранее, поэтому
|
||||
/// вызов не выделяет памяти и держит мьютекс считаные наносекунды.
|
||||
fn note(&self, name: &str, qtype: u16, verdict: Verdict, now: Instant) {
|
||||
if let Ok(mut recent) = self.recent.lock() {
|
||||
recent.record(name, qtype, verdict, now);
|
||||
}
|
||||
}
|
||||
|
||||
/// Решает судьбу запроса. Возвращает готовый ответ, если отвечаем сами.
|
||||
///
|
||||
/// Здесь не должно появиться ни одного `await`: пока задача разбирает
|
||||
@@ -579,6 +713,7 @@ impl Resolver {
|
||||
attempts.record(name, now);
|
||||
}
|
||||
self.counters.blocked.fetch_add(1, Ordering::Relaxed);
|
||||
self.note(name, q.qtype, Verdict::Blocked, now);
|
||||
return match write_blocked(pkt, &q) {
|
||||
Some(n) => Decision::Answer(n),
|
||||
None => Decision::Drop,
|
||||
@@ -606,9 +741,12 @@ impl Resolver {
|
||||
if let Some(n) = cache.get(key, packet_id(pkt), pkt, now) {
|
||||
pkt[HEADER..q.end].copy_from_slice(&asked[..asked_len]);
|
||||
self.counters.cache_hits.fetch_add(1, Ordering::Relaxed);
|
||||
drop(cache); // мьютекс кэша не держим на время записи в ленту
|
||||
self.note(name, q.qtype, Verdict::Cached, now);
|
||||
return Decision::Answer(n);
|
||||
}
|
||||
}
|
||||
self.note(name, q.qtype, Verdict::Upstream, now);
|
||||
Decision::Forward(Some(key.into()))
|
||||
}
|
||||
}
|
||||
@@ -965,6 +1103,7 @@ pub async fn run(state: Shared, upstreams: Vec<SocketAddr>, shutdown: watch::Rec
|
||||
blocklist: state.dns.blocklist.subscribe(),
|
||||
counters: Arc::clone(&state.dns.counters),
|
||||
attempts: Arc::clone(&state.dns.attempts),
|
||||
recent: Arc::clone(&state.dns.recent),
|
||||
upstreams,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user