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).
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, ...).