在 Lambda 中创建 AmazonS3Client 时出现 OutOfMemoryError

OutOfMemoryError when creating AmazonS3Client in Lambda

我有一个 AWS Lambda 函数,配置只有 128MB 内存,由 SNS 触发(它本身由 S3 触发)并将从 S3 下载文件。

在我的函数中,我有以下内容:

public class LambdaHandler {

    private final AmazonS3Client s3Client = new AmazonS3Client();

    public void gdeltHandler(SNSEvent event, Context context) {
        System.out.println("Starting");
        System.out.println("Found " + eventFiles.size() + " event files");
    }

我已将所有逻辑注释掉并从 post 中排除,因为我收到了一个 OutOfMemoryError,我已将其与 AmazonS3Client 对象的创建隔离开来。当我取出那个物体时,我没有得到错误。上面的代码会导致 OutOfMemoryError。

我为该函数分配了 128MB 的内存,这真的不足以简单地获取凭据并实例化 AmazonS3Client 对象吗?

我试过给 AmazonS3Client 构造函数

new EnvironmentVariableCredentialsProvider()

以及

new InstanceProfileCredentialsProvider()

结果相似。

创建 AmazonS3Client 对象是否只需要更多内存?

下面是堆栈跟踪:

Metaspace: java.lang.OutOfMemoryError java.lang.OutOfMemoryError: Metaspace at com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder.build(BeanDeserializerBuilder.java:347) at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:242) at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:143) at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:409) at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:358) at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:265) at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:245) at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:143) at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:439) at com.fasterxml.jackson.databind.ObjectReader._prefetchRootDeserializer(ObjectReader.java:1588) at com.fasterxml.jackson.databind.ObjectReader.(ObjectReader.java:185) at com.fasterxml.jackson.databind.ObjectMapper._newReader(ObjectMapper.java:558) at com.fasterxml.jackson.databind.ObjectMapper.reader(ObjectMapper.java:3108)

当我尝试提供 InstanceProfileCredentialsProvider 或 EnvironmentVariableCredentialsProvider 时,我得到以下堆栈跟踪:

Exception in thread "main" java.lang.Error: java.lang.OutOfMemoryError: Metaspace at lambdainternal.AWSLambda.(AWSLambda.java:62) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:94) Caused by: java.lang.OutOfMemoryError: Metaspace at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) at java.net.URLClassLoader.access0(URLClassLoader.java:73) at java.net.URLClassLoader.run(URLClassLoader.java:368) at java.net.URLClassLoader.run(URLClassLoader.java:362) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:361) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.makeRequestHandler(EventHandlerLoader.java:421) at lambdainternal.EventHandlerLoader.getTwoLengthHandler(EventHandlerLoader.java:777) at lambdainternal.EventHandlerLoader.getHandlerFromOverload(EventHandlerLoader.java:802) at lambdainternal.EventHandlerLoader.loadEventPojoHandler(EventHandlerLoader.java:888) at lambdainternal.EventHandlerLoader.loadEventHandler(EventHandlerLoader.java:740) at lambdainternal.AWSLambda.findUserMethodsImmediate(AWSLambda.java:126) at lambdainternal.AWSLambda.findUserMethods(AWSLambda.java:71) at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:219) at lambdainternal.AWSLambda.(AWSLambda.java:60) ... 3 more START RequestId: 58837136-483e-11e6-9ed3-39246839616a Version: $LATEST END RequestId: 58837136-483e-11e6-9ed3-39246839616a REPORT RequestId: 58837136-483e-11e6-9ed3-39246839616a Duration: 15002.92 ms Billed Duration: 15000 ms Memory Size: 128 MB Max Memory Used: 50 MB
2016-07-12T14:40:28.048Z 58837136-483e-11e6-9ed3-39246839616a Task timed out after 15.00 seconds

编辑 1 如果我将分配给函数的内存增加到 192MB,它工作得很好,但奇怪的是,在 cloudwatch 日志中报告只使用了 59MB 的内存。我只是失去了剩下的记忆吗?

我在 Lambda 函数中使用 AWS Java SDK 时一直观察到这一点。 看起来在创建任何 AWS 客户端(同步或异步)时您可能会退出 Metaspace。

我认为这是由于 Amazon 客户端在实例化时执行的操作,包括 AmazonHttpClient 创建以及请求处理程序链的动态加载(AmazonEc2Client#init() 私有方法的一部分)。

报告的内存使用情况可能是针对堆本身的,但可能不包括元空间。 AWS 论坛上有几个主题,但 AWS 没有对此事作出回应。

尝试将分配给 lambda 的内存从 128 MB 增加到 256 MB

减少冷启动的一种方法是将内存设置为 1536 mb 并将超时设置为 15 分钟。这将为 运行 只有您的 lambda 提供专用主机,而不是 运行 在共享主机上设置您的 lambda + 当必须启动新实例时,它将从主机上的缓存中复制代码而不是复制来自 S3.

不过这样做会更贵,如果您不想这样做,请继续阅读下文。

如何减少冷启动时间?

  1. 遵循 Lambda 最佳实践
    https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html

  2. 通过为您的函数选择更大的内存设置
    将内存视为“电源”设置,因为它还决定了您的函数将接收多少 CPU。

  3. 通过减小 ZIP 函数的大小
    这可能意味着减少您在函数 ZIP 中包含的依赖项的数量。 Java 使用 ProGuard 可以进一步减小 JAR 的大小

  4. [Java Only] 使用字节流接口,而不是 POJO 接口。
    Lambda 内部使用的 JSON 序列化库可能需要一些时间才能启动。这将需要您进行开发工作,但您可以通过使用字节流接口和轻量级 JSON 库来改进这一点。以下是一些可能有帮助的链接: http://docs.aws.amazon.com/lambda/latest/dg/java-handler-io-type-stream.html https://github.com/FasterXML/jackson-jr

  5. [Java Only] Don’t use Java 8 feature that replaces anonymous 类 (lambdas, method references, constructor references等)
    我们在内部注意到 Java 8 Lambda 相关的字节码似乎会导致次优的启动性能。如果您的代码使用任何替代匿名 类 的 Java 8 功能(lambda、方法引用、构造函数引用等),您可以通过返回匿名 类 来获得更好的启动时间。

  6. 使用不同的运行时间
    不同的运行时间有不同的冷启动时间,不同的运行时间性能。虽然 NodeJS 可能更适合繁重的 IO 工作,但 Go 可能更适合执行大量并发工作的代码。客户已经完成了一些基本的基准测试来比较 Lambda 上的语言性能,这里是对不同编程语言性能的更通用的比较。没有放之四海而皆准的答案,请根据您的要求选择合适的答案。

基本基准:https://read.acloud.guru/comparing-aws-lambda-performance-of-node-js-python-java-c-and-go-29c1163c2581

一般比较:https://benchmarksgame-team.pages.debian.net/benchmarksgame/which-programs-are-fast.html

我使用了一种有助于基于 Java 的 lambda 的策略。任何只需要单个(可重用)实例的 class 资源都可以声明为 static class 成员,并在静态初始化块中初始化。当 lambda 创建 class 的新实例来处理执行时,那些昂贵的资源已经初始化。这是一个简单的例子:

package com.mydomain.myapp.lambda.sqs;

import com.amazonaws.services.lambda.runtime.events.SQSEvent;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClientBuilder;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;

public class MyLambdaFunctionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyLambdaFunctionHandler.class);

    // These values come from the 'Environment' property for the lambda, defined in template.yaml
    private static final String ENV_NAME = System.getenv("ENV_NAME");

    // Declare these as static properties so they only need to be created once,
    // rather than on each invocation of the lambda handler method that uses them
    private static final ObjectMapper OBJECT_MAPPER;
    private static final AmazonSNS SNS;
    private static final AmazonSQS SQS;

    static {
        LOGGER.info("static initializer | START");
        Objects.requireNonNull(ENV_NAME, "ENV_NAME cannot be null");
        OBJECT_MAPPER = new ObjectMapper();
        SNS = AmazonSNSClientBuilder.defaultClient();
        SQS = AmazonSQSClientBuilder.defaultClient();
        LOGGER.info("static initializer | END");
    }

    public MyLambdaFunctionHandler() {
        LOGGER.info("constructor invoked");
    }

    public void handlerMethod(SQSEvent event) {
        LOGGER.info("Received SQSEvent with {} messages", event.getRecords().size());
        event.getRecords().forEach(message -> handleOneSQSMessage(message));
    }

    private void handleOneSQSMessage(SQSEvent.SQSMessage message) {
        // your SQS message handling code here...
    }

}

我声明为静态的属性将保留在内存中,直到 lambda 实例被 AWS 销毁。

这不是我通常编写 Java 代码的方式。基于 Lambda 的代码被区别对待,所以我认为在这里打破一些传统模式是可以的。