A parte do PostgreSQL que mais odiamos (2023)
(cs.cmu.edu)- O PostgreSQL surgiu recentemente como a escolha padrão da internet, mas, na experiência da Carnegie Mellon e da OtterTune, a implementação de MVCC traz uma carga operacional maior do que MySQL, Oracle e Microsoft SQL Server
- O projeto central é um modelo de armazenamento append-only que, ao atualizar uma linha, não sobrescreve a linha existente, mas cria uma nova versão física, junto com uma cadeia O2N que aponta da versão antiga para a nova
- Essa estrutura leva à cópia de tuplas inteiras, acúmulo de dead tuples, atualização de todos os índices e dependência de autovacuum; parte desse custo só pode ser evitada quando atualizações HOT são possíveis
- Na análise de PostgreSQL dos clientes da OtterTune, em média apenas cerca de 46% das atualizações usaram a otimização HOT, e a configuração padrão de autovacuum pode atrasar muito a limpeza em tabelas grandes
- O PostgreSQL continua sendo um DBMS atraente, mas, em workloads com muita escrita e tabelas grandes, é preciso gerenciar diretamente espaço de armazenamento, I/O, memória, manutenção de índices e a operação de vacuum
Por que o MVCC do PostgreSQL é um problema
- Há muitas opções de banco de dados — em abril de 2023, DBDB listava 897 —, mas em cada época existiu uma escolha padrão de fato
- Nos anos 2000, o MySQL, usado por Google e Facebook, era a escolha convencional
- Nos anos 2010, o MongoDB ganhou atenção com a imagem de “webscale”
- Nos últimos cinco anos, o PostgreSQL passou a ser amplamente preferido por sua estabilidade, riqueza de recursos, extensibilidade e adequação a workloads operacionais
- O foco do texto é a implementação de controle de concorrência multiversão (MVCC) do PostgreSQL
- Com base em pesquisas da Carnegie Mellon e na experiência de otimização do Amazon RDS PostgreSQL, a implementação de MVCC do PostgreSQL é considerada pior do que a de MySQL, Oracle e Microsoft SQL Server
- O Amazon Aurora PostgreSQL também sofre dos mesmos problemas estruturais
O objetivo do MVCC e a escolha de projeto do PostgreSQL
- MVCC é um método para permitir que múltiplas consultas leiam e escrevam no banco ao mesmo tempo sem se atrapalharem, sempre que possível
- O DBMS mantém múltiplas versões físicas para cada linha lógica, em vez de sobrescrever a linha existente
- As consultas leem a versão adequada para si de acordo com a ordem das versões, como o momento de criação
- Isso permite isolamento de snapshot, em que a transação vê o snapshot do banco no momento em que começou
- Operações de leitura podem reduzir situações em que ficam bloqueadas por locks explícitos de registro causados por escritas que atualizam o mesmo item
- Um DBMS com MVCC precisa decidir basicamente três coisas
- Como armazenar atualizações de linhas existentes
- Como encontrar, em tempo de execução, a versão correta de uma linha para cada consulta
- Como remover versões expiradas que já não são mais visíveis
- Por causa da primeira escolha feita pelo PostgreSQL nos anos 1980, ele carrega até hoje custos também nas outras duas áreas
Armazenamento de versões append-only
- O PostgreSQL foi projetado desde o início para suportar múltiplas versões e, ao atualizar, não sobrescreve a linha existente; ele cria uma cópia e aplica as mudanças nessa nova versão
- Esse método pode ser visto como armazenamento de versões append-only
- Ao atualizar uma tupla existente, o DBMS garante um slot livre na tabela para a nova versão da linha
- Copia o conteúdo da versão atual da linha para a nova versão
- Aplica as mudanças no slot da nova versão recém-alocado
- A tabela de exemplo é
movies, com informações sobre filmesidé a chave primária, e há colunasname,yearedirector- Existem o índice primário
movies_pkeye os índices B+Tree secundáriosidx_nameeidx_director
- Numa atualização que muda o ano de lançamento de
"Shaolin and Wu Tang"de 1985 para 1983, a tupla original é copiada e o novo ano é aplicado na nova versão- Se não houver espaço na página existente, a nova versão pode ser criada em outra página da tabela
Cadeia de versões e o modelo O2N
- Quando várias versões físicas representam a mesma linha lógica, o DBMS precisa registrar a relação entre elas
- Um DBMS com MVCC cria uma cadeia de versões como uma lista encadeada unidirecional
- A cadeia segue em apenas uma direção para reduzir custo de armazenamento e manutenção
- Há duas formas de ordenar essa cadeia
- N2O: a versão mais nova aponta para a anterior, e a cabeça da cadeia é sempre a versão mais recente
- O2N: cada versão aponta para a nova versão, e a cabeça da cadeia é a versão mais antiga
- A maioria dos DBMS, incluindo Oracle e MySQL, implementa N2O
- O PostgreSQL, com exceção do mecanismo In-Memory OLTP do Microsoft SQL Server, é um caso raro de uso de O2N
- O2N pode reduzir a necessidade de atualizar índices para apontar para a nova versão a cada modificação de tupla
- Em troca, pode ser preciso percorrer uma cadeia longa de versões para encontrar a mais recente
- O campo
t_tcidno cabeçalho da linha do PostgreSQL guarda o ID da tupla da próxima versão ou, se for a versão mais recente, o seu próprio ID de tupla- Se o índice apontar para uma versão antiga, o PostgreSQL precisa seguir a cadeia até encontrar a nova versão
Como reduzir a navegação da cadeia com índices
- Os desenvolvedores do PostgreSQL sabiam desde cedo de dois custos
- O custo alto de criar uma nova cópia da tupla inteira a cada atualização
- O desperdício de percorrer a cadeia inteira de versões para encontrar a versão mais recente que a maioria das consultas quer
- Para evitar percorrer cadeias longas, o PostgreSQL adiciona entradas de índice para cada versão física de uma linha
- Se uma única linha lógica tiver 5 versões físicas, pode haver até 5 entradas de índice para essa tupla
- Se o índice
idx_nameapontar para várias versões de"Shaolin and Wu Tang", o PostgreSQL pode acessar diretamente a versão mais recente
- O acesso à versão mais recente pode ficar mais rápido, mas os índices crescem e o custo de manutenção aumenta
Otimização de atualização HOT
- Para reduzir situações em que versões relacionadas ficam espalhadas por várias páginas e surgem várias entradas de índice, o PostgreSQL usa atualizações HOT (heap-only tuple)
- Há duas condições para uma atualização HOT ser possível
- A atualização não modifica colunas referenciadas pelos índices da tabela
- Há espaço suficiente para armazenar a nova versão na mesma página de dados da versão anterior
- Quando HOT se aplica, os índices continuam apontando para a versão antiga, e a consulta encontra a versão mais recente seguindo a cadeia de versões
- O PostgreSQL também realiza uma otimização que remove versões antigas durante a operação normal para podar a cadeia de versões
Remoção de dead tuples e vacuum
- Como o PostgreSQL cria uma cópia da linha a cada atualização, ele precisa remover as versões antigas, ou dead tuples
- O PostgreSQL inicial, nos anos 1980, não removia dead tuples
- A ideia era que manter versões antigas permitiria executar consultas de time-travel para ver o estado do banco em um ponto específico no tempo
- Mas, sem remover dead tuples, o tamanho da tabela não diminuía mesmo com deleções, e a cadeia de versões de tuplas frequentemente atualizadas ficava longa
- O PostgreSQL limpa dead tuples por meio do processo de vacuum
- O vacuum faz um scan sequencial das páginas da tabela modificadas desde a última execução e procura versões expiradas
- Se uma versão não for visível para nenhuma transação ativa, ela é considerada expired
- Como as transações atuais não acessam essa versão e as futuras usarão a versão live mais recente, reutilizar esse espaço é seguro
- Dependendo da configuração, o PostgreSQL executa autovacuum periodicamente de forma automática
- É possível ajustar globalmente a frequência de vacuum em todas as tabelas
- Também há configuração de autovacuum por tabela
- O usuário pode executar manualmente com o comando SQL
VACUUM
Problema 1: cópia da tupla inteira
- Em MVCC append-only, mesmo que só uma coluna da tupla mude, todas as colunas são copiadas para a nova versão
- Esse método aumenta muito a duplicação de dados e a necessidade de espaço de armazenamento
- O PostgreSQL pode precisar de mais memória e disco do que outros DBMS para armazenar o mesmo banco de dados
- Como resultado, consultas podem ficar mais lentas e o custo em nuvem pode aumentar
- MySQL e Oracle armazenam um delta compactado entre a nova versão e a versão atual
- Numa tabela com 1000 colunas, se apenas uma mudar, só é gravado um registro delta para essa coluna alterada
- O PostgreSQL cria uma nova versão contendo a única coluna alterada e as outras 999 que não mudaram
- Os atributos TOAST do PostgreSQL são tratados de forma diferente, então ficam fora dessa comparação
- Em 2013, a EnterpriseDB iniciou o projeto zheap para trocar o mecanismo append-only por um baseado em versões delta
- A última atualização oficial foi um post de status de 2021
- Desde então, não houve progresso claro
Problema 2: bloat de tabela
- As versões expiradas do PostgreSQL, isto é, dead tuples, ocupam mais espaço do que versões delta
- Em workloads com muita escrita, dead tuples podem se acumular mais rápido do que o autovacuum consegue limpar
- A tabela pode continuar crescendo
- Como dead tuples e live tuples ficam misturadas nas páginas, a consulta precisa carregar dead tuples na memória durante a execução
- Bloat fora de controle faz scans de tabela consumirem mais IOPS e memória, reduzindo o desempenho das consultas
- Se dead tuples tornarem as estatísticas do otimizador imprecisas, isso pode levar a planos de execução ruins
- Por exemplo, se a tabela
moviestiver 10 milhões de live tuples e 40 milhões de dead tuples, então 80% são dados obsoletos- Se o tamanho médio da tupla for 1 KB, as live tuples ocupam 10 GB e as dead tuples, cerca de 40 GB
- O tamanho total da tabela é 50 GB
- Num scan completo da tabela, o PostgreSQL precisa ler 50 GB do disco e carregar isso na memória, mesmo que a maior parte sejam dados obsoletos
- O PostgreSQL tem mecanismos de proteção para evitar que scans sequenciais poluam o cache do buffer pool, mas isso não elimina o custo de I/O em si
VACUUM vs. VACUUM FULL
- Mesmo que o autovacuum rode regularmente e acompanhe o workload, o autovacuum normal não consegue devolver espaço de armazenamento ao sistema operacional
- O
VACUUMnormal remove dead tuples e reorganiza live tuples dentro de cada página, mas não recupera páginas vazias do disco - Se o PostgreSQL conseguir esvaziar a última página, essa página pode ser truncada, mas as outras permanecem no disco
- Em uma tabela de 50 GB, mesmo removendo 40 GB de dead tuples, o PostgreSQL pode continuar mantendo 50 GB alocados do ponto de vista do sistema operacional ou do RDS
- Para realmente devolver o espaço não usado, é preciso reescrever a tabela inteira em uma nova área com
VACUUM FULLou pg_repack- Ambas as operações consomem muitos recursos e podem levar bastante tempo
- Em bancos de dados de produção, podem impactar fortemente o desempenho das consultas
- O
VACUUM FULLremove dead tuples de cada página, compacta as live tuples restantes em páginas novas e então apaga as páginas desnecessárias
Problema 3: manutenção de índices secundários
- O PostgreSQL precisa atualizar todos os índices de uma tabela sempre que uma tupla é atualizada
- Isso acontece porque tanto o índice primário quanto os índices secundários armazenam a localização física exata da versão
- A menos que seja uma atualização HOT, em que a nova versão fica na mesma página da anterior, isso é necessário em toda atualização
- No exemplo de atualização, o PostgreSQL cria a nova versão em
Table Page #2e depois insere entradas apontando para ela emmovies_pkey,idx_directoreidx_name - Essa estrutura, que exige modificar todos os índices, cria vários custos de desempenho
- Consultas de atualização ficam mais lentas
- Há I/O adicional para percorrer cada índice e inserir a nova entrada
- Surgem contenções de lock/latch em estruturas internas, como os índices e a tabela de páginas do buffer pool
- Há custo de manutenção até para índices que a consulta real nem usa
- Em DBMS cobrados por IOPS, como o Amazon Aurora, leituras e escritas extras viram problema
- Na análise dos bancos PostgreSQL dos clientes da OtterTune, em média cerca de 46% das atualizações usaram a otimização HOT
- Os mais de 50% restantes pagaram o custo de manutenção de índices
- O texto de 2016 da Uber sobre a migração de Postgres para MySQL é um caso representativo desse problema
- Em tabelas com muitos índices secundários, workloads com muita escrita sofreram grandes problemas de desempenho
- Oracle e MySQL não têm o mesmo problema porque índices secundários não armazenam o endereço físico da nova versão
- Eles guardam identificadores lógicos, como ID da tupla ou chave primária
- O DBMS usa esse identificador lógico para encontrar o endereço físico da versão atual
- Leituras por índice secundário podem ser mais lentas, mas outros benefícios da implementação de MVCC reduzem o overhead
Problema 4: dificuldade operacional do autovacuum
- O desempenho do PostgreSQL depende muito da eficácia do autovacuum para remover dados obsoletos e permitir o reaproveitamento do espaço
- RDS, Aurora e Aurora Serverless são todos variantes de PostgreSQL, então sofrem os mesmos problemas de autovacuum
- O autovacuum é complexo e difícil de ajustar para operar de forma ideal
- A configuração padrão não serve bem para todas as tabelas
- O problema fica especialmente pior em tabelas grandes
- O valor padrão de
autovacuum_vacuum_scale_factoré 20%- Em uma tabela com 100 milhões de tuplas, é preciso atualizar pelo menos 20 milhões de tuplas para disparar o autovacuum
- Isso pode fazer com que muitas dead tuples permaneçam na tabela por muito tempo, gerando custo de I/O e memória
- Transações longas podem bloquear o autovacuum
- Quando a limpeza de versões expired atrasa, dead tuples e estatísticas antigas se acumulam
- Problemas de desempenho podem gerar ainda mais transações longas, que por sua vez bloqueiam o autovacuum, criando um ciclo vicioso
- Nesses casos, pode ser necessário encerrar manualmente as transações longas
Casos de clientes da OtterTune
- Em um banco Amazon RDS PostgreSQL, a contagem de dead tuples variou em padrão de dente de serra durante duas semanas
- O autovacuum fazia uma grande limpeza cerca de uma vez por dia
- Em 14 de fevereiro, o DBMS limpou 3,2 milhões de dead tuples
- No gráfico como um todo, a quantidade de dead tuples seguia em tendência de alta, um estado anormal em que o autovacuum não conseguia acompanhar
- Em uma instância PostgreSQL RDS de um cliente da OtterTune, estatísticas antigas após uma inserção em massa causaram uma long-running query
- Essa consulta bloqueou a atualização de estatísticas pelo autovacuum
- Como resultado, mais long-running queries passaram a ocorrer
- O health check automático da OtterTune identificou o problema, mas o administrador precisou encerrar manualmente a consulta e executar ANALYZE após a inserção em massa
- O tempo de execução dessa consulta longa caiu de 52 minutos para 34 segundos
Conclusão prática
- Sempre há escolhas difíceis no projeto de um DBMS, e essas escolhas afetam o desempenho conforme o workload
- No workload intensivo em escrita específico da Uber, a amplificação de escrita em índices causada pelo MVCC do PostgreSQL foi um motivo para migrar para MySQL
- A implementação de MVCC do PostgreSQL é vista como um modelo que não deveria ser seguido ao criar um novo DBMS com MVCC
- A combinação de armazenamento append-only com autovacuum é o problema central
- Esse projeto é um legado dos anos 1980, anterior à popularização dos padrões de sistemas log-structured a partir dos anos 1990
- O PostgreSQL continua sendo um DBMS preferido, mas precisa ser operado aceitando as fraquezas do seu MVCC
- O caminho alternativo é investir muito tempo e esforço em tuning
1 comentários
Comentários do Hacker News
Mesmo achando que conhecia bastante os internos do Postgres, este texto foi excelente e aprendi muito
Uma das fraquezas fundamentais parece ser o fato de o Postgres ter escolhido a abordagem O2N, e não N2O, para rastrear versões de linhas. Mudar para N2O não resolveria todos os problemas — por exemplo, continuaria existindo o problema de armazenar cópias completas das linhas —, mas, pela perspectiva 80/20, parece que reduziria a maior parte das desvantagens da implementação atual
Como a maioria das transações vai querer a versão mais recente da linha, usando a ordem N2O só seria necessário seguir a lista encadeada quando uma versão antiga fosse necessária, e é bem possível que não fosse preciso armazenar cada versão de linha nos índices
Ele dá a primeira aula inteira nas ruas de Amsterdã porque não conseguiu entrar no hotel; além de ser uma figura interessante, tem uma capacidade incrível de explicar o funcionamento interno
A grande vantagem é que, quando a carga de trabalho consiste principalmente em INSERTs e depois exclusão de tabelas, não é preciso espaço adicional
Em geral, também não é necessário dividir a transação de inserção. Isso porque não há, na prática, limite para o tamanho dos dados gerados ou para o número total de linhas alteradas. Há um limite para o número de comandos dentro de uma transação, mas, se você não alterar tabelas com frequência demais, dá para contornar com COPY FROM
Do ponto de vista de DBA, não é preciso gerenciar espaço de rollback/undo separadamente do armazenamento das tabelas. Depende da aplicação, mas o projeto do PostgreSQL não é perdedor em todos os aspectos. Ou seja, não é como bubble sort
Ele tem bom desempenho quando a lista está quase ordenada, e isso acontece em renderização 3D ao ordenar objetos pela distância até a câmera. Se você move ou gira um pouco a câmera, o bubble sort funciona muito bem ao reordenar com base na ordem do frame anterior
Para evitar o pior caso, basta contar o número de comparações que falharam na última passada e o número de passadas realizadas até então; se passar de um limite, troca-se para outro algoritmo de ordenação
É uma tecnologia obsoleta há mais de 30 anos; então, mesmo que não seja perdedora em todos os aspectos, eu diria que é perdedora nos aspectos mais importantes
Acho que este texto está errado especialmente nesta parte
“Nos anos 2000, a sabedoria convencional escolheu o MySQL porque estrelas tecnológicas em ascensão como Google e Facebook o usavam. Nos anos 2010, foi o MongoDB, que virou ‘web-scale’ graças a escritas não duráveis. Nos últimos 5 anos, o PostgreSQL se tornou o DBMS queridinho da internet. E por bons motivos! Ele é confiável, rico em recursos, extensível e se encaixa bem na maioria das cargas de trabalho operacionais.”
Engenheiros inteligentes escolheram o Postgres não por causa de uma falácia de apelo à popularidade, mas por segurança dos dados, ACID, semelhança com Oracle, MVCC, conformidade com o padrão SQL, a equipe do Postgres, uma comunidade excelente e prestativa, tipos de dados, alto desempenho e a flexibilidade da licença BSD
Foi por esses motivos que escolhemos Postgres na ATT no começo dos anos 2000, e DBAs de Oracle aceitaram a transição com muita facilidade. Enquanto o MySQL passava por uma transição turbulenta, o PG continuou ficando mais forte e melhorando. Acho que Bruce Momjian teve um papel importante nesse sucesso, e há uma comunidade realmente excelente
Eu queria usar views de banco de dados para criar uma camada de compatibilidade “ao vivo” entre um esquema de banco de dados AS400 antigo e uma aplicação Rails moderna
Depois disso, minha preferência só cresceu por causa de coisas como segurança dos dados e DDL dentro de transações
“MySQL e Oracle armazenam um delta compactado entre a nova versão e a versão atual (pense como um git diff).”
O Git não é famoso justamente por não armazenar diffs, mas sim o objeto novo e o objeto anterior inteiro, como a abordagem que o Postgres usa aqui?
Ele compacta com packing para acelerar clones, mas a forma bruta com que o Git lida é essa coleção de blobs
Repositórios Git também fazem compressão, e essa compressão é de certa forma baseada em diff, mas não é baseada no histórico de commits como se poderia ingenuamente esperar
Em consultas SQL, também não é como se o diff fosse retornado diretamente
“A necessidade de o PostgreSQL modificar todos os índices da tabela a cada atualização tem vários impactos de desempenho. Naturalmente, como o sistema precisa fazer mais trabalho, consultas de update ficam mais lentas.”
Eu tinha curiosidade sobre essa amplificação de escrita. É verdade que o MySQL não precisa atualizar índices dessa forma, mas a replicação do MySQL depende do binlog, e toda alteração precisa ser registrada adicionalmente, além do próprio banco de dados (como o redo log do InnoDB)
Então, MySQL usado em cluster parece ter outro tipo de amplificação de escrita. O PostgreSQL reutiliza o WAL para replicação, então não há essa amplificação
Além disso, no lado receptor, o MySQL primeiro grava o binlog recebido no relay log; depois, o relay log é consumido pela thread applier, gerando escritas adicionais no InnoDB e, por padrão, binlog adicional
Este assunto não pode ser discutido sem falar de disco.
SSDs gravam páginas de 4 KB por vez. Ou seja, mesmo que você atualize só 1 bit, o disco lê 4 KB, altera o bit e depois grava de novo a página de 4 KB em um novo slot. Portanto, a penalidade da cópia depende do tipo de disco.
A AWS cobra uma taxa fixa pelo MBps de I/O, mas não sei se existe uma regra que arredonda para os 4 KB mais próximos, ou se, em vez do volume de requisições, ela rastreia o volume de gravação do próprio drive e cobra a quantidade real de I/O na implementação do armazenamento.
“Oracle e MySQL não têm esse problema na implementação de MVCC, porque índices secundários não armazenam o endereço físico da nova versão. Em vez disso, armazenam um identificador lógico (por exemplo, ID da tupla, chave primária), e o DBMS usa isso para encontrar o endereço físico da versão atual. Isso pode deixar leituras em índices secundários mais lentas, mas esses DBMSs têm outras vantagens na implementação de MVCC que reduzem o overhead.”
Há um comportamento interessante que observei no MySQL. Era um banco de dados de cerca de 500 GB, e o esquema era mais próximo de orientado a documentos do que relacional;
SELECT id WHERE something; UPDATE what WHERE id=idera várias ordens de grandeza mais rápido do queUPDATE what WHERE something.Suspeito que a razão para esse comportamento esteja provavelmente aqui. Dito isso, cargas de trabalho normais não fazem isso; só fica lento em DMLs temporários para corrigir inconsistências.
Já UPDATE de fato faz gravações e pode até bloquear a tabela.
UPDATE id=idpermite bloqueio em nível de linha. Porém, também há o risco de perder registros inseridos entre o SELECT e o UPDATE.Concordo que atualizações em massa podem ser lentas demais. No fim, muitas vezes acabamos processando as atualizações gradualmente em lotes, ou até usando COPY.
“Nos anos 2010, era o MongoDB. Ele virou ‘webscale’ graças a gravações não duráveis.”
Fugindo do tema, mas foi marketing do começo ao fim: https://news.ycombinator.com/item?id=15124306
A versão 4.2.8 (2020) ficou bastante sólida e é conhecida por não fazer gravações sujas: https://en.wikipedia.org/wiki/MongoDB#Technical_criticisms
O que mais odeio é que, mesmo em 2024, ele ainda só fica realmente utilizável se você colocar um pooler de conexões na frente (por exemplo, pgbouncer).
O OrioleDB tentou resolver esse problema com um novo mecanismo de armazenamento: https://github.com/orioledb/orioledb
O Oriole agora se juntou à Supabase, e Alexander e sua equipe estão trabalhando nisso em tempo integral. O conjunto de patches está aqui: https://www.orioledb.com/docs#patch-set
A expectativa é que também seja possível experimentá-lo na plataforma da Supabase até o fim deste ano.