Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
7
Help us understand the problem. What is going on with this article?
@citron1605

GASの処理速度が遅いのでNode.jsに移行したらwebアプリが高速に

はじめに

弓道で用いるwebアプリを開発しているんですが、データの読み書きがかなり遅かったので、
GoogleAppsScript+スプレッドシートからNode.js+Firebaseに移行しました。
(GoogleAppsScript Web APIからCloud Functions APIに変えたいうこと)

Google Apps ScriptもNode.jsも基本は一緒ですが...

これでかなりwebアプリの待機時間が短くなったので、記事にしようと思います。

私の使用用途は、スプレッドシートにユーザーやデータを保管し、それを読み書きする、つまりwebアプリのバックエンドとしての利用でGASの処理速度が遅いと感じたものであるので読む際は注意してください。

結果から

どれだけ早くなったか先に紹介しておきます。

的中を記録し、その月のデータをとってくる処理を例に紹介します。記録するデータは日付、中たった数、引いた本数、的中率の四つのデータです。記録した後、付けた日の月のデータ全てとってきます。

↓Google Apps Script + スプレッドシート
gas.gif
ちなみにタイムは2.5秒ほど
スクリーンショット 2020-04-16 18.25.04.png

↓Node.js + Firebase(Cloud Functions API使用)

ezgif.com-video-to-gif (1).gif

スクリーンショット 2020-04-16 18.36.14.png

タイムは驚異の0.4秒!!!!!!!!!!!

約2秒ほど早くなりました。

*上のフェッチしてきたgifの後半に表示されるデータを見ればわかりますが、ばらつきがある状態で行ったので正確ではないですがほぼ同じデータ量です。

私のサービスでGoogleAppsScriptを用いたスプレッドシートでの読み書きが遅いのはなぜか?

結論からいうと、部員の数だけAPIの呼び出しを行っていたため、スプレッドシートの読み書きが遅くなっていました。

Spreadsheetサービスでは、以下のようなメソッドなどを使ってスプレッドシートにアクセスをするたびにAPIが呼び出されます。

・getActiveSpreadsheet()
・getSheetByName()
・getDataRange()
・getLastRow()
・getRange()
・getValue()
・setValue()

二重のfor文の中で
sheet.getRange(i,j).getValue();
なんかした日には終わりで、行数×列数分APIアクセス実行されるので、かなり時間がかかります。

なので、このようにできるだけ呼び出すAPIを減らすために全てのセルのデータを配列として一回のアクセスでとるよう設計しています。
sheet.getDataRange().getValues();

しかし、私のwebアプリでは、部員全員のデータをとってくる処理があり、一回のAPIの呼び出し✖️部員数30で、計30回ほどAPIアクセスをする必要がありました。

この処理が遅い原因だったみたいです。

GoogleAppsScript(WebAPI)の制約

GoogleAppsScript(WebAPI)の制約↓

https://www.bugbugnow.net/2018/12/GoogelAppsScript-restriction.html

一日のURLフェッチ(APIアクセス)が20,000/日、さらには無料のGoogleアカウントでは、スクリプトの実行時間が6分と地味に短いです

用途によっては十分な気もしますが、私の使用目的では、一日10回URLフェッチを行うとして、ユーザー数2000を超えると上限に達してしまいます。もう少し大規模なアプリにしようと思ったら物足りないですね。

有料プランもあるが、そちらも上限が決まっています。

Cloud Functions APIの制約

それに比べてCloud Functions APIの制約というと

https://cloud.google.com/functions/pricing?hl=ja

呼び出し
関数の呼び出し料金は定額制です。HTTP リクエストから呼び出される関数(HTTP 関数)、バックグラウンド関数、call API から行われる呼び出しなど、呼び出し元によって料金が変わることはありません。

月間呼び出し回数 料金(100 万単位)
最初の 200 万回 無料
200 万回を超えた分 $0.40

米ドル以外の通貨でお支払いの場合は、Cloud Platform SKU に記載されている該当通貨の料金が適用されます。
呼び出し料金は 1 回あたり $0.0000004 の単価制で、関数の結果や実行時間に関係なく請求されます。ただし、毎月最初の 200 万回までは無料です。

無料枠がかなりあります。
一ユーザーが一日に10回APIアクセスするとして、1ヶ月で

10✖️30 = 300

毎月最初の 200 万回まで無料なので2000,000÷300 = 6,666人まで無料枠でいけます!

ユーザー数2,000を超えると上限に達してしまうGASのAPIに大してCloud Functions APIは 6,666人までいけるので、断然、Cloud Functions APIの方がいいですね。
さらに超えた分に対しては超えた分の請求なので都合がいいです。上限を超えた際には一回のAPIアクセスをするのに約0.0004円ほどで格安です。

ユーザーが6万6666人いるとして、毎月2,000万回APIアクセスするとしたら月約8,000円ほどなので、そこまで高くないですね。

gas+スプレッドシート からNode.js + Firebaseに移行

以上よりGoogle Apps Script + スプレッドシート ⇨ Node.js + Firebase に移行しました。

gasのコード全部で500行くらいだったのでNode.jsに書き換えるのも一週間くらいで終わりました。

↓冒頭で見せたアプリ内で使われる的中率を書き込みに行き、月のデータをフェッチする処理
gasのコードとNode.jsのコードの比較(一部)を紹介します。

gas
//的中率書き込み
function submitFetchData(e){
  var sheetID =e.parameter["id"];
  var sheetName =e.parameter["name"];
  var date = e.parameter.p1;
  var hitArrow = e.parameter.p2;
  var allArrow = e.parameter.p3;
  var hitRate = hitArrow / allArrow * 100;
  var year =e.parameter["year"];
  var month = e.parameter["month"];
  //JSONオブジェクト格納用の入れ物
  var rowData = {}; 

  //書込先スプレッドシートのIDを入力
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName);
  var arrayData = [[ date, hitArrow, allArrow, hitRate]];

  var rows = arrayData.length;
  var cols = arrayData[0].length;

  sheet.insertRows(2,1);

  //シートに配列を書き込み
  sheet.getRange(2,1,1,cols).setValues(arrayData);

  var thisDate = year + month;
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName); 

  var data = sheet.getDataRange().getValues();
  var array = data.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.month = c;break;
      }
      return result;
    },{}))
  });
  array.shift();

  var date = array.map(function(value,index){
    var thisDate = {};
    var year,month;
    year = value.month.split('/');
    thisDate.year = year[0];
    month = year[1].split('/');
    thisDate.month = month[0];
    return thisDate;
  })
  var dateArray = date.map(function(value, index){
    return value.year + value.month;
  });

   var firstIndex = dateArray.indexOf(thisDate);
   var lastIndex = dateArray.lastIndexOf(thisDate);

  var newData = sheet.getRange(Number(firstIndex+2),1, Number(lastIndex+2) - Number(firstIndex+2) +1, data[0].length).getValues();  
  //オブジェクトに変換
  var newArray = newData.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.date = c;break;
        case 1: result.hit_arrow = c;break;
        case 2: result.all_arrow = c;break;
        case 3: result.hit_rate = c;break;
      }
      return result;
    },{}))
  }); 
   var output = ContentService.createTextOutput(JSON.stringify(newArray, null, 2));
   output.setMimeType(ContentService.MimeType.TEXT); 
   return output;
}

Node.js
//的中率書き込み&月のデータフェッチ
app.get('/hitWrite', async (req, res) => {
    console.log('-----hitWrite-----')
    const rec_name = req.query["rec_name"];
    const id = req.query["id"];
    const team = req.query["team"];
    const hit_arrow = req.query["hit_arrow"];
    const all_arrow = req.query["all_arrow"];
    const hit_rate = hit_arrow / all_arrow * 100;
    const date = req.query["date"];
    const year = req.query["year"];
    const month = req.query["month"];
    const arrayData = {
        all_arrow: Number(all_arrow),
        date,
        hit_arrow: Number(hit_arrow),
        hit_rate: Number(hit_rate),
        id: Math.random().toString(32).substring(2)//ランダムID生成
    };
    let records;
    await db.collection("college").doc(team).collection(id).doc("user_data").update({
        [rec_name]: admin.firestore.FieldValue.arrayUnion(arrayData)
    })
        .then(() => {
            return (
                db.collection("college").doc(team).collection(id).doc("user_data").get()
                    .then((snapshot) => {
                        return (
                            records = snapshot.data()[[rec_name]].filter((value) => {
                                if (value.date.split('/')[0] === year && value.date.split('/')[1] === month) {
                                    return value;
                                }
                            }),
                            res.json(JSON.stringify(records))
                        )
                    })
                    .catch((err) => {
                        console.log(err)
                        res.end();
                    })
            )
        })
        .catch((err) => {
            console.log(err)
            res.end();
        })

})

かなりすっきりしました。

読みやすい、書きやすい、共同開発しやすくなりました。

おわりに

アプリの待機時間が短くなったので、Google Apps Script + スプレッドシート から Node.js + Firebase に移行してかなりよかったです。

ただ、金額的にも月にできるAPIアクセス数にも厳しさがあったGoogle Apps Scriptですが、スプレッドシートに書き込めれるので、部内でシートの共有なんかして直接記録を見たりできるのでこっちも便利だったりします。

ただ、ユーザー数が増えると厳しいのでやっぱり捨てざるをおえないってことでおさらばしないとけません。

このwebアプリのユーザー数は自分の大学のユーザーのみの30人なので、当分気にすることはないのですが...

最後まで見ていただき、ありがとうございました。

7
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
citron1605
弓道部専用のwebアプリHiRateを開発しています。研究は深層学習を用いて、てんかん波の検出を行ってます。釣りとお酒が好きです🥃
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
7
Help us understand the problem. What is going on with this article?