【问题标题】:OutOfMemoryError when creating AmazonS3Client in Lambda在 Lambda 中创建 AmazonS3Client 时出现 OutOfMemoryError
【发布时间】:2016-11-15 11:52:20
【问题描述】:

我有一个 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");
    }

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

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

我试过给 AmazonS3Client 构造函数

new EnvironmentVariableCredentialsProvider()

还有

new InstanceProfileCredentialsProvider()

结果相似。

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

下面是堆栈跟踪:

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

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

线程“主”java.lang.Error 中的异常: java.lang.OutOfMemoryError:元空间在 lambdainternal.AWSLambda.(AWSLambda.java:62) 在 java.lang.Class.forName0(本机方法)在 java.lang.Class.forName(Class.java:348) 在 lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:94) 原因: java.lang.OutOfMemoryError:元空间在 java.lang.ClassLoader.defineClass1(本机方法)在 java.lang.ClassLoader.defineClass(ClassLoader.java:763) 在 java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) 在 java.net.URLClassLoader.defineClass(URLClassLoader.java:467) 在 java.net.URLClassLoader.access$100(URLClassLoader.java:73) 在 java.net.URLClassLoader$1.run(URLClassLoader.java:368) 在 java.net.URLClassLoader$1.run(URLClassLoader.java:362) 在 java.security.AccessController.doPrivileged(Native Method) 在 java.net.URLClassLoader.findClass(URLClassLoader.java:361) 在 java.lang.ClassLoader.loadClass(ClassLoader.java:424) 在 java.lang.ClassLoader.loadClass(ClassLoader.java:357) 在 lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.makeRequestHandler(EventHandlerLoader.java:421) 在 lambdainternal.EventHandlerLoader.getTwoLengthHandler(EventHandlerLoader.java:777) 在 lambdainternal.EventHandlerLoader.getHandlerFromOverload(EventHandlerLoader.java:802) 在 lambdainternal.EventHandlerLoader.loadEventPojoHandler(EventHandlerLoader.java:888) 在 lambdainternal.EventHandlerLoader.loadEventHandler(EventHandlerLoader.java:740) 在 lambdainternal.AWSLambda.findUserMethodsImmediate(AWSLambda.java:126) 在 lambdainternal.AWSLambda.findUserMethods(AWSLambda.java:71) 在 lambdainternal.AWSLambda.startRuntime(AWSLambda.java:219) 在 lambdainternal.AWSLambda.(AWSLambda.java:60) ... 还有 3 个开始 RequestId:58837136-483e-11e6-9ed3-39246839616a 版本:$LATEST END 请求 ID:58837136-483e-11e6-9ed3-39246839616a 报告请求 ID: 58837136-483e-11e6-9ed3-39246839616a 持续时间:15002.92 毫秒 持续时间:15000 毫秒内存大小:128 MB 使用的最大内存:50 MB
2016-07-12T14:40:28.048Z 58837136-483e-11e6-9ed3-39246839616a 任务 15.00 秒后超时

编辑 1 如果我将分配给函数的内存增加到 192MB,它就可以正常工作,尽管很奇怪,在 cloudwatch 日志中报告只使用了 59MB 的内存。我只是失去了其余的记忆吗?

【问题讨论】:

  • 您找到解决方案了吗?我认为元内存部分由于杰克逊导致的类加载而过载。元空间是总内存的百分比,因此如果增加总 jvm 内存,元空间将在抛出 OutOfMemoryError 之前获得更多内存。如果可以只增加内存的元空间部分,那就太好了。 (-XX:MaxMetaspaceSize=512m) 如果可能的话,另一个解决方案可能是调整杰克逊?元空间说明:plumbr.eu/outofmemoryerror/metaspace
  • 没有我知道的解决方案...
  • FWIW,看起来可以将 JAVA_TOOL_OPTIONS 环境变量传递给 Java 8 lambda

标签: amazon-web-services amazon-s3 aws-lambda


【解决方案1】:

在 Lambda 函数中使用 AWS Java SDK 时,我一直观察到这一点。 在创建任何 AWS 客户端(同步或异步)时,您可能会脱离元空间。

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

报告的内存使用可能是针对堆本身的,但可能不包括元空间。 AWS 论坛上有一些帖子,但 AWS 没有对此事作出回应。

【讨论】:

  • 我注意到的另一件事是,在分配 192MB 的情况下,它工作得很好,但是虽然第一次 Lambda 执行需要 10 多秒才能启动并实例化 AmazonS3Client,但后续执行会在 1 小时内完成用不到 1 秒的时间来实例化 AmazonS3Client ...假设它缓存了身份验证?然而,1 小时后,它似乎删除了缓存并再次重新进行身份验证,超过 10 秒。
  • 是的,似乎有一些保持活动的机制。我没有时间进一步深入研究,但我正在寻找一种降低内存占用的方法。可能是通过删除一些实际上没有使用的依赖项,即使没有标记为“可选”。
  • 我遇到了同样的问题,但使用的是 SNS 客户端。报告的内存使用量远低于 128MB,但我仍然收到“OutOfMemoryError: Metaspace”。增加到 192MB 似乎已经“解决”了这个问题,但如果知道这里到底发生了什么会很好。
【解决方案2】:

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

【讨论】:

  • 请注意我对原始问题所做的编辑。增加内存修复了它,但提出了一个更大的问题。
【解决方案3】:

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

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

如何减少冷启动时间?

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

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

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

  4. [仅限 Java] 使用 bytestream 接口而不是 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] 不要使用 Java 8 替代匿名类(lambda、方法引用、构造函数引用等)的功能
    我们在内部注意到,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

【讨论】:

    【解决方案4】:

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

    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 的代码被区别对待,所以我觉得在这里打破一些传统模式是可以的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-14
      • 2017-12-12
      相关资源
      最近更新 更多