Google Apps Scriptで並列処理をしたい
背景と概要
これまでGoogle Apps Script (GAS)を使った並列処理を行う際は、golangやjavascript, pythonなどの外部言語を利用していました。この場合、ネイティブのGASではないため、GAS上でのトリガーによる起動はできませんでした。このため、可能であればネイティブのGASだけで並列処理ができればと常々感じていました。2018年1月19日のGASのアップデートでUrlFetchサービスに追加されたfetchAllについて調べたところ、fetchAllは与えられるリクエストが非同期で実行されていることが認められました。[Ref] これにより、GASで並列処理ができるのではないかとの結論に至りましたので本記事にて紹介させていただきます。
fetchAllについて
初めにUrlFetchサービスに追加されたfetchAllメソッドについて簡単に記載させていただきます。fetchAllメソッドの使い方を見ても動作状態などは書かれていませんでしたので、実際に調べてみようと次のような実験を行いました。
- Web Appsを5つデプロイ
- 動作は5秒のスリープ
- デプロイした5つのWeb AppsへのコールをリクエストとしてfetchAllメソッドで実行
この結果、UrlFetchApp.fetchAll()
は総実行時間は5秒程度だったのに対して、従来のUrlFetchApp.fetch()
では総実行時間は25秒程度でした。これはfetchAllメソッドが非同期で実行していることを意味します。この結果を得たことで並列処理に応用してみようと考えました。
詳細、並びに使用したスクリプトはこちらのgistでご覧いただけます。
GASを用いた並列処理
fetchAllメソッドが非同期で動作することが確認できたことで、このfetchAllメソッドとApps Script APIのscripts.runメソッドを利用してネイティブGASによる並列処理が可能なのではないかと考えました。そこで作成したのがRunAll(GitHub)です。これはGASのライブラリとして作成しました。スクリプト自体はシンプルにも関わらずライブラリとして作成した理由は、クライアント側のスコープの自由度を維持したいと考えたためです。この意味の詳細はこちらをご覧ください。
RunAllのインストール
RunAll (https://github.com/tanaikech/RunAll)を使用するまでの手順は下記の通りです。
- スタンドアロンスクリプトタイプあるいはバウンドスクリプトタイプのプロジェクトを用意する。
- ライブラリ(RunAll)をインストールする。インストール方法はこちらです。
- ライブラリのプロジェクトキーは
1FWYhQFhL7UIAZJn-FR3TlcHvXwHPJc2HwI4vtmNUAQv2OybGe-S97Lal
です。
- ライブラリのプロジェクトキーは
- 実行可能APIとして導入する。詳細はこちらです。
- スクリプトエディタ上で「公開」 -> 「実行可能APIとして導入」を選択する。
- バージョンを入力し、スクリプトにアクセスできるユーザーは、「自分のみ」として「配置」をクリックする。
- 「続行」をクリックし、「閉じる」ボタンが現れたら閉じる。
- Apps Script APIを有効にする。
- 実行可能APIとして導入したプロジェクトのスクリプトエディタを開いている場合、「リソース」 -> 「Cloud Platform プロジェクト...」を選択して表示されるプロジェクトID(このような文字列
project-id-1234567890123456789
)をコピーする。 - プロジェクトIDを使って、下記URLへブラウザでアクセスする。
https://console.cloud.google.com/apis/library/script.googleapis.com/?project=### project ID ###
- ブラウザで開いた際、「有効にする」をクリックし、有効になるのを待つ。
- 実行可能APIとして導入したプロジェクトのスクリプトエディタを開いている場合、「リソース」 -> 「Cloud Platform プロジェクト...」を選択して表示されるプロジェクトID(このような文字列
- スクリプトエディタへ戻って保存ボタンを押してプロジェクトを保存する。
- 実は、この操作はとても重要でした。これを知らない頃は、設定をしても
Requested entity was not found.
のエラーが発生して立ち往生してしまいました。スクリプトエディタの保存ボタンを押すことで現状のスクリプトが反映されるようです。
- 実は、この操作はとても重要でした。これを知らない頃は、設定をしても
使用方法
サンプルスクリプトは次の通りです。このスクリプトを実行可能APIとして導入したプロジェクトへコピーペーストしてmain()
を実行してください。
function myFunction(e) {
Utilities.sleep(1000);
return e;
}
function main() {
// Please set "workers.length < 30".
var workers = [
{
functionName: "myFunction",
arguments: ["request1"],
},
{
functionName: "myFunction",
arguments: ["request2"],
},
];
var results = RunAll.Do(workers); // Using this library
Logger.log(results);
}
main()
が実行されると、Apps Script APIのscripts.runメソッドがスクリプトにあるmyFunction(e)
を実行して結果を返します。myFunction(e)
へ与える引数はworkers
のarguments
です。レスポンスはscripts.runメソッドと同じです。このサンプルスクリプトでは、与えたworkerの処理が全て完了した後に結果が一度に返されるようにしていますが、workerが処理する関数内を工夫することで完了した結果を随時返却して次の実行へ繋げることも可能かと思われます。
workerの最大数
ここで気になるのが、どの程度のworkerを動作させることができるかです。今の場合、Apps Script APIのscripts.runメソッドで1度に呼ぶことのできる関数の最大数がworkerの最大数と一致することはわかります。workerの最大数を調べるために、上記のサンプルスクリプトを使ってworkerの数を増加させたときの実行時間と返される結果を調べました。
図1: worker数と実行時間の関係.
図2: worker数と返された結果の中からエラーを除外した数の関係. 横軸はworker数. 縦軸は総worker数からエラーが返されたworker数を引き算した数.
図1, 2は測定した結果の中の代表的な一つです。図1から、サンプルスクリプトで各workerの関数にUtilities.sleep(1000)
が入っているため、実行時間は1秒程度に集中しています。この実行時間は、worker数に対して横軸に対しておおよそ平行になっていることから、各workerは並列処理として動作したことが分かります。図2からは、worker数が30以上ではエラーの無い数が30程度にとどまっていることが分かります。エラーが返された場合、Service invoked too many times in a short time: exec qps. Try Utilities.sleep(1000) between calls.,
のようなエラーメッセージが返されます。エラーの無い数が30程度にとどまっていることは、Apps Script APIのscripts.runメソッドで1度に呼ぶことのできる関数の最大数が30程度であることを示しています。同じ実験をWeb Appsに対しても行ったところ、同様の結果を得ました。実際に使用する際は、28を最大有効数とすることで今のところ確実に結果が返されることを確認しています。fetchAllメソッド自体は、リクエスト数を1000にしても普通に動作しますので(これは流石に今後制限されるかもしれないと感じました。)、容量としてはまだまだ使える状況です。RunAllのv1.0.0としては一つのサーバー、すなわち、ライブラリをインストールしたプロジェクト単体を対象にしましたので、最大値はApps Script APIのscripts.runメソッドで1度に呼ぶことのできる関数の最大数(最大有効数は28)となります。
今後
今回の実験で単体で有効最大worker数28の並列処理が可能であることが認められたことで、今後は複数のサーバーを用意してどの程度まで計算速度を高めることができるかについても実験してみたいと思っています。また結果がまとまった際にはこちらで報告させていただきたいと思います。