Cloud RunのAlways on CPUとPub/SubでDB書き込みを最適化した話
こんにちは。metroly inc の大城です。
ちょっと前にCloud RunのAlways on CPUという機能が(まだPreviewですが)リリースされました。さっそく使ってみたので、レポートを書いてみようと思います。
僕がなぜCloud Runが好きなのか?
これまで色々なComputeサービスを使ってきましたが、ダントツで運用が楽です。サーバレスなので、勝手にスケールしてくれて利用されていない時はゼロスケールしてくれます。本当に使った分だけの課金がされますので、これを使っておけば「ほとんどの場合最も安いはず」という安心感も得られます。
さらに少し前からHTTP LoadbalancerのBackendにおけるようになったりEgressやIngressの指定ができるようになったりして、IAP、CDN、URL Mapsなどといった高度なネットワークの機能も使えるようになってもうどんどん死角がなくなっている印象です。
Always on CPUがもたらす新しいユースケース
そんなお手軽かつパワフルなCloud Runですが、これまでHTTPリクエストに対してレスポンスを返すことしか出来ませんでしたので、指定されたインスタンスを常駐させてなにかの処理をさせる・・・みたいなユースケースは対応出来ず、そういうワークロードはGKEにデプロイしていた人も多いと思います。
「サーバレスが好きだからとことんサーバレスに構築したい」といった開発者には今回のリリースは朗報だったんじゃないかなと思います。
今回のユースケースについて
弊社サービスでは、一日に一回ユーザーごとにバッチ処理をして、その結果をデータベースに書き込む処理をしています。
データベースはCloud SQL (PostgreSQL) を使っているので、DBの能力に合わせて書き込んで上げる必要があります。
そこでこの様なアーキテクチャを取りました。
毎晩Cloud Storageに結構膨大な量のデータが作成されるのですが、そこからPubSubに書き込んで、そのメッセージをWriterと呼ばれているサービスでCloud SQLのI/Oが耐えられるデータ量を順にPullをしながらCloud SQLに書き込むことをやります。
毎日のバッチ処理以外にもいつCloud Storageにデータが書き込まれるかわからないので、常に監視し続ける必要があります。
コードとして抜粋するとこんな感じです。
(RxJSだと同時並列処理が楽に書けて嬉しい)
export const main = (): void => {
logger.info(`subscribing to ${subscription.name}...`)
subscription.on("message", (message: Message) => {
message$.next(message)
})
const result$: Observable<{ writeResult: WriteResult }> = message$
.pipe(
map((message) =>
defer(() =>
from(processMessage(message))
)
)
)
.pipe(mergeAll(maxConcurrency))
result$.subscribe((v) => {
const text = `inserted: ${v.writeResult.inserted}\tdeleted: ${v.writeResult.deleted}\ttook: ${v.writeResult.elapsed}sec`
logger.info(text)
})
}
maxConcurrency で指定した数のメッセージを同時に処理していきます。main()は終了しないので、プロセスが起動されている限りは常にPub/SubのSubscriptionを監視する状態になります。
かなりお手軽にDBへの負荷を一定以下に保ちながら全件のデータの書き込みが成功しました。Cloud SQLのCPU負荷を見てみるとこんな感じです。今日は書き込みに40分ぐらいかかりました。
節約術
Production環境は別にいいのですが、開発環境やステージング環境でも同じことをやるとお客さんがいないのに常にコストがかかるのがちょっと微妙です。そこで弊社では、開発している時間帯だけ Cloud Runの --min-instanceを調整することをやっています。
Cloud SchedulerとPub/SubとCloud Buildを使って実施しています。Terraformのコードはこんな感じです。
resource "google_cloudbuild_trigger" "scale" {
count = var.is_prd ? 0 : 1
name = "scale"
description = "Managed by Terraform"
tags = ["scale"]
build {
step {
name = "gcr.io/cloud-builders/gcloud"
args = ["run", "services", "update", "$_SERVICE_NAME", "--region=$_REGION", "--min-instances=$_MIN_INSTANCES", "--max-instances=$_MAX_INSTANCES"]
}
substitutions = {
_MIN_INSTANCES = "$(body.message.data.MIN_INSTANCES)"
_MAX_INSTANCES = "$(body.message.data.MAX_INSTANCES)"
_REGION = module.service.location
_SERVICE_NAME = module.service.service_name
}
}
pubsub_config {
topic = google_pubsub_topic.scale.id
}
}
最後に
Cloud Run Always on CPU の使い方について書いてみましたがいかがだったでしょうか? サーバレスが好きで、常駐プロセスを書いてみたい人は是非試してみるといいんじゃないかと思います。コンテナ作るだけでいい感じに運用が出来て本当に手軽になったと思います。
GKEもだいぶ使いやすくなってきたとはいえ、なんだかんだGCPで手軽さKingはCloud Runだと思います。これからもウォッチしていこうと思います。
---
私達は一緒に世界で戦えるプロダクトを開発してくれるエンジニアを募集しています。エンジニアの採用枠を今回増やしましたので、ぜひ興味のある方はゆるく情報交換からでもお願いします! 詳しくはこちらから御覧ください。
この記事が気に入ったらサポートをしてみませんか?