Real Allocatable Resources on Kubernetes Nodes


## Overview

Kubernetes では、起動したノードのリソース (CPU/Memory) を全て使えるわけではありません。ノードで必須となる Container Runtime / [kubelet](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/) / [kube-proxy](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/) に加え、名前空間 kube-system 配下で起動する [DNS](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/) 等のアドオンがあります。

アドオンによるリソース占有は、ある程度の規模のクラスタであれば無視できる率ですが、小規模クラスタの場合、不要なアドオンを起動したままにしていると Kubernetes を動かすために大半のコストをかける羽目にもなります。

Google Kubernetes Engine を例にして、デフォルトで設定されるアドオンがどれくらいのリソースを占有しているかを調べ、カスタマイズを通じて理解を深めたいと思います。

## How to Inspect Allocated Resources

`kubectl describe nodes` でノードの詳細情報が得られます。どの Pod がどのくらいのリソースを占有しているかを確認できます。

```
$ kubectl describe nodes | grep 'Allocated resources:' -A 4
```

コンテナごとのリソース要求 `spec.containers[].resources.requests` を知るには `kubectl get pods` で確認できます。好みの [go-template](https://golang.org/pkg/text/template/) を用意しておくと便利です。

```shell
$ kubectl get pods --namespace=kube-system -o go-template-file=pod-requests.tpl
Host IP	Pod	Container	CPU	Memory
...
10.146.0.4	kube-dns-788979dc8f-lkxvz	kubedns	100m	70Mi
10.146.0.4	kube-dns-788979dc8f-lkxvz	dnsmasq	150m	20Mi
10.146.0.4	kube-dns-788979dc8f-lkxvz	sidecar	10m	20Mi
10.146.0.4	kube-dns-788979dc8f-lkxvz	prometheus-to-sd	<no value>	<no value>
...
```
```go:pod-requests.tpl
Host IP{{"\t"}}Pod{{"\t"}}Container{{"\t"}}CPU{{"\t"}}Memory
{{- range $i, $v := .items }}
  {{- range .spec.containers }}
{{ $v.status.hostIP }}{{"\t"}}{{ $v.metadata.name }}{{"\t"}}{{ .name }}{{"\t"}}{{ .resources.requests.cpu }}{{"\t"}}{{ .resources.requests.memory }}
  {{- end }}
{{- end}}
```

アドオンにリソースが占有されるパターンは、大きく分けて以下の３つがあることがわかります。

* 固定量で占有
* リソース空き状況に応じて増減して占有
* ノード数に比例して占有

## Google Kubernetes Engine

GKE (Google Kubernetes Engine) において、ノードで占有されるリソース配分は、以下のドキュメントに記載があります。

* GKE > Concepts > Clusters
  * [Cluster Architecture](https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture)

[Allocatable memory and CPU resources](https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#memory_cpu) に占有率についての記載がありますが、注意書きにある通り kube-system 配下の Pods を含みません。概ね 40% の vCPU と 400MB のメモリが、それぞれのノードで占有されるとあります。

> Note: The values listed for allocatable resources do not account for the resources used by kube-system pods, the amount of which varies with each Kubernetes release. These system pods generally occupy an additional 0.4 vCPU and 400 MiB memory on each node (values are approximate). It is recommended that you directly inspect your cluster if you require an exact accounting of usable resources on each node. 

以下のバージョンのノードで調べてみました。

* Node Version: 1.10.9-gke.5
* Image: Container-Optimized OS (cos)

1コアのノード1台構成だと kube-system だけで CPU を 72% 占有してしまい、デフォルトのCPU割り当て 0.1(100m) では、２つまでしかコンテナを起動することができません。

```
$ kubectl describe nodes | grep 'Allocated resources:' -A 4
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ------------  ----------  ---------------  -------------
  681m (72%)    296m (31%)  807312Ki (29%)   1216912Ki (45%)
```

3コア未満では GKE を使うメリットはなく、3コア以上でようやく半分のリソースが使えるようになる感じです。

| Cores | Allocated CPU | Allocated Memory |
|------:|--------------:|-----------------:|
|     1 |           0.7 |            0.8GB |
|     2 |           1.2 |            1.2GB |
|     3 |           1.4 |            1.4GB |
|     4 |           1.6 |            1.6GB |

kube-system 配下のワークロードを確認すると Stackdriver のアドオンが含まれており、それなりのリソースを占有することがわかります。

| Workload | Type | CPU | Memory | Purpose |
|:-------|:-------|---------:|---------:|:------------|
| [event-exporter](https://github.com/GoogleCloudPlatform/k8s-stackdriver/tree/master/event-exporter) | Deployment | - | - | Stackdriver Logging |
| [fluentd-gcp-scaler](https://github.com/GoogleCloudPlatform/k8s-stackdriver/tree/master/fluentd-gcp-scaler) | Deployment | - | - | Stackdriver Logging |
| [fluentd-gcp](https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/fluentd-gcp) | DaemonSet | 100m | 200Mi | Stackdriver Logging |
| [heapster](https://github.com/kubernetes-retired/heapster) | Deployment | 138m | 294Mi | Stackdriver Monitoring |
| [kube-dns](https://github.com/kubernetes/dns) | Deployment | 260m | 110Mi | Kube DNS |
| [kube-dns-autoscaler](https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns-horizontal-autoscaler) | Deployment | 20m | 10Mi | Kube DNS |
| [kube-proxy](https://github.com/kubernetes/kubernetes/tree/master/cmd/kube-proxy) | Pod | 100m | - | |
| [l7-default-backend](https://github.com/kubernetes/ingress-gce) | Deployment | 10m | 20Mi | HTTP load balancing |
| [metrics-server](https://github.com/kubernetes-incubator/metrics-server) | Deployment | 53m | 154Mi | Stackdriver Monitoring |

ノード数に比例して占有率が増えるものは、見積もりは容易です。kube-proxy と DaemonSet はノードごとに Pod が作成されます。

* [kube-proxy](https://github.com/kubernetes/kubernetes/tree/master/cmd/kube-proxy)
* [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/)
  * [fluentd-gcp](https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/fluentd-gcp)
 
リソース空き状況を元に占有率が増減するものは、閾値の設定を把握しておく必要があります。

* [fluentd-gcp-scaler](https://github.com/GoogleCloudPlatform/k8s-stackdriver/tree/master/fluentd-gcp-scaler)
  * [fluentd-gcp](https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/fluentd-gcp)
* [kube-dns-autoscaler](https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns-horizontal-autoscaler)
  * [kube-dns](https://github.com/kubernetes/dns)
* [addon-resizer (nanny)](https://github.com/kubernetes/autoscaler/tree/master/addon-resizer)
  * [heapster](https://github.com/kubernetes-retired/heapster)
  * [metrics-server](https://github.com/kubernetes-incubator/metrics-server)

GKE のドキュメントには、小規模クラスタ用にアドオンを無効にする方法が紹介されています。

* GKE > How to Guide > Configuring and expanding clusters
  * [Reducing Add-on Resource Usage in Smaller Clusters](https://cloud.google.com/kubernetes-engine/docs/how-to/small-cluster-tuning)

すべてを無効にすることで、CPU: 110m / Memory: 20Mi まで削減することができました。

```
$ kubectl describe nodes | grep 'Allocated resources:' -A 4
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ------------  ----------  ---------------  -------------
  110m (11%)    10m (1%)    20Mi (0%)        20Mi (0%)
```

### Stackdriver Logging

Stackdriver Logging では、以下のアドオンが追加されます。

* event-exporter
* fluentd-gcp-scaler
* fluentd-gcp

DaemonSet である fluentd-gcp により、ノードあたり CPU:100m と Memory:200Mi が占有されます。

Stackdriver Logging を無効にするには gcloud コマンドで `--logging-service none` を指定します。

```shell
$ gcloud container clusters update <CLUSTER_NAME> --logging-service none
```

[kubectl logs](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#logs) により、アクティブなコンテナのログは確認可能ですが、削除されたコンテナのログは確認できなくなります。

### Stackdriver Monitoring

Stackdriver Monitoring では、以下のアドオンが追加されます。

* heapster
* metrics-server

Stackdriver Monitoring を無効にするには gcloud コマンドで `--monitoring-service none` を指定します。

```shell
$ gcloud beta container clusters update <CLUSTER_NAME> --monitoring-service none
```

[Horizontal Pod Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) は、リソースの空き状況をチェックして、動的にコンテナの割り当てリソースを調整しますが、Stackdriver Monitoring からの情報を元にしているため無効にします。

```shell
$ gcloud container clusters update <CLUSTER_NAME> --update-addons=HorizontalPodAutoscaling=DISABLED
```

これにより heapster が削除されます。ちなみに Heapster は Kubernetes 1.11 からは Deprecated となっています。

metrics-server は Deployment でレプリカ数を制御しているため `--replicas=0` にします。

```shell
$ kubectl --namespace=kube-system scale deployment metrics-server-v0.2.1 --replicas=0
```

Stackdriver Monitoring を無効にすると、モニタリングができなくなるだけでなく、metrics-server に依存している、以下のような機能が使えなくなります。

* [kubectl top](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#top) 
* [Horizontal Pod Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/)

### Kube DNS

Kube DNS は、DNS ベースの Service Discovery の機能を提供します。kube-dns 単体で CPU:260m と Memory:110Mi を占有します。

可用性は落ちますが、kube-dns-autoscaler によるレプリケーションを無効にして kube-dns を1台構成とすれば、最低限のリソース占有で Service Discovery の機能を利用可能です。

```shell
$ kubectl scale --replicas=0 deployment/kube-dns-autoscaler --namespace=kube-system
$ kubectl scale --replicas=1 deployment/kube-dns --namespace=kube-system
```

kube-dns 自体を無効とするには `--replicas=0` とします。

```
$ kubectl scale --replicas=0 deployment/kube-dns --namespace=kube-system
```

kube-dns が無効となると、Service Discovery の機能が利用できなくなるだけでなく、DNS Lookup のためには Pod ごとに外部ネームサーバを設定することが必要になります。

* [DNS for Services and Pods > Pod's DNS Config](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-config)

## Conclusion

ノードとしては、Container Runtime / kubelet / kube-proxy さえ動いていれば、機能することがわかりました。

Kube DNS についてはノードサイズに比例して増えるものではなく、DNS ベースの Service Discovery は Kubernetes を使うメリットでもあるため、削減対象とするのは本末転倒に思えます。

Logging や Monitoring のアドオンは、本番運用では必要なものですが、カスタマイズの余地はあると感じました。GKE 謹製の Stackdriver Logging を例にすると、Fluentd を使ったログ転送エージェントが Ruby であるためなのか、デフォルトでは CPU:100m と Memory:200Mi が確保されてしまいます。DaemonSet でり、ノード数に比例して占有リソースは増えてしまうため、もっとミニマムなエージェントを使いたいところです。GKE のドキュメントでも、[独自の ScalingPolicy を使ったチューニング例](https://cloud.google.com/kubernetes-engine/docs/how-to/small-cluster-tuning#fluentd)が紹介されています。