AWS Java Lambda 与环境变量

一句话概要:对 Lambda 环境变量的任何改动都会引起一次 Lambda 的冷启动,大可放心在 handleRequest(...) 方法外使用环境变量。


AWS 上 Java Lambda 应用记要 中,我学到了 Lambda 的实例是跨请求共享的,所以为使用 Lambda 配置的环境变量时曾写出了下面复杂而多余的  AWS  Lambda 代码:
 1public class Handler implements RequestHandler<SNSEvent, String> {
 2
 3    private int threadPoolSize = getThreadPoolSizeFromEnv();
 4    private ExecutorService threadPool = Executors.newFixedThreadPool(threadPoolSize);
 5
 6    @Override
 7    public String handleRequest(SNSEvent snsEvent, Context context) {
 8        int configuredThreadPoolSize = getThreadPoolSizeFromEnv();
 9        if(configuredThreadPoolSize != threadPoolSize) {
10        threadPoolSize = configuredThreadPoolSize;
11        threadPool = Executors.newFixedThreadPool(threadPoolSize);
12    }
13
14    return "Hello Lambda";
15    }
16
17    private int getThreadPoolSizeFromEnv() {
18        return Integer.parseInt(System.getenv().getOrDefault("threadpool_size", "50"));
19    }
20}
这段代码看起来很在理,既然 Lambda 实例是共享的,那么在必变环境变量之后就可能不会重新初始始化实例,所以在每次的请求方法中对比如果环境变量值改动了就重新用最新的配置值来初始化线程池。然而上面的代码结结实实是多余的,真是把 Lambda 想得太简单了,如果是很多环境变量岂不是逐一判断。

那要怎么改上面的代码呢?给出答案之前先了解一下 Lambda  什么时候会冷启动(即得到新的 Lambda 实例),有几下几种情况

  1. 有段时间 Lambda 没被调用了会被 AWS 关掉相应的微服务,再次请求时会冷启动
  2. 重新部署了代码,当然要冷启动
  3. 任何环境变量的改动都会引起冷启动(测试用途,产品版本的环境变量是不可变的)。对环境变量有变动时,会显示出 Save and test 按钮来,相当于是一次无代码更新的重新部署,所以也会引起冷启动。

基于上面的第三点,前面的代码只需要按常规书写方式即可
1public class Handler implements RequestHandler<SNSEvent, String> {
2    private ExecutorService threadPool = Executors.newFixedThreadPool(
3        Integer.parseInt(System.getenv().getOrDefault("threadpool_size", "50")));
4    @Override
5    public String handleRequest(SNSEvent snsEvent, Context context) {
6        //使用线程池 threadPool
7        return "Hello Lambda";
8    }
9}
在每次修改环境配置后即会重新初始化 Lambda 实例以应用新的环境变量。

得到这个结果可不是 Google 搜来的,而是经过自己的试验而来。实验中有一个误导人的地方就是,当在 handleRequest(...) 方法中写上
System.out.println("Lambda instance: " + this);
只要 Lambda 部署了,再怎么重新部署或修改环境变量,上面的输出都是一样的,类似
Lambda instance com.serverless.Handler@614ddd49
根据以往的经验是 @614ddd49 不变的话就代码是同一个 Handler 实例,可对于 AWS Lambda 不是那么回事,仿佛是只要 Lambda 部署了, Handler.toString() 方法的返回值是个常量,所以我们不能以 Handler.this 的输出 Hash 值来分辨是否同一个 Lambda 实例了。
于是我们引入了一个实例变量来识别是否当前例,本次测试代码如下:
 1public class Handler implements RequestHandler<SNSEvent, String> {
 2    private int count = 0;
 3    private String bucket = System.getenv("bucket_name");
 4    public Handler() {
 5        System.out.println("Initialize Handler");
 6    }
 7    @Override
 8    public String handleRequest(SNSEvent snsEvent, Context context) {
 9        System.out.println("Lambda instance " + this.toString() + ", bucket: " + bucket);
10        System.out.println("Running on: " + getHostname() + ", count: " + (++count));
11        return context.getFunctionName();
12    }
13    private String getHostname() {
14        try {
15            return InetAddress.getLocalHost().getCanonicalHostName();
16        } catch (UnknownHostException e) {
17            return null;
18        }
19    }
20}
在 Lambda 的管理界面连续的点击 Test 按钮,我们可以看到 count 一直在累加,而且运行的主机不会变,执行也很快,下面是从 CloudWatch 上某一个 Log Stream 中抓下来的日志
Initialize Handler
START RequestId: ef3b6a59-1371-11e7-9891-4598f05eb0df Version: $LATEST
Lambda instance com.serverless.Handler@614ddd49, bucket: bucket_10880
Running on: ip-10-18-140-183.ec2.internal, count: 1
END RequestId: ef3b6a59-1371-11e7-9891-4598f05eb0df
REPORT RequestId: ef3b6a59-1371-11e7-9891-4598f05eb0df  Duration: 61.90 ms  Billed Duration: 100 ms Memory Size: 1024 MB    Max Memory Used: 44 MB
START RequestId: f1e024ac-1371-11e7-9b87-95c1c6f8d21c Version: $LATEST
Lambda instance com.serverless.Handler@614ddd49, bucket: bucket_10880
Running on: ip-10-18-140-183.ec2.internal, count: 2
END RequestId: f1e024ac-1371-11e7-9b87-95c1c6f8d21c
REPORT RequestId: f1e024ac-1371-11e7-9b87-95c1c6f8d21c  Duration: 1.17 ms   Billed Duration: 100 ms Memory Size: 1024 MB    Max Memory Used: 44 MB
START RequestId: f2541c69-1371-11e7-a085-13d13aed510a Version: $LATEST
Lambda instance com.serverless.Handler@614ddd49, bucket: bucket_10880
Running on: ip-10-18-140-183.ec2.internal, count: 3
END RequestId: f2541c69-1371-11e7-a085-13d13aed510a
REPORT RequestId: f2541c69-1371-11e7-a085-13d13aed510a  Duration: 0.98 ms   Billed Duration: 100 ms Memory Size: 1024 MB    Max Memory Used: 44 MB
构造函数只调用了一次,第一次是冷启动(时间较长), 同一个实例(计数在不断的累加)
现在我们在 Environment variables 配置界面修改环境变量 bucket_name 的值后点击 Save and test 按钮,再点击 Test 按钮,我们会发现日志输出到了一个新的 Log Stream 上去了
Initialize Handler
START RequestId: c9501e6b-1372-11e7-b753-47a8db4486ee Version: $LATEST
Lambda instance com.serverless.Handler@614ddd49, bucket: test_bucket_11
Running on: ip-10-34-15-169.ec2.internal, count: 1
END RequestId: c9501e6b-1372-11e7-b753-47a8db4486ee
REPORT RequestId: c9501e6b-1372-11e7-b753-47a8db4486ee  Duration: 47.17 ms  Billed Duration: 100 ms Memory Size: 1024 MB    Max Memory Used: 64 MB
START RequestId: cc6a091c-1372-11e7-8c38-fb47cf5f9672 Version: $LATEST
Lambda instance com.serverless.Handler@614ddd49, bucket: test_bucket_11
Running on: ip-10-34-15-169.ec2.internal, count: 2
END RequestId: cc6a091c-1372-11e7-8c38-fb47cf5f9672
REPORT RequestId: cc6a091c-1372-11e7-8c38-fb47cf5f9672  Duration: 2.77 ms   Billed Duration: 100 ms Memory Size: 1024 MB    Max Memory Used: 64 MB
START RequestId: cd243120-1372-11e7-9d5d-bb808f11a3d8 Version: $LATEST
Lambda instance com.serverless.Handler@614ddd49, bucket: test_bucket_11
Running on: ip-10-34-15-169.ec2.internal, count: 3
END RequestId: cd243120-1372-11e7-9d5d-bb808f11a3d8
REPORT RequestId: cd243120-1372-11e7-9d5d-bb808f11a3d8  Duration: 13.98 ms  Billed Duration: 100 ms Memory Size: 1024 MB    Max Memory Used: 64 MB
CloudWatch 的每个 Log Stream 也是对就于一个 Lambda 实例, 从上面也看到了修改环境变量后调用了 Handler 的构造函数,运行主机也变了(也可能不变),计数器被复位了,同样的第一次是冷启动,慢。
从上面两个输出结果的对比,在 handleRequest(...) 中的 this 总是输出为
com.serverless.Handler@614ddd49
其实它们是两个不同的 Lambda 实例。相对于 Handler.this.toString() 的值,  context.getLogStreamName() 能更准确的标示出 Lambda 的实例来,输出是
2017/03/28/[$LATEST]94f1d47fbbc84f9aa3d54eb176d3a168
不仅是对用到的 bucket_name 的修改会引起 Lambda  的冷启动,而是任何环境变量的变动都会引起同样的效果。注意到一有环境变量的改动,那个 Save and test 就会蹦出来,我们应该这样思考: 只要是需要 Save 的情况就代表是 Lambda 有变,接下来需要一个冷启动。