maybe daily dev notes

私の開発日誌

AWS Lambda特化のJavaScriptランタイム「LLRT」を紹介

最近にわかに話題沸騰中のJavaScriptランタイム LLRT を紹介する記事です。

github.com

LLRTとは

LLRT (Low Latency Runtime) は、軽量なJavaScriptランタイムです。サーバーサイド向けのJavaScriptランタイムはNode.js、Deno、Bunなどが有名ですが、それらにまた一つ加わった形になります。主にLambdaでの利用が念頭に置かれているようです。その他必要な情報は README.md にまとまっています。以下は抜粋です。

AWSのソリューションアーキテクト Richard Davison さんにより開発されています。リポジトリAWSGitHub organization (awslabs) で公開されているため、実験的ではありますが、AWS公式のプロジェクトと言って良いでしょう。

ここ5日間ほどでとんでもない勢いでGitHubスター数が伸びており、注目度は高いと言えます。このため、今はほぼ一人で開発されているようですが、より多くの投資を受ける可能性も低くはないかもしれません。

awslabsリポジトリ上位6件のGitHubスター履歴。LLRTの注目度が窺える。

実装の詳細

より踏み込んだ部分を見ていきましょう。

内部のJavaScriptエンジンは、QuickJSというC言語で実装されたエンジンが利用されています。これはFabrice Bellardさんが開発する個人プロジェクトで、ES2020/ES2023準拠 (LLRTは現状ES2020のバージョンを利用) の軽量なエンジンのようです。

このQuickJSをRustから呼び出す rquickjs というライブラリを利用し、LLRTがNode.js特有のAPIをRustで独自実装することで、Node.jsとの互換性を高めています。

もちろん完全な互換性があるわけではないため、Node.jsのプログラムがそのままLLRTで動作しない場合も多いです。互換性を示した表がREADMEにあるため、見てみると良いでしょう。今のところは、最低限AWS SDK for JS v3が動作するところまでの互換性を実現しているように見えます。

今後細かな実装は変わりうるので、上記は今のところのスナップショットとして理解してください。現に今も、QuickJSではなくHermesというエンジンを利用すべきではという議論が進んでいるようです (Issue#110)。 また、Node.js互換というよりは、WinterCG互換を目指すという目標もあるようです (Issue#112)。

LLRTの特徴

名前の通り、特徴は軽量で低レイテンシーなことです。 Node.jsなど他のJavaScriptランタイムよりLambdaのコールドスタートが最大で10倍短くなることが謳われています

実際のデータはこちらのサイトが参考になるでしょう: Lambda Cold Starts analysis

C++, Rustに匹敵するコールドスタートの短さです。Goよりも短いですね。

簡単に比率を表にまとめてみました (zip、2024-02-13のデータ)

Avg Cold start duration Avg Memory Avg duration
LLRT 100 % (35.9 ms) 100 % (23.6 MB) 100 % (1.29 ms)
Rust (al2023) 52 % 56 % 115 %
Go (al2023) 137 % 60 % 121 %
Node.js v20 428 % 268 % 704 %
Bun (al2) 944 % 270 % 21308 %

LLRTの強みが見て取れると思います。普段Node.jsばかりLambdaで使っている身としては、コールドスタートが2桁msで終わるのは驚異的です。

なお、Bunのdurationが異常に長いですが、これはコールドスタート時のみの実行時間なので、不利な比較ではあるのかもしれません。

なぜ速いか

LLRTはいかにしてこの性能を実現できたのでしょうか。また、代償として何が失われているでしょうか。 Rationaleの章を読んでみましょう。

Node.js, Bun, Denoとの大きな違いは、LLRTはランタイムにJITコンパイラの機能を持たないことです。これにより、次の2つのメリットを得られました:

  1. 複雑なJITコンパイルの仕組みを排除することで、システムは単純になり、ランタイムのサイズも小さくなります
  2. JITコンパイルのオーバーヘッドがなくなり、CPU・メモリリソースの消費を低減できます

定性的には理解できる話と思います。実際にこれでどの程度数値が改善したのかは、上で見たとおりです。

しかしながら、これによるデメリットもあります。Limitationsの章で議論されていますが、

JITコンパイル自体による性能改善が効くタスク、例えば同じ処理を何度も実行する大規模なデータ処理や数値計算などでは、性能低下が見込まれる

とのことです。記事の後半で議論しますが、何でも置き換えれば良いという話ではなく、使い所を考える必要があります。

その他の高速化のポイントとして、JavaScriptのライブラリ (主にAWS SDK関連; uuid, fast-xml-parserなど) をRustによるネイティブ実装に置き換えるということもされているようです。

LLRTの使い方

LLRTはLambdaのカスタムランタイムとして利用できます。

LambdaでLLRTを利用するには、LambdaのランタイムとしてOS-only runtime (AL2 or AL2023)を指定し、コードのバンドルにLLRTのバイナリ (bootstrapファイル) を含めることが必要です。バイナリはGitHubのリリースからダウンロードできます。

コードは例えば以下のesbuildコマンドでバンドルすると良いようです。 一部のライブラリ (主にAWS SDK関連) はLLRTランタイムに同梱されているため、バンドルに含める必要がありません。

esbuild index.js --platform=node --target=es2020 --format=esm --bundle --minify --external:@aws-sdk --external:@smithy --external:uuid

上記の手間を省くため、LLRTのLambda関数をデプロイするCDKコンストラクトを公開しました。簡単に使えるはずなので、お試しください。

GitHub - tmokmss/cdk-lambda-llrt: Experiment on LLRT easily with CDK

import { LlrtFunction } from 'cdk-lambda-llrt';

const handler = new LlrtFunction(this, 'Handler', {
    entry: 'lambda/index.ts',
});

ちなみに、ローカル環境でも同様にLLRTでJavaScriptを実行できます。

# release https://github.com/awslabs/llrt/releases から対応するバイナリをダウンロードする
wget https://github.com/awslabs/llrt/releases/download/v0.1.7-beta/llrt-darwin-arm64.zip
unzip llrt-darwin-arm64.zip

./llrt -h # help 表示

./llrt -e "console.log('OK')" # ワンライナーを実行

echo "console.log('OK')" > index.js
./llrt index.js #  ファイルから実行

LLRTの使い所

最後に、LLRTの使い所を考察します。

注意: 以降は個人的な感想を多分に含みますし、特に結論もでていません。

まず考慮のポイントは挙げると、以下でしょうか:

  • 制約
    1. (少なくとも現状は)Node.jsとの互換性は限定されたもので、多くのNode.js向けライブラリは動作しない
  • 強み
    1. コールドスタート時間の短さ
    2. 実行速度の速さ
    3. 1, 2によるユーザー体験の向上
    4. 2によるコスト削減効果
  • 弱み
    1. JITコンパイルがないことによる性能低下の可能性
    2. 互換性を検証するコスト

強み4を活かすには、それなりの規模で実行されている関数が良さそうです。さもなくば、弱み2のコストを上回るほどのメリットは享受できないためです。

また、強み3を考えると、エンドユーザーのリクエストに関わるLambda (API Gatewayのプロキシ先など)が良いでしょう。エンドユーザーと関わりのない部分においては、コールドスタート時間が数百ms程度縮んだところでメリットは大きくないためです。

ただし強み2を考えるなら、サーバーレスのイベント連携で生じがちな、イベントを受け渡し・加工・AWS API呼び出しだけのLambdaにも向いているかもしれません。このような基盤部分で実行時間を削減できると、チリツモで大きな効果が得られる場合もあるためです。その場合は、弱み1を意識して、何度も呼び出されたら結局JIT付きランタイムのほうが速くなる可能性も検証したいところです。

制約1は重要で、LLRTを使うにはLambda特化で薄く実装されたコードが向いてそうです。現時点の対応状況を見る限りでは、いっそAWS SDKのみを利用するコードに用途を絞るべきかもしれません。

そうなると、もし昨今流行りのLambdalithのような実装をしている場合は、部分的に切り出す作業が必要そうですね。 どうせ処理を切り出すのであれば、その部分だけ速い言語 (Rustなど) で書き換えるという選択も視野に入るため、

  1. LLRTに載せ替え: JavaScriptはそのまま使える (pro) が、互換性を検証する必要がある(con)
  2. 速い言語で書き換え: 新しい言語を使う必要がある (con) が、その言語においてはスタンダードな方法に乗っかれる (pro)

という2つの道のPros/consを検討することになるかもしれません。

上記のような点を考慮しながら、既存システムのパフォーマンスを観測し、LLRTという新しい道具を意識して使い所を探せば、見えてくることもあるのではないでしょうか。今後報告されてくるであろう利用事例に注視したいです。

まとめ

LLRTという新しいJavaScriptランタイムを紹介しました。AWS Lambdaユーザーにとっては面白い道具になりえるため、要チェックです。