E se sua consulta ao banco de dados não estiver sendo executada com rapidez suficiente? Como você sabe se uma consulta usa os recursos de computação de maneira ideal ou pode ser acelerada? Na última conferência HighLoad ++ em Moscou, falei sobre a introspecção do desempenho de consultas - e o que o ClickHouse DBMS fornece e sobre os recursos do SO que todos deveriam conhecer.

Toda vez que faço uma solicitação, me preocupo não apenas com o resultado, mas também com o que essa solicitação faz. Por exemplo, funciona por um segundo. É muito ou pouco? Eu sempre penso: por que não meio segundo? Então eu otimizo alguma coisa, acelero e funciona por 10 ms. Eu geralmente estou satisfeito. Mas ainda assim, neste caso, tento fazer uma expressão facial descontente e pergunto: "Por que não 5 ms?" Como posso descobrir quanto tempo é gasto no processamento da solicitação? Pode ser acelerado em princípio?
Normalmente, a velocidade de processamento da solicitação é aritmética simples. Nós escrevemos o código - provavelmente da melhor maneira - e temos algum dispositivo no sistema. Os dispositivos têm especificações. Por exemplo, a velocidade de leitura do cache L1. Ou o número de leituras aleatórias que um SSD pode fazer. Todos nós sabemos disso. Precisamos pegar essas características, adicionar, subtrair, multiplicar, dividir e verificar a resposta. Mas este é o caso ideal, isso quase nunca acontece. Quase. De fato, isso às vezes acontece no ClickHouse.
Considere os fatos triviais sobre quais dispositivos e quais recursos existem em nossos servidores.

Processador, memória, disco, rede. Organizei esses recursos especialmente dessa maneira, começando pelo mais simples e conveniente para revisão e otimização e terminando no mais inconveniente e complexo. Por exemplo, eu executo uma solicitação e vejo que meu programa parece estar na CPU. O que isso significa? O que vou descobrir que existe algum tipo de loop interno, uma função que é executada com mais freqüência, reescreve o código, recompila e uma vez - meu programa roda mais rápido.
Se você gasta muita RAM, tudo fica um pouco mais complicado. Você precisa repensar a estrutura de dados, espremer alguns bits. De qualquer forma, reinicio meu programa e ele gasta menos RAM. É verdade que isso costuma prejudicar o processador.
Se tudo depende de discos, isso também é mais difícil, porque posso alterar a estrutura de dados no disco, mas preciso convertê-los posteriormente. Se eu fizer um novo lançamento, as pessoas terão que fazer algum tipo de migração de dados. Acontece que o disco já é muito mais complicado, e é melhor pensar com antecedência.
E a rede ... Eu realmente não gosto da rede, porque muitas vezes não está claro o que está acontecendo nela, especialmente se é uma rede entre continentes, entre data centers. Algo está diminuindo a velocidade e não é nem sua rede, nem seu servidor, e você não pode fazer nada. A única coisa que você pode pensar antecipadamente é como os dados serão transmitidos e como minimizar a interação na rede.
Acontece que nem um único recurso do sistema é utilizado, e o programa está apenas esperando por algo. De fato, esse é um caso muito frequente, porque nosso sistema é distribuído, e pode haver muitos processos e fluxos diferentes, e alguém está esperando pelo outro, e tudo isso precisa estar de alguma forma conectado entre si para que seja considerado adequadamente.

O mais simples é observar a utilização de recursos, com algum valor numérico. Por exemplo, você inicia alguns top e ele escreve: o processador é 100%. Ou execute o iostat e ele escreve: os discos são 100%. É verdade que isso geralmente não é suficiente. Uma pessoa verá que o programa se baseia em discos. O que pode ser feito? Você pode simplesmente notar isso e descansar, decidir que tudo, nada pode ser otimizado. Mas, de fato, cada um dos dispositivos dentro de si é bastante complicado. O processador possui vários dispositivos de computação para diferentes tipos de operações. Os discos podem ter uma matriz RAID. Se houver um SSD, haverá dentro de seu próprio processador, seu próprio controlador, o que torna claro o que. E um valor - 50% ou 100% - não é suficiente. A regra básica: se você perceber que algum recurso é 100% utilizado, não desista. Muitas vezes, você ainda pode melhorar alguma coisa. Mas isso acontece e vice-versa. Digamos que você veja que a reciclagem é de 50%, mas nada pode ser feito.
Vamos dar uma olhada mais de perto nisso.

O recurso mais fácil e conveniente é o processador. Você olha para cima, diz que o processador é 100%. Mas deve-se ter em mente que este não é um processador 100%. O programa superior não sabe o que o processador faz lá. Ela olha da perspectiva do planejador do sistema operacional. Ou seja, agora algum tipo de thread de programa está sendo executado no processador. O processador faz alguma coisa e, em seguida, 100% será mostrado se for calculada a média ao longo do tempo. Ao mesmo tempo, o processador está fazendo algo, e não está claro quão eficaz é. Pode executar um número diferente de instruções por ciclo. Se houver poucas instruções, o próprio processador pode esperar por algo, por exemplo, carregando dados da memória. Ao mesmo tempo, a mesma coisa será exibida no topo - 100%. Estamos aguardando o processador seguir nossas instruções. E o que ele faz por dentro não é claro.
Finalmente, há apenas um rake quando você pensa que seu programa repousa no processador. Isso é verdade, mas por algum motivo o processador tem uma frequência mais baixa. Pode haver muitas razões: superaquecimento, limitação de energia. Por alguma razão, no data center, há uma limitação de energia ou a economia de energia pode simplesmente ser ativada. Em seguida, o processador mudará constantemente de uma frequência mais alta para uma mais baixa, mas se sua carga for instável, isso não será suficiente e, em média, o código será executado mais lentamente. Veja o turbostato para a freqüência atual do processador. Verifique se há superaquecimento em dmesg. Se algo assim acontecesse, diria: “Superaquecimento. Frequência reduzida. ”
Se você estiver interessado em quantas falhas de cache estavam dentro, quantas instruções são executadas por ciclo, use o registro perf. Registre algumas amostras do programa. Além disso, será possível analisá-lo usando perf stat ou perf report.

E vice-versa. Digamos que você olhe para o topo e o processador seja menos de 50% reciclado. Suponha que você tenha 32 núcleos de processador virtual em seu sistema e 16 núcleos físicos.Em processadores Intel, isso ocorre porque o hiperencadeamento é duplo. Mas isso não significa que núcleos adicionais sejam inúteis. Tudo depende da carga. Suponha que você tenha algumas operações de álgebra linear bem otimizadas ou tenha hashes para minerar bitcoins. Então o código ficará claro, muitas instruções serão executadas por ciclo, não haverá falhas no cache, previsões erradas de ramificação também. E o hyper-threading não ajuda. Ajuda quando você tem um núcleo aguardando algo, enquanto o outro pode executar simultaneamente instruções de outro encadeamento.
ClickHouse tem ambas as situações. Por exemplo, quando fizermos agregação de dados (GROUP BY) ou filtragem por conjunto (subconsulta IN), teremos uma tabela de hash. Se a tabela de hash não couber no cache do processador, ocorrerão falhas no cache. Isso dificilmente pode ser evitado. Nesse caso, o hiperencadeamento nos ajudará.
Por padrão, o ClickHouse usa apenas núcleos físicos do processador, excluindo o hyperthreading. Se você souber que sua solicitação pode se beneficiar da hiperencadeamento, basta dobrar o número de threads: SET max threads = 32, e sua solicitação será mais rápida.
Acontece que o processador está perfeitamente usado, mas você olha para o gráfico e vê, por exemplo, 10%. E sua agenda, por exemplo, é de cinco minutos no pior caso. Mesmo se for um segundo, ainda há algum tipo de valor médio. De fato, você sempre teve solicitações, elas são executadas rapidamente, em 100 ms a cada segundo, e isso é normal. Porque o ClickHouse tenta executar a solicitação o mais rápido possível. Ele não tenta usar e superaquecer completamente e constantemente seus processadores.

Vamos dar uma olhada, uma opção um pouco complicada. Há uma consulta com uma expressão na subconsulta. Dentro da subconsulta, temos 100 milhões de números aleatórios. E nós apenas filtramos esse resultado.
Vemos uma foto assim. A propósito, quem dirá com que ferramenta posso ver esta foto maravilhosa? Absolutamente verdade - perf. Estou muito feliz que você saiba disso.
Abri o perf, pensando que agora entendo tudo. Eu abro a listagem do assembler. Lá, escrevi com que frequência a execução do programa estava em uma instrução específica, ou seja, com que frequência havia um ponteiro de instrução. Aqui, os números estão em porcentagem, e está escrito que quase 90% das vezes a instrução% edx,% edx foi executada, ou seja, verificando quatro bytes como zero.
A questão é: por que um processador pode demorar tanto para simplesmente comparar quatro bytes com zero? (respostas da platéia ...) Não há resto da divisão. Há mudanças de bits, há uma instrução crc32q, mas como se o ponteiro da instrução nunca acontecesse nela. E a geração de números aleatórios não está nesta listagem. Havia uma função separada e é muito bem otimizada, não diminui a velocidade. Algo mais está desacelerando aqui. A execução do código para nesta instrução e gasta muito tempo. Loop ocioso? Não. Por que devo inserir loops vazios? Além disso, se eu inserisse o loop Idle, isso também seria visível em perf. Não há divisão por zero, há simplesmente uma comparação com zero.
O processador possui um pipeline, ele pode executar várias instruções em paralelo. E quando o ponteiro da instrução está em algum lugar, isso não significa que esteja executando esta instrução. Talvez ele esteja esperando por outras instruções.
Temos uma tabela de hash para verificar se algum número ocorre em algum conjunto. Para isso, fazemos uma pesquisa na memória. Quando fazemos uma pesquisa na memória, temos uma falta de cache, porque a tabela de hash contém 100 milhões de números, não é garantido que caiba em nenhum cache. Portanto, para executar a instrução de verificação zero, esses dados já devem estar carregados da memória. E esperamos até que eles sejam carregados.

Agora, o próximo recurso, um pouco mais complexo - drives. Às vezes, os SSDs também são chamados de unidades, embora isso não esteja totalmente correto. SSDs também serão incluídos neste exemplo.
Abrimos, por exemplo, o iostat, que mostra uma utilização de 100%.
Nas conferências, muitas vezes acontece que o orador sobe ao palco e diz com pathos: “Os bancos de dados sempre confinam no disco. Portanto, criamos um banco de dados na memória. Ela não vai desacelerar. " Se uma pessoa se aproxima de você e diz isso, você pode enviá-la com segurança. Haverá alguns problemas - você diz, eu resolvi. :)
Suponha que um programa repouse em discos, a utilização é 100. Mas isso, é claro, não significa que usamos os discos de maneira ideal.
Um exemplo típico é quando você tem muito acesso aleatório. Mesmo que o acesso seja seqüencial, você simplesmente lê o arquivo sequencialmente, mas ainda pode ser mais ou menos ideal.
Por exemplo, você tem uma matriz RAID, vários dispositivos - digamos, 8 discos. E você acabou de ler sequencialmente sem ler adiante, com um tamanho de buffer de 1 MB, e o tamanho da parte da sua faixa no RAID também é de 1 MB. Então, cada leitura que você terá em um dispositivo. Ou, se não estiver alinhado, de dois dispositivos. Meio megabyte vai para algum lugar, outro meio megabyte para algum lugar, e assim por diante - os discos serão usados alternadamente: um, depois outro e depois um terceiro.
Ele precisa ser lido com antecedência. Ou, se você tiver O_DIRECT, aumente o tamanho do buffer. Ou seja, a regra é: 8 discos, tamanho do pedaço 1 MB, defina o tamanho do buffer para pelo menos 8 MB. Mas isso funcionará da melhor maneira possível se a leitura estiver alinhada. E se não estiver alinhado, primeiro haverá peças extras e você precisará colocar mais, multiplicar por mais algumas.
Ou, por exemplo, você possui o RAID 10. Com que velocidade você pode ler o RAID 10 - por exemplo, 8 discos? Qual será a vantagem? Quatro vezes, porque há um espelho, ou oito vezes? Na verdade, depende de como o RAID é criado, com que arranjo de pedaços em faixas.
Se você usar o mdadm no Linux, poderá especificar o layout local e o local remoto, sendo quase melhor para gravação e distante para leitura.
Eu sempre recomendo usar o layout distante, porque quando você escreve no banco de dados analítico, geralmente não é tão crítico a tempo - mesmo que haja muito mais escrita do que leitura. Isso é feito por algum processo em segundo plano. Mas quando você lê, precisa concluí-lo o mais rápido possível. Portanto, é melhor otimizar o RAID para leitura, definindo o layout distante.
Por sorte, no Linux, o mdadm configurará você para o layout próximo por padrão, e você obterá apenas metade do desempenho. Existem muitos desses ancinhos.
Outro rake terrível é o RAID 5 ou o RAID 6. Tudo se adapta bem às leituras e gravações seqüenciais. No RAID 5, a multiplicidade é "o número de dispositivos menos um". Isso aumenta bem mesmo com leituras aleatórias, mas não aumenta com leituras aleatórias. Faça um registro em qualquer lugar e você precisará ler os dados de todos os outros discos, absorvê-los (XOR - aprox. Ed.) E gravar em outro local. Para isso, um certo cache de tiras é usado, um péssimo rake. No Linux, é por padrão que você cria o RAID 5 e fica mais lento para você. E você pensará que o RAID 5 sempre fica mais lento, porque isso é compreensível. Mas, de fato, o motivo é a configuração errada.
Outro exemplo Você está lendo um SSD e comprou um bom SSD, que diz 300 mil leituras aleatórias por segundo na especificação. E por algum motivo você não pode fazer isso. E você pensa - sim, todos eles estão em suas especificações, não existe. Mas todas essas leituras devem ser feitas em paralelo, com o máximo grau de paralelismo. A única maneira de fazer isso de maneira ideal é usar E / S assíncrona, que é implementada usando o sistema chama io_submit, io_getevents, io_setup, etc.
A propósito, os dados no disco, se você os armazenar, você sempre precisará compactar. Vou dar um exemplo da prática. Uma pessoa entrou em contato conosco no
chat de suporte do ClickHouse e disse:
- ClickHouse compacta os dados. Eu vejo que repousa sobre o processador. Eu tenho SSDs NVMe muito rápidos, eles têm uma velocidade de leitura de vários gigabytes por segundo. É possível, de alguma forma, desativar a compactação no ClickHouse?
"Não, de jeito nenhum", eu digo. - Você precisa manter os dados compactados.
- Vamos parar, vai haver outro algoritmo de compressão que não faz nada.
Fácil. Digite essas letras nesta linha de código.
"De fato, tudo é muito simples", ele respondeu um dia depois. Eu fiz.
- Quanto o desempenho mudou?
"Falha no teste", ele escreveu outro dia depois. - Há muitos dados. Eles não se encaixam mais nos SSDs.
Vamos agora ver como pode ser a leitura do disco. Começamos o dstat, ele mostra a velocidade de leitura.
O primeiro exemplo de dstat e iostat Aqui está a coluna de leitura - 300 MB / s. Lemos a partir de discos. É muito ou pouco - eu não sei.
Agora eu corro o iostat para verificar isso. Aqui você pode ver a discriminação por dispositivo. Eu tenho RAID, MD2 e oito discos rígidos. Cada um deles mostra reciclagem, nem chega a 100% (50-60%). Mas o mais importante é que eu li de cada disco apenas a uma velocidade de 20 a 30 MB / s. E desde a infância me lembrei da regra de que você pode ler algo em torno de 100 MB / s no disco rígido. Por alguma razão, isso ainda não mudou muito.
Segundo exemplo de dstat e iostat Aqui está outro exemplo. A leitura é mais ideal. Eu executo o dstat e tenho uma velocidade de leitura de 1 GB / s neste RAID 5 de oito unidades. O que mostra o iostat? Sim, quase 1 GB / s.
Agora as unidades estão finalmente 100% carregadas. É verdade que, por algum motivo, dois são 100% e o restante é 95%. Provavelmente, eles ainda são um pouco diferentes. Mas com cada um deles eu li 150 MB / s, ainda mais legal do que pode ser. Qual a diferença? No primeiro caso, li com tamanho insuficiente do buffer em pedaços insuficientes. É simples, digo-lhe verdades comuns.
A propósito, se você acha que os dados ainda não precisam ser compactados para o banco de dados analítico, ou seja, um relatório da conferência HighLoad ++ Siberia (
habrastaty com base no relatório -
aprox .). Os organizadores decidiram fazer os relatórios mais graves em Novosibirsk.

O próximo exemplo é a memória. Verdades comuns contínuas. Primeiro, no Linux, nunca veja o que os programas gratuitos. Para quem está assistindo, eles criaram especialmente o site linuxatemyram.com. Entre, haverá uma explicação. Você também não precisa examinar a quantidade de memória virtual, porque qual é a diferença, quanto espaço de endereço o programa alocou? Veja quanta memória física é usada.
E mais um ancinho com o qual ainda não está claro como lutar. Lembre-se: o fato de que os alocadores geralmente não gostam de dar memória ao sistema é normal. Eles criaram o mmap, mas o munmap não faz mais. A memória não retornará ao sistema. O programa pensa - eu sei melhor como vou usar a memória. Vou deixar para mim mesma. Porque as chamadas de sistema mmap e munmap são bem lentas. Alterando o espaço de endereço, redefinindo os caches TLB do processador - é melhor não fazer isso. No entanto, o sistema operacional ainda tem a capacidade de liberar memória corretamente usando a chamada do sistema madvise. O espaço de endereço permanecerá, mas fisicamente a memória pode ser descarregada.
E nunca habilite a troca em servidores de produção com bancos de dados. Você pensa - não há memória suficiente, incluirei troca. Depois disso, a solicitação irá parar de funcionar. Vai quebrar um tempo sem fim.

Com uma rede muito típico ancinho. Se você criar uma conexão TCP a cada vez, levará algum tempo até que o tamanho correto da janela seja selecionado, pois o protocolo TCP não sabe com que rapidez será necessário transmitir dados. Ele se adapta a isso.
Ou imagine: você está transferindo um arquivo e possui uma grande latência na sua rede e uma perda decente de pacotes. Então não é óbvio se é certo usar o TCP para transferir arquivos. Eu acho que está errado, já que o TCP garante consistência. Por outro lado, você pode transferir metade do arquivo e a outra ao mesmo tempo. TCP- TCP . , , , TCP . .
100- , . 10 -, , , . . .

? — . , , , 10 . , .

: « - » — . iotop, , , iops.
, . .
top -, , clickHouse-server - , - . , , Shift+H, . , ClickHouse . ParalInputsProc, . BackgrProcPool — merges . , .
? ClickHouse, , . BackgroundProcessingPool. 15 . 16 1, 1 — . 16? , Linux — , : «16 . ». :)
clickhouse-benchmark. clickhouse-client. , clickhouse-client, . - . .
: clickhouse-benchmark + perf top . clickhouse-benchmark, , , , , . peft top. peft top, . , - -, uniq: UniquesHashSet. . , . , .
, , . — -. , , XOR - . -. - -. , -.
, , crc32q. , , - , - .
, ClickHouse. , , . ClickHouse.

. , — , SHOW PROCESSLIST. . , SELECT * FROM system processes. : , , . ClickHouse top.
ClickHouse ? background-. Background- — merges. , merges , SELECT * FROM system.merges.
, . -. . — ClickHouse. . , , . , . - traf_testing. ? , , . ClickHouse .

. , . , , , , . query_log. — , - , SELECT , - . query_log , . - . — , . : .
, , — merge, inserts, . part_log. , .

query_log clickhouse-benchmark. select , , stdin clickhouse-benchmark.
query_log - , .

, , . . SET send_logs_level = 'trace', , .
, . , 98%. , . É muito simples SET send_logs_level = 'trace', , . - : merging aggregated data, . 1% . , .
, , query_log.
. SELECT * FROM system.query_log . . , , , query_log. . — , , , . .

ClickHouse . — system.events, system.metrics system.asynchronous_metrics. Events — , , . 100 . — 10 . system.metrics — . , 10 , 10 .
system.asynchronous_metrics , . . — . , system.asynchronous_metrics — , - . , .
, . SHOW PROCESSLIST . query_log, .

, . , . , . , , . , Linux, . Linux . , . , . .
, OSReadChars OSReadBytes. ? , , , . , . , , , . , - , .
, . - . , 40 , 6,7 . . , ,
. , , .
, 1,3 , 5 . Porque , — page cache. ?
. . , , . . : 3,2 , — 2,5 . , , , . Porque -, : read ahead. , — ? -, — 4 , , 512 KB. . , . , - read ahead.

. . , . , , ReadBytes — , . 3 , 3 . , , .
— IOWait. 87 . 7 , IOWait — 87. ? — . . , , 87 . , - .
— CPUWait. , , , . - — , . CPU. CPU. - , . — , , user space. , - . Bem, tudo bem.
— , Linux. - , . , , .
E agora a coisa mais avançada que temos: query_thread_log. Com ele, você pode entender em que cada thread da execução da consulta perdeu tempo.Eu procuro minha solicitação, selecione por query_id e indico a métrica "A quantidade de tempo do processador gasto no espaço do usuário". Aqui estão os nossos fluxos. Para processamento paralelo da solicitação, 16 threads foram alocados. Cada um deles gastou 800 ms. E, em seguida, outros 16 encadeamentos foram alocados para a mesclagem do estado das funções agregadas; 0,25 s foram gastos em cada uma delas. Agora eu posso entender exatamente o que cada solicitação levou tempo.Relatório de vídeo no HighLoad ++: