この記事は、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のコンテナへと転送されます。
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通りの解決策が考えられます。
- firelensコンテナで受け取ったログを結合して送信する
- Dockerのログドライバを迂回してfirelensコンテナでログを送信する
1の手法はこのIssueで議論されているように、まだ不足している部分があり簡単には利用できないようです。 そのため、今回は2の方法について解説します。
Dockerログドライバの迂回
先程の図にあった通り、firelensのコンテナへログを送信するために使える経路はいくつかあります。そのうちログドライバを使わず直接firelensに送信するためには、次の2つの手法が考えられます。
- コンテナ間通信で送信する
- コンテナ間で共有ボリュームを利用する
コンテナ間通信を使ったログ転送
firelensのコンテナはfluentd/fluent-bitのプロトコルでリッスンするようなデフォルト設定が組み込まれています。さらに、"logDriver":"awsfirelens"
を指定したアプリケーションコンテナには、FLUENT_HOST
とFLUENT_PORT
という環境変数が自動で定義されるため、この変数を使うことで、firelensのコンテナに直接ログを送信できます。
アプリケーションの標準出力ログはfluent-catやfluent-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ログドライバを回避する方法としてコンテナ間通信による転送、タスクストレージによる転送が考えられる