Cloud RunとFireStoreでサービスを作った

はじめに

ここ最近は主に身内で使うための動画キューサービスを作っていました。
最初はVPS上で動くPHPとUnityクライアントで作っていたのですが、
最近GCPのサービスに触れることが多くなってきたため、Go+Unityの構成に変更してみました。

その結果料金は100万リクエストに対して20円、APIの応答速度も5-7ms、
気軽にデプロイしてログもいい感じに見られるようになりました。

この記事では実際にGCPのプロダクトを使ってみた感触や、細かなポイントについて解説していこうと思います。

 

元々会社内向けの記事だったのですが、みんな見てもいいよねって事でこちらに公開することにしました。

リクエストを解析してみる

まずは使っているサービスにどれくらいのアクセスが来ているかを解析してみます。
現状のアクセスはこんな感じになっています。

人が集まる時間にリクエストが突然伸びて、居なくなるとほぼアクセスが無くなるという特徴があります。
そうすると、アクセスのない時間帯の料金は全く無駄になってしまうので、VPSのような24時間立っているサーバーは不向きで、
GCPならCompute EngineではなくCloud Functions、Cloud Runなどが向いています。
今回はCloud Runを利用しています。

Cloud Run

Cloud Runはフルマネージドのサーバーレスプラットフォームで、スケーラブルなコンテナ化されたアプリケーションをデプロイできます。
コンテナ化されたアプリケーションをデプロイするサービスはAWSだとECS、Fargateあたり、最近だとLambdaも対応しているみたいです。

コンテナをデプロイする系のプロダクトは、我々アプリケーション開発者があまり考えたくないOSのセキュリティに関する事情をGCP側に押し付けることができます。
OpenSSLの脆弱性が出たとしても、Googleが頑張って対応してくれます。
Laravelや、Goのバージョンに依存する脆弱性が出たら我々が引き続き対応することにはなりますが、、

また、リビジョンの切り替えが一瞬で行われることも非常に嬉しいです。
間違えて動かないバージョンをデプロイしても、コマンド一つで好きな前のバージョンに戻せるし、
新しい機能をデプロイする場合、利用者が利用中でも基本的にメンテ無しでデプロイできます。

また、サーバーレスあるあるのコールドスタートが遅い問題にも触れておきます。
下の図の「server is running」と出ている箇所の下のリクエストがコールドスタート時の応答時間ですが、
100-400ms程度で応答できていて、まあまあ早い部類?かなと思います。

あとフロントをSPAにしておいて、認証をJWT等データベース不要なものにしておくと、
RDBなどの接続の初期化の前にフロントのjsがレスポンス可能なので、ちょっとお得です。

1コンテナあたり1-1000リクエスト同時に受けられるので、ある程度アクセスが有るときは特に気にせずとも応答が可能です。
(しかも、CPUとメモリ料金はコンテナ単位でかかるので、同時にリクエストを捌いているときは安くなります)
https://cloud.google.com/run/pricing?hl=ja

koが凄い

Q.「でも、コンテナ化してデプロイするならdocker入れたりしないといけないんだよね?Dockerfile分からん」
A.「要りません」

koというGo製のアプリケーションをデプロイする専用のツールがあります。
これを使うとDocker Desktopをインストールせずにワンライナーなコマンドでdeploy出来てしまいます。
https://cloud.google.com/blog/ja/products/containers-kubernetes/ship-your-go-applications-faster-cloud-run-ko

実際にデプロイに使用しているコマンドはこんな感じです。
たったこれだけでユーザーが閲覧できる状態になります。

KO_DOCKER_REPO=gcr‌.io/XXX/api gcloud run deploy api --image=`ko publish main.go` --region asia-northeast1 --allow-unauthenticated --cpu=1

データストア

NoSQLにしました。次の観点で判断すると良いです。
- データを全部JSONで保存するって考えた場合できる?→できる
- 複雑なリレーションやOrder Byなどを書きたい?→書かない
- 安い方がいい?→安い方がいい

ソシャゲとかだと結構NoSQLは向いている場合が多いです。こんな感じ

User {
    Character {
        ...
    },
    Item {
        ...
    }
}

今回はデータはこんな感じになっています。(本当はもっといろいろあります)

Room {
    Queue: [
    {URL: "XXX", User: "ZZZ"},
   ],
    RoomMasterID: "XXX",
    CreateRoomDate: "2020-12-02T15:04:05+09:00",
}

Firestoreは1日5万件まで読み取り無料なので、ちょっと超える可能性があるのと更に高速化したかったのでRedis Enterpriseを利用しました。
Redis Enterpriseには実は30MBまでの無料枠があり、これを利用しています。
保存したかったデータは精々50KB程度なので、今回の用途ではこの無料枠が適していました。
https://redis.com/redis-enterprise-cloud/pricing/

ここまで使ってみて、実際に動画のリクエストを送信→キャッシュの効き具合をログから見てみます。
送信時はYoutubeAPIなどに実行時間が引きずられるので200msかかっています。
最初のGETリクエストはFirestoreからの読み出しを行ってRedisに書き込みを行っています。
その後はRedisのみのアクセスとなっていて非常に高速に読み取りが出来ているのが分かります。

ちなみに、アプリケーション側でキューの内容が一緒だった場合レスポンスを簡略化する仕組みが入っています。
これがデータ量の削減に繋がっていて、更に料金を安くすることに成功しています。

関係ないリクエストにはReactデカいレスポンスを送りたくない

フロント画面ではantdを使っています。
https://ant.design/

ただ、ルートパスからReactにしてしまうと、利用者でないbotなどにも1MB程度のjsを送ってしまいます。
GCPに限らず、多くのプラットフォームでは上りに対して課金が発生するため、なるべく関係のない人にレスポンスは送らないようにしたいです。

なので、フロント画面はパスを一つ掘ったところに配置して、ルートパスにはフロント画面へのリンクを配置しただけの画面を用意します。
これだけでかなりのデータ量削減に繋がっています。

クライアント関連

Unityは最近本当に書いていないので、多分ちゃんと書いている人から見ると色々怒られると思います。
大変だったのが、以下の点でしょうか。Unityじゃないところが大変だったかもしれないです。
- ニコニコのコメントのAPIがめちゃくちゃ分かりにくいため、対応が大変
- Spotifyのクライアントが実はURLを指定して流すのに対応していない。のでSpotifyのキューに入れてすぐスキップしているのですが安定させるのが大変だった。
- 人によってUnityで一切映像が流れない問題が起きていた。どうもWindowsMediaFoundation側の問題で結局その人はOS再インストールで直った。

料金を見てみよう

実際にかかった料金はCloud Runの上り0.9GiBの料金の15円だけでした。
最終的にはレスポンスの圧縮が料金に直結してくるので、不要なレスポンスを返さない、簡略化する、
寝落ちしているっぽい人は切断する等をやると料金が改善されました。
工夫を凝らすとそれだけ料金が安くなるので、そこのあたりが作っていて楽しかったです。

最終的なインフラ構成

こんな感じになりました