日比谷音楽祭おさんぽアプリ2020 開発の裏側を語る / サーバー編

by Shinjiro Sugita Naoki Kishi Yusuke Shikanai | June 18, 2020
event | #dena #dena-engineers-blog #gcp #golang #firebase

この記事は20新卒、21卒内定者が開発に参加した『日比谷音楽祭公式おさんぽアプリ2020』(以下、おさんぽアプリ)開発の裏側、サーバー編です。


この記事の概要

  • .protoファイルのおかげでモックの作成とチーム間コミュニケーションがスムーズになった
  • チケット抽選機能の要件に適したCloud Functionsを使った
  • Cloud Loggingで構造化ロギングを利用してアラート設定をした
  • Trace IDを一致させ、Cloud Traceでウォーターフォールビューを見られるようにした

日比谷音楽祭2020とおさんぽアプリ

2020年5月30日-31日の2日間、東京・日比谷公園で開催される予定だった『日比谷音楽祭 2020』。「フリーで誰もが参加できる、ボーダーレスな音楽祭」として去年に引き続き第2回の開催を予定していましたが、残念ながら新型コロナウイルス感染拡大の影響により中止となりました。
日比谷音楽祭の中止に伴い、DeNAはそれまで開発していた「会場で遊べる」おさんぽアプリを「おうちで楽しむ」おさんぽアプリとしてアップデートしました。アプリリリースまでのエピソードは弊社オウンドメディアのフルスイング『音楽×ITのコラボで「おうちで楽しむ」音楽祭を実現。その内容は?』でも取り上げています。
そんなおさんぽアプリの開発の裏側について、本ブログではサーバー編、クライアント編の2回に分けてご紹介します。 クライアント編に先立って今回の記事ではサーバー編をお届けします。

全国に散らばったサーバーチーム

はじめまして、サーバーチームでおさんぽアプリの開発をしていました20新卒の杉田です。
はじめにサーバーチームの構成や働き方について簡単にご紹介したいと思います。
サーバーチームは開発をリードする社員1名と内定者4名(20新卒1名、21卒内定者3名)という思い切ったチーム構成でした。

メンバーは全国に散らばり、フルリモートでおさんぽアプリの開発を進めていました。 やはりフルリモートでの開発にはコミュニケーション面での不安がありましたが、週一回のミーティングやSlackチャンネルでのラフなコミュニケーションのおかげで、フルリモートが開発のネックになることはありませんでした。

ミーティング議事録の最後にはメンバーそれぞれがその時の気持ちを書く欄があり、その何気ない内容から広がる雑談は毎週の密かな楽しみでした。

おさんぽアプリのサーバーで使用した技術について

次におさんぽアプリのサーバーで使用した技術についてご紹介したいと思います。この章はサーバーチームより20新卒の杉田、21卒内定者の岸、鹿内の3人でお届けします。

Cloud RunとgRPC

Cloud RunとgRPCについては杉田が担当します。
サーバーチームには技術選定の軸の1つとして「学生にとってできるだけ新鮮な技術を使ってもらいたい」という思いがありました。
そこで選んだのがGCPのCloud Runと、Cloud Runで昨年サポートが追加されたUnary gRPCの利用です。 Unary gRPCは2019年9月25日にCloud Runでのサポートが追加され、その後Cloud Runは2019年11月14日に一般利用可能(Generally Available)となりました。 簡単にCloud RunとgRPCの特徴についてまとめてみます。

Cloud Run の特徴

  • コンテナをサーバーレスで実行できる環境
  • フルマネージド環境または GKE クラスタで実行できる
  • リクエスト数に応じたオートスケーリング機能
  • 料金はコードが実行されている間のみ発生

おさんぽアプリではgRPCやバッチ、デバック用のREST APIなどいくつかのコンテナに分けたGoのサーバーをフルマネージドのCloud Runにデプロイして利用しています。 GCPではコンテナを実行する環境としてGoogle Kubernetes Engineも提供されていますが、おさんぽアプリではマシンを細かくチューニングするほどの拡張性は必要なかったためフルマネージドのCloud Runを使うことにしました。

gRPC の特徴

  • Googleが開発を始めたのオープンソースのRPCフレームワーク
  • HTTP/2の上で動作する通信規格
  • 一般的にはProtocol Buffersでシリアライズされたデータを通信で利用する

昨年、Cloud RunでUnary gRPCプロトコルが利用できるようになりました。 そこで、おさんぽアプリサーバーチームでは内定者にgRPC経験者が少なかったこともあり、クライアントとの通信にgRPCを採用しました。
私もgRPCは使ったことがなかったので開発でgRPCを使うと決まった時はワクワクでした^^

そして、実際にgRPCを使い始めるといくつかの良さが分かってきました。

gRPCを使ってみて良かったこと

  1. .protoファイルのおかげでAPI実装の認識が揃う
  2. 簡単にモックを用意できる

gRPCを使ってみて良かったことその1について、gRPCではProtocol Buffersのプロトコル定義を記述した.protoファイルから各言語のコードを生成して使用します。.protoファイルにはRPCサービスを定義することができるため、Protocol Buffersはインタフェース定義言語 (IDL)としてを役割を果たします。

実際に.protoファイルにRPCサービスを定義するには以下のような記述をします。

syntax = 'proto3';

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

grpc.io Introduction to gRPC より

.protoファイルを見るだけでどんな関数が定義されているのか推測できると思います。今回のおさんぽアプリでも.protoファイルのおかげで、サーバーチームとクライアントチームの間でAPI実装に対する認識のズレを減らすことができました。これはgRPCを使って良かったことの1つだと思います。

次にgRPCを使ってみて良かったことその2、簡単にモックを用意できることについてです。
gRPCのサーバーに新しいサービスを追加したい時は「.protoファイル記述 →protocで各言語のコードを生成 →インターフェースにしたがった実装を追加」という流れでモックを作成することができます。

たとえば上で紹介した.protoファイルからprotocでGoのコードを生成すると以下のようにサーバー用のinterfaceが定義されます。

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
	// Sends a greeting
	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

あとはこのinterfaceを満たした実装を追加すると簡単にモックを作成することができます。 簡単にモックを用意できることは今回のおさんぽアプリのようにサーバーとクライアントでチームが別れている場合に大きなメリットとなります。おさんぽアプリでもモックを用意したおかげでクライアントチームを待たせることなくスムーズに開発を進めることができました。

逆にgRPCを使って不便だと思ったことには慣れたcurlコマンドを使ってデバッグができないことがありましたが、curlライクなgRPCurlを使うことで解決できました。

Cloud Functions

この章では鹿内しかないが、端末へのpush通知の実装で用いたCloud Functionsについて、技術選定理由を中心に紹介します!

Cloud Functionsの概要

  • サーバレスのランタイム環境
  • イベントに関連するシンプルな関数を作成できる
  • 対象イベントが発生すると、Cloud Functionsがトリガーされ、コードが実行される
  • 負荷に応じた自動スケーリング

使いどころ

新型コロナウイルスの影響で残念ながらイベントは無くなってしまいましたが、本来であればアプリ内にチケット抽選の機能があり、抽選結果を参加者にpush通知で飛ばす仕様がありました。

この章で紹介するCloud Functionsは、主にこのpush通知の実装で使っていきました。

技術選定について

今回push通知の実装は、各端末への通知の送信にFCM(Firebase Cloud Messaging)を使い、ユーザに通知トピックを購読させることで、そのトピックに対して通知を送るという流れで実装しています。 この際にトピックの購読では、FCMへAPIリクエストが発生するためにpush通知自体が遅い処理となってしまい、同期的に処理してしまうとAPIサーバのパフォーマンスに影響を与えかねません。 そのため遅い処理を非同期で処理できるといった観点で、使えそうな技術を選定していきます。

プロジェクトでGCPを使うのは決まっていたため、GCPのサービスでそのような非同期処理で考えられるのはCloud FunctionsCloud Tasksでした。

Cloud Functions Cloud Tasks
特徴 pull型 push型
メリット ・小さな単位(Function)でロジックを組み立てれる
イベントをトリガーに実行できる
・実行をコントロールしやすい
(スケジュール実行や一時停止が可能)
デメリット ・実行のタイミングがコントロールできない ・タスクキューを作成する必要がある
・専用のHTTP Targetハンドラが必要

Cloud FunctionsとCloud Tasksは非同期で処理を回せるという部分は似ていますが、今回の技術選定において最も考えるべきポイントはpush型かpull型かだと思います。

チケット抽選の用途から考えて、全ユーザがpush通知の対象とはならないため、push通知の流れとして発火の起点はアプリユーザが行うアクションにしたいです。 そのためアプリユーザのアクションをトリガーとして、処理を紐付けるといった実装が今回のpush通知に適していると考えました。

これらの理由からサーバからユーザに向けてのpush型よりも、ユーザからのアクションが起点となるpull型を採用することとし、Cloud Functionsを使っていくことになりました。

実際にpush通知のトリガーにした”アプリユーザのアクション”は、プロジェクトのDBにCloud Firestoreを使っていたこともあり、Cloud Firestoreトリガーを採用することとしました。 これによりユーザがチケット抽選に応募した際に、応募したチケットのデータをFirestoreへ記録するだけで、Cloud Functionsのトリガーを発火させることができ、ユーザのアクションからトリガーするpull型を実現することができました。

Cloud Functionsデプロイできないバグ!?

最後に開発中に起きたハプニングを紹介します!

開発も少し落ち着いてきて詰めの作業にチームで奮闘していこうといったところで事件は起きました。

「Cloud Functionsにデプロイできなくなった…」

それまではデプロイできていたにも関わらず、突然全Functionにデプロイできない状況になりました…。 チーム内で色々と探っている内に、go.modのmoduleを

--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module hoge.domain/project/fuga
+module hoge.domain/project/
 go 1.13

↑このように書き換えるとデプロイ可能になるworkaroundを見つけました。 しかし以前と実装を変えていないにも関わらず突然デプロイができなくなり、その原因の特定ができなかったため、GCPサポートに問い合わせるといった流れになりました。

結論としてはgo113 runtimeで起きていたバグのようで、まだruntimeがbeta機能であることから発生してしまったバグだったようです。 (調査中にも我々と同じバグを報告した方がいたようで、どうやら多くのGCPユーザの間で同時発生的に起きていたバグみたいです)

開発もほぼ終盤で起きたハプニングでしたが、GCPのような大きいプラットフォーム側のバグを踏み抜き、サポートと連絡を取り合うことでバグの特定をしていくといった貴重な経験をすることができました。

Cloud Logging & Cloud Trace

ここからは岸が監視の面について紹介します。

今回のプロジェクトはGCP上に構築されていることもあり、監視ツールとしてGoogle Cloudのオペレーション スイート (旧Stackdriver) を利用しています。

Cloud Loggingの構造化ロギングとネストされたログ表示機能

Cloud Runはsdtout、stderrに出力したログを自動でCloud Loggingに送信しています。 しかし、デフォルトではエラーレベル (severity) の細かい設定は出来ません。このままではアラート設定がしづらいので、構造化ロギングを用いてserverityを指定してログを出力できるようにしました。

また、GAE第一世代で非常に便利だった、「あるリクエストに対応するアプリケーションログをネストして表示する機能」も実装しています。Cloud Loggingでは共通のTrace IDを持つなどいくつかの条件を満たすことで、ログをネストして表示することができます。gRPCのInterceptorで各リクエストごとにメタデータからTrace IDを抜き出して作成したLoggerをContextに埋め込むことで、任意の場所でリクエストと関連付けられたログを出力できるようにしました。

この機能のおかげで、「失敗したレスポンスに関連したログ」などを簡単に確認することができ、デバッグやバグ調査がとても捗りました。

参考 : cloud.google.com/go/logging - pkg.go.dev

Cloud TraceとCloud Loggingの連携

Cloud TraceはGoogle Cloudが提供する分散トレースシステムです。「APIリクエストに対するCloud Firestoreへのアクセスのレンテンシの割合」などが直感的に把握できるので、負荷試験のタイミングで頻繁に確認してしました。

また、Cloud Traceで使用するTrace IDとCloud Loggingで使用するTrace IDを一致させることで、Traceのウォータフォールビュー内でログも同時に見れるようにしました。

予定ではこの逆、つまりCloud LoggingからCloud Traceを参照できるような設定もしたかったのですが、残念ながら実現することは出来ませんでした。

▲「トレースの詳細表示」が押せない

▲「トレースの詳細表示」が押せない

公式ドキュメントによると、

App Engine リクエストログで新たに Cloud Trace へのリンクが設けられ、ログエントリのレイテンシの詳細を簡単に表示できるようになりました。

と書かれており、GAE以外で連携するのは難しいようです。

ログの表示(従来版)| Cloud Logging | Google Cloud

おわりにひとこと

この記事では『日比谷音楽祭公式おさんぽアプリ2020』の開発の裏側をサーバーサイドの観点からご紹介しました。
続く、『日比谷音楽祭おさんぽアプリ2020 開発の裏側を語る / クライアント編』もお楽しみください。

おわりに記事を書いたサーバーチーム3名の開発に参加した感想です。

岸 直輝

Goは以前から触っていましたが、GCPのマネージドサービスをフルに使った開発は初めてだったので、楽しんで開発することができました。監視など学生の内ではおざなりにしがちだった部分の経験を積むことが出来たのも非常に良かったです。

鹿内 裕介

プロジェクトに入った当初、入社前の学生が開発の中心として進めていくと聞き、驚きと同時に、そこに自分も属して切磋琢磨できるということで、頑張って良いものにしようとやる気に満ち溢れたのを思い出します。 開発の内容としても自分はサーバサイドを経験するのが初めてだったため、このプロジェクトを通して吸収できたことは数え切れないほどあり、本当に良い経験になりました。

杉田 親次朗

Go、gRPC、Cloud Run、私にとって初めて尽くしの開発で、たくさん勉強させてもらいました。おさんぽアプリでの開発経験は新卒入社後のエンジニア研修でも大いに役立ちました。一緒に開発してくれたサーバーチームの4人にはとても感謝しています。ありがとうございました!