Fungsi cloud Firebase sangat lambat

131

Kami sedang mengerjakan aplikasi yang menggunakan fungsi cloud firebase baru. Apa yang saat ini terjadi adalah transaksi dimasukkan ke dalam node antrian. Dan kemudian fungsi menghapus simpul itu dan meletakkannya di simpul yang benar. Ini telah dilaksanakan karena kemampuan untuk bekerja secara offline.

Masalah kita saat ini adalah kecepatan fungsinya. Fungsinya sendiri membutuhkan waktu sekitar 400ms, jadi tidak apa-apa. Namun terkadang fungsinya memakan waktu sangat lama (sekitar 8 detik), sementara entri sudah ditambahkan ke antrian.

Kami menduga bahwa server membutuhkan waktu untuk boot, karena ketika kami melakukan tindakan sekali lagi setelah yang pertama. Ini membutuhkan waktu lebih sedikit.

Apakah ada cara untuk mengatasi masalah ini? Di sini saya menambahkan kode fungsi kami. Kami curiga tidak ada yang salah dengan itu, tetapi kami menambahkannya untuk berjaga-jaga.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const database = admin.database();

exports.insertTransaction = functions.database
    .ref('/userPlacePromotionTransactionsQueue/{userKey}/{placeKey}/{promotionKey}/{transactionKey}')
    .onWrite(event => {
        if (event.data.val() == null) return null;

        // get keys
        const userKey = event.params.userKey;
        const placeKey = event.params.placeKey;
        const promotionKey = event.params.promotionKey;
        const transactionKey = event.params.transactionKey;

        // init update object
        const data = {};

        // get the transaction
        const transaction = event.data.val();

        // transfer transaction
        saveTransaction(data, transaction, userKey, placeKey, promotionKey, transactionKey);
        // remove from queue
        data[`/userPlacePromotionTransactionsQueue/${userKey}/${placeKey}/${promotionKey}/${transactionKey}`] = null;

        // fetch promotion
        database.ref(`promotions/${promotionKey}`).once('value', (snapshot) => {
            // Check if the promotion exists.
            if (!snapshot.exists()) {
                return null;
            }

            const promotion = snapshot.val();

            // fetch the current stamp count
            database.ref(`userPromotionStampCount/${userKey}/${promotionKey}`).once('value', (snapshot) => {
                let currentStampCount = 0;
                if (snapshot.exists()) currentStampCount = parseInt(snapshot.val());

                data[`userPromotionStampCount/${userKey}/${promotionKey}`] = currentStampCount + transaction.amount;

                // determines if there are new full cards
                const currentFullcards = Math.floor(currentStampCount > 0 ? currentStampCount / promotion.stamps : 0);
                const newStamps = currentStampCount + transaction.amount;
                const newFullcards = Math.floor(newStamps / promotion.stamps);

                if (newFullcards > currentFullcards) {
                    for (let i = 0; i < (newFullcards - currentFullcards); i++) {
                        const cardTransaction = {
                            action: "pending",
                            promotion_id: promotionKey,
                            user_id: userKey,
                            amount: 0,
                            type: "stamp",
                            date: transaction.date,
                            is_reversed: false
                        };

                        saveTransaction(data, cardTransaction, userKey, placeKey, promotionKey);

                        const completedPromotion = {
                            promotion_id: promotionKey,
                            user_id: userKey,
                            has_used: false,
                            date: admin.database.ServerValue.TIMESTAMP
                        };

                        const promotionPushKey = database
                            .ref()
                            .child(`userPlaceCompletedPromotions/${userKey}/${placeKey}`)
                            .push()
                            .key;

                        data[`userPlaceCompletedPromotions/${userKey}/${placeKey}/${promotionPushKey}`] = completedPromotion;
                        data[`userCompletedPromotions/${userKey}/${promotionPushKey}`] = completedPromotion;
                    }
                }

                return database.ref().update(data);
            }, (error) => {
                // Log to the console if an error happened.
                console.log('The read failed: ' + error.code);
                return null;
            });

        }, (error) => {
            // Log to the console if an error happened.
            console.log('The read failed: ' + error.code);
            return null;
        });
    });

function saveTransaction(data, transaction, userKey, placeKey, promotionKey, transactionKey) {
    if (!transactionKey) {
        transactionKey = database.ref('transactions').push().key;
    }

    data[`transactions/${transactionKey}`] = transaction;
    data[`placeTransactions/${placeKey}/${transactionKey}`] = transaction;
    data[`userPlacePromotionTransactions/${userKey}/${placeKey}/${promotionKey}/${transactionKey}`] = transaction;
}
Stan van Heumen
sumber
Apakah aman untuk tidak membalas panggilan 'Once ()' Promise di atas?
jazzgil

Jawaban:

112

firebaser di sini

Sepertinya Anda mengalami apa yang disebut cold start dari fungsi tersebut.

Jika fungsi Anda belum dijalankan dalam beberapa waktu, Cloud Functions menempatkannya dalam mode yang menggunakan sumber daya lebih sedikit. Kemudian ketika Anda menekan fungsi itu lagi, itu memulihkan lingkungan dari mode ini. Waktu yang diperlukan untuk memulihkan terdiri dari biaya tetap (mis. Memulihkan kontainer) dan sebagian biaya variabel (mis. Jika Anda menggunakan banyak modul node, mungkin perlu waktu lebih lama).

Kami terus memantau kinerja operasi ini untuk memastikan perpaduan terbaik antara pengalaman pengembang dan penggunaan sumber daya. Jadi, perkirakan waktu-waktu ini akan meningkat seiring waktu.

Kabar baiknya adalah Anda seharusnya hanya mengalami ini selama pengembangan. Setelah fungsi Anda sering dipicu dalam produksi, kemungkinan besar fungsi tersebut tidak akan pernah mengalami start cold lagi.

Frank van Puffelen
sumber
3
Moderator Catatan : Semua komentar di luar topik pada posting ini telah dihapus. Silakan gunakan komentar untuk meminta klarifikasi atau menyarankan perbaikan saja. Jika Anda memiliki pertanyaan terkait tetapi berbeda, ajukan pertanyaan baru , dan sertakan link ke pertanyaan ini untuk membantu memberikan konteks.
Bhargav Rao
55

Perbarui Mei 2020 Terima kasih atas komentar oleh maganap - di Node 10+ FUNCTION_NAMEdiganti dengan K_SERVICE( FUNCTION_TARGETapakah fungsinya sendiri, bukan namanya, diganti ENTRY_POINT). Contoh kode di bawah ini telah di udpasi di bawah ini.

Info selengkapnya di https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

Pembaruan - sepertinya banyak dari masalah ini dapat diselesaikan menggunakan variabel tersembunyi process.env.FUNCTION_NAMEseperti yang terlihat di sini: https://github.com/firebase/functions-samples/issues/170#issuecomment-323375462

Perbarui dengan kode - Misalnya, jika Anda memiliki file indeks berikut:

...
exports.doSomeThing = require('./doSomeThing');
exports.doSomeThingElse = require('./doSomeThingElse');
exports.doOtherStuff = require('./doOtherStuff');
// and more.......

Kemudian semua file Anda akan dimuat, dan semua persyaratan file tersebut juga akan dimuat, mengakibatkan banyak overhead dan mencemari cakupan global Anda untuk semua fungsi Anda.

Alih-alih memisahkan penyertaan Anda sebagai:

const function_name = process.env.FUNCTION_NAME || process.env.K_SERVICE;
if (!function_name || function_name === 'doSomeThing') {
  exports.doSomeThing = require('./doSomeThing');
}
if (!function_name || function_name === 'doSomeThingElse') {
  exports.doSomeThingElse = require('./doSomeThingElse');
}
if (!function_name || function_name === 'doOtherStuff') {
  exports.doOtherStuff = require('./doOtherStuff');
}

Ini hanya akan memuat file yang diperlukan ketika fungsi itu secara khusus dipanggil; memungkinkan Anda untuk menjaga cakupan global Anda jauh lebih bersih yang seharusnya menghasilkan cold-boots yang lebih cepat.


Ini seharusnya memungkinkan solusi yang jauh lebih rapi daripada yang saya lakukan di bawah (meskipun penjelasan di bawah masih berlaku).


Jawaban Asli

Sepertinya memerlukan file dan inisialisasi umum yang terjadi dalam lingkup global adalah penyebab besar perlambatan selama cold-boot.

Saat proyek mendapatkan lebih banyak fungsi, cakupan global semakin tercemar sehingga masalah menjadi lebih buruk - terutama jika Anda menskalakan fungsi ke dalam file terpisah (seperti dengan menggunakan Object.assign(exports, require('./more-functions.js'));di file index.js.

Saya telah berhasil melihat keuntungan besar dalam kinerja boot dingin dengan memindahkan semua kebutuhan saya ke metode init seperti di bawah ini dan kemudian menyebutnya sebagai baris pertama di dalam definisi fungsi apa pun untuk file itu. Misalnya:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
// Late initialisers for performance
let initialised = false;
let handlebars;
let fs;
let path;
let encrypt;

function init() {
  if (initialised) { return; }

  handlebars = require('handlebars');
  fs = require('fs');
  path = require('path');
  ({ encrypt } = require('../common'));
  // Maybe do some handlebars compilation here too

  initialised = true;
}

Saya telah melihat peningkatan dari sekitar 7-8 ke 2-3 saat menerapkan teknik ini ke proyek dengan ~ 30 fungsi di 8 file. Ini juga tampaknya menyebabkan fungsi perlu di-boot dingin lebih jarang (mungkin karena penggunaan memori yang lebih rendah?)

Sayangnya ini masih membuat fungsi HTTP hampir tidak dapat digunakan untuk penggunaan produksi yang dihadapi pengguna.

Berharap tim Firebase memiliki beberapa rencana di masa mendatang untuk memungkinkan pelingkupan fungsi yang tepat sehingga hanya modul yang relevan yang perlu dimuat untuk setiap fungsi.

Tyris
sumber
Hai Tyris, saya menghadapi masalah yang sama dengan operasi waktu, saya mencoba menerapkan solusi Anda. hanya mencoba untuk memahami, siapa yang memanggil fungsi init dan kapan?
Manspof
Hai @AdirZoari, penjelasan saya tentang penggunaan init () dan sebagainya mungkin bukan praktik terbaik; nilainya hanya untuk menunjukkan temuan saya tentang masalah inti. Anda akan jauh lebih baik melihat variabel tersembunyi process.env.FUNCTION_NAMEdan menggunakannya untuk menyertakan file yang diperlukan untuk fungsi itu secara bersyarat. Komentar di github.com/firebase/functions-samples/issues/… memberikan deskripsi yang sangat bagus tentang cara kerja ini! Ini memastikan bahwa cakupan global tidak tercemar dengan metode dan termasuk dari fungsi yang tidak relevan.
Tyris
1
Hai @davidverweij, saya rasa ini tidak akan membantu dalam hal kemungkinan fungsi Anda berjalan dua kali atau secara paralel. Fungsi skala otomatis sesuai kebutuhan sehingga beberapa fungsi (fungsi yang sama, atau fungsi berbeda) dapat berjalan secara paralel kapan saja. Ini berarti Anda harus mempertimbangkan keamanan data dan mempertimbangkan untuk menggunakan transaksi. Juga, lihat artikel ini tentang fungsi Anda yang mungkin berjalan dua kali: cloud.google.com/blog/products/serverless/…
Tyris
1
Pemberitahuan FUNCTIONS_NAMEhanya valid dengan node 6 dan 8, seperti yang dijelaskan di sini: cloud.google.com/functions/docs/… . Node 10 harus digunakanFUNCTION_TARGET
maganap
1
Terima kasih telah memperbarui @maganap, sepertinya itu harus digunakan K_SERVICEsesuai dengan doco di cloud.google.com/functions/docs/migrating/… - Saya telah memperbarui jawaban saya.
Tyris
7

Saya menghadapi masalah serupa dengan fungsi cloud firestore. Yang terbesar adalah kinerja. Khususnya dalam kasus permulaan tahap awal, ketika Anda tidak mampu memberi pelanggan awal untuk melihat aplikasi yang "lamban". Fungsi pembuatan dokumentasi sederhana misalnya memberikan ini:

- Eksekusi fungsi membutuhkan waktu 9522 ms, selesai dengan kode status: 200

Kemudian: Saya memiliki halaman syarat dan ketentuan yang tegas. Dengan fungsi cloud, eksekusi karena start yang dingin akan memakan waktu 10-15 detik bahkan pada waktu tertentu. Saya kemudian memindahkannya ke aplikasi node.js, dihosting di wadah appengine. Waktunya telah turun menjadi 2-3 detik.

Saya telah membandingkan banyak fitur mongodb dengan firestore dan terkadang saya juga bertanya-tanya apakah selama fase awal produk saya ini, saya juga harus pindah ke database yang berbeda. Adv terbesar yang saya miliki di firestore adalah fungsi pemicu onCreate, onUpdate objek dokumen.

https://db-engines.com/en/system/Google+Cloud+Firestore%3BMongoDB

Pada dasarnya jika ada bagian statis dari situs Anda yang dapat dipindahkan ke lingkungan appengine, mungkin bukan ide yang buruk.

Sudhakar R
sumber
1
Menurut saya, Fungsi Firebase tidak sesuai untuk tujuan sejauh menampilkan konten dinamis yang dihadapi pengguna. Kami menggunakan beberapa fungsi HTTP dengan hemat untuk hal-hal seperti penyetelan ulang kata sandi, tetapi secara umum jika Anda memiliki konten dinamis, sajikan di tempat lain sebagai aplikasi ekspres (atau gunakan bahasa yang berbeda).
Tyris
2

Saya telah melakukan hal-hal ini juga, yang meningkatkan kinerja setelah fungsinya dihangatkan, tetapi permulaan yang dingin membunuh saya. Salah satu masalah lain yang saya temui adalah dengan cors, karena diperlukan dua perjalanan ke fungsi cloud untuk menyelesaikan pekerjaan. Saya yakin saya bisa memperbaikinya.

Saat Anda memiliki aplikasi dalam fase awal (demo) dan tidak sering digunakan, kinerjanya tidak akan bagus. Ini adalah sesuatu yang harus dipertimbangkan, karena pengadopsi awal dengan produk awal perlu tampil terbaik di depan calon pelanggan / investor. Kami menyukai teknologinya sehingga kami bermigrasi dari kerangka kerja lama yang telah dicoba dan benar, tetapi aplikasi kami tampaknya cukup lamban pada saat ini. Selanjutnya saya akan mencoba beberapa strategi pemanasan agar terlihat lebih baik

Stan Swiniarski
sumber
Kami sedang menguji cron-job untuk mengaktifkan setiap fungsi. Mungkin pendekatan ini juga membantu Anda.
Jesús Fuentes
hai @ JesúsFuentes Saya hanya ingin tahu apakah fungsi bangun ini bekerja untuk Anda. Kedengarannya seperti solusi gila: D
Alexandr Zavalii
1
Hai @Alexandr, sayangnya kami belum punya waktu untuk melakukannya, tetapi itu ada dalam daftar prioritas utama kami. Ini harus bekerja secara teoritis. Masalahnya muncul pada fungsi onCall, yang perlu diluncurkan dari Aplikasi Firebase. Mungkin memanggil mereka dari klien setiap X menit? Kita lihat saja nanti.
Jesús Fuentes
1
@Alexandr haruskah kita melakukan percakapan di luar Stackoverflow? Kami mungkin saling membantu dengan pendekatan baru.
Jesús Fuentes
1
@Alexandr kami belum menguji solusi 'bangun' ini, tetapi kami sudah menerapkan fungsi kami ke europe-west1. Tetap saja, waktu yang tidak bisa diterima.
Jesús Fuentes
0

UPDATE / EDIT: sintaks baru dan pembaruan akan datang MAY2020

Saya baru saja menerbitkan sebuah paket bernama better-firebase-functions, itu secara otomatis mencari direktori fungsi Anda dan dengan benar menyarangkan semua fungsi yang ditemukan di objek ekspor Anda, sambil mengisolasi fungsi satu sama lain untuk meningkatkan kinerja boot dingin.

Jika Anda lazy-load dan cache hanya dependensi yang Anda butuhkan untuk setiap fungsi dalam lingkup modul, Anda akan menemukan bahwa itu adalah cara termudah dan termudah untuk menjaga fungsi Anda tetap efisien secara optimal melalui proyek yang berkembang pesat.

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})
George43g
sumber
menarik .. di mana saya bisa melihat repo 'better-firebase-functions'?
JerryGoyal
1
github.com/gramstr/better-firebase-functions - silakan periksa dan beri tahu saya pendapat Anda! Jangan ragu untuk berkontribusi juga :)
George43g