Cloud Functionsで大量のデータを処理する


こんにちは。Stamp Incのnoriです。

巷でかなり多く、Firebaseの言葉を聞くようになりました。
FJUGを始めてからもう3,4年くらい経ってるんですね。たくさんの人に使って便利さを実感してもらって普及活動を頑張った僕としては嬉しい限りです。

僕が開発したOSSのBallcapが、Firebase Open Sourceに掲載されるようです。👏🏻
コントリビュートしてくれた開発者のみなさんありがとうございました。

今日は、Cloud Functionsで大量のデータを処理するときのTipsを紹介します。
例えば、バッチ処理を組んでいて、数万件のデータを処理しないといけない場合とか、外部APIからとんでもサイズのデータが流れてくる場合が考えられます。

Cloud Functionsで大量のデータを処理

結論から言うとこれも分散処理をさせることが重要です。
今回は、郵便番号データをCloudFunctionsで処理したサンプルがあるのでそれを例に説明しようと思います。

サンプルコード
郵便番号データ

Cloud Functionsについて

Cloud Functionsの種類

まずCloud Functionsの種類について説明します。Cloud Functionsは大きく二つの関数に分かれています。簡単に分けるならば、直接的に呼び出す関数と、副次的に呼び出す関数になります。
同じCloud Functionsを利用した関数であってもスケール特性に差があることを知っておくのは重要です。

関数 説明 特徴
HTTP関数 httpプロトコルから呼び出すことができる関数。functions.https.onCall, functions.https.onRequest が該当します。 トラフィックを処理するために迅速にスケールアップ
バックグラウンド関数 他のサービスからイベントトリガーによって呼び出される関数。
Cloud Firestore
Database
Remote Config
Authentication
Analytics
Crashlytics
Cloud Storage
Cloud Pub/Sub
Test Lab
が該当します。 
緩やかにスケールアップ

Cloud Functionsの上限

Cloud Functionsは3つ観点から制限が設定されています。
これらの上限は、実行時の話だけでなく、関数そのものの容量やデプロイに関することも含まれています。

上限 説明
リソースに関する上限 ファンクションの処理に使用できるリソースの合計量に影響します。
時間に関する上限 実行できる時間の長さに影響します。
レートに関する上限 Cloud Functions API を呼び出すレートと、リソースを使用できるレートに影響します。レートの割り当ては、「一定時間あたりのリソース」と考えることができます。

Cloud Functionsのスケーラビリティ

今回は、Cloud Functionsの実行時に関する話をしようと思うので特に以下の上限について話をします。
まずは以下の上限があることを知っておいてください。

割り当て 上限 説明
関数の最大実行時間 540 秒 強制終了されるまで関数を実行できる時間の上限
1 秒あたりの関数呼び出し数 1 秒あたりの関数呼び出しの回数。これを超過すると、次の割り当て期間まで関数は一時停止したままになります。 100 秒あたり 100,000,000
最大関数メモリ 2,048 MB 関数が使用できるメモリ容量

Cloud Functionsの性能を向上させる

実行するマシン性能をあげる

開発者が最もCloud Functionsの制限を感じるのはまずメモリでしょう。Cloud Functionsをデフォルトで利用すると128MBのメモリで実行され1分でタイムアウトします。

次のようにrunWithへオプションを渡してあげることで、性能を向上させることが可能です。
timeoutSecondsの上限は540、memoryの上限は2GBです。

const runtimeOpts = {
  timeoutSeconds: 300,
  memory: '1GB'
}

exports.myStorageFunction = functions
  .runWith(runtimeOpts)
  .storage
  .object()
  .onFinalize((object) = > {
    // do some complicated things that take a lot of memory and time
  });

また、メモリをあげるとCPUの割当も向上します。CPUとメモリの対応は次のようになっています。
2GBを超えるような処理はCloud Functionsには向いていません。


https://cloud.google.com/functions/pricing#compute_time

サンプルコードでの実行例

今回はサンプルコードで次の処理を行いました。

  • 郵便番号データ(ZIPデータ)をダウンロード(1.7MB)
  • ZIPデータを解凍(展開後のデータ12.3MB)
  • ShiftJISでエンコード
  • データを一行づつ読み込み
  • データベースへ保存

ZIPデータの展開までは最小メモリで大丈夫でしたが、一行づつ読み込んでデータベースへ保存するところでメモリが不足し実行できませんでした。当然ですが数十万行あるデータを通信を返して保存するので、チリも積もればですね。

※うまく構成すれば、この関数一つだけでも十分捌けるかもしれません。今回は次で説明するやり方を試してみたかった。

処理の分散化

今回はCloud Functionsから別のCloud Functionsを起動させて処理を分散化させました。

こういった構成をFan-outと呼びます。

スケジューラーで実行しているCloud Functionsと、Pub/Subを使ってトピックをサブスクライブするCloud Functionsを準備しました。

前段のCloud Functions内部では、データを一行づつ読み込むところまで行い、500行に一度溜め込んんだ500行分のデータをパブリッシュするようにしました。500行の理由はCloud Firestoreのバッチ処理の上限が500だからです。

二段のCloud Functionsでは500行のデータをWriteBatchに詰めてCloud Firestoreに保存する処理のみを行っています。

この構成により、前段のデータがどれだけ大きくなっても(2GBを超えないくらいであれば)保存処理によって処理が停止することはありません。

さらに話を進めるとFan-outの構成にもCloud Functionsの上限が引っかかることがありますが、このくらいの処理ではその上限に達しないので割愛します!と言うかその上限に引っかかるようならCloud Functionsのやめ時かもですね。。。
Cloud Runなどへの以降を推奨します!