Como usar read replicas de forma transparente em APIs de alto tráfego

1. Fundamentos de Read Replicas em Arquiteturas de Alto Tráfego

Read replicas são cópias secundárias do banco de dados principal que recebem atualizações contínuas através de replicação assíncrona ou síncrona. Em APIs de alto tráfego, a separação entre operações de leitura (SELECT) e escrita (INSERT/UPDATE/DELETE) é essencial para escalar horizontalmente a capacidade de consulta sem sobrecarregar o nó primário.

O benefício central é a distribuição da carga de leitura entre múltiplos nós, permitindo que o banco primário se concentre em escritas críticas. No entanto, desafios como consistência eventual e latência de replicação precisam ser gerenciados — uma réplica pode estar alguns milissegundos atrás do primário, resultando em stale reads.

text
// Exemplo: Configuração básica de replicação MySQL
CHANGE MASTER TO
  MASTER_HOST='primary-db.internal',
  MASTER_USER='replicator',
  MASTER_PASSWORD='secret',
  MASTER_LOG_FILE='mysql-bin.000001',
  MASTER_LOG_POS=107;
START SLAVE;

2. Estratégias de Roteamento Transparente de Consultas

Para que o uso de read replicas seja transparente para a API, o roteamento deve ocorrer automaticamente sem intervenção manual. As principais estratégias incluem:

Roteamento baseado em tipo de operação: Interceptar consultas SQL e redirecionar SELECTs para réplicas, enquanto escritas vão para o primário.

Proxies de banco de dados: Ferramentas como ProxySQL, Pgpool-II ou MaxScale atuam como intermediários, roteando consultas com base em padrões.

Camada de abstração com middleware: Implementar um middleware na API que decide dinamicamente qual conexão usar.

text
// Exemplo: Middleware de roteamento em Node.js
const dbMiddleware = (req, res, next) => {
  const isReadOperation = req.method === 'GET' && !req.path.includes('/write');

  if (isReadOperation) {
    req.db = readPool;  // Pool de conexões para réplicas
  } else {
    req.db = writePool; // Pool de conexões para primário
  }
  next();
};

// Uso no endpoint
app.get('/products', dbMiddleware, async (req, res) => {
  const [rows] = await req.db.query('SELECT * FROM products');
  res.json(rows);
});

3. Gerenciamento de Consistência e Stale Reads

Em cenários onde a consistência imediata é necessária, técnicas especiais devem ser aplicadas:

Read-after-write: Após uma escrita, forçar a leitura subsequente no primário durante uma janela de tempo configurável (ex: 5 segundos).

Sticky replicas: Associar um usuário ou sessão a uma réplica específica após uma escrita, garantindo que ele veja seus próprios dados atualizados.

Cookies de sessão: Armazenar um timestamp da última escrita na sessão do usuário e rotear para o primário se a réplica ainda não replicou.

text
// Exemplo: Lógica de read-after-write
async function getUserData(userId, lastWriteTime) {
  const replicaLag = await getReplicaLag();
  const timeSinceWrite = Date.now() - lastWriteTime;

  if (timeSinceWrite < replicaLag + 1000) {
    return queryPrimary(`SELECT * FROM users WHERE id=${userId}`);
  }
  return queryReplica(`SELECT * FROM users WHERE id=${userId}`);
}

4. Configuração de Pool de Conexões para Replicas

Um pool de conexões bem configurado é crucial para o desempenho. As réplicas devem ser balanceadas e monitoradas continuamente.

text
// Exemplo: Pool de conexões com balanceamento round-robin
const replicas = [
  { host: 'replica-1.internal', port: 3306 },
  { host: 'replica-2.internal', port: 3306 },
  { host: 'replica-3.internal', port: 3306 }
];

let currentIndex = 0;

function getNextReplica() {
  const replica = replicas[currentIndex];
  currentIndex = (currentIndex + 1) % replicas.length;
  return replica;
}

// Configuração com retry e backoff exponencial
const readPool = mysql.createPoolCluster({
  removeNodeErrorCount: 5,
  restoreNodeTimeout: 10000,
  defaultSelector: 'RR' // Round-robin
});

replicas.forEach((r, i) => {
  readPool.add(`replica-${i}`, {
    host: r.host,
    port: r.port,
    user: 'reader',
    password: 'readonly',
    connectionLimit: 50
  });
});

5. Monitoramento e Métricas de Desempenho

Para garantir que as réplicas estejam operando corretamente, monitore:

  • Latência de replicação: Diferença de tempo entre primário e réplica (deve ser < 100ms em condições normais)
  • Taxa de stale reads: Percentual de consultas que retornam dados obsoletos
  • Throughput: Número de consultas por segundo por réplica
  • Conexões ativas: Evitar que uma réplica receba mais conexões que sua capacidade
text
// Exemplo: Coleta de métricas de replicação
async function checkReplicaLag() {
  const [rows] = await replicaPool.query(
    "SELECT TIMESTAMPDIFF(SECOND, MAX(exec_time), NOW()) AS lag_seconds FROM mysql.slave_relay_log_info"
  );
  return rows[0].lag_seconds;
}

// Alerta se lag > 5 segundos
setInterval(async () => {
  const lag = await checkReplicaLag();
  if (lag > 5) {
    console.error(`Replica lag critical: ${lag}s`);
    // Disparar alerta
  }
}, 10000);

6. Padrões de Implementação em APIs RESTful

A implementação mais comum é separar endpoints de leitura e escrita, mas a transparência exige que isso seja feito automaticamente.

text
// Exemplo: API RESTful com roteamento transparente completo
const express = require('express');
const app = express();

// Middleware de roteamento automático
app.use(async (req, res, next) => {
  req.db = req.method === 'GET' ? getReadConnection() : getWriteConnection();

  // Suporte a consistência via cabeçalho
  if (req.headers['x-consistency-level'] === 'strong') {
    req.db = getWriteConnection();
  }

  next();
});

// Endpoint de catálogo de produtos (leitura)
app.get('/api/products', async (req, res) => {
  const [products] = await req.db.query('SELECT * FROM products WHERE active=1');
  res.set('Cache-Control', 'public, max-age=30');
  res.json(products);
});

// Endpoint de criação de pedido (escrita)
app.post('/api/orders', async (req, res) => {
  const result = await req.db.query('INSERT INTO orders SET ?', req.body);
  // Armazenar timestamp para read-after-write
  req.session.lastWriteTime = Date.now();
  res.status(201).json({ id: result.insertId });
});

7. Casos de Uso e Boas Práticas em Alto Tráfego

E-commerce: Catálogo de produtos servido por réplicas regionais, enquanto estoque e pedidos vão para o primário. Isso reduz latência para usuários geograficamente distribuídos.

Redes sociais: Feeds de notícias podem ser servidos por réplicas, com consistência eventual aceitável para a maioria dos usuários.

Boas práticas:
- Nunca direcione escritas para réplicas
- Implemente circuit breaker para remover réplicas lentas automaticamente
- Use consistência forte apenas quando absolutamente necessário
- Configure réplicas com hardware idêntico ao primário para evitar gargalos
- Monitore a profundidade do log de replicação (relay log)

text
// Exemplo: Circuit breaker para réplicas
class ReplicaCircuitBreaker {
  constructor(replica) {
    this.replica = replica;
    this.failureCount = 0;
    this.threshold = 5;
    this.timeout = 30000; // 30 segundos
    this.isOpen = false;
  }

  async query(sql, params) {
    if (this.isOpen) {
      throw new Error('Circuit breaker open for ' + this.replica.host);
    }
    try {
      const result = await this.replica.query(sql, params);
      this.failureCount = 0;
      return result;
    } catch (error) {
      this.failureCount++;
      if (this.failureCount >= this.threshold) {
        this.isOpen = true;
        setTimeout(() => {
          this.isOpen = false;
          this.failureCount = 0;
        }, this.timeout);
      }
      throw error;
    }
  }
}

A implementação transparente de read replicas em APIs de alto tráfego exige um equilíbrio cuidadoso entre desempenho e consistência. Comece com um proxy de banco de dados para roteamento automático, adicione monitoramento de latência de replicação e implemente read-after-write para operações críticas. Com o tempo, ajuste o número de réplicas com base no throughput observado e nos padrões de tráfego.

Referências