51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 AWS Lambda 中运行 Spring Boot 应用

1、概览 {#1概览}

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

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

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

2、理解 Lamdba {#2理解-lamdba}

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

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

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

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

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

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

3、项目设置 {#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 示例 {#31spring-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 {#32通过-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 {#4lambda-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、启动时间 {#41启动时间}

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

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

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

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

4.2、同步启动 {#42同步启动}

让我们在 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、异步启动 {#43异步启动}

有时,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、部署应用 {#5部署应用}

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

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

5.1、SAM 模板 {#51sam-模板}

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 部署 {#52sam-部署}

是时候将我们的应用程序部署为 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 的限制 {#6在-lambda-中使用-spring-的限制}

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

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

6.1、冷启动 {#61冷启动}

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

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

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

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

6.2、数据库连接池 {#62数据库连接池}

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

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

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

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

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

7、总结 {#7总结}

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

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

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


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

赞(2)
未经允许不得转载:工具盒子 » 在 AWS Lambda 中运行 Spring Boot 应用