KubernetesのPodのオートスケールについて確認してみる
概要
KubernetesのオートスケールにはPodの数をスケールするHrizontal Pod AutoscalerとNodeの数をスケールするClusterAutoscalerの2種類が存在します。今回はHPAを中心にどのようにPodがスケールしているのかについて確かめていきます。
参考にしたドキュメントはこちら
- Horizontal Pod Autoscaler - Kubernetes
- autoscaler/cluster-autoscaler at master · kubernetes/autoscaler · GitHub
どのように設計すべきか
PodのオートスケールとNodeのオートスケールに関しての設計において、2つのオートスケールをどのように使い分けるか、ということが考えなければいけないことです。どちらのオートスケールをメインで使っていくかという点ですが、明らかにPodの起動時間 < Nodeの起動時間なので、Podのオートスケールで十分に捌ける用意をしておき、Nodeでのオートスケールは必要最小限に抑えるべきだと考えます。ピーク時のスパイクはPodのオートスケールで対応していきます。またNodeのスケールアウトはNodeが起動するまでの時間はできるだけ短い方が好ましく、スケールインまでの時間は長くても問題がありません。
以上の事柄から以下の項目に留意して設定していきます
- メトリクスを取得する時間間隔
- Podのmin, max、Nodeのmin, maxの設定
- Podがオートスケールする条件
- Nodeがオートスケールする条件
- スケールアウトするまでの時間
- スケールアウトするジョブ
Horizontal Pod Autoscaler
Horizontal Pod Autoscalerについてですが、仕組みについては以下のようになっています。
スケールアウト時
記した時間はデフォルトの値です
- cAdviorがPodのメトリクスを収集する(10s毎)(kubeletの--housekeeping-interval)
- kubeletがPodのCPU使用率を取得する (10s毎) (kubeletの--cpu-manager-reconcile-period)
- HeapsterがそれぞれのNodeのkubeletからmetricsを収集と集計し、データをmaster metrics api経由でHPA controllerに公開する (60s毎) (heapsterの--metric_resolution)
- HorizonalPodAutoscalerが収集したメトリクスの値と定義されたCPU使用率を比較する (30s毎) (kube-controller-managerの--horizontal-pod-autoscaler-sync-period)
- HorizonalPodAutoscalerがReplicationControllerとDeploymentのレプリカ数を調整する
- DeploymentsがReplicationControllerに働いて規定のPodの数にまで増やす
- 規定のPod数の算出式: TargetNumOfPods = ceil(sum(CurrentPodsCPUUtilization) / Target)
- 再スケーリングするまでに3分間待機する(kube-controller-managerの--horizontal-pod-autoscaler-upscale-delay)
スケールイン時
- 最後のスケーリングから5分以上経過していないとスケールインしない
- kubeletがPodのCPU使用量を取得する (10s毎)
- HeapsterがそれぞれのNodeのkubeletから値を収集する (60s毎)
- 現在のPodの使用量の平均 / Target が許容値の10%以上もしくは未満になる時にスケールダウンする
という、流れになります。スケールするまでにデフォルトの設定ではkubelet -> heapster -> hpaまでの時間100秒 + Podの起動時間がかかります。早くPodが起動できるようにそれぞれの数値をデフォルトから変更していく必要があります。ただし、Pod起動直後は一時的にCPU使用率が安定せずに増加することがあり、正確なメトリクスが取得されるまでに少々時間がかかることを考慮に入れなければなりません。
HPAの挙動の確認
HPAの有効化
以下の構成を組んでHPAの挙動を確認します。 Node1つとMaster1つの簡易な構成で組んでみます。今回はKOPSを使って構築しました。
$ kubectl run php-apache --image=k8s.gcr.io/hpa-example --requests=cpu=100m --expose --port=80 $ kubectl get pods NAME READY STATUS RESTARTS AGE php-apache-7ccc68c5cd-27c4g 1/1 Running 0 51s
以下のコマンドでHPAを有効にします。
$ kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10 deployment "php-apache" autoscaled
Podのメトリクス収集について
本題と少々それるのですが、以下のようになって、HPAのメトリクスが取得できないことがあります。
$ kubectl get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE php-apache Deployment/php-apache <unknown> / 50% 1 10 1 56s
正常にメトリクスが収集されていないようです。
$ kubectl describe hpa php-apache ... ScalingActive False FailedGetResourceMetric the HPA was unable to compute the replica count: unable to get metrics for resource cpu: unable to fetch metrics from API: the server could not find the requested resource (get pods.metrics.k8s.io)
調べてみたところ、kubernetes1.8からheapsterではなく後継のmetrics-serverがリソースの使用量のデータなどを収集するようになったそうです。これを従来のHeapsterで制御するように変更するのには、kube-controller-managerの--horizontal-pod-autoscaler-use-rest-clients
をfalse
に変更してheapsterをdeployすれば、取得できるようになります。
Kopsでの変更は以下のようにすると良いでしょう。
$ kops edit cluster ... kubeControllerManager: horizontalPodAutoscalerUseRestClients: false ...
ただ、これからheapsterの後継のmetrics-serverに切り替わるらしいので、ここをいじることはせず、metrics-serverをつかうようにしたほうがいいかもしれません。その場合のインストール方法は以下の通りです。
horizontalPodAutoscalerUseRestClients: trueの状態で $ git clone https://github.com/kubernetes-incubator/metrics-server.git $ cd metrics-server $ kubectl create -f deploy/1.8+/
これでmetrics-serverがデプロイされ、正常にメトリクスが収集されるようになります。従来のHeapsterのオプションもそのまま使えるようですので、ここではこのまま話を進めます。
Podに負荷を与える
改めて設定を確認してみます。
$ kubectl get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE php-apache Deployment/php-apache 0% / 50% 1 10 1 5h
$ kubectl describe hpa Name: php-apache Namespace: default Labels: <none> Annotations: <none> CreationTimestamp: Wed, 30 May 2018 11:24:18 +0900 Reference: Deployment/php-apache Metrics: ( current / target ) resource cpu on pods (as a percentage of request): 459% (918m) / 50% Min replicas: 1 Max replicas: 10
こちらの設定では、CPU使用率が50%で1~10つまでのpodが増減します。それでは、実際に負荷を与えてみます。
$ kubectl run -i --tty load-generator --image=busybox /bin/sh > while true; do wget -q -O- http://php-apache.default.svc.cluster.local; done OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!…
しばらくすると、TARGETのCPU使用率が上がってREPLICASも増えます。
$ kubectl get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE php-apache Deployment/php-apache 133% / 50% 1 10 4 5h
$ kubectl get pods NAME READY STATUS RESTARTS AGE load-generator-5c4d59d5dd-92xfh 1/1 Running 0 4m php-apache-7ccc68c5cd-8qd7r 1/1 Running 0 19m php-apache-7ccc68c5cd-96vms 1/1 Running 0 2m php-apache-7ccc68c5cd-bp2lw 1/1 Running 0 2m php-apache-7ccc68c5cd-xnrc4 1/1 Running 0 2m
while loopを止めるとpodの数も元に戻ります。
HPAまでの時間を調整する
ここまでで、HPAによるPodのスケールについて確認できましたが、Podのスケールアウトまでの時間をもう少し短くしたいです。今回は、設定する項目を以下の2つに絞ります。
- kube-controller-managerの--horizontal-pod-autoscaler-sync-period
- kube-controller-managerの--horizontal-pod-autoscaler-upscale-delay
Podの実際のメトリクスと定義したメトリクスを比較するhorizontal-pod-autoscaler-sync-periodと、スケールアップしてからの待機時間であるhorizontal-pod-autoscaler-upscale-delayです。
horizontal-pod-autoscaler-sync-periodを短くすると、PodのCPU使用率が上がりきる前にスケールが始まり、中途半端なPod数で3分間待機するということになるので、スケールの待機時間の方も短くします。 kopsでは以下のように設定しました。
$ kops edit cluster ... kubeControllerManager: horizontalPodAutoscalerSyncPeriod: 5s horizontalPodAutoscalerUpscaleDelay: 30s horizontalPodAutoscalerUseRestClients: true ...
これでHPAがスケールアウトするまでの時間を短くすることができます。
終わりに
今回はPodのHPAについてを紹介しました。この他にもClusterAutoscalerというNodeのスケールを制御する方法もあるので、機会があればそちらの方も確認したいです。
Kubernetesの名前解決を確認する
最近ではドキュメントも充実した感のあるKubernetesですが、実際の挙動ってどうなっているの?という部分に関しては ドキュメントを漁って読むに加えて、実際に手を動かして調べてみることでだいぶ理解が深まっていく感覚があります。 というわけで今回のテーマはKubernetesにおけるPodの名前解決はどのように動作しているのか、です。
公式ドキュメントのDNS for Services and Pods - Kubernetesを実際に手を動かしながら確認していきます。
Serviceリソースの名前解決
それでは早速シンプルな構成のk8s環境を用意して、その挙動を確認していきます。
Node: 1つ Master: 1つ
環境はGKEでも何でもいいのですが、今回はKOPSを使ってAWS上にk8s環境を用意することにしました。 Kopsでクラスタを構成すると以下の様にpodが作成されます。
$ kubectl get pods -l k8s-app=kube-dns -n kube-system NAME READY STATUS RESTARTS AGE kube-dns-56cf5cb57d-7gghr 3/3 Running 0 4d kube-dns-56cf5cb57d-n47jl 3/3 Running 0 4d
kube-dnsというpodが作成されていることがわかります。まずは適当なpodを作ってみます。 以下のbusybox-pod.yamlを用意しました。
apiVersion: v1 kind: Pod metadata: name: busybox namespace: default spec: containers: - image: busybox command: - sleep - "3600" imagePullPolicy: IfNotPresent name: busybox restartPolicy: Always
$ kubectl create -f busybox-pod.yaml $ kubectl get pods NAME READY STATUS RESTARTS AGE busybox 1/1 Running 0 2m
またServiceリソースも作成しておきます。
apiVersion: v1 kind: Service metadata: name: busybox-svc labels: app: busybox-svc spec: ports: - protocol: TCP name: busybox-svc-port port: 1234 targetPort: 5678 selector: app: busybox
$ kubectl create -f busybox-svc.yaml
以下のように作成されました。
$ kubectl get svc --all-namespaces NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE default busybox-svc ClusterIP 100.64.233.34 <none> 6379/TCP 1m default kubernetes ClusterIP 100.64.0.1 <none> 443/TCP 5d kube-system kube-dns ClusterIP 100.64.0.10 <none> 53/UDP,53/TCP 5d
このpodに入って諸々の設定を確認してみます。
$ kubectl exec -it busybox -- /bin/sh / # cat /etc/hosts # Kubernetes-managed hosts file. 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet fe00::0 ip6-mcastprefix fe00::1 ip6-allnodes fe00::2 ip6-allrouters 100.103.66.147 busybox / # cat /etc/resolv.conf nameserver 100.64.0.10 search default.svc.cluster.local svc.cluster.local cluster.local ap-northeast-1.compute.internal options ndots:5
/etc/hostsにある100.103.66.147 busybox
はこのpodのIPアドレスとホスト名(pod名)ですね。
resolv.confに書いてあるnameserver 100.64.0.10
はkube-dnsのserviceのClusterIPとなります。
searchには、namespace名.svc.cluster.localとAWSのリージョン別のドメイン名が定義されていることがわかります。
searchに設定したドメインはその名前をつけてサーチしてくれるという意味となるので、
例えばnslookup hogehogeとした場合には
を順に検索してくれるということになります
それでは、作成したserviceリソースを名前解決してみます。
busyboxとは別のpodを作ります。
apiVersion: v1 kind: Pod metadata: name: dnsutils labels: name: dnsutils spec: containers: - name: dnsutils image: tutum/dnsutils command: - sleep - "3600"
$ kubectl create -f dnsutil.yaml $ kubectl exec -it dnsutils -- /bin/sh
/# nslookup busybox-svc Server: 100.64.0.10 Address: 100.64.0.10#53 Name: busybox-svc.default.svc.cluster.local Address: 100.64.233.34
busybox-svc
で設定されている100.64.233.34
が返されました。
これでIPアドレスまでは特定できましたが、PORTはどのように通知されるのでしょうか。
kubernetesではkube-dnsにサービス名、protocol, port名, プロトコルを指定してSRVレコードを問い合わせるとポート番号が返されます。
/# nslookup > set type=SRV > _busybox-svc-port._tcp.busybox-svc.default.svc.cluster.local Server: 100.64.0.10 Address: 100.64.0.10#53 _busybox-svc-port._tcp.busybox-svc.default.svc.cluster.local service = 10 100 1234 busybox-svc.default.svc.cluster.local.
service = 10 100 1234 busybox-svc.default.svc.cluster.local.
が返ってきました。
これはservice = [Priority] [Weight] [Port] [Target]
という定義なのでPortが1234となり、
Serviceで指定したPortとなっていることがわかります。
ここまでをまとめると、
- resolve.confに定義されるnameserverはkube-dnsのserviceリソースのCluster-IPが設定される
- resolve.confに定義されるsearchにはnamespace名.svc.cluster.local...が設定される
- nslookupでサービス名を問い合わせると、kube-dnsからIPアドレスを返される
- nslookupでサービス名、protocol, port名, プロトコルを指定してSRVレコードを問い合わせるとポート名が返されます。
ということがわかりました。
別のnamespaceでの名前解決
今まではnamespace: defaultでの名前解決を実行していましたが、別のnamespaceではどうでしょうか。
新しい名前空間を作ります。
$ kubectl create ns hogehoge
先ほどのyamlを書き換えて、podとsvcを同じように作成します。
apiVersion: v1 kind: Pod metadata: name: busybox-hogehoge namespace: default spec: containers: - image: busybox command: - sleep - "3600" imagePullPolicy: IfNotPresent name: busybox-hogehoge restartPolicy: Always
busybox-svc-hogehoge.yaml apiVersion: v1 kind: Service metadata: name: busybox-svc-hogehoge labels: app: busybox-svc-hogehoge namespace: hogehoge spec: ports: - protocol: TCP name: busybox-svc-hogehoge-port port: 1234 targetPort: 5678 selector: app: busybox
そして、このような状態となります。
$ kubectl get svc -n hogehoge NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE busybox-svc-hogehoge ClusterIP 100.64.81.224 <none> 1234/TCP 1m $ kubectl get pod -n hogehoge NAME READY STATUS RESTARTS AGE busybox-hogehoge 1/1 Running 0 2m
この状態でnamespace: defaultにあるdnsutilで名前解決をしてみます。
/# nslookup busybox-svc-hogehoge Server: 100.64.0.10 Address: 100.64.0.10#53 ** server can't find busybox-svc-hogehoge: NXDOMAIN
別のnamespaceの名前解決はできません。 下のようにサービス名にnamespaceをくっつけると返ってくるようになりました。
root@dnsutils:/# nslookup busybox-svc-hogehoge.hogehoge Server: 100.64.0.10 Address: 100.64.0.10#53 Name: busybox-svc-hogehoge.hogehoge.svc.cluster.local Address: 100.64.81.224
これは、前述のresolve.conf
の設定でsearchの項目で設定されている値を考えれば、こうなるのも頷けます。
つまり
- サービス名 + namespacesで他のnamespaceのサービスの名前解決ができる
- resolve.confのsearchの項目で制御している
ということがわかります。
Podの名前解決
ここまではKubernetesのServiceの名前解決を見ていましたが、続いてpodの名前解決をみていきます。
podのDNSのAレコードは以下のように登録されています。
pod-ip-address.my-namespace.pod.cluster.local
例えば、1.2.3.4
のpodがnamespace: default
に置かれていた場合、
1-2-3-4.default.pod.cluster.local
という名前で登録されています。
実際に確認してみます。先ほどのdnsutilに入って、podの名前解決をしてみます。
$ kubectl exec -it dnsutils -- /bin/bash # nslookup 100-103-66-157.default.pod.cluster.local Server: 100.64.0.10 Address: 100.64.0.10#53 Name: 100-103-66-157.default.pod.cluster.local Address: 100.103.66.157
以上のようにPodのIPが返ってくることがわかりました。
PodのDNSポリシー
Kubernetesでは現在、以下の方式でPod単位でDNSの設定することができます。 これらのDNSポリシーはPodのspec:dnsPolicyフィールドで指定します。 それぞれのDNSポリシーを確認します。
- "Default": Podが動作しているNodeから名前解決の設定を継承する。
- "ClusterFirst": kube-dnsに設定されているstubDomainsやupstreamNameserversを参照する。そうでなければ、Nodeから名前解決の設定を継承する
- "ClusterFirstWithHostNet": NodeとPodを同じサブネットにおくhostNetworkが有効な場合はこれを設定する必要がある
- "None": Kubernetes1.9から追加された機能でPodのDNS設定はdnsConfigフィールドのみを使用して提供される。
※ Defaultは、デフォルトDNSポリシーではありません。DNSポリシーを指定しない場合、 "ClusterFirst"が選択されます。
実際にそれぞれのDNSポリシーを適用してみます。それぞれのポリシーを設定したPodを用意してみます。 まずはDefaultから。
apiVersion: v1 kind: Pod metadata: name: busybox namespace: default spec: containers: - image: busybox command: - sleep - "3600" imagePullPolicy: IfNotPresent name: busybox dnsPolicy: Default restartPolicy: Always
$ kubectl create -f busybox-default.yaml
中に入って確認してみます
$ kubectl exec -it busybox-default -- /bin/sh / # cat /etc/resolv.conf nameserver XX.XX.XX.2 (AWSのVPCのネームサーバのIPアドレス) search XXXX.compute.internal
このように確かにpodが存在しているNodeと同じ設定が入っているようです。 そして、resolve.confの設定が書き換わっていますので、クラスター内部の名前解決はできない状態となってしまいます。
/ # nslookup kubernetes.default Server: XX.XX.XX.2 Address 1: .XX.XX.XX.2 <nodeのhostname>.<zone>.internal nslookup: can't resolve 'kubernetes.default' / # / # / # nslookup busybox-svc.default.svc.cluster.local Server: XX.XX.XX.2 Address 1: .XX.XX.XX.2 <nodeのhostname>.<zone>.internal nslookup: can't resolve 'busybox-svc.default.svc.cluster.local'
続いてClusterFirstですが、こちらは何も設定しなければこのポリシーが適用されるため、 前回のKubernetes内部の名前解決と同じ設定が反映されます。確認は割愛します 前述したこちらの設定が含まれることになります。
$ kubectl exec -it busybox -- /bin/sh / # cat /etc/hosts # Kubernetes-managed hosts file. 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet fe00::0 ip6-mcastprefix fe00::1 ip6-allnodes fe00::2 ip6-allrouters 100.103.66.147 busybox / # cat /etc/resolv.conf nameserver 100.64.0.10 search default.svc.cluster.local svc.cluster.local cluster.local XXXX.compute.internal options ndots:5
続いて、ClusterFirstWithHostNetです。まずそもそもHostNetworkをtrueにする必要があります。HostNetwork設定とは、Nodeのネットワークインターフェースを直接参照するための設定です。 例えば、以下のようにHostNetworkをtrueにして、dnsPolicyをClusterFirstWithHostNetに設定してみます。
apiVersion: v1 kind: Pod metadata: name: busybox-clusterfirstwithhostnet namespace: default spec: containers: - image: busybox command: - sleep - "3600" imagePullPolicy: IfNotPresent name: busybox-clusterfirstwithhostnet dnsPolicy: ClusterFirstWithHostNet hostNetwork: true restartPolicy: Always
podを作成してみます。
$ kubectl create -f busybox-clusterfirstwithhostnet.yaml $ kubectl get pods -o wide
PodのIPがNODEのIPと同一のものになっていることがわかります。 このpodのDNSの設定はどのようになっているのかを確認してみましょう。
$kubectl exec -it busybox-clusterfirstwithhostnet -- /bin/sh/ # cat /etc/resolv.conf nameserver 100.64.0.10 search default.svc.cluster.local svc.cluster.local cluster.local XXXX.compute.internal options ndots:5
ClusterFirstと同じ設定が入っています。 確かにこちらの[コード](https://github.com/kubernetes/kubernetes/blob/317853c90c674920bfbbdac54fe66092ddc9f15f/pkg/kubelet/network/dns/dns.go#L268をみてみると、ClusterFirstと同じ設定が入るようになっています。
そして最後にdnsPolicyがNoneの場合です。
この場合、クラスタ内のkube-dnsのconfigmapにstub-domainとupstream DNS serversを設定することができます。 また、pod個別にDNSへ設定することもできます。
例として、以下のようなConfigMapとなります。
apiVersion: v1 kind: ConfigMap metadata: name: kube-dns namespace: kube-system data: stubDomains: | {"acme.local": ["1.2.3.4"]} upstreamNameservers: | ["8.8.8.8", "8.8.4.4"]
pod個別に設定する場合は以下のようになります。
apiVersion: v1 kind: Pod metadata: namespace: default name: busybox-none spec: containers: - name: busybox-none image: busybox dnsPolicy: "None" dnsConfig: nameservers: - 1.2.3.4 searches: - ns1.svc.cluster.local - my.dns.search.suffix options: - name: ndots value: "2" - name: edns0
実際にみてみます。
$ kubectl exec -it busybox -- cat /etc/resolv.conf nameserver 1.2.3.4 search ns1.svc.cluster.local my.dns.search.suffix options ndots:2 edns0
この設定では、acme.localを持ったDNS要求は1.2.3.4でlistenされているDNSに転送されます。 それ以外は8.8.8.8と8.8.4.4に転送されるという設定となります。
dnsPolicyによってそれぞれのresolve.confが書き換わるということがわかりました。
終わりに
KubernetesのDNS設定は多岐に渡り、それがyamlファイルに抽象化されていてわかりにくいところがありましたが、 実際に確認してみますと、結局のところ、podでのconfを書き換えているだけということを理解することができました。 このほかにもKuberenetesのドキュメントには多くの設定項目がありますが、理解が難しい時にはこのように手を動かして確認して行きたいですね。
参考
自己紹介
はじめましてvaru3(ばるさん)です。ゴキブリ退治の例のやつのことではなく、名前をもじった素晴らしいあだ名です。
都内のWEB系企業にて、インフラエンジニアをやっています。エンジニア歴5年目、27歳。インフラエンジニアと一口で言っても、現代ではその裾屋もだいぶ広くなっていますが、最近では主にIaaSや(AWS,GCP)コンテナオーケストレーション(Kubernetes)などを中心に設計、構築から運用監視まで幅広く関わらせていただいてます。プログラミング言語はRuby, Python, Goなどを浅くかじっている感じです。
本ブログでは自分の業務で得た知識、知見をまとめて多くの人に発信することを目的としています。今まで、多くの方々の技術情報ブログにお世話になってきましたので、自らも情報を出して言って社会貢献の一つでもやっていこうかなと。
というわけで、そんな感じです。よろしくお願いします。