节省 AWS Lambda Amazon CloudWatch Logs 成本

2025-06-11

节省 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 函数。

技术

我们实现我之前提到的日志涅槃的方法出奇地并不复杂(也许我忽略了什么?如果是这样,请告诉我!)。

基本思路如下:

  1. 像以前一样使用您选择的记录器。
  2. 默认情况下log.debug,调用不会被推送到 STDOUT。相反,我们会将它们保存在内存中。
  3. 呼叫log.info被推送至 STDOUT。
  4. 每当发生错误时(在我们的例子中是随后的调用)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...
}
Enter fullscreen mode Exit fullscreen mode

代码未以任何形式进行检测。让我们通过添加日志记录来改变这种情况。

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", {...})
}
Enter fullscreen mode Exit fullscreen mode

我在我们的代码中添加了相当多的日志记录。

请记住,此示例仅用于演示目的。在现实世界中,您很可能会有一个日志中间件来记录部分请求和响应。

如果我们允许每个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));
  }
};
Enter fullscreen mode Exit fullscreen mode

对象log方法是各种方法的门面console。函数不是console.debug立即调用,而是debug将一个函数推送到debugCallsBuffer数组中。当我们调用该error函数时,数组debugCallsBuffer会被“刷新”。

您很可能在日常工作中使用第三方包进行日志记录,以确保生成的日志消息结构化。尽管如此,这个想法仍然有效。

太棒了!现在您可以log.debug随心所欲地编写语句,而不必担心每次 AWS Lambda 函数执行都会记录大量数据。

log在开始使用该对象进行日志记录之前,我们还需要考虑两件事。

  1. 当 AWS Lambda 成功执行时,我们如何“刷新” debugCallsBuffer?否则,我们将面临记录以前 AWS Lambda 执行的调试消息的风险!
  2. 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 = []
+ }
};
Enter fullscreen mode Exit fullscreen mode

在我们的 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();
+ }
};
Enter fullscreen mode Exit fullscreen mode

再次强调,这段代码非常人为,但要点仍然成立。如果每次执行后没有“清除”缓冲区,您可能会在 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...
}
Enter fullscreen mode Exit fullscreen mode

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
PREV
使用 React Native、Expo 和 AWS Amplify 实现推送通知的指南总结
NEXT
使用 Terraform、Terragrun 和 GitHub Actions 配置 EKS 集群