ฟังก์ชันระบบคลาวด์ของ Firebase ทำงานช้ามาก


132

เรากำลังดำเนินการกับแอปพลิเคชันที่ใช้ฟังก์ชันคลาวด์ Firebase ใหม่ สิ่งที่เกิดขึ้นในขณะนี้คือธุรกรรมถูกใส่ในคิวโหนด จากนั้นฟังก์ชันจะลบโหนดนั้นและวางไว้ในโหนดที่ถูกต้อง สิ่งนี้ถูกนำมาใช้เนื่องจากความสามารถในการทำงานแบบออฟไลน์

ปัญหาปัจจุบันของเราคือความเร็วของฟังก์ชัน ฟังก์ชั่นใช้เวลาประมาณ 400ms ดังนั้นก็ไม่เป็นไร แต่บางครั้งฟังก์ชั่นใช้เวลานานมาก (ประมาณ 8 วินาที) ในขณะที่รายการถูกเพิ่มลงในคิวแล้ว

เราสงสัยว่าเซิร์ฟเวอร์ต้องใช้เวลาในการบู๊ตเพราะเมื่อเราดำเนินการอีกครั้งหลังจากครั้งแรก ใช้เวลาน้อยลง

มีวิธีใดในการแก้ไขปัญหานี้หรือไม่? ที่นี่ฉันเพิ่มรหัสของฟังก์ชันของเรา เราสงสัยว่าไม่มีอะไรผิดปกติ แต่เราได้เพิ่มไว้ในกรณีนี้

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;
}

ปลอดภัยไหมที่จะไม่คืนคำสัญญาของการโทร 'ครั้งเดียว ()' ข้างต้น?
jazzgil

คำตอบ:


112

firebaser ที่นี่

ดูเหมือนว่าคุณกำลังประสบกับสิ่งที่เรียกว่าฟังก์ชันเริ่มเย็น

เมื่อฟังก์ชันของคุณไม่ถูกเรียกใช้งานในบางครั้ง Cloud Functions จะทำให้ฟังก์ชันนี้อยู่ในโหมดที่ใช้ทรัพยากรน้อยลง จากนั้นเมื่อคุณกดฟังก์ชั่นอีกครั้งระบบจะคืนสภาพแวดล้อมจากโหมดนี้ เวลาที่ใช้ในการกู้คืนประกอบด้วยต้นทุนคงที่ (เช่นคืนค่าคอนเทนเนอร์) และต้นทุนผันแปรของชิ้นส่วน (เช่นหากคุณใช้โมดูลโหนดจำนวนมากอาจใช้เวลานานกว่านั้น)

เรากำลังตรวจสอบประสิทธิภาพของการดำเนินการเหล่านี้อย่างต่อเนื่องเพื่อให้แน่ใจว่าส่วนผสมที่ดีที่สุดระหว่างประสบการณ์ของนักพัฒนาซอฟต์แวร์และการใช้ทรัพยากร ดังนั้นคาดว่าเวลาเหล่านี้จะดีขึ้นเมื่อเวลาผ่านไป

ข่าวดีก็คือคุณควรได้รับประสบการณ์นี้ในระหว่างการพัฒนาเท่านั้น เมื่อฟังก์ชันของคุณถูกเรียกใช้บ่อยครั้งในการผลิตโอกาสที่ฟังก์ชันเหล่านั้นแทบจะไม่เริ่มเย็นอีกเลย


3
หมายเหตุผู้ดูแล : ลบความคิดเห็นนอกหัวข้อทั้งหมดในโพสต์นี้แล้ว โปรดใช้ความคิดเห็นเพื่อขอคำชี้แจงหรือแนะนำการปรับปรุงเท่านั้น หากคุณมีคำถามที่เกี่ยวข้อง แต่แตกต่างกันให้ถามคำถามใหม่และใส่ลิงก์ไปยังคำถามนี้เพื่อช่วยระบุบริบท
Bhargav Rao

56

อัปเดตพฤษภาคม 2020ขอบคุณสำหรับความคิดเห็นโดย maganap - ใน Node 10+ FUNCTION_NAMEจะถูกแทนที่ด้วยK_SERVICE( FUNCTION_TARGETเป็นฟังก์ชันเองไม่ใช่ชื่อแทนที่ENTRY_POINT) ตัวอย่างโค้ดด้านล่างได้รับการคัดลอกด้านล่าง

ดูข้อมูลเพิ่มเติมที่https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

อัปเดต - ดูเหมือนว่าปัญหาเหล่านี้จำนวนมากสามารถแก้ไขได้โดยใช้ตัวแปรที่ซ่อนอยู่process.env.FUNCTION_NAMEดังที่เห็นที่นี่: https://github.com/firebase/functions-samples/issues/170#issuecomment-323375462

อัปเดตด้วยรหัส - ตัวอย่างเช่นหากคุณมีไฟล์ดัชนีต่อไปนี้:

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

จากนั้นไฟล์ทั้งหมดของคุณจะถูกโหลดและข้อกำหนดทั้งหมดของไฟล์เหล่านั้นก็จะถูกโหลดด้วยส่งผลให้เกิดค่าใช้จ่ายจำนวนมากและสร้างมลภาวะให้กับขอบเขตทั่วโลกสำหรับฟังก์ชันทั้งหมดของคุณ

แทนที่จะแยกการรวมของคุณออกเป็น:

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');
}

สิ่งนี้จะโหลดไฟล์ที่ต้องการเมื่อมีการเรียกใช้ฟังก์ชันนั้นโดยเฉพาะ ช่วยให้คุณสามารถรักษาขอบเขตทั่วโลกของคุณให้สะอาดยิ่งขึ้นซึ่งจะส่งผลให้รองเท้าเย็นเร็วขึ้น


สิ่งนี้ควรให้วิธีแก้ปัญหาที่ดีกว่าที่ฉันทำด้านล่าง (แม้ว่าคำอธิบายด้านล่างยังคงมีอยู่)


คำตอบเดิม

ดูเหมือนว่าการต้องใช้ไฟล์และการเริ่มต้นทั่วไปที่เกิดขึ้นในขอบเขตทั่วโลกเป็นสาเหตุใหญ่ที่ทำให้การทำงานช้าลงระหว่างการบูตแบบเย็น

เนื่องจากโปรเจ็กต์มีฟังก์ชันมากขึ้นขอบเขตทั่วโลกจะถูกปนเปื้อนมากขึ้นเรื่อย ๆ ทำให้ปัญหาแย่ลงโดยเฉพาะอย่างยิ่งถ้าคุณกำหนดขอบเขตฟังก์ชันของคุณเป็นไฟล์แยกกัน (เช่นโดยใช้Object.assign(exports, require('./more-functions.js'));ในindex.jsไฟล์.

ฉันได้รับการเพิ่มประสิทธิภาพอย่างมากในการบูตแบบเย็นโดยย้ายความต้องการทั้งหมดของฉันไปยังวิธีการเริ่มต้นด้านล่างจากนั้นเรียกมันเป็นบรรทัดแรกภายในข้อกำหนดฟังก์ชันใด ๆ สำหรับไฟล์นั้น เช่น:

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;
}

ฉันได้เห็นการปรับปรุงจากประมาณ 7-8 วินาทีจนถึง 2-3 วินาทีเมื่อใช้เทคนิคนี้กับโปรเจ็กต์ที่มีฟังก์ชัน ~ 30 ใน 8 ไฟล์ สิ่งนี้ดูเหมือนว่าจะทำให้ฟังก์ชั่นจำเป็นต้องทำการบูตแบบเย็นน้อยลงบ่อยครั้ง (น่าจะเป็นเพราะการใช้หน่วยความจำลดลง?)

น่าเสียดายที่สิ่งนี้ยังคงทำให้ฟังก์ชัน HTTP แทบไม่สามารถใช้งานได้สำหรับการใช้งานจริงที่ผู้ใช้ต้องเผชิญ

หวังว่าทีม Firebase จะมีแผนในอนาคตเพื่อให้สามารถกำหนดขอบเขตของฟังก์ชันได้อย่างเหมาะสมเพื่อให้ต้องโหลดเฉพาะโมดูลที่เกี่ยวข้องสำหรับแต่ละฟังก์ชัน


เฮ้ Tyris ฉันประสบปัญหาเดียวกันกับการทำงานของเวลาฉันกำลังพยายามใช้วิธีแก้ปัญหาของคุณ แค่พยายามทำความเข้าใจใครเรียกใช้ฟังก์ชัน init และเมื่อใด
Manspof

สวัสดี @AdirZoari คำอธิบายของฉันเกี่ยวกับการใช้ init () และอื่น ๆ อาจไม่ใช่แนวทางปฏิบัติที่ดีที่สุด คุณค่าของมันเป็นเพียงการแสดงให้เห็นถึงการค้นพบของฉันเกี่ยวกับปัญหาหลัก คุณจะดีกว่ามากหากดูตัวแปรที่ซ่อนอยู่process.env.FUNCTION_NAMEและใช้สิ่งนั้นเพื่อรวมไฟล์ที่จำเป็นสำหรับฟังก์ชันนั้นอย่างมีเงื่อนไข ความคิดเห็นที่github.com/firebase/functions-samples/issues/…ให้คำอธิบายที่ดีจริงๆเกี่ยวกับการทำงานนี้! เพื่อให้แน่ใจว่าขอบเขตทั่วโลกจะไม่ถูกปนเปื้อนด้วยวิธีการและรวมถึงฟังก์ชันที่ไม่เกี่ยวข้อง
Tyris

1
สวัสดี @davidverweij ฉันไม่คิดว่าสิ่งนี้จะช่วยได้ในแง่ของความเป็นไปได้ที่ฟังก์ชันของคุณจะทำงานสองครั้งหรือแบบขนาน ฟังก์ชั่นปรับขนาดอัตโนมัติตามต้องการดังนั้นหลายฟังก์ชัน (ฟังก์ชันเดียวกันหรือต่างกัน) สามารถทำงานควบคู่กันได้ตลอดเวลา ซึ่งหมายความว่าคุณต้องคำนึงถึงความปลอดภัยของข้อมูลและพิจารณาใช้ธุรกรรม นอกจากนี้โปรดอ่านบทความนี้เกี่ยวกับฟังก์ชันของคุณที่อาจทำงานสองครั้ง: cloud.google.com/blog/products/serverless/…
Tyris

1
ประกาศFUNCTIONS_NAMEใช้ได้กับโหนด 6 และ 8 เท่านั้นดังที่อธิบายไว้ที่นี่: cloud.google.com/functions/docs/… . โหนด 10 ควรใช้FUNCTION_TARGET
maganap

1
ขอบคุณสำหรับการอัปเดต @maganap ดูเหมือนว่าควรใช้K_SERVICEตาม doco ที่cloud.google.com/functions/docs/migrating/… - ฉันได้อัปเดตคำตอบแล้ว
Tyris

7

ฉันประสบปัญหาที่คล้ายกันกับฟังก์ชันระบบคลาวด์ของ firestore ที่ใหญ่ที่สุดคือประสิทธิภาพ โดยเฉพาะอย่างยิ่งในกรณีของการเริ่มต้นในช่วงแรก ๆ เมื่อคุณไม่สามารถให้ลูกค้ารายแรก ๆ ดูแอปที่ "เฉื่อยชา" ได้ ฟังก์ชันการสร้างเอกสารอย่างง่ายสำหรับเช่นให้สิ่งนี้:

- การเรียกใช้ฟังก์ชันใช้เวลา 9522 ms เสร็จสิ้นด้วยรหัสสถานะ: 200

จากนั้น: ฉันมีหน้าข้อกำหนดและเงื่อนไขที่ตรงไปตรงมา ด้วยฟังก์ชั่นคลาวด์การดำเนินการเนื่องจากการเริ่มเย็นจะใช้เวลา 10-15 วินาทีแม้ในบางครั้ง จากนั้นฉันก็ย้ายไปยังแอป node.js ซึ่งโฮสต์บน appengine container เวลาลดลงเหลือ 2-3 วินาที

ฉันได้เปรียบเทียบคุณสมบัติหลายอย่างของ mongodb กับ firestore และบางครั้งฉันก็สงสัยเหมือนกันว่าในช่วงแรกของผลิตภัณฑ์ของฉันฉันควรย้ายไปยังฐานข้อมูลอื่นหรือไม่ ความก้าวหน้าที่ใหญ่ที่สุดที่ฉันมีใน firestore คือฟังก์ชันทริกเกอร์ onCreate, onUpdate ของวัตถุเอกสาร

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

โดยทั่วไปหากมีส่วนคงที่ของไซต์ของคุณที่สามารถถ่ายโอนไปยังสภาพแวดล้อมของ appengine ได้อาจไม่ใช่ความคิดที่ดี


1
ฉันไม่คิดว่าฟังก์ชั่น Firebase เหมาะสำหรับวัตถุประสงค์เท่าที่ผู้ใช้แสดงผลแบบไดนามิกต้องเผชิญกับเนื้อหา เราใช้ฟังก์ชัน HTTP สองสามอย่างเท่าที่จำเป็นสำหรับสิ่งต่างๆเช่นการรีเซ็ตรหัสผ่าน แต่โดยทั่วไปหากคุณมีเนื้อหาแบบไดนามิกให้แสดงที่อื่นเป็นแอปด่วน (หรือใช้ภาษาที่แตกต่างกัน)
Tyris

2

ฉันได้ทำสิ่งเหล่านี้เช่นกันซึ่งจะปรับปรุงประสิทธิภาพเมื่อฟังก์ชันอุ่นเครื่อง แต่การเริ่มเย็นก็ฆ่าฉัน ปัญหาอื่น ๆ อย่างหนึ่งที่ฉันพบคือกับ cors เนื่องจากต้องใช้เวลาสองเที่ยวไปยังฟังก์ชันระบบคลาวด์เพื่อให้งานสำเร็จลุล่วง ฉันแน่ใจว่าจะแก้ไขได้

เมื่อคุณมีแอปในช่วงเริ่มต้น (สาธิต) เมื่อไม่ได้ใช้งานบ่อยประสิทธิภาพจะไม่ดีมาก นี่เป็นสิ่งที่ควรพิจารณาเนื่องจากผู้ที่ใช้ผลิตภัณฑ์รุ่นแรก ๆ จะต้องดูดีที่สุดต่อหน้าผู้มีโอกาสเป็นลูกค้า / นักลงทุน เราชอบเทคโนโลยีนี้มากดังนั้นเราจึงย้ายจากเฟรมเวิร์กที่พยายามและจริงรุ่นเก่า แต่แอพของเราดูเหมือนจะซบเซาในตอนนี้ ต่อไปฉันจะลองใช้กลยุทธ์อุ่นเครื่องเพื่อให้ดูดีขึ้น


เรากำลังทดสอบ cron-job เพื่อปลุกทุกฟังก์ชัน บางทีแนวทางนี้ก็ช่วยคุณได้เช่นกัน
Jesús Fuentes

เฮ้ @ JesúsFuentesฉันแค่สงสัยว่าการปลุกฟังก์ชันนี้ใช้ได้ผลกับคุณหรือไม่ ดูเหมือนจะเป็นวิธีแก้ปัญหาที่บ้าคลั่ง: D
Alexandr Zavalii

1
สวัสดี @Alexandr น่าเศร้าที่เรายังไม่มีเวลาทำ แต่มันอยู่ในรายการที่สำคัญที่สุดของเรา มันควรจะทำงานในทางทฤษฎีแม้ว่า ปัญหามาพร้อมกับฟังก์ชัน onCall ซึ่งจำเป็นต้องเปิดใช้งานจากแอป Firebase อาจจะโทรหาพวกเขาจากลูกค้าทุกๆ X นาที? เราจะเห็น
Jesús Fuentes

1
@Alexandr เราจะคุยกันนอก Stackoverflow หรือไม่? เราอาจช่วยกันด้วยแนวทางใหม่ ๆ
Jesús Fuentes

1
@Alexandr เรายังไม่ได้ทดสอบวิธีแก้ปัญหา 'การปลุกระบบ' นี้ แต่เราได้ปรับใช้ฟังก์ชันของเราไปยังยุโรปตะวันตก 1 แล้ว ยังคงเป็นช่วงเวลาที่ยอมรับไม่ได้
Jesús Fuentes

0

อัปเดต / แก้ไข: ไวยากรณ์ใหม่และการอัปเดตที่กำลังจะมาในเดือนพฤษภาคม 2563

ฉันเพิ่งเผยแพร่แพ็คเกจที่เรียกว่าbetter-firebase-functionsมันค้นหาไดเร็กทอรีฟังก์ชันของคุณโดยอัตโนมัติและทำรังฟังก์ชันที่พบทั้งหมดในอ็อบเจ็กต์การเอ็กซ์พอร์ตของคุณอย่างถูกต้องในขณะที่แยกฟังก์ชันออกจากกันเพื่อปรับปรุงประสิทธิภาพการบูตแบบเย็น

หากคุณขี้เกียจโหลดและแคชเฉพาะการอ้างอิงที่คุณต้องการสำหรับแต่ละฟังก์ชันภายในขอบเขตโมดูลคุณจะพบว่านี่เป็นวิธีที่ง่ายและสะดวกที่สุดในการทำให้ฟังก์ชันของคุณมีประสิทธิภาพสูงสุดในโครงการที่เติบโตอย่างรวดเร็ว

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})

น่าสนใจ .. ฉันจะดู repo ของ 'better-firebase-functions' ได้ที่ไหน
JerryGoyal

1
github.com/gramstr/better-firebase-functions - โปรดตรวจสอบและแจ้งให้เราทราบว่าคุณคิดอย่างไร! อย่าลังเลที่จะมีส่วนร่วมเช่นกัน :)
George43g
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.