ClickHouse и его друзья (4) Конвейерный процессор и планировщик

Первоисточник: https://bohutang.me/2020/06/11/clickhouse-and-friends-processor/

В этой статье рассказывается об основных технологиях ClickHouse: процессор и DAG Scheduler.

Эти концепции не являются первыми в ClickHouse. Заинтересованные студенты могут следить за потоком данных о времени в Materialize. Брат Ху также написал прототип на голанге.

Основное внимание уделяется деталям реализации. Именно продуманный дизайн этих модулей обеспечивает общую высокую производительность ClickHouse.

Проблемы с трубопроводом

В традиционной системе баз данных поток обработки запроса примерно такой:

Среди них на этапе Plan часто добавляется конвейерная сборка (трансформатор представляет собой обработку данных):

Все трансформаторы объединяются в конвейер и затем передаются исполнителю для последовательного выполнения.Каждый раз, когда выполняется набор данных трансформатора, он будет обрабатываться и выводиться, вплоть до нижнего грузика. Можно видеть, что преимуществом этой модели является простота, но недостатком является то, что она имеет низкую производительность и не может использовать преимущества параллелизма ЦП. Ее обычно называют моделью в стиле вулкана , которая достаточна для низкой задержки OLTP и подходит для OLAP с интенсивными вычислениями. Этого далеко не достаточно, CPU менее 100% - преступление!

В приведенном выше примере, если transformer1 и transformer2 не пересекаются, их можно обрабатывать параллельно:

Это связано с некоторыми более душевными проблемами:

  1. Как добиться гибкой оркестровки трансформаторов?

  2. Как добиться синхронизации данных между трансформаторами?

  3. Как реализовать параллельное планирование между трансформаторами?

Процессор 和 DAY Scheduler

1. Трансформаторная оркестровка

ClickHouse реализует серию базовых модулей-преобразователей, см. Src / Processors / Transforms, таких как:

  • FilterTransform - ГДЕ условная фильтрация

  • SortingTransform - ЗАКАЗАТЬ 排序

  • LimitByTransform - LIMIT урожай

Когда мы выполняем:

SELECT * FROM t1 WHERE id=1 ORDER BY time DESC LIMIT 10

Для QueryPipeline ClickHouse он будет организован и собран следующим образом:

QueryPipeline::addSimpleTransform(Source)
QueryPipeline::addSimpleTransform(FilterTransform)
QueryPipeline::addSimpleTransform(SortingTransform)
QueryPipeline::addSimpleTransform(LimitByTransform)
QueryPipeline::addSimpleTransform(Sinker)

Этим достигается расположение Transformer, но как синхронизировать данные во время выполнения?

2. Синхронизация данных трансформатора.

Когда QueryPipeline выполняет оркестровку преобразователя, нам также необходимо создать соединение DAG нижнего уровня.

connect(Source.OutPort, FilterTransform.InPort)
connect(FilterTransform.OutPort, SortingTransform.InPort)
connect(SortingTransform.OutPort, LimitByTransform.InPort)
connect(LimitByTransform.OutPort, Sinker.InPort)

Таким образом, реализуется взаимосвязь потока данных. Выходной порт одного трансформатора соединен с входным портом другого. Так же, как водопроводная труба в нашей реальности, интерфейс имеет 3 или более каналов.

3. Планирование исполнения трансформатора.

Теперь, когда трубопровод собран, как можно обработать воду в трубопроводе и направить ее под давлением?

ClickHouse определяет набор состояний преобразования, и процессор реализует планирование в соответствии с этими состояниями.

    enum class Status
    {
        NeedData  // 等待数据流进入
        PortFull, // 管道流出端阻塞
        Finished, // 完成状态,退出
        Ready,    // 切换到 work 函数,进行逻辑处理
        Async,    // 切换到 schedule 函数,进行异步处理
        Wait,     // 等待异步处理
        ExpandPipeline,      // Pipeline 需要裂变
    };

Когда источник генерирует данные, его статус будет установлен на PortFull, что означает ожидание потока в InPort других преобразователей, процессор начнет планировать подготовку FilterTransformer (NeedData) для PullData, а затем его статус будет установлен на Готово, ожидая, пока процессор запланирует работу Этот метод выполняет фильтрацию данных, поэтому каждый зависит от состояния, чтобы процессор мог воспринимать, планировать и выполнять переходы между состояниями до состояния Finished.

Здесь стоит упомянуть состояние ExpandPipeline, которое, в соответствии с реализацией преобразователя, может разбить преобразователь на большее количество преобразователей для параллельного выполнения, достигая взрывного эффекта.

пример

SELECT number + 1 FROM t1;

Чтобы лучше понять механизм процессора и планировщика ClickHouse, давайте рассмотрим оригинальный экологический пример:

  1. Один источник: {0,1,2,3,4}

  2. AdderTransformer добавляет 1 к каждому числу

  3. Синкер, результат вывода

1. Источник

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();
    }
};

РАСПИСАНИЕ ДНЕЙ

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);
}

подводить итоги

С точки зрения разработчика все еще сложнее. Переход между состояниями по-прежнему должен контролироваться разработчиком. Тем не менее, восходящий поток проделал много базовой работы, такой как инкапсуляция ISource для источника, инкапсуляция ISink для приемника и базовая ISimpleTransform для разработки. Проще использовать процессор на верхнем уровне, и вы можете построить конвейер, который хотите, из строительных блоков.

Блок данных преобразователя ClickHouse - это Chunk. Преобразователь обрабатывает Chunk, который течет из восходящего OutPort, а затем выводит его в нисходящий InPort. Связанный с графом конвейер работает параллельно, чтобы ЦП работал максимально полно.

После синтаксического анализа SQL в AST ClickHouse создает план запроса на основе AST, затем создает конвейер на основе QueryPlan, и, наконец, процессор отвечает за планирование и выполнение. В настоящее время в новой версии ClickHouse по умолчанию включен QueryPipeline, и этот фрагмент кода также постоянно повторяется.

Текстовая ссылка

дальнейшее чтение

Полный текст окончен.

Наслаждайтесь ClickHouse :)

Класс учителя Йе «Оптимизация ядра MySQL» был обновлен до MySQL 8.0, отсканируйте код, чтобы начать путешествие по практике MySQL 8.0.

рекомендация

отblog.csdn.net/n88Lpo/article/details/109172165