新一代云原生日志架构 - Loggie 的设计与实践

舞动的程序
舞动的程序 1月11日 23:40 发表
摘要:Loggie 萌芽于网易严选业务的实际需求,成长于严选与数帆的长期共建,持续发展于网易数帆与网易传媒、中国工商银行的紧密协作。广泛的生态,使得项目能够基于业务需求不断完善、成熟。目前已经开源:https://github.com/loggie-io/loggie

1. 背景

严选日志平台初期,使用 filebeat 采集云内日志,用 flume 采集云外日志。期间经历了一段痛苦的运维排障时期,被问的最多的几个问题:

  • 某条日志为何没有采集?
  • 某条日志为何重复采集了?
  • 能否将漏采集的那条日志补采集?
  • 某个日志文件为何没有采集?
  • 某个日志文件的采集速度怎么这么慢(延迟超过 30s)?
  • 服务重新发布后,之前的日志怎么没有了?

而且同时维护了 2 套日志采集 agent,维护成本高。不管是 filebeat 还是 flume 都存在以下几点严重的问题:

  • 采集性能低: 大促时间,部分节点的日志打印速度超过 100MB/s,然而 filebeat 的采集性能上限只有 80MB/s 左右,flume 更差一点。
  • 资源占用高: filebeat 单节点采集文件超过 100 个,cpu 使用率超 800%;flume 空跑内存就得占用 200MB+,大促期间不得不开启限流以避免影响核心业务系统。
  • 扩展性差: fliebeat 复杂的架构以及单 output 设计无法满足多变的业务需求。

同时也调研其他开源的日志采集 agent,或多或少都存在上述问题,且都没有足够的可观测性和运维手段(工具)来帮助运维排障,更不用说完整的日志解决方案。

因此我们走向了自研的道路。

loggie 在 2022 年初已开源:https://github.com/loggie-io/loggie

欢迎大家使用,与社区一起成长!

2. 架构设计

基于 golang,借鉴经典生产者 - 消费者模式的微内核设计。一个 pipeline 只有 source、queue、sink、interceptor4 个组件概念,且 interceptor 也不是必须的。pipeline 支持配置热加载,组件热插拔,pipeline 之间强隔离,防止相互影响。

2.1 pipeline 设计

pipeline 的设计初衷主要是为了隔离性。之前运维遇到的一个严重问题:云内使用 filebeat 采集多个业务的多个日志文件,但是其中一个日志文件的采集配置的 topic 被误删,导致发送到 kafka 失败而阻塞,进而导致整个物理机几点的所有日志都阻塞。因此我们需要一种泳道隔离手段,来隔离不用业务、不同优先级的日志,避免相互影响。

2.1.1 易扩展

pipeline 的简洁设计使得我们的扩展极其便捷。在概念上,一个 pipeline 只有 4 种组件:source、queue、sink、interceptor,而且 interceptor 不是必须的。越少的概念,扩展者者就有越少的学习成本。并且,为了进一步提高扩展性,pipeline 中的所有组件都抽象为 component,所有的组件拥有一致的生命周期与实现方式。不仅方便了扩展,也方便了 loggie 对组件的统一管理。

2.1.2 隔离性与 reload

基于 pipeline,我们实现了配置热更新,组件热加载。reloader 与 discovery 组件可以基于 K8S CRD、http 监听等方式(预留接口,可以对接例如 zookeeper、consul、apollo 等配置中心),以 pipeline 维度进行 reload。因此,在保证了 reload 能力的同时仍然满足了 pipeline 隔离的目的。

2.1.3 功能增强:interceptor

interceptor 在 loggie 中是一个可选的组件,却在 loggie 中扮演着非常重要的角色。loggie 中绝大多数的增强功能都是基于 interceptor 实现的,例如限流、背压、日志切分、encode&decode、字符编码、结构化等。用户可以根据实际情况选择对应的 interceptor 提升 loggie 的适应能力。

完整内置 interceptor:https://loggie-io.github.io/docs/reference/pipelines/interceptor/overview/

3. 日志采集

对于一个日志采集 agent 来说,通常需要重点关心以下 3 点的设计与实现:

  • 高效采集: 高效指的是高性能的同时低能耗,即如何采集的快服务器资源占用有小。
  • 公平性: 例如写入快的文件不能影响写入慢的文件采集、最近更新的文件不能影响之前更新的文件的采集,删除文件的合理采集与释放。
  • 可靠性: 日志不丢失。包括进程崩溃重启、服务发布 & 迁移 & 容器漂移、下游阻塞等情况。

3.1 高效采集

日志采集,我们关心的是这么几个问题:

  • 如何及时发现新创建的文件?
  • 如何及时发现最新的写入?
  • 如何快速读取文件?

这其实是两方面的问题:

  • 事件感知: 及时发现文件事件。例如文件新建、删除、写入。
  • 文件读取: 高效读取文件内容。尽可能快的读取文件内容,减少磁盘 io,cpu 可控。

3.1.1 事件感知

如何做到文件事件感知呢?业界有两种做法:

  • 定时轮询: 定时检查目录文件状态。
  • OS 事件通知: 注册 OS 文件事件,由 OS 通知具体的文件事件。

两种方式各有利弊:

  • 定时轮询:

    • 优点:实现简单,安全可靠。
    • 缺点:轮询的时间间隔不太好确定,间隔太短耗 cpu,间隔太长发现事件就太慢。
  • OS 事件通知:

    • 优点:能够第一时间发现文件事件

    • 缺点:

      • 实现成本高(不同 OS 的实现方式不同,需要适配)。
      • 存在 bug:例如在最常用的 linux 系统中存在一些 bug(https://man7.org/linux/man-pages/man7/inotify.7.html) ,其中影响最大的几点:通知丢失、注册数量有上限、文件改名事件无法通知改名后的文件名。
      • 使用有限制:例如无法对通过 NFS(Network File System) 挂载的网盘 & 云盘生效,无法对 FUSE(filesystem in userspace) 生效等。

因此 loggie 结合两者共同实现了一个安全可靠,却又能及时感知事件的方案:

同时启用定时轮询和 OS 通知,使用 OS 通知然后搭配一个相对较长(loggie 默认为 10s)的定时轮询,将两者发现的事件进行合并以减少重复处理,这样就能同时兼具两者的优点。

但是实际测试下来,我们发现了 cpu 占用上升,分析原因:

  • OS 事件过多: 特别是写文件的时候,对应有两个 os 事件(chmod+write),一旦文件写得频繁,os 的事件将非常多,loggie 疲于处理系统事件。

所以,我们重新分析,什么样的事件是我们真正关心并需要及时感知的呢?

  • 文件写事件?

当我们无法及时发现文件写事件,会有什么影响呢?有两种情况:

  • 如果这个文件正处于采集中(持有文件句柄),那这个文件的写事件没有影响。因为正在读这个文件,后续的写入理所当然能被读到。
  • 如果这个文件处于不活跃状态(即文件已经读取到了末尾,并且一定时间内没有发现新内容,甚至文件的文件句柄被释放了),这个情况我们希望能及时感知文件的写事件以方便我们及时采集最新的写入内容。

因此,重要的是 “不活跃” 文件的写事件。

  • 文件新建(滚动)事件?

当我们没有及时发现新建事件,会有什么影响呢?

首条日志写时间到发现时间之间的日志将会延迟采集(对于 loggie 来说,最大延迟在 10s 左右,因为默认的轮询时间间隔为 10s),但是一旦感知到事件,采集可以很快追上进度。因此新建事件不那么重要。

  • 文件删除事件?

当我们没有及时发现删除事件,会有什么影响呢?有 3 种场景:

  • 文件被删除后,希望未采集完成的文件继续采集:这种情况,删除事件迟到不总要。因为当文件还未采集完,及时发现的删除事件没有意义;当文件采集完后,未及时发现的删除事件仅影响文件句柄释放延迟。
  • 文件被删除后,希望尽快释放磁盘空间:仅仅导致文件句柄释放延迟,即磁盘空间释放延迟(大概在 10s 左右)。
  • 文件被删除后,希望未采集完的文件给予一定的容忍时间再释放磁盘空间:这种情况近会导致文件最终释放延迟的时间 = 容忍时间 + 事件迟到时间。

因此,删除事件不重要。

  • 文件改名事件?

同上,不重要。但是如何得知文件改名以及改成什么名确实个值得好好思考的问题!(暂不展开)

所以,真正重要的是不活跃的文件写事件。因此我们的架构调整为:

成果:

  • 节省大量不必要的目录和文件的系统事件注册和监听以及对应监听产生的大量事件,进一步降低了 CPU 的损耗。
  • 事件处理这一层去掉了事件合并这一步,因为所有事件都是有效事件,进一步降低了 CPU 的损耗。

3.1.2 文件读取

文件读的快的原则:

  1. 减少磁盘 io: 意味着每次需要读取更多的字节数缓冲在内存,再按行分割处理。
  2. cpu 可控: 减少 gc 和线程调度。

矛盾点:

  • 原则 1 注定了我们需要重复分配长数组来承载读取的内容。这意味了很多大对象。
  • 原则 2 中的 gc 最害怕的恰恰是转瞬即逝的大对象。

因此文件读取的原理为:

  • 复用读取缓存数组: 重复利用大对象。
  • 读取字节数: 4k 的 n 倍,充分利用 os 的 page cache。

成果: 读取性能达到 2G/s(本地 macbook ssd 环境测试)。

3.2 公平性

公平性我们关心的是文件之间的读取平衡,不要造成文件读取饥饿(长时间得不到读取而造成数据延迟)。业界这块的做法有两种:

  • 每个文件对应一个读取线程,有 os 调度文件读取保证公平性

    • 优点:能够保证公平性。
    • 缺点:同时采集的文件数量多的时候 cpu 消耗太大。
  • 匹配到的文件按照更新事件倒序,按照顺序挨个读取文件

    • 优点:实现简单。
    • 缺点:无法保证公平性,更新时间早的文件必然会饥饿。

因此 loggie 实现了基于 “时间片” 的读取方式:

成果: 仅仅用一个线程(goroutine)就处理了 loggie 所有的日志读取任务,且最大可能的保证了公平性。

3.3 可靠性

loggie 保证了数据的不丢失,实现了 at least once 保证。对于文件读取来说,可靠性的实现有一定特殊性:需要保序 ack,即我们对于采集点位记录的持久化前提是当前 ack 之前的 ack 全部 done。因此为了提高保序 ack 的性能,我们的这块的设计进行了一些优化:

对于 sink 端提交的 ack 不立即进行持久化,而且进行了双重压缩:

  • 在 ack chain 阶段只会提交最尾端的 ack 到 db。
  • db 中会对提交的 ack 再进一步压缩。

成果: 大大减少了 cpu 的消耗和 ack 持久化的磁盘 io 次数。

3.4 性能对比

对比 filebeat,同等情况下,发送至 Kafka(单行、单文件、相同发送并发度、无解析场景):

  • 单文件采集对比,Loggie 和 Filebeat 消耗的 CPU 相比,大概仅为后者的 1/4,同时发送吞吐量为后者的 1.6~2.6 倍。
  • Filebeat 的极限吞吐量存在瓶颈,80MB/s 后很难提升,而 Loggie 的极限值更高,多文件采集下能轻松达到 200MB/s+。

4. 运维治理

基于 loggie 我们做了很多运维治理,以及应用分析的事情:

4.1 可观测

根据长期运维、排障经验归纳提炼的内置指标,可以指导帮助我们快速发现定位问题:

提供了 grafana 模版,可以一键配置:https://github.com/loggie-io/installation/tree/main/prometheus/grafana-dashboard

4.2 完整性

日志从采集到最终的存储,链路可能比较冗长,中间任何一个环节出问题都可能导致日志丢失。因此需要有一个日志完整性校验的机制来判断日志的采集情况。通常我们比较关心两方面问题:

  • 某个服务的某个日志数据有没有丢?
  • 丢的数据能否补?

那如何计算日志有没有丢呢?精确完整性计算的核心原理:

  • 计算维度: 机器 ip + 日志文件唯一标识

机器 ip 是确定的,但是如何唯一标识日志文件呢?

文件名可能重复,因此需要文件名 + 文件 inode(文件标识)。但是 inode 只在磁盘分区中唯一,因此需要修改为文件名 + 文件 inode(文件标识)+dev(磁盘标识)。但是 inode 存在复用的情况,如果文件被采集完后被删除了,inode 被复用给一个同名的文件,这样就变相的造成重复,因此我们需要增加文件内容的前 n 个字符编码。最终的计算维度为:机器 ip + 文件名称 + inode+dev+{fistNBytes}。

  • 近实时计算: 小时级批任务计算

之所以用小计批任务计算,主要有两个原因:

  • 日志的完整性计算不需要太实时,因为采集的日志可能因为种种原因而迟到,实时计算的话很可能会存在太多的数据丢失的情况。而且计算的量级非常大,不适合实时计算。

  • 另一面,不使用更大的时间间隔(例如 T+1)计算的原因是,通常日志都会配置轮转和清理。如果间隔过大,计算出有丢失的日志可能因为轮转和清理被删除了,那就失去了补数据的机会。

  • 计算逻辑: 日志行首 offset + 日志 size = 下一行日志行首 offset
  • 补数据: 通过调用 loggie 原生的文件数据读取接口获取(指定 offset 和读取的 size)。

计算需要的所有 metric 都附带在 loggie 采集的的日志元数据中。

成果: 简单易用的平台化展示与补数据。

4.3 日志延迟

我们计算了日志在整个链路环节中的延迟情况,重点日志还会根据延迟情况及时的报警:

端到端的延迟计算意义:

  • 帮助我们审视分析链路机器资源瓶颈。
  • 指导大促期间的合理扩缩容。
  • 规划未来增长的日志所需的容量。

4.4 日志质量

由于日志采集后,可能被后续的业务监控报警以及大数据数仓处理分析计算应用,因此日志的质量变得愈发重要。那如何衡量日志质量呢?本质上,日志从非结构化数据被采集后经过一系列处理计算变成了结构化数据,因此我们在日志结构化的过程中定义了日志质量分的计算:

  • 日志切分(字段提取)处理:切分失败扣 1 分
  • 字段不存在扣 1 分
  • 类型转换处理:转换失败扣 1 分
  • 字段约束失败扣 1 分

效果:

日志质量治理的意义:

  • 量化日志质量,持续推动服务进化。
  • 为下游日志的处理极大提高效率。
  • 提高了关键日志的准确数。

4.5 分析应用

基于采集的日志数据我们做 2 个重量级的应用,使得日志的重要程度上升了一个维度,让日志真正拥有强烈的业务含义。

4.5.1 业务实时监控

考虑以下需求:

  • 实时监控主站交易是否下跌
  • 实时监控主站 UV 访问
  • 实时监控主站的加购、下单情况
  • 实时监控风控的拦截情况
  • 实时监控服务的 qps、异常情况
  • ...

通常实现这些需求需要不小的开发周期,但是基于业务实时监控,只需要花 5-10 分钟的时间在平台上配置即可展示与报警:

核心原理:

  • 打印对应的业务日志
  • 在平台配置采集对应的日志
  • 在平台配置对应日志的数据模型以及对应指标的计算逻辑(支持明细、聚合等丰富计算)
  • 配置报警规则

成果:

大大降低了业务服务对关键业务过程的监控报警的开发成本,通过配置即可拥有业务实时监控报警的能力。

4.5.2 业务全链路监控

当前微服务盛行,用户的一个操作可能涉及底下多个微服务调用,考虑以下问题:

  • 如何从业务全局视角观察对应服务的运行情况?
  • 如何从业务链路视角发现上下游占用比例和依赖?

业务全链路监控应运而生:

以交易链路为例,从首页 -> 搜索 & 推荐 -> 商详 -> 加购 -> 下单 -> 支付这个主链路以及辐射出来的几条支链路,整体的流量是怎么样的?调用 qps 是怎么样的?平均 rt 是什么样的?错误情况如何?通过业务全链路监控一目了然。

核心实现原理:

  • 严选所有服务默认开启访问日志,默认采集所有服务的访问日志
  • 在平台配置业务链路流程对应的服务以及接口
  • 配置关键链路节点的监控报警规则
  • 平台一键生成全链路实时监控报警

业务全链路实时监控的意义非凡,使得我们能够站在更高的维度,以全局视角审视整个业务流程下的服务调用情况,及时发现链路中的薄弱环节以及异常情况,帮助我们优化服务依赖,提升服务稳定性。

5. 结语

严选基于 loggie 的日志平台化建设,我们一方面在满足基于日志的问题分析诊断这一基础需求之余,进一步了提升日志质量、问题排查效率、全链路可观测可运维能力。另一方面,我们扩展能力与丰富应用场景,只需要简单配置就使得日志拥有强烈的业务含义与监控语义,使得我们能够站在更高的维度审视 & 监控业务链路中的异常,提前发现链路服务与资源问题。

点赞 0 收藏(0)    分享
相关标签: Loggie 云原生
0 个评论
  • 消灭零评论