☄️

ECS で awslogs のデフォルト設定で blocking / non-blocking モードが選択できるようになった

に公開

はじめに

2025/4/17、Amazon ECS のアカウント設定でログドライバーモードのデフォルトを指定できるアップデートがありました。
これまではデフォルトで blocking モードでしたが、そのデフォルトを変更できるアップデートとなります。

https://aws.amazon.com/about-aws/whats-new/2025/04/amazon-ecs-set-default-log-driver-blocking-mode/

この記事では、blockingnon-blocking モードの違いから入り、どこで実装されているのかみてみます。

blocking vs non-blocking モード

blocking モード

  • ログの送信に問題が発生した場合(ネットワーク問題や CloudWatch の制限など)、コンテナの実行がブロックされます。
  • ログは失われることはありませんが、コンテナのパフォーマンスに大きな影響を与える可能性があります。

non-blocking モード

  • ログは一時的にメモリバッファに保存されます。
  • ログの送信に問題が発生しても、コンテナの実行は中断されません。
  • 一部のログが失われる可能性があります。

すなわち、アプリケーションの可用性とログ損失のトレードオフとなります。

non-blocking モードの設定

non-blocking モードを設定するには、以下の2つのパラメータを指定する必要があります:

  1. mode=non-blocking - non-blocking モードを有効にします
  2. max-buffer-size - メモリバッファのサイズを設定します(例:「4m」は4MBを意味します)

non-blocking モードでコンテナを動かすためには、以下のようなコマンドで実行する必要があります。

docker run -it --log-opt mode=non-blocking --log-opt max-buffer-size=4m alpine ping 127.0.0.1

バッファサイズについて

non-blocking モードの場合、バッファサイズとログのサイズによってはログ欠損が発生します。
こちらの記事で負荷テストを行っており、推奨となるバッファサイズを示してくれています。

https://aws.amazon.com/jp/blogs/news/preventing-log-loss-with-non-blocking-mode-in-the-awslogs-container-log-driver/

max-buffer-size が 25 MB 以上の場合、コンテナからの出力ログレートが 5 MB/s 以下であれば、ログの損失は発生しません。

結論、max-buffer-size = 25m とすることを推奨していることが読み取れます。

さらに細かい負荷テストの結果はこちらで公開されています。
https://github.com/moby/moby/issues/45999

Fargate の場合はどうなの?

AWS のブログで少々気になったのが以下の記述

AWS Fargate 上の Amazon ECS では、構成されたモードに関係なく、ロググループまたはログストリームを作成できない場合、コンテナの起動は必ず失敗します。

つまり、blocking モードということですね。

https://aws.amazon.com/jp/blogs/news/under-the-hood-fargate-data-plane/

たとえば、Fargate ですでにサポートされているさまざまな宛先へのストリーミングコンテナログのサポートを追加したいと考えました。これを行うのは非常に簡単で、Containerd の shim ログ記録プラグインを拡張するだけでした。コンテナログのルーティング向けの shim logger プラグインのセットを作成しました。これは、今日オープンソースで提供しています。これは、GitHub の amazon-ecs-shim-loggers-for-containerd リポジトリにあります。このプラグインは、Docker Engine のプロセス内のログ記録ドライバーに取って代わります。Fargate はこのログ記録ドライバーをプラットフォームバージョン 1.4 より前に使用していて、同じ機能セットを提供していました。

Fargate の場合、Containerd の shim logger プラグインを使用していることがわかります。

https://github.com/aws/shim-loggers-for-containerd/tree/4bcb0f7126a3eee95a31d8a93e3b0a0d2f7c3e91?tab=readme-ov-file

EC2 では shim-logger は使われていない?

公開情報として確認できませんでした。

なので EC2 起動タイプで awslogs ドライバーを使用するコンテナを立てて、プロセスを見に行ってみます。

見る限り shim logger プラグインは使われていなそうでしょうか
ただ探し方が合っているのかわからず...

Docker が動いていることは確かなので、Docker ベースの awslogs ログドライバーから CloudWatch Logs へログを出力しているものと推測します。

[root@ip-172-31-22-231 ~]# docker ps
CONTAINER ID   IMAGE                            COMMAND                  CREATED         STATUS                 PORTS     NAMES
7c3926475893   nginx                            "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes                     ecs-heyYo-11-nginx-bad992f3b4afb2a49f01
9dcdebb8be13   amazon/amazon-ecs-pause:0.1.0    "/pause"                 2 minutes ago   Up 2 minutes                     ecs-heyYo-11-internalecspause-aed0da8ec6de94fea401
ac325341dddc   amazon/amazon-ecs-agent:latest   "/agent"                 6 hours ago     Up 6 hours (healthy)             ecs-agent

[root@ip-172-31-22-231 ~]# ps aux | grep shim
root        2809  0.0  0.1 2084224 20596 ?       Sl   00:30   0:04 /usr/bin/containerd-shim-runc-v2 -namespace moby -id ac325341dddcf18369cc110df2718d1cd2d5a31a4c1f10145f72fdedbec730ae -address /run/containerd/containerd.sock
root       32749  0.1  0.1 2008700 20660 ?       Sl   06:13   0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 8655f1d04d67f85184130bcb46d5c9aa0bd8dd7d1e5385bedd2057a99d0d428a -address /run/containerd/containerd.sock
root       32793  0.0  0.1 2008444 20088 ?       Sl   06:13   0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 7c3926475893b271c9aed12c52934f276921a224fc4dabb10ebc58c90421366e -address /run/containerd/containerd.sock
root       33892  0.0  0.0 222292  2032 pts/1    S+   06:16   0:00 grep --color=auto shim

[root@ip-172-31-22-231 ~]# ps aux | grep docker
root        2535  0.0  0.6 2295280 104752 ?      Ssl  00:30   0:17 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --default-ulimit nofile=32768:65536
root        2836  0.0  0.0   1092     4 ?        Ss   00:30   0:00 /sbin/docker-init -- /agent
root       34364  0.0  0.0 222292  2024 pts/1    R+   06:18   0:00 grep --color=auto docker

こちらの issue にある画像を見ても、EC2 起動タイプの場合は Docker の awslogs ログドライバーで出力されていそうです。
https://github.com/moby/moby/issues/45217

Impact to AWS Customers
Amazon ECS for EC2 only. Amazon ECS on Fargate is not affected because as explained below, the log driver is always started in blocking mode and is wrapped by a buffer in non-blocking mode.

Docker と shim-logger プラグインの実装

non-blocking モードで使用するバッファの役割は、RingBuffer という構造体が担っています。

Docker(moby) では以下の部分で実装されています。
non-blocking モードの場合、max-buffer-size の値をバッファとして設定します。

https://github.com/moby/moby/blob/01f442b8/container/container.go#L480-490

	if containertypes.LogMode(cfg.Config["mode"]) == containertypes.LogModeNonBlock {
		bufferSize := int64(-1)
		if s, exists := cfg.Config["max-buffer-size"]; exists {
			bufferSize, err = units.RAMInBytes(s)
			if err != nil {
				return nil, err
			}
		}
		l = logger.NewRingLogger(l, info, bufferSize)
	}

RingLogger は以下で定義されています。

// NewRingLogger creates a new Logger that is implemented as a RingBuffer wrapping
// the passed in logger.
func NewRingLogger(driver Logger, logInfo Info, maxSize int64) Logger {
	if maxSize < 0 {
		maxSize = defaultRingMaxSize
	}
	l := newRingLogger(driver, logInfo, maxSize)
	if _, ok := driver.(LogReader); ok {
		return &ringWithReader{l}
	}
	return l
}

shim-logger の場合この辺でしょうか。
Docker の実装を採用していると明記されていますね。

// Adopted from https://github.com/moby/moby/blob/master/daemon/logger/ring.go#L128
// as this struct is not exported.
type ringBuffer struct {
	// A mutex lock is used here when writing/reading log messages from the queue
	// as there exists three go routines accessing the buffer.
	lock sync.Mutex
	// A condition variable wa
    ...

// newLoggerBuffer creates a buffer that stores messages which are
// from container and consumed by sub-level log drivers.
func newLoggerBuffer(maxBufferSize int) *ringBuffer {
	rb := &ringBuffer{
		maxSizeInBytes:   maxBufferSize,
		queue:            make([]*dockerlogger.Message, 0, ringCap),
		closedPipesCount: 0,
		isClosed:         false,
	}
	rb.wait = sync.NewCond(&rb.lock)

	return rb
}

終わり

とりあえずどこで実装されているのかだけは特定しました。
ここら辺の動きをもう少し深掘りたいですが、また別の記事で深掘りたいと思います。

FireLnes などは ECS 独自のログドライバーとなるので上記の挙動とは異なりそうですね。
こちらも機会をみて確認していきたいと思います。

Discussion