在 AWS Lambda 中运行 Spring Boot 应用

1、概览

在本教程中,我们将探讨如何使用 Serverless Application Model (SAM) 框架将 Spring Boot 应用程序部署到 AWS Lambda。

这种方法有助于将现有的 API 服务器迁移到 serverless 上。

通过这种方法,我们可以利用 AWS Lambda 的可扩展性和按执行付费的定价模式,高效、经济地运行我们的应用程序。

2、理解 Lamdba

AWS Lambda 是亚马逊网络服务(AWS)提供的 serverless 计算服务。它允许我们在无需配置或管理服务器的情况下运行代码。

Lambda 函数与传统服务器的主要区别之一是,Lambda 函数由事件驱动,生命周期很短

Lambda 函数不像服务器那样持续运行,而是只在响应特定事件时才运行,例如 API 请求、队列中的消息或上传到 S3 的文件。

我们应该注意到,lambda 在处理第一个请求时需要一定的时间来启动。这就是所谓的 “冷启动”。

如果下一个请求在短时间内出现,可以使用相同的 lambda 运行时,这被称为 “热启动”。如果同时出现多个请求,则会启动多个 Lambda 运行时。

与 Lambda 理想的毫秒级启动时间相比,Spring Boot 的启动时间相对较长,因此我们会讨论这对性能的影响。

3、项目设置

我们通过修改 pom.xml 和添加一些配置来迁移现有的 Spring Boot 项目。

Spring Boot 支持的版本有 2.2.x、2.3.x、2.4.x、2.5.x、2.6.x 和 2.7.x。

3.1、Spring Boot API 示例

我们的应用程序由一个简单的 API 组成,它可以处理对 api/v1/users 端点的任何 GET 请求:

@RestController
@RequestMapping("/api/v1/")
public class ProfileController {

    @GetMapping(value = "users", produces = MediaType.APPLICATION_JSON_VALUE)
    public List<User> getUser() {
        return List.of(new User("John", "Doe", "john.doe@baeldung.com"), 
                       new User("John", "Doe", "john.doe-2@baeldung.com"));
    }
}

该 API 会响应一个 User 对象 list:

public class User {

    private String name;
    private String surname;
    private String emailAddress;

    //省略 get/set/toString() 方法
}

让我们启动应用并调用 API:

$ java -jar app.jar
$ curl -X GET http://localhost:8080/api/v1/users -H "Content-Type: application/json"

API 响应如下:

[
   {
      "name":"John",
      "surname":"Doe",
      "email":"john.doe@baeldung.come"
   },
   {
      "name":"John",
      "surname":"Doe",
      "email":"john.doe-2@baeldung.come"
   }
]

3.2、通过 Maven 将 Spring Boot 应用转换为 Lambda

为了在 Lambda 上运行我们的应用,让我们在 pom.xml 文件中添加 aws-serverless-java-container-springboot2 依赖:

<dependency>
    <groupId>com.amazonaws.serverless</groupId>
    <artifactId>aws-serverless-java-container-springboot2</artifactId>
    <version>${springboot2.aws.version}</version>
</dependency>

然后,我们将添加 maven-shade-plugin 并移除 spring-boot-maven-plugin

maven shade plugin 用于创建 shaded(或 uber)JAR 文件。shaded JAR 文件是一个自包含的可执行 JAR 文件,它将所有依赖项都包含在 JAR 文件中,因此可以独立运行:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.3.0</version>
    <configuration>
        <createDependencyReducedPom>false</createDependencyReducedPom>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <artifactSet>
                    <excludes>
                        <exclude>org.apache.tomcat.embed:*</exclude>
                    </excludes>
                 </artifactSet>
            </configuration>
         </execution>
     </executions>
 </plugin>

总之,此配置将在 Maven 构建的打包阶段生成一个 shaded JAR 文件。

JAR 文件将包括 Spring Boot 通常会打包的所有类和资源,但 Tomcat 的类和资源除外。使用 AWS Lambda 时,我们不需要运行嵌入式 Web 容器。

4、Lambda Handler

下一步是创建一个实现 RequestHandler 的类。

RequestHandler 是一个定义了单个 handleRequest 方法的接口。根据我们构建的 Lambda 类型,有几种不同的方法来处理请求。

在本例中,我们处理来自 API Gateway 的请求,因此可以使用 RequestHandler<AwsProxyRequest, AwsProxyResponse> 版本,其中输入是 API Gateway request,响应是 API Gateway response。

AWS 提供的 Spring Boot serverless 库为我们提供了一个特殊的 SpringBootLambdaContainerHandler 类,用于通过 Spring 处理 API 调用,从而使 Spring Boot API server 代码像 Lambda 一样运行。

4.1、启动时间

注意,在 AWS Lambda 中,初始化阶段的时间限制为 10 秒。

如果我们的应用程序启动时间超过这个时间,AWS Lambda 就会超时并尝试启动一个新的 Lambda 运行时。

根据 Spring Boot 应用程序的启动速度,我们可以选择两种方式来初始化 Lambda 处理程序:

  • 同步 - 应用程序的启动时间远远小于时间限制。
  • 异步 - 应用程序的启动时间可能较长。

4.2、同步启动

让我们在 Spring Boot 项目中定义一个新的 handler:

public class LambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {
    private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;

    static {
        try {
            handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); }
        catch (ContainerInitializationException ex){
            throw new RuntimeException("Unable to load spring boot application",ex); }
    }

    @Override
    public AwsProxyResponse handleRequest(AwsProxyRequest input, Context context) {
        return handler.proxy(input, context);
    }
}

我们使用 SpringBootLambdaContainerHandler 来处理 API Gateway 请求,并通过 application context 传递这些请求。我们在 LambaHandler 类的静态构造函数中初始化该 handler,并通过 handleRequest 函数调用它。

然后,handler 对象会调用 Spring Boot 应用程序中的相应方法来处理请求并生成响应。最后,它将响应返回给 Lambda 运行时,以便传回给 API 网关。

让我们通过 Lambda handler 调用 API:

@Test
void whenTheUsersPathIsInvokedViaLambda_thenShouldReturnAList() throws IOException {
    LambdaHandler lambdaHandler = new LambdaHandler();
    AwsProxyRequest req = new AwsProxyRequestBuilder("/api/v1/users", "GET").build();
    AwsProxyResponse resp = lambdaHandler.handleRequest(req, lambdaContext);
    Assertions.assertNotNull(resp.getBody());
    Assertions.assertEquals(200, resp.getStatusCode());
}

4.3、异步启动

有时,Spring Boot 应用程序可能启动缓慢。这是因为在启动阶段,Spring 引擎会构建上下文,扫描并初始化代码中的所有 Bean。

这一过程可能会影响启动时间,并在 serverless 环境中造成很多问题。

为了解决这个问题,我们可以定义一个新的 handler:

public class AsynchronousLambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {
    private SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;

    public AsynchronousLambdaHandler() throws ContainerInitializationException {
        handler = (SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse>) 
          new SpringBootProxyHandlerBuilder()
            .springBootApplication(Application.class)
            .asyncInit()
            .buildAndInitialize();
    }

    @Override
    public AwsProxyResponse handleRequest(AwsProxyRequest input, Context context) {
        return handler.proxy(input, context);
    }
}

该方法与前一个方法类似。在这个实例中,SpringBootLambdaContainerHandler 是在 request handler 的对象构造函数中构造的,而不是在静态构造函数中。因此,它是在 Lambda 启动的不同阶段执行的。

5、部署应用

AWS SAM(Serverless Application Model)是一个开源框架,用于在 AWS 上构建 serverless 应用程序。

在为 Spring Boot 应用程序定义了 Lambda handler 后,我们需要准备好所有组件,以便使用 SAM 进行部署。

5.1、SAM 模板

SAM 模板(SAM YAML)是一个 YAML 格式的文件,用于定义部署 serverless 应用程序所需的 AWS 资源。基本上,它提供了一种声明式方法来指定 serverless 应用程序的配置。

定义 template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 30

Resources:
  ProfileApiFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: .
      Handler: com.baeldung.aws.handler.LambdaHandler::handleRequest
      Runtime: java11
      AutoPublishAlias: production
      SnapStart:
        ApplyOn: PublishedVersions
      Architectures:
        - x86_64
      MemorySize: 2048
      Environment: 
        Variables:
          JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1 
      Events:
        HelloWorld:
          Type: Api 
          Properties:
            Path: /{proxy+}
            Method: ANY

配置中的某些字段如下:

  • type - 表示此资源是使用 AWS::Serverless::Function 资源类型定义的 AWS Lambda。
  • coreUri - 指定函数代码的位置。
  • AutoPublishAlias - 指定 AWS Lambda 在自动发布函数的新版本时应使用的别名。
  • Handler - 指定了 lambda handler 类。
  • Events - 指定触发 Lambda 函数的事件。
  • Type - 指定这是一个 Api 事件源。
  • Properties - 定义了 API 网关应响应的 HTTP 方法和路径。

5.2、SAM 部署

是时候将我们的应用程序部署为 AWS Lambda 了。

第一步是下载并安装 AWS CLIAWS SAM CLI

让我们在 template.yaml 所在的路径上运行 AWS SAM CLI 并执行命令:

$ sam build

运行此命令后,AWS SAM CLI 会将 Lambda 函数的源代码和依赖项打包并构建成一个 ZIP 文件,作为我们的部署包。

让我们在本地部署应用:

$ sam local start-api

接下来,让我们在 Spring Boot 服务运行时通过 sam local 触发它:

$ curl localhost:3000/api/v1/users

API 响应与之前相同:

[
   {
      "name":"John",
      "surname":"Doe",
      "email":"john.doe@baeldung.come"
   },
   {
      "name":"John",
      "surname":"Doe",
      "email":"john.doe-2@baeldung.come"
   }
]

我们还可以将其部署到 AWS:

$ sam deploy

6、在 Lambda 中使用 Spring 的限制

虽然 Spring 是一个强大而灵活的框架,可用于构建复杂而可扩展的应用程序,但在 Lambda 上下文中使用 Spring 并不总是最佳选择。

主要原因是 Lambda 被设计为小型、单一用途的函数,可以快速高效地执行。

6.1、冷启动

AWS Lambda 函数的冷启动时间是指在处理事件之前初始化函数环境所需的时间。

有几个因素会影响 Lambda 函数的冷启动性能:

  • 包大小 - 包越大,初始化时间越长,冷启动速度越慢。
  • 初始化时间 - Spring 框架初始化和设置 application context 所需的时间。这包括初始化任何依赖项,如数据库连接、HTTP 客户端或缓存框架。
  • 自定义的初始化逻辑 - 尽量减少自定义初始化逻辑的数量,并确保针对冷启动进行优化,这一点非常重要。

我们可以使用 Lambda SnapStart 来缩短启动时间。

6.2、数据库连接池

在像 AWS Lambda 这样的 serverless 环境中,函数是按需执行的,因此维护连接池可能有点麻烦。

当一个事件触发一个 Lambda 时,AWS Lambda 引擎会创建一个新的应用程序实例。在两次请求之间,运行时会停滞或终止。

许多连接池都持有打开的连接。这可能会在热启动后重新使用连接池时造成混乱或错误,而且可能会导致某些数据库引擎的资源泄漏。简而言之,标准连接池依赖于服务器持续运行和维护连接。

为了解决这个问题,AWS 提供了一个名为 RDS Proxy 的解决方案,它为 Lambda 函数提供了连接池服务。

通过使用 RDS Proxy,Lambda 函数可以连接到数据库,而无需维护自己的连接池。

7、总结

在本文中,我们学习了如何将现有 Spring Boot API 应用转换为 AWS Lambda。

我们了解了 AWS 提供的帮助库。此外,我们还考虑了 Spring Boot 较慢的启动时间会如何影响我们的设置方式。

然后,我们了解了如何部署 Lambda 并使用 SAM CLI 进行测试。


参考:https://www.baeldung.com/spring-boot-aws-lambda