ClickHouse e seus amigos (4) Processador e agendador de pipeline

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:

  1. Como conseguir uma orquestração flexível de transformadores?

  2. Como conseguir a sincronização de dados entre transformadores?

  3. 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:

  1. Uma fonte: {0,1,2,3,4}

  2. AdderTransformer adiciona 1 a cada número

  3. 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

Leitura adicional

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

Acho que você gosta

Origin blog.csdn.net/n88Lpo/article/details/109172165
Recomendado
Clasificación