大きなコンテナイメージをKubernetesで使うためにエラーの対処法から構成を考えてみたメモ

# はじめに
自然言語処理とか画像認識の機械学習向けのKubernetes環境にPython（Anacondaから構築した環境）のコンテナを使用する場合があると思います。処理のためのモデルやデータを含めてコンテナ化することがあると思いますが、7GぐらいのコンテナになってしまうとKubernetesのPull時に必ずエラーになるので、AKS（Azure Kubernetes Service）を使って対処法を検証してみました。（エラーメッセージは出ますが最終的にはPull完了しますします）

# エラーメッセージの対処法を考える
例えば以下の様なエラーメッセージ。

```
Failed to pull image "kekekekenta.azurecr.io/nlp-tools": rpc error: code = FailedPrecondition desc = unexpected EOF
```

kubernetesのgithubやstackoverflowを探しても色々な人が悩みを持っているようですね。それらをまとめると以下のような案がありました。

- Pullのタイムアウトを長くすることができるか？
- Pullの速度（ダウンロードと展開）を速くすることができるか？
 - ネットワークは速いので展開時の時間を短くするために圧縮アルゴリズムを変更したい。

## Pullのタイムアウトを長くすることができるか？
Pull時のタイムアウトは以下のURLにも書かれていますが、デフォルト設定で2分になっています。
https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/

--runtime-request-timeout duration
> Timeout of all runtime requests except long running request - pull, logs, exec and attach. When timeout exceeded, kubelet will cancel the request, throw out an error and retry later. (default 2m0s)

AKSの実際のタイムアウトは何分に設定されているか不明ですが、Azure Container Service EngineのGithubでは以下のように書かれていました。

https://github.com/Azure/acs-engine/blob/master/parts/k8s/kubernetesconfigs.sh

> setKubeletOpts " --container-runtime=remote --runtime-request-timeout=15m --container-runtime-endpoint=unix:///run/containerd/containerd.sock"

どちらにしても、提供されているものを使っている限りはこの設定は変更できないので別の方法を探します。

## Pullの速度（ダウンロードと展開）を速くすることができるか？
Pullの速度は、コンテナイメージのダウンロードと、tar.gzされたコンテナイメージのレイヤーを展開する測度が関係してきますが、ACR（Azure Container Registry）からのダウンロードはほぼ気にならないぐらい速いので、展開する方を速くできないか考えてみます。Githubでも色々な案が出ていましたが、現段階での実装でなんとかならないかと思い、gzipの測度を圧縮レベルを変えて測定してみます。

試しに/usrをtarしたファイルを2つ用意します。

```shell-session

$ ls -la

-rw-rw-r-- 1 kenta kenta 1114408960 Jan  5 01:50 usr1.tar
-rw-rw-r-- 1 kenta kenta 1114408960 Jan  5 01:52 usr9.tar
```

gzipする時の時間は遅くても問題ないのですが、とりあえず計測してみます。 -1は圧縮率が低いけど高速。-9は圧縮率が高いけど低速です。

```shell-session

$ time gzip -1 usr1.tar 

real	0m19.791s
user	0m19.091s
sys	0m0.696s
$ time gzip -9 usr9.tar 

real	3m47.467s
user	3m46.716s
sys	0m0.747s
```

圧縮したファイルをtarで展開してみますが、そんなに変りませんね。。

```shell-session

$ time tar xfz usr1.tar.gz 

real	0m21.931s
user	0m10.419s
sys	0m4.389s
$ time tar xfz usr9.tar.gz 

real	0m20.804s
user	0m9.375s
sys	0m4.292s
```

別の方法を考えて見ます。

# コンテナイメージのレイヤーを複数に分割する
コンテナイメージを作る際はイメージサイズを小さくするためにレイヤーを纏めた方が良いとされていますが、レイヤーを纏めると１つのレイヤーの展開に時間がかかります。また、並列ダウンロードもされません。また、今回はRPCからエラーがでている事もあり、もっとレスポンスの良いコンテナイメージにしてあげる必要があるかもしれません。
そこで、レイヤーを纏めたコンテナイメージと、複数のレイヤーに分けたコンテナイメージを作成して、Pull時の動作を確認することにより、KubernetesのPullに適したコンテナ構造を検証します。

## コンテナイメージを用意

レイヤーを纏めたDockerfile（1つのRUNで纏める）を用意しました。色々インストールしています。

```dockerfile:nlp-tools/Dockerfile

FROM continuumio/anaconda3
RUN buildDeps='make libc6-dev gcc g++' \
    && set -x \
    && echo 'Installing Mecab' \
    && apt-get update && apt-get install -y $buildDeps --no-install-recommends \
    && apt-get -y install mecab libmecab-dev mecab-ipadic mecab-ipadic-utf8 \
    && mkdir -p `mecab-config --dicdir` \
    && git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git \
    && /mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -y \
    && export VAL=`mecab-config --dicdir`/mecab-ipadic-neologd; sed -i 's#^\(dicdir\s*=\s*\).*$#\1'$VAL'#' /etc/mecabrc \
    && echo 'Installing Tika' \
    && apt-get -y install openjdk-8-jre-headless tesseract-ocr tesseract-ocr-eng tesseract-ocr-jpn \
    && curl --output /tmp/tika-server.jar http://search.maven.org/remotecontent?filepath=org/apache/tika/tika-server/1.16/tika-server-1.16.jar \
    && curl --output /tmp/tika-server.jar.md5 http://search.maven.org/remotecontent?filepath=org/apache/tika/tika-server/1.16/tika-server-1.16.jar.md5 \
    && echo 'Installing Python' \
    && apt-get install -y swig \
    && conda update -y -n base conda \
    && conda install -y -c anaconda gensim requests nltk \
    && conda install -y -c conda-forge tika \
    && pip install tinysegmenter sumy mecab-python3 langdetect \
    && rm -rf /mecab-ipadic-neologdclone \
    && rm -rf /var/lib/apt/lists/*
```

複数のレイヤーに分割したDockerfile（複数のRUNで分割する）は以下の通り。

```dockerfile:layered-nlp-tools/Dockerfile

FROM continuumio/anaconda3
RUN buildDeps='make libc6-dev gcc g++' \
    && set -x \
    && echo 'Installing Mecab' \
    && apt-get update && apt-get install -y $buildDeps --no-install-recommends
RUN apt-get -y install mecab libmecab-dev mecab-ipadic mecab-ipadic-utf8 \
    && mkdir -p `mecab-config --dicdir` \
    && git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
RUN /mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -y \
    && export VAL=`mecab-config --dicdir`/mecab-ipadic-neologd; sed -i 's#^\(dicdir\s*=\s*\).*$#\1'$VAL'#' /etc/mecabrc
RUN echo 'Installing Tika' \
    && apt-get -y install openjdk-8-jre-headless tesseract-ocr tesseract-ocr-eng tesseract-ocr-jpn \
    && curl --output /tmp/tika-server.jar http://search.maven.org/remotecontent?filepath=org/apache/tika/tika-server/1.16/tika-server-1.16.jar \
    && curl --output /tmp/tika-server.jar.md5 http://search.maven.org/remotecontent?filepath=org/apache/tika/tika-server/1.16/tika-server-1.16.jar.md5
RUN echo 'Installing Python' \
    && apt-get install -y swig \
    && conda update -y -n base conda \
    && conda install -y -c anaconda gensim requests nltk \
    && conda install -y -c conda-forge tika
RUN pip install tinysegmenter sumy mecab-python3 langdetect \
    && rm -rf /mecab-ipadic-neologdclone \
    && rm -rf /var/lib/apt/lists/*
```

ビルドします。

```shell-session

$ docker build -t kekekekenta.azurecr.io/nlp-tools nlp-tools
$ docker build -t kekekekenta.azurecr.io/layered-nlp-tools layered-nlp-tools
```

ビルド後のコンテナイメージサイズは以下の通り。どちらも同じぐらいのサイズ。

```shell-session

$ docker images
REPOSITORY                                TAG                 IMAGE ID            CREATED             SIZE
kekekekenta.azurecr.io/nlp-tools          latest              370bb5c6837e        4 hours ago         7.17GB
kekekekenta.azurecr.io/layered-nlp-tools   latest              cff610a7e062        4 hours ago         7.17GB
continuumio/anaconda3                     latest              69acfdf1121f        41 hours ago        3.72GB
```

便利なdiveでレイヤーを確認。Anacondaのレイヤーは3.4G。大丈夫かな。。。
https://github.com/wagoodman/dive

```shell-session

$ dive kekekekenta.azurecr.io/nlp-tools
[● Layers]────────────────────────────────────────────────────────────────────── [Current Layer Contents]────────────────────────────────────────────────────────
Cmp Image ID                     Size  Command                                   Permission     UID:GID       Size  Filetree
    sha256:b28ef0b6fef80faa25  101 MB  #(nop) ADD file:58d5c21fcabcf1eec94e8676a drwxr-xr-x         0:0     5.2 MB  ├── bin
    sha256:60b398413f86c0f8ac  231 MB  apt-get update --fix-missing && apt-get i -rwxr-xr-x         0:0     1.1 MB  │   ├── bash
    sha256:b1f721b57648924150  3.4 GB  wget --quiet https://repo.anaconda.com/ar -rwxr-xr-x         0:0      36 kB  │   ├── cat
    sha256:abd2c1804296f74fae   20 MB  apt-get update --fix-missing  && apt-get  -rwxr-xr-x         0:0      64 kB  │   ├── chgrp
    sha256:aac6d3b7d1450a4434  3.4 GB  FROM sha256:aac6d3b7d1450a4434            -rwxr-xr-x         0:0      60 kB  │   ├── chmod
....

$ dive kekekekenta.azurecr.io/layered-nlp-tools
[● Layers]────────────────────────────────────────────────────────────────────── [Current Layer Contents]────────────────────────────────────────────────────────
Cmp Image ID                     Size  Command                                   Permission     UID:GID       Size  Filetree
    sha256:b28ef0b6fef80faa25  101 MB  #(nop) ADD file:58d5c21fcabcf1eec94e8676a drwxr-xr-x         0:0     5.2 MB  ├── bin
    sha256:60b398413f86c0f8ac  231 MB  apt-get update --fix-missing && apt-get i -rwxr-xr-x         0:0     1.1 MB  │   ├── bash
    sha256:b1f721b57648924150  3.4 GB  wget --quiet https://repo.anaconda.com/ar -rwxr-xr-x         0:0      36 kB  │   ├── cat    
    sha256:abd2c1804296f74fae   20 MB  apt-get update --fix-missing  && apt-get  -rwxr-xr-x         0:0      64 kB  │   ├── chgrp              
    sha256:546ef8f4ba85d10b69  154 MB  buildDeps='make libc6-dev gcc g++'     && -rwxr-xr-x         0:0      60 kB  │   ├── chmod         
    sha256:0cabb03f966e749806  282 MB  apt-get -y install mecab libmecab-dev mec -rwxr-xr-x         0:0      64 kB  │   ├── chown 
    sha256:7abb783748c90e7e13  2.5 GB  /mecab-ipadic-neologd/bin/install-mecab-i -rwxr-xr-x         0:0     130 kB  │   ├── cp              
    sha256:30242b13e105ca7ba4  293 MB  echo 'Installing Tika'     && apt-get -y  -rwxr-xr-x         0:0     117 kB  │   ├── dash 
    sha256:752f8655a9f8c5b758  245 MB  echo 'Installing Python'     && apt-get i -rwxr-xr-x         0:0     105 kB  │   ├── date            
    sha256:89e71c7f05b61effb0  5.9 MB  FROM sha256:89e71c7f05b61effb0            -rwxr-xr-x         0:0      77 kB  │   ├── dd    
....
```

ACRに登録します。

```shell-session

$ az acr login -n kekekekenta
$ docker push kekekekenta.azurecr.io/nlp-tools
$ docker push kekekekenta.azurecr.io/layered-nlp-tools
```

## 初回のapply動作
AKSにapplyしてみます。（yamlファイルは省略しますがKubernetesにはJobとして展開しています。また、それぞれのコンテナで同じレイヤーを使っている箇所があるので、キャッシュされたレイヤーが使われないように異なるノードに展開します。）

### レイヤーを纏めたコンテナイメージ
nlp-tools（レイヤーを纏めたコンテナイメージ）の初回Apply後の経過は以下の通り。エラーは出ますが最終的に約19分でJob動作完了。

```shell-session

$ kubectl apply -f nlp.yaml
job.batch/nlp created
$ kubectl get pods -o wide -w
NAME        READY   STATUS              RESTARTS   AGE   IP       NODE                       NOMINATED NODE   READINESS GATES
nlp-9thhn   0/1     ContainerCreating   0          14s   <none>   aks-agentpool-35064155-0   <none>           <none>
nlp-9thhn   0/1   ErrImagePull   0     2m6s   10.244.1.12   aks-agentpool-35064155-0   <none>   <none>
nlp-9thhn   0/1   ImagePullBackOff   0     2m22s   10.244.1.12   aks-agentpool-35064155-0   <none>   <none>
nlp-9thhn   0/1   ContainerCreating   0     10m   10.244.1.12   aks-agentpool-35064155-0   <none>   <none>
nlp-9thhn   0/1   Completed   0     19m   10.244.1.12   aks-agentpool-35064155-0   <none>   <none>

$ kubectl describe pods nlp-9thhn
....
Events:
  Type     Reason     Age                From                               Message
  ----     ------     ----               ----                               -------
  Normal   Scheduled  33m                default-scheduler                  Successfully assigned default/nlp-9thhn to aks-agentpool-35064155-0
  Warning  Failed     31m                kubelet, aks-agentpool-35064155-0  Failed to pull image "kekekekenta.azurecr.io/nlp-tools": rpc error: code = FailedPrecondition desc = unexpected EOF
  Warning  Failed     31m                kubelet, aks-agentpool-35064155-0  Error: ErrImagePull
  Normal   BackOff    31m                kubelet, aks-agentpool-35064155-0  Back-off pulling image "kekekekenta.azurecr.io/nlp-tools"
  Warning  Failed     31m                kubelet, aks-agentpool-35064155-0  Error: ImagePullBackOff
  Normal   Pulling    31m (x2 over 33m)  kubelet, aks-agentpool-35064155-0  pulling image "kekekekenta.azurecr.io/nlp-tools"
  Normal   Pulling    22m                kubelet, aks-agentpool-35064155-0  pulling image "kekekekenta.azurecr.io/nlp-tools"
  Normal   Pulled     14m                kubelet, aks-agentpool-35064155-0  Successfully pulled image "kekekekenta.azurecr.io/nlp-tools"
  Normal   Created    14m                kubelet, aks-agentpool-35064155-0  Created container
  Normal   Started    14m                kubelet, aks-agentpool-35064155-0  Started container
```

### 複数レイヤーに分割したコンテナイメージ
layered-nlp-tools（複数レイヤーに分割したコンテナイメージ）の初回apply後の経過は以下の通り。エラーは出ずに約8分でJob動作完了。

```shell-session

$ kubectl apply -f layerednlp.yaml
job.batch/layerednlp created
$ kubectl get pods -o wide -w
NAME               READY   STATUS              RESTARTS   AGE   IP       NODE                       NOMINATED NODE   READINESS GATES
layerednlp-ngkng   0/1     ContainerCreating   0          13s   <none>   aks-agentpool-35064155-2   <none>           <none>
layerednlp-ngkng   0/1   Completed   0     8m17s   10.244.0.12   aks-agentpool-35064155-2   <none>   <none>

$ kubectl describe pods layerednlp-ngkng
....
Events:
  Type    Reason     Age    From                               Message
  ----    ------     ----   ----                               -------
  Normal  Scheduled  11m    default-scheduler                  Successfully assigned default/layerednlp-ngkng to aks-agentpool-35064155-2
  Normal  Pulling    11m    kubelet, aks-agentpool-35064155-2  pulling image "kekekekenta.azurecr.io/layered-nlp-tools"
  Normal  Pulled     3m47s  kubelet, aks-agentpool-35064155-2  Successfully pulled image "kekekekenta.azurecr.io/layered-nlp-tools"
  Normal  Created    3m18s  kubelet, aks-agentpool-35064155-2  Created container
  Normal  Started    3m18s  kubelet, aks-agentpool-35064155-2  Started container
```

複数レイヤーに分割したコンテナイメージの方は、レイヤーを纏めたコンテナイメージと比べて、エラーも出ず、素早くPull完了していますね。

## 2回目以降のapplyの動作
２回目以降のapplyはキャッシュされているコンテナイメージを使うので、それぞれ約2秒で完了しています。

### レイヤーを纏めたコンテナイメージ

```shell-session

$ kubectl apply -f nlp.yaml
job.batch/nlp created
$ kubectl get pods -o wide -w
NAME        READY   STATUS      RESTARTS   AGE   IP            NODE                       NOMINATED NODE   READINESS GATES
nlp-zzl4s   0/1     Completed   0          28s   10.244.1.16   aks-agentpool-35064155-0   <none>           <none>
$ kubectl describe pods nlp-zzl4s
....
Events:
  Type    Reason     Age   From                               Message
  ----    ------     ----  ----                               -------
  Normal  Scheduled  53s   default-scheduler                  Successfully assigned default/nlp-zzl4s to aks-agentpool-35064155-0
  Normal  Pulling    52s   kubelet, aks-agentpool-35064155-0  pulling image "kekekekenta.azurecr.io/nlp-tools"
  Normal  Pulled     52s   kubelet, aks-agentpool-35064155-0  Successfully pulled image "kekekekenta.azurecr.io/nlp-tools"
  Normal  Created    51s   kubelet, aks-agentpool-35064155-0  Created container
  Normal  Started    51s   kubelet, aks-agentpool-35064155-0  Started container
```

### 複数レイヤーに分割したコンテナイメージ

```shell-session

$ kubectl apply -f layerednlp.yaml
job.batch/layerednlp created
$ kubectl get pods -o wide -w
NAME               READY   STATUS      RESTARTS   AGE   IP            NODE                       NOMINATED NODE   READINESS GATES
layerednlp-9p478   0/1     Completed   0          13s   10.244.0.13   aks-agentpool-35064155-2   <none>           <none>
$ kubectl describe pods layerednlp-9p478
....
Events:
  Type    Reason          Age    From                               Message
  ----    ------          ----   ----                               -------
  Normal  Scheduled       2m23s  default-scheduler                  Successfully assigned default/layerednlp-9p478 to aks-agentpool-35064155-2
  Normal  Pulling         2m22s  kubelet, aks-agentpool-35064155-2  pulling image "kekekekenta.azurecr.io/layerd-nlp-tools"
  Normal  Pulled          2m21s  kubelet, aks-agentpool-35064155-2  Successfully pulled image "kekekekenta.azurecr.io/layerd-nlp-tools"
  Normal  Created         2m21s  kubelet, aks-agentpool-35064155-2  Created container
  Normal  Started         2m21s  kubelet, aks-agentpool-35064155-2  Started container
```

# まとめ
機械学習系のコンテナイメージをKubernetesに展開する際は、なるべく小さいサイズのレイヤーに分けた方が良さそうですね。

もしくは、もっと細かくコンテナを分けた方が良いかも知れません。Pullされた後にセットアップするようなコンテナでも良いですね。

では。メモでしたー。

# 参照先

https://github.com/kubernetes/kubernetes/blob/39529006f0f47a105f6c08371df11be00726378a/pkg/kubelet/kubelet.go#L315
https://github.com/moby/moby/issues/1266
https://github.com/moby/moby/pull/34610