Cloud Run の Always on CPU で Cloud Pub/Sub から Pull する worker を試してみた

Kazuu
google-cloud-jp
Published in
13 min readSep 15, 2021

TL; DR

Cloud Run の Always on CPU を使うと、Cloud Pub/Sub から Pull する Worker を Cloud Run で実行出来ます。ただし、スケーリング等にいくつか諸注意があります。

はじめに

Cloud Run の Always on CPU が Preview でリリースされて、バックグラウンド タスクや非同期処理で使えると Twitter で宣伝したところ、私の tweet 史上、一番の反響を頂きました。ありがとうございます。また同僚の Shingo-san が素敵な解説記事を書いてくれたり、同じく同僚の Pottava-san も素敵なサンプルコードを書いてくれてたり。「tweet してるだけでいいのかい?本当に?」という私のエンジニアとしての良心の呵責があったため、私もこうして記事を書いています。

試したこと

以前、お客様から Cloud Pub/Sub から Pull する非同期 Worker を Cloud Run で使えないか、ご質問を頂きました。そのときは、「Cloud Run はあくまで Request & Response が基本のサービスだから、ちょっと厳しいですね。Request がないと CPU もスロットリングされちゃうし」と回答しました。当時はそういった用途且つコンテナだと、やはり GKE をご案内するしかなかったのが正直なところです。

今回、Cloud Run の Always on CPU が登場したことにより、こちらの要件でも使えるようになりました。いくつか注意点があるのでこの記事で実際に試したコードなどを晒しながら説明したいと思います。

試した構成はこちらです。

確認したかったポイント

  • CPUがスロットルされることなく、Pull し続けられるのか?
  • リクエストが全く無い状態で Publish / Pullの処理を走らせられるのか?
  • リクエストが全く無い場合、通常だとコンテナインスタンスが 0 になるが、min-instance オプションで回避出来るのか?
  • オートスケール出来るのか?

使ったコードはこちらです。(適当に書いてるコードなので参考程度に見てください)
アプリとしては 以下の 2 種類です。

  • Publisher
    ひたすら Pub/Sub の Topic に greeting というメッセージを 1 秒間隔で投げ続けます。
  • Subscriber
    Pub/Sub の Subscription に非同期で Pull を行い、受け取ったメッセージをログとして吐き出し続けます。非同期 Worker の位置付けです。

Always on CPU の登場により、Cloud Run は HTTP リクエストの呪縛から完全に開放された!と言いたいところですが、そうではありません。Always on CPU にも Container Contract は有効です。そのため何かしら HTTPサーバーを使ってリッスン状態にしておく必要があります。

HTTPのハンドラー関数の中で Pub/Sub に Publish したり Pull したりする処理を書いても良かったのですが、確認したいポイントにも書いた通り、リクエストに頼らないかたちで Publish / Pull の処理 をさせてみたかったので、以下のように main 関数の中で http.ListenAndServe する前に、goroutine で非同期処理させるかたちにしてみました。もっと良いやり方をご存知の方、ぜひ教えてくださいmm。

ちなみに、http.ListenAndServe しないで Cloud Run にデプロイすると、以下のような怒られ方をします。ご参考まで。

Deployment failedERROR: (gcloud.beta.run.deploy) Cloud Run error: Container failed to start. Failed to start and then listen on the port defined by the PORT environment variable. Logs for this revision might contain more information.

下準備

Pub/Sub で使うスキーマを Protocol Buffer で定義します。今回は簡単に greeting という string のメッセージだけです。

❯ gcloud beta pubsub schemas create greeting \
--type=PROTOCOL_BUFFER \
--definition='syntax = "proto3";message ProtocolBuffer {string greeting = 1;}'

作成したスキーマの validation check を行います。

❯ gcloud beta pubsub schemas validate-message \
--message-encoding=JSON \
--message='{"greeting": "Hello, Cloud Run"}' \
--schema-name=greeting
Message is valid.

次に上記で作ったスキーマを用いた Topic を作ります。

❯ gcloud beta pubsub topics create greeting \                                                                                       
--message-encoding=JSON \
--schema=greeting
Created topic [projects/kzs-sandbox/topics/greeting].

Subscription を作ります。

❯ gcloud pubsub subscriptions create greeting \                                                                                     
--topic=greeting
Created subscription [projects/kzs-sandbox/subscriptions/greeting].

コンテナのイメージを Publisher / Subscriberそれぞれビルドします。コンテナイメージのビルドには Cloud Build が便利です。

❯ gcloud builds submit — tag asia-northeast1-docker.pkg.dev/kzs-sandbox/public/publisher:v1❯ gcloud builds submit — tag asia-northeast1-docker.pkg.dev/kzs-sandbox/public/subscriber:v1

これで下準備は完了です。

Cloud Run にデプロイをして動きを見る

Publisher のデプロイをします。

❯ gcloud beta run deploy publisher \
--image=asia-northeast1-docker.pkg.dev/kzs-sandbox/public/publisher:v1 \
--allow-unauthenticated \
--set-env-vars=PROJECT_ID=kzs-sandbox,TOPIC_ID=greeting \
--service-account="******@kzs-sandbox.iam.gserviceaccount.com" \
--no-cpu-throttling \
--min-instances 1

次に Subscriber のデプロイをします。

❯ gcloud beta run deploy subscriber \
--image=asia-northeast1-docker.pkg.dev/kzs-sandbox/public/subscriber:v1 \
--allow-unauthenticated \
--set-env-vars=PROJECT_ID=kzs-sandbox,SUB_ID=greeting \
--service-account="******@kzs-sandbox.iam.gserviceaccount.com" \
--no-cpu-throttling \
--min-instances 1

--no-cpu-throttling が Always on CPU を使うためのオプションです。また、Shingo-san の記事にもある通り、--min-instances オプションを指定しておかないと Idle 状態(リクエストが無い状態)のコンテナインスタンスは CPU は allocate された状態であっても、スケールインして最終的にコンテナインスタンスが 0 になってしまいます。期待値としては非同期のワーカーとしてずっと動いていてほしいので、--min-instances オプションで常に稼働するコンテナインスタンスの数を指定します。

デプロイが無事完了したので、Cloud Run のメトリクスやログを見てみましょう。
ご覧の通り、デプロイした Cloud Run のサービスに HTTP リクエストを投げずとも、期待値通り Publish と Pull を行えています。

Publisher の ログ
Subscriber のログ

Subscriber のコンテナインスタンス数の推移です。--min-instances オプションを指定したおかげで、30分以上に渡り、概ね 1 個のコンテナインスタンスが常に稼働しています。10:20 頃に一度 0 になってますが、--min-instances オプションは常に同じコンテナを稼働させるための機能ではなく、時々コンテナの入れ替わりが走るので注意が必要です。

Subscriber のコンテナインスタンス数推移

Cloud Run 全般に言えることですが、なるべくステートレスな作りにして頂きつつ、コンテナインスタンス数は --min-instances を使って予め余裕を持たせておいて頂くと良いと思います。また突然落とされたときのデータ欠損などを避けるため、SIGTERM をハンドリングしてグレースフルシャットダウンする処理もアプリケーションに実装しておくことをおすすめします。実装はこちらを参考にしてください。

このあとも Publisher / Subscriber 双方を実行させ続けましたが、Always on CPU と --min-instance オプションにより、CPU の Allocation も維持され、コンテナインスタンスも 0 にならず、2 時間以上動作し続けてることを確認しました。

最後に皆さん気になるオートスケールです。

Subscriber に CPU 使用率を無理やり上げる用のハンドラを用意して、その中で goroutine を使って非同期でフィボナッチ数列計算をさせてみます。

では、フィボナッチ計算をさせる Subscriber のパスにアクセスしてみます。

❯ curl https://subscriber-5kwp3dlxxa-an.a.run.app/fibNow I’m doing fibonacci.

Subscriber のメトリクスを見てみます。
CPU の使用率は期待通り 100 % 張り付いています。

一方、コンテナインスタンスの数は突発的に 2 つになりましたが、基本、1 のままです。

Subscriber のコンテナインスタンス数

公式ドキュメントに記載の通り、Cloud Run はリクエストの量と、コンテナインスタンスの CPU 使用率(60%をターゲット)、--concurrency(default: 80)、--min-instances、 --max-instances の設定値を考慮してオートスケールします。

つまり、リクエストが全く無い状態ではオートスケールが作動しないことになります。ワークアラウンドとして現状考えられるのは以下 2 つです。

  • 予めコンテナインスタンス数に余裕を持たせる
  • --min-instances の数を増やして手動でスケールアウトさせる

予めどのくらいのメッセージが Pub/Sub に流れてくるか把握出来ている場合は、上記ワークアラウンドでも対応出来そうですが、そうでない場合はちょっと厳しいかもしれません。

まとめ

如何でしたでしょうか? 最後に確認したかったポイントとその結果を振り返ってみましょう。

CPUがスロットルされることなく、Pull し続けられるのか?

Yes, --no-cpu-throttling オプションを使うことで Always on CPU が有効化される。

リクエストが全く無い状態で Publish / Pullの処理を走らせられるのか?

Yes, アプリのエントリポイントでうまく listen serve させつつ、Publish / Pull の処理も実行出来れば可能。Handler 中で実行しても良し。

リクエストが全く無い場合、通常だとコンテナインスタンスが 0 になるが、--min-instance オプションで回避出来るのか?

Yes, 常駐型の非同期 worker として期待通りに動作。

オートスケール出来るのか?

CPU 使用率の上昇だけでは出来ない。オートスケールさせるためには相応のリクエスト数が必要。

Always on CPU の現実的なところがご理解頂けたかと思います。リクエストを受けて非同期処理をするようなワークロードであれば、上述のオートスケールの課題感も無く使って頂けると思います。一方、リクエストが全く無いワークロードを使おうとすると、そのスケール方法には前述の通り課題が残っているのが実情です。ここは今度の展開に期待頂ければと思います。

Always on CPU によって Cloud Run のユースケースが飛躍的に拡大したと思います。皆さんもぜひ使ってみてください!!フィードバックもお待ちしてます!!

--

--

Kazuu
google-cloud-jp

Customer Engineer at Google Cloud Japan. GKE & Cloud Run enthusiast. Opinions are my own, NOT views of my employer.