1
0

More than 1 year has passed since last update.

webhookを叩くだけのシンプルなLambdaを5つの言語で作ってみて実行速度を測る

Last updated at Posted at 2022-08-14

我が社にて、cloud watchのalertを拾って、LambdaからIFTTTのwebhookに繋げてパトライトを光らせよう、っていうプチプロジェクトが走ろうとしております。

スクリーンショット 2022-08-13 13.20.56.png

だいたいこちらの記事のような内容です。
https://kimama.cloud/2020/01/06/alarm2patlite/

ただ、今回はこのへんの事情は関係なく・・・

スクリーンショット 2022-08-13 13.22.18.png

↑ここだけの記事です。Lambdaのみ。

何をやるのか

Lambdaをいつくかの言語で作成し、どれが速いのかみてみます。
単純に今回のようなケースの場合は何が速いのかなと思ったので。
同じ条件でやるので、速い方が低料金にもなります。

環境

使用する言語

  • Rust 1.63
  • Go 1.18
  • Python 3.9
  • nodejs 16
  • java 11

Lambdaの環境

  • 128MB MEM
  • ECR with Lambda (runtimeとして用意されていない言語もあり、今回はECRで統一します)
  • sam cli
  • eventbridgeを用いて1分毎に実行する
  • 実行はinvokeとします

処理内容

実行されるとeventbridge経由で受け取ったパラメータをwebhookにPOSTするだけ

webhook用のLambda

まずはwebhook用のLambdaを作ります。ここは何でもいいんだけど、goにしてみましょう。
sam initで雛形も作れますし。

  • go
  • api gatewayにてgetのエンドポイント
  • json受け取ってテキストでレスポンス返すだけ
作成からdeployまでの道のり

1. とりあえずgithubリポジトリを作りました

https://github.com/ikegam1/lambda-post-to-webhook-by-multilanguage

2. sam init

% git clone git@github.com:ikegam1/lambda-post-to-webhook-by-multilanguage.git
% cd lambda-post-to-webhook-by-multilanguage
% sam init -o webhook-api                            
Which template source would you like to use?
	1 - AWS Quick Start Templates
Choice: 1

Cloning from https://github.com/aws/aws-sam-cli-app-templates

Choose an AWS Quick Start application template
	1 - Hello World Example
Template: 1

 Use the most popular runtime and package type? (Nodejs and zip) [y/N]: n

Which runtime would you like to use?
	3 - go1.x
Runtime: 3

What package type would you like to use?
	2 - Image
Package type: 2
% tree -l
.
├── LICENSE
├── README.md
└── functions
    ├── README.md
    ├── events
    │   ├── go.json
    │   ├── java.json
    │   ├── node.json
    │   ├── python.json
    │   └── rust.json
    ├── webhook-api
    │   ├── Dockerfile
    │   ├── main.go
    │   └── main_test.go
    └── template.yaml

(go.modとgo.sumは消しました)

なお、events/*.jsonの中身は全部こういう感じです。
ただテキストをLambdaに渡してるだけで、実際にはなんでもいいです。

{
  "q": "言語の名前"
}

3. Dockerfileを編集

functions/webhook-api/Dockerfile
FROM golang:1.18.5

WORKDIR /var/task
COPY . /var/task
RUN mkdir /var/task/cache

ENV CGO_ENABLED=0 \
  GOOS=linux \
  GOARCH=amd64 \
  GOCACHE=/var/task/cache

RUN go mod init main \
  && go mod tidy \
  && go build

CMD ["go", "run", "/var/task/main.go"]

4. main.go

function/webhook-api/main.go
package main

import (
	"fmt"
    "encoding/json"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	var res map[string]interface{}
	json.Unmarshal([]byte(request.Body), &res)

	return events.APIGatewayProxyResponse{
		Body:       fmt.Sprintf("Hello, %s", res["q"]),
		StatusCode: 200,
	}, nil
}

func main() {
	lambda.Start(handler)
}

5. template.yaml

functions/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 30

Resources:
  WebhookApiFunction:
    Type: AWS::Serverless::Function 
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
      Events:
        CatchAll:
          Type: Api
          Properties:
            Path: /
            Method: POST
    Metadata:
      DockerTag: go1.x-v1
      DockerContext: ./webhook-api
      Dockerfile: Dockerfile

Outputs:
  HelloWorldAPI:
    Description: "API Gateway endpoint URL for Prod environment for First Function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
  HelloWorldFunction:
    Description: "First Lambda Function ARN"
    Value: !GetAtt WebhookApiFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt WebhookApiFunctionRole.Arn

6. buildと実行テスト

% sum build
% sam local start-api
% % curl -XPOST http://127.0.0.1:3000/ -d '{"q":"test"}'
Hello, test%

7. deploy

わかりやすく、だいたいdefaultでいっときます。ゴミができるので消す時はsam deleteで綺麗にしましょう。

% sam deploy --guided

Configuring SAM deploy
======================

	Looking for config file [samconfig.toml] :  Not found

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [sam-app]: lambda-post-to-webhook-by-multilanguage-webhook-api
	AWS Region [ap-northeast-1]: 
	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [y/N]: n
	#SAM needs permission to be able to create roles to connect to the resources in your template
	Allow SAM CLI IAM role creation [Y/n]: 
	#Preserves the state of previously provisioned resources when an operation fails
	Disable rollback [y/N]: 
	WebhookApiFunction may not have authorization defined, Is this okay? [y/N]: y
	Save arguments to configuration file [Y/n]: 
	SAM configuration file [samconfig.toml]: 
	SAM configuration environment [default]: 

	Looking for resources needed for deployment:
	 Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xxxx
	 A different default S3 bucket can be set in samconfig.toml
	 Image repositories: Not found.
	 #Managed repositories will be deleted when their functions are removed from the template and deployed
	 Create managed ECR repositories for all functions? [Y/n]: 

CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                          
--------------------------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldFunctionIamRole                                                                                                    
Description         Implicit IAM Role created for Hello World function                                                                           
Value               arn:aws:lambda:ap-northeast-1:xxxx:function:lambda-post-to-webhook-by-multi-WebhookApiFunction-xxxx          

Key                 HelloWorldAPI                                                                                                                
Description         API Gateway endpoint URL for Prod environment for First Function                                                             
Value               https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/                                                            

Key                 HelloWorldFunction                                                                                                           
Description         First Lambda Function ARN                                                                                                    
Value               arn:aws:lambda:ap-northeast-1:xxxx:function:lambda-post-to-webhook-by-multi-WebhookApiFunction-xxxx          
--------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - lambda-post-to-webhook-by-multilanguage-webhook-api in ap-northeast-1
% time curl -XPOST "https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/" -d '{"q":"test"}'
Hello, test
curl -XPOST  -d '{"q":"test"}'  0.02s user 0.01s system 18% cpu 0.152 total

※ cold startに当たるとちょっと遅いかも

そんなわけでqというパラメータをPOSTすると正常終了するだけのapiができあがりました。
厳密にはwebhookでもなんでもないのですが、今回はネットワーク越しに何かを叩ければよしとします。

そんなわけで言語ごとにLambdaを作っていきます。

1. Go

さきほどのapiの流用で作れそうだし、これからやってみます。

作成からdeployまでの道のり

まずwebhook-apiのディレクトリをgo-funcとして複製し、下記のような構成となりました。

.
├── LICENSE
├── README.md
└── functions
    ├── README.md
    ├── events
    │   ├── go.json
    │   ├── java.json
    │   ├── node.json
    │   ├── python.json
    │   └── rust.json
    ├── go-func
    │   ├── Dockerfile
    │   ├── main.go
    │   └── main_test.go
    ├── samconfig.toml
    ├── template.yaml
    └── webhook-api
        ├── Dockerfile
        ├── main.go
        └── main_test.go

中身の作成

go-func/Dockerfile
FROM golang:1.18.5

WORKDIR /var/task
COPY . /var/task
RUN mkdir /var/task/cache

ENV CGO_ENABLED=0 \
  GOOS=linux \
  GOARCH=amd64 \
  GOCACHE=/var/task/cache

RUN go mod init main \
  && go mod tidy \
  && go build

CMD ["go", "run", "/var/task/main.go"]
go-func/main.go
package main

import (
	"context"
	"fmt"
	"encoding/json"
	"net/http"
	"bytes"

	"github.com/aws/aws-lambda-go/lambda"
)

func handler(ctx context.Context, b json.RawMessage) {
	var req map[string]interface{}
	json.Unmarshal([]byte(b), &req)
	fmt.Println("%#v", req)

	data, _ := json.Marshal(req)
    // ほんとは環境変数とかにした方がいいけど・・全部の言語でやるのしんどそうだったので今回は手抜き
	endpoint := "https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/"

	res, err := http.Post(endpoint, "application/json", bytes.NewBuffer(data))
    if err != nil {
        fmt.Println("[*] " + err.Error())
    }
    defer res.Body.Close()

	fmt.Println("[*] " + res.Status)
}

func main() {
	lambda.Start(handler)
}
events/go.json
{
  "q": "golang"
}

template.yamlへの追記

template.yaml
  GoFunction:
    Type: AWS::Serverless::Function 
    Properties:
      PackageType: Image
      MemorySize: 128
      Role: !GetAtt WebhookApiFunctionRole.Arn
      Architectures:
        - x86_64
      Events:
        ScheduledFunction:
          Type: Schedule
          Properties:
            Schedule: 'rate(1 minute)'
            Input: |
              {
              "q": "golang"
              }
    Metadata:
      DockerTag: go1.x-v1
      DockerContext: ./go-func
      Dockerfile: Dockerfile

build&test

% sam build GoFunction 
% sam local invoke GoFunction -e events/go.json
Invoking Container created from gofunction:go1.x-v1
Building image.................
Skip pulling image and use local one: gofunction:rapid-1.37.0-x86_64.

START RequestId: 6860f8cb-0a8f-43e6-9881-fc262e11b5bb Version: $LATEST
%#v map[q:golang]
[*] 200 OK
END RequestId: 6860f8cb-0a8f-43e6-9881-fc262e11b5bb
REPORT RequestId: 6860f8cb-0a8f-43e6-9881-fc262e11b5bb	Init Duration: 0.17 ms	Duration: 602.58 ms	Billed Duration: 603 ms	Memory Size: 128 MB	Max Memory Used: 128 MB	
null%

responseは空ですが、apiの実行とレスポンスは問題なさそうです。

deploy

% sam build
% sam deploy --guided GoFunction
	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [lambda-post-to-webhook-by-multilanguage-webhook-api]: 
	AWS Region [ap-northeast-1]: 
	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [Y/n]: n
	#SAM needs permission to be able to create roles to connect to the resources in your template
	Allow SAM CLI IAM role creation [Y/n]: n
	Capabilities [['CAPABILITY_IAM']]: 
	#Preserves the state of previously provisioned resources when an operation fails
	Disable rollback [y/N]: 
	WebhookApiFunction may not have authorization defined, Is this okay? [y/N]: y
	Save arguments to configuration file [Y/n]: 
	SAM configuration file [samconfig.toml]: 
	SAM configuration environment [default]: 

これでfunctionが一つ増えます。
ECRのイメージも追加されました。

スクリーンショット 2022-08-13 16.29.11.png

Lambdaも2つできてます。

スクリーンショット 2022-08-13 17.04.07.png

1分毎にLambdaが叩かれ続けます。実行時間の計測はcloudwatchに任せましょう。

2. Rust

お次はRustです。
個人的にはこれが一番速い・・といいなぁと思ってます。実際に速いんじゃないかなぁ。
実行ファイルだし、サイズも小さめだし。
ディレクトリはrust-funcとして作っていきます

参考: https://github.com/awslabs/aws-lambda-rust-runtime, https://docs.rs/reqwest/0.11.11/reqwest/, https://dev.classmethod.jp/articles/rust-app-container-on-lambda-function/

作成からdeployまでの道のり

ディレクトリ(一部)

├── rust-func
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── Dockerfile
│   └── src
│       └── rust-func.rs

Dockerfile

rust-func/Dockerfile
FROM public.ecr.aws/lambda/provided:al2

WORKDIR /var/task
COPY src/ /var/task/src/
COPY Cargo.toml Cargo.lock /var/task/

RUN yum install -y openssl-devel gcc 
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o rustup-init && \
    sh rustup-init -y && \
    source $HOME/.cargo/env
RUN export PATH=$HOME/.cargo/bin:$PATH

RUN $HOME/.cargo/bin/rustup update && \
    $HOME/.cargo/bin/rustup target add x86_64-unknown-linux-gnu
RUN $HOME/.cargo/bin/cargo build --release --target x86_64-unknown-linux-gnu

# 実行ファイルを起動するようにするため、ファイル名を "bootstrap" に変更する
RUN cp -a /var/task/target/x86_64-unknown-linux-gnu/release/rust-func ${LAMBDA_RUNTIME_DIR}/bootstrap

# カスタムランタイムはハンドラ名利用しないため、適当な文字列を指定する。
CMD [ "lambda-handler" ]

rust-func.rs

rust-func/src/rust-func.rs
use lambda_runtime::{handler_fn, Context, Error};
use std::collections::HashMap;

// エントリポイント
#[tokio::main]
async fn main() -> Result<(), Error> {
    let func = handler_fn(my_handler);
    lambda_runtime::run(func).await?;
    Ok(())
}

async fn my_handler(event: Value, _: Context) -> Result<Value, Error> {
    let q = event["q"].as_str().unwrap_or("undefined");

    let mut map = HashMap::new();
    map.insert("q", q);

    let client = reqwest::Client::new();
    let resp = client.post("https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/")
        .json(&map)
        .send()
        .await?;

    println!("status code: {}", resp.status());

    Ok(json!({ "message": format!("Hello, {}!", q) }))
}

Cargo.toml

rust-func/Cargo.toml
[package]
name = "rust-func"
version = "0.1.0"
edition = "2018"

[dependencies]
lambda_runtime = "0.4"
lambda_http = "0.4.0"
serde_json = "1.0.59"
tokio = "1.11.0"
reqwest = { version = "0.11", features = ["json"] }
openssl = "0.10.41"

[[bin]]
name = "rust-func"
path = "src/rust-func.rs"

template.yaml(一部)

template.yaml
  RustFunction:
    Type: AWS::Serverless::Function 
    Properties:
      PackageType: Image
      MemorySize: 128
      Role: !GetAtt WebhookApiFunctionRole.Arn
      Architectures:
        - x86_64
      Events:
        ScheduledFunction:
          Type: Schedule
          Properties:
            Schedule: 'rate(1 minute)'
            Input: |
              {
              "q": "rust"
              }
    Metadata:
      DockerTag: rust-latest
      DockerContext: ./rust-func
      Dockerfile: Dockerfile

build, test, deploy

% sam build RustFunction
% sam local invoke RustFunction
(略)
        path: "/Prod/",
        query: None,
        fragment: None,
    },
    status: 200,
    headers: {
        "content-type": "application/json",
        "content-length": "16",
        "connection": "keep-alive",
(略)
% sam build
% sam deploy --guided RustFunction
(略)
	Stack Name [lambda-post-to-webhook-by-multilanguage-webhook-api]: 
	AWS Region [ap-northeast-1]: 
	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [Y/n]: n
	#SAM needs permission to be able to create roles to connect to the resources in your template
	Allow SAM CLI IAM role creation [Y/n]: n
	Capabilities [['CAPABILITY_IAM']]: 
	#Preserves the state of previously provisioned resources when an operation fails
	Disable rollback [y/N]: 
	WebhookApiFunction may not have authorization defined, Is this okay? [y/N]: y
	Save arguments to configuration file [Y/n]: 
	SAM configuration file [samconfig.toml]: 
	SAM configuration environment [default]: 

(略)
	 Create managed ECR repositories for the 1 functions without? [Y/n]: y

こちらも無事deployできました。

スクリーンショット 2022-08-13 19.59.57.png

3. python

お次はpython。これは慣れてるのでスムーズにできそう。(Rustは久しく触ってなかったのでちょっと時間かかった)

作成からdeployまでの道のり

Dockerfile

python-func/Dockerfile
FROM public.ecr.aws/lambda/python:3.9

COPY requirements.txt ./
RUN python3.9 -m pip install -r requirements.txt -t .
COPY *.py ./

# Command can be overwritten by providing a different command in the template directly.
CMD ["lambda_function.lambda_handler"]

lambda_function.py

python-func/lambda_function.py
import sys
import json
import requests


def lambda_handler(event, context):
    q = event.get("q")
    try:
        endpoint = "https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/"
        payload = {"q": q}
        r = requests.post(endpoint, json.dumps(payload))

        print("OK: status=", r.status_code)

    except Exception as e:
        sys.exit(0)

requirements.txt

python-func/requirements.txt
requests ~= 2.28.1

template.yaml(一部)

template.yaml
  PythonFunction:
    Type: AWS::Serverless::Function 
    Properties:
      PackageType: Image
      MemorySize: 128
      Role: !GetAtt WebhookApiFunctionRole.Arn
      Architectures:
        - x86_64
      Events:
        ScheduledFunction:
          Type: Schedule
          Properties:
            Schedule: 'rate(1 minute)'
            Input: |
              {
              "q": "rust"
              }
    Metadata:
      DockerTag: python-v3.9
      DockerContext: ./python-func
      Dockerfile: Dockerfile

build, test, deploy

% sam build PythonFunction
% sam local invoke PythonFunction -e events/python.json
(略)
OK: status= 200
(略)
% sam build
% sam deploy --guided PythonFunction
(RustやGoの時と同じ)

さすがスクリプト言語。だいぶスッキリしてます。
あとはパフォーマンスがどうか。意外と遅くない気もするんですよね、実は。

4. nodejs

お次はnodejs。Lambdaで書くと非同期が邪魔になってあんまり意味がないやつ。

作成からdeployまでの道のり

ディレクトリ(一部)

├── node-func
│   ├── Dockerfile
│   ├── index.js
│   └── package.json

Dockerfile

node-func/Dockerfile
FROM public.ecr.aws/lambda/nodejs:16

COPY app.js package.json /var/task/
RUN npm install

CMD ["app.handler"]

template.yaml(一部)

template.yaml
  NodeFunction:
    Type: AWS::Serverless::Function 
    Properties:
      PackageType: Image
      MemorySize: 128
      Role: !GetAtt WebhookApiFunctionRole.Arn
      Architectures:
        - x86_64
      Events:
        ScheduledFunction:
          Type: Schedule
          Properties:
            Schedule: 'rate(1 minute)'
            Input: |
              {
              "q": "nodejs"
              }
    Metadata:
      DockerTag: node-v16
      DockerContext: ./node-func
      Dockerfile: Dockerfile

app.js(非同期)

※コメントをいただきましたが、非同期処理になってしまっていたため、スクリプトを見直しました。

node-func/app.js
const https = require('https')

exports.handler = (event, context) => {
    try {
        const data = JSON.stringify({
            q: event["q"],
        })

        const options = {
           method: "POST",
           headers: {
               "Content-Type": "application/json",
           },
        }

        const url = 'https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/'
        const request = https.request(url, options, response => {
            console.log(`statusCode: ${response.statusCode}`)
        })
        request.write(data)
        request.end()

        return {"statusCode": 200}

    } catch (err) {
        console.log(err)
        return err
    }
};

app.js(同期処理)

node-func/app.js
const https = require('https')

exports.handler = (event, context, callback) => {
    try {
        const data = JSON.stringify({
            q: event["q"],
        })

        const options = {
           method: "POST",
           headers: {
               "Content-Type": "application/json",
           },
        }

        const url = 'https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/'
        const request = https.request(url, options, response => {
            console.log('callback')
            callback(null, `statusCode: ${response.statusCode}`)
        })
        request.write(data)
        request.end()
    } catch (err) {
        callback(err)
    }
};

build, test, deploy

% sam build NodeFunction
% sam local invoke NodeFunction -e events/node.json
(略)
2022-08-13T13:54:27.666Z	f98e15ea-6231-48da-89fa-57e9e5a55cc5	INFO	statusCode: 200
(略)
% sam build
% sam deploy -g NodeFunction
(RustやGoの時と同じ)

5. java

そして最後はjava。実はかなり昔に少し触ったことがある程度でしかなく、Lambdaもどうすりゃいいのだろう、というレベル。

ただ、sam init時のexampleにjavaがあったのでそれを雛形にしつつやってみます。
java11 x Image(ECR) x gradleです。
ここのテンプレ。
https://github.com/aws/aws-sam-cli-app-templates/tree/master/java11-image/cookiecutter-aws-sam-hello-java-gradle-lambda-image

ちなみに、これだけは128MEMじゃ動かないんじゃ?って思ってる。参考記録になるかもですね。

・・と思いきやExampleをdeployしてみたら一応動いた。Hello Worldだけど。
ただ、すでに128MB使ってるみたいだし、これにHttp Request関連を実装しても足りるかどうかはわからない。

作成からdeployまでの道のり

ディレクトリ(一部)

なんだかjavaになるt増えますね。build関連のやつが多い。

├── java-func
│   ├── Dockerfile
│   ├── build.gradle
│   ├── gradle
│   │   ├── lambda-build-init.gradle
│   │   └── wrapper
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradlew
│   ├── lambda-build-init.gradle
│   └── src
│       ├── main
│       │   └── java
│       │       └── qiitareport
│       │           └── App.java
│       └── test
│           └── java
│               └── qiitareport
│                   ├── AppTest.java
│                   ├── TestContext.java
│                   └── TestLogger.java

Dockerfile

なんでビルドしたものを別のimageにcopyして使うんだろう?
(消してみても動いたけどな。応用が効きやすいようにしてる?まあ、ここでは深追いすまいて)

java-func/Dockerfile
FROM public.ecr.aws/lambda/java:11 as build-image

ARG SCRATCH_DIR=/var/task/build

COPY src/ src/
COPY gradle/ gradle/
COPY build.gradle gradlew ./

RUN mkdir build
COPY gradle/lambda-build-init.gradle ./build

RUN ./gradlew --project-cache-dir $SCRATCH_DIR/gradle-cache -Dsoftware.amazon.aws.lambdabuilders.scratch-dir=$SCRATCH_DIR --init-script $SCRATCH_DIR/lambda-build-init.gradle build
RUN rm -r $SCRATCH_DIR/gradle-cache
RUN rm -r $SCRATCH_DIR/lambda-build-init.gradle
RUN cp -r $SCRATCH_DIR/*/build/distributions/lambda-build/* .

FROM public.ecr.aws/lambda/java:11

COPY --from=build-image /var/task/META-INF ./
COPY --from=build-image /var/task/qiitareport ./qiitareport
COPY --from=build-image /var/task/lib/ ./lib
# Command can be overwritten by providing a different command in the template directly.
CMD ["qiitareport.App::handleRequest"]

template.yaml(一部)

template.yaml
  JavaFunction:
    Type: AWS::Serverless::Function 
    Properties:
      PackageType: Image
      MemorySize: 128
      Role: !GetAtt WebhookApiFunctionRole.Arn
      Architectures:
        - x86_64
      Environment: 
        Variables:
          JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
      Events:
        ScheduledFunction:
          Type: Schedule
          Properties:
            Schedule: 'rate(1 minute)'
            Input: |
              {
              "q": "java"
              }
    Metadata:
      DockerTag: java11
      DockerContext: ./java-func
      Dockerfile: Dockerfile

build.gradle

java-func/build.gradle
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
    implementation 'com.amazonaws:aws-lambda-java-events:3.11.0'
    testImplementation 'junit:junit:4.12'
    implementation 'com.google.code.gson:gson:2.9.1'
    implementation 'org.apache.logging.log4j:log4j-api:2.18.0'
    implementation 'org.apache.logging.log4j:log4j-core:2.18.0'
}

sourceCompatibility = 11
targetCompatibility = 11

App.java

java-home/src/main/java/qiitareport/App.java
package qiitareport;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.io.PrintStream;
import java.net.HttpURLConnection;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
 * Handler for requests to Lambda function.
 */
public class App implements RequestHandler<Map<String,String>, String>{
    Gson gson = new GsonBuilder().setPrettyPrinting().create();
    @Override
    public String handleRequest(Map<String,String> event, Context context)
    {
        LambdaLogger logger = context.getLogger();

        // process event
        logger.log("EVENT: " + gson.toJson(event));
        logger.log("EVENT TYPE: " + event.getClass().toString());

        String statusCode;
        try {
            statusCode = postWebhook(event.get("q"));
            logger.log("\n statusCode: " + statusCode + "\n");
        } catch (IOException e) {
            System.out.println(e);
            return "IOException";
        }

        return statusCode;
    }

    public String postWebhook(String query) throws IOException{

        String postdata =
        "{" +
            " \"q\": \"" +
            query + "\"" +
        "}";

        String  apiURL ="https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/";
        URL url = new URL(apiURL);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        conn.setRequestMethod("POST");
        conn.setDoInput(true);
        conn.setDoOutput(true);
        conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");

        conn.connect();

        //HttpURLConnectionからOutputStreamを取得し、json文字列を書き込む
        PrintStream ps = new PrintStream(conn.getOutputStream());
        ps.print(postdata);
        ps.close();


        //正常終了時はHttpStatusCode 200が返ってくる
        Integer statusCode = Integer.valueOf(conn.getResponseCode());
        if (statusCode != 200) {
            throw new IOException();
        }

        return statusCode.toString();
    }
}

build, test, deploy

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-handler.html
https://github.com/awsdocs/aws-lambda-developer-guide/blob/main/sample-apps/java-basic/src/main/java/example/Handler.java
↑このあたりでhandlerのあたりを

https://github.com/awsdocs/aws-lambda-developer-guide/tree/main/sample-apps/java-basic/src/test/java/example
↑このあたりでTestContextのあたりを

https://qiita.com/nururuv/items/b269af6ac5ac472ceab1
↑このあたりでPost Requestのあたりを調べつつなんとか動きました。
思ったよりスムーズにできてよかった。
ちなみに行末の;が抜けててよく怒られた。
そして今思うと、どうせならKotlinで良かったかもしんない。

ここで挙げた以外でもファイルを作ったりいじったりしてますが、ファイル数多いので記事には挙げませんでした。気になる方はgithubの方を見てくださいませ。
https://github.com/ikegam1/lambda-post-to-webhook-by-multilanguage

% sam build JavaFunction
% sam local invoke JavaFunction -e events/java.json
()
 statusCode: 200
()
% sam build
% sam deploy -g JavaFunction
(RustやGoの時と同じ)

いよいよ、パフォーマンス測定

まずはECRイメージのサイズを比較しておきます

Lang Size(MB)
Go 363.22
Rust 819.24
python 173.53
nodejs 150.34
java 417.55

Rustがダントツに多い。build時に必要だったライブラリだとかは消した状態でイメージにした方が良いかもですね。 javaのdockerファイルでbuild imageが分かれてたのはそういうことか。次やる時は工夫してみます。
あとRustはamazon linux2ベースのイメージを使っちゃったのが大きかったと思う。
とはいえ、 public.ecr.aws/lambda/provided:al2 は109MB程度みたいですが。

で、インタプリタ言語の2者が軽量。defaultのlambda用イメージにスクリプトが乗っかるだけなので軽量なんでしょうね。nodejsなんかは標準のライブラリだけで済んでる。

いよいよ処理速度

各言語、1分に1度実行で1時間経過後のデータを計測します。
cold startの影響はあまり考慮したくないので、最小値と中央値を算出してみようと思います。

まずはCloudWatchの画面に遷移し、csvをダウンロードします。(CLIでさくっと取得するほうがかっこいいかもしんない)

スクリーンショット 2022-08-14 16.25.11.png
aws consoleで該当のLambdaの画面を開くとそこから遷移できます。

スクリーンショット 2022-08-14 16.25.31.png
MinimumとAverageとMaximumがありますが、1分の粒度でみるとどれも同じになります。なのでcsvに落とす時はAverageだけでも大丈夫。データは1hを指定します。

そしてそれの最小値と中央値を測るだけの簡単なお仕事です。
ちなみにこのcsv、先頭の5行が邪魔なので削ります

スクリーンショット 2022-08-14 16.36.46.png

5つcsv作って、pandasで処理するとこんな感じです。

data = {}
data["g"]= pd.read_csv('drive/My Drive/colab/go.csv')
data["r"]= pd.read_csv('drive/My Drive/colab/rust.csv')
data["p"]= pd.read_csv('drive/My Drive/colab/python.csv')
data["n"]= pd.read_csv('drive/My Drive/colab/node.csv')
data["j"]= pd.read_csv('drive/My Drive/colab/java.csv')
for i, k in enumerate(data):
  data[k] = data[k][4:]
  data[k] = data[k].iloc[:,[2]]
  data[k][data[k].columns[0]] = data[k][data[k].columns[0]].astype('float64')
  print(data[k].head(1))

結果発表

for i, k in enumerate(data):
  print("言語", k)
  print("最小値", data[k].min(numeric_only=True))
  print("中央値", data[k].median())
言語 g
最小値 metric_alias_metrics_view_graph_0    30.95
dtype: float64
中央値 metric_alias_metrics_view_graph_0    40.39
dtype: float64
言語 r
最小値 metric_alias_metrics_view_graph_0    98.03
dtype: float64
中央値 metric_alias_metrics_view_graph_0    123.19
dtype: float64
言語 p
最小値 metric_alias_metrics_view_graph_0    127.47
dtype: float64
中央値 metric_alias_metrics_view_graph_0    161.32
dtype: float64
言語 n
最小値 metric_alias_metrics_view_graph_0    42.96
dtype: float64
中央値 metric_alias_metrics_view_graph_0    80.22
dtype: float64
言語 j
最小値 metric_alias_metrics_view_graph_0    118.02
dtype: float64
中央値 metric_alias_metrics_view_graph_0    212.05
dtype: float64
Lang min(msec) median(msec) rank(median)
Go 30.95 40.39 1位
Rust 98.03 123.19 3位
python 127.47 161.32 4位
nodejs(非同期) 42.96 80.22 2位
java 118.02 212.05 5位

とまあ、こんな結果になりました。
Goの上位やjavaの最下位は予想通りなのですが・・Rustはちょい残念ですね。
今回はプログラムの中身やライブラリに起因するところもあると思うので一概には言えませんが、1、2を争うかと思っていたのですが。
ちなみに、使用していたメモリはRustが一番少なかったように見えました。

REPORT RequestId: 81867ca4-16bb-4c0e-a2d9-adc8cf46b272 Duration: 120.50 ms Billed Duration: 121 ms Memory Size: 128 MB Max Memory Used: 27 MB

こういう感じで。nodeやpythonは60MBちょっとぐらい、javaは110MBぐらい、Goは128MBをフルで使っています。

※nodejsは非同期処理になってしまっていて、webhookのレスポンスをちゃんと待っていない可能性もあって少しズルかったです。なので非同期versionは参考記録となります。同期versionについても計測予定です。

なお、このnotebookもgitに上げて置きました。大したことはやってないですけど。
https://github.com/ikegam1/lambda-post-to-webhook-by-multilanguage/blob/main/qiita_Lambda_%20compare.ipynb

まとめ

  • Goが速い
  • javaは128MBだとやはり辛い(というか動くんですね)。試しに1024MBに増やすとかなり改善した。
    • RustやGoもメモリを増やすと速くなったのでこのあたりも指標にすれば良かった。
  • どの言語もDockerfileさえ作れればLambdaに乗せるのは難しくない。
  • 正直、今回程度のものであればお好みで。
    • お好みも含めて、今回のパトランププロジェクトは個人的にはGoにしたい
  • Imageが大きくなりすぎないように工夫はした方が良かった(反省)

延長戦

メモリを全て1024MBにした状態でもう一度計測します

ちなみにデータはこんな感じで取り出せました。

aws cloudwatch get-metric-data --metric-data-queries '{"Id":"q1","MetricStat":{"Metric":{"Namespace":"AWS/Lambda","MetricName":"Duration","Dimensions":[{"Name":"FunctionName","Value":"lambdaのfunction名"}]},"Period":60,"Stat":"Average"}}' --start-time "2022-08-14T08:45:00Z" --end-time "2022-08-14T09:45:00Z" | jq -r '.MetricDataResults[0].Values' | sed 's/[\ ,]//g' | sed -e '$d' | sed '1d' > java.csv

ちょっとsedが愚直ではありますが。

そして結果はこんな感じに。

言語 g
最小値 27.17
中央値 32.6
言語 r
最小値 50.66
中央値 91.07
言語 p
最小値 43.11
中央値 63.41
言語 n
最小値 32.39
中央値 56.45
言語 j
最小値 40.74
中央値 71.49

pythonとjavaがかなり改善されました。これらはメモリが少ないとパフォーマンスが出にくいのでしょうかね。
rustは改善したもののもうちょっと。。これはライブラリとか、ネットワークを介する処理の手続きのあたりで苦戦してそうな予感。
※ここでのnodejsは非同期のもの

お掃除

sam delete --no-prompts

すこし名残惜しくはありましたが・・一切合切消しておきました。
すごい勢いで消えていくのは気持ちよくもある。

nodejsの同期version

あらためて計測予定。(面倒なので128MB版のみ)

nodejs(同期処理)ですが、こうなりました。
非同期の時の中央値が80.22なので少し遅くなったという程度ですが、誤差レベルでした。
なおnodeしか稼働してないため、api側の負荷が下がってると思い、15秒毎にcurlで叩いてます。
厳密にやるならapiも同条件で5つ立てておくべきでしたね。

言語 n
最小値 40.03
中央値 83.16
1
0
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0