タベリーを支えるアーキテクチャ

by

@wapa5pow

「タベリー」は株式会社10Xが提供するパーソナルな献立を推薦するアプリです。iOSAndroidWebで提供しています。先日、プレスリリースで「オンライン注文機能リリース」と「2.5億円の第三者割当増資を実施したこと」をお知らせしました。献立作成、献立からの買い物リスト作成、買い物リストをネットスーパーで注文、料理を作るということがタベリー1つでできます。特にこの「オンライン注文機能」はいままでネットスーパーの商品を1つ1つ選択して注文していたものを、自動でカートに追加し注文できるのでとても便利です。

news_20190514-app

10Xではよりよいチームを目指しメンバーを募っています。エンジニアも募集しています。チームがどのように開発しているかは社長の矢本さんが書いた「10Xなプロダクトを創る」に詳しく(ほんとに詳しく結構長い)のっているのですがエンジニアが気になるどのような技術を使っているかに関して今回このブログで紹介しようと思います。少しでもチームに興味を持ってもらえたら採用フォームがあるのでぜひぜひこちらからご応募ください。

アーキテクチャ

tabely_architecture

タベリーはGCP上に構築されており使っているサービスは上記のようになります。少人数でインフラに悩まされずに開発に集中できるようにKubernetesを導入しており導入当時東京リージョンでマネージドのKubernetesを出しているのはGCPだけだったのでGCPが選定されています。

iOSのアプリはSwift、AndroidはKotlin、ウェブブラウザ用のフロントエンドはNuxt.jsで書かれています。iOSとAndroidが接続するバックエンドはGoで書かれています。フロントエンドとバックエンドの接続にはgRPCを使っています。Nuxt.jsはJavaScriptで書かれていますが、Nuxt.js以外は型がありgRPCのProtocol Buffersで生成したインターフェースとの相性がいいです。フィールド名を変えたとしても型で検知できるのでミスが起きづらくなります。型があるとリファクタもしやすいです。何回もリファクタしているのですがその都度型があってよかったーとつぶやいています。

Nuxt.jsとGoのサーバはContainer Engine(GKE)上に構築されています。GitHubのリポジトリにフロントエンドやバックエンドのソースコードがコミットされるとCircle CIが動き自動デプロイされます。手動デプロイだとエンジニアのコンテキストスイッチで時間が取られるので可能な限りさまざまなものを自動化するようにしています。Circle CIではテストやLintが通ってからコンテナイメージをビルドしてContainer Registryにプッシュします。すべてのイメージがプッシュし終わるとKubernetesにkubectl applyコマンドでデプロイします。GKEの環境は本番とステージングがありパラメータが違うものはkustomizeで調整しているのですが、最近はHelmにしてもいいかなーと思ってます。負荷に耐えるためにHorizontal Pod AutoscalerCluster Autoscalerが設定してありポッドとノードの数が自動でスケールアップ・ダウンするようになっています。

タベリーは献立作成を支援するアプリなのでレシピ画像などをCDNで配信しています。CDNのバックエンドとしてCloud Storageを使っています。CDNはimgixを使っています。imgixはクエリパラメータをつけるとCDN側で画像の大きさをリサイズしてくれるのでクライアントに適切なサイズの画像を配信できパフォーマンスが向上しネットワークコストを下げてくれるものです。最近だとさくらのImageFluxがありますが、タベリーをリリースしたときはなかったのでimgixを選択しました。

ユーザのデータはGoのバックエンドがCloud Datastoreに保存しています。大きな負荷がかかっていいようにCloud Datastoreが選定されましたが、NoSQLであるため最初は癖があり結構大変でした。複雑なクエリなどはかけないのでそのようなクエリはElasticsearchを使っています。Cloud Datastoreはトランザクションもサポートしているのでアトミックな操作が可能です。ただトランザクション内で更新できるエンティティ数が限られているので超えないように注意しなければなりません。Cloud Datastoreは今年の後半くらいにはCloud Firestoreに置き換わるらしくトランザクション内で扱えるエンティティの数が増えたりパフォーマンスが向上するらしいので楽しみに待っています。

Cloud PubSubはユーザ行動に起因するPush通知を送るためのジョブキューとして使っています。ユーザリクエスト中にプッシュ通知を送信しようとするとレスポンスに時間がかかるのでジョブキューに逃がしています。Kubernetes上にワーカーサーバをたててPubSubをSubscribeしています。いまは特に大きな負荷がかかってませんがKubernetes上だとオートスケールが楽なので今後もそんなに心配していません。

Cloud ML、Cloud Dataflow、BigQueryらへんは分析やリコメンデーションに使ってます。あとから説明します。

GKE上のNuxt.jsやGoのサーバが出力する標準出力はすべてLoggingに自動的に入ります(Fluentdをデフォルトで動かしてくれている)。AWSのEKSと違って自分でFluentdを設定する必要はありません。すごく楽です。Loggingはフィルタを設定してBigQueryに流し込むことができます。タベリーではバックエンドのリクエストログやサーバログはJSON形式で出力して必要なログをBigQueryに入れて分析に使っています。

監視はPrometheusとGrafanaを主に使っているためStackdriverはあんまり使ってません。導入当時Kubernetesを安く監視できるものがPrometheusとGrafanaだけだったのでそうなっています。最近ベータになったStackdriver Kubernetes Monitoring supportでポッドごとのCPU使用量とか見やすくなったのでいまだとこれが使えそうなきがしてます。Stackdriverにはカスタムメトリックスも送れるのでサービス固有のメトリックスを送ってそれをグラフ化することも簡単にできます。

Traceはバックエンドの各処理がどのくらいかかっているかとかCloud DatastoreやElasticsearchのリスポンスタイムが遅くないかどうかなどチェックするために入れています。前職の会社はRailsを使っていたのでNew Relicを使ってAPMがさくっととれたのですがGoではなかったのでTraceを入れて1リクエスト中の各処理の時間をとっています。

全体のアーキテクチャはこのくらいにしてフロントエンドとバックエンドのアーキテクチャをみていきます。

フロントエンド・バックエンドアーキテクチャ

tabely_app_architecture

アプリ(iOS/Android)からバックエンドはgRPCで接続しています。Cloud Load BalancingがL4となっているのはHTTP/2でgRPCは通信する必要があり構築したときはL7でその機能がなかったためです。gRPC over SSL/TLSをするために証明書を取得する必要がありいまはcertbotでLet's Encryptの証明書を使っています。Protocol BuffersでgRPCの定義を書いたらgRPCもRESTも同時に使えるようにする機能(Cloud Endoints for gRPC)があるExtensible Service Proxy(ESP)をサイドカーとしてGoのサーバと同じポッドにのせてバックエンドを提供しています。ユーザ認証はESPで使えるFireabseを使っています。ユーザ画像などをアップロードする画像APIだけはgRPCではなくREST APIを使っておりGoが処理しています。

ウェブはNuxt.jsで提供しておりNuxt.jsからはREST APIで接続しています。アプリのところで説明したESPとGoの同じポッドに接続しています。REST APIでgRPCのメソッドGetRecipeと同じレスポンスを返す/v1/getRecipe/{id}のREST APIを定義したければProtocol Buffersに次のように書けばいいので楽です。

service RecipeService {
  rpc GetRecipe (GetRecipeRequest) returns (GetRecipeResponse) {
    option (google.api.http) = { get: "/v1/getRecipe/{id}" };
  }
}

「Tabely Admin」と書かれているのは管理画面のことです。レシピを管理したりしています。管理画面のフロントエンドはNuxt.jsでバックエンドはGoで書かれています。Nuxt.js側からはCloud Load Balancing(L7)を通してgRPC-Webのプロトコルでバックエンドに接続しています。バックエンドはサイドカーとしてEnvoyとGoのサーバがのっています。EnvoyはgRPC-WebをgRPCに変換するためにいます。以前はREST APIだったので、gRPC-WebにしてProtocol Buffersでインターフェースが定義し、フロントエンドとバックエンドが同じ定義をみているので楽になったのですがJavaScriptでNuxt.jsがかかれており型の恩恵がうけれないのでウェブには適用していません。

Kubernetesのポッドの監視にはPrometheusとGrafanaを使っています。Goのプロセスが使用しているCPUやメモリ、ノードやポッドのメトリックス、ヘルスチェックなどすべての監視をここに集めています。Prometheusは独自のクエリ方式なのでほしいメトリックスを出すのがちょっとつらいです。最近個人的にDatadogを使ってなかなかいい感じでしたがPrometheusは無料でできるのが強みです(とはいってもそこそこのインスタンス使う必要があるので無料とはいいがたいですが)。

Goのエラー監視にはSentryを使っています。Goでエラー時のスタックトレースが見えるようにしています。gRPCのサービスやメソッド名、レスポンスタムなどもエラーログのプロパティ名に埋め込んでいるのですぐどこでエラーが起きたかわかります。特定のメソッド名はSentryに送りたくない場合があるのでその場合は送らないようにしています。

分析アーキテクチャ

tabely_analytics

基本的にすべてのログをBigQueryに集めるようにしています。BigQueryに集めることによりさまざまなデータソースからのデータを簡単にジョインして目的のデータを出すことができます。アプリ(iOS/Android)のログはFirebase Analyticsを使ってログをあつめFirebaseの設定でBigQueryにインポートするように設定してあります。ほぼリアルタイムでログがみえるので新しい機能をリリースした直後に数値を見ることもできます。Cloud Datastoreの特定のテーブルはKubernetesでCronJobでシェルスクリプトを動かしBigQueryにバックアップしています。GoのバックエンドのログはJSONで出力されているのでそれもBigQueryに入れています。

DigdagはBigQueryに保存されているテーブルの中間テーブルを作ります。FirebaseのイベントログをBigQueryに保存してそのまま30日分とかクエリをするとクエリが引くデータ量が多くなって大きなコストがかかります。よく使われるイベントはイベントごとに中間テーブルを作ってデータ量を少なくすればお財布にも安心です。

集約されたBigQueryのログはRedashで確認しています。RedashはSQLを発行しその結果をグラフにできます。作成されたグラフを集めてダッシュボードが作れます。指定されたSQLは定期的に更新できるので毎日朝に更新すれば出社時には最新のデータのダッシュボードをみることができます。

社長の矢本さんが主に分析クエリとダッシュボードを作ってます。エンジニア全員がSQLかけますが矢本さんに背中をあずけています(正直めちゃくちゃ助かってます)。

ときどきData Studioも使いますが、使用用途としては外部の人にデータを共有したい場合などです。

レコメンデーションアーキテクチャ

tabely_ml_architecture

タベリーではユーザの献立にどのレシピをすすめたらいいかというレコメンデーションにTensorFlowのtensorflow.contrib.factorization.WALSMatrixFactorizationを使っています。Building a Recommendation System in TensorFlow: Overviewを構築するときの参考にしました。

タベリーではユーザイベントログをFirebaseからBigQueryに流していますがそのデータを、TensorFlowで使う前にCloud Dataflowを使って加工し、Cloud Storageに保存しています。TensorFlowを使ってCloud Machine Learning EngineでCloud Storageからデータを取得し学習させてその結果をCloud Storageに保存します。保存されたCloud StorageからCloud Dataflowを使って再度BigQueryに保存します。BigQueryに保存されたユーザごとのレコメンドデータをKubernetesのCronJobでCloud Datastoreに保存してGoのバックエンドから利用できるようにしてます。

最後に

10XではGCPを活用してパーソナルな献立を推薦するタベリーを開発しています。今回はアーキテクチャの紹介でしたが、Go/Swift/Kotlinでかかれたコードも、いい感じに設計されており、テストやLintがちゃんと走っています。ここらへんはCTOの石川さんがサービスの成長にあわせて都度リファクタし正しく設計してくれているおかげかなと思います。

タベリーのオンライン注文機能はまだiOSにリリースしたばかりでこれからもっとユーザに使いやすく役立つアプリになるためにメンバーを募集しています。ちょっとでも興味があればぜひ会社の応募フォームからご連絡ください。ちょっとフォームはという方は自分のTwitterのDMやメンションなどでもお声がけください :)