kubectl top pod と docker stats メモリー使用量の違い

## はじめに
docker statsコマンドとkubectl top podコマンドで表示されるメモリー使用量に差異があることに疑問を持った方もいるのではないでしょうか？

その差は、kubectl top コマンドのみ、メモリー使用量にPage Cache(active)が含まれているためらしいのですが、検証も含め少し深掘りしたいと思います。

## kubectl topのメモリー使用量

kubectl top コマンドで取得できるメモリー使用量は、各Nodeのkubelet(内のcAdviser)が収集した値です。
ここで、メモリー使用量と言っても具体的に何の指標を利用しているのでしょうか？

[公式ドキュメント](https://kubernetes.io/docs/tasks/debug-application-cluster/resource-metrics-pipeline/#measuring-resource-usage)を見ると、以下の記載があります。
```Memory is reported as the working set, in bytes, at the instant the metric was collected.```

ふむふむ、``` working set ```らしいです。

少し飛ばして、以下のような記載があります。
``` However, calculation of the working set varies by host OS, and generally makes heavy use of heuristics to produce an estimate. It includes all anonymous (non-file-backed) memory since kubernetes does not support swap. The metric typically also includes some cached (file-backed) memory, because the host OS cannot always reclaim such pages.
```

``` includes some cached (file-backed) memory```とあるので、cacheも含まれてそうですね。

### Working setってなんぞ？
ところで、```working set```って、分かるような分からない指標です。
[Kubernetes issue](https://github.com/kubernetes/kubernetes/issues/43916)を見ると、working setの計算式は以下の通りのようです。
``` $cgroupfs/memory.usage_in_bytes - total_inactive_file ```

さらに同ページにて、```cgroupfs/memory.usage_in_bytes```の詳細もkernel.orgのドキュメントから引用されています。
```All mapped anon pages (RSS) and cache pages (Page Cache) are accounted.```

まとめると、working setの計算式は、以下のようになります。
```RSS + Page Cache - total_inactive_file```

## docker statsのメモリー使用量

一方、docker statsコマンドのメモリー使用量については、[公式ドキュメント](https://docs.docker.com/engine/reference/commandline/stats/)に以下の記載があります。

```Note: On Linux, the Docker CLI reports memory usage by subtracting page cache usage from the total memory usage. ```

Linuxおいては、memory usageからpage cacheを引いた値を算出しているようです。

## まとめると？

kubectl top および　docker statsのメモリー使用量の計算式は、おそらく以下のようになるはずです。

kubectl top : ```RSS + Page Cache - total_inactive_file```
docker stats: ```RSS - Page Cache```

activeなfile cacheがkubectl topでカウントされているので、差異があるということのようです。

## 検証してみる

file cacheで差異がでるってことは、大きいサイズのファイルをreadすれば、kubectl topのみメモリー使用量が増えるはずです。
ということで検証してみます。

### 環境
検証で利用する環境は、以下の通りです(AWS EKSを利用しています)。

OS: Amazon linux 2 (RedHat/CoreOSベース)
kubernetes version: 1.14
kubectl version : v1.17.0
docker version: 18.06.1-ce

### 前準備 - kubelet insecure portの解放

kubeletが収集した結果も参照したいので、kubeletのinsecure port(認証なしでAPI実行可能なport)を解放します。


```console:worker_node上で実行
$ vim sudo vi /etc/kubernetes/kubelet/kubelet-config.json
  <省略>
  "maxPods": 35,
  "readOnlyPort": 10255  <- 追加
}
$ sudo systemctl restart kubelet
$ sudo systemctl status kubelet
● kubelet.service - Kubernetes Kubelet
   Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; vendor preset: disabled)
  Drop-In: /etc/systemd/system/kubelet.service.d
           └─10-kubelet-args.conf, 30-kubelet-extra-args.conf
   Active: active (running) since 月 2020-02-03 06:52:35 UTC; 19s ago
```

### ダミーPodの作成
linuxコマンドを実行可能なPodを作成します。

```console
$ kubectl run busybox --image busybox --restart Never --command sleep 99999999
pod/busybox created

$ kubectl get pod
NAME                             READY   STATUS    RESTARTS   AGE
busybox                          1/1     Running   0          5s
```

### ダミーファイルの作成
ダミーPodへログインし、１００MBのファイルを作成します。

```console
$ kubectl exec -it busybox /bin/sh

以下、コンテナ内で実行
# dd if=/dev/zero of=file100 bs=1M count=100
# ls -lh
total 100M
drwxr-xr-x    2 root     root       12.0K Dec 23 19:21 bin
drwxr-xr-x    5 root     root         360 Feb  3 05:05 dev
drwxr-xr-x    1 root     root          66 Feb  3 05:05 etc
-rw-r--r--    1 root     root      100.0M Feb  3 05:12 file1
<以下、省略>
```

### メモリー使用量の計測(ファイル読み込み前)
ファイルを読み込む前に、メモリー使用量を計測しておきます。
以下の通り、使用量は1M程度と微量です。

```console:kubectl_top
$ kubetl top po
NAME                                CPU(cores)   MEMORY(bytes)
busybox                             0m           1Mi
```

```console:docker_stats(WorkerNode上で実行)
$ docker stats --no-stream db369d0c34bd
  CONTAINER ID        NAME                    CPU %      MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
  db369d0c34bd        k8s_POD_busybox_<省略>   0.00%      620KiB / 7.69GiB    0.01%               0B / 0B             0B / 0B             1
```

```console:kubelet_api(WorkerNode上で実行)
$ curl http://localhost:10255/stats/summary
<省略>
  "memory": {
     "time": "2020-02-03T05:12:23Z",
     "usageBytes": 798720,
     "workingSetBytes": 798720,
     "rssBytes": 102400,
     "pageFaults": 1459,
     "majorPageFaults": 0
  },
<省略>	
```

### メモリー使用量の計測(ファイル読み込み後)
コンテナ上で、100MBのファイルをlessコマンドで読み込みます。

```console
# less file100
```

読み込み後のメモリー計測結果が以下の通りです。
kubectl topコマンド結果およびkubelet apiのworking setの値が大きくなっていることがわかります。
一方、docker statsコマンドには変化がありません。

また、kubelet apiのレスポンスをみると、rssは小さいままなので、working setにはactivなpage cacheが含まれていることが、推察されます。

```console:kubectl_top
$ kubetl top po
NAME                                CPU(cores)   MEMORY(bytes)
busybox                             0m           101Mi      <- 1Mが101Mになった
```

```console:docker_stats(WorkerNode上で実行)
$ docker stats --no-stream db369d0c34bd                                                  
CONTAINER ID        NAME                    CPU %      MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
  db369d0c34bd      k8s_POD_busybox_<省略>   0.00%      620KiB / 7.69GiB    0.01%               0B / 0B             0B / 0B             1
```

```console:kubelet_api(WorkerNode上で実行)
$ curl http://localhost:10255/stats/summary
<省略>
  "memory": 
    "time": "2020-02-03T05:30:57Z",
    "usageBytes": 105971712,
    "workingSetBytes": 105963520,    <-サイズが大きくなった
    "rssBytes": 106496,　　　　　　　　 <-サイズは(ほぼ)変化なし
    "pageFaults": 32905,
    "majorPageFaults": 0
  },
<省略>
```

##参考

[kubelet api soruce](https://github.com/kubernetes/kubernetes/blob/4a6935b31fcc4d1498c977d90387e02b6b93288f/pkg/kubelet/server/stats/handler.go)

[kubelet config file](https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/)

[kubelet config struct](https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubelet/config/v1beta1/types.go)