Google Cloud Functions 2nd gen

Google Cloud Functions 2nd gen

Google vient de sortir en beta la seconde génération des Google Cloud Functions. Pour ceux qui ne connaissent pas encore les Google Cloud Functions vous pouvez lire mes articles J’ai testé Java Google Cloud Functions et Quarkus et les Google Cloud Functions.

Cette seconde génération apporte :

  • Un temps de traitement maximal plus important : 60mn au lieu de 10mn.
  • Des instances jusqu’à 16Go/4vCPU au lieu de 8Go/4vCPU.
  • La possibilité d’avoir des instances toujours disponibles.
  • Une meilleure gestion de la concurrence : jusqu’à 1000 appels concurrents par instance.
  • Le support des CloudEvents via EventArc : plus de 90 événements disponibles.

Toutes les nouveautés des Cloud Functions gen2 sont disponibles ici.

Cerise sur le gâteau, Quarkus est déjà prêt ! Ayant eu accès à la version alpha privée, j’ai déjà rendu l’extension Quarkus compatible ;).

Dans cet article, je vais revenir sur les deux points qui me semblent les plus intéressants : la concurrence et le support des CloudEvents.

Déploiement et premier appel

Pour commencer, déployons la même fonction dans le runtime 1st gen et 2nd gen. Je vais utiliser la fonction HTTP du test d’intégration de l’extension Quarkus pour Google Cloud Functions trouvable ici.

Première chose à faire, packager la fonction via mvn clean package. Quarkus va générer un uber jar dans le répertoire target/deployment que nous allons ensuite utiliser pour déployer notre fonction.

Pour déployer la fonction dans le runtime 1st gen :

gcloud functions deploy quarkus-example-http-v1 \
    --entry-point=io.quarkus.gcp.functions.QuarkusHttpFunction \
    --runtime=java11 --trigger-http --source=target/deployment

Le build est effectué via Cloud Build et prend environ 22s. Après déploiement, j’effectue un premier appel de la fonction via curl puis j’accède à ses logs pour voir le temps de lancement de la fonction, et le temps du premier appel.

D      quarkus-example-http-v1  47b2zh3ew2od  2022-03-22 17:35:03.446  Function execution took 277 ms, finished with status code: 200
D      quarkus-example-http-v1  47b2zh3ew2od  2022-03-22 17:35:03.169  Function execution started
       quarkus-example-http-v1                2022-03-22 17:31:38.441  2022-03-22 17:31:38.441:INFO:oejs.Server:main: Started @1476ms
[...]
I      quarkus-example-http-v1                2022-03-22 17:31:38.339  Installed features: [cdi, google-cloud-functions]
I      quarkus-example-http-v1                2022-03-22 17:31:38.339  Profile prod activated.
I      quarkus-example-http-v1                2022-03-22 17:31:38.338  quarkus-integration-test-google-cloud-functions 999-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 0.690s.
I      quarkus-example-http-v1                2022-03-22 17:31:38.266  JBoss Threads version 3.4.2.Final
       quarkus-example-http-v1                2022-03-22 17:31:37.431  2022-03-22 17:31:37.430:INFO::main: Logging initialized @457ms to org.eclipse.jetty.util.log.StdErrLog
       quarkus-example-http-v1                2022-03-22 17:31:36.969  Picked up JAVA_TOOL_OPTIONS: -XX:MaxRAM=256m -XX:MaxRAMPercentage=70

On constate que la fonction a démarré en 1,5s dont 0,7s pour le démarrage de Quarkus. Le premier appel a pris 277ms.

Faisons de même pour le runtime 2nd gen dont nous pouvons déployer la même fonction avec :

gcloud beta functions deploy quarkus-example-http-v2  \
    --entry-point=io.quarkus.gcp.functions.QuarkusHttpFunction \
    --runtime=java11 --trigger-http --source=target/deployment --gen2

Le build est effectué via Cloud Build et prend en viron 25s. Après déploiement, j’effectue un premier appel de la fonction via curl, et là je constate tout de suite que l’appel est très très long! J’accède à ses logs pour voir le temps de lancement de la fonction et le temps du premier appel.

I      quarkus-example-http-v2  2022-03-22 17:38:44.464
       quarkus-example-http-v2  2022-03-22 17:38:43.041  2022-03-22 17:38:43.041:INFO:oejs.Server:main: Started @14069ms
[...]
I      quarkus-example-http-v2  2022-03-22 17:38:41.943  Installed features: [cdi, google-cloud-functions]
I      quarkus-example-http-v2  2022-03-22 17:38:41.943  Profile prod activated.
I      quarkus-example-http-v2  2022-03-22 17:38:41.942  quarkus-integration-test-google-cloud-functions 999-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 7.283s.
I      quarkus-example-http-v2  2022-03-22 17:38:41.343  JBoss Threads version 3.4.2.Final
----> OTHER STARTING LOGS <-------

Plusieurs constatations : le temps de démarrage est beaucoup plus long, environ 14s dont 7s pour Quarkus, on retrouve le même rapport démarrage runtime vs Quarkus mais en fois 10! De plus, l'appel curl effectué juste le déploiement déclenche le démarrage d'une autre fonction. Les appels successifs seront beaucoup plus rapides.

Il y a ici un comportement très différents entre les génération 1 et 2, je vais contacter les équipes de Google sur le sujet pour investigation.

Une meilleure concurrence

Pour comparer la gestion de la concurrence, je vais simuler une charge importante avec l'outil wrk sur les deux runtimes.

Sur chaque runtime, je réalise deux tests successifs, un sur 1mn avec 10 threads pour 100 connexions, et un autre sur 5mn avec 20 threads pour 200 connexions :

wrk -c 100 -t 10 -d 60 --latency https://my-function-host/quarkus-example-http-v1
wrk -c 200 -t 20 -d 300 --latency https://my-function-host/quarkus-example-http-v1

Voici les résultats pour le runtime 1st gen :

  10 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   144.47ms  111.63ms   2.00s    97.54%
    Req/Sec    69.62     17.30   101.00     64.76%
  Latency Distribution
     50%  123.09ms
     75%  129.64ms
     90%  174.37ms
     99%  601.22ms
  40755 requests in 1.00m, 16.36MB read
  Socket errors: connect 0, read 0, write 0, timeout 175
Requests/sec:    678.27
Transfer/sec:    278.89KB


  20 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   126.24ms   31.54ms   1.92s    93.47%
    Req/Sec    79.79     13.07   101.00     76.19%
  Latency Distribution
     50%  118.99ms
     75%  122.78ms
     90%  138.27ms
     99%  224.09ms
  477829 requests in 5.00m, 191.86MB read
  Socket errors: connect 0, read 0, write 0, timeout 30
  Non-2xx or 3xx responses: 20
Requests/sec:   1592.29
Transfer/sec:    654.69KB

Et voici les résultats pour le runtime 2nd gen :

  10 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   138.16ms   63.56ms   1.95s    95.26%
    Req/Sec    65.04     23.10   101.00     63.66%
  Latency Distribution
     50%  119.94ms
     75%  140.14ms
     90%  190.22ms
     99%  230.52ms
  27713 requests in 1.00m, 8.72MB read
  Socket errors: connect 0, read 0, write 0, timeout 169
  Non-2xx or 3xx responses: 64
Requests/sec:    461.20
Transfer/sec:    148.58KB


  20 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   125.02ms   30.51ms   1.98s    92.59%
    Req/Sec    79.25     14.82   101.00     71.28%
  Latency Distribution
     50%  117.89ms
     75%  120.57ms
     90%  136.77ms
     99%  210.77ms
  474727 requests in 5.00m, 148.95MB read
  Socket errors: connect 0, read 0, write 0, timeout 79
  Non-2xx or 3xx responses: 38
Requests/sec:   1581.91
Transfer/sec:    508.26KB

Les performances moyennes entre les deux sont similaires, avec un temps moyen légèrement inférieur pour la 2nd gen. Quand on regarde en détail la latence à 99% (tail latency), on remarque une différence plus marquée pour la 2nd gen qui a une latence bien plus faible, surtout lors du premier test de charge (230ms versus 601ms). On voit bien l’intérêt d'une concurrence accrue pour les fonctions en 2nd gen : plus de requête traitée par instance, égal moins de démarrage de fonction, et donc moins de cold starts.

On peut valider ça en regardant via la console Google Cloud le nombre d'instances démarrées, et on constate qu'il y a environ deux fois moins d'instances démarrées en 2nd gen qu'en 1st gen (65 à 70 instances versus 140 à 200 instances).

1st gen - Nb instances
2nd gen - Nb instances

CloudEvents

Une des fonctionnalités les plus excitantes de la 2nd gen est la possibilité de créer des fonctions de type Cloud Events. Ce sont des fonctions événementielles qui, au lieu de recevoir un événement dans un format propriétaire au cloud Google, vont recevoir un événement au format standard tel que décrit dans la spécification Cloud Events.

Voici un exemple de cloud function recevant un événement de type Storage et utilisant l’événement propriétaire Google Cloud; c'est une fonction de type background qui utilise un objet d'évènement propriétaire StorageEvent :

public class BackgroundFunctionStorageTest implements BackgroundFunction {

    @Override
    public void accept(StorageEvent event, Context context) throws Exception {
        System.out.println("Receive event on file: " + event.name);
    }

    public static class StorageEvent {
        public String name;
    }
}

Pour déployer cette fonction et la faire écouter sur un événement sur le bucket quarkus-hello nous pouvons utiliser la commande suivante.

gcloud functions deploy quarkus-example-storage \
    --entry-point=com.example.BackgroundFunctionStorageTest \
    --trigger-resource quarkus-hello --trigger-event google.storage.object.finalize \
    --runtime=java11 --source=target/deployment

Voici un exemple de cloud function recevant un événement standard de type CloudEvents; elle utilise la librairie Java CloudEvents qui fournit l'objet CloudEvent :

public class CloudEventStorageTest implements CloudEventsFunction {
 
    @Override
    public void accept(CloudEvent cloudEvent) throws Exception {
        System.out.println("Receive event Id: " + cloudEvent.getId());
        System.out.println("Receive event Subject: " + cloudEvent.getSubject());
        System.out.println("Receive event Type: " + cloudEvent.getType());
        System.out.println("Receive event Data: " + new String(cloudEvent.getData().toBytes()));
    }
}

C'est au déploiement de cette fonction que nous allons préciser que le déclenchement se fera sur un événement de type Storage en précisant le bucket.

gcloud beta functions deploy quarkus-example-cloud-event --gen2 \
  --entry-point=com.example.CloudEventsFunctionStoragetTest \
  --runtime=java11 --trigger-bucket=example-cloud-event --source=target/deployment

Le contenu de l'événement Storage sera dans l'attribut data de l'objet CloudEvent.

Conclusion

Même si la 2nd gen est encore en preview, l'avantage offert en termes de performance et de cold start vaut à lui seul le coup de commencer à l'utiliser (même s'il reste à régler le problème du premier démarrage de fonction qui prend beaucoup de temps).

De plus, le support du standard CloudEvents permet d'écrire des fonctions moins dépendantes de Google Cloud, et surtout d'utiliser un format qui est supporté sur d'autres clouds et dans d'autres technologies (broker Kafka, client HTTP, ...).

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.