用 C++ 写一个 AWS Lambda Hello World

AWS 自 2014 年推出 Lambda 时仅支持 Node.js,而后添加了对 Python, Ruby, Java, C#, F#, PowerShell 的支持,再来到 2018 年可以自定义运行时了,比如用性能较好的 C, C++, Rust, Go 等语言。见 AWS Lambda Now Supports Custom Runtimes and Enables Sharing Common Code Between Functions.

如果使用 Python, Java 写 Lambda 时觉得还不得快,不想要明显的预热过程,也许 1000 毫秒的任务只想要 600 毫秒就能完成,内存还希望再压缩一些,那着实能在每月千百万次 Lambda 调用的情况下节省一笔可观的支出,那么可以试一试 C, C++, Rust, Go 等编译成了机器指令的语言,况且前三者没有 GC, 执行效率会更高。

本日志记录一下如何用 C++ 创建一个 AWS Lambda, 以及可如何应付 Lambda 的复用。本文主要参考自下面两处

  1. Introducing the C++ Lambda Runtime
  2. GitHub 项目 aws-lambda-cpp

自定义运行时可选择 X86_64 或 arm64 的 Amazon Linux 2023 或 Amazon Linux 2。部署时可选择的 runtime 相应有 provided.al2023, provided.al2, 推荐使用 provided.al2023。runtime provided 不被支持了。

C++ 代码可选择用 GCC 或 Clang 来编译,既然 AWS Lambda 实际的运行时会用到 Amazon Linux 2023,那我们就直接选择 Docker 镜像 amazonlinux:2023 作为我们的编译环境。

设定好工作环境

$ docker run -it amazonlinux:2023 bash

进到 Amazon Linux 容器的 Shell, 或者我们方便本地与 Docker 容器之间共享文件,可以这么做

$ mkdir work && cd work
$ docker run -it -v $(pwd):/work -w /work amazonlinux:2023 bash 

然后在容器中安装必需的组件(为区主机与容器的 Shell, 容器的 Shell 就不用提示符了, 也方便复制)

yum 安装的 gcc 版本是 11

注:

  1. aws-lambda-cpp 项目中有 CMakeLists.txt 文件,所以应用 cmake 来生成 Makefile 来编译
  2. 只生成 Release 版本的静态库,Debug 版本用 -DCMAKE_BUILD_TYPE=Debug
  3. cmake 配置命令也可以加上 -DCMAKE_INSTALL_PREFIX 参数,让生成的静态文件和安装的文件不污染系统,完整的 cmake
    cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=~/lambda-install
  4. make 命令可用 cmake --build . 替代
  5. make install 相应的 cmake 命令是 cmake --install .
  6. 无需导出 CC 和 CXX 环境变量(export CC=gcc export CXX=g++)

make(cmake --build .) 和 make install(cmake --install .) 执行的效果是

bash-5.2# cmake --build .
[ 20%] Building CXX object CMakeFiles/aws-lambda-runtime.dir/src/logging.cpp.o
[ 40%] Building CXX object CMakeFiles/aws-lambda-runtime.dir/src/runtime.cpp.o
[ 60%] Building CXX object CMakeFiles/aws-lambda-runtime.dir/src/backward.cpp.o
[ 80%] Building CXX object CMakeFiles/aws-lambda-runtime.dir/version.cpp.o
[100%] Linking CXX static library libaws-lambda-runtime.a
[100%] Built target aws-lambda-runtime
bash-5.2# cmake --install .
-- Install configuration: "Release"
-- Up-to-date: /usr/local/include/aws/http/response.h
-- Up-to-date: /usr/local/include/aws/lambda-runtime/runtime.h
-- Up-to-date: /usr/local/include/aws/lambda-runtime/version.h
-- Up-to-date: /usr/local/include/aws/lambda-runtime/outcome.h
-- Up-to-date: /usr/local/include/aws/logging/logging.h
-- Installing: /usr/local/lib64/libaws-lambda-runtime.a
-- Installing: /usr/local/lib64/aws-lambda-runtime/cmake/aws-lambda-runtime-targets.cmake
-- Installing: /usr/local/lib64/aws-lambda-runtime/cmake/aws-lambda-runtime-targets-release.cmake
-- Installing: /usr/local/lib64/aws-lambda-runtime/cmake/aws-lambda-runtime-config.cmake
-- Installing: /usr/local/lib64/aws-lambda-runtime/cmake/aws-lambda-runtime-config-version.cmake
-- Up-to-date: /usr/local/lib64/aws-lambda-runtime/cmake/packager

我们从中了解了 aws-lambda-runtime 会生成什么,主要的就是静态库  /usr/local/lib64/libaws-lambda-runtime.a,如果 cmake 配置时指定了  -DCMAKE_INSTALL_PREFIX=~/lambda-install, 这个文件就会安装到 ~/lambda-install 目录下。

创建自己的 C++ Lambda 项目

准备好了 aws-lambda-runtime 运行时库后,现在开始创建 cpp-lambda-helloworld 项目,选择自己的工作目录

mkdir cpp-lambda-helloworld && cd cpp-lambda-helloworld

然后在其中创建 main.cpp 文件,内容如下

为方便在容器中编辑文件,我们可在容器中安装 vim(yum install -y vim)。

再创建 CMakeLists.txt 文件,内容

编译构建生成 Lambda zip 包

这时在 build 目录中生成了 cpp-lambda-helloworld 可执行文件。如果现在有点好奇,执行一下它看看

bash-5.2# ./cpp-lambda-helloworld
[ERROR] [1717865932497] LAMBDA_RUNTIME Failed to get next invocation. No Response from endpoint "http:///2018-06-01/runtime/invocation/next"
[ERROR] [1717865932505] LAMBDA_RUNTIME Failed to get next invocation. No Response from endpoint "http:///2018-06-01/runtime/invocation/next"
[ERROR] [1717865932510] LAMBDA_RUNTIME Failed to get next invocation. No Response from endpoint "http:///2018-06-01/runtime/invocation/next"
[ERROR] [1717865932510] LAMBDA_RUNTIME Exhausted all retries. This is probably a bug in libcurl v8.5.0 Exiting!

这是正常的,Lambda 本来就不是本地执行的,配置好某些环境变量,本地应该可以进行测试。要部署到 AWS,我们还需要生成一个包含该执行文件 cpp-lambda-hellworld 的 zip 包,用命令

或者用 cmake 命令

cmake --build . --target aws-lambda-package-wfe_lambda_cpp # 或 make aws-lambda-package-cpp-lambda-hellworld

前面用  cmake .. -DCMAKE_BUILD_TYPE=Release 在 build 目录中生成的 Makefile 中可以找到这个 target aws-lambda-package-cpp-lambda-hellworld, 即 aws-lambda-package-${PROJECT_NAME}

target aws-lambda-package-cpp-lambda-hellworld 会生成一个压缩包 cpp-lambda-helloworld.zip, 我们大概看一下其中的内容

lib 目录中包含众多的动态库,其中主要的文件就是 bootstrap 和 bin/cpp-lambda-helloworld. bootstrap 是 AWS Lambda 运行时的入口,它将会调用 bin/cpp-lambda-helloworld

部署 C++ AWS Lambda

我们跳过创建 IAM role 的过程,比如可以选择一个现有的可执行 AWS Lambda 的 role, 之后用 aws lambda create-function 命令创建 Lambda

aws lambda create-function \
 --function-name cpp-lambda-helloworld \
 --role arn:aws:iam::123456789000:role/demo_lambda_role \
 --runtime provided.al2023 \
 --handler cpp-lambda-helloworld \
 --zip-file fileb://~/work/cpp-lambda-helloworld/build/cpp-lambda-helloworld.zip

默认内存为  128M, Timeout 是 3 秒,默认 CPU 架构是 x86_64,handler 为 C++ 可执行文件名

测试 Lambda

也可以在 AWS Lambda Web 控制台进行测试

C++ 的 AWS Lambda 工作正常。

测试验证 Lambda 实例的利用

我们知道 Lambda 在短期内,这个时间是不确定的,由调用模式和 AWS 内部资源管理来定。但基本上可认定几秒钟之类的连续调用会重用之前启动的 Lambda 实例,而无需冷启动。前后调用重用 Lambda 实例的最大好处是可以在后续调用使用前面实例的缓存和已生成(比如下载)的临时文件,这对提升 Lambda 的性特别重要,烦重的任务无须每次都做。

下面就来测试 C++ Lambda 用全局变量来缓存数据,声明一个全局计数器来统计当前 Lambda 实例被调用的次数。

在 main.cpp 中加一个全局变量 count, 新的 main.cpp 代码如下

用 aws lambda delete-function --function-name cpp-lambda-helloworld 删除然后重新创建 cpp-lambda-helloworld, 现在调用多次

说明 Lambda 实例在某个时期内也是可以被重用的,如果等待一段时间,比如 20 分钟,再调用的话会使用一个新的 Lambda, count 又会从 0 开始计数。

如果启用 Lambda 的 Function URL, 用 curl 调用

可选择其他 Linux 发行版

只要能生成 Lambda 需要的 x86_64 架构的二进制代码,可以选择任何 x86_64 的 Linux 发行版来创建 Lambda 发布包。当然如果部署 Lambda 选择的是 arm64, 那么编译的环境也最好是 arm64, 不然就尝试用交叉编译吧(总觉得不那么可靠)。

比如有些人钟爱于 Debian 系的 Ubuntu 来做开发,就是喜欢 apt 胜过于 yum,可选用 Ubuntu 24.04

$ docker run -it ubuntu:24.04

安装后 gcc/g++ 的版本为 13,无需导出 CC=gcc 和  CXX=g++

一个常见问题

如果访问 Lambda 时出现错误

{
    "errorType": "Runtime.InvalidEntrypoint",
    "errorMessage": "RequestId: cbc69b13-0187-49f2-8e65-ec2e398360b8 Error: Couldn't find valid bootstrap(s): [/var/task/bootstrap /opt/bootstrap]"
}

基本上就是在 Lambda 的 zip 包根目录中没有可执行权限的 bootstrap 文件。

其他延伸

其他剩下的就是如何处理请求数据,如何在 C++ 中使用 AWS 的那路 SDK。比如产生失败的响应可以用

针对它的测试与输出

在使用 request.payload 时默认要求 payload 是 base64 的字符串,要直接传入纯文本用 --cli-binary-format raw-in-base64-out 参数。

实际操练时那就是真正考验 C++ 的功底了。

要获得更详细的日志信息,执行 cmake 配置 aws-lambda-cpp 项目时可用 -DCMAKE_BUILD_TYPE=Debug,或在 -DCMAKE_BUILD_TYPE=Release 时指定 -DLOG_VERBOSITY=3

在 Lambda crash 时看到更详细的错误栈信息,要在编译 aws-lambda-cpp 前在系统中安装 libdw 和 binutils

  1. Debian 系统:apt install libdw-dev binutils-dev
  2. RedHat 系统:yum install elfutils-devel binutils-devel

CMake 会自动检测到它并进行连接

看到不能在 std::string 和 Aws::String 之间转换,可以关闭 AWS C++ SDK 的自定义内存管理或用 -BUILD_SHARED_LIBS=OFF 选项编译。

我们也可以在 Mac OS X 下进行,在 Intel 芯片的 Mac OS X 下安装了 gcc(Apple clang version 15.0.0 (clang-1500.3.9.4))。以上 aws-lambda-cpp 和 cpp-lambda-helloworld 除了 make aws-lambda-package-cpp-lambda-helloworld 之外可以正常执行,因为 aws-lambda-package-xxx 是要明确生成包含  Linux 下二进制代码的压缩包。

因此我们用 Mac OS X 开发调试 C++ 的 AWS Lambda 代码,最后交给 Linux 完成最终的编译打包。

 

链接:

  1. Introducing the Amazon Linux 2023 runtime for AWS Lambda
  2. Introducing the C++ Lambda Runtime(其中介绍了如何编译和使用 aws-sdk-cpp)

本文链接 https://yanbin.blog/cpp-aws-lambda-hello-world/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments