Хмарні функції Firebase дуже повільні


131

Ми працюємо над додатком, який використовує нові хмарні функції firebase. Наразі відбувається те, що транзакція ставиться у вузол черги. А потім функція видаляє цей вузол і ставить його у правильний вузол. Це було реалізовано через можливість роботи в режимі офлайн.

Наша нинішня проблема - швидкість функції. Сама функція займає близько 400 мс, так що це добре. Але іноді функції займають дуже тривалий час (близько 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;
}

Чи безпечно не повертати Обіцянку вище "один раз ()" дзвінків?
джазгіл

Відповіді:


111

тут пожежник

Здається, ви відчуваєте так званий холодний запуск функції.

Якщо ваша функція не була виконана протягом певного часу, Cloud Functions переводить її в режим, що використовує менше ресурсів. Потім, коли ви знову натиснете функцію, вона відновлює середовище з цього режиму. Час, необхідний для відновлення, складається з фіксованої вартості (наприклад, відновлення контейнера) та змінної вартості частини (наприклад, якщо ви використовуєте багато модулів вузлів, це може зайняти більше часу).

Ми постійно контролюємо ефективність цих операцій, щоб забезпечити найкращу суміш між досвідом розробника та використанням ресурсів. Тож очікуйте, що ці часи покращаться з часом.

Хороша новина полягає в тому, що ви повинні відчувати це лише під час розвитку. Після того, як ваші функції будуть часто спрацьовувати у виробництві, швидше за все, вони навряд чи вдарять холодний старт заново.


3
Примітка модератора : Усі коментарі поза темою до цієї публікації видалено. Будь ласка, використовуйте коментарі, щоб подати роз'яснення або запропонувати лише удосконалення. Якщо у вас є пов'язане, але інше питання, поставте нове запитання та включіть посилання на це, щоб допомогти забезпечити контекст.
Bhargav Rao

55

Оновлення травень 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/isissue/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 .

Мені вдалося побачити величезні вигоди в швидкості завантаження, перемістивши всі мої потреби в метод init, як показано нижче, а потім викликати його як перший рядок у будь-якому визначенні функції цього файлу. Наприклад:

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 має в майбутньому деякі плани, щоб забезпечити правильний розподіл функцій, так що для кожної функції потрібно буде завантажувати лише відповідні модулі.


Гей, Таріс, я зіткнувся з тим же питанням, коли працює час, я намагаюся реалізувати ваше рішення. просто намагаюся зрозуміти, хто викликає функцію init і коли?
Manspof

Привіт @AdirZoari, моє пояснення щодо використання init () тощо - це, мабуть, не найкраща практика; його цінність полягає лише в тому, щоб продемонструвати мої висновки щодо основної проблеми. Вам буде набагато краще переглянути приховану змінну process.env.FUNCTION_NAMEі використовувати її для умовного включення файлів, необхідних для цієї функції. Коментар на сайті github.com/firebase/functions-samples/isissue/… дає дійсно хороший опис роботи! Це гарантує, що глобальна сфера не забруднена методами та включає в себе невідповідні функції.
Тіріс

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згідно доко за адресою cloud.google.com/functions/docs/migrating/… - я оновив свою відповідь.
Тиріс

7

Я стикаюся з подібними проблемами з функціями хмари firestore. Найбільша - продуктивність. Особливо у випадку запуску на ранній стадії, коли ви не можете дозволити своїм раннім клієнтам бачити "мляві" програми. Проста функція генерації документації, наприклад, дає:

- Виконання функції займало 9522 мс, завершено кодом статусу: 200

Потім: у мене була сторінка з прямими умовами та умовами. За допомогою хмарних функцій виконання через холодний старт займе 10-15 секунд навіть часом. Потім я перемістив його в додаток node.js, розміщений на контейнері з аппендином. Час знизився до 2-3 секунд.

Я порівнював багато особливостей mongodb з firestore, і іноді мені занадто цікаво, чи під час цієї ранньої фази свого продукту я також повинен перейти до іншої бази даних. Найбільша реклама у firestore була функцією тригера onCreate, onUpdate об'єктів документа.

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

В основному, якщо є статичні частини вашого сайту, які можуть бути завантажені в середовище аппендину, можливо, це не погана ідея.


1
Я не думаю, що функції Firebase підходять за призначенням, що стосується відображення динамічного вмісту, орієнтованого на користувача. Ми використовуємо кілька функцій HTTP для таких речей, як скидання пароля, але загалом, якщо у вас є динамічний контент, подайте його в іншому місці як експрес-додаток (або використовуйте різну мову).
Тиріс

2

Я також це робив, що покращує продуктивність, коли функції розігріваються, але холодний старт мене вбиває. Одне з інших питань, з якими я стикався, - це з корсами, оскільки для виконання роботи потрібно дві поїздки до хмарних функцій. Я впевнений, що можу це виправити.

Якщо у вас є додаток на його ранній (демо) фазі, коли він не використовується часто, продуктивність не буде великою. Це те, що слід враховувати, оскільки ранні користувачі, які користуються раннім продуктом, повинні виглядати якнайкраще перед потенційними клієнтами / інвесторами. Ми любили цю технологію, тому ми перейшли зі старих випробуваних рамок, але наш додаток здається досить млявим на даний момент. Я збираюся наступного спробувати кілька стратегій розминки, щоб зробити вигляд краще


Ми випробовуємо cron-завдання, щоб пробудити кожну функцію. Можливо, і такий підхід вам допоможе.
Хесус Фуентес

Ей @ JesúsFuentes Мені було просто цікаво, чи функція пробудження працює для вас.
Здається,

1
Привіт @ Александр, на жаль, ми ще не встигли це зробити, але це в нашому головному списку пріоритетів. Це має працювати теоретично. Проблема пов'язана з функціями onCall, які потрібно запустити з програми Firebase. Можливо, телефонує їм від клієнта кожні X хв? Побачимо.
Jesús Fuentes

1
@Alexandr чи будемо вести розмову поза Stackoverflow? Ми можемо допомогти один одному в нових підходах.
Jesús Fuentes

1
@Alexandr ми ще не перевіряли цей спосіб "пробудження", але ми вже розгорнули свої функції на europe-west1. Все-таки неприйнятні часи.
Jesús Fuentes

0

ОНОВЛЕННЯ / РЕДАКТУВАННЯ: новий синтаксис та оновлення, що надходять до 20 травня

Я щойно опублікував пакет під назвою better-firebase-functions , він автоматично шукає ваш каталог функцій і правильно вкладає всі знайдені функції у вашому об’єкті експорту, при цьому виділяючи функції одна від одної, щоб покращити продуктивність холодного завантаження.

Якщо ви ледаче завантажуєте і кешуєте лише необхідні залежності для кожної функції в межах модуля, ви виявите, що це найпростіший і найпростіший спосіб зберегти свої функції оптимально ефективними під час швидко зростаючого проекту.

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

Цікаво .. де я можу побачити репо "краще-firebase-функції"?
JerryGoyal

1
github.com/gramstr/better-firebase-functions - будь ласка, перевірте це і повідомте мені, що ви думаєте! Не соромтесь також робити внесок :)
George43g
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.