ariel-tchougang / aws-lambda-java-optimization-workshop

Java optimization on AWS Lambda Workshop

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Demo Java optimization on AWS Lambda

Using Hexagonal (Ports & Adapters) Architecture & TDD

Technologies used:

  • Java 17
  • AWS Lambda
  • Amazon API Gateway
  • Spring boot 2.6.14
  • Micronaut 3.9.1
  • Quarkus 3.1.0.Final
  • Lombok
  • Maven
  • DynamoDB Local (docker container amazon/dynamodb-local:latest)
  • org.testcontainers
  • Junit 5
  • npm
  • jq (On Amazon Linux 2 run: 'sudo yum install jq' )
  • Artillery (On Amazon Linux 2 run: 'npm install -g artillery@latest' )
  • faker-js
  • AWS SAM CLI

About SAM CLI

The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API.

To use the SAM CLI, you need the following tools.

Content

This application is a user microservice exposing REST endpoints for:

  • Registering a new user,
  • Fetching existing user details

A simple Springboot application version (demo-springboot-app) is provided as starting point. A corresponding initial architecture could be the following:

Alt text

Code inside architecture

Alt text

General purpose and target architecture

The purpose of this project is to compare the performance of this application deployed as a AWS Lambda function with the following targets:

  • Springboot with serverless java container
  • Springboot with lambda function handler
  • Micronaut with lambda request handler
  • Plain Java with lambda handler

The target architecture is the following:

Alt text

Code inside architecture

Alt text

Initial configuration

For our AWS Lambda function, the default configuration is as following:

  • Memory size: 256 MB
  • Tiered compilation activated: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
  • Architecture: x86_64
  • Reserved concurrency: 100

How will we proceed for workshop

For each of the listed target, we will:

  • Build the application
  • Deploy on AWS Lambda
  • Assess initial performances
  • Apply Lambda Power Tuning Tool to find the right memory configuration
  • Redeploy with memory optimization
  • Assess performances
  • Apply Java Snapstart for AWS Lambda
  • Redeploy with Snapstart for AWS Lambda
  • Assess performances

Let's get started

Make all scripts executable

chmod 755 *.sh

Build and deploy the application on AWS Lambda

./deploy-all.sh AWS_REGION

Install Artillery, jq & faker-js

  • Artillery
echo "Installing Artillery"
npm install -g artillery@latest 

echo "Testing installation"
artillery dino
  • jq (On Amazon Linux 2)
sudo yum install jq
  • faker-js (only if needed, because generated node_modules resources are already provided in folder loadtest)
cd loadtest
npm init -y && npm install @faker-js/faker
cd ..

Deployment information

stack_name = "workshop-java-lambda-optimizations"
s3_prefix = "workshop-java-lambda-optimizations"

Lambda functions: 
    - workshop-springboot-serverless-java-container
    - workshop-springboot-lambda-function-handler
    - workshop-quarkus-lambda-request-handler
    - workshop-micronaut-lambda-request-handler
    - workshop-plain-java-lambda-request-handler

Assessing initial performances

Testing each lambda once to have a first feeling of performance

./test-app.sh TARGET_APP AWS_REGION
  • With TARGET_APP values in ['plain-java', 'serverless-java-container', 'spring', 'micronaut', 'quarkus']

Result will look like: Alt text

Run a load test benchmark with Artillery

Load test configuration files are stored in folder loadtest.

You can run a benchmark per target (may be in parallel bash windows - by default 60 seconds per benchmark)

./benchmark.sh TARGET_APP AWS_REGION
  • With TARGET_APP values in
    • plain-java
    • serverless-java-container
    • spring
    • micronaut
    • quarkus

OR

You can run your benchmarks all at once.

./run-all-benchmarks.sh AWS_REGION

Visualize your results

Then go to CloudWatch Logs Insights:

  • Select the appropriate log groups
    • /aws/lambda/workshop-springboot-serverless-java-container
    • /aws/lambda/workshop-springboot-lambda-function-handler
    • /aws/lambda/workshop-quarkus-lambda-request-handler
    • /aws/lambda/workshop-micronaut-lambda-request-handler
    • /aws/lambda/workshop-plain-java-lambda-request-handler
  • Collect your logs using this query:
  filter @type = "REPORT"
  | parse @log /\d+:\/aws\/lambda\/(?<function>.*)/
  | parse @message /Restore Duration: (?<restoreDuration>.*?) ms/
  | stats
count(*) as invocations,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 0) as p0,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 25) as p25,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 50) as p50,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 90) as p90,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 95) as p95,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 99) as p99,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 100) as p100
group by function, (ispresent(@initDuration) or ispresent(restoreDuration)) as coldstart
  | sort by coldstart desc

Result will look like: Alt text

Execute the state machine to find the right memory configuration. The execution payloads are in folder payloads. We'll be looking for the best memory size with an acceptable cost.

Execution payload template

{
  "lambdaARN": "REPLACE_WITH_TARGET_LAMBDA_ARN",
  "powerValues": [
    512,
    1024,
    1536,
    2048,
    2560,
    3072,
    4096
  ],
  "parallelInvocation": true,
  "num": 5,
  "payload": {
    "resource": "/{proxy+}",
    "path": "/REPLACE_WITH_ROOT_PATH/users",
    "httpMethod": "POST",
    "multiValueHeaders": {
      "content-type": [
        "application/json"
      ]
    },
    "pathParameters": {
      "proxy": "REPLACE_WITH_ROOT_PATH/users"
    },
    "requestContext": {
      "identity": {
        "userAgent": "curl/7.64.1"
      }
    },
    "body": "{\n \"firstName\": \"user\",\n \"lastName\": \"testing\",\n \"email\": \"user.testing@workshop.com\"}",
    "isBase64Encoded": false
  }
}

Results will look like:

  • For Lambda with Serverless Java Container Alt text

  • For Lambda with Spring Cloud Function Alt text

  • For Lambda with Micronaut Alt text

  • For Lambda with Plain java Alt text

Redeploy with memory optimization

Update template.yaml to reflect your optimal memory choices.

  • Global values
Globals:
  Function:
    Timeout: 30
    Runtime: java17
    # MemorySize: 512
  • For Serverless Java Container
  SpringbootServerlessJavaContainer:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .workshop-packages/demo-springboot-app-lambda-serverless-java-container-1.0.0-aws.jar
      FunctionName: workshop-springboot-serverless-java-container
      Handler: com.atn.digital.user.StreamLambdaHandler::handleRequest
      MemorySize: 3072 #SET NEW VALUE HERE
  • For Spring Cloud Function
  SpringbootLambdaFunctionHandler:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .workshop-packages/demo-springboot-app-lambda-function-handler-1.0.0-aws.jar
      FunctionName: workshop-springboot-lambda-function-handler
      Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
      MemorySize: 2048 #SET NEW VALUE HERE
  • For Micronaut
  MicronautLambdaRequestHandler:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .workshop-packages/demo-micronaut-app-lambda-request-handler-1.0.0-aws.jar
      FunctionName: workshop-micronaut-lambda-request-handler
      Handler: com.atn.digital.user.adapters.in.handler.UserRequestHandler::handleRequest
      MemorySize: 2560 #SET NEW VALUE HERE
  • For Plain java
  PlainJavaLambdaRequestHandler:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .workshop-packages/demo-plain-java-app-lambda-handler-1.0.0-aws.jar
      FunctionName: workshop-plain-java-lambda-request-handler
      Handler: com.atn.digital.user.adapters.in.handler.UserRequestHandler::handleRequest
      MemorySize: 2560 #SET NEW VALUE HERE
  • For Quarkus
    QuarkusLambdaRequestHandler:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .workshop-packages/demo-quarkus-app-lambda-request-handler.zip
      FunctionName: workshop-quarkus-lambda-request-handler
      Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
      MemorySize: 2048 #SET NEW VALUE HERE

Redeploy

  • If you changed something pertaining to your java code:
./deploy-all.sh UPLOAD_BUCKET AWS_REGION
  • If you didn't make any changes pertaining to your java code
./redeploy-all.sh AWS_REGION

Testing each lambda once to have a first feeling of performance

./test-app.sh TARGET_APP AWS_REGION

Result will look like: Alt text

Run a load test benchmark with Artillery

Load test configuration files are stored in folder loadtest.

  • Go to Cloudwatch
  • Delete all the workshop function log groups
  • Run you benchmark
./benchmark.sh TARGET_APP AWS_REGION

OR

./run-all-benchmarks.sh AWS_REGION

Visualize your results

  • Then go to CloudWatch Logs Insights
  • Select the appropriate log groups
  • Collect your logs using the same query

Result will look like: Alt text

Apply Java SnapStart for AWS Lambda

Update template.yaml to enable SnapStart.

  • Global values
Globals:
  Function:
    Timeout: 30
    Runtime: java17
    # MemorySize: 512
    SnapStart:
      ApplyOn: PublishedVersions

Redeploy

  • If you changed something pertaining to your java code:
./deploy-all.sh UPLOAD_BUCKET AWS_REGION
  • If you didn't make any changes pertaining to your java code
./redeploy-all.sh AWS_REGION

Testing each lambda once to have a first feeling of performance

./test-app.sh TARGET_APP AWS_REGION

Result will look like: Alt text

Run a load test benchmark with Artillery

Load test configuration files are stored in folder loadtest.

  • Go to Cloudwatch
  • Delete all the workshop function log groups
  • Run you benchmark
./benchmark.sh TARGET_APP AWS_REGION

OR

./run-all-benchmarks.sh AWS_REGION

Visualize your results

  • Then go to CloudWatch Logs Insights
  • Select the appropriate log groups
  • Collect your logs using the same query

Result will look like: Alt text

Cleanup

./cleanup-all.sh AWS_REGION

Resources

See the AWS SAM developer guide for an introduction to SAM specification, the SAM CLI, and serverless application concepts.

Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: AWS Serverless Application Repository main page.

Official AWS workshop on Java on AWS Lambda.

About

Java optimization on AWS Lambda Workshop

License:MIT License


Languages

Language:Java 96.3%Language:Shell 3.5%Language:JavaScript 0.3%