AWS Lambda 是一個流行的無服務器開發(fā)平臺,作為一名 Java 開發(fā)人員,我想使用它,但有一些問題需要解決。我將通過下面文章,和大家分享一下Java AWS Lambda開發(fā)平臺的內容。
引入
AWS Lambda 是一個流行的無服務器開發(fā)平臺,作為一名 Java 開發(fā)人員,我喜歡能夠使用這個平臺,但有一些要點需要首先解決。
- AWS Lambda 上的無服務器函數的成本對于 JVM 來說會很昂貴。
- AWS Lambda 上的冷啟動在 JVM 上可能是一個真正的問題。
- 在 AWS Lambda 上為每個請求最大化效率可能代價高昂,并且在 JVM 中效率不高。
本文的兩個主要目的如下:
- 學習如何在無服務器平臺(Lambda)上使用 AWS 服務,例如 Quarkus 框架的 DynamoDB。
- 在 AWS Lambda 上獲得最佳性能并降低成本。
演示應用程序
此存儲庫包含一個由 JDK 11 和 Quarkus 開發(fā)的 Java 應用程序示例,它是一個簡單的 AWS Lambda 函數。這個簡單的函數將接受一個 JSON 格式的水果名稱(輸入)并返回一種水果類型。
{ "name": "apple" }
水果的類型將是:
- 春季水果
- 夏季水果
- 秋季水果
- 冬季水果
演示應用程序工作流程
這個演示是一個簡單的 Java 應用程序,它獲取請求的水果信息,提取水果的類型,并返回正確的水果類型。哪有那么簡單?!
創(chuàng)建基于 Quarkus 的 Java 應用程序
Quarkus 提供了擴展 AWS Lambda 項目的明確指南??梢允褂?Maven 命令輕松訪問此項目模板。
mvn archetype:generate \ -DarchetypeGroupId=com.thinksky \ -DarchetypeArtifactId=aws-lambda-handler-qaurkus \ -DarchetypeVersion=2.1.3.Final
該命令將使用 AWS Java SDK 生成應用程序。
Quarkus 框架具有針對 DynamoDB、S3、SNS、SQS 等的擴展,我更喜歡使用提供非阻塞功能的 AWS Java SDK V2。因此,項目 pom.xml 文件需要按照本指南進行修改。
該項目具有 Lambda,它是 pom 文件中的一個依賴項。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-amazon-lambda</artifactId>
</dependency>
需要添加依賴項以使用 AWS DynamoDB 建立與 DynamoDB 的連接
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-amazon-dynamodb</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-apache-httpclient</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
我將在可以使用apache-client依賴項添加的應用程序的設置上使用 apache 客戶端。
quarkus.dynamodb.sync-client.type=apache
使用 Quarkus 在 AWS Lambda 上開發(fā) Java 應用程序的好處
常規(guī)的 AWS Lambda Java 項目將是一個普通的 Java 項目;然而,Quarkus 將在 Java 項目中引入依賴注入。
@ApplicationScoped
public class FruitService extends AbstractService {
@Inject
DynamoDbClient dynamoDB;
public List<Fruit> findAll() {
return dynamoDB.scanPaginator(scanRequest()).items().stream()
.map(Fruit::from)
.collect(Collectors.toList());
}
public List<Fruit> add(Fruit fruit) {
dynamoDB.putItem(putRequest(fruit));
return findAll();
}
}
DynamoDbClient是 AWS Java SDK.v2 中的一個類,Quarkus 將在其依賴注入生態(tài)系統(tǒng)中構建并提供該類。該FruitService是由叫做抽象類繼承AbstractService,這抽象類提供的基本對象DynamoDbClient的需求,例如ScanRequest,PutItemRequest等等。
反射在 Java 框架中很流行,但這將是 GraalVM native-image 的新挑戰(zhàn)。(但是 Quarkus 有一個簡單的解決方案來應對這個挑戰(zhàn),那就是對 classes 的注釋@RegisterForReflection。這不是在 GraalVM 中注冊反射類的最簡單方法嗎?
@RegisterForReflection
public class Fruit {
private String name;
private Season type;
public Fruit() {
}
public Fruit(String name, Season type) {
this.name = name;
this.type = type;
}
}
還值得一提的是,Quarkus 在使用 AWS Lambda 平臺的同時還提供了許多其他好處。我將在以后的一系列文章中描述它們。
在 AWS Lambda 上部署演示應用程序
現(xiàn)在是 AWS 上的部署時間,使用 Maven 和 Quarkus 框架的過程會相對簡單。但是,在部署和運行應用程序之前,需要在 AWS 上做更多準備。部署過程包括以下步驟:
1) 在 DynamoDB 中定義 Fruits_TBL 表
$ aws dynamodb create-table --table-name Fruits_TBL \
--attribute-definitions AttributeName=fruitName,AttributeType=S \
AttributeName=fruitType,AttributeType=S \
--key-schema AttributeName=fruitName,KeyType=HASH \ AttributeName=fruitType,KeyType=RANGE \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1
然后在桌子上插入一些 fruits。
$ aws dynamodb put-item --table-name Fruits_TBL \ --item file://item.json \ --return-consumed-capacity TOTAL \ --return-item-collection-metrics SIZE
這是 item.json 的內容
{
"fruitName": {
"S": "Apple"
},
"fruitType": {
"S": "Fall"
}
}
最后,從 Dynamodb 運行查詢以確保我們有項目。
$ aws dynamodb query \ --table-name Fruits_TBL \ --key-condition-expression "fruitName = :name" \ --expression-attribute-values '{":name":{"S":"Apple"}}'
2) 在 IAM 中定義一個角色以訪問 DynamoBD 并將其分配給我們的 Lambda 應用程序。
$ aws iam create-role --role-name Fruits_service_role --assume-role-policy-document file://policy.json
policy.json
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Principal": {
"Service": [
"dynamodb.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
}
然后,將 DynamoDB 權限分配給該角色。
$ aws iam attach-role-policy --role-name Fruits_service_role -- policy-arn "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
然后這個。
$ aws iam attach-role-policy --role-name Fruits_service_role --policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
并且該角色可能還需要以下權限。
$ aws iam attach-role-policy --role-name Fruits_service_role --policy-arn "arn:aws:iam::aws:policy/AWSLambda_FullAccess"
最后,AWS 平臺現(xiàn)已準備好托管我們的應用程序。
為了繼續(xù)部署過程,我們需要構建我們的應用程序并修改生成的文章。
$ mvn clean install
Quarkus 框架將負責創(chuàng)建 JAR 工件文件、壓縮該 JAR 文件并準備AWS的SAM 模板。這次應該使用JVM版本,修改方法如下:
1) 將定義的角色添加到 Lambda 以獲得適當的訪問權限
Role:arn:aws:iam::{Your-Account-Number-On-AWS}:role/fruits_service_role
2)增加超時時間
因此,SAM 模板現(xiàn)在已準備好部署在 AWS Lambda 上。
$ sam deploy -t target/sam.jvm.yaml -g
此命令會將 jar 文件以 zip 格式上傳到 AWS 并將其部署為 Lambda 函數。下一步將是通過調用請求來測試應用程序
在 AWS Lambda + JVM 平臺上觀看演示應用程序的性能
是時候運行部署的 Lambda 函數、測試它并查看它的執(zhí)行情況了。
$ aws lambda invoke response.txt --cli-binary-format raw-in-base64-out --function-name {:fruitApp} --payload file://payload.json --log-type Tail --query LogResult --output text | base64 --decode
我們可以使用以下命令找出 FUNCTION_NAME。
$ aws lambda list-functions --query 'Functions[?starts_with(FunctionName, `fruitAppJVM`) == `true`].FunctionName'
FruitAppJVM 是我在部署過程中給 SAM CLI 的 Lambda 的名稱。
然后我們可以參考AWS的web控制臺查看調用該函數的結果。
數字在說話,由于 AWS Lambda 的冷啟動功能,這對于一個簡單的應用程序來說是一個可怕的性能。
什么是 AWS Lambda 冷啟動?
運行 Lambda 函數時,只要它被積極使用,它就會保持活動狀態(tài),這意味著您的容器保持活動狀態(tài)并準備好執(zhí)行。但是,AWS 將在一段時間不活動(通常很短)后丟棄容器,并且您的函數將變得不活動或冷。當請求到達空閑 lambda 函數時,會發(fā)生冷啟動。之后,Lambda 函數將被初始化以能夠響應請求。(Java 框架的初始化模式)。
另一方面,當有可用的 lambda 容器時會發(fā)生熱啟動。
冷啟動是我們有這種糟糕性能的主要原因,因為每次冷啟動發(fā)生時,AWS 都會初始化我們的 Java 應用程序,顯然,每個請求都需要很長時間。
AWS Lambda 冷啟動挑戰(zhàn)的可用解決方案
有兩種方法可以應對這一基本挑戰(zhàn)。
- 使用不屬于本文范圍的預配置并發(fā)。
- 在應用程序的初始化和響應時間上獲得更好的性能帶來了如何在我們的 Java 應用程序中實現(xiàn)更好性能的問題。答案是從我們的 Java 應用程序創(chuàng)建一個本地二進制可執(zhí)行文件,并使用Oracle GraalVM將其部署在 AWS Lambda 上。
GraalVM 是什么?
GraalVM 是一種高性能 JDK 發(fā)行版,旨在加速用 Java 和其他 JVM 語言編寫的應用程序的執(zhí)行,同時支持 JavaScript、Ruby、Python 和許多其他流行語言。Native-Image 是一種提前技術,可將 Java 代碼編譯為獨立的可執(zhí)行文件。此可執(zhí)行文件包括應用程序類、其依賴項中的類、運行時庫類以及來自 JDK 的靜態(tài)鏈接本機代碼。它不在 Java VM 上運行,但包括來自不同運行時系統(tǒng)(稱為“Substrate VM”)的必要組件,如內存管理、線程調度等。
從 Java 應用程序構建本機二進制可執(zhí)行文件
首先,我們需要安裝 GraalVM 及其 Native-Image 。然后,通過安裝 GraalVM,我們可以使用 GraalVM 將 Java 應用程序轉換為原生二進制可執(zhí)行文件。Quarkus 使它變得簡單,它有一個 Maven/Gradle 插件,所以在一個典型的基于 Quarkus 的應用程序中,我們將有一個名為native.
$ mvn clean install -Pnative
Maven 將根據您使用的操作系統(tǒng)構建一個本地二進制可執(zhí)行文件。如果你在 Windows 上開發(fā),這個文件將只能在 Windows 機器上運行;但是,AWS Lambda 需要基于 Linux 的二進制可執(zhí)行文件。在這種情況下,Quarkus 框架將通過其插件上的一個簡單參數來滿足此要求-Dquarkus.native.container-build=true。
$ mvn clean install -Pnative \ -Dquarkus.native.container-build=true \ -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-native-image:21.2-java11
如上命令所示, using-Dquarkus.native.builder-image可以指定我們要使用的 GraalVm 版本。
AWS Lambda 環(huán)境
AWS Lambda 有幾個不同的可部署環(huán)境。
╔═════════╦═══════════════════╦════════════════════╗ ║ Runtime ║ Amazon Linux ║ Amazon Linux 2 ║ ╠═════════╬═══════════════════╬════════════════════╣ ║ Node.js ║ nodejs12.x ║ nodejs10.x ║ ║ Python ║ python3.7 and 3.6 ║ python3.8 ║ ║ Ruby ║ ruby2.5 ║ ruby2.7 ║ ║ Java ║ java ║ java11 , java8.al2 ║ ║ Go ║ go1.x ║ provided.al2 ║ ║ .NET ║ dotnetcore2.1 ║ dotnetcore3.1 ║ ║ Custom ║ provided ║ provided.al2 ║ ╚═════════╩═══════════════════╩════════════════════╝
所以我們之前通過java11(Corretto 11)在Lambda上部署了Java Application,并沒有表現(xiàn)出很好的性能。
對于 Lambda 上的純 Linux 平臺,我們目前有兩個選項,它們是provided和provided.al2。
值得一提的是,provided會使用Amazon Linux,并且provided.al2會使用Amazon Linux 2,因此,由于版本2的長期支持,強烈推薦使用版本2。
在 AWS Lambda 上部署二進制可執(zhí)行文件
正如我們所見,Quarkus 會為我們生成兩個 sam 模板;一個用于 JVM 基礎 Lambda,第二個是本機二進制可執(zhí)行文件。這次我們應該使用原生的 sam 模板,它也需要一些小的修改。
1.更改為 AWS Linux V2
Runtime: provided.al2
2. 將定義的角色添加到 Lambda 以獲得適當的訪問權限。
Role: arn:aws:iam::{Your-Account-Number-On-AWS}:role/fruits_service_role
3.增加超時時間
Timeout: 30
原生 SAM 模板的最終版本將是這樣的final.sam.native.yaml;它現(xiàn)在已準備好部署在 AWS 上。
$ sam deploy -t target/sam.native.yaml -g
此命令會將二進制文件作為 zip 格式上傳到 AWS 并將其部署為 Lambda 函數,與 JVM 版本完全一樣。現(xiàn)在,我們可以跳到令人興奮的部分,即監(jiān)控性能。
在 AWS Lambda + 自定義平臺上觀看演示應用程序的性能
是時候運行部署的 Lambda 函數、測試它并查看它的執(zhí)行情況了。
$ aws lambda invoke response.txt --cli-binary-format raw-in-base64-out --function-name {:fruitApp} --payload file://payload.json --log-type Tail --query LogResult --output text | base64 --decode
我們可以使用以下命令找出 FUNCTION_NAME。
$ aws lambda list-functions --query 'Functions[?starts_with(FunctionName, `fruitAppNative`) == `true`].FunctionName'
FruitAppNative 是我在部署過程中給 SAM CLI 的 Lambda 的名稱。
然后我們可以打開 AWS Web 控制臺查看調用該函數的結果。
在 AWS Lambda 上分析 JVM 與原生二進制的性能
我們可以在兩個類別中分析和比較 AWS Lambda 平臺上應用程序的兩個版本。
- 初始化時間:第一次調用或調用 Lambda 函數所消耗的時間稱為初始化時間。這幾乎是在 Lambda 上調用應用程序的最長持續(xù)時間,因為在此階段我們的 Java 應用程序將從頭開始。
- JVM 和 Binary 版本之間存在相當大的差異,這意味著原生二進制版本的初始化時間幾乎比 JVM 版本快八倍。
- 請求時間:我在初始化步驟后調用了 9 次 Lambda 函數,這是性能結果。
根據結果??,JVM 版本和 Native 二進制文件之間的性能存在顯著差異。
結論
Quarkus 框架將通過提供一些很好的特性,如依賴注入,幫助我們在 Java 應用程序上擁有清晰和結構化的代碼。此外,它還有助于在 GraalVM 的幫助下將我們的 Java 應用程序轉換為原生二進制文件。
與 JVM 版本相比,本機二進制版本具有明顯更好的性能。
- 二進制版本僅使用 128 MB 內存,而 JVM 版本使用 512 MB,從而在 AWS Lambda 上節(jié)省了大量資源。
- 二進制版本提供比 JVM 版本更好的請求時間,這意味著在 AWS Lambda 上可以節(jié)省更多時間。
總的來說,通過節(jié)省資源和時間,原生二進制方法已被證明是一種低成本的選擇。