fanotifyでDockerイメージを98.8%削減した方法

Habrahabrの読者に出版物の翻訳を提供します。Howは、Dockerイメージを98.8%縮小しました



数週間前、私はDockerについて社内で講演していました。 プレゼンテーション中に、管理者の1人が一見して簡単な質問をしました。「Dockerイメージの減量プログラム」のようなものはありますか?



この問題を解決するために、キャッシュディレクトリ、一時ファイルの削除、画像全体ではないにしてもさまざまな冗長パッケージの削減など、インターネット上でいくつかの非常に適切なアプローチを見つけることができます。 しかし、考えてみれば、完全に機能するLinuxシステムが本当に必要なのでしょうか? 単一の画像で本当に必要なファイルは何ですか? Goバイナリの場合、急進的でかなり効果的なアプローチを見つけました。 静的に構築され、外部の依存関係はほとんどありません。 最終的なイメージは6.12 MBです。



はい、本当に! しかし、他のアプリケーションと同様のことをする機会はありますか?



そのようなアプローチが存在する可能性があります。 アイデアは簡単です:実行時にイメージをプロファイリングして、どのファイルがアクセス/オープン/ ...されたかを判断し、気づかなかった残りのファイルをすべて削除できます。 それは有望に思えますが、このアイデアのためにPoCを書きましょう。



ソースデータ





/ bin / lsは良い例です。落とし穴なしでアイデアをテストするのは非常に簡単ですが、動的リンクを使用するため、まだ簡単ではありません。



目標ができたので、ツールを決めましょう。 主なアイデアは、ファイルアクセスイベントを監視することです。 statでもopenでも。 これにはいくつかの良い候補があります。 inotifyを使用することもできますが、設定する必要があり、各ウォッチを個別のファイルに割り当てる必要があります。これにより、最終的にこれらの同じウォッチが大量に生成されます。 LD_PRELOADを使用することもできますが、最初に個人的に使用することは私に喜びを与えません。次に、システムコールを直接インターセプトしません。 golangs?)。 静的にコンパイルされたアプリケーションでも機能するソリューションは、 ptraceを使用してリアルタイムでシステムコールをトレースすることです。 はい、彼はチューニングにも微妙ですが、それでも信頼性と柔軟性のあるソリューションになるでしょう。 あまり知られていないシステムコールはfanotifyであり、記事のタイトルから明らかになったときに使用されます。



fanotifyは当初、アンチウイルスベンダーが一度にマウントポイント全体でファイルシステムイベントをインターセプトする「価値のある」メカニズムとして作成されました。 おなじみの音? アクセスを拒否したり、ファイルアクセスのノンブロッキングモニタリングを実行したりするために使用できますが、カーネルキューがいっぱいの場合はイベントをスローする可能性があります。 後者の場合、特別なメッセージが生成され、リスナーのユーザースペースにメッセージの損失を通知します。 これがまさに私たちが必要とするものです。 邪魔にならず、一度に全体のマウントポイントを設定し、簡単に構成できます(もちろん、ドキュメントが見つかるという事実に基づいて...)。 ばかげているように見えるかもしれませんが、後でわかったように、それは本当に重要です。



使い方はとても簡単です。



  1. fanotify_initシステムコールを使用して、FAN_CLASS_NOTIFICATIONモードでfanotifyを初期化します。



    // Open ``fan`` fd for fanotify notifications. Messages will embed a // filedescriptor on accessed file. Expect it to be read-only fan = fanotify_init(FAN_CLASS_NOTIF, O_RDONLY);
          
          





  2. fanotify_markシステムコールを使用して、「/」FAN_MARK_MOUNTPOINTのFAN_ACCESSイベントとFAN_OPENイベントをサブスクライブします。



     // Watch open/access events on root mountpoint fanotify_mark( fan, FAN_MARK_ADD | FAN_MARK_MOUNT, // Add mountpoint mark to fan FAN_ACCESS | FAN_OPEN, // Report open and access events, non blocking -1, "/" // Watch root mountpoint (-1 is ignored for FAN_MARK_MOUNT type calls) );
          
          





  3. fanotify_initから受け取ったファイル記述子からメッセージを読み取り、FAN_EVENT_NEXTを使用してメッセージを反復処理します。



     // Read pending events from ``fan`` into ``buf`` buflen = read(fan, buf, sizeof(buf)); // Position cursor on first message metadata = (struct fanotify_event_metadata*)&buf; // Loop until we reached the last event while(FAN_EVENT_OK(metadata, buflen)) { // Do something interesting with the notification // ``metadata->fd`` will contain a valid, RO fd to accessed file. // Close opened fd, otherwise we'll quickly exhaust the fd pool. close(metadata->fd); // Move to next event in buffer metadata = FAN_EVENT_NEXT(metadata, buflen); }
          
          





その結果、アクセスされた各ファイルのフルネームを出力し、キューのオーバーフローの検出を追加します。 私たちの目的では、これで十分です(解決策を示すためにコメントとエラーチェックは省略されています)。



 #include <fcntl.h> #include <limits.h> #include <stdio.h> #include <sys/fanotify.h> int main(int argc, char** argv) { int fan; char buf[4096]; char fdpath[32]; char path[PATH_MAX + 1]; ssize_t buflen, linklen; struct fanotify_event_metadata *metadata; // Init fanotify structure fan = fanotify_init(FAN_CLASS_NOTIF, O_RDONLY); // Watch open/access events on root mountpoint fanotify_mark( fan, FAN_MARK_ADD | FAN_MARK_MOUNT, FAN_ACCESS | FAN_OPEN, -1, "/" ); while(1) { buflen = read(fan, buf, sizeof(buf)); metadata = (struct fanotify_event_metadata*)&buf; while(FAN_EVENT_OK(metadata, buflen)) { if (metadata->mask & FAN_Q_OVERFLOW) { printf("Queue overflow!\n"); continue; } // Resolve path, using automatically opened fd sprintf(fdpath, "/proc/self/fd/%d", metadata->fd); linklen = readlink(fdpath, path, sizeof(path) - 1); path[linklen] = '\0'; printf("%s\n", path); close(metadata->fd); metadata = FAN_EVENT_NEXT(metadata, buflen); } } }
      
      





収集するもの:



 gcc main.c --static -o fanotify-profiler
      
      





大まかに言えば、アクティブな「/」マウントポイント上のファイルへのアクセスをリアルタイムで監視するツールが用意されました。 素晴らしい。



次は? Ubuntuコンテナーを作成し、監視を開始して、/ bin / lsを実行しましょう。 FanotifyにはCAP_SYS_ADMIN機能が必要です。 これは基本的に「キャッチオール」ルート機能です。 いずれにせよ、これは特権モードで行うよりも優れています。



 # Run image docker run --name profiler_ls \ --volume $PWD:/src \ --cap-add SYS_ADMIN \ -it ubuntu /src/fanotify-profiler # Run the command to profile, from another shell docker exec -it profiler_ls ls # Interrupt Running image using docker kill profiler_ls # You know, the "dynamite"
      
      





実行結果:



 /etc/passwd /etc/group /etc/passwd /etc/group /bin/ls /bin/ls /bin/ls /lib/x86_64-linux-gnu/ld-2.19.so /lib/x86_64-linux-gnu/ld-2.19.so /etc/ld.so.cache /lib/x86_64-linux-gnu/libselinux.so.1 /lib/x86_64-linux-gnu/libacl.so.1.1.0 /lib/x86_64-linux-gnu/libc-2.19.so /lib/x86_64-linux-gnu/libc-2.19.so /lib/x86_64-linux-gnu/libpcre.so.3.13.1 /lib/x86_64-linux-gnu/libdl-2.19.so /lib/x86_64-linux-gnu/libdl-2.19.so /lib/x86_64-linux-gnu/libattr.so.1.1.0
      
      





いいね! うまくいきました。 これで、/ bin / lsを実行するために最終的に必要なものが確実にわかります。 これで、すべてを「FROMスクラッチ」Dockerイメージにコピーするだけで完了です。



しかし、それはそこにありました...しかし、すべて順を追って自分自身より先に進まないようにしましょう。



 # Export base docker image mkdir ubuntu_base docker export profiler_ls | sudo tar -x -C ubuntu_base # Create new image mkdir ubuntu_lean # Get the linker (trust me) sudo mkdir -p ubuntu_lean/lib64 sudo cp -a ubuntu_base/lib64/ld-linux-x86-64.so.2 ubuntu_lean/lib64/ # Copy the files sudo mkdir -p ubuntu_lean/etc sudo mkdir -p ubuntu_lean/bin sudo mkdir -p ubuntu_lean/lib/x86_64-linux-gnu/ sudo cp -a ubuntu_base/bin/ls ubuntu_lean/bin/ls sudo cp -a ubuntu_base/etc/group ubuntu_lean/etc/group sudo cp -a ubuntu_base/etc/passwd ubuntu_lean/etc/passwd sudo cp -a ubuntu_base/etc/ld.so.cache ubuntu_lean/etc/ld.so.cache sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/ld-2.19.so ubuntu_lean/lib/x86_64-linux-gnu/ld-2.19.so sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/ld-2.19.so ubuntu_lean/lib/x86_64-linux-gnu/ld-2.19.so sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libselinux.so.1 ubuntu_lean/lib/x86_64-linux-gnu/libselinux.so.1 sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libacl.so.1.1.0 ubuntu_lean/lib/x86_64-linux-gnu/libacl.so.1.1.0 sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libc-2.19.so ubuntu_lean/lib/x86_64-linux-gnu/libc-2.19.so sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libpcre.so.3.13.1 ubuntu_lean/lib/x86_64-linux-gnu/libpcre.so.3.13.1 sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libdl-2.19.so ubuntu_lean/lib/x86_64-linux-gnu/libdl-2.19.so sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libattr.so.1.1.0 ubuntu_lean/lib/x86_64-linux-gnu/libattr.so.1.1.0 # Import it back to Docker cd ubuntu_lean sudo tar -c . | docker import - ubuntu_lean
      
      





イメージを実行します。



 docker run --rm -it ubuntu_lean /bin/ls
      
      





その結果、以下が得られます。



 # If you did not trust me with the linker (as it was already loaded when the profiler started, it does not show in the ouput) no such file or directoryFATA[0000] Error response from daemon: Cannot start container f318adb174a9e381500431370a245275196a2948828919205524edc107626d78: no such file or directory # Otherwise /bin/ls: error while loading shared libraries: libacl.so.1: cannot open
      
      





うん。 しかし、何が間違っていたのでしょうか? このシステムコールは元々、ウイルス対策で動作するように作成されたことを思い出しましたか? リアルタイムウイルス対策は、ファイルへのアクセスを検出し、チェックを実行し、結果に基づいて決定を下す必要があります。 ここで重要なのは、ファイルの内容です。 特に、ファイルシステムの競合状態は必ずあるはずです。 これが、fanotifyがアクセスされたパスの代わりにファイル記述子を返す理由です。 ファイルの物理パスは、プローブ/ proc / self / fd / [fd]によって計算されます。 さらに、彼はどのシンボリックリンクがアクセスされたのか、それが指しているファイルのみを言うことはできません。



動作させるためには、fanotifyで見つかったファイルへのすべてのリンクを見つけ、同じ方法でフィルターされたイメージにインストールする必要があります。 findコマンドはこれに役立ちます。



 # Find all files refering to a given one find -L -samefile "./lib/x86_64-linux-gnu/libacl.so.1.1.0" 2>/dev/null # If you want to exclude the target itself from the results find -L -samefile "./lib/x86_64-linux-gnu/libacl.so.1.1.0" -a ! -path "./
      
      





これはループで簡単に自動化できます。



 for f in $(cd ubuntu_lean; find) do ( cd ubuntu_base find -L -samefile "$f" -a ! -path "$f" ) 2>/dev/null done
      
      





最終的に欠落しているセマンティックリンクのリストが得られます。 これらはすべてライブラリです。



 ./lib/x86_64-linux-gnu/libc.so.6 ./lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ./lib/x86_64-linux-gnu/libattr.so.1 ./lib/x86_64-linux-gnu/libdl.so.2 ./lib/x86_64-linux-gnu/libpcre.so.3 ./lib/x86_64-linux-gnu/libacl.so.1
      
      





元の画像からコピーして、結果の画像を再作成しましょう。



 # Copy the links sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libc.so.6 ubuntu_lean/lib/x86_64-linux-gnu/libc.so.6 sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ubuntu_lean/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libdl.so.2 ubuntu_lean/lib/x86_64-linux-gnu/libdl.so.2 sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libpcre.so.3 ubuntu_lean/lib/x86_64-linux-gnu/libpcre.so.3 sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libacl.so.1 ubuntu_lean/lib/x86_64-linux-gnu/libacl.so.1 sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libattr.so.1 ubuntu_lean/lib/x86_64-linux-gnu/libattr.so.1 # Import it back to Docker cd ubuntu_lean docker rmi -f ubuntu_lean; sudo tar -c . | docker import - ubuntu_lean
      
      





重要な注意 :この方法は制限されています。 たとえば、絶対リンクだけでなく、リンクへのリンクも返しません。 後者には、少なくともchrootが必要です。 または、findまたはその代替が存在する場合は、元のイメージから実行する必要があります。



結果のイメージを実行します。



 docker run --rm -it ubuntu_lean /bin/ls
      
      





すべてが機能するようになりました:

 bin dev etc lib lib64 proc sys
      
      





まとめ



ubuntu :209MB

ubuntu_lean :2.5MB



その結果、画像が83.5倍小さくなりました。 この圧縮率は98.8%です。



あとがき



プロファイリングに基づいたすべての方法と同様に、彼はこのシナリオで実際に行われた/使用されたことを言うことができます。 たとえば、最終イメージで/ bin / ls -lを実行してみて、自分で確認してください。

怠け者のネタバレ
機能しません。 まあ、つまり、それは機能しますが、期待どおりではありません。



プロファイリング手法には欠陥がないわけではありません。 ファイルがどのように開かれたかを正確に理解することはできません。 これは、シンボリックリンク、特にファイルシステム間(ボリューム間での読み取り)の問題です。 fanotifyを使用すると、元のシンボリックリンクが失われ、アプリケーションが破損します。



本番環境で使用する準備ができているこのような「スクイーズ」を構築する必要がある場合、ほとんどの場合、ptraceを使用します。



注釈



  1. 実際、システムコールの実験に興味がありました。 Dockerイメージはかなり良い言い訳です。
  2. 実際、FAN_UNLIMITED_QUEUEを使用してfanotify_initを呼び出してこの制限をバイパスすることは、呼び出しプロセスが少なくともCAP_SYS_ADMINであれば、完全に可能です。
  3. また、この記事の冒頭で述べた6.13MBの画像よりも2.4倍小さいですが、比較は公平ではありません。



All Articles