Arquitetura real · Sênior
Figma e Notion: sharding horizontal de Postgres
Arquitetura real em produção. Pontos centrais: migração de dados online.
Resposta curta
Arquitetura real em produção. Pontos centrais: migração de dados online. Notion e Figma chegaram ao limite de um único Postgres. Este caso compara como cada um decidiu, desenhou e executou o sharding horizontal, com foco em como mover os dados sem parar o produto.
01 · CONTEXTO
Quando um Postgres só deixa de bastar
Notion e Figma chegaram ao limite de um único Postgres. Este caso compara como cada um decidiu, desenhou e executou o sharding horizontal, com foco em como mover os dados sem parar o produto.
No Notion, o uso do produto ia superar o monolito de Postgres, que tinha atravessado cinco anos e quatro ordens de magnitude de crescimento. Quem estava de plantão acordava com picos de CPU no banco.
O ponto de inflexão foi o VACUUM do Postgres, que passou a travar de forma constante e deixou de recuperar o espaço ocupado por tuplas mortas. O risco maior era o wraparound do ID de transação, mecanismo de segurança em que o Postgres para de processar todas as escritas; o time o tratou como ameaça existencial ao produto.
Subir a instância na vertical não bastava: o desempenho das consultas e dos processos de manutenção costuma degradar bem antes de a tabela atingir o tamanho máximo que o hardware suporta.
No Figma, a base de dados cresceu quase cem vezes desde 2020. Em 2020 era um único Postgres na maior instância física da AWS; até o fim de 2022 já havia cache, réplicas de leitura e uma dúzia de bancos particionados na vertical.
A partição vertical também tinha teto, porque a menor unidade que ela separa é uma tabela. Algumas tabelas, com vários terabytes e bilhões de linhas, já eram grandes demais para um único banco, e o time via impacto de confiabilidade durante os vacuums. As tabelas de maior escrita logo passariam o máximo de IOPS suportado pelo RDS da Amazon.
02 · ESCOLHAS
Construir o roteamento ou adotar um cluster pronto
O Notion optou por sharding no nível da aplicação: definir o próprio esquema de partição e rotear as consultas pela lógica da aplicação. Considerou soluções prontas, como Citus para Postgres e Vitess para MySQL, mas a lógica de cluster delas é opaca e o time queria controle sobre a distribuição dos dados.
O Figma avaliou CockroachDB, TiDB, Spanner e Vitess. Trocar para qualquer um deles exigiria uma migração de dados complexa entre dois armazenamentos, e o time já tinha muita experiência em operar RDS Postgres internamente. Uma alternativa NoSQL também ficou de fora: o modelo relacional do Figma é muito complexo e as APIs NoSQL não oferecem essa versatilidade.
Em vez de reimplementar um banco relacional shardado genérico, o que não fazia sentido para uma equipe pequena, o Figma construiu um conjunto de recursos menor e sob medida. Um exemplo: optou por não suportar transações atômicas entre shards, porque as falhas dessas transações podiam ser contornadas.
Dividir uma tabela entre vários bancos físicos tem preço. Segundo o Figma, chaves estrangeiras e índices únicos globais deixam de ser garantidos pelo Postgres, e escritas em alguns bancos podem ter sucesso enquanto outras falham, o que exige lógica de produto resiliente a essas falhas parciais.
| Decisão | Notion | Figma |
|---|---|---|
| Quais tabelas | Todas as tabelas alcançáveis a partir de block por chave estrangeira | Grupos de tabelas relacionadas, as colos, com a mesma chave de shard |
| Chave de shard | O ID do workspace | Um punhado de chaves, como UserID, FileID e OrgID |
| Distribuição | O espaço de UUIDs dividido em baldes uniformes | O hash da chave de shard |
| Joins | A maioria evitada, pois se consulta um workspace por vez | Só entre tabelas da mesma colo e pela chave de shard |
03 · CHAVES E ROTEAMENTO
Como cada consulta encontra o seu shard
O Notion shardou todas as tabelas alcançáveis a partir de block por chave estrangeira. Nem todas precisavam ser divididas, mas, se um registro ficasse no banco principal e o bloco relacionado em outro shard, a falta de transação entre bancos poderia gerar inconsistências: a exclusão do bloco poderia ter sucesso enquanto a atualização dos comentários falharia.
Como cada bloco pertence a exatamente um workspace, o ID do workspace virou a chave de partição. Os usuários consultam em geral um workspace por vez, o que evita a maioria dos joins entre shards.
O time fixou limites de 500 GB por tabela e 10 TB por banco físico e chegou a 480 shards lógicos distribuídos de modo uniforme em 32 bancos físicos. O número 480 foi escolhido por ter muitos divisores, o que permite adicionar ou remover hosts mantendo a distribuição uniforme; com qualquer potência de dois, seria preciso dobrar o número de hosts para escalar.
Uma requisição chega à aplicação; em geral ela consulta dados de um único workspace.
A aplicação usa o ID do workspace, a chave de partição, para achar o shard lógico. Todo o roteamento acontece na própria aplicação, com uma única fonte de verdade.
A consulta vai direto ao banco físico que guarda aquele shard lógico, sem uma segunda camada de roteamento dentro do Postgres, como haveria com tabelas nativamente particionadas.
Como a maioria das consultas fica dentro de um único workspace, evita-se a maior parte dos joins entre shards.
No Figma não havia uma chave de shard única boa. Uma chave unificada exigiria chave composta, nova coluna em toda tabela, backfills caros e grande refatoração da lógica de produto; por isso o time escolheu um punhado de chaves, como UserID, FileID e OrgID, e agrupou tabelas relacionadas em colos. Dentro de uma colo há joins entre tabelas e transações completas, desde que restritos a uma única chave de shard.
Muitas dessas chaves usavam IDs autoincrementais ou prefixados por timestamp, o que geraria hotspots. Migrar para IDs mais aleatórios exigia uma migração de dados cara e demorada, então o Figma passou a rotear pelo hash da chave de shard. O custo é que varreduras por intervalo ficam menos eficientes, pois chaves sequenciais caem em shards diferentes; esse padrão não é comum no código do Figma.
Para rotear, o Figma construiu o DBProxy, um serviço em Go entre a aplicação e o PgBouncer. Consultas sem a chave de shard exigem scatter-gather, que espalha a consulta por todos os shards e junta as respostas, e cada uma gera a mesma carga que teria num banco sem shards. Por isso o time restringiu a linguagem de consulta: joins só entre tabelas da mesma colo e pela chave de shard. Para escolher esse subconjunto, rodou o planejamento lógico em sombra sobre tráfego de produção real.
04 · MIGRAÇÃO NO NOTION
Escrita dupla, backfill, verificação e troca
O roteiro do Notion para qualquer migração tem quatro etapas. Na escrita dupla, as escritas chegam ao banco antigo e ao novo. No backfill, com a escrita dupla ligada, migra-se o dado antigo. Na verificação, confere-se a integridade do banco novo. Na troca, passa-se de fato para o banco novo. É a base de uma migração online de dados.
| Opção de escrita dupla | Como funciona | Avaliação do Notion |
|---|---|---|
| Escrita direta nos dois bancos | Cada escrita é aplicada ao banco antigo e ao novo | Qualquer problema numa das escritas gera inconsistência; instável demais para datastores críticos |
| Replicação lógica | Recurso nativo do Postgres, com modelo de publicação e assinatura para difundir comandos a vários bancos | Pouca capacidade de modificar dados entre origem e destino; não acompanhou a escrita da tabela block no snapshot inicial |
| Log de auditoria e catch-up | Uma tabela registra as escritas das tabelas em migração e um processo aplica cada atualização nos bancos novos, com as modificações necessárias | Foi a opção escolhida |
Um log de auditoria registra todas as escritas das tabelas em migração; é dele que o catch-up vai ler.
O processo de catch-up percorre o log e aplica cada atualização nos bancos novos, fazendo as modificações necessárias.
Com as escritas já propagando, o backfill migra os dados antigos. Ele compara versões antes de gravar e ignora registros com atualização mais recente.
Catch-up e backfill podem rodar em qualquer ordem: os bancos novos acabam convergindo para uma réplica do monolito.
Na verificação, um script amostra UUIDs aleatórios e confere as faixas adjacentes. Nas leituras sombra, a aplicação lê dos dois bancos, compara, descarta a cópia shardada e registra as divergências.
Na troca, o servidor foi parado para o catch-up terminar de propagar as escritas aos shards, e o produto ficou fora do ar por cinco minutos de manutenção programada.
O Notion escolheu o log de auditoria porque a replicação lógica não acompanhou o volume de escrita da tabela block durante o snapshot inicial. O script final levou cerca de três dias para fazer o backfill de produção. O time também preparou e testou um log de auditoria reverso, para voltar dos shards ao monolito se fosse preciso; no fim, não precisou reverter.
A verificação teve custos e cuidados. As leituras sombra aumentaram a latência da API, mas deram confiança de que a troca seria suave. A migração e a verificação foram implementadas por pessoas diferentes: com a mesma pessoa nas duas etapas, haveria mais chance de repetir o mesmo erro e enfraquecer a verificação.
05 · MIGRAÇÃO NO FIGMA
Sharding lógico primeiro, divisão física depois
O Figma partiu de outras metas. Quis evitar backfills de tabelas grandes, manter a capacidade de reverter mesmo depois de concluída uma operação de sharding físico e fugir da escrita dupla, difícil de implementar sem downtime ou sem abrir mão da consistência.
A ideia central foi separar o sharding lógico, na camada de aplicação, do sharding físico, no Postgres. Reverter o sharding lógico era uma mudança simples de configuração; reverter uma operação física é possível, mas exige coordenação mais complexa para manter a consistência dos dados.
As consultas da aplicação passam pelo DBProxy, um serviço em Go entre a aplicação e o PgBouncer.
No DBProxy, o parser transforma o SQL em árvore sintática e o planejador lógico extrai o tipo da consulta e os IDs dos shards lógicos.
O planejador físico mapeia os shards lógicos para bancos físicos e reescreve a consulta para o shard certo.
Antes da divisão física, cada shard lógico é uma view do Postgres e toda leitura e escrita passa por elas. O sistema parece shardado, mesmo com os dados num único host.
Se surgia um bug, bastava redirecionar o tráfego de volta para a tabela principal, em segundos.
As views tinham um risco: em alguns casos poderiam mudar fundamentalmente como o planejador do Postgres otimiza as consultas. Para validar, o Figma coletou um corpus de consultas de produção sanitizadas, rodou testes de carga com e sem views e confirmou sobrecarga mínima na maioria dos casos e menor que dez por cento no pior. Também montou um framework de leituras sombra, que enviava o tráfego de leitura pelas views para comparar desempenho e correção.
Com o sharding lógico validado, vem a operação física: copiar os dados de um banco, distribuí-los em vários e redirecionar o tráfego de leitura e escrita.
Em vez de replicação lógica filtrada, cada destino recebe o conjunto inteiro de dados e só pode ler e escrever o subconjunto que pertence aos seus shards.
A operação deixa de ir de um banco para um e passa a ir de um para N, e precisa tolerar o caso em que só parte dos bancos tem sucesso.
O DBProxy passa a rotear para os bancos novos e atualiza o seu estado de topologia para não mandar consultas ao banco errado durante as divisões.
Na primeira tabela, o failover teve só dez segundos de disponibilidade parcial nos primários e nenhum impacto de disponibilidade nas réplicas.
A primeira tabela horizontalmente shardada foi para produção em setembro de 2023. De ponta a ponta, shardar essa primeira tabela levou cerca de nove meses. O Figma não viu regressões de latência ou disponibilidade após o sharding.
06 · LIÇÕES E TRADE-OFFS
O que cada caminho custou e o que ficou por fazer
| Aspecto | Notion | Figma |
|---|---|---|
| Como os dados chegam ao destino | Log de auditoria, catch-up e backfill | Cópia do conjunto inteiro de dados, com leitura e escrita restritas ao subconjunto de cada shard |
| Reversão | Log de auditoria reverso preparado e testado | Sharding lógico revertido por configuração; a operação física exige mais coordenação |
| Verificação | Amostragem de UUIDs e leituras sombra | Leituras sombra pelas views, comparando desempenho e correção |
| Troca | Servidor parado até o catch-up terminar | Segundos de disponibilidade parcial nos primários |
Em retrospectiva, o Notion diria que deveria ter shardado mais cedo. Esperou o banco ficar muito pressionado e teve de ser econômico com migrações para não aumentar a carga, o que o impediu de usar replicação lógica na escrita dupla. A chave de partição ainda não estava preenchida no banco antigo, e preenchê-la teria agravado a carga do monolito; por isso cada linha foi preenchida na hora de gravar nos shards, com um script de catch-up personalizado.
O Notion também apontou que a vazão da escrita dupla foi o gargalo da troca. Com mais uma semana otimizando o script para que o catch-up levasse menos de 30 segundos na troca, talvez fosse possível trocar no balanceador, sem downtime. E teria combinado a chave primária e a chave de partição numa única coluna.
O Figma, por sua vez, lista o que ainda falta: transações atômicas entre shards para casos críticos e índices únicos distribuídos, já que hoje só há suporte a índices que incluem a chave de shard. Também pretende reavaliar a abordagem própria frente a uma solução de código aberto ou gerenciada.
Perguntas frequentes
O que é Figma e Notion: sharding horizontal de Postgres?
Arquitetura real em produção. Pontos centrais: migração de dados online.
O que o design de Figma e Notion: sharding horizontal de Postgres cobre?
Ele está organizado nestas seções: Quando um Postgres só deixa de bastar, Construir o roteamento ou adotar um cluster pronto, Como cada consulta encontra o seu shard, Escrita dupla, backfill, verificação e troca, Sharding lógico primeiro, divisão física depois e O que cada caminho custou e o que ficou por fazer.
Quanto tempo leva Figma e Notion: sharding horizontal de Postgres e qual é o nível?
Leva cerca de 17 minutos para ler e tem nível Sênior.
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.