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
This commit is contained in:
commit
cf9608ce5d
159 changed files with 15341 additions and 0 deletions
240
docs/research/envoy.md
Normal file
240
docs/research/envoy.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# 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 → все ноды получают обновление мгновенно
|
||||
Нет поллинга, нет задержки
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue