GraalVM で作成したネイティブコードをAWS Lambda Custom Runtimes上で動かしてみた

この記事は AWS Lambda Custom Runtimes芸人 Advent Calendar 2018 の 9日目 です。

はじめに

9日目は前から気になっていたGraalVMで作成したネイティブコードをAWS Lambda上で動かしてみます。

GraalVMとは

公式サイトによるとGraalVMとは

GraalVM is a universal virtual machine for running applications written in JavaScript, Python, Ruby, R, JVM-based languages like Java, Scala, Kotlin, Clojure, and LLVM-based languages such as C and C++.

ということで、複数の言語を実行できる仮想マシンです。 さらにJavaからNodeのコードを実行して結果を参照したり、RからRubyを呼び出したりなんてこともできます(polyglotと呼ばれる)。

またネイティブコードを生成する機能もあります。今回はこちらを試してみます。

GraalVM can compile Java bytecode into native images to achieve faster startup and smaller footprint for your applications.

フットプリントが小さくなり、起動が早くなるという記述があるのでLambdaにはうってつけではないかと思います。

やること

GraalVMの機能で次のような構成を試してみたいと思います。

  • Lambda ハンドラをJavaで記述してJarファイルをビルドする
  • JarファイルをNative Image化する
  • シェルスクリプトのbootstrapと一緒にアップロードする

できたもの

完成品がこちらにになります

一つずつ説明していきます。

イメージの作成

Amazon Linux上で動くバイナリが必要なのでコンテナ上でNative Imageの生成を行います。 GraalVMを動かすだけならzlibやgccは不要なのですがイメージ作成時に必要になるのでインストールしておきます。

FROM amazonlinux:2

# install dependencies
RUN yum update -y && yum install -y tar gzip gcc zlib-devel

# download graalvm
RUN curl -L https://github.com/oracle/graal/releases/download/vm-1.0.0-rc9/graalvm-ce-1.0.0-rc9-linux-amd64.tar.gz | tar zx -C /opt

ハンドラの作成

ハンドラのJavaプログラムは特に変わったところがないので省略します。

ネイティブへの変換

ネイティブイメージへの変換時に下記のように-H:ReflectionConfigurationFilesオプションが必要でした。 今回プログラムの中ではjacksonを使ってJSONからのシリアライズ/デシリアライズを行なっているのですが、ネイティブ変換時にはリフレクションに必要な情報が削除されてしまうらしく、明示的に保存を指定する必要がありました。

docker run -v(pwd):/home --rm kazup0n/graalvm  'cd /home; /opt/graalvm-ce-1.0.0-rc9/bin/native-image -H:ReflectionConfigurationFiles=reflectconfig.json -jar /home/build/libs/handler-1.0-SNAPSHOT-all.jar'

指定したファイルはこんな感じです。

[
  {
  "name" : "bootstrap.Request",
  "allDeclaredConstructors" : true,
  "allPublicConstructors" : true,
  "allDeclaredMethods" : true,
  "allPublicMethods" : true
  },
  {
    "name" : "bootstrap.Response",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "allPublicFields": true
  }
]

bootstrap

bootstrapはサンプルを参考に下記のようになりました。 handlerは直接実行できるので、凝ったことはしていません。

#!/bin/sh

set -euo pipefail

EXEC="$LAMBDA_TASK_ROOT/$_HANDLER"

# Processing
while true
do
  HEADERS="$(mktemp)"
  # Get an event
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # Execute the handler function from the script
  RESPONSE=$(echo "$EVENT_DATA" | $EXEC)

  # Send the response
  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
done

実行

で、これらをZIPしてアップロードして実行してみます。

実行結果 実行できました。

Javaランタイムとの実行時間の比較

同様のプログラムを作成して、簡単に比較してみましたがJavaランタイムの方が速かったです。

今回のランタイム: 265.05 ms 前後 Java ランタイム: 0.5ms 前後

流石にここまで遅いということはないと思うので、bootstrapの実装など工夫することで高速化することはできると思います。

うまくいっったこと/うまくいかなったこと

GraalVMで作成したネイティブイメージをAWS Lambda上で実行できました。

今回は簡単なプログラムを変換してハンドラとしましたが以下のような構成も可能なはずです。

  • layerにGraalVMを含めて様々な種類の言語を実行する
    • 当初はこちらを想定していたのですが、アップロード中にサイズの上限に引っかかって試せませんでした。しかしこれはかなり面白いと思います
  • bootstrapをネイティブイメージとして作成してpolyglotで他の言語のハンドラを呼び出す
    • こちらも試してみましたがHTTP通信に使ったcommons httpcomponentsがSecureRandomを呼び出している箇所がうまく変換できなかったので断念しました

まとめ

ほぼGraalVMを動かすのに四苦八苦した感じになりましたが、Custom Runtimes上で動かすことができました。