Fonte original: https://bohutang.me/2020/06/11/clickhouse-and-friends-processor/
Este artigo fala sobre as principais tecnologias da ClickHouse: Processador e DAG Scheduler.
Esses conceitos não são os primeiros da ClickHouse. Os alunos interessados podem prestar atenção ao fluxo de dados de tempo de materializar. O irmão Hu também escreveu um protótipo em golang.
O foco está nos detalhes de implementação.É o design sofisticado desses módulos que fornece o alto desempenho geral do ClickHouse.
Problemas de pipeline
Em um sistema de banco de dados tradicional, um fluxo de processamento de consulta é aproximadamente:
Entre eles, no estágio de Plano, um conjunto de pipeline é frequentemente adicionado (um transformador representa um processamento de dados):
Todos os transformadores são organizados em um pipeline e então entregues ao executor para execução serial. Cada vez que um conjunto de dados do transformador é executado, ele é processado e enviado para o sinker downstream. Pode-se ver que a vantagem desse modelo é a simplicidade, mas a desvantagem é que ele tem baixo desempenho e não pode tirar proveito do paralelismo da CPU. Geralmente é chamado de modelo do estilo vulcão , que é suficiente para baixa latência de OLTP e é adequado para OLAP computacionalmente intensivo. Está longe de ser suficiente, CPU inferior a 100% é crime!
Para o exemplo acima, se o transformador1 e o transformador2 não tiverem interseção, eles podem ser processados em paralelo:
Isso envolve algumas questões mais profundas:
Como conseguir uma orquestração flexível de transformadores?
Como conseguir a sincronização de dados entre transformadores?
Como implementar o escalonamento paralelo entre transformadores?
Processador 和 DAY Scheduler
1. Orquestração do transformador
ClickHouse implementa uma série de módulos básicos de transformador, consulte src / Processors / Transforms, como:
FilterTransform - filtragem condicional WHERE
SortingTransform - ORDER BY 排序
LimitByTransform - LIMIT corte
Quando executamos:
SELECT * FROM t1 WHERE id=1 ORDER BY time DESC LIMIT 10
Para o QueryPipeline do ClickHouse, ele será organizado e montado da seguinte maneira:
QueryPipeline::addSimpleTransform(Source)
QueryPipeline::addSimpleTransform(FilterTransform)
QueryPipeline::addSimpleTransform(SortingTransform)
QueryPipeline::addSimpleTransform(LimitByTransform)
QueryPipeline::addSimpleTransform(Sinker)
Isso atinge o arranjo do Transformer, mas como sincronizar os dados durante a execução?
2. Sincronização de dados do transformador
Quando QueryPipeline realiza orquestração de transformador, também precisamos construir uma conexão DAG de nível inferior.
connect(Source.OutPort, FilterTransform.InPort)
connect(FilterTransform.OutPort, SortingTransform.InPort)
connect(SortingTransform.OutPort, LimitByTransform.InPort)
connect(LimitByTransform.OutPort, Sinker.InPort)
Desta forma, a relação do fluxo de dados é realizada, o OutPort de um transformador é conectado ao InPort de outro, assim como o duto de água em nossa realidade, a interface possui 3 ou mais canais.
3. Programação de execução do transformador
Agora que a tubulação está montada, como a água na tubulação pode ser tratada e fluída sob pressão?
ClickHouse define um conjunto de estados de transformação e o processador implementa o agendamento de acordo com esses estados.
enum class Status
{
NeedData // 等待数据流进入
PortFull, // 管道流出端阻塞
Finished, // 完成状态,退出
Ready, // 切换到 work 函数,进行逻辑处理
Async, // 切换到 schedule 函数,进行异步处理
Wait, // 等待异步处理
ExpandPipeline, // Pipeline 需要裂变
};
Quando a fonte gerar dados, seu estado será definido como PortFull, o que significa que aguardando o fluxo para o InPort de outros transformadores, o processador começará a programar o Prepare of FilterTransformer (NeedData) para PullData, e então seu estado será definido como Ready, aguardando o processador agendar o Trabalho O método realiza o processamento do Filtro de dados, então todos dependem do estado para permitir que o processador perceba, para agendar e realizar transições de estado até o estado Concluído.
O que vale mencionar aqui é o estado ExpandPipeline, que, de acordo com a implementação do transformador, pode dividir um transformador em mais transformadores para execução paralela, alcançando um efeito explosivo.
Exemplo
SELECT number + 1 FROM t1;
Para ter uma compreensão mais profunda do processador e do mecanismo do agendador da ClickHouse, vamos a um exemplo ecológico original:
Uma fonte: {0,1,2,3,4}
AdderTransformer adiciona 1 a cada número
Um Sinker, resultado de saída
1. Fonte
class MySource : public ISource
{
public:
String getName() const override { return "MySource"; }
MySource(UInt64 end_)
: ISource(Block({ColumnWithTypeAndName{ColumnUInt64::create(), std::make_shared<DataTypeUInt64>(), "number"}})), end(end_)
{
}
private:
UInt64 end;
bool done = false;
Chunk generate() override
{
if (done)
{
return Chunk();
}
MutableColumns columns;
columns.emplace_back(ColumnUInt64::create());
for (auto i = 0U; i < end; i++)
columns[0]->insert(i);
done = true;
return Chunk(std::move(columns), end);
}
};
2. MyAddTransform
class MyAddTransformer : public IProcessor
{
public:
String getName() const override { return "MyAddTransformer"; }
MyAddTransformer()
: IProcessor(
{Block({ColumnWithTypeAndName{ColumnUInt64::create(), std::make_shared<DataTypeUInt64>(), "number"}})},
{Block({ColumnWithTypeAndName{ColumnUInt64::create(), std::make_shared<DataTypeUInt64>(), "number"}})})
, input(inputs.front())
, output(outputs.front())
{
}
Status prepare() override
{
if (output.isFinished())
{
input.close();
return Status::Finished;
}
if (!output.canPush())
{
input.setNotNeeded();
return Status::PortFull;
}
if (has_process_data)
{
output.push(std::move(current_chunk));
has_process_data = false;
}
if (input.isFinished())
{
output.finish();
return Status::Finished;
}
if (!input.hasData())
{
input.setNeeded();
return Status::NeedData;
}
current_chunk = input.pull(false);
return Status::Ready;
}
void work() override
{
auto num_rows = current_chunk.getNumRows();
auto result_columns = current_chunk.cloneEmptyColumns();
auto columns = current_chunk.detachColumns();
for (auto i = 0U; i < num_rows; i++)
{
auto val = columns[0]->getUInt(i);
result_columns[0]->insert(val+1);
}
current_chunk.setColumns(std::move(result_columns), num_rows);
has_process_data = true;
}
InputPort & getInputPort() { return input; }
OutputPort & getOutputPort() { return output; }
protected:
bool has_input = false;
bool has_process_data = false;
Chunk current_chunk;
InputPort & input;
OutputPort & output;
};
3. MySink
class MySink : public ISink
{
public:
String getName() const override { return "MySinker"; }
MySink() : ISink(Block({ColumnWithTypeAndName{ColumnUInt64::create(), std::make_shared<DataTypeUInt64>(), "number"}})) { }
private:
WriteBufferFromFileDescriptor out{STDOUT_FILENO};
FormatSettings settings;
void consume(Chunk chunk) override
{
size_t rows = chunk.getNumRows();
size_t columns = chunk.getNumColumns();
for (size_t row_num = 0; row_num < rows; ++row_num)
{
writeString("prefix-", out);
for (size_t column_num = 0; column_num < columns; ++column_num)
{
if (column_num != 0)
writeChar('\t', out);
getPort()
.getHeader()
.getByPosition(column_num)
.type->serializeAsText(*chunk.getColumns()[column_num], row_num, out, settings);
}
writeChar('\n', out);
}
out.next();
}
};
AGENDADOR DIÁRIO
int main(int, char **)
{
auto source0 = std::make_shared<MySource>(5);
auto add0 = std::make_shared<MyAddTransformer>();
auto sinker0 = std::make_shared<MySink>();
/// Connect.
connect(source0->getPort(), add0->getInputPort());
connect(add0->getOutputPort(), sinker0->getPort());
std::vector<ProcessorPtr> processors = {source0, add0, sinker0};
PipelineExecutor executor(processors);
executor.execute(1);
}
Resumindo
Do ponto de vista do desenvolvedor, é ainda mais complicado. A transição de estado ainda precisa ser controlada pelo desenvolvedor. No entanto, o upstream fez muito trabalho básico, como encapsular ISource para origem, encapsular ISink para coletor e um ISimpleTransform básico para desenvolvimento. É mais fácil usar o processador no nível superior e você pode construir o pipeline que deseja em blocos de construção.
A unidade de dados do transformador do ClickHouse é o Chunk. O transformador processa o Chunk que flui do OutPort upstream e o envia para o InPort downstream. O pipeline conectado ao gráfico funciona em paralelo para fazer com que a CPU funcione o máximo possível.
Depois que um SQL é analisado em um AST, ClickHouse constrói um Plano de Consulta baseado no AST, então constrói um pipeline baseado no QueryPlan e, finalmente, o processador é responsável pelo agendamento e execução. No momento, a nova versão do ClickHouse habilitou QueryPipeline por padrão, e este trecho de código também está em constante iteração.
Link no texto
https://github.com/vectorengine/vectorsql/tree/master/src/processors
https://github.com/ClickHouse/ClickHouse/tree/master/src/Processors/Transforms
Leitura adicional
ClickHouse e seus amigos (3) Protocolo MySQL e pilha de chamadas Write
ClickHouse e seus amigos (2) Protocolo MySQL e pilha de chamadas de leitura
O texto completo acabou.
Aproveite o ClickHouse :)
A aula "MySQL Core Optimization" do professor Ye foi atualizada para o MySQL 8.0, leia o código para iniciar a jornada de prática do MySQL 8.0