发布于 2026-01-06 0 阅读
0

正确实现大规模日志记录

正确实现大规模日志记录

如何避免分布式 Node.js 平台中的日志孤岛

原文发布于:https://blog.bitsrc.io/logging-at-scale-done-right-714896554d94

分布式平台非常适合解决许多问题,例如扩展性、高可用性,甚至是大型代码库的可维护性。

但尽管它们带来了诸多好处,在使用时也存在一些需要考虑的额外问题。本文将探讨其中之一:分布式日志记录。

因为在本地对单个服务进行日志记录很容易,但是当需要并行处理数十个甚至数百个服务时,事情就会变得有点复杂。

您的日志可能出现什么问题?

从单实例应用程序迁移到基于微服务的平台本身就是一个相当大的项目。

具体来说,在记录日志时,可能会出现一些问题:

  1. 碎片化的真相:这是最显而易见也是最常见的问题,您的日志文件保存在每台服务器的本地,因此,每当您需要查看发生了什么时,您只能获得部分信息。为了全面了解整个平台的运行状况,您需要手动收集所有日志文件,将它们合并并进行综合分析。

  2. 缺乏上下文:编写日志代码时,如果不考虑全局,另一个副作用是只关注单个进程。你可能无法记录诸如运行服务的服务器的 IP 地址或名称,或者任何给定时间有多少个实例处于活动状态等信息。当涉及多个组件时,上下文至关重要;而当只有一个组件时,上下文则不那么重要。

  3. 存储空间不足:除非您运行的是关键业务服务,否则您不会一直查看日志。因此,将日志存储在本地最终会填满您分配给它们的所有存储空间。即使您考虑轮换日志(例如使用 log-rotate),活动高峰期也会因为日志大小的快速增长而导致数据丢失。

我还可以继续说下去,但我想你现在已经明白我的意思了,日志记录有很多地方可能会出错,当出现问题并发现自己需要手动处理成千上万行日志时,你会特别后悔没有制定更好的日志记录策略。

为了尽量避免这些问题,我们或许应该考虑换一种方式来解决。

传统日志记录设置与可扩展设置对比传统日志记录设置与可扩展设置对比

可扩展日志记录策略有哪些好处?

可扩展的日志策略正如其名:您可以根据需要记录任意数量的日志。就像当平台流量激增时,您可以(也应该)扩展处理能力或带宽一样,您的日志记录能力也应该具有类似的弹性。

经验法则应该是:

平台运行负载越重,所需的日志记录就越多。

那么,可扩展战略的优势是什么呢?

  1. 首先,您不再受限于现有服务器的硬件。您可以在服务器上安装一个容量很小的硬盘,同时使用容量巨大的云存储来接收日志消息。

  2. 您的日志记录活动不会影响服务器的 I/O 操作。换句话说,您不会持续地写入磁盘,从而释放资源供应用程序的实际需求使用。

  3. 通过集中管理日志,可以更轻松地浏览和查看它们。您无需逐个服务器手动下载日志文件,然后再尝试合并它们才能查看。采用分布式方法,您可以将日志发送到其他地方,并通过此过程合并它们,然后再将它们存储在一个中心化的公共位置。

  4. 记录日志,然后置之不理。通常情况下,本地日志记录需要您操心日志格式、日志文件大小、周期性以及其他变量等问题。而在分布式架构中,日志服务会在收到日志后自动处理这些问题,您的开发人员(以及他们开发的服务)无需为此担心,只需发送日志事件即可。

  5. 这样更容易在所有服务中保持标准格式。与前一点相关,如果您有一个集中式日志服务,能够接收和处理来自不同位置的日志事件,那么您可以将 ETL 代码集中放在其中。这样,您就可以控制日志格式,而不会影响平台的其他部分或增加额外的工作量。

这只是我目前能想到的,具体情况和平台可能会有所不同,随着你开始考虑这种架构,其他好处也可能开始出现。

既然我已经(希望如此)说服了你分布式开发的优势,那么让我来解释一下你可以使用哪些工具来实现这一点。

完成这项工作所需的工具

迁移到分布式环境时有很多选择,有些完全免费,而有些则需要花费不少钱。当然,免费方案需要手动安装,而付费服务则托管在云端,您只需将日志指向他们即可。

第三方服务提供弹性日志存储功能,并提供 Web 用户界面,可以浏览日志并从中获取统计信息。

就本例而言,我将介绍 ELK(ElasticLogstashKibana)技术栈,但您完全可以搜索其他选项,并选择最适合您需求的选项。

ELK堆栈

该技术栈的工作原理是为您提供传输数据、存储数据、使其可浏览以及最终提供用户界面以搜索和收集日志统计信息所需的三个产品。

实现这一目标的方法是使用这个优秀的开源免费技术栈的三个组件:

  • Elastic:这本质上是一个 NoSQL 数据库,尤其擅长搜索。它将作为日志事件的主要存储介质,使后续的搜索和检索变得非常便捷。

  • Logstash:这是将服务器日志导入 Elastic 的方式。通过在服务器上安装小型代理,您可以配置它们来读取、转换日志文件,并将日志文件的每一行传输到您的 Elastic 服务器。

  • Kibana:最后,一旦您的日志被传输并存储在 Elastic 中,Kibana 将作为一个用户友好的 UI,能够与 Elastic 的 REST API 进行交互。

从您的 Node.js 应用连接到 ELK

你的 ELK 技术栈已经准备就绪并运行正常(如果你还没有,可以参考网上众多的教程),但还没有内容。现在让我们把应用连接到它,你会发现这有多么简单。

既然我们使用的是 Node.js,我认为有两种方法可以实现:我们可以继续沿用目前的日志记录方式,很可能是记录到一个文件中,然后配置 Logstash来捕获该文件的更新并将其重新发送到 Elasticsearch;或者我们可以使用日志库,例如Winston,并配置它的某个传输方式来完成这项工作。

猜猜我要说的是哪一个?

从温斯顿到埃拉斯蒂斯

Winston 的妙处在于,我们甚至可以避免配置 Logstash。别误会,Logstash 是一个非常有用的工具,它在日志的传输和格式化方面能帮我们做很多事情,这有时简直是救星,尤其是在我们无法访问应用程序代码并修改其日志记录方式的情况下。

如果无法改变现状,我们就需要获取已保存的数据,并对其进行适当的处​​理以满足我们的存储需求,之后再将其发送到 Elasticsearch。Logstash 正好可以胜任这项工作。您可以找到许多资料,其中介绍了其他应用程序最常见的日志格式以及如何配置 Logstash 来支持这些格式。

但如果您负责应用程序的代码编写,那就无需这样做。借助 Winston 等库,我们可以轻松重定向(甚至扩展)日志记录目标位置,确保信息最终出现在我们需要的地方。

为了实现这一目标,我们将使用 Winston 及其对应的插件winston-elasticsearch

所以,要安装东西,我们可以简单地这样做:

    $ npm i winston --save
    $ npm i winston-elasticsearch --save
Enter fullscreen mode Exit fullscreen mode

之后,你可以按照以下步骤创建一个可以稍后修改的新日志记录器对象。如果你已经有了基于 Winston 的日志记录器,那么只需获取与传输相关的代码并将其添加到你自己的记录器中即可。


const winston = require('winston');
const Elasticsearch = require('winston-elasticsearch');

const esTransportOpts = {
  level: 'info'
};

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: "logfile.log", level: 'error' }), //save errors on file
    new Elasticsearch(esTransportOpts) //everything info and above goes to elastic
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({ //we also log to console if we're not in production
    format: winston.format.simple()
  }));
}
Enter fullscreen mode Exit fullscreen mode

这段代码创建了一个新的日志记录器对象,根据环境的不同,它有两到三种不同的传输方式。显然,这里我使用的是默认值,并让插件连接到我本地的 Elasticsearch 实例。

因此,使用以下代码,我可以登录到我的本地副本:

//Logging tests...
logger.info("Test!")
logger.error("This is an error message!")
logger.error("This is an error message with an object!", { error: true, message: "There was a problem!"})
Enter fullscreen mode Exit fullscreen mode

默认情况下,如果您当前未使用 Kibana,则可以像这样直接查询 Elastic 的 REST API:

    $ curl [http://localhost:9200/logs-2019.07.29/_search](http://localhost:9200/logs-2019.07.29/_search)
Enter fullscreen mode Exit fullscreen mode

请注意索引是按日期创建的,因此您可能需要将其调整为当前日期。结果如下:

{
    "took": 994,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 4,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [{
            "_index": "logs-2019.07.29",
            "_type": "_doc",
            "_id": "Cl2KP2wBTq_AEn0ZM0t0",
            "_score": 1.0,
            "_source": {
                "@timestamp": "2019-07-29T21:01:57.472Z",
                "message": "Test!",
                "severity": "info",
                "fields": {}
            }
        }, {
            "_index": "logs-2019.07.29",
            "_type": "_doc",
            "_id": "C12KP2wBTq_AEn0ZM0t0",
            "_score": 1.0,
            "_source": {
                "@timestamp": "2019-07-29T21:01:57.474Z",
                "message": "This is an error message!",
                "severity": "error",
                "fields": {}
            }
        }, {
            "_index": "logs-2019.07.29",
            "_type": "_doc",
            "_id": "DF2KP2wBTq_AEn0ZM0t0",
            "_score": 1.0,
            "_source": {
                "@timestamp": "2019-07-29T21:01:57.475Z",
                "message": "This is an error message with an object!There was a problem!",
                "severity": "error",
                "fields": {
                    "error": true
                }
            }
        }]
    }
}
Enter fullscreen mode Exit fullscreen mode

上述 JSON 中最有趣的部分是最后一次命中(检查 hits 数组),请注意 fields 元素只有一个属性,因为该库将 message 字段与我传递给 error 方法的第一个参数混合在一起。

连接到 Elastic 的远程实例

理想情况下,您需要连接到远程 Elastic 实例。为此,您只需将 Elastic 客户端配置传递给 ES Transport 配置对象即可。如下所示:

const esTransportOpts = {
  level: 'info',
  clientOpts: {
      host: "http://your-host:your-port",
      log:"info"
  }
};
Enter fullscreen mode Exit fullscreen mode

这样一来,你就会自动将日志消息发送到网络(以太网)中。

发送数据前对其进行转换

您可以对日志消息进行一些预处理,专门针对 Elastic,这要归功于您可以在 ES 传输属性中设置的 transformer 属性,例如:

const esTransportOpts = {
  level: 'info',
  transformer: logData => {
      return {
        "@timestamp": (new Date()).getTime(),
        severity: logData.level,
        message: `[${logData.level}] LOG Message: ${logData.message}`,
        fields: {}
      }
  }
};
Enter fullscreen mode Exit fullscreen mode

该转换器函数将忽略所有元属性(基本上是我们可能想要记录的任何对象),并通过在实际消息前添加“ [LEVEL] LOG Message: ”字符串来扩展该消息。

结论

就是这样,抱歉说了这么多,但正如你所看到的,实际上搭建一个集中式日志平台并将你的Node.js应用程序连接到它,非常简单:

  1. 设置弹性
  2. 安装WinstonWinston-elasticsearch
  3. 请使用我上面给你的代码或运输代码。
  4. ???
  5. 利润!!!

大功告成!(最后一句可能有点夸张,但前三步确实有效 :P)

如果您有使用 Elastic 构建集中式日志平台的经验,请在评论区留言告诉我。

否则,下次再见!

文章来源:https://dev.to/deleteman123/logging-at-scale-done-right-3m0e