Ajuste de desempenho no Azure Cosmos DB for PostgreSQL

APLICA-SE A: Azure Cosmos DB para PostgreSQL (com tecnologia da extensão da base de dados Citus para PostgreSQL)

Executar uma base de dados distribuída em todo o seu potencial proporciona um elevado desempenho. No entanto, alcançar esse desempenho pode fazer alguns ajustes no código da aplicação e na modelação de dados. Este artigo aborda algumas das técnicas mais comuns e eficazes para melhorar o desempenho.

Conjunto de ligações do lado do cliente

Um conjunto de ligações contém as ligações da base de dados abertas para reutilização. Quando necessário, uma aplicação solicita uma ligação do conjunto e o conjunto devolve uma que já está estabelecida, se possível, ou estabelece uma nova. Quando terminar, a aplicação liberta a ligação de volta ao conjunto em vez de a fechar.

Adicionar um conjunto de ligações do lado do cliente é uma forma fácil de aumentar o desempenho da aplicação com alterações mínimas ao código. Nas nossas medidas, executar instruções de inserção de linha única é cerca de 24x mais rápido num cluster com o agrupamento ativado.

Para obter exemplos da adição de conjuntos no código de aplicação específicos da linguagem, veja o guia de pilhas de aplicações.

Nota

O Azure Cosmos DB para PostgreSQL também fornece conjuntos de ligações do lado do servidor com o pgbouncer, mas serve principalmente para aumentar o limite de ligação do cliente. O desempenho de uma aplicação individual beneficia mais do conjunto do lado do cliente do que do lado do servidor. (Embora ambas as formas de agrupamento possam ser utilizadas ao mesmo tempo sem danos.)

Âmbito de consultas distribuídas

Atualizações

Ao atualizar uma tabela distribuída, tente filtrar consultas na coluna de distribuição, pelo menos quando faz sentido, quando os novos filtros não alteram o significado da consulta.

Em algumas cargas de trabalho, é fácil. Cargas de trabalho transacionais/operacionais, como aplicações SaaS multi-inquilino ou a Internet das Coisas, distribuem tabelas por inquilino ou dispositivo. As consultas estão no âmbito de um ID de inquilino ou dispositivo.

Por exemplo, no nosso tutorial multi-inquilino , temos uma ads tabela distribuída por company_id. A forma ingénua de atualizar um anúncio é afixá-lo desta forma:

-- slow

UPDATE ads
   SET impressions_count = impressions_count+1
 WHERE id = 42; -- missing filter on distribution column

Embora a consulta identifique exclusivamente uma linha e a atualize, o Azure Cosmos DB para PostgreSQL não sabe, no momento do planeamento, qual a partição horizontal que a consulta irá atualizar. A extensão Citus utiliza um ShareUpdateExclusiveLock em todas as partições horizontais para ser segura, o que bloqueia outras consultas que tentam atualizar a tabela.

Apesar de ter id sido suficiente para identificar uma linha, podemos incluir um filtro extra para tornar a consulta mais rápida:

-- fast

UPDATE ads
   SET impressions_count = impressions_count+1
 WHERE id = 42
   AND company_id = 1; -- the distribution column

O planeador de consultas do Azure Cosmos DB para PostgreSQL vê um filtro direto na coluna de distribuição e sabe exatamente qual a partição horizontal a bloquear. Nos nossos testes, adicionar filtros para a coluna de distribuição aumentou o desempenho da atualização paralela em 100x.

Associações e CTEs

Vimos como as instruções UPDATE devem ser confinadas pela coluna de distribuição para evitar bloqueios de partições horizontais desnecessários. Outras consultas também beneficiam do âmbito, normalmente para evitar a sobrecarga de rede de dados desnecessariamente baralhados entre nós de trabalho.

-- logically correct, but slow

WITH single_ad AS (
  SELECT *
    FROM ads
   WHERE id=1
)
SELECT *
  FROM single_ad s
  JOIN campaigns c ON (s.campaign_id=c.id);

Podemos acelerar a consulta ao filtrar na coluna de distribuição, company_id, na instrução CTE e SELECT principal.

-- faster, joining on distribution column

WITH single_ad AS (
  SELECT *
    FROM ads
   WHERE id=1 and company_id=1
)
SELECT *
  FROM single_ad s
  JOIN campaigns c ON (s.campaign_id=c.id)
 WHERE s.company_id=1 AND c.company_id = 1;

Em geral, ao associar tabelas distribuídas, tente incluir a coluna de distribuição nas condições de associação. No entanto, ao associar entre uma tabela distribuída e de referência, não é necessário, uma vez que os conteúdos da tabela de referência são replicados em todos os nós de trabalho.

Se parecer inconveniente adicionar os filtros adicionais a todas as suas consultas, tenha em atenção que existem bibliotecas auxiliares para várias arquiteturas de aplicações populares que facilitam a tarefa. Eis as instruções:

Registo de bases de dados eficiente

Registar sempre todas as instruções SQL adiciona sobrecarga. Nas nossas medições, a utilização de um nível de registo mais criterioso melhorou as transações por segundo em 10x vs. registo completo.

Para uma operação diária eficiente, pode desativar o registo, exceto erros e consultas de execução anormalmente longa:

definição valor reason
log_statement_stats OFF Evitar a criação de perfis gerais
log_duration OFF Não precisa de saber a duração das consultas normais
log_statement NENHUM Não registar consultas sem um motivo mais específico
log_min_duration_statement Um valor mais longo do que aquilo que pensa que as consultas normais devem demorar Mostra as consultas anormalmente longas

Nota

As definições relacionadas com o registo no nosso serviço gerido têm em conta as recomendações acima. Pode deixá-los tal como estão. No entanto, por vezes, vimos os clientes alterarem as definições para tornar o registo agressivo, o que levou a problemas de desempenho.

Contenção de bloqueio

A base de dados utiliza bloqueios para manter os dados consistentes no acesso simultâneo. No entanto, alguns padrões de consulta requerem uma quantidade excessiva de bloqueio e existem alternativas mais rápidas.

Estado de funcionamento do sistema e bloqueios

Antes de aprofundar as ineficiências comuns de bloqueio, vamos analisar como podemos ver os bloqueios e a atividade em todo o cluster da base de dados. A vista citus_stat_activity oferece uma visão detalhada.

A vista mostra, entre outras coisas, como as consultas são bloqueadas por "eventos de espera", incluindo bloqueios. Agrupar por wait_event_type pinta uma imagem da saúde do sistema:

-- general system health

SELECT wait_event_type, count(*)
  FROM citus_stat_activity
 WHERE state != 'idle'
 GROUP BY 1
 ORDER BY 2 DESC;

Um NULL wait_event_type significa que a consulta não está à espera de nada.

Se vir bloqueios na saída da atividade de estatística, pode ver as consultas bloqueadas específicas com citus_lock_waits:

SELECT * FROM citus_lock_waits;

Por exemplo, se uma consulta estiver bloqueada noutra a tentar atualizar a mesma linha, verá as instruções bloqueadas e de bloqueio apresentadas:

-[ RECORD 1 ]-------------------------+--------------------------------------
waiting_gpid                          | 10000011981
blocking_gpid                         | 10000011979
blocked_statement                     | UPDATE numbers SET j = 3 WHERE i = 1;
current_statement_in_blocking_process | UPDATE numbers SET j = 2 WHERE i = 1;
waiting_nodeid                        | 1
blocking_nodeid                       | 1

Para ver não só os bloqueios a acontecer neste momento, mas também padrões históricos, pode capturar bloqueios nos registos do PostgreSQL. Para saber mais, veja a definição do servidor log_lock_waits na documentação do PostgreSQL. Outro excelente recurso são sete sugestões para lidar com bloqueios no Blogue de Dados do Citus.

Problemas comuns e soluções

Comandos DDL

Os Comandos DDL como truncate, drope create index todos têm bloqueios de escrita e bloqueiam escritas em toda a tabela. Minimizar estas operações reduz os problemas de bloqueio.

Sugestões:

  • Tente consolidar o DDL em janelas de manutenção ou utilize-as com menos frequência.

  • O PostgreSQL suporta a compilação de índices em simultâneo para evitar colocar um bloqueio de escrita na tabela.

  • Considere definir lock_timeout numa sessão SQL antes de executar um comando DDL pesado. Com lock_timeouto , o PostgreSQL abortará o comando DDL se o comando aguardar demasiado tempo por um bloqueio de escrita. Um comando DDL à espera de um bloqueio pode fazer com que as consultas posteriores sejam colocadas em fila atrás de si.

Ligações inativas nas transações

As transações inativas (não consolidadas) por vezes bloqueiam outras consultas desnecessariamente. Por exemplo:

BEGIN;

UPDATE ... ;

-- Suppose the client waits now and doesn't COMMIT right away.
--
-- Other queries that want to update the same rows will be blocked.

COMMIT; -- finally!

Para limpar manualmente todas as consultas de inatividade longa no nó de coordenação, pode executar um comando semelhante ao seguinte:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'citus'
 AND pid <> pg_backend_pid()
 AND state in ('idle in transaction')
 AND state_change < current_timestamp - INTERVAL '15' MINUTE;

O PostgreSQL também oferece uma definição idle_in_transaction_session_timeout para automatizar o término da sessão de inatividade.

Impasses

O Azure Cosmos DB for PostgreSQL deteta impasses distribuídos e cancela essas consultas, mas a situação é menos eficiente do que evitar impasses. Uma origem comum dos impasses vem da atualização do mesmo conjunto de linhas numa ordem diferente a partir de várias transações ao mesmo tempo.

Por exemplo, executar estas transações em paralelo:

Sessão A:

BEGIN;
UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1;
UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1;

Sessão B:

BEGIN;
UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1;
UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1;

-- ERROR:  canceling the transaction since it was involved in a distributed deadlock

Sessão Um ID atualizado 1 e 2, enquanto a sessão B atualizou 2 e 1. Escreva código SQL para transações cuidadosamente para atualizar linhas pela mesma ordem. (Por vezes, a ordem de atualização é denominada "hierarquia de bloqueio".)

Na nossa medição, a atualização em massa de um conjunto de linhas com muitas transações foi 3x mais rápida ao evitar o impasse.

E/S durante a ingestão

Normalmente, o estrangulamento de E/S é menos problemático para o Azure Cosmos DB para PostgreSQL do que para o PostgreSQL de nó único devido à fragmentação. Os fragmentos são tabelas individualmente mais pequenas, com melhores índices e taxas de acertos na cache, o que produz um melhor desempenho.

No entanto, mesmo com o Azure Cosmos DB for PostgreSQL, à medida que as tabelas e os índices crescem, a E/S do disco pode tornar-se num problema para a ingestão de dados. Os aspetos a ter em conta são um número crescente de entradas de "E/S" wait_event_type apresentadas no citus_stat_activity:

SELECT wait_event_type, wait_event count(*)
  FROM citus_stat_activity
 WHERE state='active'
 GROUP BY 1,2;

Execute a consulta acima repetidamente para capturar informações relacionadas com o evento de espera. Note como as contagens de diferentes tipos de eventos de espera mudam.

Veja também as métricas no portal do Azure, em particular a métrica IOPS que está a atingir o limite máximo.

Sugestões:

  • Se os dados forem ordenados naturalmente, como numa série temporal, utilize a criação de partições de tabelas do PostgreSQL. Veja este guia para saber como criar partições de tabelas distribuídas.

  • Remova os índices não utilizados. A manutenção de índices causa a amplificação de E/S durante a ingestão. Para descobrir quais os índices que não são utilizados, utilize esta consulta.

  • Se possível, evite indexar dados aleatórios. Por exemplo, alguns algoritmos de geração de UUID não seguem nenhuma ordem. Indexar tal valor causa muito sobrecarga. Experimente antes uma sequência bigint ou aumente monotonicamente os UUIDs.

Resumo dos resultados

Em referências de ingestão simples com INSERTs, UPDATEs, blocos de transação, observámos as seguintes acelerações de consulta para as técnicas neste artigo.

Técnica Aceleração da consulta
Âmbito de consultas 100x
Conjunto de ligações 24x
Registo eficiente 10x
Evitar o impasse 3x

Passos seguintes