Google Cloud Functions 2nd gen
Tags: gcp, java, serverless
Published: 2022-04-04 08:19:11

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](https://www.loicmathieu.fr/wordpress/informatique/quarkus-et-les-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](https://cloud.google.com/functions/docs/2nd-gen/overview).

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](https://github.com/quarkusio/quarkus/tree/main/integration-tests/google-cloud- functions).

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](https://github.com/wg/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).

<img src="https://loicmathieu.fr/wordpress/wp-content/uploads/cloud-function-gen1-instance.png" alt="" width="618" height="343" class="size-full wp-image-1445" /> 1st gen - Nb instances

<img src="https://loicmathieu.fr/wordpress/wp-content/uploads/cloud-function-gen2-instances.png" alt="" width="601" height="338" class="size-full wp-image-1446" /> 2nd gen - Nb 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](https://github.com/cloudevents/spec).

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:

```java
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:

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