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 CLI 和 AWS 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