LoginSignup
18
17

More than 5 years have passed since last update.

Lambdaファンクションの関数インスタンスは再利用される

Last updated at Posted at 2016-09-04

Lambdaで環境変数っぽいことを実現するためにエイリアス名を使う方法があります。
http://qiita.com/mashiro/items/ab4cab5e623397d6b29d

しかし、ハマりどころがあるので書いておきます。

サンプル

devステージとproステージでDynamoDBのテーブルを切り替える例です。

  • configモジュールを使用
  • 環境変数NODE_ENVにdev,proという値を設定して切りかえ
    • Lambdaのコンテキスト変数からエイリアス名を取り出す
envtest/                                                                                                                                         
├── config                                                                                                                                       
│   ├── dev.js                                                                                                                                   
│   └── pro.js                                                                                                                                   
├── db.js                                                                                                                                        
├── index.js                                                                                                                                     
├── node_modules                                                                                                                                 
│   ├── co                                                                                                                                       
│   └── config                                                                                                                                   
└── package.json           
index.js
"use strict";
const co = require("co");
const db = require("./db");

exports.handle = function(e, ctx, cb) {
  const aliaseName = ctx.invokedFunctionArn.match(/([^:]+)$/)[1];
  process.env.NODE_ENV = aliaseName;
  console.log("NODE_ENV: ", process.env.NODE_ENV);
  co(function*(){
    return yield db.getFromDB();
  }).then(result => {
    cb(null, result);
  }).catch(err => {
    cb(err);
  });
};
db.js
const config = require("config");

const AWS = require("aws-sdk");
AWS.config.setPromisesDependency(Promise);
const DynamoDB = new AWS.DynamoDB.DocumentClient();

module.exports.getFromDB = function*() {
  console.log(`get from table ${config.tablePrefix}.Table`);
  return yield DynamoDB.get({
    TableName: `${config.tablePrefix}.Table`,
    Key: {
      key: "hogehoge"
    }
  }).promise();
};

動作

一見動きそうですが、このようなエラーになります。

$ apex invoke envtest -L -a dev                                                                      
START RequestId: d7c859e0-7284-11e6-92b7-4568188a1126 Version: $LATEST                                                                           
2016-09-04T09:49:16.384Z        d7c859e0-7284-11e6-92b7-4568188a1126    NODE_ENV:  dev                                                           
2016-09-04T09:49:16.384Z        d7c859e0-7284-11e6-92b7-4568188a1126    get from table undefined.Table                                           
2016-09-04T09:49:16.488Z        d7c859e0-7284-11e6-92b7-4568188a1126    {"errorMessage":"Requested resource not found","errorType":"ResourceNotFo
undException"

なぜでしょう? これはrequire("config")されるタイミングにあります。

index.js
const db = require("./db");

という部分がありますが、これにより芋づる式に、

db.js
const config = require("config");

ココが呼ばれます。

これはhandle関数の下記の部分よりも先に呼ばれてしまうのです。

index.js
exports.handle = function(e, ctx, cb) {
  const aliaseName = ctx.invokedFunctionArn.match(/([^:]+)$/)[1];
  process.env.NODE_ENV = aliaseName;
  console.log("NODE_ENV: ", process.env.NODE_ENV);

つまり、NODE_ENVにdevだとか、proだとかがセットされる前です。

修正案

これを解消するには、

index.js
"use strict";
const co = require("co");

exports.handle = function(e, ctx, cb) {
  const aliaseName = ctx.invokedFunctionArn.match(/([^:]+)$/)[1];
  process.env.NODE_ENV = aliaseName;
  console.log("NODE_ENV: ", process.env.NODE_ENV);
  co(function*(){
    const db = require("./db");     //ココに持ってくる
    return yield db.getFromDB();
  }).then(result => {
    cb(null, result);
  }).catch(err => {
    cb(err);
  });
};

こうなります。

$ apex invoke envtest -L -a dev
START RequestId: 8a0fbd26-7286-11e6-8438-af4d75d76e02 Version: $LATEST
2016-09-04T10:01:24.967Z    8a0fbd26-7286-11e6-8438-af4d75d76e02    NODE_ENV:  dev
2016-09-04T10:01:24.967Z    8a0fbd26-7286-11e6-8438-af4d75d76e02    get from table Sample.DEV.Table
END RequestId: 8a0fbd26-7286-11e6-8438-af4d75d76e02
REPORT RequestId: 8a0fbd26-7286-11e6-8438-af4d75d76e02  Duration: 67.24 ms  Billed Duration: 100 ms     Memory Size: 128 MBMax Memory Used: 26 MB   
{"Item":{"value":"DEV環境","key":"hogehoge"}}

できた気がしますね。

しかし、Lambdaファンクションの関数インスタンスは再利用されるのです

話はまだ終わりません。ここからが本当に書きたかったことです。

devもproもバージョン9を向ける

下記のようにdevもproもバージョン9を向けるようにしてみましょう。

aws lambda list-aliases --function-name sample_envtest
{
    "Aliases": [
        {
            "FunctionVersion": "9",
            "Name": "dev",
        },
        {
            "FunctionVersion": "9",
            "Name": "pro",
        }
    ]
}

実行結果

proを実行してみます。ふむ、期待通りですね

$ apex invoke envtest -L -a pro
START RequestId: b45a8038-7287-11e6-91a6-e3f26108a1b5 Version: 9
2016-09-04T10:09:45.389Z    b45a8038-7287-11e6-91a6-e3f26108a1b5    NODE_ENV:  pro
2016-09-04T10:09:45.390Z    b45a8038-7287-11e6-91a6-e3f26108a1b5    get from table Sample.PRO.Table
END RequestId: b45a8038-7287-11e6-91a6-e3f26108a1b5
REPORT RequestId: b45a8038-7287-11e6-91a6-e3f26108a1b5  Duration: 77.82 ms  Billed Duration: 100 ms     Memory Size: 128 MBMax Memory Used: 26 MB   
{"Item":{"value":"PRO環境","key":"hogehoge"}}

続いてdevを実行すると・・・

$ apex invoke envtest -L -a dev
START RequestId: dad8792f-7287-11e6-8961-2d198a82fe02 Version: 9
2016-09-04T10:10:49.968Z    dad8792f-7287-11e6-8961-2d198a82fe02    NODE_ENV:  dev
2016-09-04T10:10:49.969Z    dad8792f-7287-11e6-8961-2d198a82fe02    get from table Sample.PRO.Table
END RequestId: dad8792f-7287-11e6-8961-2d198a82fe02
REPORT RequestId: dad8792f-7287-11e6-8961-2d198a82fe02  Duration: 134.01 ms Billed Duration: 200 ms     Memory Size: 128 MBMax Memory Used: 26 MB   
{"Item":{"value":"PRO環境","key":"hogehoge"}}

おやおや? Sample.PRO.Tableを読んでしまっている!
NODE_ENV: devと出ているのに!

関数インスタンスの再利用問題

一度requireされたモジュールはキャッシュされるのはNode.jsを使う方なら周知の事実でしょう。
だからdb.jsでrequire("config")された内容はキャッシュされます。

私はLambdaはCGIのように実行されるたびにプロセス実行をするものだと思っていました。
しかし、この結果を見るに1回目にproとして実行されたプロセスが再利用されているように見えます。

これが「関数インスタンスが再利用される」というやつですね

devを$LATESTに向けてみる

aws lambda list-aliases --function-name sample_envtest
{
    "Aliases": [
        {
            "FunctionVersion": "$LATEST",
            "Name": "dev",
        },
        {
            "FunctionVersion": "9",
            "Name": "pro",
        }
    ]
}

実はこの環境では$LATEST==9なのですが、ともかくやってみます。

$ apex invoke envtest -L -a dev
START RequestId: b5ad53ae-7289-11e6-99c6-e397f9b62998 Version: $LATEST
2016-09-04T10:24:06.605Z    b5ad53ae-7289-11e6-99c6-e397f9b62998    NODE_ENV:  dev
2016-09-04T10:24:06.605Z    b5ad53ae-7289-11e6-99c6-e397f9b62998    get from table Sample.DEV.Table
END RequestId: b5ad53ae-7289-11e6-99c6-e397f9b62998
REPORT RequestId: b5ad53ae-7289-11e6-99c6-e397f9b62998  Duration: 101.35 ms Billed Duration: 200 ms     Memory Size: 128 MBMax Memory Used: 27 MB   
{"Item":{"value":"DEV環境","key":"hogehoge"}}

$ apex invoke envtest -L -a pro
START RequestId: b7771774-7289-11e6-a3ca-55d16d901553 Version: 9
2016-09-04T10:24:09.589Z    b7771774-7289-11e6-a3ca-55d16d901553    NODE_ENV:  pro
2016-09-04T10:24:09.589Z    b7771774-7289-11e6-a3ca-55d16d901553    get from table Sample.PRO.Table
END RequestId: b7771774-7289-11e6-a3ca-55d16d901553
REPORT RequestId: b7771774-7289-11e6-a3ca-55d16d901553  Duration: 46.22 ms  Billed Duration: 100 ms     Memory Size: 128 MBMax Memory Used: 26 MB   
{"Item":{"value":"PRO環境","key":"hogehoge"}}

なるほど。どうやらLambdaファンクションの関数インスタンスはバージョンごとに再利用されるようです。
ログに、

START RequestId: b5ad53ae-7289-11e6-99c6-e397f9b62998 Version: $LATEST
START RequestId: b7771774-7289-11e6-a3ca-55d16d901553 Version: 9

という出方をしているので、$LATESTと最新バージョは違うものとして扱われるようです。

まとめ

この挙動は注意しておかないとハマりそうです。Lambdaファンクションの内部実装がコンテナで、コンテナ起動時間の問題を軽減するためにある程度の単位で再利用されるというのは小耳に挟んでいました。

Q: AWS Lambda は関数インスタンスを再利用しますか?

パフォーマンス向上のため、AWS Lambda は新しく関数のインスタンスを作成するのではなく、関数のインスタンスを保持してその後のリクエストに対応することがあります。ただし、常にインスタンスを再利用するわけではありません。

が、こうなるというところまで頭が回っていませんでした。
回避するためにはconfigの内容がキャッシュされない方法で、configを毎回評価する必要があるでしょう。

大域変数であるprocess.env.NODE_ENVにdev,proをセットするのも怪しくなってきました。
同時invokeとLambdaファンクションのプロセスの関係がどうなるのかがまだわかりませんが、スレッドセーフでは無いかもしれません。

雑惑

apexには環境変数っぽいことをする機能があります。
apex deployするとこっそりとこんなファクションがかまされています。

try {
  var config = require('./.env.json')
  for (var key in config) {
    process.env[key] = config[key]
  }
} catch (err) {
  // ignore
}

exports.handle = require('./index').handle

これを素直に使ったほうが良いのかなと思うのですが、この機能を使うと、

バージョン番号
10 proの最新
9 devの最新

となってしまうのが嫌なのですよね。。。

18
17
0

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
18
17