見出し画像

バスアプリのバスロケーション機能をより使いやすくした話 〜バスロケーションデータ編〜

こんにちは、けんにぃです。ナビタイムジャパンでバスロケーション機能のバックエンドサーバの開発を担当しています。

にわとりさんの記事に続き、本稿ではバスロケーションデータがどのように開発されているのか、および課題に対して工夫した点などをお話していきます。

開発プロセス編はこちら

GTFS リアルタイムについて

バスロケーションデータは GTFS リアルタイムという Protocol Buffers でシリアライズされた形式で各バス会社から配信されており、運行中のバスの位置や車両の混雑具合、遅れ時分などを参照することができます。

各バス会社が配信している情報

GTFS リアルタイムで配信される情報は、バス位置を示す緯度経度など一部のプロパティを除き、ほとんどのプロパティは配信が任意となっています。

この仕様は各バス会社の裁量で好きなプロパティを配信できるという意味ではよいことなのですが、弊社のアプリでバスロケーション機能を提供する場合には提供する情報にある程度統一感を出しておかないと UI の設計が難しくなるため、各バス会社がどのプロパティを配信しているのかをあらかじめ把握しておく必要があります。

そこで各バス会社が配信しているプロパティを調査して、アプリ開発者に共有することにしました。地道な作業なので大変でしたが、導入計画や希望するプロパティが配信されていなかったときの検討などに役立てることができました。

GTFS データ取得時の課題

GTFS リアルタイムのデータは次の手順で取得しています。

  1. 配信サーバから定期的にダウンロード

  2. GTFS の Protocol Buffers をパース

配信データはだいたい 30 秒〜 60 秒間隔で配信されています。
このデータ取得のためのスクリプトが Python で記述されていたのですが、対応会社を増やすにつれて次のような問題に直面しました。

Python のアップデートのために実行環境の整備が必要

Python のバージョンがサポート切れを起こすと管理者にアップデートを求められます。使用しているライブラリの中には新しい Python では動かないものがあり、アップデートは結構手間がかかりました。

対応会社数が増えるにつれてプログラムの実行時間が増大する

Python 製のスクリプトはダウンロードを直列で実行していたので、対応会社数の増大に対してスケールできない設計になっていました。これを改善するには並列処理をするかスケールアウトをする必要があります。
会社 1 社あたりのダウンロードはそれほどかからないため、並列化すればパフォーマンスは改善しそうだなと考えました。

しかし Python ではグローバルインタプリタロック (GIL) という仕組みが原因で、並列処理がそれほど速くならないという問題が知られています。

今後バス会社導入を加速させるにあたり、Python での並列化でどこまで改善できるのかという懸念がありました。

取得データのサイズが大きいと GTFS のパースに時間がかかる

これが一番致命的な課題でした。
Python だと GTFS のパースが致命的に遅く、データによってはパースに 1 分以上かかるものもありました。パースに 1 分以上かかるとパースが終わる前に次のデータが配信されてしまうため、バッチ処理が配信間隔に追いつかないという事態になります。

わざわざ定期的に GTFS をダウンロードしなくてもユーザーからのリクエストが来たときだけ GTFS を取得すればいいようにも思えますが、バスロケーションデータのようなリアルタイムデータは過去のデータを参照できないと不具合の調査が難しくなるため、都度ログとして保存しておく必要があります。

どうやって解決したか?

Python コードをリファクタして少しでも速くなるように対応したのですが、それでも速度が十分に出ませんでした。コードは簡単なバッチ処理を行うシンプルなものでしたので作り直すのも手間ではないと思い、別のプログラム言語でリプレイスしようと考えました。

プログラム言語の検討においては次の要件を加味して検討しました。

  1. アップデートやデプロイなど CI が簡単に運用できる

  2. 並列処理が簡単に実装できる

  3. GTFS のパースが高速

  4. Python 並に学習コストが低い

この要件にマッチするものは Go かなぁと思って Go でリプレイスしました。

Go で置き換えたツールはほんの数秒で大量の GTFS をダウンロードしてパースしてくれます。特に並列処理のゴルーチンを使えばとても簡単に並列化ができてしまうので、C++ でゴリゴリ並列処理を書いてきた私としては結構感動しました。

実行時間を改善前後で比較をすると

  • Python: 約 65 秒

  • Go: 約 7 秒

という結果で 10 分の 1 程度の改善ができています。

またコンパイルしたシングルバイナリを実行環境に配置するだけでアップデートが完了するため、実行環境の整備もいらず環境構築や CI がシンプルになりました。

古いデータを保存する

過去の運行実績データは調査・分析用として保存するようにしています。以前は gzip で圧縮していたのですが、対応会社数が増えるにつれてログのサイズが肥大化してきました。このままだとバッチ処理でディスク容量の枯渇などトラブルが発生しそうだったため zstd で圧縮するようにしました。

zstd は圧縮速度・圧縮率ともに優れたアーカイバで、今回のようなバッチ処理などでよく使われるようになってきています。gzip で圧縮する場合と比較してサイズが半分くらいになりました。

圧縮したログは S3 に転送して保存するようにしています。

まとめ

以上、バスロケーション機能のバックエンドのお話でした。バスロケーション機能は見た目だけでなく裏側も大きく改善されていますので、是非ダウンロードしてご利用ください。