ClickHouseと彼の友人(4)パイプラインプロセッサとスケジューラ

元のソース:https://bohutang.me/2020/06/11/clickhouse-and-friends-processor/

この記事では、ClickHouseのコアテクノロジーであるプロセッサとDAGスケジューラについて説明します。

これらの概念はClickHouseの最初のものではありません。興味のある学生は、マテリアライズの時間データフローに注意を払うことができます。Hu兄弟はgolangでプロトタイプも作成しました。

ClickHouseの全体的な高性能を実現するのは、これらのモジュールの洗練された設計です。

パイプラインの問題

従来のデータベースシステムでは、クエリ処理フローは大まかに次のとおりです。

その中で、計画段階では、パイプラインアセンブリが追加されることがよくあります(トランスフォーマーはデータ処理を表します)。

すべてのトランスフォーマーはパイプラインに配置され、シリアル実行のためにエグゼキューターに渡されます。トランスフォーマーデータセットが実行されるたびに、ダウンストリームシンカーに至るまで処理および出力されます。このモデルの利点は単純であることがわかりますが、欠点はパフォーマンスが低く、CPUの並列処理を利用できないことです。これは通常、火山スタイルモデルと呼ばれ、OLTPの低遅延に十分であり、計算量の多いOLAPに適しています。それは十分とはほど遠いです、100%未満のCPUは犯罪です!

上記の例では、transformer1とtransformer2に交差がない場合、それらを並行して処理できます。

これには、さらにいくつかの精神的な問題が含まれます。

  1. トランスフォーマーの柔軟なオーケストレーションを実現するにはどうすればよいですか?

  2. トランス間のデータ同期を実現するにはどうすればよいですか?

  3. トランス間の並列スケジューリングを実装する方法は?

プロセッサー和DAYスケジューラー

1.トランスフォーマーオーケストレーション

ClickHouseは、一連の基本的なトランスモジュールを実装しています。次のようなsrc / Processors / Transformsを参照してください。

  • FilterTransform-WHERE条件付きフィルタリング

  • SortingTransform-ORDERBY排序

  • LimitByTransform-LIMITクロップ

実行するとき:

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

ClickHouseのQueryPipelineの場合、次のように配置および組み立てられます。

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)

このようにして、データフローの関係が実現されます。実際の水道管と同じように、ある変圧器のOutPortが別の変圧器のInPortに接続されます。インターフェイスには3つまたは複数のチャネルがあります。

3.トランスフォーマー実行スケジューリング

パイプラインが組み立てられたので、パイプライン内の水をどのように処理して圧力下で流すことができますか?

ClickHouseは一連の変換状態を定義し、プロセッサはこれらの状態に従ってスケジューリングを実装します。

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

ソースがデータを生成すると、そのステータスはPortFullに設定されます。つまり、他のトランスフォーマーのInPortへの流入を待機し、プロセッサーはPullDataのPrepare of FilterTransformer(NeedData)のスケジュールを開始し、ステータスはReadyに設定され、プロセッサーが作業をスケジュールするのを待ちます。このメソッドはデータフィルター処理を実行するため、誰もが状態に依存して、プロセッサーに認識させ、終了状態まで状態遷移をスケジュールして実行します。

ここで言及する価値があるのは、ExpandPipeline状態です。これは、トランスフォーマーの実装に応じて、トランスフォーマーをより多くのトランスフォーマーに分割して並列実行し、爆発的な効果を実現できます。

SELECT number + 1 FROM t1;

ClickHouseのプロセッサとスケジューラのメカニズムをより深く理解するために、元の生態学的な例を見てみましょう。

  1. 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のトランスフォーマーデータユニットはチャンクです。トランスフォーマーは、アップストリームのOutPortから流れるチャンクを処理してからダウンストリームのInPortに出力します。グラフ接続されたパイプラインは並列に動作し、CPUを可能な限りフルに動作させます。

SQLがASTに解析された後、ClickHouseはASTに基づいてクエリプランを構築し、次にQueryPlanに基づいてパイプラインを構築し、最後にプロセッサがスケジューリングと実行を担当します。現在、ClickHouseの新しいバージョンではデフォルトでQueryPipelineが有効になっており、このコードも常に繰り返されています。

テキスト内リンク

参考文献

全文は終わりました。

ClickHouseをお楽しみください:)

TeacherYeの「MySQLCoreOptimization」クラスがMySQL8.0にアップグレードされました。コードをスキャンして、MySQL8.0の練習の旅を始めてください。

おすすめ

転載: blog.csdn.net/n88Lpo/article/details/109172165