Облачные функции 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;
}
Стэн ван Хоймен
источник
Безопасно ли не возвращать обещание из вышеуказанных вызовов 'once ()'?
jazzgil

Ответы:

112

Огнебазер здесь

Похоже, вы испытываете так называемый холодный запуск функции.

Если ваша функция не выполнялась какое-то время, Cloud Functions переводит ее в режим, в котором используется меньше ресурсов. Затем, когда вы снова нажимаете функцию, она восстанавливает среду из этого режима. Время, необходимое для восстановления, состоит из фиксированной стоимости (например, восстановление контейнера) и частично переменной стоимости (например, если вы используете много узловых модулей, это может занять больше времени).

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

Хорошая новость в том, что вы должны испытать это только во время разработки. Если ваши функции будут часто запускаться в производственной среде, скорее всего, они никогда больше не начнут холодный запуск.

Франк ван Пуффелен
источник
3
Примечание модератора : все комментарии не по теме к этому сообщению были удалены. Используйте комментарии, чтобы запросить разъяснения или предложить только улучшения. Если у вас есть связанный, но другой вопрос, задайте новый вопрос и включите ссылку на него, чтобы лучше понять контекст.
Бхаргав Рао
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/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
источник
Привет, Тайрис, у меня такая же проблема с операцией времени, я пытаюсь реализовать ваше решение. просто пытаюсь понять, кто и когда вызывает функцию инициализации?
Manspof
Привет @AdirZoari, мое объяснение использования init () и т.д., вероятно, не лучшая практика; его ценность - просто продемонстрировать мои выводы относительно основной проблемы. Было бы намного лучше взглянуть на скрытую переменную process.env.FUNCTION_NAMEи использовать ее для условного включения файлов, необходимых для этой функции. Комментарий на github.com/firebase/functions-samples/issues/ ... дает действительно хорошее описание этой работы! Это гарантирует, что глобальная область не загрязнена методами и не включает нерелевантные функции.
Тайрис
1
Привет @davidverweij, я не думаю, что это поможет с точки зрения возможности выполнения ваших функций дважды или параллельно. Функции автоматически масштабируются по мере необходимости, поэтому несколько функций (одна и та же функция или разные) могут выполняться параллельно в любое время. Это означает, что вам необходимо учитывать безопасность данных и рассматривать возможность использования транзакций. Также ознакомьтесь с этой статьей о ваших функциях, которые могут запускаться дважды: cloud.google.com/blog/products/serverless/…
Тайрис,
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, размещенное в контейнере appengine. Время сократилось до 2-3 секунд.

Я сравнивал многие функции mongodb с firestore, и иногда мне тоже интересно, стоит ли на этом раннем этапе моего продукта перейти на другую базу данных. Самым большим советом, который у меня был в firestore, была функциональность триггера onCreate, onUpdate объектов документа.

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

В принципе, если на вашем сайте есть статические части, которые можно выгрузить в среду appengine, возможно, неплохая идея.

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

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

Когда у вас есть приложение на ранней (демонстрационной) стадии, когда оно не используется часто, производительность не будет высокой. Это то, что следует учитывать, так как те, кто раньше внедряет продукт, должны выглядеть лучше перед потенциальными клиентами / инвесторами. Нам понравилась эта технология, поэтому мы перешли со старых проверенных временем фреймворков, но на данный момент наше приложение кажется довольно медленным. Я собираюсь попробовать несколько разогревающих стратегий, чтобы он выглядел лучше

Стэн Свинярски
источник
Мы тестируем cron-задание для пробуждения каждой отдельной функции. Возможно, этот подход вам тоже поможет.
Хесус Фуэнтес,
привет @ JesúsFuentes, мне просто интересно, сработала ли функция пробуждения для вас. Звучит как безумное решение: D
Александр Завалий
1
Привет, @Alexandr, к сожалению, у нас еще не было времени на это, но это в нашем списке первоочередных задач. Хотя теоретически это должно работать. Проблема связана с функциями onCall, которые необходимо запускать из приложения Firebase. Может быть, звонить им от клиента каждые X минут? Посмотрим.
Хесус Фуэнтес
1
@Alexandr мы поговорим за пределами Stackoverflow? Мы можем помочь друг другу новыми подходами.
Хесус Фуэнтес
1
@Alexandr, мы еще не тестировали этот обходной путь "пробуждения", но мы уже развернули наши функции на europe-west1. Все-таки неприемлемые времена.
Хесус Фуэнтес
0

ОБНОВЛЕНИЕ / РЕДАКТИРОВАНИЕ: новый синтаксис и обновления появятся в мае 2020 г.

Я только что опубликовал пакет под названием better-firebase-functions, он автоматически выполняет поиск в вашем каталоге функций и правильно размещает все найденные функции в вашем объекте экспорта, одновременно изолируя функции друг от друга для повышения производительности при холодной загрузке.

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

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})
George43g
источник
интересно .. где я могу увидеть репозиторий "better-firebase-functions"?
JerryGoyal
1
github.com/gramstr/better-firebase-functions - пожалуйста, проверьте это и дайте мне знать, что вы думаете! Не стесняйтесь вносить свой вклад :)
George43g