Goldfields Pipeline, por SeanMac (Wikimedia Commons)
Se você está tentando otimizar o desempenho do seu aplicativo baseado em PostgreSQL, provavelmente está focando nas ferramentas usuais:EXPLAIN (BUFFERS, ANALYZE) , pg_stat_statements , auto_explain , log_statement_min_duration , etc
Talvez você esteja analisando a contenção de bloqueio com log_lock_waits , monitorando o desempenho do seu ponto de verificação, etc.
Mas você pensou em latência de rede ? Os jogadores sabem sobre a latência da rede, mas você achou que isso importava para o seu servidor de aplicativos?
A latência é importante
As latências típicas de rede de ida e volta de cliente/servidor podem variar de 0,01 ms (localhost) até ~0,5 ms de uma rede comutada, 5 ms de WiFi, 20 ms de ADSL, 300 ms de roteamento intercontinental e ainda mais para coisas como links de satélite e WWAN .
Um SELECT trivial pode levar na ordem de 0,1 ms para executar no lado do servidor. Um INSERIR trivial pode levar 0,5 ms.
Toda vez que seu aplicativo executa uma consulta, ele precisa esperar que o servidor responda com sucesso/falha e possivelmente um conjunto de resultados, metadados de consulta, etc. Isso incorre em pelo menos um atraso de ida e volta na rede.
Quando você está trabalhando com consultas pequenas e simples, a latência de rede pode ser significativa em relação ao tempo de execução de suas consultas se seu banco de dados não estiver no mesmo host que seu aplicativo.
Muitos aplicativos, principalmente ORMs, são muito propensos a executar
Da mesma forma, se você estiver preenchendo o banco de dados a partir de um ORM, provavelmente estará fazendo centenas de milhares de INSERT triviais s… e esperando depois de cada um que o servidor confirme que está tudo bem.
É fácil tentar focar no tempo de execução da consulta e tentar otimizá-lo, mas não há muito o que fazer com um trivial INSERT INTO ...VALUES ... . Solte alguns índices e restrições, certifique-se de que seja agrupado em uma transação e pronto.
Que tal se livrar de todas as esperas da rede? Mesmo em uma LAN, eles começam a somar milhares de consultas.
COPIAR
Uma maneira de evitar a latência é usar COPY . Para usar o suporte COPY do PostgreSQL, seu aplicativo ou driver precisa produzir um conjunto de linhas semelhante ao CSV e transmiti-las ao servidor em uma sequência contínua. Ou o servidor pode ser solicitado a enviar ao seu aplicativo um fluxo semelhante ao CSV.
De qualquer forma, o aplicativo não pode intercalar uma COPY com outras consultas e as inserções de cópia devem ser carregadas diretamente em uma tabela de destino. Uma abordagem comum é COPIAR em uma tabela temporária, então faça um INSERT INTO ... SELECT ... , ATUALIZAR... DE .... , EXCLUIR DE... USANDO... , etc para usar os dados copiados para modificar as tabelas principais em uma única operação.
Isso é útil se você estiver escrevendo seu próprio SQL diretamente, mas muitos frameworks de aplicativos e ORMs não o suportam, além disso, ele só pode substituir diretamente o simples INSERT . Seu aplicativo, framework ou driver cliente precisa lidar com a conversão para a representação especial necessária para COPY , procure qualquer metadado de tipo necessário, etc.
(Drivers notáveis que fazem suporte COPIAR incluem libpq, PgJDBC, psycopg2 e a gem Pg… mas não necessariamente os frameworks e ORMs construídos sobre eles.)
PgJDBC – modo de lote
O driver JDBC do PostgreSQL tem uma solução para esse problema. Ele se baseia no suporte presente nos servidores PostgreSQL desde 8.4 e nos recursos de lote da API JDBC para enviar um lote de consultas ao servidor e aguarde apenas uma vez pela confirmação de que todo o lote foi executado corretamente.
Bem, em teoria. Na realidade, alguns desafios de implementação limitam isso para que os lotes só possam ser feitos em partes de algumas centenas de consultas, na melhor das hipóteses. O driver também só pode executar consultas que retornam linhas de resultados em blocos em lote se puder descobrir antecipadamente o tamanho dos resultados. Apesar dessas limitações, o uso de Statement.executeBatch() pode oferecer um grande aumento de desempenho para aplicativos que executam tarefas como carregamento de dados em massa de instâncias de banco de dados remotas.
Por ser uma API padrão, ela pode ser usada por aplicativos que funcionam em vários mecanismos de banco de dados. O Hibernate, por exemplo, pode usar o batching JDBC, embora não o faça por padrão.
libpq e lotes
A maioria (todos?) dos outros drivers PostgreSQL não tem suporte para batching. O PgJDBC implementa o protocolo PostgreSQL de forma totalmente independente, enquanto a maioria dos outros drivers usa internamente a biblioteca C libpq que é fornecido como parte do PostgreSQL.
libpq não suporta lotes. Ele tem uma API assíncrona sem bloqueio, mas o cliente ainda pode ter apenas uma consulta “em andamento” por vez. Ele deve esperar até que os resultados dessa consulta sejam recebidos antes de enviar outra.
O servidor do PostgreSQL suporta lotes muito bem, e o PgJDBC já o usa. Então eu escrevi suporte em lote para libpq e o submeteu como candidato para a próxima versão do PostgreSQL. Como ele altera apenas o cliente, se aceito, ainda acelerará as coisas ao se conectar a servidores mais antigos.
Eu estaria realmente interessado em comentários de autores e usuários avançados da libpq drivers de cliente baseados e desenvolvedores de libpq aplicativos baseados. O patch se aplica bem ao PostgreSQL 9.6beta1 se você quiser experimentá-lo. A documentação é detalhada e há um programa de exemplo abrangente.
Desempenho
Achei que um serviço de banco de dados hospedado como RDS ou Heroku Postgres seria um bom exemplo de onde esse tipo de funcionalidade seria útil. Em particular, acessá-los de nossas próprias redes realmente mostra o quanto a latência pode prejudicar.
Com latência de rede de ~320ms:
- 500 inserções sem lote:
167,0s - 500 inserções com lotes:
1,2s
… que é mais de 120x mais rápido.
Normalmente, você não executará seu aplicativo em um link intercontinental entre o servidor de aplicativos e o banco de dados, mas isso serve para destacar o impacto da latência. Mesmo em um soquete unix para localhost, vi uma melhoria de desempenho de mais de 50% para 10.000 inserções.
Lote em aplicativos existentes
Infelizmente, não é possível habilitar automaticamente o batching para aplicativos existentes. Os aplicativos precisam usar uma interface um pouco diferente, onde enviam uma série de consultas e só então solicitam os resultados.
Deve ser bastante simples adaptar aplicativos que já usam a interface assíncrona libpq, especialmente se eles usarem o modo sem bloqueio e um select() /enquete() /epoll() /WaitForMultipleObjectsEx ciclo. Aplicativos que usam a libpq síncrona interfaces exigirão mais mudanças.
Agrupando outros drivers de cliente
Da mesma forma, drivers de cliente, estruturas e ORMs geralmente precisarão de alterações internas e de interface para permitir o uso de lotes. Se eles já estiverem usando um loop de eventos e E/S sem bloqueio, eles devem ser bastante simples de modificar.
Eu adoraria ver usuários de Python, Ruby, etc. capazes de acessar essa funcionalidade, então estou curioso para ver quem está interessado. Imagine poder fazer isso:
import psycopg2 conn = psycopg2.connect(...) cur = conn.cursor() # this is just an idea, this code does not work with psycopg2: futures = [ cur.async_execute(sql) for sql in my_queries ] for future in futures: result = future.result # waits if result not ready yet ... process the result ... conn.commit()
A execução em lote assíncrona não precisa ser complicada no nível do cliente.
COPIAR é mais rápido
Onde os clientes práticos ainda devem favorecer COPIAR . Aqui estão alguns resultados do meu laptop:
inserting 1000000 rows batched, unbatched and with COPY batch insert elapsed: 23.715315s sequential insert elapsed: 36.150162s COPY elapsed: 1.743593s Done.
Agrupar o trabalho fornece um aumento de desempenho surpreendentemente grande, mesmo em uma conexão de soquete unix local…. mas COPIAR deixa ambas as abordagens individuais de inserção muito atrás de si na poeira.
Usar COPIAR .
A imagem
A imagem para este post é do gasoduto Goldfields Water Supply Scheme de Mundaring Weir perto de Perth na Austrália Ocidental para os campos de ouro do interior (deserto). É relevante porque demorou tanto para terminar e foi alvo de críticas tão intensas que seu designer e principal proponente, C. Y. O'Connor, cometeu suicídio 12 meses antes de ser colocado em funcionamento. Localmente, as pessoas costumam (incorretamente) dizer que ele morreu