「DockerはKernel共有する」とよく聞くけどそれによって何が起きるか調べてみた

# 前置き
* 「Dockerはkernelを共有する」。よく聞くフレーズだと思います。しかし具体的にカーネル共有するから何が起きるんでしょうか？
* コンテナはOSディストリビューションも分離できてるし、IPも分離できてるし、ファイルシステムも分離できてるし、で何を共有してるのか良くわかりません。
  * せいぜい「Linuxしか動かない」ぐらいにも思えます
  * 実際はそんなことはありません
* 実際に共有してるもので障害が起きたことがあるので、それを契機に調べてみました 

# 記事が長いので先に結論
* Linuxの持つ分離機能で分離されていないものがコンテナ同士やホストで共有されています
* 特にカーネルパラメータで上限設定されているものが共有されていると、コンテナ・ホスト間でリソースの奪い合いが起きます
    * 例として、「オープンできるファイル数の上限」や「ファイル変更監視の上限数」です
    * コンテナでそういったリソースを使い切ると、別コンテナやホスト側でコマンドが失敗する等の影響が出ます
 
* システム全体の上限を超えてコンテナが消費するケースについては回避が難しいと思います
  * あらかじめ上限を超えそうとわかっている場合はホストのカーネルパラメータを上げておくといいと思います 
  * どういうコンテナが動くかわかっている場合は、そのアプリのパラメーターチューニングでググって、ホストのパラメータを上げるといいと思います
 
* ユーザーごとに上限が設定されるような項目については、 緩和策としてコンテナの実行ユーザーを変えることが有効そうです
 

# 調査方針：引き算で考えてみる
* 何を共有しているか？ではなく、カーネルにどういう項目があって、そのうち何が分離されているか調べる方針で行きます。
  * 「カーネル全体」 － 「分離された項目」 = 「分離されていない（共有されている）項目」 
* とはいえカーネル全体だと膨大になることが想像できるので、先に分離されているものから見た後に、どんなカーネルパラメータがあるかを調べて見て、その中で分離されていなさそうなものを取り上げていきます。
  * 特に上限のあるものに着目してみます。コンテナ間で奪い合って競合が起きるからです
  * 他の視点もあればコメントください
* あたりを付けたら実際に共有されているものを奪い合うなどしてみて、何が起きるか観察します

# 分離されているものを調べる
* [Docker内部で利用されているLinuxカーネルの機能 (namespace/cgroups)](https://qiita.com/wellflat/items/7d62f2a63e9fcddb31cc)が良いので本記事では割愛

# カーネル全体にどういうものがあるのか調べる
* [man-pages](http://man7.org/linux/man-pages/index.html)
  * [「max」でgoogle検索](https://www.google.com/search?q=max&sitesearch=man7.org%2Flinux%2Fman-pages&sa=Search+online+pages)
* [nginx - カーネルパラメーターのチューニング](https://qiita.com/sion_cojp/items/c02b5b5586b48eaaa469)

# 見つけたコンテナ間で共有されてそうな項目
|    内容   |
|:----------------------------------|
| オープンできるファイルの最大数 |
| 変更監視するファイル数上限 |
| パケット受信時にキューに繋ぐことができるパケットの最大数 |

* このうち、検証が簡単そうな「オープンできるファイルの最大数」、「変更監視するファイル数上限」でホントに競合するか検証してみます

# 競合をおこしてみる検証
* 環境情報
  - VMPlayperの仮想マシンで構築
  - Linux ubuntu 4.4.0-134-generic
  - Docker version 18.05.0-ce, build f150324

## オープンできるファイルの最大数で競合を起こしてみる

### 初期状態チェック
|    項目   | 確認コマンド | 結果　　 　　|
|:-----------|:-----------|:-----------|
| 現在オープン中のファイル数 | lsof ｜ wc -l  | 26000 |
| 最大オープン数 | cat /proc/sys/fs/file-max | 808880 |

### やってみる
#### 手順
1. 競合しやすいように上限を減らします

    ```bash
echo 100000 > /proc/sys/fs/file-max
    ```
2. ターミナルを2つ開き、コンテナを2つ立てます

    ```bash:ターミナル1
docker run -it centos bash
    ```
    ```bash:ターミナル2
docker run -it centos bash
    ```
3. 2つのコンテナ内でファイルを大量(50000個)に作ります

    ```bash
mkdir files
touch files/file{1..50000}
    ```

4. 2つのコンテナ内にファイルをopenするためのスクリプト`open-files.py`を置きます
    
    ```python:open-files.py
    import time
    numList = range(1, 50000)
    def open_files(n):
        return open('files/file' + str(n),'w')
    fileList = map(open_files, numList)
    time.sleep(60)
    ```

5. コンテナ1で`python open-files.py`を実行
    * この時点ではまだ上限は超えないのでエラーにはなりません
    * hostで`lsof`するとオープンしているファイル数がコンテナ内でオープンした50000個分増えているのがわかります
        * システムが裏側でファイル操作してたりするので若干の誤差はあります

    ```bash:hostで実行
root@ubuntu:~# lsof | wc -l
76055
    ```
6. コンテナ2で`python open-files.py`を実行します

    ```bash
[root@d18f088f96c6 /]# python open-files.py 
Traceback (most recent call last):
  File "open-files.py", line 8, in <module>
    fileList = map(open_files, numList)
  File "open-files.py", line 6, in open_files
    return open('files/file' + str(n),'w')
IOError: [Errno 23] Too many open files in system: 'files/file42987'
    ```
    * オープンできるファイル数上限を超えたためにエラー。コンテナ1のせいでコンテナ2がファイルオープンできません

#### 分かったこと
* オープンできるファイル数には上限があります
* 上限はコンテナ・ホスト全体のオープン数の合計にかかります
  *  そのため、どれか1つのコンテナが大量にオープンすると他のコンテナやホストでファイルオープンできない事態が発生します


## 変更監視するファイル数上限 で競合を起こしてみる
* コンテナAで大量のファイル変更監視をすればこの項目を枯渇させることができ、コンテナBやホストでファイル変更監視できないという事態を起こせそうです
* この項目はシステム全体上限だけでなく、ユーザー単位でも上限があります
  * 参考：[lsyncdで上限ファイル数を超えた時の対処策](http://wadap.hatenablog.com/entry/20100123/1264211202) 
  
### 初期状態チェック
|    項目   | 確認できるファイル | 結果　　 　　|
|:-----------|:-----------|:-----------|
| inotify インスタンスキューに入る最大イベント数 | /proc/sys/fs/inotify/max_queued_events | 16384 |
| ユーザーごとのinotify インスタンスの数の上限 | /proc/sys/fs/inotify/max_user_instances | 128 |
| ユーザーごとの作成可能な監視対象の数の上限 |/proc/sys/fs/inotify/max_user_watches| 524288 |

* inotifyというのは、ファイル変更監視に用いられる Linuxのシステムコールです
* 今回は「ユーザーごとの作成可能な監視対象の数の上限」を100に下げてみて競合するのを確認してみます
  * dockerのデフォルトではrootユーザーがコンテナ内外で共通のため、rootでやれば競合するはずです 

### やってみる
* 最初はコンテナ内で`tail -f`することで挑戦しましたが、うまくいきませんでした
  * 原因が気にはなりますが、そこの調査は目的ではないので今回はパスします 
* docker内でcreate-react-appでセットアップした後に`npm run start`で監視してみます
  * reactのアプリを作る際にデバッグ用サーバーが立ち、ソース変更するとすぐにサーバーに反映される、というコマンドです 
  * もうちょっと自然な検証がないか検討中。。

#### 手順
1. nodejs dockerコンテナを起動

    ```bash:hostで実行
root@ubuntu:~# docker run -it --rm --name node node bash
root@1c7afe16ce2c:/# 

    ```
2. create-react-appでプロジェクトセットアップ

    ```bash:コンテナで実行
root@1c7afe16ce2c:/# npm install -g create-react-app
    ```
    ```bash:コンテナで実行
root@1c7afe16ce2c:/# create-react-app test
    ```

3. test/srcディレクトリにファイルを1000個生成

    ```bash:コンテナで実行
root@1c7afe16ce2c:/test# touch src/file{1..1000}.txt
    ```

4. ファイル変更監視の上限変更
   
    ```bash:hostで実行
root@ubuntu:~# echo 100 > /proc/sys/fs/inotify/max_user_watches
   ```
    * ちなみに、コンテナの中で上限を確認しても確かに変更されています
    
    ```bash:コンテナで実行
root@1c7afe16ce2c:/test# cat /proc/sys/fs/inotify/max_user_watches
100
    ```

5. コンテナの中でファイル変更監視する前はhostで`tail -f`実行してもエラーを出さないことを確認
   
    ```bash:hostで実行
root@ubuntu:~# tail -f file_on_host 
^C
    ```

6. コンテナの中でファイル変更監視を実行
    
    ```bash:コンテナで実行
root@1c7afe16ce2c:/test# npm run start
    ```
    * この時点で上限100を超えたファイル変更監視をしてるはずですが、コンテナ内ではエラーは出ていません
      * おそらくポーリング等で回避していて、エラーメッセージを出していないだけだと思われます
       
7. hostで`tail -f`を実行。エラーが出る
   
    ```bash:hostで実行
root@ubuntu:~# tail -f file_on_host 
tail: inotify 資源を使い果たしました
tail: inotify を使用できません。ポーリングに戻ります
    ```
    * tailの場合はポーリングで回避していますが、アプリによってはエラーで終了してしまいます

#### 分かったこと
* ファイル変更監視できる数には上限があります
* 上限はシステム全体にかかるものと、ユーザーごとにかかるものがあります
* 同じユーザーで動いているコンテナやホストでは、片方がファイル変更監視数を使い切るともう片方のコンテナまたはホストでファイル変更監視に失敗します
    * アプリによっては、変更監視に失敗してもポーリング等で回避してくれます
    

# まとめ
* 結論は冒頭で述べた通りです
* 実際に検証して競合が起きることを確認できました

# おまけ - この記事を書くきっかけとなった障害について
* 2つ目に検証したファイル変更監視で起きました。
* Kubernetes環境です
* デバッグモードでwebサーバのコンテナを起動した結果、コンテナ内のwwwroot配下の大量ファイルに変更監視が走りました。
* その結果ホストでファイル変更監視ができない状態になりました。
  * その後しばらく（数日）してから、なぜかkubeletが落ちてしまい、systemdで再起動しようとしました。(ここの真相は追えてないです)
  * kubeletが設定ファイルに変更監視をかけられず、再起動しようとしても起動できない状態でした
* という障害でした