guard/docs/research/envoy.md
loki5512344 cf9608ce5d
Initial commit: Rampart v0.2.0
Multi-layer DDoS protection for Minecraft servers.

- rampart-core: Edge node with XDP/eBPF + Rust L7 filtering
- rampart-manager: REST API with JWT auth, Redis sync
- rampart-cli: CLI tool for operators
- velocity-plugin: Domain check, HMAC verify, server registry, load balancer
- paper-plugin: Auto-registration, heartbeat, HMAC verify
- dashboard: React + Vite web UI for management
2026-07-20 20:53:32 +02:00

240 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Envoy - EWMA, Circuit Breaker, xDS API
> Актуально: v0.5+
> Envoy как референс для алгоритмов балансировки.
---
## EWMA балансировщик (как в Envoy)
### Почему LEAST_CONN недостаточно
```
Проблема:
Сервер A: 50 игроков, TPS 20 (быстрый)
Сервер B: 48 игроков, TPS 12 (лагающий)
LEAST_CONN выберет B → плохо
EWMA учитывает реальное время ответа:
Сервер A: быстро → высокий score → больше игроков
Сервер B: медленно → низкий score → меньше игроков
```
### Формула
```
effective_load = rtt_ewma × (active_requests + 1)
rtt_ewma_new = α × rtt_ewma_old + (1 - α) × rtt_sample
α = 0.95 (decay, параметр сглаживания)
```
### Реализация
```rust
// balancer/ewma.rs
use std::sync::atomic::{AtomicU64, Ordering};
pub struct EwmaBackend {
pub name: String,
rtt_ewma_us: AtomicU64, // EWMA в микросекундах
active: AtomicU64,
}
impl EwmaBackend {
pub fn new(name: String) -> Self {
Self {
name,
rtt_ewma_us: AtomicU64::new(1000), // стартовое значение 1мс
active: AtomicU64::new(0),
}
}
pub fn record_rtt(&self, rtt: Duration) {
let sample = rtt.as_micros() as u64;
let old = self.rtt_ewma_us.load(Ordering::Relaxed);
// EWMA: 95% старое + 5% новое измерение
let new = (old * 95 + sample * 5) / 100;
self.rtt_ewma_us.store(new, Ordering::Relaxed);
}
pub fn effective_load(&self) -> u64 {
let rtt = self.rtt_ewma_us.load(Ordering::Relaxed);
let active = self.active.load(Ordering::Relaxed);
rtt.saturating_mul(active + 1)
}
pub fn acquire(&self) { self.active.fetch_add(1, Ordering::Relaxed); }
pub fn release(&self) { self.active.fetch_sub(1, Ordering::Relaxed); }
}
pub struct EwmaBalancer {
backends: Vec<Arc<EwmaBackend>>,
}
impl EwmaBalancer {
pub fn select(&self) -> Option<Arc<EwmaBackend>> {
self.backends.iter()
.min_by_key(|b| b.effective_load())
.cloned()
}
}
```
---
## Circuit Breaker
```
CLOSED → нормальная работа, трафик идёт
↓ TPS < 12 или timeout > 3 сек подряд (N раз)
OPEN → сервер выведен из ротации
↓ через 30 сек (recovery timeout)
HALF_OPEN → пробный трафик (1 соединение)
↓ успешно → CLOSED
↓ снова ошибка → OPEN (увеличиваем timeout × 2)
```
```rust
// balancer/circuit_breaker.rs
pub enum State {
Closed,
Open { tripped_at: Instant, timeout: Duration },
HalfOpen,
}
pub struct CircuitBreaker {
state: State,
failure_count: u32,
failure_threshold: u32, // сколько ошибок до OPEN
}
impl CircuitBreaker {
pub fn should_route(&mut self) -> bool {
match &self.state {
State::Closed => true,
State::Open { tripped_at, timeout } => {
if tripped_at.elapsed() >= *timeout {
self.state = State::HalfOpen;
true
} else {
false
}
}
State::HalfOpen => true,
}
}
pub fn record_success(&mut self) {
self.failure_count = 0;
self.state = State::Closed;
}
pub fn record_failure(&mut self) {
self.failure_count += 1;
if self.failure_count >= self.failure_threshold {
let timeout = match &self.state {
State::Open { timeout, .. } => *timeout * 2, // exponential backoff
_ => Duration::from_secs(30),
};
self.state = State::Open {
tripped_at: Instant::now(),
timeout: timeout.min(Duration::from_secs(300)), // max 5 мин
};
}
}
}
```
---
## Health Scoring
```rust
pub fn calculate_health_score(server: &ServerEntry) -> f64 {
let tps_score = (server.tps / 20.0).min(1.0); // 0..1
let player_score = 1.0 - (server.online as f64 / server.max_players as f64);
let mspt_score = (1.0 - server.mspt / 50.0).max(0.0); // 50ms MSPT = 0 score
let ram_score = 1.0 - (server.ram_used as f64 / server.ram_max as f64);
// Взвешенная сумма
tps_score * 0.40
+ player_score * 0.30
+ mspt_score * 0.20
+ ram_score * 0.10
}
// Балансировщик выбирает по score вместо LEAST_CONN
pub fn select_by_score(servers: &[ServerEntry]) -> Option<&ServerEntry> {
servers.iter()
.filter(|s| calculate_health_score(s) > 0.3) // минимальный порог
.max_by(|a, b| {
calculate_health_score(a)
.partial_cmp(&calculate_health_score(b))
.unwrap()
})
}
```
---
## Consistent Hashing (друзья на одном Hub)
```rust
// Игроки с одной группой попадают на один Hub
// При добавлении новых Hubs - минимальная миграция игроков
use std::collections::BTreeMap;
pub struct ConsistentHash {
ring: BTreeMap<u64, String>, // hash → server_name
vnodes: u32, // виртуальные ноды (больше = равномернее)
}
impl ConsistentHash {
pub fn new(vnodes: u32) -> Self {
Self { ring: BTreeMap::new(), vnodes }
}
pub fn add_server(&mut self, name: &str) {
for i in 0..self.vnodes {
let key = hash(&format!("{}-{}", name, i));
self.ring.insert(key, name.to_string());
}
}
pub fn get_server(&self, player_uuid: &Uuid) -> Option<&str> {
if self.ring.is_empty() { return None; }
let hash = hash(&player_uuid.to_string());
// Идём по кольцу вправо от hash
self.ring.range(hash..)
.next()
.or_else(|| self.ring.iter().next())
.map(|(_, name)| name.as_str())
}
}
// Применение: для хабов, где важно чтобы друзья были рядом
// Для game серверов - EWMA (важна нагрузка, не стабильность)
```
---
## xDS API (Envoy паттерн для динамической конфигурации)
> Актуально v0.6+ - если нод станет 100+
xDS - протокол от Envoy/Istio для динамической доставки конфигурации нодам. Вместо того чтобы каждая нода поллила Redis - Manager пушит изменения через gRPC stream.
```
Manager (xDS сервер)
↓ gRPC stream (двунаправленный)
Edge ноды / LB ноды (xDS клиенты)
При изменении конфига (новый сервер, новое правило):
Manager → push → все ноды получают обновление мгновенно
Нет поллинга, нет задержки
```