この記事はBob Reselmanによるゲスト投稿です。
コンテナーが大はやりですが、それも不思議ではありません。コンテナーはスタンドアロンの仮想マシンが持つ利点をすべて備えたうえでロード時間の短縮を可能にするほか、システム管理者や開発者にとっての管理のしやすさも改善します。
ただし、コンテナーはそのままで最適なパフォーマンスが保証されるわけではありません。実際、デプロイしたコンテナーの実行が非常に遅いということもありえます。このような危険を避けるコツは、前もって計画し、予防策を講じることです。
コンテナーのパフォーマンス改善に役立つ3つの簡単なステップを紹介しましょう。
- DockerfileのコマンドをRUN句で連結する
- マルチステージビルドを使う
- イメージのキャッシュを事前にロードする
でもその前に、まずは背景情報です。
コンテナーは仮想マシンではない
コンテナーを利用する際に理解しておくべき重要事項の1つは、コンテナーは仮想マシンではないということです。仮想マシンはコンピューターをソフトウェアで表現したものとして実行され、物理的マシンから独立していますが、コンテナーはホストマシンのOS、カーネル、ファイルシステムに依存します(図1を参照)。
この違いが現実的に何を意味するかと言うと、たとえば仮想マシンはWindowsを実行し、それをLinuxを実行するマシンにホストできますが、ネイティブなWindowsホストマシンで実行されるコンテナーはWindowsを実行し、Linuxホストマシンで実行されるコンテナーはLinuxを実行する必要があるということです。
簡単に言うと、ホスト上のコンテナーは他のコンテナーとは完全に分離されているように見えますが、陰では1つのホストで実行されるすべてのコンテナーがホストのカーネルとファイルシステムを使用しています。このようにホストのリソースと不可分であることは、特にパフォーマンスの最適化に影響を及ぼします。
DockerfileのコマンドをRUN句で連結する
コンテナーのパフォーマンス改善の鍵となるのが、Dockerコンテナーの基になるDockerイメージを構成するレイヤーの数とサイズを最適化することです(DockerレイヤーはDockerイメージの要素であり、ファイルとしてホストのファイルシステムに保存されます。DockerレイヤーとDockerコンテナーの違いや関係については、こちらを参照してください)。
Dockerfileは、DockerのbuildコマンドがDockerイメージを作成するときに使用するインストラクションが記述されたテキスト文書です。下のリスト1のDockerfileを見てみましょう。
FROM jenkins/jenkins
USER root
EXPOSE 8080
RUN curl --silent --location https://deb.nodesource.com/setup_10.x | bash -
RUN apt-get update
RUN apt-get install -y apt-utils
RUN apt-get install -y libltdl7
RUN apt-get install -y npm
RUN apt-get install -y dnsutils
RUN rm -rf /var/lib/apt/lists/*
リスト1: 複数のRUNコマンドを使用してDockerイメージを構成するDockerfile
このDockerfileには7つのRUNコマンドがあることに注目してください。コマンドが増えるごとに、Dockerfileに対してbuildを実行した結果のDockerイメージはサイズが大きくなり、ロード時間が長くなります。また、Dockerfileで複数のRUNコマンドを使用すると、イメージを使用するコンテナーが実行インスタンスとしてロードされるまでの時間が長くなり、パフォーマンスを妨げます。
Dockerfileの本質的な効率性を改善する簡単な方法は、Dockerfile内のRUNコマンドの数を減らすことです。
下のリスト2は、&&演算子を使ってコマンドを連結することで、リスト1の7つのコマンドを1つに減らしています。結果として、Dockerイメージに関連するレイヤーのサイズと数が削減されます。
FROM jenkins/jenkins
USER root
EXPOSE 8080
RUN curl --silent --location https://deb.nodesource.com/setup_10.x | bash - \
&& apt-get update && apt-get install -y \
apt-utils \
libltdl7 \
npm \
dnsutils \
&& rm -rf /var/lib/apt/lists/*
リスト2: 単一のRUNコマンドを使用してDockerイメージを構成するDockerfile
コマンドの連結は強力なテクニックです。&&演算子を使用してDockerfile内のRUNコマンドを減らすのは単純なことですが、あなどってはいけません。単一のRUNコマンドに連結できるコマンドがないか、いつも目を光らせておくと、コンテナのロード時間のパフォーマンスが時にはかなり顕著に向上します。
マルチステージビルドを使う
上で触れたように、Dockerfileのインストラクション(RUNコマンドなど)が増えるたびにレイヤーが追加され、Dockerイメージ全体のサイズが大きくなります。コンテナーをすばやくロードするには、レイヤーのサイズと数を抑えるのが肝心です。
これを簡単に実現するもう1つの方法は、マルチステージビルドを使うことです。マルチステージビルドを使うと、複数のFROMステートメントでイメージのベースレイヤーを定義し、特定のFROMステートメントに関連するインストラクションのセットを指定し、固有の名前を付けることができます。その後、後続のステージでは、名前によって特定のビルドステージを参照できます。
下のリスト3は、マルチステージビルドの例です。
#First stage
FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
#Second stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
リスト3: マルチステージビルドを使用すると、コンテナーのロード時間の効率が改善される
Dockerでは、名前付きのステージは使用後に破棄されます。そのため、コンテナーイメージのサイズが削減されます。
リスト3では、#First stageのFROM句でステージ名buiderを定義しています。そして#Second stageでは、最後のCOPYコマンドでbuilderという名前のステージを参照しています。 名前付きのbuilderステージは使用後に破棄されます。#Second stageのインストラクションだけがDockerイメージのレイヤーの一部になります。
繰り返しになりますが、Dockerイメージのレイヤーが少ないほど、イメージを使用するコンテナーのロード時間は速くなります。つまり、パフォーマンスが良くなります。
イメージのキャッシュを事前にロードする
最後にご紹介するDockerのパフォーマンス改善方法は、イメージのキャッシュを事前にロードすることです。
Dockerは非常に独特なプロセスによってコンテナーをビルドします。docker runコマンドを実行すると、Dockerはまず、該当するイメージのDockerfileの定義に従って、コンテナーに必要な構成レイヤーがホストマシンにすでにダウンロードされているかを確認します。ダウンロードされていない場合、Dockerはインターネットに接続し、デフォルトのDocker Hubから、あるいはDockerクライアントの設定またはDockerfileで直接定義されたその他のDockerリポジトリから必要なレイヤーをダウンロードします。
レイヤーがまだホストマシンに存在しない場合、必要なレイヤーの数、サイズ、場所によっては、ダウンロードに長い時間がかかる可能性があります。
下のリスト4では、Linuxのtimeコマンドを使用して、必要なレイヤーが1つもホストマシンに存在しなかった場合にコンテナーを準備して実行するのにかかった時間を計測しています。docker runの実行に4秒余り要していることに注目してください。これはかなりの時間です。
time docker run -d --name pinger -p 3000:3000 reselbob/pinger:v2.3
real 0m4.389s
user 0m0.036s
sys 0m0.024s
リスト4: 必要なレイヤーが1つもホストマシンに存在しなかった場合にコンテナーを準備して実行するのにかかった時間
では、レイヤーが事前にロードされている場合はどうでしょうか。はるかに短い時間で済んでいることがわかるでしょう。もしかしたら、8倍近く速くなっているかもしれません。
たしかに、Dockerレイヤーをホストマシンに事前にロードするようデプロイメントプロセスを構築し、サポートするには、ある程度の時間をかけていろいろと計画しなければなりません。しかし、劇的なパフォーマンスの向上を考慮すると、労力をかける価値はあります。
まとめ
Dockerは現在の企業にとってキーとなる技術です。コンテナーが提供する柔軟性と独立性は、ソフトウェアを継続的に修正してすばやくリリースし、市場の要求に応えることを可能にします。
しかし、Dockerテクノロジーを効率的に利用するとは、単にDockerイメージをリポジトリに置いて、イメージを基にコンテナーを立ち上げる以上のことを意味します。計画とベストプラクティスが必要です。
ここで紹介した3つのコンテナーのパフォーマンス改善方法が、最大限の結果を出すお役に立てば幸いです。
筆者のBob Reselmanについて:全国的に著名なソフトウェア開発者、システムアーキテクト、業界アナリスト、テクニカルライター/ジャーナリスト。コンピュータープログラミングに関する書籍のほか、ソフトウェア開発テクノロジーおよびテクニック、ソフトウェア開発カルチャーに関する記事を多数執筆。元Cap GeminiのPrincipal ConsultantおよびコンピューターメーカーGatewayのPlatform Architect。ロサンゼルス在住。現在は、ソフトウェア開発およびテスト活動のかたわら、自動化が雇用に与える影響に関する書籍を執筆中。連絡はLinkedInwww.linkedin.com/in/bobreselmanまで。
(この記事は、開発元Gurock社の Blog 「Three Easy Ways to Improve a Container’s Performance」2020年2月6日の翻訳記事です。)