AWSをいじるようになりLambdaに手を出したところ、盛大にハマった話です。
どこでハマったか
API Gatewayを利用し、Webから非同期でLambdaを呼びだそうとしたところ、盛大にハマりました。
まずはわかる人向けの結論
API GatewayからLambdaを呼び、そのLambda上からLambdaを非同期呼出することで解決。
本編
Lambdaって?
ランバダ?- ラムダと読みます
- コードをAWSクラウド上で実行してくれる
- 対応言語はNodeJS, Java, Python
- AWSクラウド上の色々な所で実行トリガーを設定できる
- 例えばS3へのファイルアップロード
- DynamoDBのテーブル更新時 (insert, update, ...)
例えば
- S3に画像がアップロードされた瞬間、サムネイル画像を生成する
- S3にログファイルがアップされたら、即座にファイルの解析を行う
- DynamoDBにレコードがインサートされたら何かをする~~(利用シーン思いつかず)~~
デメリット
- 300秒以上かかる処理はできない(2016/06月現在)
- メモリ1.5G以上消費する処理はつらい
- Javaが遅い(らしい)
メリット
- EC2いらない
- 安い
- スケーラブル
- 関数に対して割り当てるメモリを調整できる(128M ~ 1536M)
- CPUはメモリに比例して勝手に強くなる
- オンデマンド(https経由)からも呼べる
何ができるのか
- クライアント(ブラウザ等)が直接Lambdaを起動できる
- GET, POST等でデータを送信できる
- そしてそのデータ(Payload)に対して/応じてごにょごにょできる
但し、HTTPS経由でのLambda関数の起動はAWS Lambda単体では実現できないのでAWS API Gatewayと連携します。
API Gatewayとは
- LambdaをHTTPS経由で呼び出せるようにするやつ
- 詳細はこちら
- これとLambdaをうまく組み合わせて準備完了
で、何をしたいのか
- ブラウザからPOSTしたデータをS3に書き出したい
- けど書き出している時間は待ちたくない
- 要はポストするだけして即座に"200 OK"が欲しい
- 厳密にいうと202
理想のイメージ
まずは普通にやってみる
今回はNode.jsを採用しました。exports.handler = function() {}
に渡されているevent
にPOSTされたpayloadが入ってます。
'use strict';
var AWS_ACCESS_KEY_ID = 'ACCESS_KEY_HERE';
var AWS_SECRET_ACCESS_KEY = 'SECRET_ACCESS_KEY_HERE';
var S3_BUCKET_NAME = 'target.bucket.name';
var AWS = require('aws-sdk');
AWS.config.update({
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
region: 'ap-northeast-1'
});
exports.handler = function(event, context, callback) {
var params = {
Bucket: S3_BUCKET_NAME,
Key: 'payload.json',
Body: JSON.stringify(event),
ContentType: 'application/json; charset=utf-8',
ACL: 'public-read'
};
s3.putObject(params, function(err, data) {
if (err) {
context.done(err);
} else {
context.done(null, 'Successfully uploaded data.');
}
});
};
上記のコード管理画面内にある「API endpoints」タブよりエンドポイントURLを作成することができるので、そこから生成したURLをレストクライアント等から叩いてみます。
無事成功したので、ここから非同期化を目指します。
非同期化へのアプローチ
- 無思考的に
setTimeout()
を試す - Node.jsのchild_processを使う
- API Gatewayの統合リクエストヘッダーに
X-Amz-Invocation-Type
を追加する - LambdaからLambdaを呼び出す
setTimeout();
setTimeout(function() {
s3.putObject(params, function(err, data) {
if (err) {
context.done(err);
} else {
context.done(null, 'Successfully uploaded data.');
}
});
}, 0);
- S3へのアップロード部分の実行を遅延させてみる
-
setTimeout()
は実行キューへの登録が遅延されるだけで、結局同一スレッドで実行されるので意味なし - (おさらい)Javascriptはシングルスレッド
ちなみに
setTimeout()
後にプロセスを強制終了してみると...プロセスが一瞬で終わります。
当然setTimeout()
内の処理も実行されません。
setTimeout(function() {
s3.putObject(params, function(err, data) {
if (err) {
context.done(err);
} else {
context.done(null, 'Successfully uploaded data.');
}
});
}, 0);
process.exit();
// 即オワタ\(^o^)/
child_process
- Node.jsはchild_processモジュールを通じて親プロセス(メインストリーム)から子プロセスを生成することができる
- そして子プロセスは親プロセスから切り離す事ができる!
何が嬉しいのか
- 子プロセスを親から切り離すことによって、親プロセスが死んでも子プロセスは動き続ける事ができるようになる。
- 更にこの状態で子が親に対して、自分の処理終了を待たないように宣言することができる。
- 通常は切り離したとしても親は子の終了を待つ
実際のコード
'use strict';
exports.handler = function(event, context, callback) {
var spawn = require('child_process').spawn;
spawn('node', ['longlongProcess.js'], {
stdio: 'ignore',
detached: true
}).unref();
}
-
spawn
でOSコマンドを実行できる -
detached: true
が切り離し命令 -
unref()
することで子の終了を待機しなくなる - これで勝つる!(フラグ)
...それLambdaで出来無いよ
Lambdaではメインストリームの処理が終了した瞬間、派生する処理は全て殺されます。なので前述の例でいうとunref()
が実行された瞬間、親プロセスは以降の行に処理が無いのでそこで終了します。
=子も終了します。
なので
下記の様なコードにしても同様、意図した動作になりません。
Lambdaでない環境であれば、detach
してる子プロセスは生き残り処理が続行されますが、Lambdaではprocess.exit()
によって子も殺されます。無念。。
'use strict';
exports.handler = function(event, context, callback) {
var spawn = require('child_process').spawn;
// unref()せず、直後に親を殺してみる
spawn('node', ['longlongProcess.js'], {
stdio: 'ignore',
detached: true
});
process.exit();
}
X-Amz-Invocation-Type
AWS API Gatewayの公式ドキュメントに下記のような記述があります。
これは要するに、API Gatewayの
追加して下さいということです。
しかし
やってはみるものの一向に非同期にならず、10秒スリープするようなスクリプトを呼ぶと、ご丁寧に10秒待たされる。
API GatewayのLambdaに関連しそうなドキュメントをひたすら読むも謎は解けず...
\ここで盛大にハマる/
一応試したことリスト
- メソッドリクエストから統合リクエストに対してヘッダーマッピング式をつかってマッピングしてみる
- ヘッダーのキーの大文字、小文字等を試してみる
- X-Amz-Invocation-Type
- XAmzInvocationType
- x-amz-invocation-type
- xaMziNvOCatIoNtYpE
(やけくそ)
- InvocationTypeのEventやらDryRunやらを試してみる
- POSTだのGETだのリクエストメソッドを変えてみる
時は流れ
ふと初心に戻りLambdaの公式ドキュメントを読み返していた所、衝撃の事実を発見。
Amazon API Gateway を使用して HTTPS 経由で Lambda 関数を呼び出す場合、Lambda は常に RequestResponse **(同期)**呼び出しタイプを使用します。
/(^o^)\
ということで最終手段
Lambda => Lambda
LambdaからLambdaを呼びます。
イメージ図はこんな感じです。
実際のコード
- Lambda上でaws-sdkを取得し、そこから更にLambdaを非同期呼出
-
lambda.invokeAsync()
がポイント -
InvokeArgs
には受け取ったpayload(event)をそのままぶん投げる
'use strict';
var AWS_ACCESS_KEY_ID = 'ACCESS_KEY_HERE';
var AWS_SECRET_ACCESS_KEY = 'SECRET_ACCESS_KEY_HERE';
var FUNCTION_NAME = 'nextLambdaFunction';
var AWS = require('aws-sdk');
AWS.config.update({
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
region: 'ap-northeast-1'
});
exports.handler = function(event, context, callback) {
var lambda = new AWS.Lambda();
var params = {
FunctionName: FUNCTION_NAME,
InvokeArgs: JSON.stringify(event)
};
lambda.invokeAsync(params, function(err, data) {
if (err) {
context.done(err);
} else {
context.done(null, data);
}
});
};
上記の構成、コードにてようやく非同期化に成功しました。
まとめ
- Lambdaをうまく活用すればEC2いらずで経済的!
- HTTPS経由の非同期呼び出しは工夫すればなんとかなる!
最後に
ドキュメント読もう。ちゃんと。