Rust Logo

Sistemas Distribuídos com Rust

Concorrência, paralelismo e tolerância a falhas

🏆 UFPR - Licenciatura em Computação

Introdução

Por que Rust?

  • Zero-cost abstractions
  • Sem data races
  • Ownership model
  • Ecossistema maduro

Segurança Garantida

Rust elimina classes inteiras de bugs em sistemas distribuídos:

Problema Solução Rust
Data races Checagem em tempo de compilação
Memory leaks Sistema de ownership

Aplicações Práticas

O modelo de programação do Rust é particularmente adequado para aplicações que exigem alta performance e segurança, como servidores de backend, sistemas embarcados e soluções tolerantes a falhas em redes distribuídas.


🌍 Casos Reais na Indústria

Linkerd 2.x (Service Mesh)

🚀 1ms overhead 🦀 100% Rust 🔒 mTLS nativo

Por que Rust?

  • Performance determinística: Sem GC para latência previsível em proxies
  • Segurança: Proteção contra vulnerabilidades de memória em sistemas críticos
  • Concorrência segura: Manipulação eficiente de +10k conexões concorrentes

Arquitetura Chave:

┌───────────────────────────────┐
│   Aplicação                  │
│   (Pod Kubernetes)           │
└──────────────┬───────────────┘
               │
┌──────────────▼───────────────┐
│  Linkerd-Proxy (Rust)        │
│  ┌─────────────────────────┐ │
│  │  Protocolo HTTP/2       │ │
│  │  + mTLS automático      │ │
│  └─────────────────────────┘ │
└──────────────┬───────────────┘
               │
┌──────────────▼───────────────┐
│   Service Mesh Control Plane │
└──────────────────────────────┘

Trecho Relevante (Linkerd-proxy):

// Filtro de autorização simplificado
impl<S> tower::Service<http::Request<hyper::Body>> for AuthFilter<S>
where
    S: Service<http::Request<hyper::Body>>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = S::Future;

    fn poll_ready(&mut self, cx: &mut Context<>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: http::Request<hyper::Body>) -> Self::Future {
        if !authorize(req.headers()) {
            return async { Err("Unauthorized".into()) }.boxed();
        }
        self.inner.call(req)
    }
}
Crates principais: tokio, hyper, tower, rustls

Impacto Mensurável:

70%
Menor uso de CPU vs Envoy (C++)
0
Vulnerabilidades CVEs em Rust
10μs
P99 latency add

TiKV (Key-Value Store Distribuído)

🏗️ CNCF Graduated 🦀 Rust Core ⚡ 1M+ ops/sec

Arquitetura com Rust:

  • Raft Consensus: Implementação customizada em Rust
  • Storage Engine: RocksDB + abstrações seguras
  • Processamento Paralelo: Thread pools isoladas com Rayon

Fluxo de Escrita:

  1. Cliente envia operação para líder Raft
  2. Líder replica para >50% dos nós
  3. Commit aplicado ao storage engine
  4. Resposta ao cliente
// Trecho simplificado do Raft Store
impl RaftStore {
    pub async fn propose(&self, cmd: Command) -> Result<RaftResponse> {
        let ctx = self.raft_ctx.lock().await;
        let data = cmd.to_bytes();
        
        // Submete à replicação Raft
        let res = self.raft_group.propose(ctx, data).await?;
        
        // Aplica quando commitado
        if let Some(response) = self.apply_responses.get(&res.index) {
            return Ok(response.clone());
        }
        Err(Error::Timeout)
    }
}
Dependências chave: async-raft, rocksdb, grpcio

🔍 Fundamentos Teóricos: Rust vs Outras Linguagens

Comparação de Linguagens para Sistemas Distribuídos

Rust oferece um equilíbrio único entre performance e segurança em sistemas distribuídos:

Vantagens do Rust

  • ✅ Segurança de memória sem garbage collector
  • ✅ Concorrência sem data races em tempo de compilação
  • ✅ Performance comparável a C/C++
  • ✅ Compilação para WASM (edge computing)

Cenários Típicos

  • 🦀 Rust: Middlewares críticos, proxies, sistemas de coordenação
  • 🐹 Go: Microsserviços, APIs, CLIs distribuídos
  • ☕ Java: Sistemas empresariais com JVM
  • 🐍 Python: Prototipagem, scripts de automação

Arquitetura Típica com Rust

┌─────────────────────────────────────────────────┐
│   Camada de Aplicação (Python/Java/Go)         │
└───────────────┬─────────────────┬───────────────┘
                │ HTTP/gRPC       │
┌───────────────▼─────┐ ┌─────────▼───────────────┐
│   Serviços Rust     │ │   Message Broker        │
│   (High-Perf)       │ │   (Kafka/RabbitMQ)      │
└───────────┬─────────┘ └─────────┬───────────────┘
            │                     │
┌───────────▼─────────────────────▼───────────────┐
│   Componentes Críticos em Rust                 │
│   • Service Mesh (Linkerd)                     │
│   • Sistemas de Consenso (Raft)                │
│   • Data Processing Pipelines                  │
└─────────────────────────────────────────────────┘
    

Rust brilha em componentes onde performance e confiabilidade são críticas.

Tabela Comparativa de Garantias

Característica Rust Go Java C++
Segurança de Memória ✅ Compile-time ✅ GC ✅ GC ❌ Manual
Thread Safety ✅ Garantido ✅ Canal-based ✅ Monitor ❌ Opcional
Performance ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
Startup Time ⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️⭐️
Ecossistema DS ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️

Legenda: GC = Garbage Collector, DS = Distributed Systems

🧠 Conceitos Fundamentais

Concorrência e Segurança de Memória

Rust oferece garantias de segurança em tempo de compilação, evitando condições de corrida e vazamentos de memória através de seu sistema de ownership e borrowing. Isso é especialmente valioso em sistemas distribuídos onde a concorrência é essencial.

// Uso seguro de múltiplas threads com compartilhamento imutável
use std::thread;

fn main() {
    let dados = vec![1, 2, 3, 4];
    
    let handles: Vec<_> = (0..4).map(|i| {
        // Usamos move para transferir ownership da cópia
        thread::spawn(move || {
            println!("Thread {} processando valor: {}", i, dados[i]);
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }
    
    // Dados originais ainda estão disponíveis aqui
    println!("Dados originais: {:?}", dados);
}

Note como Rust permite compartilhamento seguro de dados entre threads quando eles são imutáveis. Para dados mutáveis, Rust exige mecanismos como Arc<Mutex<T>> para garantir segurança.

Modelo de Ownership

O sistema de ownership do Rust previne acessos inválidos e uso após liberação de memória através de três regras fundamentais:

  1. Cada valor em Rust tem um owner (dono)
  2. Só pode haver um owner por vez
  3. Quando o owner sai de escopo, o valor é liberado
// Demonstração de borrowing e ownership
fn main() {
    let mut mensagem = String::from("Processamento");
    
    // Empresta uma referência imutável
    calcular_tamanho(&mensagem);
    
    // Empresta uma referência mutável
    modificar(&mut mensagem);
    
    println!("Mensagem final: {}", mensagem);
}

fn calcular_tamanho(s: &String) -> usize {
    s.len() // s é uma referência imutável
}

fn modificar(s: &mut String) {
    s.push_str(" distribuído"); // s é uma referência mutável
}

Este exemplo mostra como Rust gerencia empréstimos (borrowing) de forma segura, permitindo ou uma única referência mutável ou múltiplas referências imutáveis.

Tipos de Comunicação

Rust oferece vários padrões para comunicação entre threads e processos, essenciais para sistemas distribuídos:

1. Channels (Múltiplos Produtores, Único Consumidor)

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();
    
    // Produtor 1
    let tx1 = tx.clone();
    thread::spawn(move || {
        let mensagens = vec!["Task1", "Task2", "Task3"];
        for msg in mensagens {
            tx1.send(msg).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });
    
    // Produtor 2
    thread::spawn(move || {
        let mensagens = vec!["Ack1", "Ack2"];
        for msg in mensagens {
            tx.send(msg).unwrap();
            thread::sleep(Duration::from_secs(2));
        }
    });
    
    // Consumidor
    for recebida in rx {
        println!("Processando: {}", recebida);
    }
}

2. Shared State com Arc<Mutex<T>>

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let contador = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..4 {
        let contador = Arc::clone(&contador);
        let handle = thread::spawn(move || {
            let mut num = contador.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Total: {}", *contador.lock().unwrap());
}

Estes padrões são fundamentais para construir sistemas distribuídos seguros e eficientes em Rust.


Pré Requisitos Rust

⚙️ Configuração do Ambiente Rust

Instalação do Rust

Para começar a trabalhar com Rust, você precisa instalar o compilador e o gerenciador de pacotes:

Linux/macOS

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

Windows

Baixe e execute o rustup-init.exe

Verifique a instalação:

rustc --version
cargo --version

Ferramentas Úteis

Para desenvolvimento eficiente:

  • Rust Analyzer: Extensão para VS Code/IDEs (autocompletar, verificação em tempo real)
  • Cargo: Gerenciador de pacotes e sistema de build (já vem com a instalação)

Testando Online (REPL)

Se não puder instalar localmente, use:

Para os exemplos assíncronos (Tokio/Reqwest), você precisará de instalação local pois o playground não suporta todas as bibliotecas.

Configurando Dependências

Para os exemplos desta aula, adicione ao seu Cargo.toml:

[dependencies]
tokio = { version = "1.0", features = ["full"] }
reqwest = "0.11"
crossbeam = "0.8"
serde = { version = "1.0", features = ["derive"] }

Depois execute:

cargo build

🦀 Rust na Prática

Trabalhando com Threads

Rust permite a criação de múltiplas threads com segurança em tempo de compilação, evitando condições de corrida.

use std::thread;

fn main() {
    let t1 = thread::spawn(|| {
        println!(\"Thread 1 ativa\");
    });

    let t2 = thread::spawn(|| {
        println!(\"Thread 2 ativa\");
    });

    t1.join().unwrap();
    t2.join().unwrap();
}

Programação Assíncrona com Tokio

Com o runtime tokio, é possível criar aplicações assíncronas eficientes para servidores e clientes distribuídos.

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let tarefa1 = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        println!(\"Tarefa 1 concluída\");
    });

    let tarefa2 = tokio::spawn(async {
        println!(\"Tarefa 2 concluída\");
    });

    tarefa1.await.unwrap();
    tarefa2.await.unwrap();
}

HTTP com a Crate Reqwest

Podemos consumir APIs com segurança e performance usando reqwest.

use reqwest;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let resposta = reqwest::get(\"https://httpbin.org/ip\").await?
        .text().await?;
    println!(\"Resposta: {}\", resposta);
    Ok(())
}

Mensageria com Crate Crossbeam

O canal crossbeam permite comunicação segura entre threads, útil para arquiteturas com múltiplos workers.

use crossbeam::channel;
use std::thread;

fn main() {
    let (tx, rx) = channel::unbounded();

    thread::spawn(move || {
        tx.send(\"Mensagem distribuída\").unwrap();
    });

    println!(\"Recebido: {}\", rx.recv().unwrap());
}

⚡ Padrões Avançados em Sistemas Distribuídos

Padrões Essenciais com Rust

Estes são os padrões fundamentais para construir sistemas resilientes:

🔗 Circuit Breaker

Previne cascata de falhas abrindo o circuito quando erros atingem um limiar.

use tower::timeout::Timeout;
use tower::retry::Retry;
use tower::buffer::Buffer;
use tower::limit::RateLimit;

// Stack de resiliência típica
let service = Timeout::new(
    Retry::new(
        CircuitBreaker::new(
            RateLimit::new(
                Buffer::new(MyService, 10),
                100 // req/s
            )
        ),
        ExponentialBackoff::default()
    ),
    Duration::from_secs(3)
);
Quando usar: Chamadas HTTP, acessos a bancos de dados
Crates recomendadas: tower, rust-circuit-breaker

🔄 Retry com Backoff Exponencial

Reconecta automaticamente com intervalos crescentes.

use backoff::ExponentialBackoff;

let operation = || async {
    reqwest::get("https://api.example.com")
        .await?
        .error_for_status()
};

backoff::future::retry(ExponentialBackoff::default(), operation).await;
Quando usar: Conexões instáveis, serviços cloud
Crates: backoff, tokio-retry

🚧 Bulkhead (Compartimentação)

Isola recursos para evitar falhas em cascata.

use tokio::sync::Semaphore;

let semaphore = Arc::new(Semaphore::new(10)); // 10 conexões máximas

async fn process() {
    let _permit = semaphore.acquire().await; // Bloqueia se exceder
    // Operação limitada
}
Quando usar: Limitar conexões, threads, ou uso de CPU
Mecanismo nativo: tokio::sync::Semaphore

🏗️ Casos Reais em Produção

Arquitetura Completa de Serviço Distribuído

Exemplo de serviço Rust pronto para produção integrando múltiplos componentes:

use tonic::{transport::Server, Request, Response, Status};
use etcd_client::{Client, LeaseGrantOptions};
use tokio::signal;
use std::time::Duration;

mod pb {
    tonic::include_proto!("meuservico");
}

#[derive(Default)]
struct MeuServico;

#[tonic::async_trait]
impl pb::servico_server::Servico for MeuServico {
    async fn health_check(
        &self,
        _: Request<pb::HealthCheckRequest>
    ) -> Result<Response<pb::HealthCheckResponse>, Status> {
        Ok(Response::new(pb::HealthCheckResponse { healthy: true }))
    }
}

#[tokio::main]
async fn main() -> Result<, Box<dyn std::error::Error>> {
    // 1. Inicia serviço gRPC
    let addr = "[::1]:50051".parse()?;
    let servico = MeuServico::default();
    
    // 2. Conecta ao etcd para service discovery
    let mut etcd = Client::connect(["http://etcd:2379"], None).await?;
    let lease = etcd.lease_grant(30, None).await?; // TTL de 30s
    
    // 3. Registra instância
    etcd.put(
        format!("services/meuservico/{}", uuid::Uuid::new_v4()),
        addr.to_string(),
        Some(LeaseGrantOptions::new().with_lease(lease.id()))
    ).await?;

    // 4. Health check periódico
    tokio::spawn(async move {
        let mut interval = tokio::time::interval(Duration::from_secs(10));
        loop {
            interval.tick().await;
            etcd.lease_keep_alive(lease.id()).await.unwrap();
        }
    });

    // 5. Shutdown graceful
    Server::builder()
        .add_service(pb::servico_server::ServicoServer::new(servico))
        .serve_with_shutdown(addr, async {
            signal::ctrl_c().await.unwrap();
            println!("Desligando graciosamente...");
        })
        .await?;

    Ok(())
}

Dependências necessárias (Cargo.toml):

[dependencies]
tokio = { version = "1.0", features = ["full"] }
tonic = "0.8"
etcd-client = "0.9"
prost = "0.11"
uuid = { version = "1.0", features = ["v4"] }

Protocol Buffer (meuservico.proto):

syntax = "proto3";
package meuservico;
service Servico {
  rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
}
message HealthCheckRequest {}
message HealthCheckResponse { bool healthy = 1; }

Checklist para Production-Ready

// Exemplo com OpenTelemetry
tracing_subscriber::fmt()
    .with_max_level(Level::INFO)
    .init();

let meter = global::meter("meuservico");
let request_counter = meter.u64_counter("requests").init();

Crates recomendadas: opentelemetry, tracing, metrics

// Configuração via etcd
let watch = etcd.watch("config/meuservico").await?;
tokio::spawn(async move {
    while let Some(resp) = watch.message().await.unwrap() {
        update_config(resp.events);
    }
});

Alternativas: consul, config-rs, envconfig

tokio::select! {
    _ = servidor => println!("Servidor finalizado"),
    _ = signal::ctrl_c() => {
        println!("Recebido SIGINT");
        servidor.shutdown().await;
    }
}

Padrões: Finalizar conexões, esperar tarefas, liberar recursos

🐞 Debugging em Sistemas Distribuídos com Rust

Cenários Problemáticos Comuns

1. Deadlock Clássico

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main() {
    let a = Arc::new(Mutex::new(1));
    let b = Arc::new(Mutex::new(2));

    let t1 = {
        let a = Arc::clone(&a);
        let b = Arc::clone(&b);
        thread::spawn(move || {
            let _ga = a.lock().unwrap(); // Lock A
            thread::sleep(Duration::from_secs(1));
            let _gb = b.lock().unwrap(); // Espera B (DEADLOCK)
            println!("Thread 1 completou");
        })
    };

    let t2 = thread::spawn(move || {
        let _gb = b.lock().unwrap(); // Lock B
        let _ga = a.lock().unwrap(); // Espera A
        println!("Thread 2 completou");
    });

    t1.join().unwrap();
    t2.join().unwrap();
}
🔍 Como Identificar:
  • Programa "trava" sem erro aparente
  • Uso de CPU cai para ~0%
  • Logs param de ser emitidos
🛠️ Solução:
// Estratégia 1: Ordem consistente de locks
let _ga = a.lock().unwrap();
let _gb = b.lock().unwrap();

// Estratégia 2: Timeout
b.try_lock_for(Duration::from_millis(100))?

2. Condição de Corrida em Canais

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    
    thread::spawn(move || {
        tx.send(1).unwrap();
        tx.send(2).unwrap(); // ⚠️ Pode bloquear se consumidor parar
    });

    println!("{}", rx.recv().unwrap());
    // Esqueceu de receber a segunda mensagem
}

Problema: Thread produtor pode ficar bloqueada indefinidamente se o consumidor não ler todas as mensagens.

Solução: Usar try_send() ou buffer limitado:

let (tx, rx) = mpsc::sync_channel(10); // Buffer de 10 itens

Análise de Performance com Flamegraphs

Passo a Passo:

  1. Adicione as dependências:
    [dependencies]
    pprof = { version = "0.11", features = ["flamegraph"] }
    tokio = { version = "1.0", features = ["rt-multi-thread"] }
  2. Instrumente seu código:
    use pprof::ProfilerGuard;
    
    let guard = ProfilerGuard::new(100).unwrap(); // 100Hz
    // ... código sendo analisado ...
    if let Ok(report) = guard.report().build() {
        let file = std::fs::File::create("flamegraph.svg").unwrap();
        report.flamegraph(file).unwrap();
    }
  3. Execute e visualize:
    cargo run --release
    xdg-open flamegraph.svg

Exemplo de Saída:

Exemplo de Flamegraph

Pontos-chave para análise:

  • 🔴 Blocos largos = hotspots de CPU
  • 🟡 Chamadas de sistema = possíveis IO bounds
  • 🟢 Muitas pequenas chamadas = possível thrashing

Exercício Prático: Debugging

Encontre o Problema:

use std::sync::Arc;
use tokio::sync::Mutex;

#[tokio::main]
async fn main() {
    let data = Arc::new(Mutex::new(Vec::new()));
    
    for i in 0..10 {
        let data = Arc::clone(&data);
        tokio::spawn(async move {
            let mut guard = data.lock().await;
            guard.push(i);
            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
            guard.pop(); // ⚠️ Onde está o erro?
        });
    }
}

🧩 Integração com o Ecossistema Rust

Ferramentas Essenciais para Produção

📊

tokio-console

Monitoramento em tempo real de tasks, recursos e channels async

Configuração:
[dependencies]
tokio = { version = "1.0", features = ["full", "tracing"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
// main.rs
use tracing_subscriber::prelude::*;

#[tokio::main]
async fn main() {
    // Configure o subscriber
    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::new(
            "my_crate=info,tokio=debug",
        ))
        .with(tracing_subscriber::fmt::layer())
        .init();

    // Sua aplicação aqui...
}
Uso:
# Instale o console
cargo install tokio-console

# Execute seu projeto com instrumentação
RUSTFLAGS="--cfg tokio_unstable" cargo run

# Em outro terminal:
tokio-console
🔥

pprof

Gera flamegraphs e análise de performance

Configuração:
[dependencies]
pprof = { version = "0.11", features = ["flamegraph"] }
Exemplo de Uso:
use pprof::ProfilerGuard;

fn main() {
    let guard = ProfilerGuard::new(100).unwrap(); // 100Hz
    
    // Código a ser analisado
    heavy_computation();
    
    if let Ok(report) = guard.report().build() {
        let file = std::fs::File::create("flamegraph.svg").unwrap();
        report.flamegraph(file).unwrap();
    }
}
🛡️

cargo-deny

Auditoria de dependências e verificação de segurança

Instalação:
cargo install cargo-deny
Configuração (deny.toml):
[advisories]
db-path = "~/.cargo/advisory-db"
vulnerability = "deny"
unmaintained = "warn"

[bans]
multiple-versions = "deny"
Uso:
# Verificar todas as regras
cargo deny check all

# Verificar atualizações
cargo deny check update

Template para Sistemas Distribuídos

Estrutura Recomendada

cargo new --lib meu-sistema-distribuido
cd meu-sistema-distribuido
.
├── Cargo.toml
├── src/
│   ├── lib.rs      # Core lógico
│   ├── client.rs   # Cliente gRPC/REST
│   ├── server.rs   # Serviço principal
│   └── raft/       # Implementação consenso
├── protos/         # Definições Protocol Buffer
├── scripts/        # Scripts de deploy
└── tests/          # Testes de integração

Template Inicial (Cargo.toml)

[package]
name = "meu-sistema-distribuido"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1.0", features = ["full"] }
tonic = "0.8"              # gRPC
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"            # Logging estruturado

[dev-dependencies]
tokio-test = "0.4"
testcontainers = "0.15"    # Testes com containers

🚀 Comando para Iniciar Rápido

cargo new --template https://github.com/rust-distributed/rust-dist-template \
  --bin meu-sistema-distribuido

Nota: Requer Git instalado. Template fictício para ilustração.

📚 Material Complementar

Mapa Mental de Conceitos

Sistemas Distribuídos
Comunicação
Síncrona (gRPC/tonic)
Assíncrona (Kafka/rdkafka)
Streaming (WebSockets)
Consenso
Raft (async-raft)
Paxos (rust-paxos)
Gossip (serf-rs)
Padrões
CQRS (eventual-rs)
Saga (temporal-sdk)
Circuit Breaker (tower)

Legenda de Crates Rust:

  • tonic: Implementação gRPC
  • rdkafka: Cliente Apache Kafka
  • async-raft: Implementação Raft
  • tower: Middleware para serviços

Glossário de Termos Técnicos

Consenso

Definição: Mecanismo para múltiplos nós concordarem em um valor

Em Rust: async_raft::Raft

Sharding

Definição: Partição horizontal de dados

Em Rust: sled::Db::sharded()

Event Sourcing

Definição: Armazenamento de estados como sequência de eventos

Em Rust: cqrs-es::Aggregate

Backpressure

Definição: Controle de fluxo em sistemas assíncronos

Em Rust: tokio::sync::Semaphore

Idempotência

Definição: Operação com mesmo efeito quando aplicada múltiplas vezes

Em Rust: tower::idempotent::Idempotent

Two-Phase Commit (2PC)

Definição: Protocolo atômico de commit distribuído

Em Rust: tpc::Coordinator

Recursos Recomendados

📖 Livros

🛠️ Projetos de Referência


⚖️ Algoritmo Raft

O que é o Raft?

O Raft é um algoritmo de consenso distribuído projetado para ser fácil de entender, dividindo o problema em três subproblemas:

  1. Liderança: Um único líder é eleito para gerenciar a replicação de logs
  2. Replicação de Log: O líder garante que as entradas do log sejam replicadas na maioria dos nós
  3. Segurança: Garante que nenhum nó retorne um valor antigo após uma atualização

Comparado ao Paxos, Raft é mais compreensível e vem sendo adotado em sistemas como etcd, Consul e TiKV.

Estados do Raft

  • Líder: Responsável por todas as requisições de clientes e replicação para seguidores
  • Seguidor: Nó passivo que apenas responde a requisições do líder
  • Candidato: Estado temporário durante eleições
  • Timeout de Eleição: Período aleatório (150-300ms) para evitar empates
          [Seguidor]
               ↑ ↓
        Timeout | Recebe voto majoritário
               ↓ ↑
          [Candidato] → Votos suficientes → [Líder]
                                               ↓
                                     Perde comunicação ←

Implementação Básica em Rust

Estrutura expandida de um nó Raft com máquina de estados:

use std::collections::HashMap;
use tokio::sync::mpsc;

#[derive(Debug, Clone)]
pub enum EstadoRaft {
    Seguidor,
    Candidato,
    Lider,
}

pub struct NoRaft {
    pub id: u64,
    pub estado: EstadoRaft,
    pub termo_atual: u64,
    pub votos_recebidos: u64,
    pub votou_em: Option,
    pub nos_conhecidos: HashMap>,
}

impl NoRaft {
    pub fn novo(id: u64) -> Self {
        Self {
            id,
            estado: EstadoRaft::Seguidor,
            termo_atual: 0,
            votos_recebidos: 0,
            votou_em: None,
            nos_conhecidos: HashMap::new(),
        }
    }

    pub async fn iniciar_eleicao(&mut self) {
        self.estado = EstadoRaft::Candidato;
        self.termo_atual += 1;
        self.votos_recebidos = 1; // Vota em si mesmo
        self.votou_em = Some(self.id);
        
        // Envia RequestVote para outros nós
        for (id, tx) in &self.nos_conhecidos {
            if *id != self.id {
                let mensagem = RaftMensagem::SolicitacaoVoto {
                    candidato_id: self.id,
                    termo: self.termo_atual,
                };
                tx.send(mensagem).await.unwrap();
            }
        }
    }
}

#[derive(Debug)]
pub enum RaftMensagem {
    SolicitacaoVoto { candidato_id: u64, termo: u64 },
    VotoConcedido { votante_id: u64, termo: u64 },
    AppendEntries { lider_id: u64, termo: u64 },
}

Esta implementação mostra a estrutura básica para eleições, incluindo:

  • Transição de estados (Seguidor → Candidato → Líder)
  • Lógica de votação
  • Comunicação entre nós via channels

Exemplo Completo: Lógica de Heartbeat

Implementação do mecanismo de heartbeat que mantém a liderança:

use tokio::time::{interval, Duration};

impl NoRaft {
    pub async fn executar_heartbeat(&self) {
        let mut interval = interval(Duration::from_millis(100));
        
        loop {
            interval.tick().await;
            
            if let EstadoRaft::Lider = self.estado {
                for (id, tx) in &self.nos_conhecidos {
                    if *id != self.id {
                        let mensagem = RaftMensagem::AppendEntries {
                            lider_id: self.id,
                            termo: self.termo_atual,
                        };
                        tx.send(mensagem).await.unwrap();
                    }
                }
            }
        }
    }
}

O heartbeat é enviado periodicamente para:

  1. Manter a autoridade do líder
  2. Evitar novas eleições
  3. Replicar entradas do log (não mostrado neste exemplo simplificado)

Bibliotecas Úteis

Implementações completas e ferramentas para Raft em Rust:

  • raft-rs - Implementação de referência da CoreOS
  • tokio - Para concorrência e timers assíncronos
  • serde - Serialização de mensagens entre nós
  • async-raft - Implementação async/await moderna

Exemplo de Cargo.toml para um projeto Raft:

[dependencies]
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
async-raft = "0.7"
thiserror = "1.0"

Padrões Comuns em Implementações Reais

Em sistemas de produção, você encontrará:

  • Snapshotting: Compactação periódica do log
  • Membership Changes: Adição/remoção dinâmica de nós
  • Log Compaction: Para evitar crescimento infinito do log
  • Pre-Vote: Extensão para evitar eleições desnecessárias
< exercicios">

🔧 Exercícios Práticos

Exercício 1: Concorrência com Threads

Crie um programa que:

  1. Inicie 4 threads worker
  2. Cada thread processa itens de uma fila compartilhada
  3. Use Arc<Mutex<Vec<i32>>> para compartilhamento seguro
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Fila compartilhada protegida por Mutex
    let fila = Arc::new(Mutex::new(vec![1, 2, 3, 4, 5, 6, 7, 8]));
    
    let mut handles = vec![];

    for id in 1..=4 {
        let fila = Arc::clone(&fila);
        let handle = thread::spawn(move || {
            loop {
                // Bloqueia o mutex para acessar a fila
                let mut guard = fila.lock().unwrap();
                if let Some(item) = guard.pop() {
                    println!("Thread {} processando item {}", id, item);
                    // Libera o lock antes do processamento demorado
                    drop(guard);
                    thread::sleep(std::time::Duration::from_millis(100));
                } else {
                    break; // Fila vazia
                }
            }
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

Explicação:

  • Arc permite compartilhamento seguro entre threads
  • Mutex garante acesso exclusivo à fila
  • drop(guard) libera o lock explicitamente
  • Cada thread processa itens até esvaziar a fila

Exercício 2: Comunicação com Channels Avançado

Crie um sistema com:

  1. 1 thread produtora que gera números aleatórios
  2. 3 threads worker que processam os números
  3. 1 thread consumidora que mostra resultados finais
use std::sync::mpsc;
use std::thread;
use rand::Rng;

fn main() {
    let (tx_prod, rx_work) = mpsc::channel();
    let (tx_res, rx_res) = mpsc::channel();
    
    // Produtor
    thread::spawn(move || {
        let mut rng = rand::thread_rng();
        for _ in 0..10 {
            let num = rng.gen_range(1..100);
            tx_prod.send(num).unwrap();
        }
    });

    // Workers
    for id in 1..=3 {
        let rx_work = rx_work.clone();
        let tx_res = tx_res.clone();
        thread::spawn(move || {
            for num in rx_work {
                let resultado = num * id;
                tx_res.send((id, num, resultado)).unwrap();
            }
        });
    }

    // Consumidor
    for _ in 0..10 {
        let (id, num, res) = rx_res.recv().unwrap();
        println!("Worker {}: {} → {}", id, num, res);
    }
}

Dependência necessária:

[dependencies]
rand = "0.8"

Fluxo do programa:

  1. Produtor gera números e envia para workers
  2. Cada worker multiplica pelo seu ID
  3. Consumidor coleta e exibe resultados

Exercício 3: Simulação de Eleição Raft

Implemente:

  1. 3 nós Raft (Seguidor, Candidato, Líder)
  2. Lógica de eleição com timeout
  3. Transição de estados baseada em votos
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

#[derive(Debug, Clone)]
enum Estado {
    Seguidor,
    Candidato,
    Lider,
}

struct No {
    id: u64,
    estado: Estado,
    termo: u64,
    votos: u64,
}

impl No {
    fn new(id: u64) -> Self {
        Self {
            id,
            estado: Estado::Seguidor,
            termo: 0,
            votos: 0,
        }
    }

    fn iniciar_eleicao(&mut self) {
        self.estado = Estado::Candidato;
        self.termo += 1;
        self.votos = 1; // Vota em si mesmo
        println!("Nó {} iniciou eleição (Termo {})", self.id, self.termo);
    }

    fn receber_voto(&mut self) {
        self.votos += 1;
        if self.votos >= 2 { // Simples maioria para 3 nós
            self.estado = Estado::Lider;
            println!("Nó {} tornou-se líder!", self.id);
        }
    }
}

fn main() {
    let nos = Arc::new(Mutex::new(vec![
        No::new(1),
        No::new(2),
        No::new(3),
    ]));

    // Simulação de timeout para nó 1
    let nos_clone = Arc::clone(&nos);
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(2));
        let mut guard = nos_clone.lock().unwrap();
        guard[0].iniciar_eleicao();
    });

    // Nó 2 concede voto
    let nos_clone = Arc::clone(&nos);
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(3));
        let mut guard = nos_clone.lock().unwrap();
        guard[0].receber_voto();
    });

    // Nó 3 concede voto
    let nos_clone = Arc::clone(&nos);
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(4));
        let mut guard = nos_clone.lock().unwrap();
        guard[0].receber_voto();
    });

    thread::sleep(Duration::from_secs(5));
}

Conceitos implementados:

  • Transição de estados do Raft
  • Lógica de eleição simplificada
  • Timeout simulado com thread::sleep
  • Compartilhamento seguro com Arc

Exercício 4: Heartbeat com Tokio

Crie um serviço de heartbeat que:

  1. Envie pulsos periódicos a cada 500ms
  2. Use Tokio para tarefas assíncronas
  3. Pare após 5 heartbeats
use tokio::time::{interval, Duration};

#[tokio::main]
async fn main() {
    let mut interval = interval(Duration::from_millis(500));
    let mut count = 0;

    while count < 5 {
        interval.tick().await;
        println!("Heartbeat {}", count + 1);
        count += 1;
    }
    
    println!("Finalizado após 5 heartbeats");
}

Dependência necessária:

[dependencies]
tokio = { version = "1.0", features = ["full"] }

Pontos-chave:

  • interval.tick().await pausa sem bloquear a thread
  • Tokio permite concorrência eficiente
  • Modelo similar ao usado em sistemas reais de Raft

Exercício 5: Cliente HTTP Distribuído

Crie um cliente que:

  1. Faça requisições paralelas para múltiplos URLs
  2. Use Reqwest e Tokio
  3. Agregue os resultados
use reqwest;
use tokio;

#[tokio::main]
async fn main() -> Result<(), Box> {
    let urls = vec![
        "https://httpbin.org/get?query=1",
        "https://httpbin.org/get?query=2",
        "https://httpbin.org/get?query=3",
    ];

    let mut tasks = vec![];

    for url in urls {
        let task = tokio::spawn(async move {
            let resp = reqwest::get(url).await?
                .text().await?;
            Ok::(resp)
        });
        tasks.push(task);
    }

    for task in tasks {
        match task.await? {
            Ok(resp) => println!("Resposta: {}", resp),
            Err(e) => eprintln!("Erro: {}", e),
        }
    }

    Ok(())
}

Dependências:

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }

Funcionamento:

  1. Cria uma tarefa assíncrona para cada URL
  2. Executa requisições em paralelo
  3. Coleta e exibe resultados na ordem de conclusão

🧠 Quiz de Rust para Sistemas Distribuídos

0/20 0/20

1. Qual é o principal mecanismo de segurança de memória em Rust?

2. Qual crate é mais usada para programação assíncrona em Rust?

3. No algoritmo Raft, o que acontece se um líder falhar?

📚 Referências