🌤️

レガシーなバッチ処理をリプレイスしたら爆速になった話

に公開

はじめに

こんにちは。BABY JOB株式会社 開発部です!

今回はBABY JOBの開発現場の紹介として、昨年実現したSalesforceへのAPI連携バッチのリプレイスプロジェクトについて取り上げます!

プロジェクトの概要

旧仕様

リプレイスされる前の仕様は以下のようなものでした。

  • 社内にある1台のノートPCのWindowsバッチにより稼働
  • 当社開発部が数名規模だった頃に外注で実装されたもの
  • データローダをCLIモードで実行していて、時々手動で実行

図式:旧仕様におけるシステム構成

課題感

Salesforceへ連携したいデータはプロダクトの成長に伴って頻繁に変化していて、その都度このバッチを修正する必要がありました。もちろん、プロダクトの成長に伴って実装に修正が入るのは当たり前のことです。

ですが、問題だったのはこのプログラムが著しく属人化してしまっていることでした。

開発部は当時、チームメンバーの一人を除き全員がMacユーザーでした。Windowsバッチで実装されていたためにチーム内にたった一人だけのWindowsユーザーに属人化の力学が強く働き[1]、しかし目下の利益を生まないという理由でこの構造は長く改善されてきませんでした。

このプログラムを稼働させていたノートPCのサポート期間が切れ、PCを新調しなければならない状況にもありました。元々外注して実装されていたこともあり、動作基盤を置き換える作業を自信を持ってできる人はいなかったのです。

  • プロダクトとはGitリポジトリが異なる
  • ユニットテストがない

こういった要素も保守性の低さを助長していました。

以上のような課題感を、「今後の事業の継続性にも関わるリスク」として、とうとう手を打つことになったのが昨年3月のことです。

新仕様

  • プロダクトコード内にSalesforceへデータを連携するAPIを実装
  • Amazon EventBridgeによりAPIを定期実行、社内の管理者用のアプリからワンボタンで実行可能
  • 内部的にBulk API 2.0を実行している

図式:新仕様におけるシステム構成

データローダからBulk API 2.0へ

データローダからBulk API 2.0に移行するにあたって重要だったポイントを3つ紹介します!

  • 非同期処理
  • 並列処理
  • 空文字のデータの連携仕様

非同期処理

データローダではターミナル上で処理の完了を待機していましたが、Bulk API 2.0ではリクエストを実行後はSalesforceサーバー側で非同期処理でジョブが進行します。

各ジョブが完了したかはクライアント側から確認しに行く必要があります。

今回の要件においては、完了時の通知や同時実行可能数の制約の実現にあたって完了は必ず検知する必要がありました。並列処理の制御の問題と合わせて、ジョブの状態遷移の制御が必要になっていきました。

並列処理

データローダでは全プロセスにわたって直列で処理していましたが、Bulk API 2.0ではジョブを並列的に処理するようになりました!

ここは特に苦労したので少し詳細に踏み込みます。実際に連携するデータは下図のような構成でした。Salesforce上の5つのオブジェクトを連携していて、それぞれのオブジェクトが参照関係にあるデータを持ちデータ毎に別々のCSVに出力して連携していました。

図式:連携データのツリー構造

その中で

  • 連携データの新旧で差がないことを保証するため、連携するデータの取得SQLやCSVのスキーマは極力変えず実現したい
  • 1リクエストに乗るデータには上限(100MB)があるため同じSQLの結果であっても必要に応じて分割してアップロードしなければならない

という制約がありました。

Bulk API 2.0では、ジョブを登録してCSVをアップロードし、アップロード完了をリクエストして連携が開始する仕様になっています。

図式:Bulk API 2.0の連携フロー

アップロード完了のリクエストを最後に要求するのは複数ファイルを前提としてだろうからジョブとCSVは1:nで連携できそうだと考えてました。分割したデータや参照関係のデータは1ジョブにまとめてアップロードして制御することになるだろうと。

図式:分割データと参照データをマトリクスにして期待していたジョブのカバー範囲を示す

ですが実際は1:1でした!

淡い期待は崩れ去り、参照関係のデータ同士はおろか、分割したデータ同士のジョブの進行順の制御までクライアント側で実装する必要が生じました。

図式:分割データと参照データをマトリクスにして期待に反したジョブのカバー範囲を示す

議論の末、CSVをテーブル毎に分けるのではなく集約毎にまとめたあとで1CSVあたり100MB以下に分割する方式に転換しました。複雑なSQLはそのまま、CSVのスキーマもシンプルな結合に近い変更に留めつつアップロードが可能になりました。

図式:問題解決

このアイデアがブレイクスルーとなって開発が軌道に乗りました。

空文字のデータの連携仕様

細かいですが、ぜひ共有しておきたいと思った仕様を最後に1個だけ紹介します。

データローダとBulk API 2.0とでは空文字・NULLのAPI連携のフォーマットが異なりました。

旧構成 Bulk API 2.0
""NULLを入れると""NULLに上書きする[2] ""NULLを入れると無視され、#N/Aを入れるとNULLに上書きする

同じCSVをアップロードしてもこのように結果が異なる場合があるのでお気をつけください!

成果

Salesforce連携バッチがプロダクトのリポジトリに移管され、複雑なSQLにはユニットテストが整備されました[3]。開発チームの全メンバーがバッチの改修にコミットできる体制になったのです。サポート切れの社内ノートPCも無事クローズできました。

副産物として、バッチ処理が大幅に高速化しました。
対応前は一度の実行に2時間掛かっていたのですが、対応後はなんと12分で完了するようになりました。全体的に構成が大きく変化したので複数の要因が絡んでいますが、処理の並列化による寄与が特に大きかったと見ています。

元々は主に開発部にとってのメリットの大きいプロジェクトと捉えていましたが、Salesforceに気軽に最新の情報が連携可能になったことで社内の業務効率が大きく改善したと他部署からも非常に喜ばれるプロジェクトになりました☺️

学び

技術スパイクの重要性🏐

今回はなし崩し的に状態遷移の制御が複雑化してしまいました。プロセスマネージャーパターンを採用して状態遷移の責務を1箇所に集約する、より保守性の高い設計が取れたかもしれません。

技術スパイクは工数見積もりだけでなく、設計の大域最適化の観点からも重要だと気づきました。

エンジニアのスキルは「不言実行」され、「不言不実行」される🥷

プロジェクトを通してSQLのチューニングや認証ロジックなど、メンバーの持っている知見がしれっと発揮され問題解決するシーンが今回紹介しませんでしたが多々ありました。逆にたまたまメンバーが持っていなかったばかりにより良い設計のタイミングを逸してしまったことも、思い返してみればありました。

エンジニアのスキルが現場で実際に活きるときとは前触れなく訪れ、実装の明暗をはっきり分けてしまうものだなあと学びました。日々のインプットがいかに差を分けるか、です。

おわりに

表向きはおむつサブスクのベンダーですが、当社開発部ではこういった社内にエンドユーザーを持つアプリケーションの開発も時に行なっています。

今回のプロジェクトは外部アプリケーションとの連携を含めたイチからの仕組み作りという要件で、設計やプロジェクト管理に至るまで幅広く学びの多いプロジェクトとなりました。

この記事を執筆している現在、今回作ったバッチは実行回数が500回を超えようかというところです。こうして実行回数という目にみえる形で価値が現れているのを見るとエンジニアとしての仕事にやり甲斐を感じますね。

では今回はこのあたりで。最後まで読んでいただきありがとうございました!

脚注
  1. Macで開発しているメンバーも全く改修できないわけではありませんでしたが、社内のWindows PCにリモート接続して作業しなければなりませんでした。 ↩︎

  2. データローダのInsert null values設定によって挙動が異なります。当時の構成ではこの設定値がtrueだったために、この挙動になっていました。 ↩︎

  3. リプレイスしたバッチ処理にユニットテストを実装した時の話はこちら ↩︎

BABY JOB  テックブログ

Discussion