LabHub

ブログ

Grafana LokiとLogQLマスターガイド:ログパイプライン設計から運用まで

한국어English日本語

Grafana LokiとLogQLマスターガイド:ログパイプライン設計から運用まで

概要

ログはシステム障害分析の最後の砦だ。メトリクスが「何が問題なのか」を教えてくれるとすれば、ログは「なぜ問題が発生したのか」を説明する。しかし大規模分散システムで1日数百ギガバイトのログを収集して検索することは決して単純ではない。ElasticsearchベースのELKスタックは強力なフルテキスト検索を提供するが、インデックスコストと運用の複雑さが高い。

Grafana Lokiはこの問題に対して根本的に異なるアプローチを取る。ログ本文をインデックス化せず、ラベルベースのインデックスだけを保持したまま、ログチャンクをオブジェクトストレージに圧縮して保存する。この設計のおかげでElasticsearch比でストレージコストが70-80%削減され、数テラバイト規模のログもS3やGCSのような安価なオブジェクトストレージで運用できる。

この記事では、Loki 3.xの内部アーキテクチャ、3つのデプロイモード、LogQLのクエリ構文と最適化パターン、Grafana Alloyを活用したログ収集パイプライン、TSDBストレージ設定とリテンション、アラートルールの構成、実践的なトラブルシューティング事例、そして障害復旧手順までを扱う。

Lokiアーキテクチャの詳細分析

Lokiはマイクロサービスベースのアーキテクチャとして設計されており、各コンポーネントが明確な役割を担う。中心となる哲学は「ログはメトリクスのように扱う」だ。Prometheusのラベルモデルをそのままログに適用することで、同一のラベルセットでメトリクスとログを連携できる。

主要コンポーネント

Distributorはログ収集エージェント(Alloy、Promtailなど)からのPushリクエストを受け取る入口だ。受信したログストリームのラベルを検証し、コンシステントハッシュリング(Consistent Hash Ring)を通じて適切なIngesterへルーティングする。レートリミットとバリデーションをこの段階で処理するため、不正なラベルや過剰なトラフィックはIngesterに到達する前に遮断される。

Ingesterはログデータをインメモリバッファに蓄積したあと、チャンク単位で圧縮してオブジェクトストレージへフラッシュする。WAL(Write-Ahead Log)を活用してプロセスの異常終了時のデータ損失を防ぐ。Ingesterが最も多くのメモリを使用するコンポーネントであるため、リソース見積もりの際に最も注意が必要だ。

Query Frontendはクライアントのクエリリクエストを受け取り、複数のQuerierに分散して実行する。時間範囲を複数の区間に分割するquery splittingと結果キャッシュによってクエリ性能を向上させる。

Querierは実際にクエリを処理するワーカーだ。Ingesterのインメモリデータとオブジェクトストレージのブロックデータを同時に探索して結果をマージする。

Compactorはオブジェクトストレージのインデックスファイルを定期的にマージし、リテンションポリシーに従って期限切れのデータを削除する。Loki 3.6からは水平スケール可能なCompactorが導入され、大規模な削除リクエストの処理速度が大きく改善された。

Index Gatewayはインデックスデータに対するクエリを中央で処理し、Querierが直接オブジェクトストレージにアクセスする回数を減らす。マイクロサービスモードで特に有用だ。

データフロー

[Application] --> [Grafana Alloy] --> [Distributor]
                                           |
                                    [Hash Ring]
                                           |
                                      [Ingester]
                                       /      \
                              [WAL]         [Object Storage (S3/GCS)]
                                                  |
                              [Compactor] <-------+
                                                  |
                              [Query Frontend] ---+---> [Querier]
                                                           |
                                                  [Index Gateway]

デプロイモードの選択

Lokiは3つのデプロイモードを提供しており、ログボリュームと運用成熟度に応じて選択する。

Monolithicモード

すべてのコンポーネントが単一プロセスで実行される。開発環境や1日数ギガバイト以下の小規模環境に適している。

# Helm values - Monolithicモード
loki:
  deploymentMode: SingleBinary
  auth_enabled: false
  commonConfig:
    replication_factor: 1
  storage:
    type: filesystem
  schemaConfig:
    configs:
      - from: '2024-01-01'
        store: tsdb
        object_store: filesystem
        schema: v13
        index:
          prefix: index_
          period: 24h
singleBinary:
  replicas: 1
  persistence:
    size: 50Gi

Simple Scalableモード(推奨)

Loki Helm Chartのデフォルトモードであり、read、write、backendの3つのターゲットに分離される。1日数百ギガバイトから約1TBまでのログを処理できるため、ほとんどのプロダクション環境で最適な選択だ。

# Helm values - Simple Scalableモード
loki:
  deploymentMode: SimpleScalable
  auth_enabled: true
  commonConfig:
    replication_factor: 3
  storage:
    type: s3
    s3:
      endpoint: s3.ap-northeast-2.amazonaws.com
      region: ap-northeast-2
      bucketnames: company-loki-logs
      access_key_id: ${AWS_ACCESS_KEY_ID}
      secret_access_key: ${AWS_SECRET_ACCESS_KEY}
  schemaConfig:
    configs:
      - from: '2024-01-01'
        store: tsdb
        object_store: s3
        schema: v13
        index:
          prefix: index_
          period: 24h
write:
  replicas: 3
  persistence:
    size: 50Gi
  resources:
    requests:
      cpu: '1'
      memory: 2Gi
    limits:
      memory: 4Gi
read:
  replicas: 3
  resources:
    requests:
      cpu: '1'
      memory: 2Gi
    limits:
      memory: 4Gi
backend:
  replicas: 2
  persistence:
    size: 50Gi
gateway:
  replicas: 2

writeターゲットはStatefulSetとしてデプロイされWALデータを保持し、readターゲットはDeploymentとしてデプロイされ自動スケーリングが可能だ。必ずリバースプロキシ(gateway)を前段に配置し、APIリクエストをread/writeノードへルーティングしなければならない。

Microservicesモード

各コンポーネントを独立したプロセスとしてデプロイする。1日1TBを超える環境や、コンポーネントごとの細かなスケーリングが必要な場合に選択する。デフォルトのHelmデプロイではDistributor 3個、Ingester 3個、Querier 3個、Query Frontend 2個、Index Gateway 2個、Compactor 1個が生成される。

デプロイモード1日のログボリューム運用の複雑さ主な対象
Monolithic数GB以下低い開発/テスト環境
Simple Scalable数十GB ~ 1TB中程度ほとんどのプロダクション
Microservices1TB超高い大規模マルチテナント

LogQLクエリのマスター

LogQLはPrometheusのPromQLから着想を得たLoki専用のクエリ言語だ。ログストリームセレクタから始めてパイプラインステージをチェーンする構造であり、ログ検索(Log Query)とメトリクス変換(Metric Query)の2種類をサポートする。

ストリームセレクタとラインフィルタ

すべてのLogQLクエリはストリームセレクタから始まる。ストリームセレクタはLokiのインデックスを活用するため、可能な限り具体的に指定するとクエリ性能が向上する。

# 基本のストリームセレクタ
{namespace="production", app="payment-service"}

# ラインフィルタのチェーン - フィルタは順番に適用されるため、最も狭いフィルタを先に配置する
{namespace="production", app="payment-service"}
  |= "error"
  != "health-check"
  |~ "timeout|connection refused"

# Loki 3.x のパターンマッチフィルタ - regexより10倍高速
{namespace="production"} |> "error <_> timeout"

フィルタ演算子のまとめ:

パーサとラベル抽出

LokiはJSON、logfmt、pattern、regexp、unpackのパーサを提供する。JSONやlogfmt形式であれば専用パーサが最も効率的であり、非定型ログにはpatternパーサがregexpより高速だ。

# JSONパーサ - 構造化ログからフィールドを抽出
{app="api-gateway"} | json | status >= 500

# logfmtパーサ
{app="auth-service"} | logfmt | level="error" | duration > 5s

# patternパーサ - nginx access log のパース
{app="nginx"}
  | pattern "<ip> - - [<timestamp>] \"<method> <path> <_>\" <status> <bytes>"
  | status >= 400
  | line_format "{{.ip}} {{.method}} {{.path}} {{.status}}"

# regexpパーサ - 複雑な非定型ログ
{app="legacy-service"}
  | regexp "(?P<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) \\[(?P<level>\\w+)\\] (?P<message>.*)"
  | level="ERROR"

パーサ性能比較の原則: json/logfmtが最も速く、次がpattern、regexpが最も遅い。可能であればline filterをパーサの前に配置し、パース対象のログ行数を先に減らすべきだ。

メトリクスクエリ - ログを数値に変換する

LogQLのメトリクスクエリはログストリームを時系列データに変換する。この機能により、別途メトリクスを収集しなくてもログデータだけでダッシュボードとアラートを構成できる。

# 秒あたりのエラーログ発生率
rate({namespace="production", app="payment-service"} |= "error" [5m])

# サービス別の5xxエラー件数
sum by (app) (
  count_over_time(
    {namespace="production"} | json | status >= 500 [5m]
  )
)

# unwrapを活用したレスポンスタイムP99の算出
quantile_over_time(0.99,
  {app="api-gateway"}
    | json
    | unwrap response_time_ms
    | __error__=""
  [5m]
) by (endpoint)

# 平均レスポンスタイムのトレンド
avg_over_time(
  {app="api-gateway"}
    | json
    | unwrap duration_seconds
    | __error__=""
  [5m]
) by (service)

# bytes_over_time - ログボリュームのモニタリング
sum by (namespace) (bytes_over_time({namespace=~".+"} [1h]))

__error__="" フィルタは、パースやunwrapの段階で発生したエラーを持つ行を除外する必須パターンだ。これを落とすと数値でない値が混ざり、不正確な結果になる。

クエリ最適化の実践Tips

  1. ストリームセレクタを可能な限り具体的に指定する。 {app="payment"} の代わりに {namespace="production", app="payment", env="prod"} のようにラベルを追加するほど、検索対象のストリームが減る。
  2. ラインフィルタをパーサより先に配置する。 |= "error" | json の順序が | json |= "error" より速い。ラインフィルタが先に適用され、パース対象の行が減るためだ。
  3. regexの代わりにパターンマッチや文字列フィルタを使う。 |~ "error.*timeout" より |= "error" |= "timeout" のほうがはるかに速い。
  4. 時間範囲を最小限に絞る。 直近1時間の検索と直近7日間の検索では性能差が劇的だ。
  5. 不要なラベル抽出をしない。 | json はすべてのJSONフィールドをラベルとして抽出するため、特定のフィールドだけが必要なら | json status, duration のように明示する。

Grafana Alloyベースのログ収集パイプライン

Promtailは2025年2月にLTSに入り、2026年3月にEOLを迎える。Grafana Alloyが公式の後継コレクタであり、ログだけでなくメトリクス、トレース、プロファイリングまで単一のエージェントで収集する。

Promtail vs Alloy の比較

項目PromtailGrafana Alloy
収集対象ログのみログ、メトリクス、トレース、プロファイル
設定言語YAML (scrape_configs)River (HCL類似DSL)
OTel互換非対応ネイティブOTLP対応
Kubernetesログ収集ファイルシステムベース (/var/log/containers)Kubernetes APIベース (loki.source.kubernetes)
処理パイプラインstagesブロックloki.processコンポーネント
状態EOL (2026-03-02)活発に開発中
マイグレーションツール-alloy convert --source-format=promtail

Alloy設定例 - Kubernetesログ収集

// Kubernetes Pod ディスカバリ
discovery.kubernetes "pods" {
  role = "pod"
}

// ラベルの再マッピング - 必要なラベルだけを選択
discovery.relabel "pods" {
  targets = discovery.kubernetes.pods.targets

  rule {
    source_labels = ["__meta_kubernetes_namespace"]
    target_label  = "namespace"
  }
  rule {
    source_labels = ["__meta_kubernetes_pod_name"]
    target_label  = "pod"
  }
  rule {
    source_labels = ["__meta_kubernetes_pod_container_name"]
    target_label  = "container"
  }
  rule {
    source_labels = ["__meta_kubernetes_pod_label_app"]
    target_label  = "app"
  }
}

// Kubernetes ログソース
loki.source.kubernetes "pods" {
  targets    = discovery.relabel.pods.output
  forward_to = [loki.process.pipeline.receiver]
}

// ログ処理パイプライン
loki.process "pipeline" {
  // JSONログのパース
  stage.json {
    expressions = {
      level   = "level",
      message = "msg",
    }
  }

  // levelラベルの正規化
  stage.label_drop {
    values = ["filename", "stream"]
  }

  // debugログのドロップ - プロダクションでのボリューム削減
  stage.match {
    selector = "{level=\"debug\"}"
    action   = "drop"
  }

  forward_to = [loki.write.default.receiver]
}

// Lokiへ送信
loki.write "default" {
  endpoint {
    url = "http://loki-gateway.monitoring.svc:3100/loki/api/v1/push"
    tenant_id = "production"
  }
  external_labels = {
    cluster = "prod-kr-01",
    region  = "ap-northeast-2",
  }
}

PromtailからAlloyへの移行

既存のPromtail設定をAlloyへ変換するコマンドが提供されている。

# Promtail設定をAlloy River構文へ変換
alloy convert --source-format=promtail --output=alloy-config.river promtail-config.yaml

# 変換結果の検証 (dry-run)
alloy run --stability.level=generally-available alloy-config.river

# Kubernetes DaemonSetのデプロイ (Helm)
helm upgrade --install alloy grafana/alloy \
  --namespace monitoring \
  --set alloy.configMap=true \
  -f alloy-values.yaml

ストレージ設計とリテンション管理

TSDBインデックスストア

Loki 2.8から導入されたTSDBは現在推奨されるインデックスストアだ。従来のBoltDB Shipperに比べてクエリ性能が改善され、TCO(Total Cost of Ownership)が低い。インデックス期間は必ず24時間に設定しなければならない。

# loki.yaml - TSDB + S3 ストレージ設定
schema_config:
  configs:
    - from: '2024-01-01'
      store: tsdb
      object_store: s3
      schema: v13
      index:
        prefix: loki_index_
        period: 24h

storage_config:
  tsdb_shipper:
    active_index_directory: /loki/tsdb-index
    cache_location: /loki/tsdb-cache
  aws:
    s3: s3://ap-northeast-2/company-loki-chunks
    s3forcepathstyle: false

# Loki 3.4+ 統合ストレージ設定 (Thanos Object Storage Client)
common:
  storage:
    s3:
      endpoint: s3.ap-northeast-2.amazonaws.com
      region: ap-northeast-2
      bucketnames: company-loki-data
      access_key_id: ${AWS_ACCESS_KEY_ID}
      secret_access_key: ${AWS_SECRET_ACCESS_KEY}

Loki 3.4からThanos Object Storage Clientが統合され、MimirやPyroscopeなど他のGrafanaデータベースと同一のストレージ設定形式を使用する。従来のAWS SDKベースの設定も互換性があるが、新規デプロイでは統合設定を使うことが推奨される。

リテンションポリシーの構成

リテンションはCompactorが担当する。retention_enabled: true を必ず設定しなければならず、グローバルリテンションとストリーム単位のリテンションの両方をサポートする。

# loki.yaml - リテンション設定
compactor:
  working_directory: /loki/compactor
  compaction_interval: 10m
  retention_enabled: true
  retention_delete_delay: 2h
  retention_delete_worker_count: 150

limits_config:
  retention_period: 720h # グローバル: 30日
  retention_stream:
    - selector: '{namespace="production"}'
      priority: 1
      period: 2160h # プロダクション: 90日
    - selector: '{namespace="staging"}'
      priority: 2
      period: 168h # ステージング: 7日
    - selector: '{level="debug"}'
      priority: 3
      period: 72h # デバッグログ: 3日

注意事項: retention_delete_delay は削除の遅延時間だ。誤ってリテンションを短くしすぎた場合、この時間内に設定を戻せばデータ損失を防げる。最低でも2時間以上に設定することを強く推奨する。

ストレージコストの最適化

  1. チャンク圧縮アルゴリズム: snappy(デフォルト)より gzip のほうが圧縮率は高いがCPU使用量が増える。ストレージコストが主な関心事ならgzipを、書き込み性能が重要ならsnappyを選ぶ。
  2. chunk_target_size: デフォルトは1.5MBだ。この値を上げるとオブジェクト数が減りAPI呼び出しコストが下がるが、Ingesterのメモリ使用量が増える。
  3. S3 Intelligent-Tiering: ログのアクセスパターンは時間が経つほど減少するため、S3 Intelligent-Tieringやライフサイクルポリシーを適用すると長期保管コストを削減できる。

Loki vs Elasticsearch の比較

ログ管理ソリューションを選ぶ際に最も比較される2つのシステムの違いを整理する。

比較項目Grafana LokiElasticsearch (ELK)
インデックス方式ラベルのみインデックスフルテキストインデックス
ストレージコスト非常に低い(オブジェクトストレージ)高い(SSDブロックストレージ)
クエリ言語LogQLKQL / Lucene
検索速度(非定型)遅い(grep方式)速い(転置インデックス)
検索速度(ラベルベース)速い速い
メモリ要求量低い高い(JVMヒープ)
運用の複雑さ中程度高い(シャーディング、リバランシング)
Grafana連携ネイティブプラグインが必要
1日100GB基準のコストElasticsearch比 20-30%基準
適した環境Kubernetes、クラウドネイティブフルテキスト検索、セキュリティ分析(SIEM)

Lokiを選ぶべきケース: Kubernetes環境でコスト効率の良いログ管理が必要で、Grafanaをメインのダッシュボードとして使い、ラベルベースの構造化クエリが主なパターンである場合。

Elasticsearchを維持すべきケース: 非定型ログに対する任意のフルテキスト検索が頻繁であるか、セキュリティログ分析(SIEM)やビジネスインテリジェンス用途でログを活用する場合。

アラートルールの構成

LokiはLogQLベースのアラートルールをサポートしており、Rulerコンポーネントが定期的にクエリを実行し、条件が満たされるとAlertmanagerへアラートを送信する。

# loki-alert-rules.yaml
groups:
  - name: application-errors
    rules:
      - alert: HighErrorRate
        expr: |
          sum by (app, namespace) (
            rate({namespace="production"} |= "error" [5m])
          ) > 10
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: 'High error rate in {{ $labels.app }}'
          description: '{{ $labels.app }} in {{ $labels.namespace }} has error rate {{ $value }}/s for 5 minutes.'

      - alert: SlowResponseTime
        expr: |
          quantile_over_time(0.95,
            {app="api-gateway"}
              | json
              | unwrap response_time_ms
              | __error__=""
            [5m]
          ) by (app) > 3000
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: 'P95 response time exceeds 3s for {{ $labels.app }}'

      - alert: LogVolumeSpike
        expr: |
          sum by (namespace) (bytes_over_time({namespace=~".+"} [5m]))
            / sum by (namespace) (bytes_over_time({namespace=~".+"} [5m] offset 1h))
          > 3
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: 'Log volume spike in {{ $labels.namespace }} (3x increase)'

LogVolumeSpikeアラートは特に有用だ。ログボリュームが急増した場合、アプリケーションに問題があるか、誤ったデバッグ設定がデプロイされた可能性がある。放置するとIngesterのOOMやストレージコストの急増につながる。

実践トラブルシューティング

症状1: Ingester OOM (Out of Memory)

原因: ラベルのカーディナリティが高すぎるか、単一テナントのアクティブストリーム数が過剰な場合。代表的にはrequest_idやtrace_idのような一意の値をラベルに設定した場合に発生する。

診断:

# アクティブストリーム数の確認
curl -s http://loki:3100/metrics | grep loki_ingester_streams_created_total

# テナント別のストリーム数の確認
curl -s http://loki:3100/loki/api/v1/index/stats \
  --header "X-Scope-OrgID: production" \
  --data-urlencode 'query={namespace="production"}' \
  --data-urlencode 'start=1h'

# Ingesterのメモリ使用量の確認
kubectl top pods -n monitoring -l app.kubernetes.io/component=ingester

解決:

  1. 高カーディナリティのラベルを構造化メタデータ(structured metadata)へ移行する。
  2. max_streams_per_user リミットを適切に設定する。
  3. Ingesterのメモリリミットを増やすか、replicasを増加させる。

症状2: クエリタイムアウト

原因: クエリの時間範囲が広すぎるか、ストリームセレクタが包括的すぎる場合。

解決:

# limits_config のチューニング
limits_config:
  max_query_length: 721h # 最大クエリ時間範囲
  max_query_parallelism: 32 # クエリ並列度
  query_timeout: 5m # 単一クエリのタイムアウト
  split_queries_by_interval: 30m # クエリ分割間隔
  max_query_series: 500 # 最大シリーズ数

症状3: out-of-orderログの拒否

Lokiは基本的にログがタイムスタンプ順に到着することを前提とする。複数のエージェントが同一ストリームに書き込む場合やネットワーク遅延がある場合、順序が入れ替わることがある。

# Loki 3.4+ out-of-order 許容設定
limits_config:
  unordered_writes: true
  max_chunk_age: 2h

症状4: Compactorの遅延

Compactorが処理速度に追いつけないと、インデックスファイルが溜まりクエリ性能が低下する。

解決: Loki 3.6の水平スケール可能なCompactorを活用するか、compaction_intervalを短くしてCompactorのCPU/メモリリソースを増やす。

障害復旧手順

Ingester障害の復旧

# 1. 障害が発生したIngester Podの確認
kubectl get pods -n monitoring -l app.kubernetes.io/component=ingester

# 2. WAL状態の確認
kubectl exec -n monitoring ingester-0 -- ls -la /loki/wal/

# 3. 障害Podの再起動 (WALがあるためデータ復旧が可能)
kubectl delete pod -n monitoring ingester-0

# 4. 復旧後のハッシュリング状態の確認
curl -s http://loki:3100/ring | jq '.shards[] | {addr, state, tokens}'

# 5. flushの強制実行 (必要時)
curl -X POST http://loki:3100/ingester/flush

オブジェクトストレージへのアクセス障害

オブジェクトストレージにアクセスできないと、IngesterのWALにデータが溜まり続け、最終的にOOMが発生する。

  1. ただちにS3/GCSの接続状態とIAM権限を確認する。
  2. Ingesterの flush_check_periodchunk_idle_period を一時的に延ばして時間を稼ぐことができる。
  3. ストレージが復旧したら POST /ingester/flush で蓄積されたデータをフラッシュする。
  4. 長期障害の際はIngesterのディスク容量を監視し、必要ならPVCを拡張する。

Loki 3.x の主な変更点まとめ

Bloom filterは特定のテキスト文字列(エラーメッセージ、UUIDなど)を検索するフィルタクエリの速度を大きく向上させる。まだ実験的な機能だが、大規模環境ではgrep方式のフルスキャンを画期的に減らせる。

運用チェックリスト

プロダクションのLokiデプロイ前に必ず確認すべき項目を整理する。

デプロイ前

ラベル設計

運用中

まとめ

Grafana Lokiは「インデックスがなくても大規模なログを効率的に管理できる」ことを証明したシステムだ。Elasticsearchのフルテキストインデックスが必要ない大半の運用環境で、Lokiは70-80%少ないコストで同等レベルの障害分析能力を提供する。

核心はラベル設計だ。過剰なラベルはIngesterのOOMと性能低下を招き、少なすぎるラベルはすべてのクエリをフルスキャンにしてしまう。namespace、app、env、levelレベルのラベルから始め、実際のクエリパターンに応じて段階的に調整することが推奨される。

Loki 3.xのBloom filter、パターンマッチフィルタ、ネイティブOTelサポートは、従来の「検索速度が遅い」という弱点を急速に補っている。PromtailからAlloyへの移行とTSDBインデックスストアの導入は、もはや選択ではなく必須だ。Simple Scalableモードから始め、ログボリュームが増えたらMicroservicesモードへ移行する段階的なアプローチが、失敗を減らす最も現実的な戦略だ。

参考資料

コメント

まだコメントはありません。

ログインするとコメントできます