「入門Kubernetes」の「5.7.2 limitsを使ったリソース利用量制限について」の章で、次の記述がありました。 本当にそうなるのかな? と疑問に思ったので確かめてみる事にしました。
256MBのメモリリソースの制限があるコンテナでは、メモリ使用量が256MBを越えると、それ以上のメモリを使用できなくなります。(例えば、mallocが失敗します)。
malloc()が失敗して、NULLが戻されるのと、いきなりメモリ不足でキル(kill)されるとでは、問題判別方法も異なってきますから、それに、この種の問題は、プロジェクト現場で追求できないことも多いので、気づいた時に確認しておくと、後々に役立つ事もあると思います。そこで、現在、利用できる Kubernetes 1.9 では、limitの制限がある場合、どの様な振る舞いになるか、検証を進めます。
この「入門Kubernetes」は内容がコンパクトに纏まった内容の濃い本だと思います。本をディスっているわけではなくて、疑問に思ったら鵜呑みに信じないで、試してみるという、自分のいつもの勉強法なんです。
C言語 malloc()でメモリを確保するコンテナの開発
メモリ制限超過時の動作をシンプルに捉えるために、C言語のmalloc()システムコールを使ったコンテナをビルドして確かめる事にしました。
C言語のソースコードは、GitHub takara9/resource_eaterに置いてありますが、コンテナ内のプロセスが、メモリを確保する動作について記述しておきます。メモリ取得ループの中で、data_chain_tの型を確保して、リストとして繋げていきます。 そして、リスト中のデータのポインタ ptr_nextに、数メガバイトのメモリブロックを取得する形で、メモリを確保して、コンテナのメモリを増加させていきます。 malloc()システムコールで制限値までメモリを確保した時に、私が読んだ本の通りであれば、malloc()が失敗するとNULLが戻るので、メッセージを出力してコンテナが終了する事になります。 コンテナはポッドやデプロイメントを削除するまで、ログが残っていますから、malloc()で失敗した事を知ることができるはずです。
typedef struct data_chain {
void *ptr_data;
void *ptr_next;
} data_chain_t;
省略
int main( int argc, char *argv[] )
{
int i,j,limit,wait;
float sz;
data_chain_t *top = malloc(sizeof(data_chain_t));
data_chain_t *cur, *prv;
省略
// メモリ取得ループ
for(j=0,i=1; i < NUMOFBLK; i++) {
if ((i % 1000) == 0) {
printf("%6d %5.1f MB\n",i, sz);
sleep(wait);
}
cur->ptr_next = malloc(sizeof(data_chain_t));
if (cur->ptr_next == NULL) {
printf("Malloc Error at ptr_next"); <--- malloc()が失敗してNULLが戻されたら、STDOUTにメッセージして終了
exit(1);
}
j++;
cur->ptr_data = malloc(BLOCKSIZ);
if (cur->ptr_data == NULL) {
printf("Malloc Error at ptr_data"); <-- 上記同様
exit(1);
}
C言語のプログラムを動かすコンテナのビルド方法やDockerHubへの登録は、GitHub takara9/resource_eaterに書き残しておきたいと思います。
メモリ制限が無い場合の振る舞い
メモリ制限しないのケースと、制限ありケースの2ケースで比較します。 最初は、メモリ制限なしの場合をみていきます。 ポッドをデプロイするためのYAMLは以下になります。 spec.containersの中にリソース制限に関わるものがありません。
apiVersion: v1
kind: Pod
metadata:
name: memory-eater
spec:
containers:
- name: memory-eater
image: maho/memory_eater:0.1
次の実行結果では、右からループの回数(malloc()の回数)そして、プロセスが獲得したメモリ量です。一回のループで8192バイトを確保していきます。ループの1000回ごとにプリントしているので、端数が解りにくいですが、ループでの累積メモリ獲得量が、300MBを超えたら、解放を開始します。 次の結果では、limitが無いので次の様に、プログラムが要求するだけのメモリを確保することができました。
$ kubectl create -f pod-0.yaml
pod "memory-eater" created
$ kubectl logs -f memory-eater
1000 7.8 MB
2000 15.6 MB
3000 23.5 MB
4000 31.3 MB
5000 39.1 MB
6000 47.0 MB
7000 54.8 MB
8000 62.6 MB
省略
37000 289.6 MB
38000 297.4 MB <-- 最大値到達後に解放開始
1000 292.2 MB
2000 284.4 MB
3000 276.5 MB
4000 268.7 MB
5000 260.9 MB
6000 253.0 MB
省略
36000 18.2 MB
37000 10.4 MB
38000 2.6 MB
メモリ制限のあるケース
メモリの利用制限としては、次のYAMLの spec.containers.resources 以下にある通り、limitsに メモリ 128メガバイトを指定します。
apiVersion: v1
kind: Pod
metadata:
name: memory-eater-128
spec:
containers:
- name: memory-eater
image: maho/memory_eater:0.1
resources:
requests:
memory: "128Mi"
limits:
memory: "128Mi"
YAML作成時の参考資料 Assign Memory Resources to Containers and Pods
予測される結果としては、プロセスのメモリ獲得量が、128メガバイトを超えたところがで、malloc()失敗メッセージが表示されコンテナ終了となることが期待されます。しかし、結果は次の様になりました。 STDOUTに何も表示されず、終了(コマンドが復帰)しました。
$ kubectl create -f pod-2.yaml
pod "memory-eater-128" created
$ kubectl logs -f memory-eater-128
表示なしで終了
メッセージも表示せず止まった様に伺えますから、停止の原因を探っていきます。 次のコマンドの結果で、Last Stateの部分で、Reason: OOMKilled が表示されています。 この OOMKilled は、 Out Of Memory によって強制終了された事を表しています。
$ kubectl describe memory-eater-128
the server doesn't have a resource type "memory-eater-128"
$ kubectl describe pod memory-eater-128
Name: memory-eater-128
Namespace: default
Node: 10.132.253.30/10.132.253.30
Start Time: Fri, 06 Apr 2018 08:40:02 +0000
Labels: <none>
Annotations: <none>
Status: Running
IP: 172.30.184.126
Containers:
memory-eater:
Container ID: docker://504a3935dca2ae74ed47c1e26fde5552a4d351590c88dc58f4938d1603f5a329
Image: maho/memory_eater:0.1
Image ID: docker-pullable://maho/memory_eater@sha256:846aea00e33be613579c470f2bfd0a98c2a7254d0675708f7292a020510c6b5c
Port: <none>
State: Running
Started: Fri, 06 Apr 2018 08:40:20 +0000
Last State: Terminated
Reason: OOMKilled <<<------------- *** 注目 ***
Exit Code: 137
Started: Fri, 06 Apr 2018 08:40:03 +0000
Finished: Fri, 06 Apr 2018 08:40:19 +0000
Ready: True
Restart Count: 1
Limits:
memory: 128Mi
Requests:
memory: 128Mi
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-n2xgw (ro)
Conditions:
Type Status
Initialized True
Ready True
PodScheduled True
Volumes:
default-token-n2xgw:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-n2xgw
Optional: false
QoS Class: Burstable
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 33s default-scheduler Successfully assigned memory-eater-128 to 10.132.253.30
Normal SuccessfulMountVolume 33s kubelet, 10.132.253.30 MountVolume.SetUp succeeded for volume "default-token-n2xgw"
Normal Pulled 15s (x2 over 32s) kubelet, 10.132.253.30 Container image "maho/memory_eater:0.1" already present on machine
Normal Created 15s (x2 over 32s) kubelet, 10.132.253.30 Created container
Normal Started 15s (x2 over 32s) kubelet, 10.132.253.30 Started container
まとめ
プロセスがメモリ利用の制限値を超えた場合、メモリ残量無し(Out Of Memory)として、コンテナが強制終了されるという結果になりました。
強制終了する際に、その時点までにSTDOUTヘ出力されていると考えられるメッセージも一緒に消去されることから、ログを追いかけて原因を把握することも難しいと考えられます。
アプリケーションを長期間稼働させると、徐々にメモリの占有量増加していき、無駄にメモリ資源を占有し続け、システムの全体のパフォーマンスへ悪影響を与えることがあります。 このため、メモリが肥大化したプロセスが再起動する処置は適切なものと思います。Kubernetesもどんどん進化している最中のソフトウェアですから、参照した本が書かれた時期のバージョンと、現在のバージョンでは振る舞いが変わっているかもしれません。この様な振る舞いが異なることの詳細は追求していませんが、このレポートが、Kubernetesに関わる方の参考になればと思います。
補足
Assign Memory Resources to Containers and Pods のv1.9/v1.10によると、メモリ制限を超えたコンテナは、強制終了すると書かれていました。