将处理MongoDB的Express应用程序部署到AWS Lambda上,实现无服务器化
首先
通过使用AWS Lambda将后端应用变成无服务器的形式,可以在可用性、成本和扩展性方面获得优势。
如果涉及与数据库进行交互的后端应用,以前人们会担心数据库连接所需的时间,另外Lambda的冷启动速度也是一个问题。然而,最近(大约在2020年左右),这些问题已经得到相当大的解决,将后端应用程序构建成无服务器架构变得相当现实。
在这篇文章中,我会介绍使用Lambda实现将DB转变为MongoDB、并将后端应用写成Express的无服务器化流程。我们将假设创建的Lambda函数通过API Gateway进行公开。
重新利用与MongoDB的连接。
应对DB连接问题的方法在MongoDB的官方文档中有总结。
连接AWS Lambda的最佳实践
重点有两个。
将DB连接的变量放置在Lambda处理程序的外部
首先,将包含DB连接的部分变量定义在Lambda的处理程序函数之外是重点之一。在处理程序之外定义的变量将在同一容器上执行多次的Lambda调用中被重复使用,可以将其视为缓存。因此,可以重复使用连接。
MongoDB的文档中包含了详细的示例代码,但概念上大致如下。
let cachedDb = null; // handlerの外側に定義しておく。
module.exports.handler = (event, context, callback) => {
if (!cachedDb) {
cachedDb = // ここでDBコネクションを作る。
}
// 以下略
};
在上面的例子中,我们将DB连接(cachedDb)本身设置为变量,但实际上可能会将包含DB连接的Express应用(app)设置为变量。这样做的好处是不需要每次都创建app(相比于连接创建所需的时间,这只是微不足道的节省)。
然而,只有在同一个容器上运行Lambda函数时才能实现此重用。如果同时有许多Lambda调用,则会创建新容器等来应对它(称为冷启动)。在新创建的容器的首次调用中,不能实现重用,需要创建新的数据库连接。
将callbackWaitsForEmptyEventLoop设置为false。
第二点是在处理程序函数中将 context.callbackWaitsForEmptyEventLoop 这个设置设为 false。关于这个设置的详细说明可以在 MongoDB 的文档中找到,但不论如何,我们简单地按照指示将其设为 false。
// 前略
module.exports.handler = (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false; // これ
// 以下略
};
使用AWS Serverless Express,将Express应用程序与API Gateway进行”Lambda代理集成”。
将API Gateway放在Lambda之前并公开是一种常见的模式。在这种情况下,连接Lambda和API Gateway的简单方法是”Lambda代理集成”。简单说,这就是API Gateway对Lambda函数发出请求的接口规范。
Lambda Proxy集成只是其中一种方法,所以并不一定非要使用它,但它很方便,所以值得推荐。有关详细信息,请参考以下文章。
使用或不使用API Gateway + Lambda代理联接所导致的差异。
在处理Express应用程序时,您可以使用这里的AWS Serverless Express库。使用这个库,它可以根据Lambda代理集成规则自动调整Express应用程序在Lambda上的运行。您可以将其想象为下图所示的转换代理。
有了AWS Serverless Express,Express应用程序开发人员将不再需要考虑Lambda集成代理规范。他们可以简单地将平时处理的应用程序直接部署到Lambda上。
只需按照GitHub上的README文件操作即可了解如何使用AWS Serverless Express。但是请注意,如果想将Lambda的处理函数设置为async,请参考这里的用法。
根据以上情况,以下是一个处理函数的实现示例。
MongoDBのコネクション再利用のテクニックと、AWS Serverless Expressを交えて、ハンドラ関数実装の具体例を載せておきます。TypeScriptで書いた例です。
import { Handler } from "aws-lambda";
import * as awsServerlessExpress from "aws-serverless-express";
// これは何らかの自前の実装。
// この中で、MongoDBへのコネクション作成なども行っているという想定。よってasync関数。
import { createAppAsync } from "./app";
// createServerを呼んだ最終結果を代入しておくための変数を、handlerの外側で定義しておく。
let server: ReturnType<typeof awsServerlessExpress.createServer>;
export const handler: Handler = async (event, context) => { // handlerをasyncにした例
context.callbackWaitsForEmptyEventLoop = false; // セオリー通りにfalse
if (!server) { // Lambdaがコールドスタートしたときは、このif文の中に入る。
const app = await createAppAsync(); // MongoDBに接続しつつ、Expressのappを作って・・・
server = awsServerlessExpress.createServer(app); // AWS Serverless Expressに渡す。
}
// appをAWS Serverless Expressに渡したので、あとはよしなにやってもらうだけ。
return awsServerlessExpress.proxy(server, event, context, "PROMISE").promise;
// 上記は、handlerがasyncの場合の呼び方。
// 4番目の引数 "PROMISE" をつけたり、末尾に .promise をつけたりする。
};
実装ができたら、コードをLambdaにデプロイしたり、API GatewayのLambdaプロキシ統合の設定をしたりして、APIとして公開しましょう。このあたりの詳細は、この記事では割愛します。
最近,冷启动问题已逐渐变得可以接受的范围之内。
到目前为止,我们成功地通过重新利用与MongoDB的连接,并且在Lambda函数执行时重复使用容器(即热启动),以提高性能效率。
虽然如此,至少在第一次调用时会发生冷启动。冷启动会比较慢,但由于Lambda本身的改进,最近(2020年)似乎有了很大的改善。
試しに自分で作ったLambda関数を実行したところ、コールドスタートとウォームスタートの差は1〜2秒くらいで、現実的に十分許容できる印象です。先ほどのMongoDBの公式ドキュメントにおいても、通常1秒以内と言われていました(参照)。
コールドスタートに関する近況については、以下の記事が参考になります。VPC Lambda(通常と違って、自分のVPC内で実行するLambda)の場合のオーバーヘッドも改善されていることや、Provisioned Concurrencyによってお金の力でコールドスタート頻度を下げられること(自前の暖機運転が不要になること)も触れられています。
重新整理Lambda的冷启动情况
以下是用中文本地化後的同義句:“总结”
介绍了将访问MongoDB的Express应用程序通过Lambda和API Gateway实现无服务器化的流程。重点如下:
-
- ウォームスタート時に再利用したい変数をハンドラ関数の外側に定義したり、callbackWaitsForEmptyEventLoopをfalseにすることで、DBコネクションを再利用する。
-
- AWS Serverless Expressを使うと、ExpressアプリをLambdaプロキシ統合の仕組みに簡単に従わせられる。LambdaとAPI Gatewayを簡単に繋げられる。
- 2019年頃からLambdaが根本的に進歩しており、コールドスタートの問題も許容範囲内になっている。