Funkcje chmury Firebase działają bardzo wolno

131

Pracujemy nad aplikacją, która korzysta z nowych funkcji chmury Firebase. Obecnie dzieje się tak, że transakcja jest umieszczana w węźle kolejki. Następnie funkcja usuwa ten węzeł i umieszcza go we właściwym węźle. Zostało to zaimplementowane ze względu na możliwość pracy w trybie offline.

Naszym obecnym problemem jest szybkość funkcji. Sama funkcja zajmuje około 400 ms, więc to w porządku. Ale czasami funkcje zajmują bardzo dużo czasu (około 8 sekund), podczas gdy wpis został już dodany do kolejki.

Podejrzewamy, że serwer potrzebuje czasu na uruchomienie, bo gdy wykonujemy akcję jeszcze raz po pierwszej. Zajmuje to znacznie mniej czasu.

Czy jest jakiś sposób na rozwiązanie tego problemu? Tutaj dodałem kod naszej funkcji. Podejrzewamy, że nie ma w tym nic złego, ale dodaliśmy to na wszelki wypadek.

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
źródło
Czy bezpiecznie jest nie zwracać obietnicy powyższych wywołań „Once ()”?
jazzgil

Odpowiedzi:

111

firebaser tutaj

Wygląda na to, że masz tak zwany zimny start funkcji.

Gdy Twoja funkcja nie była wykonywana od jakiegoś czasu, Cloud Functions przełącza ją w tryb, który zużywa mniej zasobów. Następnie po ponownym uruchomieniu funkcji przywraca środowisko z tego trybu. Czas potrzebny do przywrócenia składa się ze stałego kosztu (np. Przywrócenie kontenera) i częściowego kosztu zmiennego (np. Jeśli używasz wielu modułów węzłów, może to zająć więcej czasu).

Nieustannie monitorujemy wydajność tych operacji, aby zapewnić najlepsze połączenie doświadczenia programisty i wykorzystania zasobów. Więc spodziewaj się, że te czasy poprawią się z czasem.

Dobra wiadomość jest taka, że ​​powinieneś tego doświadczyć tylko podczas programowania. Gdy funkcje są często uruchamiane w środowisku produkcyjnym, istnieje prawdopodobieństwo, że prawie nigdy nie osiągną zimnego startu.

Frank van Puffelen
źródło
3
Uwaga moderatora : wszystkie komentarze niezwiązane z tematem w tym poście zostały usunięte. Prosimy o używanie komentarzy, aby poprosić o wyjaśnienia lub zasugerować tylko ulepszenia. Jeśli masz podobne, ale inne pytanie, zadaj nowe pytanie i dołącz link do tego, aby uzyskać kontekst.
Bhargav Rao
55

Aktualizacja maj 2020 Dzięki za komentarz maganap - w Węźle 10+ FUNCTION_NAMEjest zastępowane K_SERVICE( FUNCTION_TARGETto sama funkcja, a nie jej nazwa, zastępującENTRY_POINT ). Poniższe przykłady kodu zostały zaktualizowane poniżej.

Więcej informacji na https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

Aktualizacja - wygląda na to, że wiele z tych problemów można rozwiązać za pomocą ukrytej zmiennej, process.env.FUNCTION_NAMEjak widać tutaj: https://github.com/firebase/functions-samples/issues/170#issuecomment-323375462

Aktualizuj za pomocą kodu - na przykład, jeśli masz następujący plik indeksu:

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

Następnie wszystkie pliki zostaną załadowane, a wszystkie wymagania tych plików również zostaną załadowane, co spowoduje duże obciążenie i zanieczyszczenie globalnego zakresu wszystkich funkcji.

Zamiast tego oddzielaj elementy dołączone jako:

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

Spowoduje to załadowanie wymaganych plików tylko wtedy, gdy ta funkcja zostanie specjalnie wywołana; pozwalając na utrzymanie globalnego zasięgu znacznie czystszym, co powinno skutkować szybszymi zimnymi butami.


Powinno to pozwolić na znacznie lepsze rozwiązanie niż to, co zrobiłem poniżej (chociaż wyjaśnienie poniżej nadal jest aktualne).


Oryginalna odpowiedź

Wygląda na to, że wymaganie plików i ogólna inicjalizacja zachodząca w zakresie globalnym jest ogromną przyczyną spowolnienia podczas zimnego rozruchu.

W miarę jak projekt uzyskuje więcej funkcji, zasięg globalny jest coraz bardziej zanieczyszczany, co pogarsza problem - szczególnie jeśli zakres funkcji obejmuje oddzielne pliki (na przykład przy użyciu Object.assign(exports, require('./more-functions.js'));w pliku index.js.

Udało mi się zaobserwować ogromny wzrost wydajności zimnego rozruchu, przenosząc wszystkie moje wymagania do metody init, jak poniżej, a następnie wywołując ją jako pierwszą linię w dowolnej definicji funkcji dla tego pliku. Na przykład:

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

Widziałem poprawę od około 7-8s do 2-3s podczas stosowania tej techniki do projektu z ~ 30 funkcjami w 8 plikach. Wydaje się również, że funkcje muszą być rzadziej uruchamiane na zimno (prawdopodobnie z powodu mniejszego zużycia pamięci?)

Niestety, nadal sprawia to, że funkcje HTTP są ledwo użyteczne do użytku produkcyjnego dla użytkownika.

Mając nadzieję, że zespół Firebase ma w przyszłości pewne plany, które pozwolą na odpowiednie określenie zakresu funkcji, tak aby tylko odpowiednie moduły musiały być ładowane dla każdej funkcji.

Tyris
źródło
Hej Tyris, mam ten sam problem z operacją czasu, próbuję wdrożyć twoje rozwiązanie. próbuję tylko zrozumieć, kto wywołuje funkcję init i kiedy?
Manspof
Cześć @AdirZoari, moje wyjaśnienie używania init () i tak dalej nie jest prawdopodobnie najlepszą praktyką; jego wartość polega tylko na zademonstrowaniu moich ustaleń dotyczących podstawowego problemu. Byłoby znacznie lepiej, gdybyś spojrzał na ukrytą zmienną process.env.FUNCTION_NAMEi użył jej do warunkowego dołączenia plików wymaganych dla tej funkcji. Komentarz na github.com/firebase/functions-samples/issues/… daje naprawdę dobry opis tego działania! Zapewnia, że ​​zakres globalny nie jest zanieczyszczony metodami i zawiera nieistotne funkcje.
Tyris
1
Cześć @davidverweij, nie sądzę, aby to pomogło, jeśli chodzi o możliwość działania twoich funkcji dwukrotnie lub równolegle. Funkcje automatycznie skalują się zgodnie z potrzebami, dzięki czemu wiele funkcji (ta sama lub różne) może działać równolegle w dowolnym momencie. Oznacza to, że musisz wziąć pod uwagę bezpieczeństwo danych i rozważyć użycie transakcji. Zapoznaj się
Tyris
1
Uwaga FUNCTIONS_NAMEdotyczy tylko węzłów 6 i 8, jak wyjaśniono tutaj: cloud.google.com/functions/docs/… . Węzeł 10 powinien użyćFUNCTION_TARGET
maganap
1
Dzięki za aktualizację @maganap, wygląda na to, że powinien być używany K_SERVICEzgodnie z doco na cloud.google.com/functions/docs/migrating/ ... - Zaktualizowałem moją odpowiedź.
Tyris
7

Mam podobne problemy z funkcjami chmury Firestore. Największą jest wydajność. Szczególnie w przypadku startupów na wczesnym etapie, kiedy nie możesz pozwolić swoim klientom na oglądanie „powolnych” aplikacji. Prosta funkcja generowania dokumentacji np. Daje to:

- Wykonanie funkcji trwało 9522 ms, zakończone kodem statusu: 200

Potem: miałem prostą stronę z warunkami. W przypadku funkcji chmurowych wykonanie z powodu zimnego startu nawet czasami zajmowałoby 10-15 sekund. Następnie przeniosłem go do aplikacji node.js hostowanej w kontenerze appengine. Czas spadł do 2-3 sekund.

Porównywałem wiele funkcji mongodb z firestore i czasami też zastanawiam się, czy na tym wczesnym etapie mojego produktu powinienem również przenieść się do innej bazy danych. Największą reklamą, jaką miałem w Firestore, była funkcja wyzwalacza onCreate, onUpdate obiektów dokumentów.

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

Zasadniczo, jeśli istnieją statyczne części witryny, które można przenieść do środowiska Appengine, być może nie jest to zły pomysł.

Sudhakar R
źródło
1
Uważam, że Funkcje Firebase nie nadają się do celu, jeśli chodzi o wyświetlanie dynamicznych treści dla użytkowników. Z kilku funkcji HTTP korzystamy rzadko do takich rzeczy, jak resetowanie hasła, ale ogólnie, jeśli masz zawartość dynamiczną, udostępniaj ją w innym miejscu jako aplikację ekspresową (lub użyj języka różnic).
Tyris
2

Zrobiłem też te rzeczy, co poprawia wydajność po rozgrzaniu funkcji, ale zimny start mnie zabija. Jednym z innych problemów, które napotkałem, są cors, ponieważ wykonanie zadania wymaga dwóch podróży do funkcji chmury. Jestem jednak pewien, że mogę to naprawić.

Jeśli masz aplikację we wczesnej fazie (demo), kiedy nie jest często używana, wydajność nie będzie świetna. Należy to wziąć pod uwagę, ponieważ pierwsi użytkownicy z wczesnymi produktami muszą wyglądać jak najlepiej przed potencjalnymi klientami / inwestorami. Uwielbialiśmy tę technologię, więc przenieśliśmy się ze starszych, wypróbowanych i sprawdzonych frameworków, ale nasza aplikacja wydaje się w tym momencie dość powolna. Następnie spróbuję kilku strategii rozgrzewki, aby wyglądało lepiej

Stan Świniarski
źródło
Testujemy zadanie crona, aby obudzić każdą funkcję. Może to podejście też Ci pomoże.
Jesús Fuentes,
hej @ JesúsFuentes Zastanawiałem się tylko, czy budzenie funkcji działa dla Ciebie. Brzmi jak szalone rozwiązanie: D
Alexandr Zavalii
1
Cześć @Alexandr, niestety nie mieliśmy jeszcze czasu, aby to zrobić, ale jest to na naszej liście priorytetów. Powinien jednak działać teoretycznie. Problem dotyczy funkcji onCall, które wymagają uruchomienia z aplikacji Firebase. Może dzwonisz do nich od klienta co X minut? Zobaczymy.
Jesús Fuentes
1
@Alexandr, czy możemy porozmawiać poza Stackoverflow? Możemy pomóc sobie nawzajem, stosując nowe podejście.
Jesús Fuentes
1
@Alexandr Nie testowaliśmy jeszcze tego obejścia „wybudzania”, ale wdrożyliśmy już nasze funkcje w europie-zachodzie1. Wciąż niedopuszczalne czasy.
Jesús Fuentes
0

AKTUALIZACJA / EDYCJA: nowa składnia i aktualizacje w MAJ 2020

Właśnie opublikowałem pakiet o nazwie better-firebase-functions, który automatycznie przeszukuje katalog funkcji i prawidłowo zagnieżdża wszystkie znalezione funkcje w obiekcie eksportu, jednocześnie izolując funkcje od siebie, aby poprawić wydajność zimnego rozruchu.

Jeśli ładujesz z opóźnieniem i buforujesz tylko zależności, których potrzebujesz dla każdej funkcji w zakresie modułu, okaże się, że jest to najprostszy i najłatwiejszy sposób na utrzymanie optymalnej wydajności funkcji w szybko rozwijającym się projekcie.

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})
George43g
źródło
ciekawe .. gdzie mogę zobaczyć repozytorium „Better-Firebase-functions”?
JerryGoyal
1
github.com/gramstr/better-firebase-functions - sprawdź to i daj mi znać, co myślisz! Zapraszam również do udziału :)
George43g