inode数の制限をテストしてみる
inode数の制限をテストしてみる
多数のファイルを生成する
各ディストリビューションのDocker上にコンテナを起動し、そのコンテナ上で0バイトのファイルを無制限に生成する実験を行った。実験に使用したテスト用の簡易プログラム(inode_full.c)を、以下に示す。
#include <stdio.h>
#include <stdlib.h>
#define FILE_COUNT 10
int main( int argc, char **argv )
{
FILE *fptr;
char filename[20];
int cnt = 0;
int max_cnt = FILE_COUNT;
int print_term = 1;
if( argc > 1 ){
max_cnt = atol( argv[1] );
}
if( max_cnt > 10 ){
print_term = (int)( (double)max_cnt * 0.1 );
}
for( cnt = 1; cnt <= max_cnt; cnt++ ){
sprintf( filename, "%08d.txt", cnt );
fptr = fopen( filename, "w" );
if( fptr == NULL ){
printf( "fopen [%d]回目でエラー\n", cnt );
return( EXIT_FAILURE );
}
if( ( cnt % print_term ) == 0 ){
printf( "fopen [%d]回目成功\n", cnt );
}
fclose( fptr );
}
return( EXIT_SUCCESS );
}
表7:コンテナ上で大量のファイルを作成した結果
| ディストリビューション | inode数上限 | 発生した事象 |
|---|---|---|
| CentOS 6.6 x86_64 CentOS 7.0 x86_64 |
655,360 | コンテナに割り当てられたinodeが枯渇し、コンテナ上ではファイル生成不能となる。ベースOSには全く影響が発生しない |
| Ubuntu Server 14.04 x86_64 | 6,291,456 | OSのinode数の限界までファイルが生成される。ベースOS側もinodeが枯渇しファイル生成不能となり、ベースOS全体が障害状態となる |
ストレージドライバとしてdevice-mapperを使用した場合、コンテナに割り当てられる仮想ディスクは1つのデバイスとして提供される。そのため、ディスク容量と同様にinode数にも上限が存在し、コンテナ内で大量にファイルを生成した場合には、その上限は超えられない。仮想ディスクを生成すると、その領域に新たにinode領域が生成されるので、Dockerコンテナの数が増えても、ベースOSのinode数の減少は設定ファイルなどの分のみなので、極めて影響が少ない。
一方aufsを使用した場合は、ベースOSのファイルシステムの一部フォルダをそのまま提供するので、コンテナごとに新たなinode領域は付与されない。そのため、ベースOSの空きinode数を完全に枯渇させるまでコンテナ内でファイルを生成できてしまう。aufsではinode数の制限は行えないため、制限が必要な場合は、ストレージドライバをdevice-mapperに変更するといった対策が必要となる。
inode枯渇状態になっても作成済みのファイルに対する追記は可能なため、ディスク空き領域の枯渇よりはベースOSへの影響は少ない。実際、inode数枯渇後に生成したファイルを削除したところ、どちらのディストリビューションでもコンテナ、ベースOSとも停止することなく、復旧できた。
メモリ使用量の制限をテストしてみる
メモリ制限なしの状態で大量にメモリ消費するアプリを動かす
各ディストリビューションのDocker上にコンテナを起動し、そのコンテナ上でmalloc関数を使用して無制限にメモリを確保し続けて、空きメモリを枯渇させる実験を行った。実験に使用したテスト用の簡易プログラム(memory_full.c)を、以下に示す。
#include <stdio.h>
#include <stdlib.h>
#define MALLOC_CNT 100
#define MEMORY_SIZE 4096
int main( int argc, char **argv )
{
char *ptr = NULL;
int cnt = 0;
int max_cnt = MALLOC_CNT;
if( argc > 1 ){
max_cnt = atol( argv[1] );
}
for( cnt = 1; cnt < max_cnt; cnt++ ){
ptr = (char *)malloc(MEMORY_SIZE);
if( ptr == NULL ){
printf( "malloc [%d]回目でエラー\n", cnt );
return( EXIT_FAILURE );
}
if( ( cnt > 0 ) && ( ( cnt % 1000 ) == 0 ) ){
printf( "malloc [%08x] [%d]回目成功\n", ptr, cnt );
}
}
return( EXIT_SUCCESS );
}
表8:コンテナ上でメモリを消費し続けるアプリを動かした結果(コンテナのメモリ制限はなし)
| ディストリビューション | メモリ上限 | 発生した事象 |
|---|---|---|
| CentOS 6.6 x86_64 CentOS 7.0 x86_64 Ubuntu Server 14.04 x86_64 |
物理:無制限 SWAP:無制限 |
物理+SWAPの上限値までメモリを消費した時点でメモリ確保アプリのプロセスが停止された |
Dockerコンテナに対してメモリ使用量の制限を行わない場合は、ディストリビューションによる差異は発生せず、物理メモリ+SWAPを消費し尽くした時点で、ベースOSのOOM Killerにプロセスがkillされて終了した。ただ停止させられるプロセスはOOM Killerが自動で判断するため、このプロセスが確実にターゲットになる保証はない。OOM Killerは、メモリ枯渇が発生している時点でメモリ使用量が最大のプロセスを停止させる可能性が高く、RDBなどのメモリ使用量の高い他のプロセスが狙われるリスクも想定される。
システムを安定稼働させるためには、Dockerコンテナに対してメモリ使用量の制限を行うことが必要となる。
コンテナのメモリを1GBに制限した状態で大量にメモリ消費するアプリを動かす
上記と同一の実験を、今度はDockerコンテナの起動オプションでメモリを1GBに設定した状態で行った。
表9:コンテナ上でメモリを消費し続けるアプリを動かした結果(コンテナのメモリは1GBに制限)
| ディストリビューションメモリ上限 | 発生した事象 | |
|---|---|---|
| CentOS 6.6 x86_64 CentOS 7.0 x86_64 |
物理:1GB SWAP:1GB |
物理+SWAPの上限値までメモリを消費した時点でメモリ確保アプリのプロセスが停止された |
| Ubuntu Server 14.04 x86_64 | 物理:1GB SWAP:制限不可 |
物理+OSのSWAPの上限値までメモリを消費した時点でメモリ確保アプリのプロセスが停止された |
CentOS 6.6/7.0では物理メモリとSWAPを1GBずつ、計2GBを確保した時点で、やはりベースOSのOOM Killerにプロセスがkillされた。ところがUbuntuの場合は、SWAPの容量制限が行えないため、物理メモリ1GBとベースOSの全部のSWAPを消費するまで停止されなかった。SWAP領域を作成する場合、物理メモリ容量の50%~150%程度にすることが多い。昨今のメモリ搭載量の多い物理サーバではSWAP領域もかなり大きくなる傾向にあり、それが全て消費されるとなるとSWAPに依存する他のプロセスへの影響も大きくなる。何らかの対策を検討する必要があると考えられる。
プロセス数の制限をテストしてみる
大量のプロセスを生成する
各ディストリビューションのDocker上にコンテナを起動し、コンテナ上でfork関数により子プロセスを無制限に生成する実験を行った。実験に使用したテスト用の簡易プログラム(process_full.c)を、以下に示す。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_PROCESS_CNT 10
int main( int argc, char **argv )
{
int cnt = 0;
int max_cnt = MAX_PROCESS_CNT;
pid_t pid = 0;
if( argc > 1 ){
max_cnt = atol( argv[1] );
}
for( cnt = 1; cnt <= max_cnt; cnt++ ){
pid = fork();
if( pid < 0 ){
printf( "fork [%d]回目でエラー\n", cnt );
return( EXIT_FAILURE );
}
if( pid == 0 ){
printf( "fork [%d]回目 pid=[%d]\n", cnt, getpid() );
sleep( 30 );
exit( 0 );
}
}
return( EXIT_SUCCESS );
}
表10:コンテナ上でプロセスを大量に生成した結果
| ディストリビューション | プロセス数上限 | 発生した事象 |
|---|---|---|
| CentOS 6.6 x86_64 CentOS 7.0 x86_64 |
32,768 | 21,000~23,000件程度のプロセスが生成された時点で、コンテナごと停止された。プロセス数が多くなるとベースOSの挙動も不安定になる。ロードアベレージも通常ではありえない状態となる。ベースOSもほぼハングアップ状態となった。 |
| Ubuntu Server 14.04 x86_64 | 32,768 | 31,000件近くのプロセスが生成された時点で、プロセス生成アプリが停止された。大量のプロセス生成中は、他のプロセスもforkエラーを多発するために障害にリスクが存在する。 |
CentOS 7.0のOSの挙動
プロセス数が増えていくと、ベースOSのスローダウンが顕著になっていった。ロードアベレージも以下のような状況となる。
コンテナが停止した時点で、ベースOSの/var/log/messagesには以下のエラーログが出力されていた。
Apr 6 18:29:20 kernel-centos7 kernel: Out of memory: Kill process 906 (docker) score 1 or sacrifice child
Apr 6 18:29:20 kernel-centos7 kernel: Killed process 28108 (bash) total-vm:11744kB, anon-rss:0kB, file-rss:52kB
Ubuntu 14.04のOSの挙動
プロセス数自体は一気に30,000プロセス台まで増えて、ベースOSのスローダウンは発生しなかった。コンテナが停止した時点で、ベースOSの/var/log/messagesには以下のエラーログが出力されていた。
Apr 14 13:04:05 tissvr137 dnsmasq[24651]: cannot fork into background: メモリを確保できません
Apr 14 13:04:05 tissvr137 dnsmasq[24651]: FAILED to start up
また、この処理の実行中にCronなどの実行がエラーになるケースが見られた。
Apr 14 13:11:03 tissvr137 dnsmasq[25499]: read /opt/docker/container.hosts - 1 addresses
Apr 14 13:12:01 tissvr137 cron[1159]: (CRON) error (can't fork)
Apr 14 13:13:01 tissvr137 CRON[25546]: (root) CMD (/usr/sbin/service dnsmasq force-reload >/dev/null 2>&1)
Apr 14 13:13:01 tissvr137 CRON[25550]: (root) CMD (/opt/docker/get-containerip.sh > /opt/docker/container.hosts)
プロセス数のまとめ
Dockerコンテナで生成できるプロセス数の上限については、制限をかけることができず、このようにプロセスが無制限に生成される状況となった場合、ベースOSのスローダウンや障害など大きな影響が発生する可能性が高くなる。Dockerコンテナで生成できるプロセス数に、何らかの上限を設定する方法の検討が必要だと考えられる。
総括
今回の検証で、選択するストレージドライバによってファイルシステムの構成や機能に差異があることがわかった。ディスク容量やinode数の制限を考慮すると、device-mapperは非常に優秀である。一方aufsのようにベースOSからDockerコンテナのファイルを制御できる状態は、ファイルの配布や設定の一括変更など運用の利便性も考えられ、一概に問題があるともいえない。今回評価は行わなかったが、vfsやCoreOSが採用しているbrtfsなどについても今後、評価をしてみたい。
メモリ使用量に関しては基本的には、標準のパラメータで制限を行えるが、Ubuntuを使用する場合はコンテナのメモリ使用量の監視などで対応が必要である。
ディスクI/O速度、プロセス数上限に関しては現在、明確に制限を行うことができていない。
今後Dockerをマルチテナントのサーバ仮想化基盤として提供するためには、セキュリティーに加えて、Noisy Neighbor対策が必要となる。そのためには、Dockerコンテナに対して、現状の物理マシンやサーバ仮想化環境の仮想マシンに対して行えているリソース使用量の制限や監視(CPU、メモリ、ディスク容量、ディスクI/O速度、NIC I/O速度)に加えて、プロセス数やinode数などもその対象とすることが必要になると考えられる。
Docker本体のバージョンアップや専用OSの開発、周辺ツールの整備により、徐々にDocker環境も仮想化基盤として整備されつつある。今後の動向にも注目し、安定したDocker環境の構築を実現するために検証を続けていきたい。