Docker Desktop は、仮想マシンを実行して Docker コンテナーをホストします。 VM 内の各コンポーネント (Docker エンジン自体を含む) は、個別の分離されたコンテナーとして実行されます。 この分離の追加レイヤーは、Docker Desktop 診断レポートに含めることができるようにすべてのログをキャプチャする方法という興味深い新しい問題をもたらします。 何もしないと、ログは個々のコンテナに個別に書き込まれますが、これは明らかにあまり役に立ちません。
Docker デスクトップ VM は、 Linux Kit を使用してビルドされた ISO から起動し、Docker イメージのリストから、機能とバインド マウントのリストと共に起動します。 LinuxKit VM 定義の最小限の例については、 https://github.com/linuxkit/linuxkit/blob/master/examples/minimal.yml を参照してください — LinuxKit リポジトリには、その他の例とドキュメントがあります。 Docker Desktop の LinuxKit VM は 2 つのフェーズで起動します: 最初のフェーズでは、init プロセスは runc を使用して一連のワンショットの「ブート時」アクションを順番に実行し、それらをコンテナに分離します。 これらのアクションは通常、ディスクのフォーマット、スワップの有効化、sysctl 設定とネットワーク インターフェイスの構成を行います。 2番目のフェーズには、同時に開始され、コンテナ化されたタスクとして永久に実行される「サービス」が含まれています。
次の図は、 ブート プロセスの簡略化された概要を示しています。
デフォルトでは、「ブート時」アクションの stdout と stderr は、VM コンソールと .* 内の /var/log/onboot
ファイルの両方に書き込まれます。 「サービス」 stdout
は、 stderr
永遠に成長するために残されている開いているファイル /var/log
に直接接続されています。
当初は、syslog 互換のログ デーモンを、ポートまたはポート (あるいはその両方) を公開 /dev/log
する通常のサービスとして実行することで、VM にログを追加することを検討しました。 その後、他のサービスが syslog に接続してログを書き込みます。 残念ながら、サービスで実行されているロギングデーモンは後で起動するため、「起動時」アクションからのすべてのログを見逃します。 さらに、サービスは同時に開始されるため、syslog デーモンの起動と syslog クライアントの起動の間に競合が発生し、ログが失われるか、各クライアントの起動が syslog サービスの開始の待機をブロックする必要があります。 syslogデーモンを「オンブート」アクションとして実行すると、サービスとの競合を回避できますが、「オンブート」アクションリストのどこに配置するかを選択する必要があります。 理想的には、ログが失われないように最初にロギングデーモンを起動しますが、その場合、永続ディスクやネットワークにアクセスしてログを有用な場所に保存することはできません。
要約すると、Dockerデスクトップに 次のようなロギングメカニズムを追加し たいと考えました。
- すべてのログ(起動時のアクションとサービスログの両方)をキャプチャできました。
- ログを別々のファイルに書き込むことで、診断レポートで読みやすくすることができます。
- ログファイルをローテーションして、永遠に大きくならないようにすることができます。
- アップストリームのLinuxKitプロジェクト内で開発することができます。そして
- 既存のLinuxKitユーザーにYAML定義の書き直しや既存のコードの変更を強制することはありません。
最初の起動時のアクションの前に起動し、各コンテナからの最後の数千行のコンソール出力をメモリにバッファリングするmemlogdと呼ばれる「メモリログデーモン」を追加することで、ロギングのファーストクラスのサポートを実装することにしました。 memlogdはメモリ内のバッファリングのみであるため、ネットワークや永続ストレージを必要としません。 ログダウンローダーは、ネットワークと永続ストレージが利用可能になった後に起動し、memlogdに接続し、ログを永続的な場所にストリーミングします。
メモリ内バッファーがいっぱいになる前にログがストリーミングされる限り、行が失われることはありません。 memlogd の使用は LinuxKit では全くオプションです。イメージに含まれていない場合、ログはコンソールに書き込まれ、以前と同様にファイルを開くために直接書き込まれます。
設計
Go ライブラリ コンテナー/リング を使用して、制限された循環バッファーを作成することにしました。 バッファーは、スパム ログ クライアントが VM で大量のメモリを消費するのを防ぐために制限されています。 ただし、バッファーがいっぱいになると、最も古いログ行が削除されます。 次の図は、初期設計を示しています。
ロギングクライアントは、ファイルディスクリプタ ("linuxkit-external-logging.sock" というラベル) を介してログエントリを送信します。 ログダウンロードプログラムは、クエリソケット(「memlogdq.sock」というラベルが付いています)に接続します。 内部バッファからログを読み取り、別の場所に書き込みます。
設計目標の 1 つは、新しいログ システムを使用するために個々のコンテナー定義を変更しないようにすることでした。 ロギングソケットをコンテナに明示的にバインドマウントしたり、コンテナのコードを変更して接続したりする必要はありません。 それでは、コンテナからの出力を自動的にキャプチャし、それをすべてlinuxkit-external-logging.sockに渡すにはどうすればよいでしょうか。
ブート時のアクションまたはサービスが起動されると、VM の init システムは、標準出力と標準エラーに対して FIFO (コンテナー化の場合) またはソケットペア (runc の場合) を作成します。 慣例により、LinuxKit コンテナは通常、ログエントリを stderr に書き込みます。 したがって、init システムを変更すると、コンテナ定義やコードを変更することなく、stderr FIFO とソケットペアに書き込まれたログをキャプチャできます。 ログがキャプチャされたら、次のステップはそれらをmemlogdに送信することです—どうすればよいですか?
Linuxのあまり知られていない機能は、Unixドメインソケットを介して開いているファイル記述子を他のプロセスに渡すことができることです。 ログ行をプロキシする代わりに、開いているソケットを直接memlogdに渡すことができます。 これを利用するために、memlogdのデザインを変更しました。
コンテナが起動すると、init システムは標準出力および標準エラー出力のファイル記述子をコンテナの名前と共に memlogd に渡します。 Memlogd は、選択ループ内のすべてのファイル記述子を監視します。 データが使用可能になると、データが読み取られ、コンテナー名でタグ付けされ、メモリ内のリングバッファーに追加される前にタイムスタンプが付けられます。 コンテナが終了すると、fd は閉じられ、memlogd はループから fd を削除します。
つまり、次のことを意味します。
- ロギングシステムを認識するためにコンテナYAML定義やコードを変更する必要はありません。そして
- コンテナとmemlogdの間でログをプロキシする必要はありません。
memlogd のクエリ
Docker デスクトップシステムで memlogd の動作を確認するには、次のコマンドを試してください。
docker run -it --privileged --pid=host justincormack/nsenter1 /usr/bin/logread -F -socket /run/guest-services/memlogdq.sock
これにより、ルート名前空間で特権コンテナーが実行されます ("memlogdq.sock" を含む) ログのクエリに使用)ユーティリティ「logread」を実行し、ストリームを「たどる」、つまり中断されるまでmemlogdから端末にコピーし続けるように指示します。 出力は次のようになります。
2019-02-22T16:04:23Z,docker;time="2019-02-22T16:04:23Z" level=debug msg="ttrpc サーバーの登録"
ここで、最初のタイムスタンプはmemlogdがメッセージを受信した日時を示し、「docker」はログがDockerサービスから送信されたことを示します。 行の残りの部分は、stderr に書き込まれる出力です。
カーネル ログ (kmsg)
Docker デスクトップでは、Linux カーネルのバグを理解して修正するのに役立つ診断レポートに Linux カーネル ログを含めています。 この目的のために kmsg パッケージ を作成しました。 このサービスが開始されると、/dev/kmsg に接続し、カーネルログをストリーミングして stderr に出力します。 stderr は自動的に memlogd に送信されるため、カーネル ログも VM のログに含まれ、診断レポートに含まれます。 /dev/kmsg を介したカーネルログの読み取りは特権操作であるため、kmsg サービスには機能CAP_SYSLOGが必要であることに注意してください。
ログの永続化
Docker Desktopでは、ログエントリをファイルに保持し(サービスごとに1つ)、大きくなったらローテーションしてから、スペースのリークを防ぐために最も古いものを削除します。 この目的のためにログ 書き込みパッケージ を作成しました。 このサービスが開始されると、クエリ ソケット memlogdq.sock に接続されます。 書き込まれたログをダウンロードし、ログ ファイルを管理します。
概要
Docker Desktopに必要な機能を提供する比較的シンプルで軽量でありながら拡張可能なロギングシステムができました:「起動時」のアクションとサービスの両方からログをキャプチャし、ファイルシステムがマウントされた後、ローテーションでログをファイルに保持します。 私たちは上流の LinuxKitプロジェクト でロギングシステムを開発し、シンプルでモジュール化された設計により、他のLinuxKit開発者が簡単に拡張できることを願っています。
参照
- LinuxKit リポジトリ のmemlogd に関するドキュメント
- メモリログの使用例
- memlogd のソースコード
- カーネルログメッセージを含めることができる kmsg サービス
この投稿は、マグヌス・スキェグスタッドとの共同作業でした。