App下載

使用 Java 的 AWS Lambda:快速且低成本的方法

可樂派掌門人 2021-09-17 10:11:54 瀏覽數(shù) (3210)
反饋

AWS Lambda 是一個(gè)流行的無服務(wù)器開發(fā)平臺,作為一名 Java 開發(fā)人員,我想使用它,但有一些問題需要解決。我將通過下面文章,和大家分享一下Java AWS Lambda開發(fā)平臺的內(nèi)容。

引入

AWS Lambda 是一個(gè)流行的無服務(wù)器開發(fā)平臺,作為一名 Java 開發(fā)人員,我喜歡能夠使用這個(gè)平臺,但有一些要點(diǎn)需要首先解決。

  1. AWS Lambda 上的無服務(wù)器函數(shù)的成本對于 JVM 來說會很昂貴。
  2. AWS Lambda 上的冷啟動在 JVM 上可能是一個(gè)真正的問題。
  3. 在 AWS Lambda 上為每個(gè)請求最大化效率可能代價(jià)高昂,并且在 JVM 中效率不高。

本文的兩個(gè)主要目的如下:

  • 學(xué)習(xí)如何在無服務(wù)器平臺(Lambda)上使用 AWS 服務(wù),例如 Quarkus 框架的 DynamoDB。
  • 在 AWS Lambda 上獲得最佳性能并降低成本。

演示應(yīng)用程序

此存儲庫包含一個(gè)由 JDK 11 和 Quarkus 開發(fā)的 Java 應(yīng)用程序示例,它是一個(gè)簡單的 AWS Lambda 函數(shù)。這個(gè)簡單的函數(shù)將接受一個(gè) JSON 格式的水果名稱(輸入)并返回一種水果類型。

{
  "name": "apple"
}

水果的類型將是:

  • 春季水果
  • 夏季水果
  • 秋季水果
  • 冬季水果

演示應(yīng)用程序


演示應(yīng)用程序工作流程

這個(gè)演示是一個(gè)簡單的 Java 應(yīng)用程序,它獲取請求的水果信息,提取水果的類型,并返回正確的水果類型。哪有那么簡單?!

演示應(yīng)用程序工作流程

創(chuàng)建基于 Quarkus 的 Java 應(yīng)用程序

Quarkus 提供了擴(kuò)展 AWS Lambda 項(xiàng)目的明確指南??梢允褂?Maven 命令輕松訪問此項(xiàng)目模板。

mvn archetype:generate \
 -DarchetypeGroupId=com.thinksky \
 -DarchetypeArtifactId=aws-lambda-handler-qaurkus \ 
 -DarchetypeVersion=2.1.3.Final

該命令將使用 AWS Java SDK 生成應(yīng)用程序。

Quarkus 框架具有針對 DynamoDB、S3、SNS、SQS 等的擴(kuò)展,我更喜歡使用提供非阻塞功能的 AWS Java SDK V2。因此,項(xiàng)目 pom.xml 文件需要按照本指南進(jìn)行修改。

該項(xiàng)目具有 Lambda,它是 pom 文件中的一個(gè)依賴項(xiàng)。

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-amazon-lambda</artifactId>
</dependency>

需要添加依賴項(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依賴項(xiàng)添加的應(yīng)用程序的設(shè)置上使用 apache 客戶端。

quarkus.dynamodb.sync-client.type=apache

使用 Quarkus 在 AWS Lambda 上開發(fā) Java 應(yīng)用程序的好處

常規(guī)的 AWS Lambda Java 項(xiàng)目將是一個(gè)普通的 Java 項(xiàng)目;然而,Quarkus 將在 Java 項(xiàng)目中引入依賴注入。

@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è)類,Quarkus 將在其依賴注入生態(tài)系統(tǒng)中構(gòu)建并提供該類。該FruitService是由叫做抽象類繼承AbstractService,這抽象類提供的基本對象DynamoDbClient的需求,例如ScanRequest,PutItemRequest等等。

反射在 Java 框架中很流行,但這將是 GraalVM native-image 的新挑戰(zhàn)。(但是 Quarkus 有一個(gè)簡單的解決方案來應(yīng)對這個(gè)挑戰(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 平臺的同時(shí)還提供了許多其他好處。我將在以后的一系列文章中描述它們。

在 AWS Lambda 上部署演示應(yīng)用程序

現(xiàn)在是 AWS 上的部署時(shí)間,使用 Maven 和 Quarkus 框架的過程會相對簡單。但是,在部署和運(yùn)行應(yīng)用程序之前,需要在 AWS 上做更多準(zhǔn)備。部署過程包括以下步驟:

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è)角色以訪問 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 平臺現(xiàn)已準(zhǔn)備好托管我們的應(yīng)用程序。

為了繼續(xù)部署過程,我們需要構(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問權(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

此命令會將 jar 文件以 zip 格式上傳到 AWS 并將其部署為 Lambda 函數(shù)。下一步將是通過調(diào)用請求來測試應(yīng)用程序

在 AWS Lambda + JVM 平臺上觀看演示應(yīng)用程序的性能

是時(shí)候運(yùn)行部署的 Lambda 函數(shù)、測試它并查看它的執(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控制臺查看調(diào)用該函數(shù)的結(jié)果。

AWS 網(wǎng)絡(luò)控制臺

數(shù)字在說話,由于 AWS Lambda 的冷啟動功能,這對于一個(gè)簡單的應(yīng)用程序來說是一個(gè)可怕的性能。

什么是 AWS Lambda 冷啟動?

運(yùn)行 Lambda 函數(shù)時(shí),只要它被積極使用,它就會保持活動狀態(tài),這意味著您的容器保持活動狀態(tài)并準(zhǔn)備好執(zhí)行。但是,AWS 將在一段時(shí)間不活動(通常很短)后丟棄容器,并且您的函數(shù)將變得不活動或冷。當(dāng)請求到達(dá)空閑 lambda 函數(shù)時(shí),會發(fā)生冷啟動。之后,Lambda 函數(shù)將被初始化以能夠響應(yīng)請求。(Java 框架的初始化模式)。

另一方面,當(dāng)有可用的 lambda 容器時(shí)會發(fā)生熱啟動。

冷啟動是我們有這種糟糕性能的主要原因,因?yàn)槊看卫鋯影l(fā)生時(shí),AWS 都會初始化我們的 Java 應(yīng)用程序,顯然,每個(gè)請求都需要很長時(shí)間。

AWS Lambda 冷啟動挑戰(zhàn)的可用解決方案

有兩種方法可以應(yīng)對這一基本挑戰(zhàn)。

  • 使用不屬于本文范圍的預(yù)配置并發(fā)。
  • 在應(yīng)用程序的初始化和響應(yīng)時(shí)間上獲得更好的性能帶來了如何在我們的 Java 應(yīng)用程序中實(shí)現(xiàn)更好性能的問題。答案是從我們的 Java 應(yīng)用程序創(chuàng)建一個(gè)本地二進(jìn)制可執(zhí)行文件,并使用Oracle GraalVM將其部署在 AWS Lambda 上。

GraalVM 是什么?

GraalVM 是一種高性能 JDK 發(fā)行版,旨在加速用 Java 和其他 JVM 語言編寫的應(yīng)用程序的執(zhí)行,同時(shí)支持 JavaScript、Ruby、Python 和許多其他流行語言。Native-Image 是一種提前技術(shù),可將 Java 代碼編譯為獨(dú)立的可執(zhí)行文件。此可執(zhí)行文件包括應(yīng)用程序類、其依賴項(xiàng)中的類、運(yùn)行時(shí)庫類以及來自 JDK 的靜態(tài)鏈接本機(jī)代碼。它不在 Java VM 上運(yùn)行,但包括來自不同運(yùn)行時(shí)系統(tǒng)(稱為“Substrate VM”)的必要組件,如內(nèi)存管理、線程調(diào)度等。

從 Java 應(yīng)用程序構(gòu)建本機(jī)二進(jìn)制可執(zhí)行文件

首先,我們需要安裝 GraalVM 及其 Native-Image 。然后,通過安裝 GraalVM,我們可以使用 GraalVM 將 Java 應(yīng)用程序轉(zhuǎn)換為原生二進(jìn)制可執(zhí)行文件。Quarkus 使它變得簡單,它有一個(gè) Maven/Gradle 插件,所以在一個(gè)典型的基于 Quarkus 的應(yīng)用程序中,我們將有一個(gè)名為native.

$ mvn clean install -Pnative

Maven 將根據(jù)您使用的操作系統(tǒng)構(gòu)建一個(gè)本地二進(jìn)制可執(zhí)行文件。如果你在 Windows 上開發(fā),這個(gè)文件將只能在 Windows 機(jī)器上運(yùn)行;但是,AWS Lambda 需要基于 Linux 的二進(jìn)制可執(zhí)行文件。在這種情況下,Quarkus 框架將通過其插件上的一個(gè)簡單參數(shù)來滿足此要求-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       ║
╚═════════╩═══════════════════╩════════════════════╝

所以我們之前通過java11(Corretto 11)在Lambda上部署了Java Application,并沒有表現(xiàn)出很好的性能。

對于 Lambda 上的純 Linux 平臺,我們目前有兩個(gè)選項(xiàng),它們是provided和provided.al2。

值得一提的是,provided會使用Amazon Linux,并且provided.al2會使用Amazon Linux 2,因此,由于版本2的長期支持,強(qiáng)烈推薦使用版本2。

在 AWS Lambda 上部署二進(jìn)制可執(zhí)行文件

正如我們所見,Quarkus 會為我們生成兩個(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問權(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

此命令會將二進(jìn)制文件作為 zip 格式上傳到 AWS 并將其部署為 Lambda 函數(shù),與 JVM 版本完全一樣?,F(xiàn)在,我們可以跳到令人興奮的部分,即監(jiān)控性能。

在 AWS Lambda + 自定義平臺上觀看演示應(yīng)用程序的性能

是時(shí)候運(yùn)行部署的 Lambda 函數(shù)、測試它并查看它的執(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 控制臺查看調(diào)用該函數(shù)的結(jié)果。

AWS 網(wǎng)絡(luò)控制臺

在 AWS Lambda 上分析 JVM 與原生二進(jìn)制的性能

我們可以在兩個(gè)類別中分析和比較 AWS Lambda 平臺上應(yīng)用程序的兩個(gè)版本。

  • 初始化時(shí)間:第一次調(diào)用或調(diào)用 Lambda 函數(shù)所消耗的時(shí)間稱為初始化時(shí)間。這幾乎是在 Lambda 上調(diào)用應(yīng)用程序的最長持續(xù)時(shí)間,因?yàn)樵诖穗A段我們的 Java 應(yīng)用程序?qū)念^開始。
AWS Lambda 平臺
  • JVM 和 Binary 版本之間存在相當(dāng)大的差異,這意味著原生二進(jìn)制版本的初始化時(shí)間幾乎比 JVM 版本快八倍。
  • 請求時(shí)間:我在初始化步驟后調(diào)用了 9 次 Lambda 函數(shù),這是性能結(jié)果。
AWS Lambda 平臺

根據(jù)結(jié)果??,JVM 版本和 Native 二進(jìn)制文件之間的性能存在顯著差異。

結(jié)論

Quarkus 框架將通過提供一些很好的特性,如依賴注入,幫助我們在 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 版本更好的請求時(shí)間,這意味著在 AWS Lambda 上可以節(jié)省更多時(shí)間。

總的來說,通過節(jié)省資源和時(shí)間,原生二進(jìn)制方法已被證明是一種低成本的選擇。


0 人點(diǎn)贊