Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Have your Serverless Kotlin Functions and Eat T...

Have your Serverless Kotlin Functions and Eat Them Too

It’s a sad reality that JVM functions have poor cold-start performance on serverless platforms, like AWS Lambda. You may have been tempted to compile your jar to a native image, or to pay extra to keep your functions warm. You may have decided that serverless is only suited to asynchronous tasks without latency requirements. But it doesn’t have to be this way.

You can have your Kotlin Functions and you can eat them too. You can run an entire API service; but only if you design it right. In this talk, I’m going to teach you how to minimize your function’s init phase, abandon your heavyweight dependencies, and even eliminate the need for reflection. When we’re done, you won’t have to settle for other languages.

Avatar for Andrew O'Hara

Andrew O'Hara

March 08, 2024
Tweet

Other Decks in Technology

Transcript

  1. Getting Started A Few Things to Note Code and materials

    at andrewohara.dev/cake Platform agnostic, but focused on AWS Lambda Raw data available Tutorials linked throughout 2
  2. Serverless Lots to Love Pricing scales down to zero 01

    React to variety of events 02 No servers to manage, instant scalability 03 3
  3. Serverless What’s a Cold Start? t+3m Warm Invoke 25 ms

    t+6m Warm Invoke 35 ms t+1m Warm Invoke 30 ms t+5m Cold Invoke 4 500 ms t0 Cold Invoke 4 000 ms t+2m Warm Invoke 28 ms Warm Invoke 40 ms 4
  4. Serverless Not just Workers HTTP API Synchronous Response time CRITICAL!

    Worker Asynchronous Overall throughput important • Events • Scheduled Tasks • API Gateway Target 5
  5. Battle of the Stacks Serverless Support Supported ✓ No support

    ✗ out-of-date ✗ Custom runtime only ✗ Supported ✓ Supported Supported ✓ ✓ No support ✗ 8
  6. Battle of the Stacks Sample Application 23 MB JAR 28

    MB JAR 21 MB JAR 31 MB JAR Posts API POST /posts GET /posts GET /posts/{post_id} DELETE /posts/{post_id} AWS DynamoDB SDK Jackson JSON 2 GB RAM on AWS Lambda 9
  7. On Deploy SnapStart Restore Snapshot 1 Cold Start 2 Save

    Snapshot On Invoke 1 Restore Snapshot 2 Invoke Function 11
  8. # sam.yaml . . . ApiHandler: Type: AWS::Serverless::Function Properties: Timeout:

    10 Handler: dev.aohara.posts.LambdaHandler Architectures: [x86_64] Runtime: java21 . . . AutoPublishAlias: SnapStart SnapStart: ApplyOn: PublishedVersions . . . SnapStart Configuration 12 https://docs.aws.amazon.com/lambda/latest/dg/snapstart-activate.html
  9. SnapStart Limitations AWS Lambda Java 11 and above Most AWS

    regions Elastic File System not supported disk limit of 512 MB Singular state Connections AWS credentials CRaC can help Exclusivity Storage Restore Performance X86_64 No Provisioned Concurrency https://bell-sw.com/blog/how-to-use-crac-with-java-applications/ 14
  10. Graal VM Compiled Java Challenges • Custom runtime • Reflection

    • Library support • Native toolchain Advantages • Faster startup • Smaller runtime • Cloud agnostic 15
  11. Graal VM Support Spring AOT plugin compiles to graal binary

    01 Spring AOT doesn’t support serverless 02 https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html 16
  12. Graal VM Support Gradle plugin handles graal build 01 Supports

    dockerized build 02 https://quarkus.io/guides/aws-lambda#deploy-to-aws-lambda-custom-native-runtime 17
  13. Graal VM Support Gradle plugin handles graal build 01 Supports

    dockerized build 02 Only supports legacy REST API Gateway on AWS 03 https://guides.micronaut.io/latest/micronaut-creating-first-graal-app.html 18
  14. Graal VM Support Instructions for dockerized build 01 Core library

    uses no reflection 03 Custom runtime for AWS Lambda 02 https://www.http4k.org/guide/tutorials/going_native_with_graal_on_aws_lambda/ 19
  15. Battle of the Stacks Must go Deeper SnapStart • Exclusive

    with some blockers • Mediocre (650-800 ms) Graal VM • Difficult with many blockers • Decent (450-650 ms) 21
  16. Optimize Strategies Less Init Minimize computation and external calls Smaller

    Artifact Fewer and lighter dependencies Less Reflection Prefer static mapping or code generation 22
  17. Perfect for Serverless Less Init Only does exactly what you

    tell it to do Smaller Artifact Core module only ~ 1 MB Less Reflection Zero in core module 23
  18. Optimize Migration Path 2 300 ms Start ? Optimize AWS

    SDK ? Lighter AWS SDK ? Optimize JSON ? Minimal Logger 25
  19. Optimize V2 AWS SDK Start 0 Bloatware 1 Credentials 3

    Http Client 2 // build.gradle.kts dependencies { implementation("software.amazon.awssdk:dynamodb-enhanced:2.24.0") . . . } https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/lambda-optimize-starttime.html 26
  20. Optimize V2 AWS SDK Start 0 Bloatware 1 Credentials 3

    Http Client 2 // build.gradle.kts dependencies { implementation("software.amazon.awssdk:dynamodb-enhanced:2.24.0") { exclude("software.amazon.awssdk", "apache-client") exclude("software.amazon.awssdk", "netty-nio-client") } . . . } 27
  21. Optimize V2 AWS SDK Start 0 Bloatware 1 Credentials 3

    Http Client 2 // PostsRepo.kt val dynamoDb = DynamoDbClient .builder() .httpClient(UrlConnectionHttpClient.create()) .build() . . . // build.gradle.kts dependencies { implementation("software.amazon.awssdk:url-connection-client:2.24.0") . . . } 28
  22. Optimize V2 AWS SDK Start 0 Bloatware 1 Credentials 3

    Http Client 2 // PostsRepo.kt val dynamoDb = DynamoDbClient .builder() .httpClient(UrlConnectionHttpClient.create()) .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) .build() . . . 29
  23. 2 300 ms 21 MB JAR Start 2 151 ms

    16 MB JAR Optimize AWS SDK ? Lighter AWS SDK ? Optimize JSON ? Minimal Logger Optimize V2 AWS SDK 30 ? Graal VM
  24. // PostsRepo.kt class PostsRepo(dynamoDb: DynamoDb, tableName: TableName) { private val

    table = dynamoDb.tableMapper<Post, String, Unit>( tableName = tableName, hashKeyAttribute = Attribute.string().required("id"), autoMarshalling = Jackson ) ... } // build.gradle.kts dependencies { . . . implementation("org.http4k:http4k-connect-amazon-dynamodb:5.6.11.0") } Optimize Lighter AWS SDK 31
  25. // PostsApp.kt fun createApp(env: Map<String, String>): HttpHandler { val posts

    = createRepo( dynamoDb = DynamoDb.Http(http = Java8HttpClient()), tableName = env["TABLE_NAME"] ) return routes( ... ) } Optimize Lighter AWS SDK 32
  26. Optimize Lighter AWS SDK 2 300 ms 21 MB JAR

    Start 2 151 ms 16 MB JAR Optimize AWS SDK 1 345 ms 12 MB JAR Lighter AWS SDK ? Optimize JSON ? Minimal Logger 33 ? Graal VM
  27. Optimize JSON Connect already has reflectionless JSON 03 Jackson +

    kotlin-reflect > 5 MB 01 Class Inspection eats into startup 02 34
  28. Optimize JSON with Kotshi Builds JSON adapters ahead-of-time 03 Kotshi

    is a plugin for Moshi 01 Uses KSP or KAPT for codegen 02 https://www.http4k.org/guide/howto/make_json_faster/#kotshi 35
  29. // build.gradle.kts dependencies { implementation("se.ansman.kotshi:api:2.15.0") ksp("se.ansman.kotshi:compiler:2.15.0") // generate adapters .

    . . } // Post.kt @JsonSerializable data class Post(val id: String, val title: String, val content: String) @JsonSerializable data class PostData(val title: String, val content: String) Optimize JSON 36 https://www.http4k.org/guide/howto/make_json_faster/#kotshi
  30. 37 Optimize JSON // PostsJson.kt @KotshiJsonAdapterFactory object PostsJsonAdapterFactory : JsonAdapter.Factory

    by KotshiPostsJsonAdapterFactory val postsJson = Moshi.Builder() .add(PostsJsonAdapterFactory) ... .withStandardMappings() .done() .let(::ConfigurableMoshi) https://www.http4k.org/guide/howto/make_json_faster/#kotshi
  31. 38 Optimize JSON // PostsController.kt fun Post.toJson(): String = postsJson.asFormatString(this)

    https://www.http4k.org/guide/howto/make_json_faster/#kotshi // PostsRepo.kt class PostsRepo(dynamoDb: DynamoDb, tableName: TableName) { private val table = dynamoDb.tableMapper<Post, String, Unit>( tableName = tableName, hashKeyAttribute = Attribute.string().required("id"), autoMarshalling = postsJson ) ... }
  32. Optimize JSON 2 300 ms 21 MB JAR Start 2

    151 ms 16 MB JAR Optimize AWS SDK 1 345 ms 12 MB JAR Lighter AWS SDK 1 146 ms 7 MB JAR Optimize JSON ? Minimal Logger 39 ? Graal VM
  33. Optimize Logger Log4j 2 is over 2 MB 01 Advanced

    logger for a simple application 02 40
  34. // build.gradle.kts dependencies { . . . implementation("org.slf4j:slf4j-simple:2.0.11") } #

    src/main/resources/simplelogger.properties org.slf4j.simpleLogger.defaultLogLevel=info org.slf4j.simpleLogger.logFile=System.out Optimize Logger 41
  35. Optimize Logger 2 300 ms 21 MB JAR Start 2151

    ms 16 MB JAR Optimize AWS SDK 1345 ms 12 MB JAR Lighter AWS SDK 1146 ms 7 MB JAR Optimize JSON 603 ms 5 MB JAR Minimal Logger 42 ? Graal VM
  36. Optimize Graal VM // build.gradle.kts dependencies { implementation("org.http4k:http4k-serverless-lambda-runtime:5.13.6.0") . .

    . } // PostsApp.kt fun createApp(env: Map<String, String>) = routes( . . . ) class LambdaHandler : ApiGatewayV2LambdaFunction(::createApp) fun main() { ApiGatewayV2FnLoader(::createApp) .asServer(AwsLambdaRuntime()) .start() } https://www.http4k.org/guide/tutorials/going_native_with_graal_on_aws_lambda/ 43
  37. 44 Optimize Graal VM # sam.yaml . . . ApiHandler:

    Type: AWS::Serverless::Function Properties: Timeout: 10 Architectures: [x86_64] CodeUri: build/app.zip Handler: unused Runtime: provided.al2 . . . . . . https://www.http4k.org/guide/tutorials/going_native_with_graal_on_aws_lambda/
  38. Optimize Graal VM 2 300 ms 21 MB JAR Start

    2151 ms 16 MB JAR Optimize AWS SDK 1345 ms 12 MB JAR Lighter AWS SDK 1146 ms 7 MB JAR Optimize JSON 603 ms 5 MB JAR Minimal Logger 45 268 ms 21 MB ZIP Graal VM
  39. Have your Cake And Eat it Too SnapStart • Easily

    provide decent boost • Few limitations Graal VM • Typically superior to Snapstart • Some challenges Optimize • Powerful on its own • Synergizes with other solutions ✓ ✓ ✗ 47