Sintonização de desempenho em hiperescala (Citus)

APLICA-SE A: Base de Dados do Azure para PostgreSQL - Hiperescala (Citus)

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

Agrupamento de ligação do lado do cliente

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

Adicionar um pool de ligação do lado do cliente é uma maneira fácil de aumentar o desempenho da aplicação com alterações mínimas de código. Nas nossas medições, executar as declarações de inserção de uma única linha vai cerca de 24x mais rápido num grupo de servidores Hyperscale (Citus) com pooling ativado.

Para exemplos específicos da linguagem de adicionar pooling no código de aplicação, consulte o guia de pilhas de aplicações.

Nota

A Hiperescala (Citus) também fornece uma ligação do lado do servidor usando pgbouncer, mas serve principalmente para aumentar o limite de ligação ao cliente. O desempenho de uma aplicação individual beneficia mais do cliente, em vez de uma piscina do lado do servidor. (Embora ambas as formas de agrupamento possam ser utilizadas ao mesmo tempo sem danos.)

Escosagem distribuída consultas

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 alterarem o significado da consulta.

Em algumas cargas de trabalho, é fácil. Cargas de trabalho transacionais/operacionais como aplicações SaaS multi-arrendatários ou a Internet das Coisas distribuem tabelas por inquilino ou dispositivo. As consultas são procuradas por um inquilino ou identificação de dispositivo.

Por exemplo, no nosso tutorial multi-inquilino temos uma ads mesa distribuída por company_id. A maneira ingénua de atualizar um anúncio é destacar assim:

-- slow

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

Embora a consulta identifique uma linha e a atualize, a Hyperscale (Citus) não sabe, no momento do planeamento, que fragmentos a consulta irá atualizar. A Citus leva um ShareUpdateExclusiveLock em todos os fragmentos para ser seguro, o que bloqueia outras consultas que tentam atualizar a tabela.

Mesmo que o id seja 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 planejador de consultas Hyperscale (Citus) vê um filtro direto na coluna de distribuição e sabe exatamente qual o fragmento único a bloquear. Nos nossos testes, a adição de filtros para a coluna de distribuição aumentou o desempenho da atualização paralela em 100x.

Junções e CTEs

Vimos como as declarações de ATUALIZAÇÃO devem ser feitas pela coluna de distribuição para evitar bloqueios de fragmentos desnecessários. Outras consultas também beneficiam de escoamento, geralmente para evitar a sobrecarga da rede de dados desnecessariamente baralhados entre nós de trabalhadores.

-- 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 filtrando a coluna de distribuição, company_idna declaraçã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 juntar mesas distribuídas, tente incluir a coluna de distribuição nas condições de junção. No entanto, ao juntar-se entre uma tabela distribuída e uma tabela de referência não é necessário, porque o conteúdo da tabela de referência é replicado em todos os nós dos trabalhadores.

Se parecer inconveniente adicionar os filtros extra a todas as suas consultas, lembre-se que existem bibliotecas auxiliares para várias aplicações populares que facilitam as coisas. Aqui estão as instruções:

Registo de bases de dados eficiente

Registar todas as declarações sql a toda a hora 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 um funcionamento diário eficiente, pode desativar a sessão, exceto erros e consultas anormalmente longas:

definição valor reason
log_statement_stats OFF Evite perfis de despesas gerais
log_duration OFF Não preciso saber a duração das consultas normais.
log_statement NENHUMA Não faça perguntas sem uma razão mais específica
log_min_duration_statement Um valor mais longo do que o que acha que as consultas normais devem levar 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 referidas. Pode deixá-los tal como estão. No entanto, por vezes vimos os clientes alterarem as configurações para tornar a exploração de madeira agressiva, 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.

Saúde do sistema e fechaduras

Antes de mergulhar em ineficiências comuns de bloqueio, vamos ver como ver fechaduras e atividade em todo o cluster de base de dados. A visão citus_stat_activity dá uma visão detalhada.

A vista mostra, entre outras coisas, como as consultas são bloqueadas por "eventos de espera", incluindo fechaduras. Agrupamento por wait_event_type pinta um quadro de 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 NU QUER wait_event_type DIZER que a consulta não está à espera de nada.

Se vir bloqueios na saída da atividade de stat, pode ver as consultas bloqueadas específicas utilizando citus_lock_waits:

SELECT * FROM citus_lock_waits;

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

-[ 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ó as fechaduras acontecendo no momento, mas padrões históricos, você pode capturar fechaduras nos registos PostgreSQL. Para saber mais, consulte a definição do servidor log_lock_waits na documentação PostgreSQL. Outro grande recurso são sete dicas para lidar com fechaduras no Citus Data Blog.

Problemas e soluções comuns

Comandos DDL

Comandos DDL como truncate, drope create index todos pegam fechaduras de escrita, e bloqueiam escritos em toda a mesa. Minimizar estas operações reduz os problemas de bloqueio.

Dicas:

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

  • PostgreSQL suporta índices de construção simultaneamente, para evitar colocar um bloqueio de escrita na mesa.

  • Considere a definição lock_timeout numa sessão SQL antes de executar um comando DDL pesado. Com lock_timeout, PostgreSQL abortará o comando DDL se o comando esperar demasiado tempo para uma fechadura de escrita. Um comando DDL à espera de uma fechadura pode fazer com que as consultas posteriores façam fila atrás de si mesma.

Idle em ligações de transação

As transações idle (não comprometidas) 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 quaisquer consultas de longa duração no nó coordenador, pode executar um comando como este:

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;

PostgreSQL também oferece uma definição de idle_in_transaction_session_timeout para automatizar a rescisão da sessão de inatividade.

Impasses

Citus deteta impasses distribuídos e cancela as suas consultas, mas a situação é menos executante do que evitar impasses em primeiro lugar. Uma fonte comum de impasses vem da atualização do mesmo conjunto de linhas numa ordem diferente 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 o código SQL para transações cuidadosamente para atualizar linhas na mesma ordem. (A ordem de atualização é por vezes chamada de "hierarquia de bloqueio.")

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

I/O durante a ingestão

O estrangulamento de I/O é tipicamente menos um problema para a Hiperescala (Citus) do que para o pós-sQL de nó único por causa do fragmento. Os fragmentos são tabelas individualmente mais pequenas, com melhores índices e taxas de cache, produzindo um melhor desempenho.

No entanto, mesmo com Hyperscale (Citus), à medida que as tabelas e índices crescem, o disco I/O pode tornar-se um problema para a ingestão de dados. As coisas a ter em conta são um número crescente de entradas de IO wait_event_type que aparecem em citus_stat_activity:

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

Executar 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, particularmente a métrica do IOPS que está no limite.

Dicas:

  • Se os seus dados forem naturalmente encomendados, como numa série de tempo, utilize a partição da tabela PostgreSQL. Consulte este guia para aprender a dividir as mesas distribuídas em Hiperescala (Citus).

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

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

Resumo dos resultados

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

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

Passos seguintes