本文由云+社区发表

作者:J2X

我们都有过上机器查日志的经历,当集群数量增多的时候,这种原始的操作带来的低效率不仅给我们定位现网问题带来极大的挑战,同时,我们也无法对我们服务框架的各项指标进行有效的量化诊断,更无从谈有针对性的优化和改进。这个时候,构建具备信息查找,服务诊断,数据分析等功能的实时日志监控系统尤为重要。

ELK (ELK Stack: ElasticSearch, LogStash, Kibana, Beats) 是一套成熟的日志解决方案,其开源及高性能在各大公司广泛使用。而我们业务所使用的服务框架,如何接入 ELK 系统呢?

业务背景

我们的业务框架背景:

  • 业务框架是基于 NodeJs 的 WebServer
  • 服务使用 winston 日志模块将日志本地化
  • 服务产生的日志存储在各自机器的磁盘上
  • 服务部署在不同地域多台机器

接入步骤

我们将整个框架接入 ELK 简单归纳为下面几个步骤:

  • 日志结构设计:由传统的纯文本日志改成结构化对象并输出为 JSON.
  • 日志采集:在框架请求生命周期的一些关键节点输出日志
  • ES 索引模版定义:建立 JSON 到 ES 实际存储的映射

一、日志结构设计

传统的,我们在做日志输出的时候,是直接输出日志的等级(level)和日志的内容字符串(message)。然而我们不仅关注什么时间,发生了什么,可能还需要关注类似的日志发生了多少次,日志的细节与上下文,以及关联的日志。 因此我们不只是简单地将我们的日志结构化一下为对象,还要提取出日志关键的字段。

1. 将日志抽象为事件

我们将每一条日志的发生都抽像为一个事件。事件包含:

事件元字段

  • 事件发生时间:datetime, timestamp
  • 事件等级:level, 例如: ERROR, INFO, WARNING, DEBUG
  • 事件名称: event, 例如:client-request
  • 事件发生的相对时间(单位:纳秒):reqLife, 此字段为事件相对请求开始发生的时间(间隔)
  • 事件发生的位置: line,代码位置; server, 服务器的位置

请求元字段

  • 请求唯一ID: reqId, 此字段贯穿整个请求链路上发生的所有事件
  • 请求用户ID: reqUid, 此字段为用户标识,可以跟踪用户的访问或请求链路

数据字段

不同类型的事件,需要输出的细节不尽相同,我们将这些细节(非元字段)统一放到d -- data,之中。使我们的事件结构更加清晰,同时,也能避免数据字段对元字段造成污染。

e.g. 如 client-init事件,该事件会在每次服务器接收到用户请求时打印,我们将用户的 ip, url等事件独有的统一归为数据字段放到 d 对象中

举个完整的例子

{
"datetime":"2018-11-07 21:38:09.271",
"timestamp":1541597889271,
"level":"INFO",
"event":"client-init",
"reqId":"rJtT5we6Q",
"reqLife":5874,
"reqUid": "999793fc03eda86",
"d":{
"url":"/",
"ip":"9.9.9.9",
"httpVersion":"1.1",
"method":"GET",
"userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"headers":"*"
},
"browser":"{"name":"Chrome","version":"70.0.3538.77","major":"70"}",
"engine":"{"version":"537.36","name":"WebKit"}",
"os":"{"name":"Mac OS","version":"10.14.0"}",
"content":"(Empty)",
"line":"middlewares/foo.js:14",
"server":"127.0.0.1"
}

一些字段,如:browser, os, engine为什么在外层 有时候我们希望日志尽量扁平(最大深度为2),以避免 ES 不必要的索引带来的性能损耗。在实际输出的时候,我们会将深度大于1的值输出为字符串。而有时候一些对象字段是我们关注的,所以我们将这些特殊字段放在外层,以保证输出深度不大于2的原则。

一般的,我们在打印输出日志的时候,只须关注事件名称数据字段即可。其他,我们可以在打印日志的方法中,通过访问上下文统一获取,计算,输出。

2. 日志改造输出

前面我们提到了如何定义一个日志事件, 那么,我们如何基于已有日志方案做升级,同时,兼容旧代码的日志调用方式。

升级关键节点的日志

// 改造前
logger.info('client-init => ' + JSON.stringfiy({
url,
ip,
browser,
//...
})); // 改造后
logger.info({
event: 'client-init',
url,
ip,
browser,
//...
});

兼容旧的日志调用方式

logger.debug('checkLogin');

因为 winston 的 日志方法本身就支持 string 或者 object 的传入方式, 所以对于旧的字符串传入写法,formatter 接收到的实际上是{ level: 'debug', message: 'checkLogin' }。formatter 是 winston 的日志输出前调整日志格式的一道工序, 这一点使我们在日志输出前有机会将这类调用方式输出的日志,转为一个纯输出事件 -- 我们称它们为raw-log事件,而不需要修改调用方式。

改造日志输出格式

前面提到 winston 输出日志前,会经过我们预定义的formatter,因此除了兼容逻辑的处理外,我们可以将一些公共逻辑统一放在这里处理。而调用上,我们只关注字段本身即可。

  • 元字段提取及处理
  • 字段长度控制
  • 兼容逻辑处理

如何提取元字段,这里涉及上下文的创建与使用,这里简单介绍一下 domain 的创建与使用。

//--- middlewares/http-context.js
const domain = require('domain');
const shortid = require('shortid'); module.exports = (req, res, next) => {
const d = domain.create();
d.id = shortid.generate(); // reqId;
d.req = req; //... res.on('finish', () => process.nextTick(() => {
d.id = null;
d.req = null;
d.exit();
}); d.run(() => next());
} //--- app.js
app.use(require('./middlewares/http-context.js')); //--- formatter.js
if (process.domain) {
reqId = process.domain.id;
}

这样,我们就可以将 reqId 输出到一次请求中所有的事件, 从而达到关联事件的目的。

二、日志采集

现在,我们知道怎么输出一个事件了,那么下一步,我们该考虑两个问题:

  1. 我们要在哪里输出事件?
  2. 事件要输出什么细节?

换句话说,整个请求链路中,哪些节点是我们关注的,出现问题,可以通过哪个节点的信息快速定位到问题?除此之外,我们还可以通过哪些节点的数据做统计分析?

结合一般常见的请求链路(用户请求,服务侧接收请求,服务请求下游服务器/数据库(*多次),数据聚合渲染,服务响应),如下方的流程图

流程图

那么,我们可以这样定义我们的事件:

用户请求

  • client-init: 打印于框架接收到请求(未解析), 包括:请求地址,请求头,Http 版本和方法,用户 IP 和 浏览器
  • client-request: 打印于框架接收到请求(已解析),包括:请求地址,请求头,Cookie, 请求包体
  • client-response: 打印于框架返回请求,包括:请求地址,响应码,响应头,响应包体

下游依赖

  • http-start: 打印于请求下游起始:请求地址,请求包体,模块别名(方便基于名字聚合而且域名)
  • http-success: 打印于请求返回 200:请求地址,请求包体,响应包体(code & msg & data),耗时
  • http-error: 打印于请求返回非 200,亦即连接服务器失败:请求地址,请求包体,响应包体(code & message & stack),耗时。
  • http-timeout: 打印于请求连接超时:请求地址,请求包体,响应包体(code & msg & stack),耗时。

字段这么多,该怎么选择? 一言以蔽之,事件输出的字段原则就是:输出你关注的,方便检索的,方便后期聚合的字段。

一些建议

  1. 请求下游的请求体和返回体有固定格式, e.g. 输入:{ action: 'getUserInfo', payload: {} } 输出: { code: 0, msg: '', data: {}} 我们可以在事件输出 action,code 等,以便后期通过 action 检索某模块具体某个接口的各项指标和聚合。

一些原则

  1. 保证输出字段类型一致 由于所有事件都存储在同一个 ES 索引, 因此,相同字段不管是相同事件还是不同事件,都应该保持一致,例如:code不应该既是数字,又是字符串,这样可能会产生字段冲突,导致某些记录(document)无法被冲突字段检索到。
  2. ES 存储类型为 keyword, 不应该超过 ES mapping 设定的 ignore_above 中指定的字节数(默认4096个字节)。否则同样可能会产生无法被检索的情况

三、ES 索引模版定义

这里引入 ES 的两个概念,映射(Mapping)与模版(Template)。

首先,ES 基本的存储类型大概枚举下,有以下几种

  • String: keyword & text
  • Numeric: long, integer, double
  • Date: date
  • Boolean: boolean

一般的,我们不需要显示指定每个事件字段的在ES对应的存储类型,ES 会自动根据字段第一次出现的document中的值来决定这个字段在这个索引中的存储类型。但有时候,我们需要显示指定某些字段的存储类型,这个时候我们需要定义这个索引的 Mapping, 来告诉 ES 这此字段如何存储以及如何索引。

e.g.

还记得事件元字段中有一个字段为 timestamp ?实际上,我们输出的时候,timestamp 的值是一个数字,它表示跟距离 1970/01/01 00:00:00 的毫秒数,而我们期望它在ES的存储类型为 date 类型方便后期的检索和可视化, 那么我们创建索引的时候,指定我们的Mapping。

PUT my_logs
{
"mappings": {
"_doc": {
"properties": {
"title": {
"type": "date",
"format": "epoch_millis"
},
}
}
}
}

但一般的,我们可能会按日期自动生成我们的日志索引,假定我们的索引名称格式为 my_logs_yyyyMMdd (e.g. my_logs_20181030)。那么我们需要定义一个模板(Template),这个模板会在(匹配的)索引创建时自动应用预设好的 Mapping。

PUT _template/my_logs_template
{
"index_patterns": "my_logs*",
"mappings": {
"_doc": {
"properties": {
"title": {
"type": "date",
"format": "epoch_millis"
},
}
}
}
}

提示:将所有日期产生的日志都存在一张索引中,不仅带来不必要的性能开销,也不利于定期删除比较久远的日志。

小结

至此,日志改造及接入的准备工作都已经完成了,我们只须在机器上安装 FileBeat -- 一个轻量级的文件日志Agent, 它负责将日志文件中的日志传输到 ELK。接下来,我们便可使用 Kibana 快速的检索我们的日志。

此文已由腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

Node 框架接入 ELK 实践总结的更多相关文章

  1. 从Nest到Nesk -- 模块化Node框架的实践

    文: 达孚(沪江Web前端架构师) 本文原创,转至沪江技术 首先上一下项目地址(:>): Nest:https://github.com/nestjs/nest Nesk:https://git ...

  2. 1.Node.js 接入微信公众平台开发

    一.写在前面的话   Node.js是一个开放源代码.跨平台的JavaScript语言运行环境,采用Google开发的V8运行代码,使用事件驱动.非阻塞和异步输入输出模型等技术来提高性能,可优化应用程 ...

  3. 大雄的elk实践

    目录 一.ElK环境搭建 1.1.elasticsearch 1..kibana 1..logstash二.elk实践 2.1 使用elk分析nginx日志 一.ElK环境搭建   1.1 elast ...

  4. Cookie和Session在Node.JS中的实践(三)

    Cookie和Session在Node.JS中的实践(三) 前面作者写的COOKIE篇.SESSION篇,算是已经比较详细的说明了两者间的区别.机制.联系了.阅读时间可能稍长,因为作者本身作图也做了不 ...

  5. Node.js 中流操作实践

    本文节选自 Node.js CheatSheet | Node.js 语法基础.框架使用与实践技巧,也可以阅读 JavaScript CheatSheet 或者 现代 Web 开发基础与工程实践 了解 ...

  6. atitit. 统计功能框架的最佳实践(1)---- on hibernate criteria

    atitit. 统计功能框架的最佳实践(1)---- on hibernate criteria 1. 关键字 1 2. 统计功能框架普通有有些条件选项...一个日期选项..一个日期类型(日,周,月份 ...

  7. 开源调度框架Quartz最佳实践

    开源调度框架Quartz最佳实践 Quartz是一个Java调度框架,当前的最新版本为2.2.1. 以Quartz 2.2.1版为例,Quartz最佳实践(用于生产系统)总结如下: 1.跳过更新检查Q ...

  8. 基于Python玩转人工智能最火框架 TensorFlow应用实践

    慕K网-299元-基于Python玩转人工智能最火框架 TensorFlow应用实践 需要联系我,QQ:1844912514

  9. TypeScript在node项目中的实践

    TypeScript在node项目中的实践 TypeScript可以理解为是JavaScript的一个超集,也就是说涵盖了所有JavaScript的功能,并在之上有着自己独特的语法.最近的一个新项目开 ...

随机推荐

  1. 设计一个卖不同种类车的4s店

    # 定义奔驰车类 class BenchiCar(object): # 定义车的方法 def move(self): print('---奔驰车在移动---') def stop(self): pri ...

  2. python爬虫入门(六) Scrapy框架之原理介绍

    Scrapy框架 Scrapy简介 Scrapy是用纯Python实现一个为了爬取网站数据.提取结构性数据而编写的应用框架,用途非常广泛. 框架的力量,用户只需要定制开发几个模块就可以轻松的实现一个爬 ...

  3. C# 发展史

    C# 发展史 Intro 本文主要总结介绍C# 每个版本带来的不同的语言特性. C#,读作C Sharp,是微软推出的一种基于.NET平台的.面向对象的高级编程语言.是微软公司在2000年发布的一种新 ...

  4. RNN(Recurrent Neural Network)的几个难点

    1. vanish of gradient RNN的error相对于某个时间点t的梯度为: \(\frac{\partial E_t}{\partial W}=\sum_{k=1}^{t}\frac{ ...

  5. QT5:C++实现基于Multimedia的音乐播放器(序)

    前段时间C++课设,决定做个播放器,于是参考了网上的代码后,做了个很简陋的音乐播放器(只写了MP3格式)出来,虽然功能甚少,但还是决定把过程记录一下. 成品如下图: 播放器功能: 上.下一首,音量控制 ...

  6. linux dialog详解(图形化shell)

    liunx 下的dialog 工具是一个可以和shell脚本配合使用的文本界面下的创建对话框的工具.每个对话框提供的输出有两种形式:   1.  将所有输出到stderr 输出,不显示到屏幕.   2 ...

  7. SSM-SpringMVC-04:SpringMVC深入浅出理解HandleMapping(源码刨析)

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 先从概念理解,从中央调度器,携带参数request,调度到HandleMapping处理器映射器,处理器映射器 ...

  8. awk 手册

    1. 前言 有关本手册 : 这是一本awk学习指引, 其重点着重于 : l        awk 适于解决哪些问题 ? l        awk 常见的解题模式为何 ? 为使读者快速掌握awk解题的模 ...

  9. Linux kernel的中断子系统之(八):softirq

    返回目录:<ARM-Linux中断系统>. 总结:中断分为上半部和下半部,上半部关中断:下半部开中断,处理可以延迟的事情.下半部有workqueue/softirq/tasklet三种方式 ...

  10. Debian虚拟机安装VirtualBox增强功能

    作者:荒原之梦 原文链接:http://zhaokaifeng.com/?p=573 本文中使用的Debian是安装在VirtualBox中的虚拟机,具体参数如下: Debian版本:Linux de ...