Logtailのマルチラインログ収集のパフォーマンスが7倍向上した秘密

> 本記事は[こちらのブログ](https://www.alibabacloud.com/blog/the-secret-of-performance-improvement-of-logtails-multi-line-log-collection-by-7-times_602043?spm=a2c65.11461433.0.0.4ee35355zOUPsx)を参考にしています。
> 翻訳にはアリババクラウドの[ModelStudio](https://www.alibabacloud.com/en/product/modelstudio)([Qwen](https://www.alibabacloud.com/en/solutions/generative-ai/qwen))を使用しております。

# Logtailのマルチラインログ収集性能改善の秘密

_By Yitao_

## 1. 背景

ログ分析の分野では、広く使用されているログ収集ツールであるLogtailは、パフォーマンスが向上すれば全体的な効率を大幅に高めることができます。最近、Logtailのパフォーマンステスト中に、著者は行頭で正規表現を使用してマルチラインログを処理する場合、収集性能が低下することに気付きました。この現象の原因は何でしょうか？次に、Logtailのマルチラインログ収集におけるパフォーマンス向上の秘密を探ってみましょう。

## 2. 分析

この現象を理解するには、まずLogtailがマルチラインログを処理する仕組みを理解する必要があります。Logtailのマルチラインログマージ機能は、特定のログ形式に基づいて散在しているマルチラインデータを集約し、完全なイベントにします。以下のプロセスは、この機能がどのように動作するかを示しています。

ユーザーは行頭の正規表現を指定します。Logtailはその正規表現を使用して各ログ行の先頭を照合します。行が一致しない場合、Logtailは一致する行の先頭の正規表現を見つけるまで待ち続けます。例えば、以下のようなログ形式があると仮定します。一般的には、`cnt.*` を行頭の正規表現として指定します。Logtailはこの正規表現を使用して各行を照合し、これらの単一行ログを完全なマルチラインログにマージします。
cnt:13472391,thread:2,log:Exception in thread main java.lang.NullPointerExceptionat  
com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitle
    at com.example.myproject.Book.getTitle
    at com.example.myproject.Book.getTitle
    at com.example.myproject.Book.getTitle

Logtailは `boost::regex_match` 関数を使用して正規表現の照合を行います。この正規関数は、入力されたログバッファに対して入力された正規表現 `reg` に基づいて完全一致を行います。たとえば、前述のログにおいて、`cnt.*` は最初の行にあるすべての1072文字に一致します。cpp
bool BoostRegexMatch(const char* buffer, size_t size, const boost::regex& reg, string& exception) {
    // ...
    if (boost::regex_match(buffer, buffer + size, reg))
        return true;
    // ...
}

![1](https://yqintl.alicdn.com/d45f4992386dbc746e199d6ae09d72699f1e4f4f.png)

著者は以下のテストコードを作成し、行頭の正規表現に関係ないログ部分（`. *` によって一致される部分）の長さが増加すると、`boost::regex_match` の実行時間も線形に増加することを発見しました。cpp
static void BM_Regex_Match(int batchSize) {
    std::string buffer = "cnt:";
    std::string regStr = "cnt.*";
    boost::regex reg(regStr);
    std::ofstream outFile("BM_Regex_Match.txt", std::ios::trunc);
    outFile.close();

    for (int i = 0; i < 1000; i++) {
        std::ofstream outFile("BM_Regex_Match.txt", std::ios::app);
        buffer += 'a';

        int count = 0;
        uint64_t durationTime = 0;
        for (int i = 0; i < batchSize; i++) {
            count++;
            uint64_t startTime = GetCurrentTimeInMicroSeconds();
            if (!boost::regex_match(buffer, reg)) {
                std::cout << "error" << std::endl;
            }
            durationTime += GetCurrentTimeInMicroSeconds() - startTime;
        }
        outFile << i << "\t" << "durationTime: " << durationTime << std::endl;
        outFile << i << "\t" << "process: " << formatSize(buffer.size() * (uint64_t)count * 1000000 / durationTime)
                << std::endl;
        outFile.close();
    }
}

int main(int argc, char** argv) {
    logtail::Logger::Instance().InitGlobalLoggers();
    std::cout << "BM_Regex_Match" << std::endl;
    BM_Regex_Match(10000);
    return 0;
}

![2](https://yqintl.alicdn.com/6063c37c70c44db50500f6f7937ba43e3a5d2909.png)

この時、行頭の正規表現を使用する際、通常は単一行ログの先頭部分のみを一致させる必要があることに注意が必要です。例えば、`cnt` ログの場合、`. *` の部分全体を一致させる必要はなく、この部分を一致させることは不要なパフォーマンス消費につながります。この影響は特にログが非常に長い場合に顕著です。実際、Boostライブラリは `boost::regex_search` 関数を提供しており、適切なフラグ（例：`boost::match_continuous`）を設定することで、プレフィックスのみを一致させる要件を満たすことができます。これはまさに行頭の正規表現を一致させるために必要なものです。では、`boost::regex_search` の使い方を見てみましょう。cpp
bool BoostRegexSearch(const char* buffer, size_t size, const boost::regex& reg, string& exception) {
    // ...
    if (boost::regex_search(buffer, buffer + size, what, reg, boost::match_continuous)) {
        return true;
    }
    // ...
}

Logtailでは、行頭の正規表現の既存の実装により、ユーザーの正規表現には `. *` のサフィックスがあります。これを自動的に削除し、正規表現の先頭に `^` を追加することで、一致の効率を向上させることができます。

![3](https://yqintl.alicdn.com/273a56c2a90b20277d205bbb6a8ddc001220888c.png)

`boost::regex_match` と同様に、著者は `boost::regex_search` もログの長さに基づいてテストしました。結果として、`boost::regex_search` にかかる時間は比較的安定しており、ログの長さに応じて増加しないことがわかりました。

![4](https://yqintl.alicdn.com/2a3677686f2e0bb42bb4ab04117e9b4f7293591b.png)cpp
static void BM_Regex_Search(int batchSize) {
    std::string buffer = "cnt:";
    std::string regStr = "^cnt";
    boost::regex reg(regStr);
    std::ofstream outFile("BM_Regex_Search.txt", std::ios::trunc);
    outFile.close();

    for (int i = 0; i < 1000; i++) {
        std::ofstream outFile("BM_Regex_Search.txt", std::ios::app);
        buffer += 'a';

        int count = 0;
        uint64_t durationTime = 0;
        for (int i = 0; i < batchSize; i++) {
           count++;
           uint64_t startTime = GetCurrentTimeInMicroSeconds();
           if (!boost::regex_search(buffer, reg)) {
               std::cout << "error" << std::endl;
            }
            durationTime += GetCurrentTimeInMicroSeconds() - startTime;
        }

        outFile << i << "\t" << "durationTime: " << durationTime << std::endl;
        outFile << i << "\t" << "process: " << formatSize(buffer.size() * (uint64_t)count * 1000000 / durationTime)
               
# ログ収集性能の最適化

- 行頭に正規表現 `cnt.*` だけを設定した同じコレクション構成です。
- 実際にリアルタイムで同じログを生成します。このログの特徴は、最初の行の長さが比較的長いことです。
cnt:13472391,thread:2,log:Exception in thread main java.lang.NullPointerExceptionat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitle
    at com.example.myproject.Book.getTitle
    at com.example.myproject.Book.getTitle
at com.example.myproject.Book.getTitle

- 最初のコレクションのサイズは 10485760 で、起動パラメータを `max_fix_pos_bytes: 10485760` に変更して、最初の読み込み中にログが大量に書き込まれるのを防ぎます。
- 同時に 8 つのポッドを実行し、ログをローカルファイルに出力します。テストの結果、最適化された Logtail 2.1.1 バージョンのログ収集速度は 633 MB/s に達しました。一方、行頭の正規表現が最適化される前の Logtail 1.8.7 バージョンでは 90 MB/s 未満であり、7 倍の向上が見られました。

| メトリック                  | 元の値       | 複数行最適化後 |
|-----------------------------|--------------|----------------|
| iLogtail 収集速度 (MB/s)   | 90 MB/s      | 633 MB/s       |

![](https://yqintl.alicdn.com/a8c8dfda06407c4989c674266e6f79db1268c4ca.png)
![](https://yqintl.alicdn.com/ed4c0699ba054004e588dbee9d523c1b0b486059.png)

## 4. まとめ

ログの内容が非常に長い場合、行頭の正規表現に関連しない部分が長ければ長いほど、この最適化によってもたらされる性能向上が顕著になることが理論的に示されています。簡単な 1 行のコード修正により、Logtail の収集性能を大幅に向上させることができることが確認されました。