Load Balancer em Zig — Tutorial Passo a Passo

Load Balancer em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um load balancer round-robin em Zig que distribui requisições HTTP entre múltiplos servidores backend, com health checks e detecção de servidores inativos.

O Que Vamos Construir

Nosso load balancer vai:

  • Distribuir requisições HTTP usando algoritmo round-robin
  • Manter lista de servidores backend configuráveis
  • Implementar health checks periódicos
  • Detectar e remover servidores inativos automaticamente
  • Reencaminhar requisições e respostas completas
  • Exibir métricas de distribuição em tempo real

Por Que Este Projeto?

Load balancers são componentes críticos em qualquer infraestrutura web. Construir um nos ensina sobre proxy reverso, distribuição de carga, monitoramento de saúde e tolerância a falhas. Em Zig, temos controle total sobre sockets e buffers.

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir load-balancer
cd load-balancer
zig init

Passo 2: Configuração dos Backends

const std = @import("std");
const net = std.net;
const posix = std.posix;
const io = std.io;
const mem = std.mem;
const fmt = std.fmt;
const time = std.time;
const Thread = std.Thread;

const MAX_BACKENDS = 16;
const TAMANHO_BUF = 65536;
const HEALTH_CHECK_INTERVALO_MS = 5000;

/// Estado de um servidor backend.
const BackendStatus = enum {
    saudavel,
    doente,
    desconhecido,

    pub fn str(self: BackendStatus) []const u8 {
        return switch (self) {
            .saudavel => "SAUDAVEL",
            .doente => "DOENTE",
            .desconhecido => "DESCONHECIDO",
        };
    }
};

/// Representa um servidor backend.
const Backend = struct {
    host: [64]u8,
    host_len: usize,
    porta: u16,
    status: BackendStatus,
    requisicoes_total: u64,
    requisicoes_falhas: u64,
    ultimo_check: i64,
    tempo_resposta_ms: u64,

    pub fn hostStr(self: *const Backend) []const u8 {
        return self.host[0..self.host_len];
    }

    pub fn endereco(self: *const Backend) ?[4]u8 {
        var ip: [4]u8 = undefined;
        var it = mem.splitScalar(u8, self.hostStr(), '.');
        var i: usize = 0;
        while (it.next()) |parte| {
            if (i >= 4) return null;
            ip[i] = fmt.parseInt(u8, parte, 10) catch return null;
            i += 1;
        }
        if (i == 4) return ip;
        return null;
    }
};

/// Configuração do load balancer.
const LoadBalancerConfig = struct {
    porta: u16,
    backends: [MAX_BACKENDS]Backend,
    num_backends: usize,

    pub fn adicionarBackend(self: *LoadBalancerConfig, host: []const u8, porta: u16) void {
        if (self.num_backends >= MAX_BACKENDS) return;
        var backend = &self.backends[self.num_backends];
        @memcpy(backend.host[0..host.len], host);
        backend.host_len = host.len;
        backend.porta = porta;
        backend.status = .desconhecido;
        backend.requisicoes_total = 0;
        backend.requisicoes_falhas = 0;
        backend.ultimo_check = 0;
        backend.tempo_resposta_ms = 0;
        self.num_backends += 1;
    }
};

Passo 3: Algoritmo Round-Robin e Health Check

/// Load Balancer com round-robin e health checks.
const LoadBalancer = struct {
    config: LoadBalancerConfig,
    indice_atual: usize, // para round-robin
    total_requisicoes: u64,

    const Self = @This();

    pub fn init(config: LoadBalancerConfig) Self {
        return .{
            .config = config,
            .indice_atual = 0,
            .total_requisicoes = 0,
        };
    }

    /// Seleciona o próximo backend saudável (round-robin).
    pub fn proximoBackend(self: *Self) ?*Backend {
        if (self.config.num_backends == 0) return null;

        // Tentar encontrar um backend saudável
        var tentativas: usize = 0;
        while (tentativas < self.config.num_backends) : (tentativas += 1) {
            const idx = self.indice_atual % self.config.num_backends;
            self.indice_atual += 1;

            const backend = &self.config.backends[idx];
            if (backend.status != .doente) {
                return backend;
            }
        }

        // Nenhum saudável: tentar qualquer um
        const idx = self.indice_atual % self.config.num_backends;
        self.indice_atual += 1;
        return &self.config.backends[idx];
    }

    /// Verifica a saúde de um backend tentando conectar.
    pub fn healthCheck(backend: *Backend) void {
        const ip = backend.endereco() orelse {
            backend.status = .doente;
            return;
        };

        const addr = net.Address.initIp4(ip, backend.porta);
        const socket = posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0) catch {
            backend.status = .doente;
            return;
        };
        defer posix.close(socket);

        const inicio = time.milliTimestamp();

        posix.connect(socket, &addr.any, addr.getOsSockLen()) catch {
            backend.status = .doente;
            return;
        };

        backend.tempo_resposta_ms = @intCast(time.milliTimestamp() - inicio);
        backend.status = .saudavel;
        backend.ultimo_check = time.milliTimestamp();
    }

    /// Executa health checks em todos os backends.
    pub fn healthCheckTodos(self: *Self) void {
        for (0..self.config.num_backends) |i| {
            healthCheck(&self.config.backends[i]);
        }
    }

    /// Encaminha uma requisição para o backend selecionado.
    pub fn encaminhar(
        self: *Self,
        dados: []const u8,
        socket_cliente: posix.socket_t,
        stdout: anytype,
    ) !void {
        const backend = self.proximoBackend() orelse {
            const resp = "HTTP/1.1 503 Service Unavailable\r\n" ++
                "Content-Type: text/plain\r\nConnection: close\r\n\r\n" ++
                "Nenhum backend disponivel";
            _ = posix.write(socket_cliente, resp) catch {};
            return;
        };

        self.total_requisicoes += 1;
        backend.requisicoes_total += 1;

        const ip = backend.endereco() orelse {
            backend.status = .doente;
            return;
        };

        // Conectar ao backend
        const addr = net.Address.initIp4(ip, backend.porta);
        const socket_backend = posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0) catch {
            backend.requisicoes_falhas += 1;
            backend.status = .doente;
            const resp = "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
            _ = posix.write(socket_cliente, resp) catch {};
            return;
        };
        defer posix.close(socket_backend);

        posix.connect(socket_backend, &addr.any, addr.getOsSockLen()) catch {
            backend.requisicoes_falhas += 1;
            backend.status = .doente;
            const resp = "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
            _ = posix.write(socket_cliente, resp) catch {};
            return;
        };

        // Enviar requisição ao backend
        _ = posix.write(socket_backend, dados) catch return;

        // Relay resposta de volta ao cliente
        var buf_resp: [TAMANHO_BUF]u8 = undefined;
        while (true) {
            const n = posix.read(socket_backend, &buf_resp) catch break;
            if (n == 0) break;
            _ = posix.write(socket_cliente, buf_resp[0..n]) catch break;
        }

        try stdout.print("  -> {s}:{d} (req #{d})\n", .{
            backend.hostStr(), backend.porta, backend.requisicoes_total,
        });
    }

    /// Imprime o status de todos os backends.
    pub fn imprimirStatus(self: *const Self, writer: anytype) !void {
        try writer.print("\n  --- Status dos Backends ---\n", .{});
        try writer.print("  {s:<20} {s:<10} {s:<10} {s:<10} {s}\n", .{
            "BACKEND", "STATUS", "REQS", "FALHAS", "RESP(ms)",
        });
        try writer.print("  {s:-<60}\n", .{""});

        for (0..self.config.num_backends) |i| {
            const b = &self.config.backends[i];
            try writer.print("  {s}:{d:<14} {s:<10} {d:<10} {d:<10} {d}\n", .{
                b.hostStr(), b.porta, b.status.str(),
                b.requisicoes_total, b.requisicoes_falhas, b.tempo_resposta_ms,
            });
        }
        try writer.print("  Total requisicoes: {d}\n\n", .{self.total_requisicoes});
    }
};

Passo 4: Servidor Principal

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const porta: u16 = 8000;

    // Configurar backends
    var config = LoadBalancerConfig{
        .porta = porta,
        .backends = undefined,
        .num_backends = 0,
    };
    config.adicionarBackend("127.0.0.1", 8081);
    config.adicionarBackend("127.0.0.1", 8082);
    config.adicionarBackend("127.0.0.1", 8083);

    var lb = LoadBalancer.init(config);

    // Health check inicial
    try stdout.print("  Verificando saude dos backends...\n", .{});
    lb.healthCheckTodos();
    try lb.imprimirStatus(stdout);

    // Criar socket do servidor
    const endereco = net.Address.initIp4(.{ 0, 0, 0, 0 }, porta);
    const socket_servidor = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0);
    defer posix.close(socket_servidor);

    const optval: i32 = 1;
    try posix.setsockopt(socket_servidor, posix.SOL.SOCKET, posix.SO.REUSEADDR, mem.asBytes(&optval));
    try posix.bind(socket_servidor, &endereco.any, endereco.getOsSockLen());
    try posix.listen(socket_servidor, 128);

    try stdout.print(
        \\
        \\  ==========================================
        \\     LOAD BALANCER - Round Robin - Zig
        \\  ==========================================
        \\  Escutando na porta {d}
        \\  Backends configurados: {d}
        \\
        \\  Teste: curl http://localhost:{d}/
        \\  ==========================================
        \\
    , .{ porta, lb.config.num_backends, porta });

    var ultimo_health_check = time.milliTimestamp();

    // Loop principal
    while (true) {
        // Health check periódico
        const agora = time.milliTimestamp();
        if (agora - ultimo_health_check > HEALTH_CHECK_INTERVALO_MS) {
            lb.healthCheckTodos();
            ultimo_health_check = agora;
        }

        const socket_cliente = posix.accept(socket_servidor, null, null) catch continue;

        var buf: [TAMANHO_BUF]u8 = undefined;
        const n = posix.read(socket_cliente, &buf) catch {
            posix.close(socket_cliente);
            continue;
        };

        if (n == 0) {
            posix.close(socket_cliente);
            continue;
        }

        lb.encaminhar(buf[0..n], socket_cliente, stdout) catch {};
        posix.close(socket_cliente);

        // Imprimir status a cada 10 requisições
        if (lb.total_requisicoes % 10 == 0) {
            lb.imprimirStatus(stdout) catch {};
        }
    }
}

Testes

test "config - adicionar backends" {
    var config = LoadBalancerConfig{
        .porta = 8000,
        .backends = undefined,
        .num_backends = 0,
    };
    config.adicionarBackend("127.0.0.1", 8081);
    config.adicionarBackend("127.0.0.1", 8082);
    try std.testing.expectEqual(@as(usize, 2), config.num_backends);
}

test "round-robin distribui igualmente" {
    var config = LoadBalancerConfig{
        .porta = 8000,
        .backends = undefined,
        .num_backends = 0,
    };
    config.adicionarBackend("127.0.0.1", 8081);
    config.adicionarBackend("127.0.0.1", 8082);

    // Marcar como saudáveis
    config.backends[0].status = .saudavel;
    config.backends[1].status = .saudavel;

    var lb = LoadBalancer.init(config);

    const b1 = lb.proximoBackend().?;
    const b2 = lb.proximoBackend().?;
    try std.testing.expect(b1.porta != b2.porta);
}

test "backend doente e pulado" {
    var config = LoadBalancerConfig{
        .porta = 8000,
        .backends = undefined,
        .num_backends = 0,
    };
    config.adicionarBackend("127.0.0.1", 8081);
    config.adicionarBackend("127.0.0.1", 8082);

    config.backends[0].status = .doente;
    config.backends[1].status = .saudavel;

    var lb = LoadBalancer.init(config);

    const b = lb.proximoBackend().?;
    try std.testing.expectEqual(@as(u16, 8082), b.porta);
}

Compilando e Executando

# Iniciar backends de teste (em terminais separados):
# python3 -m http.server 8081
# python3 -m http.server 8082
# python3 -m http.server 8083

# Iniciar o load balancer:
zig build run

# Testar distribuição:
for i in $(seq 1 10); do curl -s http://localhost:8000/; done

Conceitos Aprendidos

  • Round-robin para distribuição de carga
  • Health checks para detecção de falhas
  • Proxy reverso com relay de dados
  • Gerenciamento de estado de servidores
  • Tolerância a falhas com remoção automática

Próximos Passos

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.