Arquitetura real · Pleno
Uber: por que trocou Postgres por MySQL
Arquitetura real em produção. Pontos centrais: storage engines (LSM) e migração de dados online.
Resposta curta
Arquitetura real em produção. Pontos centrais: storage engines (LSM) e migração de dados online. A arquitetura inicial do Uber era um backend monolítico em Python que usava Postgres para guardar dados. Depois a arquitetura passou a microsserviços e a novas plataformas de dados, e em muitos dos casos em que antes se usava Postgres passou-se a usar o Schemaless, uma…
01 · CONTEXTO
De Postgres a MySQL: o que o Uber relatou
A arquitetura inicial do Uber era um backend monolítico em Python que usava Postgres para guardar dados. Depois a arquitetura passou a microsserviços e a novas plataformas de dados, e em muitos dos casos em que antes se usava Postgres passou-se a usar o Schemaless, uma camada de sharding de banco de dados construída sobre o MySQL.
O artigo de engenharia que explica a troca lista cinco limitações do Postgres: arquitetura ineficiente para escritas, replicação de dados ineficiente, problemas de corrupção de tabelas, suporte fraco a MVCC nas réplicas e dificuldade de migrar para versões novas.
O texto analisa essas limitações comparando como o Postgres e o MySQL, com o motor de armazenamento InnoDB, representam tabelas e índices no disco.
02 · FORMATO EM DISCO
Onde o índice aponta
Um aspecto central do desenho do Postgres são as linhas imutáveis, chamadas de tuplas e identificadas por um ctid, que representa conceitualmente o local da tupla no disco. Um mesmo registro pode ter vários ctids, por exemplo quando existem várias versões dele. Os índices, o primário e os secundários, apontam diretamente para esses locais.
Para atualizar um registro, o Postgres adiciona uma nova tupla à tabela e guarda em cada tupla um campo de versão e um ponteiro para a tupla anterior. Enquanto as duas versões existem, os índices precisam ter entradas para as duas.
O InnoDB, motor de armazenamento do MySQL, também suporta MVCC e dados mutáveis, mas seus registros de índice secundário guardam um ponteiro para o valor da chave primária, e não para o local da linha no disco.
| Aspecto | Postgres | MySQL com InnoDB |
|---|---|---|
| Ponteiro no índice secundário | Local da tupla no disco (ctid) | Valor da chave primária |
| Atualização de uma linha | Adiciona uma nova tupla, com versão e ponteiro para a anterior | Normalmente no próprio lugar; a versão antiga vai para o rollback segment |
| Buscas num índice secundário | Uma | Duas |
| Quando o local da linha muda | Todos os índices precisam ser atualizados | Só o índice primário precisa ser atualizado |
O custo do desenho do InnoDB é uma busca a mais: uma consulta por índice secundário procura primeiro a chave primária e depois consulta o índice primário para achar o local da linha. O texto trata isso como uma leve desvantagem frente ao Postgres nesse tipo de consulta.
03 · CUSTO DAS ESCRITAS
Uma atualização pequena, várias escritas
No exemplo do artigo, a tabela tem três índices: a chave primária e dois secundários. Atualizar o ano de nascimento de uma única linha exigiu pelo menos quatro escritas físicas no Postgres: a nova tupla e uma nova entrada em cada índice, inclusive nos que cobrem campos que não mudaram.
Cada uma dessas escritas também precisa ser refletida no WAL, então o total de escritas em disco é ainda maior. Numa tabela com uma dúzia de índices, uma atualização num campo coberto por um único índice precisa ser propagada a todos os doze.
No InnoDB a mesma atualização altera o campo no próprio lugar, copia a linha antiga para o rollback segment e atualiza apenas os índices que de fato cobrem o campo alterado; o índice da chave primária e o do nome não precisam ser tocados.
A limpeza de versões antigas também muda. No InnoDB, as linhas elegíveis para limpeza estão diretamente no rollback segment, enquanto o autovacuum do Postgres precisa varrer tabelas inteiras para identificar linhas apagadas.
04 · REPLICAÇÃO
WAL físico versus log lógico
O Postgres implementa a replicação por streaming enviando o WAL do mestre às réplicas. Como o WAL foi projetado para recuperação de falhas, ele carrega informação de baixo nível sobre as atualizações em disco, no nível das tuplas e dos seus deslocamentos.
O Uber começou com servidores físicos numa colocation na costa oeste e, para recuperação de desastres, adicionou servidores numa segunda colocation na costa leste. O mestre Postgres, com suas réplicas, ficava no data center oeste, e um conjunto de réplicas no leste.
A aplicação grava no mestre Postgres, que mantém um registro de mudanças (WAL) usado na recuperação de falhas.
O mestre envia o WAL às réplicas; o conteúdo do WAL descreve as tuplas e seus deslocamentos no disco.
Uma pequena mudança lógica vira várias escritas físicas, e as entradas de WAL de todas elas se propagam pela rede.
Com réplicas em outro data center, esse volume pesa no link entre os dois locais.
O WAL também era arquivado em um serviço de armazenamento, e no pico inicial a banda até ele não acompanhou.
Segundo o texto, a verbosidade do protocolo de replicação pode gerar um volume avassalador de dados para bancos com muitos índices, e não é possível obter um link entre costas com a mesma banda de uma interconexão local. O arquivamento do WAL também sofreu: no pico inicial, a banda até o serviço de armazenamento não acompanhava a taxa de escrita dos WALs.
O MySQL oferece replicação baseada em instruções, em linhas e uma mista. A replicação por instruções costuma ser a mais compacta, mas pode obrigar as réplicas a executar instruções caras; a por linhas é mais verbosa, porém mais previsível e eficiente nas réplicas.
Como só o índice primário aponta para os deslocamentos das linhas, o fluxo de replicação do MySQL precisa conter apenas atualizações lógicas, e as réplicas inferem sozinhas as mudanças de índice. O texto conclui que o log binário de replicação do MySQL é bem mais compacto que o fluxo de WAL do Postgres.
A atualização normalmente é aplicada no próprio lugar e a versão antiga da linha vai para o rollback segment.
O fluxo de replicação leva só atualizações lógicas de linhas; as réplicas inferem sozinhas as mudanças de índice.
Como o fluxo é lógico, a réplica pode ter semântica MVCC verdadeira e as leituras nela não bloqueiam a replicação.
Na atualização de versão, atualiza-se uma réplica por vez e, ao fim, promove-se uma delas a novo mestre.
05 · RÉPLICAS, CORRUPÇÃO E ATUALIZAÇÃO
Quando a replicação física cobra a conta
O Postgres não tem suporte real a MVCC em réplicas: como elas aplicam o WAL, têm uma cópia dos dados em disco idêntica à do mestre. Se uma réplica tem uma transação aberta, as atualizações que afetam as linhas seguradas por ela ficam bloqueadas e a aplicação do WAL é pausada até a transação terminar. Como a réplica pode ficar muito atrasada, o Postgres aplica um timeout e encerra a transação que bloqueia.
O texto diz que as réplicas podem rotineiramente ter um atraso de segundos em relação ao mestre, e que por isso é fácil escrever código que tem transações encerradas à força. Um exemplo é uma transação implícita que fica aberta até o fim do envio de um e-mail de recibo.
No MySQL, como o fluxo de replicação carrega atualizações lógicas, as réplicas podem ter semântica MVCC verdadeira, e as consultas de leitura nelas não bloqueiam a replicação.
O Uber relatou um bug do Postgres: durante uma promoção rotineira de mestre, as réplicas seguiram incorretamente as trocas de timeline e aplicaram mal alguns registros do WAL, e registros que deveriam ter sido marcados como inativos não foram. Uma consulta por uma chave passaria a devolver dois registros, e os resultados duplicados fizeram a lógica de aplicação falhar em vários casos.
Como o bug afetou todos os servidores, as linhas corrompidas eram diferentes de réplica para réplica, e a equipe não sabia ao certo quantas réplicas, nem se o mestre, estavam afetados. A correção das réplicas foi ressincronizá-las a partir de um novo snapshot do mestre, um processo trabalhoso, porque só havia capacidade para retirar poucas réplicas do pool de balanceamento por vez.
Segundo o texto, o bug afetou só certas versões do Postgres e foi corrigido há muito tempo, mas os autores ainda consideram preocupante que essa classe de bug possa acontecer: pela forma como a replicação funciona, o problema poderia se espalhar por todos os bancos de uma hierarquia de replicação.
No MySQL a replicação é lógica, então uma operação como rebalancear uma B-tree nunca corrompe um índice. Um problema típico é uma instrução pulada ou, menos comum, aplicada duas vezes, o que pode deixar dados faltando ou inválidos, mas não causa uma indisponibilidade do banco.
A replicação física também dificulta a atualização de versão: segundo o texto, não é possível replicar entre releases GA diferentes do Postgres. O procedimento incluía rodar o pg_upgrade no mestre, que pode levar muitas horas num banco grande sem servir tráfego, e depois apagar cada réplica e restaurá-la a partir de um novo snapshot.
O Uber concluiu a subida do Postgres 9.1 para o 9.2, mas o processo levou tantas horas que não podia ser repetido, e as instâncias legadas continuavam no 9.2 à época do texto. Para quem usa o Postgres 9.4 ou mais novo, o texto cita o pglogical, que permite replicar entre releases diferentes, mas ressalva que ele não é integrado à árvore principal do Postgres.
No MySQL, segundo o texto, é trivial replicar entre releases diferentes. O jeito típico de atualizar é aplicar a mudança a uma réplica por vez e, ao fim, promover uma delas a novo mestre, com quase nenhum tempo parado.
06 · OUTRAS DIFERENÇAS E CONCLUSÃO
Cache, conexões e o balanço do Uber
O Postgres deixa o kernel cachear os dados recentes no page cache, e ler dados por essa via exige chamadas de sistema para localizá-los, cada uma com uma troca de contexto, mais cara que acessar a memória principal. O InnoDB implementa o próprio LRU, o buffer pool, o que resulta em menos trocas de contexto.
O MySQL cria uma thread por conexão, e o Postgres usa um processo por conexão, bem mais caro. O Uber relatou problemas para escalar o Postgres além de algumas centenas de conexões ativas; a documentação recomenda fortemente um pool de conexões fora do processo, e o pgbouncer foi geralmente bem-sucedido, mas bugs de aplicação que abriram conexões demais causaram indisponibilidades prolongadas.
O texto conclui que o Postgres serviu bem no início do Uber, mas surgiram problemas significativos ao escalar com o crescimento. Restaram instâncias Postgres legadas, e a maior parte dos bancos de dados passou a ser construída sobre o MySQL, tipicamente com o Schemaless, ou, em casos especializados, em bancos NoSQL como o Cassandra.
Perguntas frequentes
O que é Uber: por que trocou Postgres por MySQL?
Arquitetura real em produção. Pontos centrais: storage engines (LSM) e migração de dados online.
O que o design de Uber: por que trocou Postgres por MySQL cobre?
Ele está organizado nestas seções: De Postgres a MySQL: o que o Uber relatou, Onde o índice aponta, Uma atualização pequena, várias escritas, WAL físico versus log lógico, Quando a replicação física cobra a conta e Cache, conexões e o balanço do Uber.
Quanto tempo leva Uber: por que trocou Postgres por MySQL e qual é o nível?
Leva cerca de 15 minutos para ler e tem nível Pleno.
Agora projete você mesmo
Rode este caso como uma entrevista na escola de System Design: você toma as decisões, o tutor as questiona e você vê a nota do seu desenho.