节省 AWS Lambda Amazon CloudWatch Logs 成本
AWS Lambda是众多无服务器开发者的首选计算服务。它功能强大且灵活,允许使用各种编程语言执行任意代码。但这项服务并非总是一帆风顺——容易出现故障。一旦发生故障,了解故障原因至关重要。
了解 AWS Lambda 执行周期的一个常用方法是记录日志。如果谨慎实施,它可以为您节省数小时的调试时间,但它也是一把双刃剑。记录的日志越多,您需要为Amazon CloudWatch Logs支付的费用就越高。
那么,如何才能鱼与熊掌兼得呢?如何确保我们的日志足以帮助我们调试潜在的 AWS Lambda 问题,同时又能降低成本?
这篇博文将展示我所知道的一种技术,在我个人看来,它非常适合这个特定的用例。
让我们开始吧。
本博文中的所有代码均使用 TypeScript 编写。请将代码片段视为伪代码,而非实际实现。
极端的问题
根据我的经验,开发人员容易犯的一个错误就是走极端——要么记录所有内容,要么根本不记录。从操作角度来看,这两种情况都是有问题的。
记录每一个动作无疑会给你一种温暖舒适的感觉,因为如果发生什么事,所有细节都一目了然。希望这能让你快速发现任何潜在问题,并立即解决。
这种方法的问题在于,你的 Amazon CloudWatch 账单很可能会非常高昂(相对于你使用的其他服务而言)。我见过一些无服务器应用程序,其中 Amazon CloudWatch 占了 AWS 账单的 80%。这不太理想。
没有任何日志记录或记录量极少也会带来灾难性的影响。这一次,问题不再是 AWS CloudWatch 的成本,而是调试 AWS Lambda 函数中的问题所需的潜在时间。每个错误都会变成一个谋杀谜案,需要花费数小时才能解决。这可不是什么理想的情况。
在理想情况下,当一切正常时,我们可以只记录最少量的内容,而当发生错误时,则记录非常详细的日志。事实证明,“理想世界”是可以实现的,而且根据我的个人经验,无论使用哪种编程语言,它都适用于大多数 AWS Lambda 函数。
技术
我们实现我之前提到的日志涅槃的方法出奇地并不复杂(也许我忽略了什么?如果是这样,请告诉我!)。
基本思路如下:
- 像以前一样使用您选择的记录器。
- 默认情况下
log.debug
,调用不会被推送到 STDOUT。相反,我们会将它们保存在内存中。 - 呼叫
log.info
将被推送至 STDOUT。 - 每当发生错误时(在我们的例子中是随后的调用)
log.error
,记录器就会释放所有log.debug
消息以及该log.error
消息。
有了这样的设置,如果 AWS Lambda 函数执行成功,我们只需支付log.info
数据费用(以及其他费用,具体取决于您的设置)。相反,如果出现问题,我们将获得调试执行所需的所有上下文信息。
现在开始实施。
实施
这是一个非常精心设计的 AWS Lambda 函数示例。
import { APIGatewayProxyEvent, APIGatewayProxyHandler } from "aws-lambda";
export const handler: APIGatewayProxyHandler = async event => {
try {
const data = await performWork(event);
const result = await performOtherWork(data);
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (e) {
return {
statusCode: 500,
message: "Something blew up!"
};
}
};
async function performWork(event: APIGatewayProxyEvent) {
// implementation...
}
async function performOtherWork(data: unknown) {
// implementation...
}
代码未以任何形式进行检测。让我们通过添加日志记录来改变这种情况。
import { APIGatewayProxyEvent, APIGatewayProxyHandler } from "aws-lambda";
+ import { log } from './logger'
import { APIGatewayProxyEvent, APIGatewayProxyHandler } from "aws-lambda";
export const handler: APIGatewayProxyHandler = async event => {
+ log.info("event", event);
try {
const data = await performWork(event);
const result = await performOtherWork(data);
+ log.info("response", {...})
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (e) {
+ log.error("error!", e)
return {
statusCode: 500,
message: "Something blew up!"
};
}
};
async function performWork(event: APIGatewayProxyEvent) {
+ log.debug("entering performWork", event)
// implementation...
+ log.debug("exiting performWork", {...})
}
async function performOtherWork(data: unknown) {
+ log.debug("entering performOtherWork", data)
// implementation...
+ log.debug("exiting performOtherWork", {...})
}
我在我们的代码中添加了相当多的日志记录。
请记住,此示例仅用于演示目的。在现实世界中,您很可能会有一个日志中间件来记录部分请求和响应。
如果我们允许每个log.X
调用都推送到 STDOUT,那么在流量足够高的情况下,我们的Amazon CloudWatch 账单可能会相当高。
Amazon CloudWatch Logs 的定价有很多维度。其中之一就是日志消息的大小,在本例中我并没有关注它。更多详情,请查看Amazon CloudWatch 定价页面。
但是我们的记录器有一个我之前提到过的小技巧。默认情况下,调用log.debug
不会被推送到 STDOUT。以下是记录器的一种人为实现。
// logger.ts
export const log = {
debugCallsBuffer: [] as (() => void)[],
info(message: string, ...args: any[]) {
console.log(message, ...args);
},
error(message: string, ...args: any[]) {
console.error(message, ...args);
console.log("Debug buffer");
this.debugCallsBuffer.forEach(debugLogCall => debugLogCall());
},
debug(message: string, ...args: any[]) {
this.debugCallsBuffer.push(() => console.debug(message, ...args));
}
};
对象log
方法是各种方法的门面console
。函数不是console.debug
立即调用,而是debug
将一个函数推送到debugCallsBuffer
数组中。当我们调用该error
函数时,数组debugCallsBuffer
会被“刷新”。
您很可能在日常工作中使用第三方包进行日志记录,以确保生成的日志消息结构化。尽管如此,这个想法仍然有效。
太棒了!现在您可以log.debug
随心所欲地编写语句,而不必担心每次 AWS Lambda 函数执行都会记录大量数据。
log
在开始使用该对象进行日志记录之前,我们还需要考虑两件事。
- 当 AWS Lambda 成功执行时,我们如何“刷新”
debugCallsBuffer
?否则,我们将面临记录以前 AWS Lambda 执行的调试消息的风险! debugCallsBuffer
当您的 AWS Lambda 即将超时时,我们如何“刷新” ?
AWS Lambda 成功执行后刷新
如果您阅读AWS Lambda 执行环境的文档,您将了解执行环境重用、冻结和解冻执行环境的概念。这些概念对于理解本节背后的原因至关重要,因此如果您还没有读过,请考虑阅读一下。
如果 AWS Lambda 服务没有复用执行环境,我们就无法在 AWS Lambda 运行时级别进行缓存。有关在 AWS Lambda 运行时级别进行缓存的更多信息,请阅读本文或本文。
以下是我们在本节中试图解决的问题的直观表示。
如果我们没有debugCallsBuffer
在 AWS Lambda 成功时清除,我们可能会在上次执行出错时记录调试消息!这并不理想。
这个问题的解决方案非常依赖于具体的实现。为我们设计的日志记录器示例添加一个清晰的函数就可以了。
// logger.ts
export const log = {
debugCallsBuffer: [] as (() => void)[],
info(message: string, ...args: any[]) {
console.log(message, ...args);
},
error(message: string, ...args: any[]) {
console.error(message, ...args);
console.log("Debug buffer");
this.debugCallsBuffer.forEach(debugLogCall => debugLogCall());
},
debug(message: string, ...args: any[]) {
this.debugCallsBuffer.push(() => console.debug(message, ...args));
},
+ clear() {
+ this.debugCallsBuffer = []
+ }
};
在我们的 AWS Lambda 处理程序代码中...
import { APIGatewayProxyEvent, APIGatewayProxyHandler } from "aws-lambda";
import { log } from './logger'
import { APIGatewayProxyEvent, APIGatewayProxyHandler } from "aws-lambda";
export const handler: APIGatewayProxyHandler = async event => {
log.info("event", event);
try {
const data = await performWork(event);
const result = await performOtherWork(data);
log.info("response", {...})
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (e) {
log.error("error!", e)
return {
statusCode: 500,
message: "Something blew up!"
};
+ } finally {
+ log.clear();
+ }
};
再次强调,这段代码非常人为,但要点仍然成立。如果每次执行后没有“清除”缓冲区,您可能会在 AWS CloudWatch 日志控制台中看到奇怪的日志。
在 AWS Lambda 超时之前刷新
AWS Lambda 是一项有时间限制的计算服务。截至撰写本文时,AWS Lambda 函数的最长运行时长为15 分钟。当然,此设置是可配置的。
如果您正在使用框架来部署和开发无服务器应用程序(依我拙见,您应该这样做),您使用的框架可能会为您设置默认函数超时。请注意这一点。
我认为,如果我的 AWS Lambda 函数即将超时,最好从 中释放所有日志debugCallsBuffer
。函数超时将导致错误。如果该函数使用Amazon API Gateway前端,Amazon API Gateway 将向调用者返回 500 状态码(除非您已将其配置为其他方式)。有了所有可用的日志,我修复超时问题的可能性就会更高。
要了解 AWS Lambda 函数是否即将超时,我们可以使用从服务传递给 AWS Lambda 函数的上下文getRemainingTimeInMillis
中检索到的函数。以下是在 AWS Lambda 即将超时之前记录错误(进而释放日志)的示例实现。debug
import {
APIGatewayProxyEvent,
APIGatewayProxyHandler,
Context
} from "aws-lambda";
import { log } from "./logger";
const logBeforeTimeout = (context: Context) => {
const deadline = context.getRemainingTimeInMillis() - 100;
const timeoutId = setTimeout(() => {
log.error("About to timeout");
}, deadline);
return () => clearTimeout(timeoutId);
};
export const handler: APIGatewayProxyHandler = async (event, context) => {
const cleanup = logBeforeTimeout(context);
try {
const result = await performWork(event);
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (e) {
return {
statusCode: 500,
message: "Something blew up!"
};
} finally {
cleanup();
}
};
async function performWork(event: APIGatewayProxyEvent) {
// implementation...
}
该logBeforeTimeout
函数启动一个计时器,并在截止日期之前记录错误。无论执行是否成功,都必须清除计时器。如果不这样做,就会导致内存泄漏!
给 Node.js 用户提个小建议。计时器完全有可能
setTimeout
永远不运行。如果Node.js 事件循环被阻塞(想象一下代码中某个地方的无限循环),你的计时器就永远不会运行。我知道的一个解决方案是在单独的线程中运行计时器。
结束语
就是这样。这就是如何降低 AWS CloudWatch Logs 成本,并在问题发生时获取每行日志的方法。这篇博客侧重于理论,因为我想传达一个想法(我认为它与语言无关),而不是具体的实现。
你觉得这个方法怎么样?是不是不太好?也许你还有其他方案?我很乐意听取你的想法和意见。
欲了解更多无服务器内容,请在 Twitter 上关注我 - @wm_matuszewski。
感谢您宝贵的时间。祝您拥有美好的一天👋。
鏂囩珷鏉ユ簮锛�https://dev.to/aws-builders/ saving-on-aws-lambda-amazon-cloudwatch-logs-costs-51od