839の日記

業務とは関係ない趣味の話を書くブログです。

趣味のGKEクラスタを安価に運用する

元々VPSサーバ2台で運用していたものをGKEに移した際に色々やったことをざっくりと書く。

GKEノード構成

確約利用割引  |  Compute Engine ドキュメント  |  Google Cloud の3年契約ノードを1台、preemptive VM2台の合計3台構成にしている。 3台ともマシンタイプはn1-standard-1でリージョンはus-west1を選択している。

ノードの使用料金は以下のようになる。

  • 確約利用割引のn1-standard-1
    • $0.014225 / vCPU hour, $0.001907 / GB hour
    • 1CPU、3.75GBメモリなので0.014225+0.001907*3=$0.02137625/hour
    • 月に約$15.4
  • preemptive VMのn1-standard-1
    • $0.006655 / vCPU hour, $0.000892 / GB hour
    • 1CPU、3.75GBメモリなので0.006655+0.000892*3.75=$0.009331/hour
    • 2台なので月に約$13.4
  • 2つ合わせて合計$28.8/month

元々VPS2台で3500円ぐらいだったのでノードだけ見れば少し安くなっている。 とはいえ他にも料金はかかるのでトータルとしてはGKEに移行した後のほうが高い。 具体的にかかる料金はストレージとネットワークのegress料金で、この辺を足すと1000円弱は高くなった。

GCPのマネージドLB、RDSなどを使わない

GCPのLBは使うと一気に月額料金が跳ね上がるので使っていない。 代わりにEIPを1つ確保し、そのEIPをCloudDNSでサービスのドメインに紐づけている。

1台のノードに確保したEIPを紐付け、そのノードが入口となってクラスタ内の各サービスにアクセスがproxyされる。 このEIPを紐付けるVMに確約利用割引VMを利用している。 元々はこのLB役もpreemptive VMで運用できるか検討したけど、LB役のVMが落ちるとダウンタイムが発生する問題があった。 1日数分程度なので許容かなと思ったけど24hごとに1回きれいに落ちるわけでもなく *1、1日に数回落ちることもあったのであまりUXがよくなさそうだった。 1ヶ月ほどpreemptive VMで検証していた頃は外形監視によるSLAは98%程度だったはず。

EIPが紐付いているpreemptive VMが死んで復旧した後も勝手にEIPがつくわけではないので自前でつけ直す処理を書いていた。

  1. クラスタ内のノードに指定されたEIPが存在しているか
  2. 存在していなかったら3台のノードから自分を除いてランダムに1台選出*2
  3. 選出されたノードに対してGCPAPIを叩いてEIPをattach
  4. 選出されたノードに対してkubeclientでnodeにlabelを付与してnginx-ingress-controller podがそのノードで動くようにする

このあたりをgoで書いて、k8sのCronJobとして1分に1回走らせていた。 この頻度で復旧に数分程度はかかる。 ボトルネックとしてはGCPのEIP attachがまぁまぁ遅いのと、付与したラベルがk8sに気づかれるまでのタイムラグが目立った。

このような背景で確約利用割引VMを使うことに決め、LBの役割はそのVMに任せることにした。 LB以外にもDBやalertmanager、prometheus-exporterなどの全サービスに関わる重要目なpodは確約利用割引VMに配置するようにしている。

preemptive VMが落ちたときに503が返る問題

podの冗長化

確約利用割引VMを使うことでnginx-ingress-controllerのpodは安定して稼働するようになったけど、まだ503が返るケースがあった。 最初から想定したものとしては2台あるpreemptive VMのうち1台にappのpodが偏ってしまって1ノード落ちるとサービスが503になるケース。

このケースの対策としては GitHub - kubernetes-sigs/descheduler: Descheduler for Kubernetes の機能を使うことで解決した。 具体的には同じノードに同じdeploymentのpodが複数個ある場合に他のノードに再配置してくれる機能を使った。 これで2ノードにpodが冗長化されたので1ノードが落ちてもサービスにアクセス不可ということはなくなったので万事解決…。

preemptive VMのプリエンプト通知hook

と思ったらそれでも503が1日に1、2回出るケースがあった。 原因はpreemptive VMのshutdownでk8sのdrain処理はせずにぶちっと切られてしまうので、 ユーザのアクセス時にノードが落ちると503が返るというもの。

この辺の挙動は プリエンプティブル VM インスタンス  |  Compute Engine ドキュメント  |  Google Cloud に詳しく書いてある。

  1. Compute Engine は、プリエンプト通知を ACPI G2 ソフトオフ信号の形式でインスタンスに送信します。シャットダウン スクリプトを使用して、プリエンプト通知を処理し、インスタンスが停止する前にクリーンアップ操作を完了できます。
  2. インスタンスが 30 秒後に停止しない場合、Compute Engine は ACPI G3 メカニカルオフ信号をオペレーティング システムに送信します。
  3. Compute Engine は、インスタンスを TERMINATED 状態に移行させます。

G2シグナルをハンドルして自分が対象だったらdrain処理を開始するといったことが必要になり、プリエンプト通知時にhookして何か処理をするのはサンプルが公式で紹介されている。
Creating and starting a preemptible VM instance  |  Compute Engine Documentation  |  Google Cloud

そのまま採用しても良かったけど、もう少し調べてみるとGCPのorgにそれ用のツールがあったのでそちらを使うことにした。
GitHub - GoogleCloudPlatform/k8s-node-termination-handler: A solution to gracefully handle GCE VM terminations in kubernetes clusters

このツールはGCPのmetadata APIを叩いて自分が終了対象になっていたらdrainなどの処理を行ってくれる。 実際に試してみたところちゃんと終了通知をハンドルしてdrain処理を行ってくれていて、外形監視の503アラートも出なくなった。

f:id:husq:20200103003240p:plain
検知してpodをevictするログ

まとめ

  • 確約利用割引VM1台とpreemptive VM2台を運用してノード費用は月額$28.8程度
  • preemptive VMを運用していて発生した503の対策にはdeschedulerとk8s-node-termination-handlerを利用した

おまけ

主題からは外れているけど今回書いた記事とテーマが近い話。

deschedulerのリソース再配置

元々はリソースの再配置も含めてdeschedulerの利用を検討していたけど、 QoSの設定が割と難しく最優先にしても再配置を対象外にしたいpod*3が選ばれて503になってしまう問題があり採用を見送った。 確約利用割引VMは余っていても重要なpodしか配置しない制約を課して、preemptiveは24hに1回落ちるのでそれを再配置扱いにしている。

またdeschedulerはemptyDirやhostPathのvolumeを利用していると再配置の対象外となる。 自分の場合はpodでnginxとrailsのcontainerが存在し、emptyDirを使ってrails側のassetsをnginx側に渡している。 再配置の対象外としないためにはdeschedulerの実行時に --evict-local-storage-pods フラグを付けておく必要がある。
refs: https://github.com/kubernetes-sigs/descheduler/blob/0.9.0/pkg/descheduler/pod/pods.go#L57 *4

f:id:husq:20200104031116p:plain
deschedulerによって冗長なpodがevictされるログ

k8s-termination-handlerの実装やissue

そんなに量が多くなさそうだったので実装は一通り軽く読んだ。 preemptive VM以外も対象になっているようだったけど、preemptive VM以外は興味がなかったので読み飛ばしている。

DaemonSetとして動作しているけど、デプロイしたところpreemptive VMのノードにだけ*5k8s-termination-handlerのpodが動作していた。 実装的にはmetadata APIをpollingしていて、終了対象になっていたらpodのevictやtaintラベルの付与を行った後にdrain処理を行っている。 evictするときに自分自身をちゃんと対象外にしていて少し親近感を感じた*6

issueを見てて面白いなーと思ったのは以下。

[Question] Handles simulated termination events? · Issue #8 · GoogleCloudPlatform/k8s-node-termination-handler · GitHub
Q. terminationの挙動をsimulateできる?
A. gcloudコマンドでできるよ

試してみた、ちゃんとできた。

Any plans to add a slack notifications? · Issue #10 · GoogleCloudPlatform/k8s-node-termination-handler · GitHub
Q. slack通知の予定ある?
A. 回答なし

どういうpayloadにするか議論になると面倒そうだけど自分もほしい...。
通知が飛んできたタイミングと何か別のアラートがきたタイミングが一緒ならpreemptiveが落ちたのか、みたいな安心感がある。

ということでPRを出した。
Added a feature to notify slack by 8398a7 · Pull Request #21 · GoogleCloudPlatform/k8s-node-termination-handler · GitHub

f:id:husq:20200104031459p:plain
通知結果

ひとまず自分はforkしたimageで運用しているが特に問題はない。 落ちたときにアラートが上がらないとクラスタの正常稼働が安心できるけど、そのうち鬱陶しくなるかもしれない。 SLACK_WEBHOOK_URL が未指定なら通知処理はスキップするようにしているので、邪魔になったら切るかも。

Q: Is this tool discontinued or ready and production safe? · Issue #11 · GoogleCloudPlatform/k8s-node-termination-handler · GitHub
Q. productionで使ってもいい?
A. 正気の沙汰じゃないので、testやstaging環境とかの節約だけにしといて

I would question your sanity considering using preemptive nodes on live environments

[Propose] Call removing a node from a cluster for a preemptible VM. · Issue #15 · GoogleCloudPlatform/k8s-node-termination-handler · GitHub
Q. ノードの削除を明示的に実行したらcontrol planeが早くハンドリングできるんじゃない?(という意味...?)
A. とちるとノードが減ったまま戻ってこなくなるからやめとこ

What is the API that it uses to know that the node will be down after 30 seconds? · Issue #17 · GoogleCloudPlatform/k8s-node-termination-handler · GitHub
Q. どうやってG2シグナルのハンドルしたらいい?
A. metadata APIでできるよ
Q. metadata APIで取れるのはG3だけのように思える

metadata APIとプリエンプト通知に関する公式ソースを見つけられなかったのでコメントが真なのかわからないけど、もしも真なら割とギリギリにdrain処理をしていることになる。
simulateで何度かnodeをterminateしてみると適切にdeschedulerでpodが分散された後なら問題なさそうだった。

*1:もしもきれいに24hに1回なら落ちるタイミングを朝5時とかにしてやろうと思っていた時期もあった

*2:自分自身を選択するとつけ直す際にpublic IPがなくなるシーンでGCPAPIと疎通できなくなる

*3:DBやingress-nginx-controllerが選ばれると即ダウンタイム

*4:このifはやばいでしょ…と思った

*5:これがどこで制御されているかは読んでない

*6:EIPをつけ外しするpodで自分自身を対象にしたら外した段階でネットワークに疎通できなくなる問題でハマったことがあったので