AWS Lambda 是一個(gè)流行的無(wú)服務(wù)器開(kāi)發(fā)平臺(tái),作為一名 Java 開(kāi)發(fā)人員,我想使用它,但有一些問(wèn)題需要解決。我將通過(guò)下面文章,和大家分享一下Java AWS Lambda開(kāi)發(fā)平臺(tái)的內(nèi)容。
引入
AWS Lambda 是一個(gè)流行的無(wú)服務(wù)器開(kāi)發(fā)平臺(tái),作為一名 Java 開(kāi)發(fā)人員,我喜歡能夠使用這個(gè)平臺(tái),但有一些要點(diǎn)需要首先解決。
- AWS Lambda 上的無(wú)服務(wù)器函數(shù)的成本對(duì)于 JVM 來(lái)說(shuō)會(huì)很昂貴。
- AWS Lambda 上的冷啟動(dòng)在 JVM 上可能是一個(gè)真正的問(wèn)題。
- 在 AWS Lambda 上為每個(gè)請(qǐng)求最大化效率可能代價(jià)高昂,并且在 JVM 中效率不高。
本文的兩個(gè)主要目的如下:
- 學(xué)習(xí)如何在無(wú)服務(wù)器平臺(tái)(Lambda)上使用 AWS 服務(wù),例如 Quarkus 框架的 DynamoDB。
- 在 AWS Lambda 上獲得最佳性能并降低成本。
演示應(yīng)用程序
此存儲(chǔ)庫(kù)包含一個(gè)由 JDK 11 和 Quarkus 開(kāi)發(fā)的 Java 應(yīng)用程序示例,它是一個(gè)簡(jiǎn)單的 AWS Lambda 函數(shù)。這個(gè)簡(jiǎn)單的函數(shù)將接受一個(gè) JSON 格式的水果名稱(chēng)(輸入)并返回一種水果類(lèi)型。
{ "name": "apple" }
水果的類(lèi)型將是:
- 春季水果
- 夏季水果
- 秋季水果
- 冬季水果
演示應(yīng)用程序工作流程
這個(gè)演示是一個(gè)簡(jiǎn)單的 Java 應(yīng)用程序,它獲取請(qǐng)求的水果信息,提取水果的類(lèi)型,并返回正確的水果類(lèi)型。哪有那么簡(jiǎn)單?!
創(chuàng)建基于 Quarkus 的 Java 應(yīng)用程序
Quarkus 提供了擴(kuò)展 AWS Lambda 項(xiàng)目的明確指南。可以使用 Maven 命令輕松訪問(wèn)此項(xiàng)目模板。
mvn archetype:generate \ -DarchetypeGroupId=com.thinksky \ -DarchetypeArtifactId=aws-lambda-handler-qaurkus \ -DarchetypeVersion=2.1.3.Final
該命令將使用 AWS Java SDK 生成應(yīng)用程序。
Quarkus 框架具有針對(duì) DynamoDB、S3、SNS、SQS 等的擴(kuò)展,我更喜歡使用提供非阻塞功能的 AWS Java SDK V2。因此,項(xiàng)目 pom.xml 文件需要按照本指南進(jìn)行修改。
該項(xiàng)目具有 Lambda,它是 pom 文件中的一個(gè)依賴(lài)項(xiàng)。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-amazon-lambda</artifactId>
</dependency>
需要添加依賴(lài)項(xiàng)以使用 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依賴(lài)項(xiàng)添加的應(yīng)用程序的設(shè)置上使用 apache 客戶端。
quarkus.dynamodb.sync-client.type=apache
使用 Quarkus 在 AWS Lambda 上開(kāi)發(fā) Java 應(yīng)用程序的好處
常規(guī)的 AWS Lambda Java 項(xiàng)目將是一個(gè)普通的 Java 項(xiàng)目;然而,Quarkus 將在 Java 項(xiàng)目中引入依賴(lài)注入。
@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 中的一個(gè)類(lèi),Quarkus 將在其依賴(lài)注入生態(tài)系統(tǒng)中構(gòu)建并提供該類(lèi)。該FruitService是由叫做抽象類(lèi)繼承AbstractService,這抽象類(lèi)提供的基本對(duì)象DynamoDbClient的需求,例如ScanRequest,PutItemRequest等等。
反射在 Java 框架中很流行,但這將是 GraalVM native-image 的新挑戰(zhàn)。(但是 Quarkus 有一個(gè)簡(jiǎn)單的解決方案來(lái)應(yīng)對(duì)這個(gè)挑戰(zhàn),那就是對(duì) classes 的注釋@RegisterForReflection。這不是在 GraalVM 中注冊(cè)反射類(lèi)的最簡(jiǎn)單方法嗎?
@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 平臺(tái)的同時(shí)還提供了許多其他好處。我將在以后的一系列文章中描述它們。
在 AWS Lambda 上部署演示應(yīng)用程序
現(xiàn)在是 AWS 上的部署時(shí)間,使用 Maven 和 Quarkus 框架的過(guò)程會(huì)相對(duì)簡(jiǎn)單。但是,在部署和運(yùn)行應(yīng)用程序之前,需要在 AWS 上做更多準(zhǔn)備。部署過(guò)程包括以下步驟:
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 的內(nèi)容
{
"fruitName": {
"S": "Apple"
},
"fruitType": {
"S": "Fall"
}
}
最后,從 Dynamodb 運(yùn)行查詢以確保我們有項(xiàng)目。
$ aws dynamodb query \ --table-name Fruits_TBL \ --key-condition-expression "fruitName = :name" \ --expression-attribute-values '{":name":{"S":"Apple"}}'
2) 在 IAM 中定義一個(gè)角色以訪問(wèn) DynamoBD 并將其分配給我們的 Lambda 應(yīng)用程序。
$ 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 權(quán)限分配給該角色。
$ aws iam attach-role-policy --role-name Fruits_service_role -- policy-arn "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
然后這個(gè)。
$ aws iam attach-role-policy --role-name Fruits_service_role --policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
并且該角色可能還需要以下權(quán)限。
$ aws iam attach-role-policy --role-name Fruits_service_role --policy-arn "arn:aws:iam::aws:policy/AWSLambda_FullAccess"
最后,AWS 平臺(tái)現(xiàn)已準(zhǔn)備好托管我們的應(yīng)用程序。
為了繼續(xù)部署過(guò)程,我們需要構(gòu)建我們的應(yīng)用程序并修改生成的文章。
$ mvn clean install
Quarkus 框架將負(fù)責(zé)創(chuàng)建 JAR 工件文件、壓縮該 JAR 文件并準(zhǔn)備AWS的SAM 模板。這次應(yīng)該使用JVM版本,修改方法如下:
1) 將定義的角色添加到 Lambda 以獲得適當(dāng)?shù)脑L問(wèn)權(quán)限
Role:arn:aws:iam::{Your-Account-Number-On-AWS}:role/fruits_service_role
2)增加超時(shí)時(shí)間
因此,SAM 模板現(xiàn)在已準(zhǔn)備好部署在 AWS Lambda 上。
$ sam deploy -t target/sam.jvm.yaml -g
此命令會(huì)將 jar 文件以 zip 格式上傳到 AWS 并將其部署為 Lambda 函數(shù)。下一步將是通過(guò)調(diào)用請(qǐng)求來(lái)測(cè)試應(yīng)用程序
在 AWS Lambda + JVM 平臺(tái)上觀看演示應(yīng)用程序的性能
是時(shí)候運(yùn)行部署的 Lambda 函數(shù)、測(cè)試它并查看它的執(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 是我在部署過(guò)程中給 SAM CLI 的 Lambda 的名稱(chēng)。
然后我們可以參考AWS的web控制臺(tái)查看調(diào)用該函數(shù)的結(jié)果。
數(shù)字在說(shuō)話,由于 AWS Lambda 的冷啟動(dòng)功能,這對(duì)于一個(gè)簡(jiǎn)單的應(yīng)用程序來(lái)說(shuō)是一個(gè)可怕的性能。
什么是 AWS Lambda 冷啟動(dòng)?
運(yùn)行 Lambda 函數(shù)時(shí),只要它被積極使用,它就會(huì)保持活動(dòng)狀態(tài),這意味著您的容器保持活動(dòng)狀態(tài)并準(zhǔn)備好執(zhí)行。但是,AWS 將在一段時(shí)間不活動(dòng)(通常很短)后丟棄容器,并且您的函數(shù)將變得不活動(dòng)或冷。當(dāng)請(qǐng)求到達(dá)空閑 lambda 函數(shù)時(shí),會(huì)發(fā)生冷啟動(dòng)。之后,Lambda 函數(shù)將被初始化以能夠響應(yīng)請(qǐng)求。(Java 框架的初始化模式)。
另一方面,當(dāng)有可用的 lambda 容器時(shí)會(huì)發(fā)生熱啟動(dòng)。
冷啟動(dòng)是我們有這種糟糕性能的主要原因,因?yàn)槊看卫鋯?dòng)發(fā)生時(shí),AWS 都會(huì)初始化我們的 Java 應(yīng)用程序,顯然,每個(gè)請(qǐng)求都需要很長(zhǎng)時(shí)間。
AWS Lambda 冷啟動(dòng)挑戰(zhàn)的可用解決方案
有兩種方法可以應(yīng)對(duì)這一基本挑戰(zhàn)。
- 使用不屬于本文范圍的預(yù)配置并發(fā)。
- 在應(yīng)用程序的初始化和響應(yīng)時(shí)間上獲得更好的性能帶來(lái)了如何在我們的 Java 應(yīng)用程序中實(shí)現(xiàn)更好性能的問(wèn)題。答案是從我們的 Java 應(yīng)用程序創(chuàng)建一個(gè)本地二進(jìn)制可執(zhí)行文件,并使用Oracle GraalVM將其部署在 AWS Lambda 上。
GraalVM 是什么?
GraalVM 是一種高性能 JDK 發(fā)行版,旨在加速用 Java 和其他 JVM 語(yǔ)言編寫(xiě)的應(yīng)用程序的執(zhí)行,同時(shí)支持 JavaScript、Ruby、Python 和許多其他流行語(yǔ)言。Native-Image 是一種提前技術(shù),可將 Java 代碼編譯為獨(dú)立的可執(zhí)行文件。此可執(zhí)行文件包括應(yīng)用程序類(lèi)、其依賴(lài)項(xiàng)中的類(lèi)、運(yùn)行時(shí)庫(kù)類(lèi)以及來(lái)自 JDK 的靜態(tài)鏈接本機(jī)代碼。它不在 Java VM 上運(yùn)行,但包括來(lái)自不同運(yùn)行時(shí)系統(tǒng)(稱(chēng)為“Substrate VM”)的必要組件,如內(nèi)存管理、線程調(diào)度等。
從 Java 應(yīng)用程序構(gòu)建本機(jī)二進(jìn)制可執(zhí)行文件
首先,我們需要安裝 GraalVM 及其 Native-Image 。然后,通過(guò)安裝 GraalVM,我們可以使用 GraalVM 將 Java 應(yīng)用程序轉(zhuǎn)換為原生二進(jìn)制可執(zhí)行文件。Quarkus 使它變得簡(jiǎn)單,它有一個(gè) Maven/Gradle 插件,所以在一個(gè)典型的基于 Quarkus 的應(yīng)用程序中,我們將有一個(gè)名為native.
$ mvn clean install -Pnative
Maven 將根據(jù)您使用的操作系統(tǒng)構(gòu)建一個(gè)本地二進(jìn)制可執(zhí)行文件。如果你在 Windows 上開(kāi)發(fā),這個(gè)文件將只能在 Windows 機(jī)器上運(yùn)行;但是,AWS Lambda 需要基于 Linux 的二進(jìn)制可執(zhí)行文件。在這種情況下,Quarkus 框架將通過(guò)其插件上的一個(gè)簡(jiǎn)單參數(shù)來(lái)滿足此要求-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 有幾個(gè)不同的可部署環(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 ║ ╚═════════╩═══════════════════╩════════════════════╝
所以我們之前通過(guò)java11(Corretto 11)在Lambda上部署了Java Application,并沒(méi)有表現(xiàn)出很好的性能。
對(duì)于 Lambda 上的純 Linux 平臺(tái),我們目前有兩個(gè)選項(xiàng),它們是provided和provided.al2。
值得一提的是,provided會(huì)使用Amazon Linux,并且provided.al2會(huì)使用Amazon Linux 2,因此,由于版本2的長(zhǎng)期支持,強(qiáng)烈推薦使用版本2。
在 AWS Lambda 上部署二進(jìn)制可執(zhí)行文件
正如我們所見(jiàn),Quarkus 會(huì)為我們生成兩個(gè) sam 模板;一個(gè)用于 JVM 基礎(chǔ) Lambda,第二個(gè)是本機(jī)二進(jìn)制可執(zhí)行文件。這次我們應(yīng)該使用原生的 sam 模板,它也需要一些小的修改。
1.更改為 AWS Linux V2
Runtime: provided.al2
2. 將定義的角色添加到 Lambda 以獲得適當(dāng)?shù)脑L問(wèn)權(quán)限。
Role: arn:aws:iam::{Your-Account-Number-On-AWS}:role/fruits_service_role
3.增加超時(shí)時(shí)間
Timeout: 30
原生 SAM 模板的最終版本將是這樣的final.sam.native.yaml;它現(xiàn)在已準(zhǔn)備好部署在 AWS 上。
$ sam deploy -t target/sam.native.yaml -g
此命令會(huì)將二進(jìn)制文件作為 zip 格式上傳到 AWS 并將其部署為 Lambda 函數(shù),與 JVM 版本完全一樣。現(xiàn)在,我們可以跳到令人興奮的部分,即監(jiān)控性能。
在 AWS Lambda + 自定義平臺(tái)上觀看演示應(yīng)用程序的性能
是時(shí)候運(yùn)行部署的 Lambda 函數(shù)、測(cè)試它并查看它的執(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 是我在部署過(guò)程中給 SAM CLI 的 Lambda 的名稱(chēng)。
然后我們可以打開(kāi) AWS Web 控制臺(tái)查看調(diào)用該函數(shù)的結(jié)果。
在 AWS Lambda 上分析 JVM 與原生二進(jìn)制的性能
我們可以在兩個(gè)類(lèi)別中分析和比較 AWS Lambda 平臺(tái)上應(yīng)用程序的兩個(gè)版本。
- 初始化時(shí)間:第一次調(diào)用或調(diào)用 Lambda 函數(shù)所消耗的時(shí)間稱(chēng)為初始化時(shí)間。這幾乎是在 Lambda 上調(diào)用應(yīng)用程序的最長(zhǎng)持續(xù)時(shí)間,因?yàn)樵诖穗A段我們的 Java 應(yīng)用程序?qū)念^開(kāi)始。
- JVM 和 Binary 版本之間存在相當(dāng)大的差異,這意味著原生二進(jìn)制版本的初始化時(shí)間幾乎比 JVM 版本快八倍。
- 請(qǐng)求時(shí)間:我在初始化步驟后調(diào)用了 9 次 Lambda 函數(shù),這是性能結(jié)果。
根據(jù)結(jié)果??,JVM 版本和 Native 二進(jìn)制文件之間的性能存在顯著差異。
結(jié)論
Quarkus 框架將通過(guò)提供一些很好的特性,如依賴(lài)注入,幫助我們?cè)?Java 應(yīng)用程序上擁有清晰和結(jié)構(gòu)化的代碼。此外,它還有助于在 GraalVM 的幫助下將我們的 Java 應(yīng)用程序轉(zhuǎn)換為原生二進(jìn)制文件。
與 JVM 版本相比,本機(jī)二進(jìn)制版本具有明顯更好的性能。
- 二進(jìn)制版本僅使用 128 MB 內(nèi)存,而 JVM 版本使用 512 MB,從而在 AWS Lambda 上節(jié)省了大量資源。
- 二進(jìn)制版本提供比 JVM 版本更好的請(qǐng)求時(shí)間,這意味著在 AWS Lambda 上可以節(jié)省更多時(shí)間。
總的來(lái)說(shuō),通過(guò)節(jié)省資源和時(shí)間,原生二進(jìn)制方法已被證明是一種低成本的選擇。