Google Cloud Functions 2nd gen
Google has just released in beta the second generation of Google Cloud Functions. For those who are not yet familiar with Google Cloud Functions you can read my article Quarkus and Google Cloud Functions.
This second generation brings:
- A longer maximum processing time: 60mn instead of 10mn.
- Instances up to 16GB/4vCPU instead of 8GB/4vCPU.
- The ability to have instances always available.
- Better concurrency management: up to 1000 concurrent calls per instance.
- CloudEvents support via EventArc: more than 90 events available.
All the new features of Cloud Functions gen2 are available here.
Icing on the cake, Quarkus is already ready for them! I have had access to the private alpha version, so I already made the Quarkus extension compatible ;).
In this article, I will talk about to the two points that seem to me the most interesting ones: better concurrency and support for CloudEvents.
Deployment and first call
First, let’s deploy the same function in the 1st gen and 2nd gen runtimes. I will use the HTTP function from the Quarkus extension integration test for Google Cloud Functions available here.
First thing to do, package the function via mvn clean package
. Quarkus will generate an uber jar in the target/deployment
directory which we will then use to deploy our function.
To deploy the function in the 1st gen runtime:
gcloud functions deploy quarkus-example-http-v1 \ --entry-point=io.quarkus.gcp.functions.QuarkusHttpFunction \ --runtime=java11 --trigger-http --source=target/deployment
The build is done via Cloud Build and takes about 22s. After deployment, I make a first call to the function via curl then I access its logs to see the function startup time, and the time of the first call.
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
We note that the function started in 1.5s including 0.7s for starting Quarkus. The first call took 277ms.
Let’s do the same for the 2nd gen runtime for which we can deploy the same function with:
gcloud beta functions deploy quarkus-example-http-v2 \ --entry-point=io.quarkus.gcp.functions.QuarkusHttpFunction \ --runtime=java11 --trigger-http --source=target/deployment --gen2
The build is done via Cloud Build and takes about 25s. After deployment, I make a first call to the function via curl, and then I immediately notice that the call is very very long! I access its logs to see the function startup time and the time of the first call.
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 <-------
Several observations: the start-up time is much longer, around 14s including 7s for Quarkus, we find the same ratio start-up runtime vs Quarkus one but 10 times more! Also, the curl call just after the deployment triggers another function startup. Successive calls will be much faster.
There is a very different behavior here between generations 1 and 2, I will contact the Google team on the subject for investigation.
Better concurrency
To compare the concurrency management, I will simulate a heavy load with the tool wrk on both runtimes.
On each runtime, I perform two successive tests, one over 1 minute with 10 threads for 100 connections, and another over 5 minutes with 20 threads for 200 connections:
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
Here are the results for the 1st gen runtime:
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
And here are the results for the 2nd gen runtime:
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
The average performance is similar for the two runtimes, with a slightly lower average time for the 2nd gen. When we look in detail at the 99% latency (tail latency), we notice a more marked difference for the 2nd gen which has a much lower latency, especially during the first load test (230ms versus 601ms). We can clearly see the interest of increased concurrency for 2nd gen functions: more requests processed per instance, equals fewer function startups, and therefore fewer cold starts.
We can validate this by looking at the number of instances started via the Google Cloud console, and we see that there are about half as many instances started in 2nd gen as in 1st gen (65 to 70 instances versus 140 to 200 instances).
CloudEvents
One of the most exciting 2nd gen functionality is the ability to create functions of Cloud Events type. These are event functions that, instead of receiving an event in a proprietary Google Cloud format, will receive one in a standard format as described in the Cloud Events specification.
Here is an example of a cloud function receiving an event of type Storage and using the proprietary Google Cloud event; it's a background function that uses a proprietary StorageEvent
event object:
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; } }
To deploy this function and make it listen on an event on the quarkus-hello bucket we can use the following command:
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
Here is an example of a cloud function receiving a standard event of type CloudEvents; it uses the Java CloudEvents library which provides the CloudEvent
object:
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())); } }
It is at deployment time of this function that we will specify that the trigger will be on a Storage type event by specifying the 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
The content of the Storage event will be in the data
attribute of the CloudEvent object.
Conclusion
Even if the 2nd gen is still in preview, the advantage offered in terms of performance and cold start alone makes it worth starting to use it (even if it remains to solve the issue of the first function startup which take a lot of time).
Moreover, support for the CloudEvents standard makes it possible to write functions that are less dependent on Google Cloud, and above all to use a format that is supported on other clouds and in other technologies (Kafka broker, HTTP client, .. .).