Pular para o conteúdo principal

Afinidade

A afinidade de cliente (sticky sessions) garante que conexões repetidas do mesmo IP de cliente sejam roteadas para o mesmo backend. Isso é crítico para protocolos stateful e aplicações baseadas em sessão.

Visão Geral

O edgeProxy mantém uma tabela de binding que mapeia IPs de cliente para IDs de backend. O diagrama a seguir mostra o ciclo de vida do binding:

Client Affinity

Configuração

VariávelPadrãoDescrição
EDGEPROXY_BINDING_TTL_SECS600Tempo de vida do binding (10 minutos)
EDGEPROXY_BINDING_GC_INTERVAL_SECS60Intervalo de limpeza

Estruturas de Dados

ClientKey

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ClientKey {
pub client_ip: IpAddr,
}

Binding

#[derive(Clone, Debug)]
pub struct Binding {
pub backend_id: String,
pub created_at: Instant,
pub last_seen: Instant,
}

Armazenamento

Bindings são armazenados em um DashMap lock-free:

pub struct RcProxyState {
// ...
pub bindings: Arc<DashMap<ClientKey, Binding>>,
// ...
}

Ciclo de Vida

1. Nova Conexão

Quando um cliente conecta pela primeira vez:

// Sem binding existente - usar load balancer
let backend = pick_backend(&backends, local_region, client_region);

// Criar novo binding
state.bindings.insert(
ClientKey { client_ip },
Binding {
backend_id: backend.id.clone(),
created_at: Instant::now(),
last_seen: Instant::now(),
},
);

2. Conexões Subsequentes

Quando o mesmo cliente reconecta:

// Verificar binding existente
if let Some(mut entry) = state.bindings.get_mut(&client_key) {
// Atualizar timestamp last_seen
entry.last_seen = Instant::now();

// Usar backend existente
chosen_backend_id = Some(entry.backend_id.clone());
}

3. Expiração do Binding

Bindings expiram após BINDING_TTL_SECS de inatividade:

pub fn start_binding_gc(
bindings: Arc<DashMap<ClientKey, Binding>>,
ttl: Duration,
interval: Duration,
) {
tokio::spawn(async move {
loop {
sleep(interval).await;

let now = Instant::now();
bindings.retain(|_, binding| {
now.duration_since(binding.last_seen) < ttl
});
}
});
}

4. Falha do Backend

Se o backend vinculado ficar não saudável:

// Lookup do backend do binding
let backend = rt.backends
.iter()
.find(|b| b.id == backend_id && b.healthy)
.cloned();

// Se não encontrado ou não saudável
if backend.is_none() {
// Remover binding obsoleto
state.bindings.remove(&client_key);

// Fallback para load balancer
return pick_backend(&backends, ...);
}

Casos de Uso

1. Aplicações Stateful

Jogos, servidores de chat ou qualquer aplicação mantendo estado de conexão:

Cliente A ──▶ game-server-1 (estado do jogador)
Cliente A ──▶ game-server-1 (mesmo servidor, estado preservado)

2. Protocolos Baseados em Sessão

Aplicações usando cookies ou tokens de sessão:

Cliente B ──▶ web-server-2 (sessão criada)
Cliente B ──▶ web-server-2 (sessão recuperada)

3. Pool de Conexões

Conexões de banco de dados ou conexões HTTP persistentes:

Cliente C ──▶ db-replica-1 (conexão 1)
Cliente C ──▶ db-replica-1 (conexão 2, mesma réplica)

Performance

Uso de Memória

Cada binding usa aproximadamente:

ClientKey: 16 bytes (IPv4) ou 40 bytes (IPv6)
Binding: ~80 bytes (String + 2 Instants)
Overhead DashMap: ~64 bytes por entrada

Total: ~160 bytes por cliente

Para 1 milhão de clientes: ~160 MB

Garbage Collection

GC executa a cada BINDING_GC_INTERVAL_SECS:

// Iterar todos os bindings
bindings.retain(|_, binding| {
now.duration_since(binding.last_seen) < ttl
});

Complexidade de tempo: O(n) onde n = total de bindings

Concorrência

DashMap fornece leituras lock-free e escritas sharded:

  • Leitura (lookup de binding): Sem bloqueio
  • Escrita (criar/atualizar binding): Locking por-shard
  • GC (retain): Locks breves por-shard

Tuning

Conexões de Alta Frequência

Para clientes fazendo muitas conexões curtas:

# TTL menor para liberar memória mais rápido
export EDGEPROXY_BINDING_TTL_SECS=60

# GC mais frequente
export EDGEPROXY_BINDING_GC_INTERVAL_SECS=10

Sessões de Longa Duração

Para conexões persistentes ou reconexões infrequentes:

# TTL maior para manter afinidade
export EDGEPROXY_BINDING_TTL_SECS=3600 # 1 hora

# GC menos frequente (menor CPU)
export EDGEPROXY_BINDING_GC_INTERVAL_SECS=300

Alto Volume de Clientes

Para milhões de clientes únicos:

# TTL agressivo para limitar memória
export EDGEPROXY_BINDING_TTL_SECS=300

# GC frequente
export EDGEPROXY_BINDING_GC_INTERVAL_SECS=30

Limitações

1. Apenas Baseado em IP

Afinidade é baseada no IP do cliente, não em:

  • Cookies HTTP
  • Tickets de sessão TLS
  • Tokens de aplicação

Implicação: Clientes atrás de NAT compartilham afinidade.

2. Sem Sync Cross-POP

Bindings são locais para cada instância de POP:

Cliente → POP-SA → sa-node-1 (binding criado)
Cliente → POP-US → us-node-1 (binding diferente!)

Solução: Usar geo-routing DNS para garantir que clientes atinjam POPs consistentes.

Próximos Passos