Lambda Pythonが楽しい季節になって参りました。
クラスを書いていると変わった挙動に出会ったので、軽く調べてメモしておきます。
クラス変数の挙動試験
キッカケ
最初は、Pythonにおけるクラス変数とインスタンス変数の関係的なものを知らず、変数の初期化の役割をクラス変数にやらせていました。それが、どうもLambdaで実行するとクラス変数の値の変更がキャッシュされてしまうことがあるようで、そうでないこともあるようで、色々と不審に思って調べ始めました。
テストコード
内容はこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import os import time from socket import gethostname def lambda_handler(event, context): test = CacheTest() test.update() class CacheTest: int = 1 list = [] tuple = () dict = {} def __init__(self): print "hostname:",gethostname() print " int:",self.int, " (%s)" % hex(id(self.int)) print " list:",self.list, "(%s)" % hex(id(self.list)) print "tuple:",self.tuple,"(%s)" % hex(id(self.tuple)) print " dict:",self.dict, "(%s)" % hex(id(self.dict)) def update(self): now = int(time.time()) self.int += 1 self.list.append(now) self.tuple += (now,) self.dict[now] = True |
通常のLinuxでの実行結果
お手元のテスト環境で lambda_handler() を1つ書いてを数回pythonコマンドで実行すると、出力内容はこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
hostname: gedowfather-centos7-01 int: 1 (0x1614c68) list: [] (0x7f2e3303ee18) tuple: () (0x7f2e33119050) dict: {} (0x16d58f0) hostname: gedowfather-centos7-01 int: 1 (0x996c68) list: [] (0x7f5d57017e18) tuple: () (0x7f5d570f2050) dict: {} (0xa578f0) hostname: gedowfather-centos7-01 int: 1 (0x255dc68) list: [] (0x7f5b0298be18) tuple: () (0x7f5b02a66050) dict: {} (0x261e8f0) |
当然、クラス変数は毎回初期化されます。
Lambdaでの実行結果
これがLambdaで実行すると、こうなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
hostname: ip-10-0-224-247 int: 1 (0x1387158) list: [] (0x7f65ed462200) tuple: () (0x7f65ed562050) dict: {} (0x7f65e7c70a28) hostname: ip-10-0-224-247 int: 1 (0x1387158) list: [1449641688] (0x7f65ed462200) tuple: () (0x7f65ed562050) dict: {1449641688: True} (0x7f65e7c70a28) hostname: ip-10-0-108-6 int: 1 (0xfa6158) list: [] (0x7fc2f17213f8) tuple: () (0x7fc2f186e050) dict: {} (0x7fc2ebf7ca28) hostname: ip-10-0-108-6 int: 1 (0xfa6158) list: [1449641714] (0x7fc2f17213f8) tuple: () (0x7fc2f186e050) dict: {1449641714: True} (0x7fc2ebf7ca28) hostname: ip-10-0-224-247 int: 1 (0x1387158) list: [1449641688, 1449641701] (0x7f65ed462200) tuple: () (0x7f65ed562050) dict: {1449641688: True, 1449641701: True} (0x7f65e7c70a28) hostname: ip-10-0-224-247 int: 1 (0x1387158) list: [1449641688, 1449641701, 1449641743] (0x7f65ed462200) tuple: () (0x7f65ed562050) dict: {1449641688: True, 1449641701: True, 1449641743: True} (0x7f65e7c70a28) |
これからわかることは、このような感じ。
コード更新後の挙動
コードを更新すると、結果はこうなります。
1 2 3 4 5 |
hostname: ip-10-0-224-247 int: 1 (0x191a158) list: [] (0x7fc5d12ab440) tuple: () (0x7fc5d13f8050) dict: {} (0x7fc5cbb06a28) |
プロセスを作りなおしたっぽいのがわかります。
原因と対策
ここまできて、ようやく公式で見つけたのが、Q: AWS Lambda は関数インスタンスを再利用しますか?
パフォーマンス向上のため、AWS Lambda は新しく関数のインスタンスを作成するのではなく、関数のインスタンスを保持してその後のリクエストに対応することがあります。ただし、常にインスタンスを再利用するわけではありません。
ということで、内部ではプロセスを保持したまま lambda_handler() を何度も実行しているだけで、そのため残る仕様のものは残っている、というだけのことでした。新規に関数インスタンスを作成するのは、コード更新時は確実で、他にも時間経過とかあるかもしれませんが不明です。
ちなみに、お手元Linuxで1処理の中で複数回 lambda_handler() を実行しても、以下のようにリストと辞書のクラス変数は値が残ったので、この仕様はPython独特のものなのでしょう。(Python初心者なのでこれ以上は突っ込まないようにしますが……)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
hostname: gedowfather-centos7-01 int: 1 (0xf04c68) list: [] (0x7f0008d79e18) tuple: () (0x7f0008e54050) dict: {} (0xfc7920) hostname: gedowfather-centos7-01 int: 1 (0xf04c68) list: [1449715275] (0x7f0008d79e18) tuple: () (0x7f0008e54050) dict: {1449715275: True} (0xfc7920) hostname: gedowfather-centos7-01 int: 1 (0xf04c68) list: [1449715275, 1449715275] (0x7f0008d79e18) tuple: () (0x7f0008e54050) dict: {1449715275: True} (0xfc7920) |
Pythonではクラス変数は不変のものとして扱い、変数の初期化は __init__() のインスタンス変数でやるのが基本のようなことをどこかで見たので、そのように書いていけば特に問題は出ないかと思います。
データキャッシュ
この挙動を突いて、外部からもってきたデータとかキャッシュできるやん☆って一瞬考えましたが、そんな黒魔術はやめたほうがいいですよね~ってことで、ついでによくある質問にあった /tmp について。Q: AWS Lambda 関数のためにディスクにスクラッチスペースが必要な場合はどうすればよいですか?
各 Lambda 関数では /tmp ディレクトリに 500 MB の容量を割り当てることができます。
500MBも使えるので、おとなしくここでmtimeでの読み書きキャッシュすることにしましょうかね、と。複数ホストで実行されるため、台数分はキャッシュを作ることになりますが、オンデマンド価格JSONなど、作り直しのタイミングがさほど重要じゃないものには十分でしょう。
まとめると、『よくある質問』はちゃんと全部読みましょうってことです、ハイ。