こんにちは、インフラ部の id:sue445 です。
今回はRendertronをGKEとCloud Runの両方で構築した話をしたいと思います。
- tl;dr;
- 前置き
- 今までのRendertronの問題点
- GKE版Rendertronについて
- Cloud Run版Rendertronについて
- GKE版Rendertron vs Cloud Run版Rendertron
- Rendertronをオートスケールにした結果
- 付録:GKEとCloud Runのどっちを使ったらいいか迷った時の判断基準
tl;dr;
- Rendertronをオートスケール構成にしたことで検索エンジンのクローラのクロール量が1.5倍に増えた
- GKE版のRendertronとCloud Run版のRendertronを両方本番投入してパフォーマンスを比較した結果、GKE版のRendertronを本採用した
- 動かすアプリケーションの特性によってはCloud RunよりもGKEを使った方がいいこともある
前置き
pixivではSEOのために Rendertron を用いてDynamic Renderingを行っています。
Rendertronについては下記エントリを参照ください。
今までのRendertronの問題点
pixivで利用しているRendertronはオンプレとクラウドのハイブリッド構成でしたが、検索エンジンからの流入が増えたことでサーバの負荷が耐えきれなくなることがありました。
検索エンジンのクローラから常に一定の量のリクエストがあるのではなく波があります。クローラからのリクエストが急増するとサーバが高負荷になって502エラーや429エラーが返り、検索エンジンのインデックスにのらなくなります。
高負荷時のアラートを検知してクラウド側のサーバを増やしてはいるのですが、インフラ部による手動対応だったので本番投入時にはリクエストが落ち着いてることも少なくありません。
また、コストの問題もありました。現状だとオンプレのサーバを過剰に用意することで対応しているためコストの最適化ができていませんでした。
今後のSEO施策によってリクエスト量がもっと増える可能性があったので、リクエスト量によってオートスケールするRendertronの要望があったので今回作成しました。
GKE版Rendertronについて
GKEの採用理由について
KubernetesのHorizontal Pod Autoscaler*1やGKEのクラスタオートスケーラー *2のように負荷に応じてpodやnodeが自動で増減する仕組みが今回の要件にあっていたのが理由の1つです。
Cloud Runも選択肢にあったのですが、オンプレの負荷もギリギリで最速で本番投入する必要があったので別サービスでGKEの導入実績と運用実績があったGKEで最初に作りました。
実際3日で作って本番投入できたのでこの判断は間違ってなかったと思ってます。
下記が3日間の内訳です。
- TerraformでGKEのクラスタやVPC周りを作るのに1日
- 既存のRendertronのDockerizeで1日
- RendertronをGKE上で動かすためのKubernetesの設定を作るのに1日
GKE版Rendertronの構成
全体
Rendertronのサーバを含めた全体像はおおまかに下記のようになっています。
フロントのnginxで通常のリクエストだったらAP(アプリケーション)サーバに、検索エンジンのクローラのリクエストだったらRendertronのサーバにリクエストを振り分けています。
GCP側はVPC内にGKEのクラスタがいるだけのシンプルな構成です。
GKE内部
GKEのクラスタ内部を細分化すると下記のようになっています。
フロントサーバからのリクエストはIngressを通してnode内の各podに割り振られます。
オートスケールnodeではプリエンプティブルVM*3と呼ばれる、通常の8割引価格のVMを利用してハイスペックなマシンを低価格で利用しています。
全てプリエンプティブルVMのnodeにするのが理想なのですが、プリエンプティブルVMはその性質上いつシャットダウンされても文句言えない代物なので可用性を高めるために通常のVMも常設nodeとして併用しています。
オンプレでのRendertronだとCPUの負荷が異常に高かったのでGKE版RendertronではN2マシンタイプのVMを利用しています。
2種類のnodeでスペックが違うのでVMの台数で比較はできないのですが、検索エンジンのクローラのリクエスト量に応じて通常時はクラスタ全体で計80〜120コア前後、ピーク時は計270コア以上の構成でVMがオートスケールで増減して起動しています。
2種類のnodeでスペックを変えている理由は下記です。
- プリエンプティブルじゃない方の通常のVMの費用を抑えるためにスペックを低めにした
- GCEのVMは合計スペックが同じであれば金額は同じなのだが(例:n1-standard-1が2台とn1-standard-2が1台だと金額は同じ)、VMの台数が増えるとDaemonSetで起動するDatadogのagentが増えてDatadog側の費用が増えるので*4オートスケール側の台数が増えすぎないようにスペックを少し高めにしている
pod内部
さらにpod内の構成は下記のようになっています。
フロントのnginxで受けたリクエストをRendertronのエンドポイントに変換するためにRendertron(nodejs)の前段にnginxを置いています。
細かいですがGCPのLoad BalancerのTCPセッションタイムアウトが600秒固定なので *5、ここのnginxの keepalive_timeout
を明示的に長くしています。(GCLBではこの設定がないとLoad Balancer側で502エラーが多発するので必須)
DockerやKubernetesの流儀的にはnginxとRendertronのコンテナをわけるのが一般的なのですが、Dockerizeの時点でCloud Runでも同じDockerイメージを利用することを視野に入れていたのでこのような構成になっています。
Kubernetesの設定と解説
一部伏せてますが本番で使ってる設定とほぼ同じものです
rendertron-deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: rendertron labels: app: rendertron spec: selector: matchLabels: app: rendertron template: metadata: labels: app: rendertron spec: containers: - name: rendertron ports: - containerPort: 80 image: asia.gcr.io/path-to-image:abcdef # NOTE: HPAとオートスケーラーのためにlimitsとrequestsは必ず同じ値で指定すること resources: limits: cpu: 2000m memory: 1024Mi requests: cpu: 2000m memory: 1024Mi livenessProbe: httpGet: port: 80 path: /_ah/health failureThreshold: 5 periodSeconds: 10 readinessProbe: httpGet: port: 80 path: /_ah/health failureThreshold: 10 periodSeconds: 10 tolerations: - effect: NoSchedule key: cloud.google.com/gke-preemptible operator: Equal value: "true"
- ここで肝になるのが
resources
の設定limits
とrequests
が大きく差異があるとHPAで意図通りにオートスケールしないので同じにしておくのが安定-
requests
を省略してlimits
だけ書いてもいいのだが、他の人が見た時に書き忘れと誤解して変な値を書かれるのが嫌なので意図的に同じ値を書いている cpu
とmemory
を色々調整したがcpu: 2000m
,memory: 1024Mi
に落ち着いたcpu: 1500m
だと502エラーが多発してダメだったので増やしたmemory: 700Mi
だと重いページをRendertronで処理してる最中にメモリが足りなくなってOOMでpodが死んで502エラーがちょいちょい出たので増やした
- オートスケールnodeに
cloud.google.com/gke-preemptible="true":NoSchedule
を設定しているため、プリエンプティブルVM上でpodが起動できるようにtolerations
を設定 *6
rendertron-hpa.yaml
apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata: name: rendertron spec: scaleTargetRef: kind: Deployment apiVersion: apps/v1 name: rendertron minReplicas: 5 maxReplicas: 500 targetCPUUtilizationPercentage: 40
- podのCPU使用率が40%を超えた時にpodを増やす設定
rendertron-ingress.yaml
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: rendertron labels: app: rendertron annotations: kubernetes.io/ingress.global-static-ip-name: name-of-static-global-ip spec: rules: - http: paths: - path: /* backend: serviceName: rendertron servicePort: 80
kubernetes.io/ingress.global-static-ip-name
にはグローバルIPアドレスの名前を渡す
rendertron-service.yaml
kind: Service apiVersion: v1 metadata: name: rendertron labels: app: rendetron spec: type: LoadBalancer selector: app: rendertron ports: - name: rendertron protocol: TCP port: 80 targetPort: 80
Tips
nodeのストレージサイズをケチり過ぎたらpodが起動できなくなった
nodeを起動して半日くらい経つと特定のnodeのpodで
error determining status: rpc error: code = Unknown desc = Error: No such container: d85fcdee54e18ba04b04faf08ff47906a1e7ba3946f9b40cab31e90b27532230
のようなエラーでCreateContainerErrorが多発し、Pods unavailableになりpodが全く起動できなくなる事象にハマりました。
エラー内容でググってもそれっぽい事例がなくて数日間ハマっていたのですが、Pods unavailableが多発し始めた時間帯でDisk latencyが急に悪化してるのを見つけたのが解決の糸口になりました。
GCEだとVMに割り当てたストレージサイズによってパフォーマンスが変動します。
https://cloud.google.com/compute/docs/disks/performance?hl=ja#cpu_count_size
GKEだと基本的にアプリからローカルへのファイル書き込みは行わないのでnodeでDockerイメージをpullできるだけの十分なストレージサイズだけ割り当てていました。(24GB)
しかしそれだとグラフのようなLatency悪化が発生するため多めに128GBのストレージを割り当てたことでpodが起動できなくなる事象は解決しました。
これはGCPだけの問題ではなく、AWSのEBSでもストレージサイズとパフォーマンスが関係していて同じ事象になると思われるため注意してください。
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ebs-volume-types.html
N1マシンタイプのnodeとN2マシンタイプのnodeを比較した結果、N2マシンタイプが安くなった
GCEのN2マシンタイプはN1マシンタイプよりもCPUの性能がいい分、1コア辺りの金額が15%ほど高いです *7
しかしN2マシンタイプのnodeだと1台辺りのスループットが上がり必要なnodeの台数が大幅に減ったためトータルで月900ドルほど安くなりました。
東京リージョンだとN2マシンタイプはasia-northeast1-bとasia-northeast1-cでしか使えないので注意してください。*8
Cloud Run版Rendertronについて
Cloud Runについて
簡単に説明するとDockerイメージをGCP上で楽に動かすための仕組みです。(AWSでいうところのFargateみたいなもの)
https://cloud.google.com/run?hl=ja
Cloud Runは実行形態によってGCPのフルマネージド環境上で動かすフルマネージドCloud RunやGKE上で動かすAnthos GKEがありますが、このエントリでは特別なことがない限りCloud Runはフルマネージドの方を指します。
Cloud Runの採用理由について
GKEでいくつかアプリを運用してみて色々なつらみが見えてきたためです。
また、GCPの料金計算ツール*9でもGKE版の半額程度でいけるという試算が出たため検証してみる価値があると思いました。
GKEのつらみ
- Kubernetesのリリースサイクルが早いので追従するのが大変
- Regularチャンネルで月1〜2回
- GKEに自動アップグレード機能もあるのだが、外部要因で急に動かなくなるのが嫌なので手動アプデしてる
- 動かすアプリの特性に応じたnodeのスペックを選択する必要があるのが大変
- standard系のマシンタイプを利用するとアプリによってはリソースが無駄になることがある
- 例)CPUよりメモリを食い気味なアプリであればhighmem系のマシンタイプを利用、逆であればhighcpu系のマシンタイプを利用、highcpuでメモリが少なすぎる時はcustomで微調整
- VM起因のトラブル
- 前述のストレージサイズをケチりすぎてpodが起動できなくなった事例
- GKEはKubernetesクラスタとしては確かにマネージドだしGCEのVMの管理もしてくれるんだけど、トラブった時はVMのメトリクスも見に行く必要がある
- GKEだと可用性のために通常のVM(定価)とプリエンプティブルVM(8割引)を併用してるが、前者が意外にコストを圧迫する
Cloud Run版Rendertronの構成
フロントサーバのupstreamをGKEからCloud Runにしたくらいです。
Dockerイメージ側でEXPOSEするポート番号を追加したくらいで*10GKE版で使ってたDockerイメージをほぼそのまま使ってます。
当初の予定だとフロントのnginxから直接Cloud Runのエンドポイントに流して、コンテナ内の処理はGKE版と同じにする想定でした。しかしCloud Runのエンドポイントに対してHostヘッダを上書きしようとすると404エラーになりました。
$ curl -s -I -H "Host: www.pixiv.net" https://path-to-rendertron.a.run.app/ HTTP/2 404 content-type: text/html; charset=UTF-8
カスタムドメインのマッピングを利用すれば別Hostでアクセスできるようでしたが、 https://cloud.google.com/run/docs/mapping-custom-domains?hl=ja に
Cloud Run(フルマネージド)の場合、サービスをカスタム ドメインにマッピングすると、HTTPS 接続用のマネージド証明書が自動的に発行、更新されます。
という記載があり断念しました。(( www.pixiv.net
の証明書は別のところで管理しているのでGCP側で証明書を発行したくない))
そのため苦肉の策でフロントのnginxとCloud Runの間に別のnginxを置いて、そこで www.pixiv.net
で受けてCloud Runのエンドポイントに変換するようにしました。今回は検証目的だったので雑にGKEでnginxのpodを立てましたが、コスパを考えると本採用時には既存のオンプレ側に載せる可能性もありました。
GKE版Rendertron vs Cloud Run版Rendertron
「推測するな、計測せよ」という言葉があるため実際に両方のRendertronを数日間本番環境で併用して計測しました。
レスポンスタイム
- Cloud Run版Rendertron(上):4〜4.5秒
- GKE版Rendertron(下):だいたい2〜3秒、たまに4秒台
rps
- GKE版Rendertron(上):17〜60rps
- 検索エンジンのクローラのリクエスト量に応じて増減
- Cloud Run版Rendertron(下):16rps前後
- Cloud Run版はGKE版の半分のリクエストしか流していなかったので比較しやすいようにグラフでは2倍しています(実際のrpsは7〜8rps)
費用
GKE版Rendertron
VMの費用だけだと1日32ドルくらいで、LBやログの金額も含めると1日45ドルくらいでした。
Cloud Run版Rendertron
1日30〜32ドルくらいでした。ただしGKEの半分のリクエストしかさばいてないのでGKEと同じリクエスト量だと1日60ドルくらい(+ログの料金)かかる見込みでした。
Cloud Runはリクエスト処理中のCPUの割り当て時間とメモリ使用量で料金がかかるのですが、大半がCPUの割り当て時間でコストがかかっています。
比較の結果
GKEの運用のつらさとCloud Runのパフォーマンスのつらさを天秤にかけた結果、前者の方がまだマシなつらさだったのでpixivのRendertronではGKEを本採用しました。
Rendertronの処理が1〜2秒くらいで終わる軽い処理であればCloud Runの方が安くなったかもしれません。
Rendertronをオートスケールにした結果
オートスケールRendertron導入直前は1日800万リクエストだった検索エンジンのクロール量が、オートスケールRendertron導入後に1日1200万リクエストに増えました。(1.5倍増)
これは、検索エンジンのクローラがちょっと本気を出した時に今まではRendertronが音を上げて502エラーや429エラーを返していたためクローラが手加減してたのですが、Rendertronがオートスケールするようになってエラーを返さなくなったためクローラがさらに本気を出してきたと考えています。
検索エンジンのクロール量が増えたことによりpixivの最新情報がすぐに検索結果に反映されたり、インデックスが増えたことにより検索流入が増えSEO的にとても嬉しい結果になりました。
付録:GKEとCloud Runのどっちを使ったらいいか迷った時の判断基準
GKEとCloud Runのどっちでいくか迷ってた時期にまとめていたメモです。
前提知識
前置き
- GKE(Kubernetes)やCloud RunはDockerコンテナを動かす仕組みの1つなので、アプリケーションがDockerizeされていればどちらで動かしてもいい
- Cloud RunもさらにフルマネージドのやつかAnthos(自分のKubernetesクラスタ上で動かす)かに分かれるんだけど、基本的にはフルマネージドを使うでいいと思う
- 前述の通りnode(VM)の管理が必要というGKEのつらみがあるので、Anthosはそれに対する解決策にはならない
tl;dr;
ほとんどの場合フルマネージドのCloud Runの方を使った方が幸せになれるが、それだと実現できないこともあるのでその場合はGKEを使った方がいい。
なぜCloud Runを使った方がいいか
前述の通りGKEにはつらみが多いため。
とはいえCloud Runだと無理なケースや、できなくはないが難しいケースもあるのでその場合はGKEを使った方がいいと思ってます。
Cloud Runだと無理なケース
下記に当てはまる場合はフルマネージドのCloud Runだと無理なのでGKEを使った方がいい
- 送信元のIPアドレスを固定化しないといけない場合
- フルマネージドのCloud RunはVPC外で実行されてるのでCloud NATを通せない
- 特殊なVM(node)上で動かす必要がある場合
- 例1)CPUのスペックが必要
- Rendertronでベンチマークとった感じだとCloud RunはGCEでいうところのN1マシンタイプ相当のCPUが使われてるように見えるので、CPUの性能が必要になる場合は自分でGKEのクラスタを作ってN2やC2のマシンタイプのVMでnodeを作る必要がある
- Cloud Runの実行時に指定できるのはCPUの コア数 なので1コア辺りのCPUの性能とは違う
- 例2)GPUが必要
- 例1)CPUのスペックが必要
- 既存のKubernetesやHelmの設定を再利用したい場合
- Cloud RunでデプロイするものはDockerイメージなのでこの場合不向き
- あとは https://www.elastic.co/jp/elastic-cloud-kubernetes など
Cloud Runだと厳しいケース
Cloud Runでもできなくはないが下記のような場合はGKEの方が有利
- 1リクエスト辺りの処理が極端に重い(CPUやメモリを喰う)場合はコストがかさむ
- 体感的には1リクエスト数秒かかるケースだと厳しい
*1: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
*2: https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-autoscaler?hl=ja
*3: https://cloud.google.com/compute/docs/instances/preemptible?hl=ja
*4:Datadogのagentはホスト数単位での課金
*5: https://cloud.google.com/load-balancing/docs/https?hl=ja#timeouts_and_retries
*6: https://cloud.google.com/kubernetes-engine/docs/how-to/preemptible-vms?hl=ja
*7:https://cloud.google.com/compute/vm-instance-pricing?hl=ja
*8:https://cloud.google.com/compute/docs/regions-zones?hl=ja#available
*9:https://cloud.google.com/products/calculator?hl=ja
*10:後述の通りDocker側でnginxが不要になったのでnodejsのportをEXPOSEするようにした