こんにちは菊池です。
今回は以前紹介した、サーバーレスで実装したデータ基盤のデータ収集システムについての記事になります。
以前紹介した記事はこちら:
このデータ収集基盤をサーバーレスで構築する際に、とある処理が Cloud Functions の最大実行時間をオーバーしそうになりました。正確に言うと実装時はオーバーしていないものの、今後オーバーする可能性がありそうな状況です。この状況をどうやって回避したのかについて書きましたので、参考になれば幸いです。
Cloud Functions の最大実行時間
Cloud Functions で実装を始めた当初、Cloud Functions の最大実行時間は 540 秒 ( 9 分) でした。今では第二世代の Cloud Functions も使えるようになり、最大実行時間の制限は 60 分に増えています。だいぶ増えましたね。
少し脱線しますが、うっかり永久ループする処理を Cloud Functions で実行した場合、止める方法知ってますか? あるいは、実装ミスって Cloud Functions から Cloud Functions を鼠算的に実行しつづけて「おや?なんでこんなにたくさん動いてるの?」って気がついて肝を冷やしたりしたことあります? といった暑い夏が涼しくなる怪談話もいつか書こうかな。今年は暑いですね。
最大実行時間をオーバーしそうな処理
先ほど書いたように Cloud Functions には最大実行時間の制限があります。以前記事にしたデータ収集システムを実装するにあたって、素直に実装すると Cloud Functions の最大実行時間をオーバーしそうな処理がありました。
データ取得処理の基本的な処理の流れは、以下の概要図のようになっています。
- Cloud Tasks からデータ取得処理の Cloud Functions を実行
- Cloud Functions から外部の API を呼び出してデータを取得
- 取得したデータを Cloud Storage に保存
- 取得したデータの内容によって複数の後続タスクを Cloud Tasks に登録する
ここで、とある API のレスポンスが件数の多い JSON リストで、このリストの要素ひとつひとつに対して後続のタスクを Cloud Tasks に登録する処理を実装した場合、データ量によっては最大実行時間の制限をオーバーしてしまいそうな状況になりました。
以下の図のように、API レスポンスが要素数の多い JSON リストの場合に要素ひとつひとつを順番に処理すると、全てを処理し終える前に最大実行時間が来てしまいます。結果、全て処理し終える前に実行が中断されてしまうということに。
この API はデータの一部分を順番に取得するような、ページ数や取得件数を指定できるリクエストパラメータが提供されていませんでした。このため、レスポンスの JSON リスト件数を少なめに固定して後続の登録タスク数を減らし、Cloud Functions の最大実行時間内に処理を終わらせるといった方法を採ることができません。
レスポンスデータの量は時期によって増減するので、単純に要素ひとつひとつに対して後続のタスクを登録するようなループで実装すると、データ量が多い場合に最大実行時間を超えそうです。たとえ今は大丈夫なんで!と動かしたところで、枕を高くして眠れなさそうです。
※実装当時は最大実行時間の制限 9 分に収まるのか怪しい状況でした。いまは第二世代で 60 分まで増えているので素直に実装しても大丈夫そうではありますが、それでもチョット心配なところ。
実装の選択肢
ということで、さてどうしようかなと、いくつか考えたのが
- インスタンスを立ち上げて実行する
- 取得したデータを Cloud Storage に保存して Cloud Dataflow でバッチ処理させる
- JSON リストを分割する Cloud Functions を再帰的に実行して 1 件になったら後続の処理を実行する
他にも何か方法があるかな?
インスタンスを立ち上げて実行する
実行時間の制限をほとんど気にしないで済むので、これもありかなと思います。とはいえ、全てサーバーレスで実装する道を探りたかったので一旦キープの保留としました。変なこだわりですが「ここだけインスタンス使うのもな」という思いもちょっとあります。
取得したデータを Cloud Storage に保存して Cloud Dataflow でバッチ処理させる
まずは Cloud Functions で API を呼び出して取得した件数の多い JSON リストを Cloud Storage に保存します。この保存したデータを Cloud Dataflow で読み込んで、要素ひとつひとつに対して後続のタスクを登録するための Pub/Sub メッセージをパブリッシュするとか、直接 Cloud Tasks のタスク登録を実行するとかできそう。
これもありなんですが、ちょっと大袈裟かもしれない。そこまでの量でもないんだけどな。ということで、これも一旦キープの保留としました。
JSON リストを分割する Cloud Functions を再帰的に実行して 1 件になったら後続の処理を実行する
Cloud Functions は最大実行時間の制限があるので、処理するデータ量や計算量をある程度の範囲内に収めておかないと厳しい。今回は単純にループで後続の処理を実行すると、データ量によって処理時間が変わるのが問題になります。そこでデータ量によらず、ある程度処理時間を固定する方法がないかな?と考えました。
そこで、とりあえず思いついたのが、リストを分割する処理であればある程度の処理時間内に収まりそうでは? ということです。
例えばリストを半分に分割する処理であれば、Cloud Functions の最大実行時間内に終わりそう。半分にしたリストをさらに半分にする Cloud Functions を実行するというのはどうでしょう。これを何度か繰り返して、リストの中身が 1 件になったら後続のタスク登録処理を実行する。というように、時間のかかる処理を何らかの手段で分割して、ある程度実行時間が見積もれる単位にして実行する。というやり方でできそうな予感。
リストの分割数は半分だけでなくデータ量や実際の実行時間を参考にして、n 分割するのも良さそうです。それに後続の処理を実行するタイミングもリストの中身が 1 件になったらだけでなく、m 件になったらループで実行するといったこともできそう。この辺は実際の実行時間やデータ量を元にパラメータで指定できると嬉しい。
分割して実行する実装の詳細
先ほどのアイディア「JSON リストを分割する Cloud Functions を再帰的に実行して 1 件になったら後続の処理を実行する」について、もう少し詳しく説明してみたいと思います。
まずは、取得したリストを Cloud Functions で実装した「リスト分割処理」に入力します。この「リスト分割処理」は入力されたリストをパラメータで指定した数に分割します。これは単純な処理なので実行時間もさほどかかりません。
例えば、分割数のパラメータに 2 を指定して要素数 100 のリストを入力した場合、要素数 50 のリスト二つに分割します。あるいは分割パラメータに 4 を指定して要素数 100 のリストを入力した場合、要素数 25 のリスト四つに分割します。
その後、分割したそれぞれのリストを再び「リスト分割処理」で分割するように Cloud Tasks に登録します。
以下の図では入力リストを n 件で分割して、それぞれのリストを再び「リスト分割処理」するタスクとして Cloud Tasks に登録している様子です。
分割されたリストと共に Cloud Tasks へタスク登録された「リスト分割処理」は、それぞれ Cloud Tasks にキューされた後に再び Cloud Functions で実行されます。先ほど分割されたリストが、再び分割処理されることになります。
以下の図は、先ほどの図で Cloud Tasks に登録されたタスクのひとつが実行された様子になります。
このように「リスト分割処理」を繰り返すと、最終的にリストの要素数は 1 になります。
要素数が 1 になったところで、本来リストの要素それぞれに対して実行したかった処理をタスクとして Cloud Tasks に登録します。
以下の図では要素数が 1 になったので、リストの分割処理は行わずに、後続のタスクを Cloud Tasks に登録している様子になります。
ここでは本来実行したい「関連データ取得処理登録」をリストの要素数が 1 になったタイミングで実行していますが、要素数がある数を下回った段階で要素それぞれに対してループで実行しても良いと思います。
データ量と分割数から、分割で実行される Cloud Functions の数は計算できるので、あらかじめ全体の分割回数を見積もってもいいと思います。そこから Cloud Functions の料金を導き出すと一安心。ですが Cloud Functions 安いんですよね。
まとめ
時間のかかる処理を細かく分けて関数で実行するところが、個人的に面白かったので記事にしてみました。
サーバーレスの世界ではいくつか制限があります。GCP では Cloud Functions や Cloud Run などありますが、それぞれ実行時間の制限があるのでバッチ的な処理では使いにくいと感じますよね。そんなときに「そういえばあんなのあったな」と参考になれば幸いです。
ちなみにこれ、実際に動かしてます。実装も簡単でした。
それではまた、機会がありましたら記事でお会いしましょう。菊池でした。