Simultaneidade do PostgreSQL: não esférica, não é cavalo, não está no vácuo



Escalar um DBMS é um futuro que avança continuamente. Os DBMSs melhoram e escalam melhor nas plataformas de hardware, enquanto as próprias plataformas de hardware aumentam a produtividade, o número de núcleos e a memória - o Achilles está alcançando a tartaruga, mas ainda não o fez. O problema de dimensionar o DBMS está em pleno andamento.

O Postgres Professional teve um problema com o dimensionamento não apenas teoricamente, mas também praticamente: com seus clientes. E mais de uma vez. Um desses casos será discutido neste artigo.

O PostgreSQL se adapta bem aos sistemas NUMA, se for uma única placa-mãe com vários processadores e vários barramentos de dados. Algumas otimizações podem ser lidas aqui e aqui . No entanto, existe outra classe de sistemas, eles têm várias placas-mãe, cuja troca de dados é realizada por interconexão, enquanto uma instância do sistema operacional está trabalhando neles e para o usuário esse design parece uma única máquina. E, embora formalmente, esses sistemas também possam ser atribuídos ao NUMA, mas, em essência, eles estão mais próximos dos supercomputadores, como o acesso à memória local do nó e o acesso à memória do nó vizinho diferem radicalmente. A comunidade do PostgreSQL acredita que a única instância do Postgres em execução nessas arquiteturas é uma fonte de problemas, e ainda não existe uma abordagem sistemática para resolvê-los.

Isso ocorre porque a arquitetura do software que utiliza memória compartilhada é fundamentalmente projetada para o fato de que o tempo de acesso de diferentes processos à memória própria e remota é mais ou menos comparável. No caso em que trabalhamos com muitos nós, a aposta na memória compartilhada como um canal de comunicação rápido deixa de se justificar, porque, devido à latência, é muito "mais barato" enviar uma solicitação para executar uma determinada ação ao nó (nó) em que dados interessantes do que enviar esses dados no barramento. Portanto, para supercomputadores e, em geral, sistemas com muitos nós, as soluções de cluster são relevantes.

Isso não significa que a combinação de sistemas com vários nós e a arquitetura de memória compartilhada típica do Postgres precise ser encerrada. Afinal, se os processos do postgres passam a maior parte do tempo realizando cálculos complexos localmente, essa arquitetura será muito eficiente. Em nossa situação, o cliente já havia comprado um poderoso servidor de vários nós e tivemos que resolver os problemas do PostgreSQL nele.

Mas os problemas eram graves: as solicitações de gravação mais simples (alteram vários valores de campo em um registro) foram executadas em um período de alguns minutos a uma hora. Como foi confirmado posteriormente, esses problemas se manifestaram em toda a sua glória justamente por causa do grande número de núcleos e, consequentemente, pelo paralelismo radical na execução de solicitações com uma troca relativamente lenta entre os nós.

Portanto, o artigo será, por assim dizer, com duplo objetivo:

  • Compartilhar experiência: o que fazer se, em um sistema com vários nós, o banco de dados ficar mais lento a sério. Por onde começar, como diagnosticar para onde se mover.
  • Descreva como os problemas do DBMS do PostgreSQL podem ser resolvidos com um alto nível de simultaneidade. Incluindo como a alteração no algoritmo para bloquear bloqueios afeta o desempenho do PostgreSQL.

Servidor e DB


O sistema consistia em 8 lâminas com 2 soquetes em cada. No total, mais de 300 núcleos (excluindo hipertrefeição). Um pneu rápido (tecnologia proprietária do fabricante) conecta as lâminas. Não é um supercomputador, mas para uma instância do DBMS, a configuração é impressionante.
A carga também é bastante grande. Mais de 1 terabyte de dados. Cerca de 3000 transações por segundo. Mais de 1000 conexões para o postgres.

Tendo começado a lidar com as expectativas de gravação a cada hora, a primeira coisa que fizemos foi gravar no disco como causa de atrasos. Assim que começaram os atrasos incompreensíveis, os testes começaram a ser realizados exclusivamente em tmpfs . A imagem não mudou. O disco não tem nada a ver com isso.

Introdução ao diagnóstico: exibições


Como os problemas surgiram provavelmente devido à alta competição de processos que "batem" nos mesmos objetos, a primeira coisa a verificar são os bloqueios. No PostgreSQL, há uma visão pg.catalog.pg_locks e pg_stat_activity para essa verificação. O segundo, já na versão 9.6, adicionou informações sobre o que o processo está aguardando ( Amit Kapila, Ildus Kurbangaliev ) - wait_event_type . Os valores possíveis para este campo estão descritos aqui .

Mas primeiro, basta contar:

 postgres=# SELECT COUNT(*) FROM pg_locks; count —---— 88453 (1 row) postgres=# SELECT COUNT(*) FROM pg_stat_activity; count —---— 1826 (1 row) postgres=# SELECT COUNT(*) FROM pg_stat_activity WHERE state ='active'; count —---— 1005 

Estes são números reais. Atingiu até 200.000 bloqueios.
Ao mesmo tempo, esses bloqueios dependiam do pedido infeliz:

 SELECT COUNT(mode), mode FROM pg_locks WHERE pid =580707 GROUP BY mode; count | mode —-----+---------------— 93 | AccessShareLock 1 | ExclusiveLock 

Ao ler o buffer, o DBMS usa o bloqueio de share , enquanto escreve - exclusive . Ou seja, os bloqueios de gravação foram responsáveis ​​por menos de 1% de todas as solicitações.
Na visualização pg_locks , os tipos de bloqueio nem sempre são os descritos na documentação do usuário.

Aqui está a placa de fósforo:

 AccessShareLock = LockTupleKeyShare RowShareLock = LockTupleShare ExclusiveLock = LockTupleNoKeyExclusive AccessExclusiveLock = LockTupleExclusive 

A consulta SELECT mode FROM pg_locks mostrou que CREATE INDEX (sem CONCURRENTLY) aguardaria 234 INSERTs e 390 INSERTs para buffer content lock . Uma solução possível é “ensinar” INSERTs de diferentes sessões para interceptar menos em buffers.

É hora de usar perf


O utilitário perf coleta muitas informações de diagnóstico. No modo de record ... ele grava estatísticas de eventos do sistema em arquivos (por padrão, eles estão em ./perf_data ) e no modo de report analisa os dados coletados, por exemplo, é possível filtrar eventos que dizem respeito apenas ao postgres ou a um determinado pid :

 $ perf record -u postgres  $ perf record -p 76876  ,  $ perf report > ./my_results 

Como resultado, veremos algo como



Como usar o perf para diagnosticar o PostgreSQL é descrito, por exemplo, aqui , assim como no wiki pg .

No nosso caso, até o modo mais simples forneceu informações importantes sobre o perf top - perf top , que funciona, é claro, no espírito do sistema operacional top . Com o perf top vimos que na maioria das vezes o processador gasta nos PinBuffer() do núcleo, bem como nas PinBuffer() e LWLockAttemptLock(). .

PinBuffer() é uma função que aumenta o contador de referências ao buffer (mapeando uma página de dados para a RAM), graças a quais processos do postgres sabem quais buffers podem ser forçados a sair e quais não.

LWLockAttemptLock() - função de captura do LWLock . LWLock é um tipo de bloqueio com dois níveis de shared e exclusive , sem definir deadlock , os bloqueios são pré-alocados para a shared memory , os processos em espera estão aguardando em uma fila.

Essas funções já foram seriamente otimizadas no PostgreSQL 9.5 e 9.6. Os spinlocks dentro deles foram substituídos pelo uso direto de operações atômicas.

Gráficos de chama


É impossível sem eles: mesmo que fossem inúteis, ainda valeria a pena contar sobre eles - eles são extraordinariamente bonitos. Mas eles são úteis. Aqui está uma ilustração do github , não do nosso caso (nem nós nem o cliente estamos prontos para a divulgação de detalhes ainda).



Essas belas imagens mostram claramente o que os ciclos do processador levam. O mesmo perf pode coletar dados, mas o flame graph visualiza os dados de maneira inteligente e constrói árvores com base nas pilhas de chamadas coletadas. Você pode ler mais sobre criação de perfil com gráficos de chama, por exemplo, aqui , e baixar tudo o que precisa aqui .

No nosso caso, uma enorme quantidade de nestloop era visível nos gráficos de chama. Aparentemente, as JOINs de um grande número de tabelas em várias solicitações de leitura simultâneas causaram um grande número de bloqueios de access share de access share .

As estatísticas coletadas pelo perf mostram para onde os ciclos do processador vão. E, embora vimos que a maior parte do tempo do processador passa por bloqueios, não vimos exatamente o que leva a expectativas tão longas de bloqueios, pois não vemos exatamente onde as expectativas de bloqueio ocorrem, porque O tempo da CPU não é desperdiçado em espera.

Para ver as próprias expectativas, você pode criar uma solicitação para a visualização do sistema pg_stat_activity .

 SELECT wait_event_type, wait_event, COUNT(*) FROM pg_stat_activity GROUP BY wait_event_type, wait_event; 

revelou que:

 LWLockTranche | buffer_content | UPDATE ************* LWLockTranche | buffer_content | INSERT INTO ******** LWLockTranche | buffer_content | \r | | insert into B4_MUTEX | | values (nextval('hib | | returning ID Lock | relation | INSERT INTO B4_***** LWLockTranche | buffer_content | UPDATE ************* Lock | relation | INSERT INTO ******** LWLockTranche | buffer_mapping | INSERT INTO ******** LWLockTranche | buffer_content | \r 

(os asteriscos aqui simplesmente substituem os detalhes da solicitação que não divulgamos).

Você pode ver os valores buffer_content (bloqueando o conteúdo dos buffers) e buffer_mapping (bloqueando os componentes da placa de hash shared_buffers ).

Para ajuda ao gdb


Mas por que existem tantas expectativas para esses tipos de bloqueios? Para informações mais detalhadas sobre as expectativas, tive que usar o depurador GDB . Com o GDB , podemos obter uma pilha de chamadas de processos específicos. Ao aplicar a amostragem, ou seja, Depois de coletar um certo número de pilhas de chamadas aleatórias, você pode ter uma idéia de quais pilhas têm as maiores expectativas.

Considere o processo de compilação de estatísticas. Consideraremos a coleção “manual” de estatísticas, embora na vida real sejam utilizados scripts especiais que fazem isso automaticamente.

Primeiro, o gdb precisa ser anexado ao processo do PostgreSQL. Para fazer isso, encontre o pid processo pid servidor, digamos em

 $ ps aux | grep postgres 

Digamos que encontramos:

 postgres 2025 0.0 0.1 172428 1240 pts/17  S   23  0:00 /usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data 

e agora insira o pid no depurador:

 igor_le:~$gdb -p 2025 

Uma vez dentro do depurador, escrevemos bt [ou seja, backtrace ] ou where . E temos muitas informações sobre esse tipo:

 (gdb) bt #0 0x00007fbb65d01cd0 in __write_nocancel () from /lib64/libc.so.6 #1 0x00000000007c92f4 in write_pipe_chunks ( data=0x110e6e8 "2018‐06‐01 15:35:38 MSK [524647]: [392‐1] db=bp,user=bp,app=[unknown],client=192.168.70.163 (http://192.168.70.163) LOG: relation 23554 new block 493: 248.389503\n2018‐06‐01 15:35:38 MSK [524647]: [393‐1] db=bp,user=bp,app=["..., len=409, dest=dest@entry=1) at elog.c:3123 #2 0x00000000007cc07b in send_message_to_server_log (edata=0xc6ee60 <errordata>) at elog.c:3024 #3 EmitErrorReport () at elog.c:1479 

Depois de coletar estatísticas, incluindo pilhas de chamadas de todos os processos do postgres, coletadas repetidamente em diferentes momentos, vimos que o buffer partition lock dentro do relation extension lock durava 3706 segundos (cerca de uma hora), ou seja, bloqueia uma parte da tabela de hash do buffer gerente, necessário substituir o buffer antigo, para substituí-lo posteriormente por um novo correspondente à parte estendida da tabela. Um certo número de bloqueios de buffer content lock do buffer content lock também foi perceptível, o que correspondeu à expectativa de bloquear as páginas do índice da B-tree para inserção.



A princípio, duas explicações vieram para um tempo de espera tão monstruoso:

  • Alguém pegou esse LWLock e ficou preso. Mas isso é improvável. Porque nada complicado acontece dentro do bloqueio da partição de buffer.
  • Encontramos algum comportamento patológico do LWLock . Ou seja, apesar do fato de ninguém ter demorado muito na fechadura, sua expectativa durou excessivamente.

Patches de diagnóstico e tratamento de árvores


Ao reduzir o número de conexões simultâneas, provavelmente descarregaríamos o fluxo de solicitações para bloqueios. Mas isso seria como se render. Em vez disso, Alexander Korotkov , arquiteto-chefe do Postgres Professional (claro, ele ajudou a preparar este artigo), propôs uma série de patches.

Antes de tudo, era necessário obter uma imagem mais detalhada do desastre. Não importa quão boas sejam as ferramentas acabadas, as correções de diagnóstico de sua própria fabricação também serão úteis.

Foi criado um patch que adiciona um registro detalhado do tempo gasto na relation extension , o que está acontecendo dentro da função RelationAddExtraBlocks() . Portanto, descobrimos o tempo gasto dentro do RelationAddExtraBlocks().

E em apoio a ele, outro patch foi escrito relatando em pg_stat_activity o que estamos fazendo agora em relation extension . Isso foi feito da seguinte maneira: quando a relation expande, application_name se torna RelationAddExtraBlocks . Agora, esse processo é convenientemente analisado com o máximo de detalhes usando gdb bt e perf .

Na verdade, remendos médicos (e não diagnósticos) foram escritos dois. O primeiro patch mudou o comportamento dos bloqueios das folhas da B‐tree : antes, quando solicitado a inserção, a folha era bloqueada como share e, depois disso, era exclusive . Agora ele imediatamente se torna exclusive . Agora, este patch já foi confirmado para o PostgreSQL 12 . Felizmente, este ano, Alexander Korotkov recebeu o status de committer - o segundo commit do PostgreSQL na Rússia e o segundo na empresa.

O valor NUM_BUFFER_PARTITIONS também foi aumentado de 128 para 512 para reduzir a carga nos bloqueios de mapeamento: a tabela de hash do gerenciador de buffer foi dividida em partes menores, na esperança de que a carga em cada parte específica fosse reduzida.

Depois de aplicar esse patch, os bloqueios no conteúdo dos buffers desapareceram, mas, apesar do aumento de NUM_BUFFER_PARTITIONS , o buffer_mapping permaneceu, ou seja, lembramos de bloquear partes da tabela de hash do gerenciador de buffer:

 locks_count | active_session | buffer_content | buffer_mapping ----‐‐‐--‐‐‐+‐------‐‐‐‐‐‐‐‐‐+‐‐‐------‐‐‐‐‐‐‐+‐‐------‐‐‐ 12549 | 1218 | 0 | 15 

E mesmo isso não é muito. A árvore B não é mais um gargalo. A extensão da heap- veio à tona.

Tratamento de consciência


Em seguida, Alexander apresentou a seguinte hipótese e solução:

Aguardamos muito tempo no buffer parittion lock do buffer parittion lock ao buffer parittion lock buffer. Talvez no mesmo buffer parittion lock exista alguma página muito exigida, por exemplo, a raiz de alguma B‐tree Nesse ponto, há um fluxo contínuo de solicitações de shared lock das solicitações de leitura.

A fila de espera no LWLock "não é justa". Como shared lock podem ser obtidos quantos forem necessários ao mesmo tempo, se o shared lockshared lock sido realizado, os shared lock subsequentes serão transmitidos sem enfileiramento. Portanto, se o fluxo de bloqueios compartilhados tiver intensidade suficiente para que não haja "janelas" entre eles, aguardar um exclusive lock será quase infinito.

Para corrigir isso, você pode tentar oferecer - um patch de comportamento "cavalheiro" dos bloqueios. Isso desperta a consciência dos shared locker e eles fazem fila honestamente quando já existe um exclusive lock (curiosamente, fechaduras pesadas - hwlock - não têm problemas com a consciência: sempre fazem fila honestamente)

 locks_count | active_session | buffer_content | buffer_mapping | reladdextra | inserts>30sec ‐‐‐‐‐‐-‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐--‐-‐+‐‐‐‐‐‐-‐‐‐‐‐‐+‐‐‐‐------ 173985 | 1802 | 0 | 569 | 0 | 0 

Está tudo bem! Não há insert longas. Embora os bloqueios nas peças das placas de hash permanecessem. Mas o que fazer, essas são as propriedades dos pneus do nosso pequeno supercomputador.

Este patch também foi oferecido à comunidade . Mas não importa como o destino desses patches na comunidade se desenvolva, nada os impede de entrar nas próximas versões do Postgres Pro Enterprise , projetadas especificamente para clientes com sistemas muito carregados.

Moral


Bloqueios share leves de alta moral - blocos exclusive pulam a fila - resolveram o problema de atrasos por hora em um sistema de vários nós. A tag hash do buffer manager não funcionou devido ao excesso de fluxo de share lock , o que não deixou chance para os bloqueios necessários para suplantar buffers antigos e carregar novos. Problemas com a extensão do buffer para as tabelas do banco de dados foram apenas uma consequência disso. Antes disso, era possível expandir o gargalo com acesso à raiz da B-tree

O PostgreSQL não foi projetado para arquiteturas e supercomputadores NUMA. A adaptação a essas arquiteturas do Postgres é um trabalho enorme que exigiria (e possivelmente exigiria) os esforços coordenados de muitas pessoas e até de empresas. Mas as conseqüências desagradáveis ​​desses problemas arquitetônicos podem ser mitigadas. E precisamos: os tipos de carga que levaram a atrasos semelhantes aos descritos são bastante típicos, sinais de socorro semelhantes de outros lugares continuam chegando até nós. Problemas semelhantes apareceram anteriormente - em sistemas com menos núcleos, apenas as consequências não eram tão monstruosas e os sintomas foram tratados com outros métodos e outros patches. Agora outro medicamento apareceu - não universal, mas claramente útil.

Portanto, quando o PostgreSQL trabalha com a memória de todo o sistema como local, nenhum barramento de alta velocidade entre os nós pode comparar com o tempo de acesso à memória local. As tarefas surgem por causa disso difícil, muitas vezes urgente, mas interessante. E a experiência de resolvê-los é útil não apenas para os decisivos, mas também para toda a comunidade.

Source: https://habr.com/ru/post/pt423685/


All Articles