AWS Lambda 做API + DynamoDB做數(shù)據(jù)庫 + GraalVM增速 + Annotation Processor 代碼生成

我們知道 Java (JIT - Just In Time 編譯運行)比較耗內(nèi)存窟却,冷啟動時間比Go / Python / NodeJs 要長板熊。所以通常不是最佳的 Lambda / Serverless 語言。它最多的是用在長期待機處理大量請求的場景妥衣。但是Java這么好的生態(tài)以及工程性琉雳,不用在 Lambda上面太浪費了划乖。Python和NodeJs之所以快是因為輕量辆沦,Go之所以快是因為AOT(有人說Go編譯也很快)昼捍。那Java現(xiàn)在也有GraalVM可以做AOT (Ahead-Of-Time)編譯,Java程序員是不是也迎來了Lambda計算的春天呢肢扯?想像一下妒茬,編寫調(diào)試時候有JIT和Junit,發(fā)布的時候有AOT蔚晨,挺美好的乍钻!

一般的數(shù)據(jù)庫連接都是長連接,Lambda 雖然也可以有provisioned instance, 但那樣還不如買vm. 所以長連接的高性能數(shù)據(jù)庫是不用想了铭腕。也只能用DynamoDB這種基于HTTPs的數(shù)據(jù)庫可以結(jié)合使用银择。
山哥做了很多調(diào)研和測試之后,發(fā)現(xiàn) Java11 Corretto 那個runtime谨履,用AWS SDK v2, 初次調(diào)用GetItem, 至少也要4秒多欢摄。有個外國老哥用 AWS SDK v1, 測出來的結(jié)果是11秒多熬丧。(這位外國老哥做了一個很好的開始并且文章寫得圖文并茂很專業(yè)笋粟。抄了他不少代碼怀挠,其中有些代碼不work了,也一一改正過來害捕。https://arnoldgalovics.com/java-cold-start-aws-lambda-graalvm/

AWS 官網(wǎng)和博客都有專門的文章教我們怎么減少jar 體積和獲得更快的冷啟動時間绿淋。山哥都試過了,收效不大尝盼,所以最終還是朝著GraalVM這個方向才取得了顯著的成果吞滞。

Milestone 1

在做了大量的調(diào)試之后,在Java11 Corretto Runtime上盾沫,已經(jīng)無法再優(yōu)化了裁赠,讓我們稱之為里程碑 1。

  • 初始運行時間:4 秒
  • 后續(xù)請求:200 ~ 400 毫秒
  • 最少運行內(nèi)存 136 MB

結(jié)論: 如果用 Java Runtime, 更適合的是處理批量請求而不是交互性請求赴精。

探索 Lambda 的Custom Runtime

學(xué)習(xí)階段

通過研究AWS Lambda的文檔佩捞,我們發(fā)現(xiàn)它需要的是一個boostrap的可執(zhí)行文件。
比如可以用Shell來寫Lambda

Example function.zip

.
├── bootstrap
├── function.sh

Custom Runtime 的原理是:

  • 用bootstrap來啟動你的程序
  • 有任何的返回或者想報錯蕾哟,通過調(diào)用lambda提供的幾個http API來實現(xiàn)一忱。(可以用CURL來調(diào)用)

用 Java 實現(xiàn)階段

設(shè)想:我們可以實現(xiàn)一個 Java 寫的 runtime library, 幫忙去調(diào)用 lambda的那幾個HTTP API, 那么你后續(xù)只要實現(xiàn)具體的業(yè)務(wù)代碼,就可以比較輕松地打包成為一個可執(zhí)行的 Jar了谭确,然后再用GraalVM編譯成為AOT帘营,就有望節(jié)省冷啟動時間。
實現(xiàn):代碼一抄一改逐哈,幾分鐘過去了.. Tada.. 上 Runtime 代碼:https://gitee.com/gzten/aws-lambda-java-runtime

對于業(yè)務(wù)邏輯芬迄,我們做個簡單的。

  • 啟動器 main 方法:
public class NativeDynamoDBApp {
    public static void main(String[] args) {
        new LambdaRuntime(Arrays.asList(
                new RequestHandlerRegistration<>(new APIGatewayRequestHandler(), APIGatewayProxyRequestEvent.class, APIGatewayProxyResponseEvent.class),
                new RequestHandlerRegistration<>(new IdentityRequestHandler(), String.class, APIGatewayProxyResponseEvent.class)
        )).run();
    }
}
  • 業(yè)務(wù)邏輯
    API gateway的參數(shù)獲取部分代碼還很爛鞠眉,有待優(yōu)化薯鼠。
@Slf4j
public class IdentityRequestHandler implements RequestHandler<String, APIGatewayProxyResponseEvent> {

    private static final String tableName = "gzten.user";
    private static final DynamoDbClient dynamo = DynamoDbClient.builder()
            .region(Region.AP_EAST_1)
            .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
            .httpClient(UrlConnectionHttpClient.builder().build())
            .build();

    @Override
    public APIGatewayProxyResponseEvent handleRequest(String input, Context context) {
        var resp = new APIGatewayProxyResponseEvent();
        resp.setStatusCode(200);
        resp.setIsBase64Encoded(false);
        resp.setHeaders(Map.of("Content-Type", "application/json"));

        log.info("Got input: {}", input);
        Map<String, String> param = JsonUtil.fromJson(input, Map.class);
        String username;
        if (param.containsKey("httpMethod")) {
            if (param.get("httpMethod").equals("POST")) {
                username = ((Map<String, String>)JsonUtil.fromJson(param.get("body"), Map.class)).get("id");
            } else {
                String s = String.format("Not supported method: %s", param.get("httpMethod"));
                log.info(s);
                resp.setStatusCode(400);
                resp.setBody(s);
                return resp;
            }
        } else {
            username = param.get("id");
        }

        var itemResp = dynamo.getItem(
                GetItemRequest.builder()
                    .tableName(tableName)
                    .key(singleMapWithString("username", pathParam["id"]!!))
                    .build()
            );
        if (itemResp.hasItem()) {
            String body = JsonUtil.toJson(itemResp.item());
            log.info("Got the item from DynamoDB at {} as {}", LocalDateTime.now(), body);
            resp.setBody(body);
        } else {
            String s = String.format("Not found object for id: %s", username);
            resp.setStatusCode(400);
            resp.setBody(s);
        }

        return resp;
    }

}

注意這個返回結(jié)果是這個樣子的:

{
  "username": {
    "S": "sam"
  },
  "password": {
    "S": "are you okay"
  },
  "lastUpdateTime": {
    "N": "1638672322225"
  },
  "creationTime": {
    "N": "1638672322225"
  }
}

你需要另外寫代碼來轉(zhuǎn)換成你想要的樣子。

GraalVM 增速階段

一般的代碼GraalVM是可以編譯的械蹋,但如果用了反射出皇,就要在reflect-config.json里面注明反射的要求,GraalVM有個Agent可以幫你自動生成這些信息哗戈。參考文章

$GRAAL_HOME/bin/java -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ -jar your.jar

GraalVM配置文件的目錄結(jié)構(gòu):
META-INF/native-image/${groupId}/${artifactId}

Screen Shot 2022-02-02 at 6.46.55 PM.png

咱先在Mac/Windows上面把Jar編譯通過郊艘,再想辦法搞成Amazon Linux 2的版本。國外的人直接用Maven / Gradle一次性搞定唯咬,我們這里網(wǎng)絡(luò)特殊纱注,還是手工做比較快。

GraalVM安裝:

到這里下載:https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-22.0.0.2

mkdir /opt/local/graalvm
cd /opt/local/graalvm
mv ~/Downloads/graalvm-ce-java11-darwin-amd64-22.0.0.2.tar.gz ./
tar -xzvf graalvm-ce-java11-darwin-amd64-22.0.0.2.tar.gz
mv ~/Downloads/native-image-installable-svm-java17-darwin-amd64-22.0.0.2.jar ./
export GRAAL_HOME=graalvm-ce-java11-22.0.0.2

# 本地安裝native image胆胰,在線裝太慢
$GRAAL_HOME/bin/gu install -L native-image-installable-svm-java17-darwin-amd64-22.0.0.2.jar

編譯命令:

$GRAAL_HOME/bin/native-image --verbose -jar aws-*.jar

注意

我們一般用蘋果或者Windows狞贱,AOT是直接編譯成平臺依賴的二進制代碼的,所以直接用GraalVM是不成的蜀涨,我們可以用Docker瞎嬉,裝一個AL2(Amazon Linux 2)的版本蝎毡,在里面編譯。
Docker 安裝:(先下載Linux amd64版的GraalVM氧枣,像上面Mac版那樣. 我們用的香港的Lambda目前只支持x86_64, 不支持ARM64指令集的Graviton2芯片)

# Default it is AMD64 version
docker pull amazon/aws-lambda-provided

# Prepare the graalvm
mkdir /opt/local/aws-lambda
cd /opt/local/aws-lambda
Download graalvm-ce-java11-linux-amd64-22.0.0.2.tar.gz from github
Download native-image-installable-svm-java11-linux-amd64-22.0.0.2.jar from github

tar -xzvf graalvm-ce-java11-linux-amd64-22.0.0.2.tar.gz

# Run docker
$ docker run --name graalvm-aws-lambda-j11 -v /opt/local/aws-lambda:/opt/local/aws-lambda -d amazon/aws-lambda-provided
$ docker ps
CONTAINER ID   IMAGE                        COMMAND                  CREATED      STATUS      PORTS     NAMES
b0778d64e5ac   amazon/aws-lambda-provided   "/lambda-entrypoint.…"   1 mins ago   Up 1 mins             graalvm-aws-lambda-j11

然后可以交互運行:

# Go to shell with interactive mode
docker exec -it b0778d64e5ac bash

cd /opt/local/aws-lambda
export JAVA_HOME=/opt/local/aws-lambda/graalvm-ce-java11-22.0.0.2
export PATH=$JAVA_HOME/bin:$PATH
gu install -L native-image-installable-svm-java11-linux-amd64-22.0.0.2.jar
mkdir work
cd work

開另外一個窗口沐兵,開始反復(fù)的手工操作:

  • Step 1, ?當(dāng)我們把Maven編譯出來Jar, Copy到Docker mount的文件夾里:
cd /opt/local/aws-lambda/work
cp ~/git/java/aws-lambda-java-dynamodb-native/sample/target/aws-lambda-java-dynamodb-native-sample-1.0.0.jar ./
  • Step 2,Docker里運行命令編譯:
# -O3是優(yōu)化便监,默認(rèn)是1級扎谎,3級會慢些但是性能更好
native-image --verbose -jar aws-*.jar -O3 -H:Name=function
  • Step 3,打包:
rm hey.zip && \
  zip hey.zip function bootstrap
  • Step 4, 上傳到AWS Lambda烧董,讓我們看看第一次如何Setup
  • 指定函數(shù)名
  • 選擇運行時為Amazon Linux 2自己引導(dǎo)
  • 設(shè)置角色毁靶,注意角色要有權(quán)限使用DynamoDB
  • All in one:
    Screen Shot 2022-02-02 at 7.13.39 PM.png
  • 上傳代碼并設(shè)置Handler 為 cn.gzten.lambda.dynamo.sample.IdentityRequestHandler::handleRequest::handleRequest不是必須,這里是跟非Custom Runtime的保持命名一致)
    Screen Shot 2022-02-02 at 7.17.07 PM.png

你可以用在線測試功能來測試它

Screen Shot 2022-02-02 at 7.20.19 PM.png

?
另外逊移,要設(shè)置 API Gateway 來觸發(fā)老充,那樣在本地就可以用POSTMAN試了!

優(yōu)化過的結(jié)果是:

  • 冷啟動700毫秒到1.1秒 (128M螟左,1024M大概600毫秒啡浊,沒必要)
  • 連續(xù)發(fā)請求的情況下,100-200毫秒
  • 內(nèi)存占用67M

工作效率提升篇 (Annotation Processor 代碼生成)

我們發(fā)現(xiàn)DynamoDB的數(shù)據(jù)結(jié)構(gòu)是Map<String, AttributeValue>, 這個很反人性胶背,你要花很大的工夫去轉(zhuǎn)成你想要的POJO巷嚣。
AWS DynamoDB有個Enhanced Client, 可以通過注解的模式,幫你做了轉(zhuǎn)換:
https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-dynamodb-enhanced.html
山哥試驗過后钳吟,發(fā)現(xiàn)在Java JIT下廷粒,用得挺爽的『烨遥可以一旦用GraalVM坝茎,你運行時就會得到這個報錯:com.oracle.svm.core.jdk.UnsupportedFeatureError: Defining anonymous classes at runtime is not supported.
即使我們把所有的類做了reflect-config.json還是報這個錯。那說明GraalVM對匿名類的支持是不足的而DynamoDB Enhanced Client 又用了這個暇番,兩害相權(quán)嗤放,忍痛放棄 這個Enhanced Client, 也節(jié)省了幾M空間。那咋辦呢壁酬?自己寫工具類做轉(zhuǎn)換吧次酌!可控!

業(yè)務(wù)POJO

package cn.gzten.lambda.dynamo.sample.data;

import cn.gzten.lambda.dynamo.annotation.DynamoBean;
import cn.gzten.lambda.dynamo.annotation.PartitionKey;
import com.fasterxml.jackson.annotation.*;
import lombok.Data;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Data
@ AppDynamoBean(tableName = "gzten.user")
public class AppUser {
    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");

    @ AppDynamoKey
    private String username;

    private String password;

    @JsonIgnore
    private Long lastUpdateTime;

    @JsonIgnore
    private Long creationTime;

    @JsonAlias("lastUpdateDateTime")
    public String getLastUpdateDateTime() {
        if (lastUpdateTime == null) {
            return "";
        }
        LocalDateTime dateTime = Instant.ofEpochMilli(lastUpdateTime)
                .atOffset(ZoneOffset.UTC)
                .toZonedDateTime().toLocalDateTime();
        return dateTime.format(DT_FMT);
    }

    @JsonAlias("creationDateTime")
    public String getCreationDateTime() {
        if (creationTime == null) {
            return "";
        }
        LocalDateTime dateTime = Instant.ofEpochMilli(creationTime)
                .atOffset(ZoneOffset.UTC)
                .toZonedDateTime().toLocalDateTime();
        return dateTime.format(DT_FMT);
    }

}


工具類舆乔,結(jié)合 JsonAttributeValueUtil

package cn.gzten.lambda.dynamo.sample.util;
package cn.gzten.lambda.dynamo.util;

import cn.gzten.lambda.dynamo.annotation.AppDynamoBean;
import cn.gzten.lambda.dynamo.annotation.AppDynamoKey;
import cn.gzten.lambda.runtime.exception.AppServiceException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;

import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;

public class AppTableMapper {
    private static final ObjectMapper OBJECT_MAPPER = com.fasterxml.jackson.databind.json.JsonMapper.builder()
            .configure(MapperFeature.USE_ANNOTATIONS, false)
            .build();
    public static <T> Optional<T> itemToBean(Map<String, AttributeValue> item, Class<T> clazz) {

        if (item == null) {
            return Optional.empty();
        }
        try {
            return Optional.of(OBJECT_MAPPER.treeToValue(JsonAttributeValueUtil.fromAttributeValue(item), clazz));
        } catch (JsonProcessingException e) {
            throw new AppServiceException(e);
        }
    }

    public static <T> Optional<T> getItem(DynamoDbClient dynamo, Class<T> clazz, String partitionKey) {
        AppDynamoBean beanAnnotation;
        try {
            beanAnnotation = clazz.getDeclaredAnnotation(AppDynamoBean.class);
            if (beanAnnotation == null) {
                throw new AppServiceException("Your class provided is not annotated with @AppDynamoBean!");
            }
        } catch (NullPointerException e) {
            throw new AppServiceException("Your class provided is not annotated with @AppDynamoBean!");
        }

        String tableName = beanAnnotation.tableName();
        Field[] fields = clazz.getDeclaredFields();
        String keyName = null;
        for(Field field : fields) {
            try {
                var keyAnnotation = field.getDeclaredAnnotation(AppDynamoKey.class);
                if (keyAnnotation != null) {
                    keyName = field.getName();
                    break;
                }
            } catch (NullPointerException e) {}
        }
        if (keyName == null) {
            throw new AppServiceException("Your class provided has no key field with @AppDynamoKey!");
        }
        var resp = dynamo.getItem(GetItemRequest.builder()
                .tableName(tableName)
                .key(singleMapWithString(keyName, partitionKey)).build());
        if (resp.hasItem()) {
            return itemToBean(resp.item(), clazz);
        } else {
            return Optional.empty();
        }
    }

    public static final Map<String, AttributeValue> singleMapWithString(final String key, final String value) {
        return Collections.singletonMap(key, AttributeValue.builder().s(value).build());
    }
}

那么你就可以這樣用:

Optional<AppUser> user = AppTableMapper. getItem(dynamoClient, AppUser.class, "my-user-name");

這已經(jīng)不錯了岳服,但是在獲取 tableName和Key的時候,用的是反射希俩,就算不是反射吊宋,因為最終返回變String用了Json序列化,還是要在GraalVM那里注冊reflect-config.json颜武。

有沒有辦法做這兩件事璃搜?

  • 自動生成Mapper方法文兑,而避免用反射
  • 對于注解了@AppDynamoBean的類,因為要變JSON腺劣,那自動注冊reflect-config.json而不是手動添加。

答案是有的因块!用Annotation Processor橘原,在編譯期生成一個AppUserDynamoBean,并且把AppUser注冊到reflect-config.json
詳情參看文章:http://www.reibang.com/p/e6516affa2c1

最終形態(tài) (AppUserDynamoBean是自動生成的涡上,不用編寫代碼趾断,根據(jù)AppUser的注解生成)

        Optional<AppUser> user = AppUserDynamoBean.getItem(dynamo, username);
        if (user.isPresent()) {
            String body = JsonUtil.toJson(user.get());
            log.info("Got the item from DynamoDB at {} as {}", LocalDateTime.now(), body);
            resp.setBody(body);
        } else {
            String s = String.format("Not found object for id: %s", username);
            resp.setStatusCode(400);
            resp.setBody(s);
        }

參考文檔:

Lambda 環(huán)境變量:https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市吩愧,隨后出現(xiàn)的幾起案子芋酌,更是在濱河造成了極大的恐慌,老刑警劉巖雁佳,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脐帝,死亡現(xiàn)場離奇詭異,居然都是意外死亡糖权,警方通過查閱死者的電腦和手機堵腹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來星澳,“玉大人疚顷,你說我怎么就攤上這事〗耍” “怎么了腿堤?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長如暖。 經(jīng)常有香客問我笆檀,道長,這世上最難降的妖魔是什么盒至? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任误债,我火速辦了婚禮,結(jié)果婚禮上妄迁,老公的妹妹穿的比我還像新娘寝蹈。我一直安慰自己,他們只是感情好登淘,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布箫老。 她就那樣靜靜地躺著,像睡著了一般黔州。 火紅的嫁衣襯著肌膚如雪耍鬓。 梳的紋絲不亂的頭發(fā)上阔籽,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音牲蜀,去河邊找鬼笆制。 笑死,一個胖子當(dāng)著我的面吹牛涣达,可吹牛的內(nèi)容都是我干的在辆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼度苔,長吁一口氣:“原來是場噩夢啊……” “哼匆篓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起寇窑,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鸦概,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后甩骏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窗市,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年饮笛,在試婚紗的時候發(fā)現(xiàn)自己被綠了谨设。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡缎浇,死狀恐怖扎拣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情素跺,我是刑警寧澤二蓝,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站指厌,受9級特大地震影響刊愚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜踩验,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一鸥诽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧箕憾,春花似錦牡借、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春碴里,著一層夾襖步出監(jiān)牢的瞬間沈矿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工咬腋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留羹膳,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓根竿,卻偏偏與公主長得像陵像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子犀填,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容