2 行のコードで CPU を 30% 節約した方法

Didi テクノロジーを「スター ⭐️」に設定します

記事の更新情報をできるだけ早く受け取る

ClickHouse は、リアルタイム データ分析用のオープン ソースの高性能列型分散データベースです。ベクトル化されたコンピューティング エンジン、マルチコア並列コンピューティング、および高圧縮率をサポートしています。分析データベースの中で単一テーブルのクエリ パフォーマンスで第 1 位にランクされています。Didi は 2020 年に Clickhouse の導入を開始し、オンライン配車やログ検索などのコア ビジネスにサービスを提供しています。ノード数は 300 以上で、PB レベルのデータが毎日書き込まれ、毎日数千万件のクエリが行われます。最大のクラスターには 200 以上のノードがあります。この記事では、クリックハウスのパフォーマンス最適化における問題点の発見から最終的な解決策、より良い効果を得るまでのポイントを中心に紹介します。

01

問題が見つかりました

オンラインノードの負荷は比較的高いため、CPU を主に使用する場所を配置する必要があります。最初に確認する必要があるのは、どのモジュールが CPU を占有しているかということです。Clickhouse では、CPU を大量に使用するモジュールは主にクエリ、書き込み、マージです。以下の図に示すように、top コマンドを使用して、CPU 使用率が最も高いプロセスを見つけます。プロセスを見つけた後、top -Hp pid コマンドを使用して、CPU 使用率が最も高いスレッドを表示します。

60fcd3e559fba4523cdd54badec95051.png

1. 最初にランク付けされているのは BackgrProcPool スレッドです。このスレッドは、ReplicatedMergeTree テーブルのマージおよび変更タスクの実行を担当し、大量のデータを処理する必要があります。

2. 2 番目にランク付けされているのは、クエリ分析、最適化、実行プランの生成など、クライアントの http リクエストの処理を担当する HTTPHandler スレッドです。最終的に生成された物理的な実行プランは、QueryPipelineEx スレッドによって実行されます。

3. 下を見ると、6 つの連続した BackgrProcPool スレッドが CPU の 30% 以上を占有していることがわかります。これらは主にディスク間のデータ移動を担当します。ディスク使用量が設定されたしきい値 (デフォルトでは 90%) を超えると、BgMoveProcPool スレッドがディスク上のパーツ ファイルを別のディスクに移動します。同時に、テーブルに移動 TTL が設定されている場合、パーツのデータの有効期限が切れたときに、パーツはターゲット ディスクに移動されます。主に実現するために使用されます。データの冷却と加熱は別個に行われます。BgMoveProc スレッド プール内のスレッドのデフォルトの最大数は 8 で、すべての MergeTree テーブル ディスク間のデータの移動を担当します。

4. 図の残りの ZookeeperSend スレッドと ZookeeperRecv スレッドは、それぞれ ZK への操作リクエストの送信と、対応する操作の応答の受信を担当し、ReplicatedMergeTree テーブルのコピー同期メカニズムは ZK に依存して実現されます。Clickhouse には他にも多くのスレッドがあるため、ここでは 1 つずつ紹介しません。

top コマンドは一定期間監視しており、これら 8 つの BgMoveProPool スレッドの CPU 使用率がほぼ常に上位にあることがわかりました。一部のディスクの使用率が 90% に達し、すべての Move スレッドが使用されている可能性があります。ディスク間でデータを再配置していますか? ただし、オンラインディスクの使用率が 80% になるとアラームが鳴りますが、アラームに問題はありませんか?

df -h コマンドを使用してディスク使用量を確認します。実行後、12 個のディスクの使用率が約 50% であることがわかり、これは非常に奇妙なことです。ディスク容量は十分であり、オンライン クラスターはホット構成されていません。コールド分離。BgMoveProcPool スレッドが CPU を占有すべきではないのは当然ですが、何をしているのでしょうか?

02

質問を確認する

BgMoveProcPool スレッドが実行している内容を確認するには、この時点で pstack pid コマンドを使用してスタックを取得し、スタックを複数回出力して、BgMoveProcPool スレッドが MergeTreePartsMover::selectPartsForMove メソッド内にあり、スタックが次のようになっていることを確認します。以下に続きます:

#0  0x00000000100111a4 in DB::MergeTreePartsMover::selectPartsForMove(std::__1::vector<DB::MergeTreeMoveEntry, std::__1::allocator<DB::MergeTreeMoveEntry> >&, std::__1::function<bool (std::__1::shared_ptr<DB::IMergeTreeDataPart const> const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >*)> const&, std::__1::lock_guard<std::__1::mutex> const&) ()
#1  0x000000000ff6ef5a in DB::MergeTreeData::selectPartsForMove() ()
#2  0x000000000ff86096 in DB::MergeTreeData::selectPartsAndMove() ()
#3  0x000000000fe5d102 in std::__1::__function::__func<DB::StorageReplicatedMergeTree::startBackgroundMovesIfNeeded()::{lambda()#1}, std::__1::allocator<{lambda()#1}>, DB::BackgroundProcessingPoolTaskResult ()>::operator()() ()
#4  0x000000000ff269df in DB::BackgroundProcessingPool::workLoopFunc() ()
#5  0x000000000ff272cf in _ZZN20ThreadFromGlobalPoolC4IZN2DB24BackgroundProcessingPoolC4EiRKNS2_12PoolSettingsEPKcS7_EUlvE_JEEEOT_DpOT0_ENKUlvE_clEv ()
#6  0x000000000930b8bd in ThreadPoolImpl<std::__1::thread>::worker(std::__1::__list_iterator<std::__1::thread, void*>) ()
#7  0x0000000009309f6f in void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void ThreadPoolImpl<std::__1::thread>::scheduleImpl<void>(std::__1::function<void ()>, int, std::__1::optional<unsigned long>)::{lambda()#3}> >(std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void ThreadPoolImpl<std::__1::thread>::scheduleImpl<void>(std::__1::function<void ()>, int, std::__1::optional<unsigned long>)::{lambda()#3}>) ()
#8  0x00007ff91f4d5ea5 in start_thread () from /lib64/libpthread.so.0
#9  0x00007ff91edf2b0d in clone () from /lib64/libc.so.6

selectPartsForMove メソッドは BgMoveProcPool スレッドで何度もキャプチャされて実行されており、selectPartsForMove メソッドに時間がかかっていることがわかります メソッド名から、移動可能な Part を見つけてシステムに問い合わせるメソッドであることが分かります.part_log テーブルを使用して、MovePart レコードを表示します。

SELECT * FROM system.part_log WHERE event_time > now() - toIntervalDay(1) AND event_type = 'MovePart'

上記の SQL を実行して、MovePart の過去 1 日のレコードをクエリしますが、一致するものはありません。これまでのところ、BgMoveProcPool スレッドが移動可能なパーツをクエリしているとほぼ確信できますが、結果はすべて空であり、CPU は無効な計算を行っています。上記の分析に従って、問題のあるコードが特定されたため、次のステップでは、次のように selectPartsForMove のソース コードを調査します。

bool MergeTreePartsMover::selectPartsForMove(MergeTreeMovingParts & parts_to_move, const AllowedMovingPredicate & can_move, const std::lock_guard<std::mutex> & /* moving_parts_lock */) {
    std::unordered_map<DiskPtr, LargestPartsWithRequiredSize> need_to_move;
    ///  1. 遍历所有的disk,将使用率超过阀值的disk添加need_to_move中
    if (!volumes.empty()) {
        for (size_t i = 0; i != volumes.size() - 1; ++i) {
            for (const auto & disk : volumes[i]->getDisks()) {
                UInt64 required_maximum_available_space = disk->getTotalSpace() * policy->getMoveFactor(); /// move_factor默认0.9
                UInt64 unreserved_space = disk->getUnreservedSpace();
                if (unreserved_space < required_maximum_available_space)
                    need_to_move.emplace(disk, required_maximum_available_space - unreserved_space);
            }
        }
    }
    /// 2. 遍历所有的part,首先如果Part的MoveTTL已过期则添加到需要移动的集合parts_to_move中,否则为超过阈值的disk添加候选Part
    time_t time_of_move = time(nullptr);
    for (const auto & part : data_parts) {
        /// 检查该part能否被move, 
        if (!can_move(part, &reason))
            continue;




        /// 检查part的move_ttl
        auto ttl_entry = data->selectTTLEntryForTTLInfos(part->ttl_infos, time_of_move);
        auto to_insert = need_to_move.find(part->volume->getDisk());
        if (ttl_entry) { /// 已过期,则需要移动到目标磁盘
            auto destination = data->getDestinationForTTL(*ttl_entry);
            if (destination && !data->isPartInTTLDestination(*ttl_entry, *part))
                reservation = data->tryReserveSpace(part->getBytesOnDisk(), data->getDestinationForTTL(*ttl_entry));
        }
        if(reservation) /// 需要移动
            parts_to_move.emplace_back(part, std::move(reservation));
        else {  /// 候选Part
            if (to_insert != need_to_move.end())
                to_insert->second.add(part);
        }
    }
    /// 3. 为候选的Part申请空间并添加到需要移动的集合parts_to_move中
    for (auto && move : need_to_move) {
        for (auto && part : move.second.getAccumulatedParts()) {
            auto reservation = policy->reserve(part->getBytesOnDisk(), min_volume_index);
            if (!reservation)
                break;


            parts_to_move.emplace_back(part, std::move(reservation));
            ++parts_to_move_by_policy_rules;
            parts_to_move_total_size_bytes += part->getBytesOnDisk();
        }
    }

SelectPartsForMove メソッドは主に 3 つのことを行います。

  • まずすべてのディスクを調べ、使用量がしきい値を超えているディスクを need_to_move に追加します。

  • 次に、すべてのパーツを走査します。まず、パーツの MoveTTL が期限切れの場合は、移動する必要があるセットの Parts_to_move に追加します。それ以外の場合は、しきい値を超えたディスクの候補パーツを追加します。

  • 最後に、候補パーツ用のスペースを申請し、それを移動する必要のある Parts_to_move セットに追加します。

最も時間がかかるステップは 2 番目のステップで、テーブル内のパーツの数が増えるにつれて増加します。system.parts をクエリすると、合計で 300,000 を超えるパーツがあることがわかり、最大のテーブルには部品点数は60,000点以上、時間がかかるのも無理はありません。

問題はここで明らかで、BgMoveProcPool スレッドは 300,000 個を超えるパーツが移動条件を満たしているかどうかを常にチェックしていますが、そのたびに条件を満たすパーツがなく、無効な計算が行われていました。

03

問題を解く

オンラインノードのディスク容量は十分であり、データのコールド・ホット階層化が設定されていないため、各部の確認にCPUを浪費する必要がありません。

ディスク使用率が 90% に達したとき、取得した need_to_move が空で、ホット層とコールド層別が設定されていない、つまり move_ttl が空の両方の条件が満たされた場合、すべての部分をチェックせずに大幅なコストを節約できますか?計算は繰り返されるため、パーツを走査して確認する前に次の 2 行のコードを追加します。need_to_move が空で move_ttl が空の場合は、直接 false を返します。

if (need_to_move.empty() && !metadata_snapshot->hasAnyMoveTTL())
    return false;

04

実際の効果

国内のパブリッククラスタに公開し、topコマンドで各スレッドが消費するCPUを観察すると、最前線にあったBgMoveProcPoolスレッドがなくなり、8つのBgMoveProcPoolスレッドが占めるCPUが減少していることがわかります。約30%前から4%未満。

3e8419e6ca3013cd15dfbcd22c6413dd.png

マシン全体の CPU を見てみると、CPU がアップグレード前の約 20% から約 10% に低下しており、スパイクもそれほど高くないことがわかります。

1c1e87a68b1eecbc7ef9c4232b43dd88.png

この最適化はコミュニティに貢献し、マスターにマージされました。

05

フォローアップの考え

多くの場合、データ量が少なく同時実行性が低いときは、コードに問題はありませんが、データ量と同時実行性が増加すると、多くの問題が発生します。コードを記述するプロセスでは、コードのすべての行を尊重してください。プログラムをより堅牢にします。クリックハウスは今後もログ取得シナリオにおいて、安定・低コスト・高スループット・高性能なPBレベルのログ取得システムの構築に取り組んでまいります。 

おすすめ

転載: blog.csdn.net/DiDi_Tech/article/details/131566458