インフラ・DevOps
VictoriaLogsがログをカラム型レイアウトで保存する方法
How VictoriaLogs Stores Your Logs in a Columnar Layout (victoriametrics.com)
要約
VictoriaLogsは、ログデータの効率的な保存と高速なクエリのために、ディスク上でカラム型レイアウトを採用しています。ログはまず内部形式に正規化され、ストリームごとにグループ化された後、メモリ上のバッファを経て、最終的に日付ごとのパーティションに格納される「パート」と呼ばれる単位にまとめられます。このカラム型アプローチにより、データの圧縮率向上、不要なスキャンの削減、および高速な検索が可能になります。
全文翻訳
ブログ / VictoriaLogsがログをカラム型レイアウトで保存する方法
VictoriaLogsがログをカラム型レイアウトで保存する方法
オープンソース技術
VictoriaLogs
オブザーバビリティ
共有:
VictoriaLogsを実行している場合、日常業務は3つのことに集約されます。ログの送信、クエリ、ディスクがいっぱいにならないように保持期間の設定です。それ以外のすべてはディスク上で静かに処理されます。
この記事では、1つのログ行が到着してからディスクに最終的に落ち着くまでを追跡し、VictoriaLogsが内部で何を行っているのか、なぜクエリが速く返ってくるのか、なぜディスク上に多くのファイルが見られることがあるのか、そして何かがおかしい場合にどのフラグとメトリックが重要なのかを説明します。この記事は誰でも理解できるように書かれており、プログラミングの背景は必要なく、Goコードを読む必要もありません。さらに深く知りたい場合は、VictoriaLogsのソースコードが常に参照となります。
1. ログ行の到着
# VictoriaLogsは、JSON Lines、Elasticsearch bulk、Loki push、OpenTelemetry、syslogなど、多くのプロトコルでログを受け入れます(完全なリストについては、データ取り込みドキュメントを参照してください)。
どのプロトコルを使用しても、VictoriaLogsが最初に行うことは、そのレコードをシステム全体が理解する単一の内部形式に変換することです。それはタイムスタンプ、名前付きフィールドのセット、そして「ストリーム識別子」です。
各プロトコルは、独自の小さなプロセッサでこの変換を行います。クエリ引数またはリクエスト自体のヘッダーでこれを制御できます。
保存したくないフィールドを削除するには、ignore_fieldsクエリ引数またはVL-Ignore-Fieldsヘッダーを使用します。
値からターミナルカラーコードを削除するには、decolorize_fieldsクエリ引数またはVL-Decolorize-Fieldsヘッダーを使用します。
各レコードに余分なフィールドを追加するには、extra_fieldsクエリ引数またはVL-Extra-Fieldsヘッダーを使用します。
VictoriaLogsのメインメッセージフィールド(_msg)を指すには、_msg_fieldクエリ引数またはVL-Msg-Fieldヘッダーを使用します。
タイムスタンプが含まれるフィールドを指定するには、_time_fieldクエリ引数またはVL-Time-Fieldヘッダーを使用します。
ストリーム識別子を定義するフィールドを選択するには、_stream_fieldsクエリ引数またはVL-Stream-Fieldsヘッダーを使用します。
ストリーム識別子は、この記事全体で最も重要な考え方です。同じストリームフィールドを共有するログは、単一のストリームとして扱われ、そのストリームがどのように見えるかを決定するのはあなたです。たとえば、_stream_fields=pod,containerと設定すると、同じpodとcontainerを持つすべてのログが1つのストリームを形成します。
VictoriaLogsは各ストリームのログをディスク上でまとめて保持し、そのグループ化が圧縮をうまく機能させ、クエリがすべてをスキャンするのではなく、必要なストリームのみに触れることを可能にします。
同じストリームフィールドを持つログは1つのストリームを形成します。
オペレーターとしての実用的なルール: ストリームフィールドは安定した低カーディナリティ(host, app, pod, containerなど、わずかな異なる値しか持たないもの)に保ち、高カーディナリティの値(trace_idやuser_idのように非常に多くのユニークなエントリを持つもの)は通常のフィールドとして扱い、ストリームフィールドとしては使用しないようにしてください。
受信したレコードを正規化した後、VictoriaLogsはそれらを1つずつ処理するわけでもありません。メモリ内のバッファに蓄積し、約1秒ごと(またはバッファがいっぱいになったらより早く)に、バッチ全体をまだRAM内にある小さな検索可能なチャンク(インメモリパート)に変換します。
バッファされたバッチは、インメモリパートにフラッシュされます。
そのインメモリバッファは単一の共有キューではありません。受信した各バッチが同じバッファを待つ必要があれば、互いに無駄な時間を費やすことになるため、VictoriaLogsはバッファをシャードに分割し、CPUコアごとに1つずつ、受信バッチを順番に分散させます。
したがって、3CPUマシンでは、3つのバッファシャードが並列で埋まり、各シャードは独自にフラッシュし、約1秒ごとにバッチを新しいインメモリパートとして書き出します。
バッファはCPUごとのシャードに分割され、それぞれが独自のインメモリパートをフラッシュします。
パートは、VictoriaLogs(および他のVictoriaMetrics製品)全体で中心的なデータ構造の1つです。検索可能、つまりクエリ可能な自己完結型のデータバンドルです。
ほとんどの場合、バッファされたバッチはインメモリパートにフラッシュされますが、まれに、バッチが大きすぎてインメモリサイズ制限を超える場合、パートは直接ディスクに小さなパートまたは大きなパートとして書き込まれます。
ヒント
メトリックvl_insert_flush_duration_seconds: バッファされたバッチをインメモリパートに変換するのにかかる時間です。
2. 日次パーティション
# バッチがフラッシュされると、VictoriaLogsは各ログをパーティションに格納します。パーティションは正確に1日分のログ(UTC)を保持します。各ログのタイムスタンプを読み取り、それが属する日を計算し、そこにルーティングします。
つまり、ログは日付ごとに分離されています。ディスク上でこれを直接確認でき、日付ごとに1つのディレクトリがあります。
$ tree victoria-logs-data/
victoria-logs-data/
└── partitions/
├── 20260109
├── 20260110
├── 20260111
└── 20260112
この日ごとのレイアウトは単なる実装の詳細ではありません。日常的な2つの操作が安価である理由です。
保持は、日ごとのディレクトリ全体を削除することによって達成されます。ログが-retentionPeriod(デフォルト7日)を超えて古くなった場合、またはディスクベースの保持が有効になった場合、VictoriaLogsは個々のログ行を探すのではなく、日ごとのフォルダ全体を削除します。
クエリはほぼ常に時間範囲が限定されています(_time:1h, _time:5m)。そのため、VictoriaLogsは時間範囲と重なる日次パーティションのみを開けばよく、残りは無視できます。
パーティションはディスク上のフォルダそのものではありません。2つの側面があります。既に書き込まれたパートを保持するディスク側の側面と、バッファシャードと先ほど見たインメモリパートを保持するメモリ側の側面です。
パーティションにはメモリ側とディスク側があります。
1日をクエリすると、パーティションは両方の側面から同時に結果を提供し、必要に応じてディスクから関連するパートを取得します。
ヒント
メトリックvl_storage_partsは、どこにあるかに基づいてパートの数を示します: {type="storage/inmemory"} はまだメモリ内にあるパート、{type="storage/small"} および {type="storage/big"} はディスク上にあるパートです。そしてvl_pending_rows{type="storage"} は、まだバッファにあり、パートに変換されていない行数をカウントします。
3. パート: VictoriaLogsが実際に保存する単位
# セクション1でパートについて触れました。パートは、バッファがフラッシュ時に生成する、自己完結型の検索可能なログバンドルです。言わなかったのは、パートには3つの種類があることです。これらは実際には、そのライフサイクルの異なる段階にある同じデータです。
インメモリパートは最初に作成されるため、新しく取り込まれたログはディスクを待つことなくほぼ即座にクエリ可能になります。
小さなパートは、耐久性のためにディスクに書き出されたインメモリパートです。
大きなパートは、小さなパートが時間とともに結合された結果です。
インメモリパートは、小さなパート、次に大きなパートに結合されます。
インメモリバッファ内のログは、ディスクに触れる前にまずインメモリパートにフラッシュされます。これが取り込みを安価に保つ理由です。
パートは、大きな、まれなチャンクでしかディスクに到達しません。小さなパートはメモリ内でより大きなものにマージされるため、VictoriaLogsはディスクへの書き込みと読み取りをはるかに少なく行います。これにより、低IOPSのHDDでも、毎秒約1GiBのログを吸収できます。
その最初の種類にはトレードオフが隠されています。インメモリパートはRAMに存在するため、ログは数秒以内にクエリ可能になりますが、まだディスク上に安全ではありません。VictoriaLogsは、短い間隔でディスクへのフラッシュを保証することで、このギャップを埋めます。これを制御するフラグは-inmemoryDataFlushInterval(デフォルト5秒)です。これは、インメモリデータがディスクに到達することが保証される頻度です。
ディスク上では、各パートはパーティションのdatadbフォルダ内にある16文字の16進数名(単なるID)のディレクトリです。別のindexdbフォルダには、その日のストリームカタログが格納されています。
$ tree victoria-logs-data/partitions/20260109/
victoria-logs-data/
└── partitions/
└── 20260109/
├── indexdb/
└── datadb/
├── 1882C35B4CE64498/
├── 1882C35B4CE664F8/
├── 1882C35B4CE66BDB/
└── parts.json
parts.jsonファイルは、現在アクティブなパートのリストにすぎません。これにより、VictoriaLogsは起動時に読み取るべきディレクトリを知ることができます。
$ cat victoria-logs-data/