みなさん、こんにちは。サーバエンジニアの長南です。
以前この Timers Tech Blog に
という記事を書かさせていただきました。その中で
モバイルアプリのバッチ処理部分の一部を Go 言語を使って書きかえる取り組みを行っています。
という大それたことを書いていたわけですが、このプロジェクトはその後どうなったのでしょうか?
結論からいうと、このバッチ処理の Go 言語への移植は成功し、これまで半年以上の実績を残しています。弊社 Timers inc. のサーバーエンジニア採用ページ
に書かれている通り、Go言語で書かれたバッチ処理が、グローバルな家族アプリFammを支える状況が生まれています。この記事では、この移植プロジェクトの概要をご紹介します。
バッチ処理ではどんなことをやっているのか
Famm では毎月ユーザーさんの選んだ写真をつかってカレンダーを印刷するということを行うのですが、写真選択の締切後
- ユーザーの情報を収集し、注文情報を作成
- カレンダー印刷入稿データを作成
- 課金処理
- 印刷・発送の手配
- 「注文完了した」旨のお知らせの発行
といったことを行っています。おかげさまで Famm は順調にユーザー数を増やしているのですが、その一方で締切後のバッチ処理量も増え、これらの処理をいかに効率的にこなすのかが課題になっていました。処理量が異なるので単純な比較はできないのですが、一時期は一連のバッチ処理が 30時間にもおよび、今後サービスの成長を受け止められるのかということが心配されていました。
入稿データ作成と課金処理については今回のプロジェクト前に処理の並列化を行って、ある程度時間を稼げていましたが、注文情報を作るところは処理時間もさることながらロジックも入りくみ、障害を起こしやすいところで、この部分にメスを入れないと未来はないという状況となっていました。
この注文処理の作成部分を Go 言語に置き換えるのが今回のプロジェクトです。
注文処理 Go 言語化のあゆみ
この Go 言語化ですが、本格的に開発を行ったのは2018年前半のことでした。幸いなことにリリースした後は大きなトラブルなく10回目の実行を迎えています。
- 2017/11 技術選定、移植対象のディスカッション
- 2017/12 プロジェクトの git レポジトリ立ち上げ
- 2018/01 先行してリファクタを行っていた会社さんにアドバイスをいただきに訪問
- 2018/02 本格開発開始
- 2018/06 品質を担保するためリリース時期を1か月延期
- 2018/07 本番リリース
- 2019/04 リリース後10回目の実行
当時のバッチ処理が抱える問題については2017年後半あたりからエンジニア中心に危機感を持ち始め何度かディスカッションが行われ、最終的に Go 言語を採用することになったのですが、当時のディスカッションのメモを見返してみると
項目 | PHP | Go | Node.js | Python | Kotlin | Scala | Ruby |
---|---|---|---|---|---|---|---|
学習コスト | ○ | ○ | ○ | ○ | △ | △ | ○ |
現状社員の習熟度 | ○ | △ | ○ | ○ | △ | × | △ |
型安全性 | × | ○ | × | × | ○ | ○ | × |
と、型安全性の強さとメンバーの習熟度(あるいは学習コスト)が大きな決め手となっていました。学習については社内勉強会を開き、開発担当以外のメンバーでも基本的な部分では Go でコードがかけるようになりました。メンバーのモチベーションが高かったところと Go 言語の仕様が小さくシンプルだったのが功を奏したように思います。
クリーンアーキテクチャ
開発当時の課題の一つに、「バッチ処理に限らずテストコードをいかに整備して書いていくのか」というのがありました。せっかく新しい言語でスクラッチから開発するわけですから、その部分も見通しをよくするためにクリーンアーキテクチャで開発することに決め、Go 言語の Interface を活用して mock をつくり、テストコードを書きやすい状況を整えて開発を行いました。
開発はメンバー2人ほぼ専任であたりましたが、大きく低レイヤーの infra 部分と、ビジネスロジックまわりの domain, usecase 部分に分けて開発を行い、私は infra 担当で冒頭に掲げた既存DB資産の活用などといった泥臭いところを担当し、罠にハマりそうなところを事前に埋める役を演じました。
結果できたものの分量を計測してみると
- Go 言語のソースファイル : 222 ( うちテストコード : 108 )
- Go 言語のソース行数 : 34915 ( うちテストコード : 20992 )
と、テストコードを整備しつつ 1 ソースファイルあたり約 157 行とそこそこシンプルな構成でコードを書くことができました。 言語の差もあるので一概にはいえませんが、PHP でどうだったのかというのを振り返ると、ビジネスロジック部分のコードで 1 ソースファイルあたり約 182 行だったので、見通しを良くしながら移植することができたのではないかと思います。
パフォーマンス
懸念のパフォーマンスですが、処理で時間がかかっているのが、DBへのクエリです。移植前のバッチでは処理対象ごとにクエリーを発行して必要な情報を集めていました。処理は分かりやすいのですが、ユーザーが2倍になれば発行クエリの数も2倍になります。そしてDBの構成やインデックスに工夫をしたとしてもユーザーが増えるにしたがってクエリ性能も低下していきます。
これを解決するために、DBからデータを取得するところはある程度まとめて取得、ビジネスロジック部分は go routine を使い並列化、結果を書き込むところも「おまとめ」した単位で一括挿入するように実装しました。
この他に無駄だと思われるロジックの見直しを徹底的に行った結果、注文作成で14.5時間かかっていた処理を1時間ほどに短縮でき、10倍超の速度向上を達成しました。開発開始時には現状の1.5倍の速度向上を目標にしていたので、この結果は想定以上の成果となりました。
まとめ
今回の Go 言語移植が成功したのは、個々の技術要素の特徴を比較することよりも、解決したい問題を洗い出しどうやってアプローチするのかというディスカッションを徹底的に行ったことと、非機能要件で軽視されがちなパフォーマンスの問題に真剣に取り組み、可能なかぎりコードに盛り込んだことがあったと思っています。「Go言語だから成功した」のではなく「成功のためにGo言語をうまく使った」ということで、直面している課題に真摯に向き合うことが大切だったように思います。
積極採用中!!
このような形でバッチ処理の Go 言語移植には成功しましたが、まだまだ改善の余地はたくさん残されています。課金処理や印刷手配部分の業務効率化や Go 言語を使った AWS Lambda による Serverless 化など挑戦しがいのある課題が残っています。腕に覚えがあり、みんなを幸せにしたいマインドをお持ちの方はぜひ一緒にサービスを育てていきましょう!
子育て家族アプリFammを運営するTimers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings