写点什么

vivo 大数据日志采集 Agent 设计实践

  • 2022-11-28
    广东
  • 本文字数:8848 字

    阅读完需:约 29 分钟

作者:vivo 互联网存储技术团队- Qiu Sidi


在企业大数据体系建设过程中,数据采集是其中的首要环节。然而,当前行业内的相关开源数据采集组件,并无法满足企业大规模数据采集的需求与有效的数据采集治理,所以大部分企业都采用自研开发采集组件的方式。本文通过在 vivo 的日志采集服务的设计实践经验,为大家提供日志采集 Agent 在设计开发过程中的关键设计思路。

一、概述

在企业大数据体系的建设过程中,数据的处理一般包含 4 个步骤:采集、存储、计算和使用。其中,数据采集,是建设过程中的首要的环节,也是至关重要的环节,如果没有采集就没有数据,更谈不上后续的数据处理与使用。所以,我们看到的企业中的运营报表、决策报表、日志监控、审计日志等的数据来源都是基于数据采集。一般的,我们对数据采集的定义是,把各种分散的源头上的数据(可以包括企业产品的埋点的日志、服务器日志、数据库、IOT 设备日志等)统一汇聚到大数据存储组件的过程(如下图所示)。其中,日志文件类型的采集场景,是各种数据采集类型中最常见的一种。接下来,将围绕该场景提出我们的设计实践方案。

通常,日志采集服务可以分为几个部分(业界常见的架构如下图所示):日志采集 Agent 组件(常见的开源采集 Agent 组件有 Flume、Logstash、Scribe 等)、采集传输与存储组件(如 kafka、HDFS)、采集管理平台。Bees 采集服务是 vivo 自研的日志采集服务,本文章是通过在 Bees 采集服务中的关键组件 bees-agent 的开发实践后,总结出一个通用的日志采集 Agent 设计中的核心技术点和一些关键思考点,希望对大家有用。

二、特性 &能力

  1. 具备基本的日志文件的实时与离线采集能力

  2. 基于日志文件,无侵入式采集日志

  3. 具备自定义的过滤超大日志的能力

  4. 具备自定义的过滤采集、匹配采集、格式化的能力

  5. 具备自定义的限速采集的能力

  6. 具备秒级别的实时采集时效性

  7. 具备断点续传能力,升级和停止不丢数据

  8. 具备可视化的、中心化的采集任务管理平台

  9. 丰富的监控指标与告警(包括采集流量、时效性、完整性等)

  10. 低系统资源开销(包括磁盘、内存、CPU 及网络等)

三、设计原则

  1. 简单优雅

  2. 健壮稳定

四、关键设计

目前业界流行的日志采集 Agent 组件,开源的有 Flume、Logstash、Scribe、FileBeats、Fluentd 等,自研的有阿里的 Logtail。它们都有不错的性能与稳定性,如果想要快速上手,可以不妨使用它们。但是一般大企业会有个性化的采集需求,比如采集任务大规模管理、采集限速、采集过滤等,还有采集任务平台化、任务可视化的需求,为了满足上面这些需求我们自研了一个日志采集 Agent。

在做一切的设计和开发之前,我们设定了采集 Agent 最基本的设计原则,即简单优雅、健壮稳定。

日志文件采集的一般流程会包括:文件的发现与监听、文件读取,日志内容的格式化、过滤、聚合与发送。当我们开始着手开始设计这样一个日志采集 Agent 时,会遇到不少关键的难点问题,比如:日志文件在哪里?如何发现日志文件新增?如何监听日志内容追加?如何识别一个文件?宕机重启怎么办?如何断点续传?等等问题,接下来,我们针对日志采集 Agent 设计过程中遇到的关键问题,为大家一一解答。(注:下文出现的文件路径与文件名都为演示样例非真实路径)

4.1 日志文件发现与监听


Agent 要如何知道采集哪些日志文件呢?


最简单的设计,就是在 Agent 的本地配置文件中,把需要采集的日志文件路径都一一罗列进去,比如 /home/sample/logs/access1.log、/home/sample/logs/access2.log、/home/sample/logs/access3.log 等,这样 Agent 通过读取配置文件得到对应的日志文件列表,这样就能遍历文件列表读取日志信息。但是实际情况是,日志文件是动态生成的,像一般 tomcat 的业务日志,每个小时都会滚动生成一个新的的日志文件,日志名字通常会带上时间戳,命名类似 /data/sample/logs/access.2021110820.log,所以采用直接配置固定的文件列表方式是行不通的。


所以,我们想到可以使用一个文件夹路径和日志文件名使用正则表达式或者通配符来表示(为了方便,下文统一使用通配符来表示)。机器上的日志一般固定存在某一个目录下,比如  /data/sample/logs/ 下,文件名由于某种规则是滚动产生的(比如时间戳),类似 access.2021110820.log、access.2021110821.log、access.2021110822.log,我们可以简单粗暴使用 access.*.log 的通配方法来匹配这一类的日志,当然实际情况可以根据你需要的匹配粒度去选择你的正则表达式。有了这个通配符方法,我们的 Agent 就能的匹配滚动产生的一批日志文件了。


如何持续发现和监听到新产生的日志文件呢?


由于新的日志文件会由其他应用程序(比如 Nginx、Tomcat 等)持续的按小时动态产生的,Agent 如何使用通配符快速去发现这个新产生的文件呢?


最容易想到的,是使用轮询的设计方案,即是通过一个定时任务来检查对应目录下的日志文件是否有增加,但是这种简单的方案有个问题,就是如果轮询间隔时间太长,比如间隔设置为 10s、5s,那么日志采集的时效性满足不了我们的需求;如果轮询间隔时间太短,比如 500ms,大量的无效的轮询检查又会消耗许多 CPU 资源。幸好,Linux 内核给我们提供一种高效的文件事件监听机制:Linux Inotify 机制。该机制可监听任意文件的操作,比如文件创建、文件删除和文件内容变更,内核会给应用层一个对应的事件通知。Inotify 这种的事件机制比轮询机制高效的多,也不存在 CPU 空跑浪费系统资源的情况。在 java 中,使用 java.nio.file.WatchService,可以参考如下核心代码:

/** * 订阅文件或目录的变更事件 */public synchronized BeesWatchKey watchDir(File dir, WatchEvent.Kind<?>... watchEvents) throws IOException {    if (!dir.exists() && dir.isFile()) {        throw new IllegalArgumentException("watchDir requires an exist directory, param: " + dir);    }    Path path = dir.toPath().toAbsolutePath();    BeesWatchKey beesWatchKey = registeredDirs.get(path);    if (beesWatchKey == null) {        beesWatchKey = new BeesWatchKey(subscriber, dir, this, watchEvents);        registeredDirs.put(path, beesWatchKey);        logger.info("successfully watch dir: {}", dir);    }    return beesWatchKey;} public synchronized BeesWatchKey watchDir(File dir) throws IOException {    WatchEvent.Kind<?>[] events = {            StandardWatchEventKinds.ENTRY_CREATE,            StandardWatchEventKinds.ENTRY_DELETE,            StandardWatchEventKinds.ENTRY_MODIFY    };    return watchDir(dir, events);}
复制代码

综合以上思考,日志文件的发现和日志内容变更的监听,我们使用的是"inotify 机制为主+轮询机制兜底"、"通配符"的设计方案,如下图所示:

4.2 日志文件的唯一标识


要设计日志文件的唯一标识,如果直接使用日志文件的名称是行不通的,日志文件名可能被频繁重复使用,比如,一些应用程序使用的日志框架在输出日志时,对于当前应用正在输出的日志命名是不带任何时间戳信息的,比如固定是 access.log,只有等到当前小时写入文件完毕时,才把文件重命名为 access.2021110820.log,此时新生产的日志文件命名也是 access.log,该文件名对于采集 Agent 来说是重复的,所以文件名是无法作为文件唯一标识。


我们想到使用 Linux 操作系统上的文件 inode 号作为文件标识符。Unix/Linux 文件系统使用 inode 号来识别不同文件,即使移动文件或重命名文件,inode 号是保持不变的,创建一个新文件,会给这个新文件分配一个新的不重复的 inode 号,这样就能与现有磁盘上的其他文件很好区分。我们使用 ls -i access.log 可以快速查看该文件的 inode 号,如下代码块所示:

ls -i access.log62651787 access.log
复制代码


一般来说,使用系统的 inode 号作为标识,已经能满足大多数的情况了,但是为了更严谨的考虑,还可以进一步升级方案。因为 Linux 的 inode 号存在复用的情况,这里的"复用"要和"重复"区别一下,在一台机器上的所有文件不会同一时刻出现重复的两个 inode 号,但是当文件删除后,另一个新文件创建时,这个文件的 inode 号是可能复用之前删除文件的 inode 号的,代码逻辑处理不好,很可能造成日志文件漏采集,这一点是要注意的。为了规避这个问题,我们把文件的唯一标识设计为" 文件 inode 与文件签名组合",这里的文件签名使用的是该文件内容前 128 字节的 Hash 值,代码参考如下:

public static String signFile(File file) throws IOException {        String filepath = file.getAbsolutePath();        String sign = null;        RandomAccessFile raf = new RandomAccessFile(filepath, "r");        if (raf.length() >= SIGN_SIZE) {           byte[] tbyte = new byte[SIGN_SIZE];           raf.seek(0);           raf.read(tbyte);           sign = Hashing.sha256().hashBytes(tbyte).toString();        }        return sign;    }
复制代码


关于 inode 再补充点小知识。Linux inode 是会满的,inode 的信息存储本身也会消耗一些硬盘空间,因为 inode 号只是 inode 内容中的一小部分,inode 内容主要是包含文件的元数据信息:如文件的字节数、文件数据 block 的位置、文件的读写执行权限、文件的时间戳等,可以用 stat 命令,查看某个文件完整的 inode 信息(stat access.log)。因为这样的设计,操作系统是将硬盘分成两个区域的:一个是数据区,存放文件数据;另一个是 inode 区,存放 inode 所包含的信息。每个 inode 节点的大小,一般是 128 字节或 256 字节。查看每个硬盘分区的 inode 总数和已经使用的数量,可以使用 df -i 命令。由于每个文件都必须有一个 inode,如果一个日志机器上,日志文件小而且数量太多,是有可能发生操作系统 inode 用完了即是 inode 区磁盘满了,但是我们使用的数据区硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。所以在日志打印规范上是要避免产生大量的小日志文件的。

4.3 日志内容的读取


发现并且能有效监听日志文件后,我们应该如何去读取这个日志文件中实时追加的日志内容呢?日志内容的读取,我们期望从日志文件中把每一行的日志内容逐行读取出来,每一行以\n 或者\r 为分隔符。很显然,我们不能直接简单采用 InputStreamReader 去读取,因为 Reader 只能按照字符从头到尾读取整个日志文件,不适合读取实时追加日志内容的情况;最合适的选择应该是使用 RandomAccessFile。RandomAccessFile 它为代码开发者提供了一个可供设置的指针,通过指针开发者可以访问文件的随机位置,参考下图:


通过这种方式,当某一时刻出现线程读取到文件末尾时,只需要记录当前的位置,线程就进入等待状态,直到有新的日志内容写入后,线程又重新启动,启动后可以接着上次的尾部往下读取,代码参考如下。另外,在进程挂或者宕机恢复后,也会用到 RandomAccessFile 来从指定点位开始读取,不需要从整个文件头部重新读取。关于断点续传的能力后文会提到。

RandomAccessFile raf = new RandomAccessFile(file, "r");byte[] buffer;private void readFile() {    if ((raf.length() - raf.getFilePointer()) < BUFFER_SIZE) {        buffer = new byte[(int) (raf.length() - raf.getFilePointer())];    } else {        buffer = new byte[BUFFER_SIZE];    }    raf.read(buffer, 0, buffer.length);}
复制代码


4.4 实现断点续传

机器宕机、Java 进程 OOM 重启、Agent 升级重启等这些是常有的事,那么如何在这些情况下保障采集数据的正确呢?这个问题主要考虑的是采集 Agent 断点续传的能力。一般的,我们在采集过程中需要记录当前的采集点位(采集点位,即 RandomAccessFile 中最后的指针指向的位置,一个整型数值),当 Agent 把对应缓冲区的数据成功发送到 kafka 后,此时可以先把最新点位的数值更新到内存,并且通过一个定时任务(默认是 3s)持久化内存中的采集点位数值到本地的磁盘的点位文件中。这样,当出现进程停止,重新启动时,加载本次磁盘文件中的采集点位,并使用 RandomAccessFile 移动到对应的点位,实现了从上一次停止的点位继续往下采集的能力,Agent 可以恢复到原有的状态,从而实现了断点续传,有效规避重复采集或者漏采集的风险。


Agent 针对的每一个采集任务会有一个对应的点位文件,一个 Agent 如果有多个采集任务,将会对应多个点位文件。一个点位文件存储的内容格式为 JSON 数组(如下图所示)。其中 file 表示任务所采集的文件的名字,inode 即文件的 inode,pos 即 position 的缩小,表示点位的数值;

[    {        "file": "/home/sample/logs/bees-agent.log",        "inode": 2235528,        "pos": 621,        "sign": "cb8730c1d4a71adc4e5b48931db528e30a5b5c1e99a900ee13e1fe5f935664f1"    }]
复制代码

4.5 实时数据发送

前面主要介绍了,日志文件的实时的发现、实时的日志内容变更监听、日志内容的读取等设计方案,接下来介绍 Agent 的数据发送


最简单的模型是,Agent 通过 Kafka Client 把数据直接发送到 Kafka 分布式消息中间件,这也是一种简洁可行的方案。实际上在 Bees 的采集链路架构中,在 Agent 与 Kafka 的数据链路中我们增加了一个"组件 bees-bus“(如下图所示)。


bees-bus 组件主要起到汇聚数据的作用,类似于 Flume 在采集链路中聚合的角色。Agent 基于 Netty 开源框架实现 NettyRpcClient 与 Bus 之间通讯实现数据发送。网络传输部分展开讲内容较多,非本文章重点就此带过(具体可参考 Flume NettyAvroRpcClient 实现)。


这里稍微补充下,我们引入 bees-bus 的目的主要有以下几个:

  1. 收敛来自于 Agent 过多的网络连接数,避免所有 Agent 直连 Kafka broker 对其造成较大的压力;

  2. 数据汇聚到 Bus 后,Bus 具备流量多路输出的能力,可以实现跨机房 Kafka 数据容灾;

  3. 在遇到流量陡增的情况下, 会导致 topic 分区所在 broker 机器磁盘 IO 繁忙进而导致数据反压到客户端, 由于 kafka 副本迁移比较耗时所以出现问题后恢复较慢,Bus 可以起到一层缓冲层的作用。


4.6 离线采集能力

除了上面常见的实时日志采集的场景外(一般是日志采集到 kafka 这类消息中间件),Bees 采集还有一个离线日志采集的场景。所谓离线日志采集,一般是指把日志文件是采集到 HDFS 下(参考下图)。


这些日志数据是用于下游的 Hive 离线数仓建设、离线报表分析使用。该场景数据时效性没有那么强,一般是按天为单位使用数据(我们常说的 T+1 数据),所以日志数据采集无需像实时日志采集一样,实时的一行一行的采集。离线采集一般可以按照固定时间一个批次采集。我们默认是每隔一小时定时采集上个小时产生的一个完整的小时日志文件,比如在 21 点的 05 分,采集 Agent 则开始采集上个小时产生的日志文件(access.2021110820.log),该文件保存了 20 点内产生的完整的(20:00~20:59)日志内容。



实现离线的采集能力,我们的 Agent 通过集成 HDFS Client 的基本能力来实现,HDFS Client 中使用 FSDataOutputStream 可以快速的完成一个文件 PUT 到 HDFS 的目录下。


尤其要关注的一点是,离线采集需要特别的增加了一个限流采集的能力。由于离线采集的特点是,在整点左右的时刻,所有的机器上的 Agent 会几乎同时全量开启采集,如果日志量大、采集速度过快,可能会造成该时刻公司网络带宽被快速占用飙升,超出全网带宽上限,进一步会影响其他业务的正常服务,引发故障;还有一个需要关注的就是离线采集整点时刻对机器磁盘资源的需求是很大,通过限流采集,可以有效削平对磁盘资源的整点峰值,避免影响其他服务。

4.7 日志文件清理策略

业务日志源源不断的产生落到机器的磁盘上,单个小时的日志文件大小,小的可能是几十 MB,大的可以是几十 GB,磁盘很有可能在几小时内被占满,导致新的日志无法写入造成日志丢失,另一方面可能导致更致命的问题,linux 操作系统报 “No space left on device 异常",引发其他进程的各种故障;所以机器上的日志文件需要有一个清理的策略。


我们采用的策略是,所有的机器都默认启动了一个 shell 的日志清理脚本,定期检查固定目录下的日志文件,规定日志文件的生命周期为 6 小时,一旦发现日志文件是 6 小时以前的文件,则会对其进行删除(执行 rm 命令)。


因为日志文件的删除,不是由日志采集 Agent 自身发起和执行的,那么可能出现”采集速度跟不上删除速度(采集落后 6 小时)“的情况。比如日志文件还在采集,但是删除脚本已经检测到该文件生命周期已达 6 小时准备对其进行删除;这种情况,我们只需要做好一点,保证采集 Agent 对该日志文件的读取句柄是正常打开的,这样的话,即使日志清理进程对该文件执行了 rm 操作(执行 rm 后只是将该文件从文件系统的目录结构上解除链接 unlink,实际文件还未从磁盘彻底删除),采集 Agent 持续打开的句柄,依然能正常采集完此文件;这种"采集速度跟不上删除速度"是不能长时间存在,也有磁盘满的风险,需要通过告警识别出来,根本上来说,需要通过负载均衡或者降低日志量的方法,来减少单机器日志长时间采集不过来的情况。

4.8 系统资源消耗与控制

Agent 采集进程是随着业务进程一起部署在一个机器上的,共同使用业务机器的资源(CPU、内存、磁盘、网络),所以在设计时,要考虑控制好 Agent 采集进程对机器资源的消耗,同时要做好对 Agent 进程对机器资源消耗的监控。一方面保障业务有稳定的资源可以正常运行;另外可以保障 Agent 自身进程正常运作。通常我们可以采用以下方案:


1. 针对 CPU 的消耗控制。

我们可以较方便采用 Linux 系统层面的 CPU 隔离的方案来控制,比如 TaskSet;通过 TaskSet 命令,我们可以在采集进程启动时,设定采集进程绑定在某个限定的 CPU 核心上面(进程绑核,即设定进程与 CPU 亲和性,设定以后 Linux 调度器就会让这个进程/线程只在所绑定的核上面去运行);这样的设定之后,可以保障采集进程与业务进程在 CPU 的使用上面互相不影响。


2. 针对内存的消耗控制。

由于采集 Agent 采用 java 语言开发基于 JVM 运行,所以我们可以通过 JVM 的堆参数配置即可控制;bees-agent 一般默认配置 512MB,理论上最低值可以是 64MB,可以根据实际机器资源情况和采集日志文件大小来配置;事实上,Agent 的内存占用相对稳定,内存消耗方面的风险较小。


3.针对磁盘的消耗控制。

由于采集 Agent 是一个 IO 密集型进程,所以磁盘 IO 的负载是我们需要重点保障好的;在系统层面没有成熟的磁盘 IO 的隔离方案,所以只能在应用层来实现。我们需要清楚进程所在磁盘的基准性能情况,然后在这个基础上,通过 Agent 自身的限速采集能力,设置采集进程的峰值的采集速率(比如:3MB/s、5MB/s);除此之外,还需要做好磁盘 IO 负载的基础监控与告警、采集 Agent 采集速率大小的监控与告警,通过这些监控告警与值班分析进一步保障磁盘 IO 资源。


4.针对网络的消耗控制。

这里说的网络,重点要关注是跨机房带宽上限。避免同一时刻,大批量的 Agent 日志采集导致跨机房的带宽到达了上限,引发业务故障。所以,针对网络带宽的使用也需要有监控与告警,相关监控数据上报到平台汇总计算,平台通过智能计算后给 Agent 下发一个合理的采集速率。

4.9 自身日志监控

为了更好的监控线上所有的 Agent 的情况,能够方便地查看这些 Agent 进程自身的 log4j 日志是很有必要的。为了达成这一目的,我们把 Agent 自身产生的日志采集设计成一个普通的日志采集任务,就是说,采集 Agent 进程自身,自己采集自己产生的日志,于是就可以把所有 Agent 的日志通过 Agent 采集汇聚到下游 Kafka,再到 Elasticsearch 存储引擎,最后通过 Kibana 或其他的日志可视化平台可以查看。

4.10 平台化管理

目前的生产环境 Agent 实例数量已经好几万,采集任务数量有上万个。为了对这些分散的、数据量多的 Agent 进行有效的集中的运维和管理,我们设计了一个可视化的平台,管理平台具备以下 Agent 控制能力:Agent 的现网版本查看,Agent 存活心跳管理,Agent 采集任务下发、启动、停止管理,Agent 采集限速管理等;需要注意的是,Agent 与平台的通讯方式,我们设计采用简单的 HTTP 通讯方式,即 Agent 以定时心跳的方式(默认 5 分钟)向平台发起 HTTP 请求,HTTP 请求体中会包含 Agent 自身信息,比如 idc、ip、hostname、当前采集任务信息等,而 HTTP 返回体的内容里会包含平台向 Agent 下发的任务信息,比如哪个任务启动、哪个任务停止、任务的具体参数变更等。



五、与开源能力对比

bees-agent 与 flume-agent 对比

  1. 内存需求大大降低。bees-agent 采用无 Channel 设计,大大节省内存开销,每个 Agent 启动 ,JVM 堆栈最低理论值可以设置为 64MB;

  2. 实时性更好。bees-agent 采用 Linux inotify 事件机制,相比 Flume Agent 轮询机制,采集数据的时效性可以在 1s 以内;

  3. 日志文件的唯一标识,bees-agent 使用 inode+文件签名,更准确,不会出现日志文件误采重采;

  4. 用户资源隔离。bees-agent 不同 Topic 的日志采集任务,采用不同的线程隔离采集,互相无影响;

  5. 真正的优雅退出。bees-agent 在正常采集过程中,随时使用平台的"停止命令"让 Agent 优雅退出,不会出现无法退出的尴尬情况,也能保证日志无任何丢失;

  6. 更丰富的指标数据。bees-agent 包括采集速率、采集总进度,还有 机器信息、JVM 堆情况、类数量、JVM GC 次数等;

  7. 更丰富的定制化能力。bees-agent 具备关键字匹配采集能力、日志格式化能力、平台化管理的能力等;


六、总结

前文介绍了 vivo 日志采集 Agent 在设计过程中的一些核心技术点:包括日志文件的发现与监听、日志文件的唯一标识符设计、日志文件的实时采集与离线采集的架构设计、日志文件的清理策略、采集进程对系统资源的消耗控制、平台化管理的思路等,这些关键的设计思路覆盖了自研采集 agent 大部分的核心功能,同时也覆盖了其中的难点痛点,能让后续的开发环节更加畅通。当然,还有一些高阶的采集能力未涵盖本文介绍在内,比如"如何做好日志采集数据的完整性对账","数据库类型的场景的采集设计"等,大家可以继续探索解决方案。


从 2019 年起,vivo 大数据业务的日志采集场景就是由 Bees 数据采集服务支撑。bees-agent 在生产环境持续服务,至今已有 3 年多的稳定运行的记录,有数万个 bees-agent 实例正在运行,同时在线支撑数万个日志文件的采集,每天采集 PB 级别的日志量。实践证明,bees-agent 的稳定行、健壮性、丰富的功能、性能与合理的资源情况,都符合最开始设计的预期,本文的设计思路的也一再被证实行之有效。

发布于: 刚刚阅读数: 3
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020-07-10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
vivo大数据日志采集Agent设计实践_大数据_vivo互联网技术_InfoQ写作社区