entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

RendertronをGKEとCloud Runで構築しました

こんにちは、インフラ部の id:sue445 です。

今回は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のサーバを含めた全体像はおおまかに下記のようになっています。

f:id:pxvpxv:20200612103031p:plain

フロントのnginxで通常のリクエストだったらAP(アプリケーション)サーバに、検索エンジンのクローラのリクエストだったらRendertronのサーバにリクエストを振り分けています。

GCP側はVPC内にGKEのクラスタがいるだけのシンプルな構成です。

GKE内部

GKEのクラスタ内部を細分化すると下記のようになっています。

f:id:pxvpxv:20200612103227p:plain

フロントサーバからのリクエストは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内の構成は下記のようになっています。

f:id:pxvpxv:20200612103257p:plain

フロントの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 の設定
    • limitsrequests が大きく差異があるとHPAで意図通りにオートスケールしないので同じにしておくのが安定
    • requests を省略して limits だけ書いてもいいのだが、他の人が見た時に書き忘れと誤解して変な値を書かれるのが嫌なので意図的に同じ値を書いている
    • cpumemory を色々調整したが 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

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が急に悪化してるのを見つけたのが解決の糸口になりました。

f:id:pxvpxv:20200612103339p:plain

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のリリースサイクルが早いので追従するのが大変
  • 動かすアプリの特性に応じた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にしたくらいです。

f:id:pxvpxv:20200612103426p:plain

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を数日間本番環境で併用して計測しました。

レスポンスタイム

f:id:pxvpxv:20200612103453p:plain

  • Cloud Run版Rendertron(上):4〜4.5秒
  • GKE版Rendertron(下):だいたい2〜3秒、たまに4秒台

rps

f:id:pxvpxv:20200612103504p:plain

  • GKE版Rendertron(上):17〜60rps
    • 検索エンジンのクローラのリクエスト量に応じて増減
  • Cloud Run版Rendertron(下):16rps前後
    • Cloud Run版はGKE版の半分のリクエストしか流していなかったので比較しやすいようにグラフでは2倍しています(実際のrpsは7〜8rps)

費用

GKE版Rendertron

VMの費用だけだと1日32ドルくらいで、LBやログの金額も含めると1日45ドルくらいでした。

f:id:pxvpxv:20200612103603p:plain

Cloud Run版Rendertron

1日30〜32ドルくらいでした。ただしGKEの半分のリクエストしかさばいてないのでGKEと同じリクエスト量だと1日60ドルくらい(+ログの料金)かかる見込みでした。

Cloud Runはリクエスト処理中のCPUの割り当て時間とメモリ使用量で料金がかかるのですが、大半がCPUの割り当て時間でコストがかかっています。

f:id:pxvpxv:20200612103548p:plain

比較の結果

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が必要
  • 既存のKubernetesやHelmの設定を再利用したい場合

Cloud Runだと厳しいケース

Cloud Runでもできなくはないが下記のような場合はGKEの方が有利

  • 1リクエスト辺りの処理が極端に重い(CPUやメモリを喰う)場合はコストがかさむ
    • 体感的には1リクエスト数秒かかるケースだと厳しい
20191219021925
sue445
2018年7月に中途入社。CIおじさん。好きな言葉は「全ての手作業を自動化」。 好きなアニメは女児アニメ全般(プリキュア・プリティーシリーズ・アイカツ)とサザエさん。 多数のgemをOSSで公開 https://rubygems.org/profiles/sue445 。代表作はプリキュアのRuby実装のrubicure (https://github.com/sue445/rubicure)