sh4.tech

Dica 3 min de leitura Kafka · configuração

Uma lista de brokers velha que dava 504

Uma tela de um sistema financeiro respondia 504. A causa era uma lista de brokers do Kafka que ainda apontava para um cluster substituído meses antes. Por que o cliente não reclamou na inicialização, e como fazê-lo falhar em um segundo.

Quando aconteceu
Publicado
Medido
, log de produção e config do produtor no repositório; os 12 s são calculados a partir da config
Stack
PHP (CodeIgniter 4), php-rdkafka sobre librdkafka, Kafka em modo KRaft
Também em
English

O sintoma

Em 22 de setembro uma tela do sistema financeiro começou a responder 504. O gateway na frente da aplicação desiste depois de alguns segundos, então o usuário via uma página de erro genérica, e o log da aplicação tinha uma linha para cada tentativa:

ERROR: Local: Timed out

"Local" é a palavra importante. Na librdkafka, os erros que começam com Local: são produzidos pelo próprio cliente, antes de qualquer broker dizer alguma coisa. O cliente estava esperando algo e ninguém respondia.

A causa

A config de produção do serviço tinha a lista de brokers fixa no código, assim (aqui com nomes de host, o arquivo real tinha IPs):

app/Config/Boot/production.php, antes
defined('KAFKA_BROKER_PRODUCER') || define('KAFKA_BROKER_PRODUCER', ["kafka-old-1:9092", "kafka-old-2:9092"]);

Essa lista veio do primeiro commit do projeto. Em maio a empresa passou para um cluster novo com três brokers, e os serviços foram reapontados um a um; este ficou com a lista antiga, e os brokers antigos tinham ficado inalcançáveis. A correção foi uma linha, a mesma lista que os outros serviços já usavam em produção:

depois
defined('KAFKA_BROKER_PRODUCER') || define('KAFKA_BROKER_PRODUCER', ["kafka-1:9092", "kafka-2:9092", "kafka-3:9092"]);

Por que nada reclamou na inicialização

A biblioteca tinha uma checagem que parecia certa:

a checagem que nunca dispara
$ok = $producer->addBrokers(implode(",", $brokers));
if ($ok == 0)
    throw new KafkaException("Could not connect to any broker");

O addBrokers() devolve quantos brokers foram adicionados à lista do cliente. Ele interpreta a string e retorna. A conexão acontece depois, em segundo plano, então com dois brokers mortos ele devolve 2 e a exceção nunca dispara.

Depois, cada envio fazia um poll(3000) e um flush(3000) com até três tentativas. Sem nenhum broker vivo, isso dá uns 12 segundos até o erro, para uma mensagem. Some um segundo envio, ou o flush que roda de novo no destrutor, e a requisição passa do timeout do gateway. O usuário vê um 504, e o erro de verdade fica num log que ninguém estava lendo.

Fazer falhar em um segundo

Se você quer saber na inicialização que um broker responde, peça metadados. O getMetadata() precisa de um broker de verdade e lança exceção quando o timeout passa:

checagem real de conexão
try {
    $producer->getMetadata(false, null, 1000);   // 1 s, sem lista de tópicos
} catch (\RdKafka\Exception $e) {
    throw new KafkaException("No Kafka broker answered: " . $e->getMessage());
}

Rodar isso em toda requisição é exagero; um health check com cache basta. E para qualquer coisa que o usuário espera, deixe o flush curto e devolva um erro claro, ou grave a mensagem numa tabela de outbox e mande fora da requisição. Assim um Kafka lento vira uma mensagem atrasada, e a tela continua funcionando.

O que ficou

  • Lista de brokers por serviço, copiada à mão, envelhece. Um lugar só (variável de ambiente vinda da mesma fonte, ou um nome de DNS por broker) muda junto com o cluster.
  • O addBrokers() conta strings. Quem prova a conexão é um pedido de metadados.
  • Local: Timed out quer dizer que o cliente nunca teve resposta. Olhe a rede e a config antes do broker.
  • Um produce síncrono dentro de uma requisição HTTP transforma qualquer problema do Kafka em problema do gateway, com uma mensagem pior.
Se for levar uma coisa sóQuando um cluster muda de lugar, procure os endereços antigos em todos os repositórios, inclusive nos serviços de que ninguém se lembra. Aqui a lista ficou num arquivo que ninguém tinha motivo para abrir desde o primeiro commit.

Arquivado pelo problema que resolve

Luciano Serra
Luciano Serra Founder-engineer · cofundador e CTO, construiu do zero uma rede de 15.000 telas, 2015–2024 · São Paulo, remoto

Desenho sistemas de produção do zero e assumo os que já estão rodando. Nos dois casos eles precisam aguentar quando o negócio cresce. Cofundei uma empresa de mídia out-of-home e construí a tecnologia dela por dez anos; meus sócios captaram o dinheiro e cuidaram do comercial.

Seu negócio está crescendo mais rápido que os sistemas? Vamos conversar →