DataKitログ収集システムの設計と実装
はじめに¶
ログ収集 (ロギング) は、Guance DataKit の重要な側面です。アクティブに収集または受動的に受信したログ データを処理し、最終的に Guance センターにアップロードします。ログ収集はデータソースに応じて「ネットワークフローデータ」と「ローカルディスクファイル」の2種類に分けられます。
ネットワークストリームデータ¶
基本的に、ネットワーク インターフェイスをサブスクライブすることで、ログ生成側から送信されたデータを受動的に受信します。
最も一般的な例は、Docker ログを表示することです。 docker logs -f CONTAIENR_NAME コマンドを実行すると、Docker は別プロセスを起動してメインプロセスに接続し、メインプロセスから送信されたデータを受信してターミナルに出力します。 Docker ログ プロセスとメイン プロセスは同じホスト上にありますが、それらの対話はローカル ループバック ネットワークを通じて行われます。
Kubernetes クラスターなどのより複雑なネットワーク ログ シナリオでは、ログはさまざまなノードに分散され、API サーバーによって転送する必要があります。これは Docker の単一アクセス トレースの 2 倍複雑です。
ただし、ネットワーク経由で取得されるほとんどのログには、ログの場所を特定できないという問題があります。ログ受信側は、最初からできるだけ多くのログを受信することしか選択できません (一度に数十万件になる場合もあります)。または、tail -f のように末尾から開始し、現在生成されている最新のデータのみを受け取ります。ログ受信側のプロセスを再起動すると、その間のログは失われます。
DataKit のコンテナ ログ収集は、当初はネットワーク受信を使用していました。上記の問題に長い間悩まされてきました。その後、後述する「ローカルディスクファイル」の採取方法に徐々に調整されていきました。
ローカルディスクファイル¶
ローカル ログ ファイルを収集するのが最も一般的で効率的な方法です。複雑な送信手順が不要になり、ディスク ファイルに直接アクセスできるため、操作性が向上します。ただし、実装はより複雑で、次のような一連の詳細な問題が発生します。
- ディスク上のデータをより効率的に読み取るにはどうすればよいですか?
- ファイルが削除またはローテーションされた場合はどうすればよいですか?
- ファイルを再度開くときに「続きを読む」最後の位置を見つけるにはどうすればよいですか?
これらの問題は、Docker ログ サービスの拡散に相当します。詳細と実行はすべて自分で行い、最後のネットワーク送信部分のみ省略されています。実装の複雑さは、単にネットワークを使用して受信するよりもはるかに面倒です。
この記事では主に「ローカルディスクファイル」に焦点を当て、下から上に「ファイルの検出」、「データの収集と処理」、「送信と同期」の3つの側面に分けて、DataKitログ収集システムの設計と実装の詳細を順番に紹介します。
さらに、DataKit ログ収集の実行フローは次のとおりであり、上記の「3 つの側面」をカバーおよび細分化しています。
glob 検出ファイル Docker API 検出ファイル Containerd (CRI) 検出ファイル
| | |
`-----------------------+----------------------------`
|
ログスケジューラに追加し、指定された行に割り当てます
|
.-----------------------+-------------------------.
| | | |
line1 line2 line3 line4
|
| +- データの収集、分岐
| |
| +- データのトランスコーディング
.-----> | |
| | +- 特殊文字の処理
| |- 文書A |
| | 1 つの取得サイクル +- 複数行の処理
| | |
| | +- Pipeline処理
| | |
| | +-送信
| | |
| | +- ファイル収集場所を同期する
| | |
| Pipeline | `-ファイルステータスの検出
| ループ |
| |
| +- 文書 B |-
| |
| |
| +- ファイル C |-
| |
+----------+
ログファイルの検出と場所の特定¶
ログ ファイルを読み取って収集する必要があるため、まずディスク上のファイルの場所を特定する必要があります。 DataKit には、2 つのコンテナ ログと 1 つの通常のログを含む 3 つの主なタイプのファイル ログがあります。それらの収集方法は似ています。この記事でもこの3種類を中心に紹介します。それらは次のとおりです。
- 通常のログファイル
- Docker Stdout/Stderr、Docker サービス自体がログ管理とディスク配置を実行します (DataKit は現在、
json-fileドライバーの解析のみをサポートしています) - Containerd Stdout/Stderr、Containerd にはログ出力戦略がありません。現段階では、Containerd Stdout/Stderr は Kubernetes の kubelet コンポーネントによって管理されており、将来的には
Containerd(CRI)と総称される予定です。
通常のログファイルの検出¶
通常のログ ファイルが最も一般的なタイプです。これらは、読み取り可能なレコード データをディスク ファイルに直接書き込むプロセスです。ログ ファイルは、有名な「log4j」フレームワークまたは echo "this is log" >> /tmp/log コマンドの実行によって生成されます。
この種のログのファイル パスは、ほとんどの場合固定されています。たとえば、Linux プラットフォーム上の MySQL のログ パスは /var/log/mysql/mysql.log です。 DataKit MySQL コレクターを実行すると、デフォルトでログ ファイルを見つけるためのパスが検索されます。ただし、ログ ストレージ パスは構成可能であり、DataKit はすべての状況を考慮できるわけではないため、ファイル パスの手動指定をサポートする必要があります。
DataKit でファイル パスを構成するには、glob パターンを使用します。これは、ファイル名の検索にワイルドカードを使用します (もちろん、ワイルドカードを使用しないこともできます)。
たとえば、現在次のファイルがあります。
tree /tmp
/tmp
├── datakit
│ ├── datakit-01.log
│ ├── datakit-02.log
│ └── datakit-03.log
└── mysql.d
└── mysql
└── mysql.log
3 directories, 4 files
DataKit ログ コレクターでは、次のように logfiles パラメーターを構成することで、収集するログ ファイルを指定できます。
datakitディレクトリ内のすべてのファイルを収集します。グロブは/tmp/datakit/*です。datakitという名前のすべてのファイルを収集します。対応するグロブは/tmp/datakit/datakit-*logです。mysql.logを収集しますが、真ん中にmysql.dとmysqlという 2 つのディレクトリがあります。mysql.logファイルを見つける方法はいくつかあります。- 直接指定:
/tmp/mysql.d/mysql/mysql.log - アスタリスク単一指定:
/tmp/*/*/mysql.log、この方法は基本的に使用されません - 二重アスタリスク (
double star):/tmp/**/mysql.log、二重アスタリスク**を使用して中間の多層ディレクトリ構造を置き換えるのは、より簡単で一般的に使用される方法です。
- 直接指定:
glob を使用して構成ファイル内のファイル パスを指定すると、DataKit はルールに一致するファイルをディスクで定期的に検索します。収集リストにないことが判明した場合は追加して収集します。
コンテナの Stdout/Stderr ログファイルの場所の特定¶
コンテナにログを出力するには、次の 2 つの方法があります。
- 1 つは、マウントされたディスク ディレクトリに直接書き込む方法です。この方法は、ホストからは上記の「通常のログ ファイル」と同じように見えます。これらはディスク上の固定の場所にあるファイルです。
- もう 1 つの方法は、Stdout/Stderr に出力することで、コンテナーのランタイムがディスクの配置を収集して管理します。これもより一般的な方法です。この配置パスは、ランタイム API にアクセスすることで取得できます。
DataKit は、コマンド ラインで docker inspect --format='{{.LogPath}}' $INSTANCE_ID を実行するのと同様に、Docker または Containerd の sock ファイルに接続して API にアクセスすることにより、指定されたコンテナーの LogPath を取得します。
docker inspect --format='{{.LogPath}}' cf681e
/var/lib/docker/containers/cf681eXXXX/cf681eXXXX-json.log
コンテナー LogPath を取得した後、このパスと対応する構成を使用してログ コレクションを作成します。
ログデータの収集と処理¶
ログ収集スケジューラー¶
DataKit は Golang で記述されているため、ログ ファイルのパスを取得した後、通常は 1 つ以上のゴルーチンを開始してファイルを個別に収集することを選択します。モデルはシンプルで実装が簡単です。 DataKit は以前にもこれを行っていました。
ただし、ファイルが多すぎると、開くゴルーチンの数も増加し、ゴルーチンの管理に非常に悪影響を及ぼします。したがって、DataKit はログ収集スケジューラを実装します。
ほとんどのスケジューラ モデルと同様に、DataKit はスケジューラの下に複数のPipelineを実装します。新しいログ コレクションがスケジューラに登録されると、DataKit は各Pipelineの重みに従ってログ コレクションを割り当てます。
各Pipelineはループ内で実行されます。つまり、ファイル A が 1 回収集され (状況に応じて、N 秒間継続的に収集されます)、次にファイル B が収集され、次にファイル C が収集されます。これにより、ゴルーチンの数を効果的に制御し、多数のゴルーチンによる根本的なスケジューリングとリソースの競合を回避できます。
このログ収集でエラーが見つかった場合、そのログはPipelineから削除され、次のサイクルでは再度収集されません。
データの読み取りと行への分割¶
ログ データの読み取りに関しては、ほとんどの場合、呼び出されるたびにログの完全な行を返す Readline() のようなメソッド関数をまず思い浮かべます。しかし、これは DataKit には実装されていません。
より詳細な制御とより高いパフォーマンスを確保するために、DataKit は最も基本的な Read() メソッドのみを使用し、毎回 4KiB データを読み取り (バッファ サイズは 4KiB、実際の読み取りはこれより少ない場合があります)、改行文字 \n を使用して 4KiB データを N 個の部分に手動で分割します。次の 2 つの状況が発生します。
- この 4KiB データの最後の文字はたまたま改行文字であるため、余りなしで N 個の部分に分割できます。
- この 4KiB データの最後の文字は改行文字ではありません。上記と比較すると、この分割はN-1のみであり、残りの部分があります。残りの部分は次の 4KiB データのヘッダーに追加され、以下同様に続きます。
DataKit コードでは、最大のメモリ再利用を達成するために、update CursorPosition、copy、および truncate が同じバフに対して継続的に実行されます。
処理後、読み取られたデータは 1 行ずつになり、実行フローの次のレベルであるトランスコーディングと特殊文字の処理に進むことができます。
文字コード変換と特殊文字の処理¶
特殊文字のトランスコーディングと処理は、データの形成後に行う必要があります。そうしないと、文字の途中から切り捨てられ、切り捨てられたデータの一部が処理されます。たとえば、UTF-8 の漢字は 3 バイトを占め、最初のバイトが収集されるときにトランスコードされます。これは未定義の動作です。
データのトランスコーディングは非常に一般的な動作であり、エンコーディング タイプとビッグ エンディアンとリトル エンディアン (存在する場合) を指定する必要があります。この記事では「特殊文字処理」に焦点を当てます。
ここでの「特殊文字」とは、データ内の色の文字を指します。たとえば、次のコマンドはコマンド ライン ターミナルに赤い単語 rea を出力します。
何も処理せずにカラー文字を削除しない場合、最終的なログデータには\033[0;31mも含まれることになり、見栄えが悪く容量を圧迫するだけでなく、その後のデータ処理に悪影響を与える可能性があります。したがって、ここで特殊な色の文字を除外します。
オープンソースコミュニティには多くのケースがありますが、そのほとんどは正規表現を使用して実装されており、パフォーマンスはあまり良くありません。
ただし、成熟したログ出力フレームワークの場合は、カラー文字をオフにする方法が必要です。 DataKit では、ログ生成側でカラー文字の印刷を回避できるように、このアプローチを推奨しています。
行データの解析¶
「行データの解析」は主にコンテナーの Stdout/Stderr ログを対象としています。コンテナー ランタイムがログを管理および書き込むときに、生成時間、ソースが stdout か stderr か、このログが切り捨てられているかどうかなど、いくつかの追加情報フィールドが追加されます。DataKit はこのデータを解析し、対応するフィールドを抽出する必要があります。
- 単一の Docker JSON ファイル ログの形式は次のとおりです。これは JSON 形式であり、テキストは
logフィールドにあります。logコンテンツの末尾が\nの場合、このデータ行は完全で切り捨てられていないことを意味します。\nでない場合は、データが長すぎて 16KB を超えたため切り捨てられ、残りの部分は次の JSON にあることを意味します。
{"log":"2022/09/14 15:11:11 Bash For Loop Examples. Hello, world! Testing output.\n","stream":"stdout","time":"2022-09-14T15:11:11.125641305Z"}
- 単一の Containerd (CRI) ログの形式は次のとおりで、各フィールドはスペースで区切られています。 Docker と同様に、Containerd (CRI) にも、
Fに加えて、ログ トランケーション マーク、つまり 3 番目のフィールドPがあります。PはPartialを意味し、不完全で切り詰められています。FはFullを意味します。
2016-10-06T00:17:09.669794202Z stdout P log content 1
2016-10-06T00:17:09.669794202Z stdout F log content 2
スプライシング後のログデータはlog content 1 log content 2です。
行データを解析することで、ログテキストやstdout/stderrなどの情報を取得し、マークに基づいて不完全な切り捨てられたログであるかどうかを判断し、ログのスプライシングを実行できます。通常のログ ファイルには切り捨てがなく、ファイル内の 1 行のデータは理論的には無限に長くなる可能性があります。
さらに、ログの 1 行が切り捨てられた場合、それは、以下で説明する複数行のログではなく、スプライシング後のログの 1 行に属します。これらは 2 つの異なる概念です。
複数行データ¶
複数行の処理は、ログ収集の非常に重要な側面です。データを失うことなく、特性を満たしていないデータを特性を満たすようにすることができます。たとえば、ログ ファイルには次のデータが含まれており、これは一般的な Python スタック出力です。
2020-10-23 06:41:56,688 INFO demo.py 1.0
2020-10-23 06:54:20,164 ERROR /usr/local/lib/python3.6/dist-packages/flask/app.py Exception on /0 [GET]
Traceback (most recent call last):
File "/usr/local/lib/python3.6/dist-packages/flask/app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
2020-10-23 06:41:56,688 INFO demo.py 5.0
複数行の処理を行わない場合、最終的なデータは上記7行となり、原文と全く同じになります。 3 行目の Traceback (most recent call last): または 4 行目の File "/usr/local/lib/python3.6/dist-packages/flask/app.py", line 2447, in wsgi_app は固定形式ではないため、これは後続のPipelineの解析には役立ちません。
効果的な複数行処理の後、これら 7 行のデータは 3 行になり、結果は次のようになります。
2020-10-23 06:41:56,688 INFO demo.py 1.0
2020-10-23 06:54:20,164 ERROR /usr/local/lib/python3.6/dist-packages/flask/app.py Exception on /0 [GET]\nTraceback (most recent call last):\n File "/usr/local/lib/python3.6/dist-packages/flask/app.py", line 2447, in wsgi_app\n response = self.full_dispatch_request()
2020-10-23 06:41:56,688 INFO demo.py 5.0
ご覧のとおり、ログ データの各行は 2020-10-23 のような特徴的な文字列で始まり、特徴を満たさない元のテキストの行 3、4、および 5 が行 2 の末尾に追加されます。これにより、見た目がはるかに美しくなり、後続のPipeline フィールドの解析に役立ちます。
この機能は複雑ではなく、特徴文字列の正規表現を指定するだけです。
DataKit ログ コレクターは、multiline_match 項目で構成されます。上記の例では、この項目の構成は ^\d{4}-\d{2}-\d{2} である必要があり、これは 2020-10-23 のような行の先頭と一致します。
具体的な実装は、データ構造のスタック構造に似ています。特性を満たしている場合、前の項目がスタックからポップされてからスタックにプッシュされます。特性を満たさない場合は、スタックにプッシュされ、前の項目の末尾に追加されるだけです。このようにして、外部から受信したポップデータはすべて特性を満たすことになります。
さらに、DataKit は自動複数行もサポートします。ロギングコレクターの設定項目は、auto_multiline_detection および auto_multiline_extra_patterns です。そのロジックは非常に単純で、multiline_match のセットを提供し、元のテキストに従ってすべてのルールを走査して照合し、照合が成功した場合にその重みを増やして、次回最初に選択できるようにします。
自動複数行は構成を簡素化する方法です。ユーザー設定に加えて、「デフォルトの自動複数行ルール リスト」も提供します。詳細については記事末尾のリンクをご覧ください。
Pipeline による解析とログの status¶
Pipelineは、テキスト データの実行ルールを記述するためのさまざまな関数と構文を提供するシンプルなスクリプト言語です。これは主に、文字列テキストの行を複数の意味のあるフィールドに分割するなど、非構造化テキスト データを分割するため、または構造化テキスト (JSON など) から部分情報を抽出するために使用されます。
Pipelineの実装は比較的複雑です。これは、抽象構文ツリー (AST) と一連の内部ステート マシンおよび純関数関数で構成されますが、ここでは詳しく説明しません。
使用シナリオを見て、簡単な例を示してください。原文は次のとおりです。
Pipeline スクリプト:
最終結果:
{
"message": "2020-10-23 06:41:56,688 INFO demo.py 1.0",
"msg": "demo.py 1.0",
"status": "info",
"time": 1603435316688000000
}
注: Pipeline 解析後の
statusフィールドはINFOですが、DataKit がマッピング処理を行っているため、厳密のために小文字のinfoとして表示されます。
Pipelineはログ データ処理の最後のステップです。 DataKit は、Pipeline の結果を使用して行プロトコルを構築し、オブジェクトをシリアル化し、パッケージ化して Dataway に送信する準備をします。
データの送信と同期¶
データ送信は非常に一般的な動作です。 DataKit には基本的に「パッケージ化」、「トランスコーディング」、「送信」という 3 つのステップがあります。
ただし、送信後の操作は「現在のファイルの読み取り位置の同期」と「ファイルの状態の検出」という重要かつ重要な作業です。
同期¶
記事の最初のセクションで「ネットワーク ストリーム データ」を紹介したときに、定点でログ ファイルの読み取りを継続できるようにするために、DataKit では「ファイルの先頭からの読み取り」または tail -f モードのみをサポートするのではなく、現在のファイルの読み取り位置 (位置) を記録するという重要な操作が導入されていると述べました。
ログ収集によってディスク ファイルからデータが読み取られるたびに、ファイル内のデータの場所が記録されます。一連の処理と送信が完了して初めて、この位置情報が他のデータと結びついて、単一のディスクファイルに同期されます。
この利点は、DataKit がログ収集を開始するたびに、このログ ファイルが以前に収集されている場合は、データ収集の重複や途中で一部のデータが失われることなく、最後の場所を見つけて今回も収集を続行できることです。
関数の実装は複雑ではありません。
DataKit はログ収集を開始するときに、「ファイルの絶対パス + ファイル i ノード + ファイル ヘッダーの N バイト」を使用して排他的なキー値を形成します。このキーを使用して、指定されたパスを持つファイル内の位置を見つけます。
- 位置が見つかった場合、ファイルは前回収集されており、現在の位置から読み取られることを意味します。
- 位置が見つからない場合は、これが新しいファイルであることを意味します。状況に応じてファイルの先頭から読み取るか、ファイルの末尾から読み取るかを選択します。
ファイル状態の確認¶
ディスク ファイルの状態は静的ではありません。ファイルが削除されたり、名前が変更されたり、長期間変更されなかった可能性があります。 DataKit はこれらの状況に対処する必要があります。
-
ファイルは長期間変更されていません:
- DataKit はファイルの変更時刻 (
file Modification Date) を定期的に取得します。現在の時間が特定の制限を超えていることが判明した場合、ファイルは「非アクティブ」であるとみなされ、収集のためにファイルが閉じられなくなります。 - このロジックは、グロブ ルールを使用してログ ファイルを検索する場合にも存在します。グロブ ルールに一致するファイルが見つかっても、そのファイルが長期間変更 (または「更新」) されていない場合、そのファイルに対してログ収集は実行されません。
- DataKit はファイルの変更時刻 (
-
ファイルが反転 (回転) されます。
- ファイルのローテーションは非常に複雑なロジックです。平たく言えば、ファイル名は変更されませんが、そのファイル名が指す特定のファイル (inode など) は変更されます。典型的な例は、Docker ログのダウンロードです。
DataKit は、現在収集されているファイルがローテーションされているかどうかを定期的にチェックします。チェックのロジックは次のとおりです。このファイル名を使用して新しいファイル ハンドルを開き、SameFile() のような関数を呼び出し、2 つのハンドルが同じポイントを指しているかどうかを判断します。それらが矛盾している場合は、現在のファイル名がローテーションされていることを意味します。
ファイルがローテーションされたことを検出すると、DataKit は現在のファイルの残りのデータを (EOF まで) 収集し、ファイルを再度開きます。この時点ではすでに新しいファイルなので、操作プロセスは通常どおり続行されます。
まとめ¶
ログ収集は非常に複雑なシステムであり、多くの詳細な処理と最適化ロジックが含まれます。この記事は、DataKit ログ収集システムの設計と実装を紹介することを目的としています。ベンチマーク レポートや類似プロジェクトとのパフォーマンス比較はありません。これは必要に応じて後で完了できます。
追加のリンク: