GAE/Goで位置情報マッチングAPIの設計を考える
先日、サイバーエージェントさんの学生版ガレスタというインターンに参加しました。
6週間で企画からサービス完成までを4人(ビジネス1人, デザイナー1人, エンジニア2人)で行うというものでした。
今記事ではこのインターンで得た技術的なことを、つらつら書いていきたいと思います(´・ω・`)
要件
位置情報を用いたマッチングモバイルアプリのAPI
技術選定
言語
- チャット実装時のマルチスレッド処理が簡潔
- 薄くAPI作れるから短期間開発○
- 丁度いい型安全
- 開発効率を上げるエコシステムが整ってる
- モバイルAPIの実例多い
- インターン後仲間集める時に、若く優秀なエンジニアを集めやすい
http通信
- 薄い
- 慣れてる
- 標準なのでバージョン管理安心
ORマッパー
RDB
- 慣れてる
KVS
- 3.2から追加された"Geo"系コマンドを使って位置情報管理したい
インフラ
- サーバーサイドは一人なのでとにかく開発に集中したい
- PaaSの中で最も妥当な柔軟性、料金と感じた
アプリケーション設計
$GOPATH ├── app │ ├── app.yaml │ ├── config.go │ ├── handler.go # Application層 │ └── main.go # UI層 └── src ├── domain # Domain層 │ ├── config.go │ ├── detail │ ├── feed │ ├── location │ └── registration ├── infra # Infrastructure層 │ ├── cache │ ├── config │ ├── objstorage │ └── orm └── middleware
レイヤードアーキテクチャを用いた軽量DDDを採用しました。
DDDを完璧に実現しようとすると、分析、設計、実装というフローを踏まないといけないのですが、今回は短時間且つ仕様も変わっていくため、分析はせず軽量なDDDを実現しました。
DDDについてはまとまった知識がなかったので、今回エリック・エヴァンスのDDD本を初めて読みました。
GAEは、app.yamlが置いてあるディレクトリ以下を全てビルドしてしまうので、Domain層以下は別packageとして管理しています。
集約毎にディレクトリを分け、その中でentity、value object、repositoryを定義しています。
infra配下ではDBに依存するコードをまとめ、出入りするデータをrepositoryにinterfaceとして定義しておきます。
このようにすることで、ミドルウェアの差し替えを容易にしています。
GAE/GoのDDD設計はまだまだデファクトスタンダードが定まっていないようなので、これからも熟考していこうと思います。
インフラ設計
全体アーキテクチャ
ほとんどのデータはCloudSQLにMySQLを乗せて管理しました。
CloudSQLはフェイルオーバー時に数分のダウンタイムが発生してしまうというデメリットがありますが、短期開発ということで立ち上げ時の設定が楽であるという最大のメリットを汲み採用しました。
CloudStorageでは画像中心に、サイズの大きいオブジェクトを管理しています。
特筆すべきはredisです。
redisによる位置情報管理
位置情報機能の要件は"近い順にアクティブユーザーを返す"でした。
そこで最初に考えたアーキテクチャはこんなものでした
列志向のBigtableに位置情報を溜めていき、アクティブユーザーを返す度にBigQueryを回すというものです。
ありえませんね。
ユーザー毎にクエリを回すというこのモデルではパフォーマンスにもお財布にも優しくないですね。
サイバーエージェントの社員さんに聞いたところ、redis3.2から位置情報を扱うコマンドが追加されたらしく、使ってみることにしました。
>>Aさんから半径100km以内のユーザーを近い順に頂戴!<<
わがままですね。
でもこんなわがままを、ジェントルマンなredisは1コマンドで叶えてくれます。
GEORADIUSBYMEMBERという夢のコマンドをredisにささやきます。
// GEOADDで追加 > GEOADD location 13.583333 37.316667 "Aさん" 13.361389 38.115556 "Bさん" (integer) 2 // GEORADIUSBYMEMBERで取得 > GEORADIUSBYMEMBER location "Aさん" 100 km 1) "Aさん" 2) "Bさん"
GEOADDでsorted set型の集合にぶっこみ、ソートします。
GEORADIUSBYMEMBERを使用すればO(N+log(M))で検索することが可能です。
*N=指定範囲内の要素数, M=インデックス内の項目数
これで当初よりシンプル&ハイパフォーマンス&低料金で要件を実現することが出来ました。
社員さんありがとうございます〜〜
テーブル設計
ゼロイチでDB設計したことなかったので、しっかり以下3つ読みました。
プライマリーキーをとりあえず"id"というサロゲートキーにしてしまうアンチパターン「idリクアイアド」を避け、
なるべく自然キーを用いた設計を心がけました。
API設計
ゼロイチでAPI設計したことなかったので、しっかりこれを読みました。
リソースのページネーションを実現する際、最初は相対位置を利用していましたが、絶対位置を利用することでパフォーマンスと整合性の改善をしました。
まとめ
何が言いたいのかまとまっていませんが、とりあえず多くの技術的学びがあったということです。
痛感したのは、どんなに短期開発でも時間かけるべき部分には大いに時間をかけるべきだということです。
どこを丁寧に行うか適切に判断することが出来れば、スピードと品質は必ずしもトレードオフにならないと感じました。
このアプリは近いうちにリリースするので、その時にまた報告致します。