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<StorageEvent> {
@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, .. .).