kubernetes
ibmcloud

limitを使ったメモリ量制限時の振る舞いについて

「入門Kubernetes」の「5.7.2 limitsを使ったリソース利用量制限について」の章で、次の記述がありました。 本当にそうなるのかな? と疑問に思ったので確かめてみる事にしました。

256MBのメモリリソースの制限があるコンテナでは、メモリ使用量が256MBを越えると、それ以上のメモリを使用できなくなります。(例えば、mallocが失敗します)。

30126927_1683624661720799_8931795863999610880_n.jpg

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()で失敗した事を知ることができるはずです。

c
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の中にリソース制限に関わるものがありません。

pod-0.yaml
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メガバイトを指定します。

pod-2.yaml
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によると、メモリ制限を超えたコンテナは、強制終了すると書かれていました。