DMM.comの、一番深くておもしろいトコロ。

ECS + firelensで大きなサイズのログをNewRelicに転送する

ECS + firelensで大きなサイズのログをNewRelicに転送する

  • このエントリーをはてなブックマークに追加

この記事は、DMMグループ Advent Calendar 2021 7日目の記事です。ITインフラ本部SRE部の小野輝也が担当します。

DMMが扱うサービスでは日々多くのログが出力され、それらはフィルタリングや加工の後に様々な場所に転送されていきます。転送されたログは分析や障害調査に利用されるため、サービスの性質や目的に沿ったログ基盤を構築することは、サービスの品質向上に関わる重要なタスクとなります。

私が担当している事業部では、利用するロギングサービスとしてCloudwatch LogsからNewRelic Logsへの移行を行いました。理由は次の通りです。

  • NRQLによる高度な検索が可能
  • Logs in Contextを活用することでAPMとの連携が可能
  • Cloudwatch Logsよりログ挿入・維持コストが低い(クラウド費用のおよそ1/5がCloudwatch Logsにかかっていましたが、NewRelic Logsに移行することで1/20ほどに圧縮できます)

ログを出力するアプリケーションサーバはECS Fargateで稼働していたため、移行に際してfirelensを活用しました。本記事では、ECS FargateからNewRelic logsにログ送信する際の落とし穴と対処法について紹介します。

firelens

firelensは、ECSのタスク定義で指定するだけで簡単に利用できるログルーターです。fluent-bitまたはfluentdと共に動作し、どちらを利用するか選択することができます。 firelensのコンテナをアプリケーションコンテナのサイドカーとして同じタスクに乗せる構成が一般的です。

次の図は、アプリケーションコンテナが出力するログがどのようにfirelensコンテナに送信されるかを示した図になります。アプリケーションコンテナの標準出力ログは、DockerのfluentdログドライバによりDockerデーモンを通してfirelensのコンテナへと転送されます。

f:id:dmmadcale2021:20211125142829p:plain
dockerログドライバを使ったログの転送

ログを生成するアプリケーションコンテナではコンテナ定義に次のように記述します。

"logConfiguration": {
    "logDriver":"awsfirelens"
}

fluent-bitを利用する場合、firelensコンテナのコンテナ定義には次のように記述します。optionsで追加のfluent-bitの設定ファイルを指定することが可能で、これはデフォルトの設定ファイルから追加で読み込まれます。

"firelensConfiguration": {
    "type": "fluentbit",
    "options": {
        "config-file-type": "file",
        "config-file-value": "/fluent-bit.conf"
    }
},

NewRelicにログを転送する場合はnrlogs OUTPUTを利用するため、fluent-bitの設定ファイルは次のようになります。なお、$NEWRELIC_LICENSEは有効なライセンスに書き換える必要があります。

[OUTPUT]
    Name          nrlogs
    Match         *
    license_key   $NEWRELIC_LICENSE

Dockerのログドライバを利用する際の落とし穴

Dockerのログドライバには、大きなサイズのログが送信されてきた場合、16KBごとに分割して転送するといった仕様があります。そのため、日本語のようなマルチバイト文字が分割されると、ログの中に非文字のバイトが残ってしまう可能性があります。このような表示できないバイト列を含むログを送信した場合のログ基盤サービスの挙動はさまざまであり、Cloudwatch Logsでは�(代替文字)で表示されますが、NewRelic Logsでは挿入時にエラーとなります。

次のようなクエリでエラーを確認できます。

SELECT * FROM NrIntegrationError

不完全なバイト列を含むログがLogs APIに送信されている場合、次のようなエラーが検索結果として返ってきます。

Error unmarshalling message payload

対処法

このように分割されてしまうとNewRelicにログを送信することができません。これには次のような2通りの解決策が考えられます。

  1. firelensコンテナで受け取ったログを結合して送信する
  2. Dockerのログドライバを迂回してfirelensコンテナでログを送信する

1の手法はこのIssueで議論されているように、まだ不足している部分があり簡単には利用できないようです。 そのため、今回は2の方法について解説します。

Dockerログドライバの迂回

先程の図にあった通り、firelensのコンテナへログを送信するために使える経路はいくつかあります。そのうちログドライバを使わず直接firelensに送信するためには、次の2つの手法が考えられます。

  1. コンテナ間通信で送信する
  2. コンテナ間で共有ボリュームを利用する

コンテナ間通信を使ったログ転送

firelensのコンテナはfluentd/fluent-bitのプロトコルでリッスンするようなデフォルト設定が組み込まれています。さらに、"logDriver":"awsfirelens"を指定したアプリケーションコンテナには、FLUENT_HOSTFLUENT_PORTという環境変数が自動で定義されるため、この変数を使うことで、firelensのコンテナに直接ログを送信できます。

アプリケーションの標準出力ログはfluent-catfluent-cat-goにパイプラインで繋ぐことで送信します。

./main | fluent-cat-go -H $FLUENT_HOST -p $FLUENT_PORT mytag

ボリュームを使ったログ転送

もう1つの方法はECSのタスクストレージをコンテナ間で共有することで転送します。アプリケーションコンテナはバインドマウントされた領域にログを書き込み、firelensコンテナからはtail INPUTを使ってログを読み出します。

タスク定義は次のようになります。

{
    "family": "sample",
    ...
    "volumes": [
        {
            "name": "log_volume"
        }
    ]
    ...
    "containerDefinitions": [
        {
            "name": "app",
            "image": "app-image-name:${TAG}",
            "logConfiguration": {
                "logDriver":"awsfirelens"
            },
            "mountPoints": [
                {
                    "sourceVolume": "log_volume",
                    "containerPath": "/var/log/mylog"
                }
            ]
        },
        {
            "name": "firelens",
            "image": "firelens:${TAG}",
            "firelensConfiguration": {
                "type": "fluentbit",
                "options": {
                    "config-file-type": "file",
                    "config-file-value": "/fluent-bit.conf"
                }
            },  
            "mountPoints": [
                {
                    "sourceVolume": "log_volume",
                    "containerPath": "/var/log/mylog"
                }
            ],
        }
    ]
    ...
}

アプリケーションコンテナのDockerfileにはVOLUMEディレクティブを追加する必要があります。ここに書かれたパスがコンテナ定義にあるmountPoints.containerPathと一致する場合、コンテナ内のファイルがデータボリュームに公開されます。 デフォルトではこのディレクトリの所有者はrootになっているため、必要に応じて変更します。

# ログ領域
RUN mkdir /var/log/mylog && chown www-data:www-data -R /var/log/mylog
VOLUME ["/var/log/mylog"]

アプリケーションの標準出力ログはこのボリュームのファイルに吐き出すように変更します。例えば次のようになります。

./main > /var/log/mylog/log

fluent-bitの設定ファイルにはtail INPUTの設定を記述することでログを読み込みます。

[INPUT]
    Name              tail
    Path              /var/log/mylog/log
    Buffer_Chunk_Size 50M
    Buffer_Max_Size   200M
    Mem_Buf_Limit     200M
    Refresh_Interval  5
    Tag               app-tag

実際のサービスではこちらの共有ボリュームを利用する方針を採用しました。コンテナ間通信を使う方針だと、firelensコンテナへの転送ツールをイメージに含める必要がある、転送ツールの内部バッファをチューニングする必要がある、など考慮すべき点が増えるといったデメリットを考えてそのようにしました。

なお、タスクストレージの容量は限られているため、必要に応じてログローテーションの仕組みを導入する必要があります。

まとめ

  • 長いログを出力するときはDockerのログドライバによる分割にうまく対処する必要がある
  • Dockerログドライバを回避する方法としてコンテナ間通信による転送、タスクストレージによる転送が考えられる