Alterando o esquema das tabelas do PostgreSQL sem bloqueios longos. Palestra Yandex

Se ao mesmo tempo muitas operações forem executadas para alterar o esquema do banco de dados, o serviço não poderá funcionar corretamente na gravação. O desenvolvedor Vladimir Kolyasinsky explicou quais operações no PostgreSQL requerem bloqueios de longo prazo e como a equipe do Yandex.Connect fornece quase 100% de acesso de gravação ao serviço durante essas operações. Além disso, você aprenderá sobre a biblioteca do Django, projetada para automatizar parte dos processos descritos.


Temos cargas pesadas, milhares de RPS e tempo de inatividade em alguns minutos, para não mencionar mais tempo, é inaceitável. É necessário que as migrações ocorram despercebidas pelo usuário. E com tais cargas, não será possível acordar às quatro da manhã, rolar alguma coisa quando não houver carga e voltar para a cama - porque a carga funciona 24 horas.

- Boa noite a todos! Meu nome é Vladimir, trabalho na Yandex há cinco anos. Nos últimos dois anos, desenvolvi serviços e serviços internos para organizações.

Um pouco sobre o que esses serviços são para as organizações. Estamos usando um grande número de serviços internos há muito tempo: um wiki para armazenar e trocar dados, um messenger para comunicação rápida com colegas, um rastreador para organizar o processo de trabalho, formulários para realizar pesquisas por dentro e por fora, além de muitos outros serviços.

Há algum tempo, decidimos que nossos serviços são legais e que podem ser úteis não apenas dentro do Yandex, mas também para pessoas de fora. Começamos a trazê-los para uma plataforma Yandex.Connect unificada, adicionando serviços externos existentes, como o Mail para um domínio.



Atualmente, estou desenvolvendo o Form Designer e o Wiki. A pilha usada é principalmente serviços escritos em Python da segunda e terceira versões; Django 1.9-1.11. Como banco de dados, a maioria é PostgreSQL. Também é o aipo com MongoDB e SQS como corretores. Tudo isso funciona no Docker.

Vamos para o problema que estamos enfrentando. Os serviços são populares, são usados ​​por centenas de milhares de pessoas todos os dias, os dados são acumulados, as tabelas se tornam cada vez mais e, com o tempo, muitas operações de alteração de esquemas de banco de dados, que foram executadas despercebidas pelos usuários ontem, começam a interferir na operação normal dos serviços.

Hoje falaremos sobre como lidamos com essas situações e como atingimos alta disponibilidade de serviços de leitura e gravação.

Primeiro, vamos considerar quais operações com o PostgreSQL requerem longos bloqueios na tabela. Por bloqueio, refiro-me a qualquer tipo de bloqueio que interfira no funcionamento normal da tabela - seja acesso exclusivo, que interfira na escrita e na leitura, ou níveis de bloqueio mais fracos que impedem apenas a gravação.

A seguir, veremos como evitar bloqueios durante essas operações. Depois, falaremos sobre quais operações com o PostgreSQL são inicialmente rápidas e não exigem bloqueios longos. E no final, vamos falar sobre nossa biblioteca zero_downtime_migrations, que usamos para automatizar algumas das técnicas descritas anteriormente para evitar bloqueios longos.

Operações que requerem um bloqueio longo:



Criando um índice. Por padrão, ele não bloqueia as operações de leitura na tabela, mas todas as operações de gravação serão bloqueadas durante todo o tempo em que o índice é criado; portanto, o serviço será somente leitura.

Além disso, essas operações incluem a adição de uma nova coluna com um valor padrão, pois sob o capô o PostgreSQL substituirá a tabela inteira e, por esse tempo, será bloqueado tanto para leitura quanto para gravação. Além disso, todos os seus índices serão substituídos.

Sobre a alteração do tipo de coluna - algo semelhante acontecerá, a placa também será substituída novamente. Deve-se observar que isso não apenas leva muito tempo em tabelas grandes, mas também requer um tempo curto até o dobro da quantidade de memória livre ocupada pela tabela.

Além disso, a operação VACUUM FULL requer o mesmo nível de bloqueio que as operações anteriores - este é o acesso exclusivo. O VACUUM FULL também bloqueará todas as operações de leitura e gravação na tabela.

As duas últimas operações estão adicionando propriedades exclusivas à coluna e, em geral, adicionando CONSTRAINT. Eles também exigem bloqueio durante a verificação dos dados, embora levem muito menos tempo do que os considerados anteriormente, pois não substituem as tabelas sob o capô.





Criando um índice. Aqui é bastante simples, ele pode ser criado usando a palavra-chave CONCURRENTLY. Qual a diferença? Essa operação levará mais tempo, já que não uma, mas várias passagens pela tabela serão executadas e também aguardará a conclusão de todas as operações atuais que possam potencialmente alterar o índice. E também pode falhar - por exemplo, se um índice exclusivo for violado ao criar um índice exclusivo. Em seguida, o índice será marcado como inválido e precisará ser excluído e recriado. O comando REINDEX não é recomendado, pois funciona da mesma forma que o CREATE INDEX regular, ou seja, bloqueará a tabela para gravação.

Em relação à exclusão do índice - a partir da versão 9.3, você também pode excluir o índice CONCURRENTLY para evitar o bloqueio durante a exclusão, embora em geral seja uma operação tão rápida.



Vamos dar uma olhada em adicionar uma nova coluna com um valor padrão. Aqui está uma operação padrão que é executada quando queremos executar esse comando, incluindo o Django que executa essa operação.

Como posso reescrevê-lo para evitar a substituição da tabela? Primeiro, em uma transação, adicione uma nova coluna sem um valor padrão e adicione um valor padrão em uma solicitação separada. Qual a diferença aqui? Quando adicionamos um valor padrão a uma coluna existente, isso não altera os dados existentes na tabela. Somente metadados são alterados. Ou seja, para todas as novas linhas esse valor padrão já estará garantido. Resta atualizar todas as linhas existentes que estavam na tabela no momento em que este comando foi executado. O que faremos em lotes de vários milhares de cópias para não bloquear por muito tempo uma grande quantidade de dados.

Após atualizar todos os dados, resta apenas executar SET NOT NULL se criarmos uma coluna NOT NULL. Se não criarmos, então não. Dessa forma, você pode evitar a substituição da tabela ao fazer esse tipo de alteração.

Essa sequência de comandos leva mais tempo que a execução de um comando regular, pois depende do tamanho da tabela e do número de índices nela, e o comando usual simplesmente bloqueia todas as operações e sobrescreve a tabela independentemente da carga, pois não há carga no momento. Mas isso não importa muito, porque durante a operação a tabela está disponível para leitura e gravação. Leva muito tempo, você só precisa seguir isso e pronto.



Sobre como alterar o tipo de coluna. A abordagem é semelhante à adição de uma coluna com um valor padrão. Primeiro, adicionamos uma coluna separada do tipo que precisamos e, em seguida, adicionamos gatilhos para alterar os dados na coluna original para gravar nas duas colunas de uma só vez, em uma nova com o tipo de dados que precisamos. Para todas as novas entradas, elas irão imediatamente para ambas as colunas. Precisamos atualizar todos os existentes. O que fazemos em porções, como no slide anterior, foi semelhante.

Depois disso, permanece em uma transação excluir o acionador, excluir a coluna antiga e renomear a coluna antiga para uma nova. Assim, alcançamos o mesmo resultado: alteramos o tipo da coluna, enquanto o bloqueio da tabela não demorou muito.



Sobre como adicionar uma coluna exclusiva. Um bloqueio é realizado no momento da criação. Isso pode ser evitado se você souber que a exclusividade no PostgreSQL é garantida através da criação de um índice exclusivo. Nós mesmos podemos criar o índice exclusivo necessário usando CONCURRENTLY. E depois de criar esse índice, crie CONSTRAINT usando esse índice. Depois disso, a definição do índice inicial da tabela desaparecerá e o resultado que a definição da tabela nos mostrará não será diferente após a execução dessas duas operações.



E, em geral, ao adicionar CONSTRAINT. Você pode usar esta técnica para evitar o bloqueio durante a verificação de dados. Primeiro, adicionamos CONSTRAINT com a palavra-chave NOT VALID. Isso significa que não é garantido que este CONSTRAINT seja executado para todas as linhas da tabela. Mas, ao mesmo tempo, para todas as novas linhas, esse CONSTRAINT já será aplicado e as exceções correspondentes serão lançadas se não forem executadas.

Só podemos validar todos os valores existentes, o que pode ser feito com um comando VALIDATE CONSTRAINT separado e, ao mesmo tempo, esse comando não interfere mais na leitura ou gravação na tabela. Uma tabela para esse horário estará disponível.

Operações que inicialmente funcionam rapidamente no PostgreSQL e não exigem bloqueios longos:



Uma dessas operações é adicionar uma coluna sem valores padrão e quaisquer restrições. Como nenhuma alteração é feita na tabela em si, apenas seus metadados são alterados. E todos os valores NULL que vemos como resultado de SELECT são misturados simplesmente na saída.

Além disso, adicionar valores padrão a um rótulo existente é uma operação rápida, pois apenas os metadados são alterados. A tabela e o bloqueio são obtidos literalmente pelos poucos milissegundos necessários para inserir essas informações.

Além disso, a operação rápida da configuração de SET NOT NULL, aqui leva um pouco mais do que o descrito anteriormente, cerca de alguns segundos por tabela de 30 milhões de registros. Esse tempo também pode ser evitado se for importante.

Renomear uma coluna, alterar o comprimento de uma coluna também não leva à substituição de uma coluna. Excluir uma coluna e, em geral, muitas entidades no PostgreSQL também é uma operação rápida.



Em relação à adição de uma coluna NOT NULL. Para evitar o bloqueio durante a validação, você pode executar o método mencionado anteriormente - adicione CONSTRAINT correspondente a CHECK (a coluna NÃO É NULL) NOT VALID e valide-a com um comando separado.

A diferença em geral é que essa restrição existirá no nível da tabela e não no nível da coluna na definição da tabela. Outra diferença é que isso pode afetar o desempenho, cerca de um por cento. Nesse caso, não haverá bloqueio, se o serviço estiver muito carregado, mesmo alguns segundos de bloqueio poderão levar a uma enorme fila de transações se acumulando e haverá um problema no serviço.



A exclusão de dados no PostgreSQL geralmente é uma operação rápida, uma vez que os dados não são excluídos imediatamente, apenas a coluna é marcada como desatualizada nos atributos da tabela, e os dados serão realmente excluídos somente após o início do próximo vácuo.



Vamos conversar sobre a biblioteca . Eu estou falando sobre Django, migração. Em geral, o Django é uma biblioteca para Python, uma estrutura da web, originalmente criada para criar rapidamente sites como notícias, desde então, foi significativamente atualizada. Existe um sistema ORM que permite a comunicação com registros no banco de dados, com tabelas, como se fossem objetos ou classes Python. Ou seja, cada tabela possui sua própria classe em Python. E quando fazemos alterações em nosso código Python, ou seja, adicionamos novos atributos como colunas à tabela, o Django durante o processo de criação da migração percebe essas alterações e cria os arquivos de migração para fazer alterações espelhadas no próprio banco de dados para que não divergam.

A biblioteca foi escrita para automatizar algumas das técnicas discutidas anteriormente para evitar bloqueios longos na tabela durante essas migrações. Ele trabalha com o Django desde a versão 1.8 a 2.1 inclusive, e o Python de 2.7 a 3.7 inclusive.

Com relação aos recursos atuais da biblioteca, está adicionando uma coluna com um valor padrão sem bloqueios, anulável ou não, criando um índice CONCURRENTLY, bem como a capacidade de reiniciar quando falha. Na implementação padrão do Django, se adicionarmos uma coluna com um valor padrão, a tabela estará bloqueada e, se for grande, poderá levar 40 minutos de bloqueio na minha experiência. A mesa está trancada e pronto, aguarde até que as alterações sejam copiadas e feitas. 30 minutos se passaram - eles pegaram o erro de conexão com o banco de dados, a migração caiu, as alterações não foram confirmadas e você deve iniciar novamente, aguarde 40 minutos novamente, bloqueando novamente a tabela por esse tempo.


Link do GitHub

A biblioteca permite retomar a migração do local em que foi interrompida. Quando você trava e reinicia, uma caixa de diálogo é exibida com várias opções de ação, ou seja, você pode continuar a atualizar os dados. Geralmente, isso é uma atualização de dados porque é o processo mais longo. A migração simplesmente continuará de onde parou. Essa operação também leva mais tempo do que uma operação padrão com bloqueio de tabela, mas, ao mesmo tempo, o serviço permanece operacional no momento.



Sobre a conexão como um todo. Existe documentação; em resumo, você precisa substituir o mecanismo nas configurações do banco de dados Django pelo mecanismo da biblioteca. Também existem vários mixins se você usar seus motores para se conectar.



Um exemplo de trabalho é sobre como adicionar uma coluna com um valor padrão. Aqui, adicionamos colunas com um valor booleano, True por padrão. Quais operações são executadas pelo SchemaEditor padrão? As operações que você pode ver se executar o SQL migram. Isso é bastante útil, pelo próprio tipo de migração, nem sempre é claro o que o Django pode realmente mudar lá. E é útil começar e ver se as operações esperadas por nós estão concluídas e se algo desnecessário e desnecessário chegou lá.

Quais comandos o SchemaEditor executa? Primeiro, uma nova coluna é adicionada a uma transação, o valor padrão é adicionado. Então, até que essa atualização retorne e atualize zero, os dados serão atualizados.

Então SET NOT NULL é definido na coluna e o valor padrão será excluído, repetindo o comportamento do Django, que armazena o valor padrão não no banco de dados, mas no nível lógico do código.

Aqui, em geral, também há espaço para crescer. Por exemplo, você pode criar um índice auxiliar para encontrar rapidamente essas linhas com um valor NULL à medida que se aproxima da atualização de toda a tabela.



Você também pode corrigir o ID máximo para o tempo de atualização quando iniciamos a migração, para que, por ID, você possa encontrar rapidamente valores que ainda não foram atualizados.

Em geral, a biblioteca está em desenvolvimento, aceitamos solicitações de pool. Quem se importa - participe.

Vale a pena prestar atenção que, com o crescimento dos bancos de dados, as migrações têm uma propriedade inevitável para desacelerar. Você precisa acompanhar quais bloqueios a tabela possui, executar migrações SQL para ver quais operações são aplicadas. De nossa parte, no Yandex.Connect, usamos essa biblioteca onde seus recursos permitem. E onde eles não permitem, nós mesmos, por nossas próprias mãos, falsas migrações de Django, executamos nossas consultas SQL.

Assim, alcançamos alta disponibilidade de serviços de leitura e gravação. Temos cargas pesadas, milhares de RPS e tempo de inatividade em alguns minutos, para não mencionar mais tempo, é inaceitável. É necessário que as migrações ocorram despercebidas pelo usuário. E com essas cargas, não será possível acordar às quatro da manhã, rolar alguma coisa quando não houver carga e voltar para a cama - porque a carga funciona 24 horas por dia.

Vale a pena notar que mesmo operações rápidas no PostgreSQL ainda podem causar uma lentidão no serviço e erros devido à maneira como a fila de bloqueio funciona no PostgreSQL.

Imagine que uma operação seja iniciada que, mesmo por alguns milissegundos, exija acesso exclusivo. Um exemplo dessa operação é adicionar uma coluna sem um valor padrão. Imagine que, no momento de seu lançamento em outra transação, haja alguma outra operação longa - digamos, SELECT com agregação. Nesse caso, nossa operação fará uma fila para ela. Isso acontecerá porque o acesso a conflitos exclusivos com todos os outros tipos de bloqueios.

Enquanto nossa operação de adicionar uma coluna estiver aguardando um bloqueio, todos os outros farão fila para ele e não serão executados até que seja concluído. Ao mesmo tempo, a operação que está sendo executada - SELECT com agregação - pode não entrar em conflito com as outras, e se não fosse a criação da coluna, elas não teriam ficado na fila, mas teriam sido executadas em paralelo.

Essa situação pode criar grandes problemas no serviço. Portanto, antes de iniciar o ALTER TABLE ou qualquer outra operação que exija bloqueio exclusivo de acesso, é necessário procurar para que consultas longas não sejam direcionadas ao banco de dados no momento. Ou você pode simplesmente inserir um tempo limite de log muito pequeno. Então, se não fosse possível fechar rapidamente a trava, a operação cairia. Poderíamos simplesmente reiniciá-lo e não bloquear a tabela por um longo tempo, enquanto a operação aguardará a concessão de uma concessão para bloqueios. Isso é tudo, obrigado.

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


All Articles