我が社にて、cloud watchのalertを拾って、LambdaからIFTTTのwebhookに繋げてパトライトを光らせよう、っていうプチプロジェクトが走ろうとしております。
だいたいこちらの記事のような内容です。
https://kimama.cloud/2020/01/06/alarm2patlite/
ただ、今回はこのへんの事情は関係なく・・・
↑ここだけの記事です。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を編集
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
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
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
中身の作成
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"]
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)
}
{
"q": "golang"
}
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のイメージも追加されました。
Lambdaも2つできてます。
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
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
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
[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(一部)
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できました。
3. python
お次はpython。これは慣れてるのでスムーズにできそう。(Rustは久しく触ってなかったのでちょっと時間かかった)
作成からdeployまでの道のり
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
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
requests ~= 2.28.1
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
FROM public.ecr.aws/lambda/nodejs:16
COPY app.js package.json /var/task/
RUN npm install
CMD ["app.handler"]
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(非同期)
※コメントをいただきましたが、非同期処理になってしまっていたため、スクリプトを見直しました。
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(同期処理)
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して使うんだろう?
(消してみても動いたけどな。応用が効きやすいようにしてる?まあ、ここでは深追いすまいて)
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(一部)
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
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
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でさくっと取得するほうがかっこいいかもしんない)
aws consoleで該当のLambdaの画面を開くとそこから遷移できます。
MinimumとAverageとMaximumがありますが、1分の粒度でみるとどれも同じになります。なのでcsvに落とす時はAverageだけでも大丈夫。データは1h
を指定します。
そしてそれの最小値と中央値を測るだけの簡単なお仕事です。
ちなみにこのcsv、先頭の5行が邪魔なので削ります
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