MOV タクシー配車アプリ RubyからGolangへ刷新 マイクロサービス化

RubyからGolangへの移行を進める過程で、システムアーキテクチャがマイクロサービス化していくという稀有な体験をしたので記事を書きました。

次世代タクシー配車アプリMOV(モブ)及び、タクシー車両内の乗務員向けアプリに係る WebAPI 50前後をRailsからGolang net/http に刷新しました。その過程でマイクロサービス化が進んだ事例を紹介します。MOV サーバエンジニア池田 周平です。サービスを継続しつつシステム刷新するために、なぜその判断を行ったかについてお伝えできれば幸いです。

MOV(旧タクベル)ご存知でしょうか?神奈川、東京でサービス提供中のタップ操作でタクシー配車ができる配車アプリです。 スクリーンショット 2018-12-28 11.03.58.png

実証実験を繰り返しサービスリリースしました。 立ち上げ初期段階から居たメンバーに話を聞くとRailsで高速にプロトタイピングを繰り返していたそうです。 リリース直前にGAE FEサーバのスケールとスピンアップ速度の観点でGAE SE Golangへ刷新を決め、新設WebAPIはGolangで実装しましたが大部分はRailsのままリリース当日を迎えました。

スクリーンショット 2019-01-08 19.19.41.png

APIバージョンをv1, v2, v3と上げ、アプリの強制バージョンアップでRubyからGolangに切り替える作戦を立てるも、後方互換性を維持し古いアプリでも配車可能であり続けるという運用方針があり、コード2重メンテ問題を回避するため断念しました。開発チームではGolang刷新を完遂したいとたまに話題にしつつも、日々の機能追加や運用に工数を割いていました。

GAE 1%カナリアリリース

リリース直後特有の繁忙期が過ぎたある日、マネージャからGolang移行チーム設立と担当を任されました。移行チームが出来て任されると自分ごとになりムクムクとやる気が出てくる点ワタシ自身が驚きました。

まず最初に考えたことは安全に移行するための仕組みです。

車両軌跡データやユーザアプリと乗務員アプリ間で配車ステート更新を行うため一部のデータ構造や通信シーケンスが複雑です。開発環境でバグ取りきれず本番環境で不具合に繋がる可能性がどうしてもありました。電車やタクシー内で考えた結論はカナリアリリースの仕組みを作り WebAPIごとに最初の24時間はRuby99%, Golang1%でトラフィックを分割して監視、その後割合を増やしていくという作戦でした。

GAEはservice versionごとの段階的なトラフィック切り替えをサポートしていますが、ルーティングを制御するdispatch.yamlの更新はゼロイチ切り替えのためproxyサーバを用意して切り替えることにしました。

GAE標準のトラフィック切り替え機能

スクリーンショット 2018-12-28 13.20.36.png

Golang移行対象WebAPIの1%カナリアリリース

Proxyのversion1と2の差は、刷新対象のWebAPIについてトラフィック流し先がRuby版かGolang版であるかの違いです。 スクリーンショット 2018-12-28 13.47.20.png

このようなアーキテクチャ設計にした狙いは、ゼロイチでルーティングを切り替えるGAE disptach.yamlの仕組みから最小工数でカナリアリリースできるように変更できる点、RubyとGolangサービスで共通formatのログを出力できる点です。1手で2つのメリットあるため採用しました。Cloud Functionsを利用するといった他の選択肢もありました。複数ある選択肢から早期に絞りこめたのはGoogleテックコンサルの皆様に定期的に相談できる恵まれた環境にあった点が大きかったです。サポートとても感謝しています。

Proxyサーバを実装する

GAE dispatch.yamlを代替するサービスを便宜上proxyサーバと呼んでいます。実態はHTTPサーバでHTTP CONNECT methodはログ出力するため利用していません。次のようなざっくり目標と設計で開発しました。

  • Proxyは通信の土管に徹するサービス
  • 処理のオーバーヘッドは50ms以下が目標
  • APIサービスへのHTTP通信はNon-blocking I/Oで必ず行う
  • JSONパースは処理時間が掛かるため行わない
  • BigQueryへログ出力を行う

Golangで素直に書いていけば実現する仕様です。本体は200行くらいでどのサービスに通信を振り分けるかはrouterをそのまま利用して実装します。仮想コードの方が伝わるので載せておきます。


func init() {
    router := router.New()
    setAction(router, "/", iamproxy)
    setAction(router, "/v1/drivers/*path", rubyService)
    setAction(router, "/v1/users/*path", golangService)
    http.Handle("/", router)
}

func main() {}

先ほど本番サーバをStackdriver traceで計測したらproxyの処理時間は15msでした。サービス間の通信はurlfetchを利用していて、GAEだとGCPの同一データセンター内にdeployされる特性があるためレイテンシは低めです。詳細は公式の Google App Engine 上でのマイクロサービス アーキテクチャ に記載があります。

routerはwild card routingといった標準的な機能が利用できるものを採用しました。特に高速化のために内部で Radix tree を生成し、サーバ起動時にURL routingの名前空間が重複しているとエラー吐いてどこが重複したか教えてくれるようならライブラリを利用するとよいです。複数のAPIを段階的に移行する際の助けになりました。

Proxy導入でログ出力を強化する

複雑な構造を持つJSONをRubyからGolangに移行したとき、RubyとGolangの細かな仕様差、たとえばゼロ値で苦しめられます。 元の構造では気軽nullを扱っておりJSON objectの各プロパティの型がnullableになっていました。ArrayやStringが空のとき [] or "" or null がAPIごとにバラバラだったため、正しく移植しきれず何箇所かで失敗することが予想できました。

現行WebAPIがどういった構造のJSONを送受信しているか蓄積し知るためProxyサーバで HTTP Request/Response/Headerを全てStackdriverにログ出力して、Stackdriver exportでBigQueryにsyncしました。この設定はボタンポチポチで設定できる割に効果が大きく移行をサポートする強力なツールになりました。(開発者用のログはオートモーティブ事業部内での厳しい個人情報取扱規程・個人情報保護指針があり、位置情報や選択内容及びアカウント情報は全てサーバ内部のメモリ上で削除し個人を特定出来ない形でログ送信しています)

億単位の通信ログからBigQueryで目当てのログを検索できたときは、1歩時代が進んだ感あって気に入っています。またStackdriverはStackdriverMonitoringで簡単に監視設定を作り込みSlackやPagerDutyと連携できるため頻度高く利用しています。

機能追加とGolang移行のバランス

サービスへの機能追加とシステム刷新のバランスは難しい問題です。開発の手を止めることが出来ればよいのですが、システム刷新を理由にリリース直後のアプリで機能追加速度を落としたくはありませんでした。50前後あったWebAPI次の5つに分類し段階ごとにリリースしました。特にHTTP GETなWebAPIはステートレスでテスト容易だったため早期に移行を行いました。5段に分けてリリースした理由は、1段目は単純なAPIを移行し実行可能性を調査するため、2段目移行は移行対象が大きすぎて全体把握と品質担保が難しいといった問題を回避するために、QAを行いやすい単位で分割していきました。

  • アカウントAPI
  • 配車API(method:GET)
  • 配車正常系API
  • 配車準正常系・異常系API
  • その他(お知らせ等)

移行中のGoとRubyどちらに実装すればいいといったコンフリクトが複数発生するなと予期していました。実際はごく少数しか発生せずスムースに移行完了でき、きっと私が気づかない所で開発チームメンバーが配慮・調整してくださっていたんだと思います。

振り返るとシステム刷新する上で開発チーム全体への影響を最小にするポイントはいかに早く刷新コードをmasterブランチへマージするかという点です。Proxyサーバでトラフィックを切り替えない限り蓋ができる構造だったため、完成次第masterブランチに投入して本番リリースを行いました。蓋を開けるタイミングはQA完了後です。早い段階で刷新したコードが他の開発者の目に触れる場所に設置できた点はとても良かったです。

マイクロサービス化と開発組織

現在アプリから見えるサーバは次のような構成になっていて9名の開発者で開発しています。(これ以外にもFleet Management System, タクシー事業者向け管理サービス, CS対応向け管理サービスとその開発チームありますが図からは省略しています)定めたというより自然とこのような形に落ち着きました。言語化すると次の基準です。現在のスナップショットでしかないため人員が増えるとルールは変化していきそうです。ポイントはサーバ間認証のサービスを増やしていっている点で部品としてふるまうためマイクロサービス化の副作用少なくメリットを享受できると考えています。

  • API Service本体は全員が開発
  • 各マイクロサービスは2名前後で開発
  • user/driverのaccess tokenを認証認可するサービスはAPIに集約する
  • MOV以外でも利用需要がありそうな機能はサーバ間認証でマイクロサービス化

スクリーンショット 2018-12-29 15.13.53.png

GEOはgeographyの略で一緒に開発していたエンジニアがすごい勢いで空間参照系について詳しくなり、いつの間にか作り終わっていた機能です。代表的な機能はユーザや車両がどの営業区域にいるか経度緯度情報からの地域判定です。PostGIS DBのCPUで判定計算している点が特徴です。

E2Eテストを書く

proxyを対象にE2Eテストを書きました。テストケースは本番の配車履歴から頻出パターンを抽出しました。検査項目はHTTP ResponseのJSON SchemaチェックとRDBのレコード状態の確認です。RSpecの生産性の高さと、使い捨て前提の割り切りでサクサク書け手動で再現するには2時間くらい掛かる分量になりました。捨てるにはもったいない出来だったのでbotに組み込みQA環境を自動試験しています。

スクリーンショット 2018-12-29 16.43.36.png

まとめ

振り返るとRubyからGolangへの移行を進める過程で、システムアーキテクチャがマイクロサービス化していくという稀有な体験ができしました。短期間で大きなトラブルなく安全に移行完了できたのはカナリアリリース、ログ基盤、E2Eテストと足回りを強化したあと移行を行った点にあると思います。

このアーキテクチャは現在のスナップショットであって、今後開発リストにあまたある新規開発を行っていく上で変化していくでしょう。チーム内では配車サービスを独立といった話をしています。

お話変わってDeNAでは年に1度TechConを開催しています。今回移行作業を強力にサポートしてくださった惠良 和隆さんの発表もあります。ぜひ。 https://techcon.dena.com/2019/

最後まで読んで頂きましてありがとうございます。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る