作者:vivo 互联网存储技术团队 - Qiu Sidi
在企业大数据体系建设过程中,数据采集是其中的首要环节。然而,以后行业内的相干开源数据采集组件,并无奈满足企业大规模数据采集的需要与无效的数据采集治理,所以大部分企业都采纳自研开发采集组件的形式。本文通过在 vivo 的日志采集服务的设计实践经验,为大家提供日志采集 Agent 在设计开发过程中的要害设计思路。
一、概述
在企业大数据体系的建设过程中,数据的解决个别蕴含 4 个步骤:采集、存储、计算和应用。其中,数据采集,是建设过程中的首要的环节,也是至关重要的环节,如果没有采集就没有数据,更谈不上后续的数据处理与应用。所以,咱们看到的企业中的经营报表、决策报表、日志监控、审计日志等的数据起源都是基于数据采集。个别的,咱们对数据采集的定义是,把各种扩散的源头上的数据(能够包含企业产品的埋点的日志、服务器日志、数据库、IOT 设施日志等)对立汇聚到大数据存储组件的过程(如下图所示)。其中,日志文件类型的采集场景,是各种数据采集类型中最常见的一种。接下来,将围绕该场景提出咱们的设计实际计划。
通常,日志采集服务能够分为几个局部(业界常见的架构如下图所示):日志采集 Agent 组件(常见的开源采集 Agent 组件有 Flume、Logstash、Scribe 等)、采集传输与存储组件(如 kafka、HDFS)、采集治理平台。Bees 采集服务是 vivo 自研的日志采集服务,本文章是通过在 Bees 采集服务中的要害组件 bees-agent 的开发实际后,总结出一个通用的日志采集 Agent 设计中的核心技术点和一些要害思考点,心愿对大家有用。
二、个性 & 能力
- 具备根本的日志文件的实时与离线采集能力
- 基于日志文件,无侵入式采集日志
- 具备自定义的过滤超大日志的能力
- 具备自定义的过滤采集、匹配采集、格式化的能力
- 具备自定义的限速采集的能力
- 具备秒级别的实时采集时效性
- 具备断点续传能力,降级和进行不丢数据
- 具备可视化的、中心化的采集工作治理平台
- 丰盛的监控指标与告警(包含采集流量、时效性、完整性等)
- 低系统资源开销(包含磁盘、内存、CPU 及网络等)
三、设计准则
- 简略优雅
- 强壮稳固
四、要害设计
目前业界风行的日志采集 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.log
62651787 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 的目标次要有以下几个:
- 收敛来自于 Agent 过多的网络连接数,防止所有 Agent 直连 Kafka broker 对其造成较大的压力;
- 数据汇聚到 Bus 后,Bus 具备流量多路输入的能力,能够实现跨机房 Kafka 数据容灾;
- 在遇到流量陡增的状况下,会导致 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 比照
- 内存需要大大降低。bees-agent 采纳无 Channel 设计,大大节俭内存开销,每个 Agent 启动,JVM 堆栈最低理论值能够设置为 64MB;
- 实时性更好。bees-agent 采纳 Linux inotify 事件机制,相比 Flume Agent 轮询机制,采集数据的时效性能够在 1s 以内;
- 日志文件的惟一标识,bees-agent 应用 inode+ 文件签名,更精确 ,不会呈现日志文件误采重采;
- 用户资源隔离。bees-agent 不同 Topic 的日志采集工作,采纳不同的线程隔离采集,相互无影响;
- 真正的优雅退出。bees-agent 在失常采集过程中,随时应用平台的 ” 进行命令 ” 让 Agent 优雅退出,不会呈现无奈退出的难堪状况,也能保障日志无任何失落;
- 更丰盛的指标数据。bees-agent 包含采集速率、采集总进度,还有 机器信息、JVM 堆状况、类数量、JVM GC 次数等;
- 更丰盛的定制化能力。bees-agent 具备关键字匹配采集能力、日志格式化能力、平台化治理的能力等;
六、总结
前文介绍了 vivo 日志采集 Agent 在设计过程中的一些核心技术点:包含日志文件的发现与监听、日志文件的惟一标识符设计、日志文件的实时采集与离线采集的架构设计、日志文件的清理策略、采集过程对系统资源的耗费管制、平台化治理的思路等,这些要害的设计思路笼罩了自研采集 agent 大部分的外围性能,同时也笼罩了其中的难点痛点,能让后续的开发环节更加畅通。当然,还有一些高阶的采集能力未涵盖本文介绍在内,比方 ” 如何做好日志采集数据的完整性对账 ”,” 数据库类型的场景的采集设计 ” 等,大家能够持续摸索解决方案。
从 2019 年起,vivo 大数据业务的日志采集场景就是由 Bees 数据采集服务撑持。bees-agent 在生产环境继续服务,至今已有 3 年多的稳固运行的记录,有数万个 bees-agent 实例正在运行,同时在线撑持数万个日志文件的采集,每天采集 PB 级别的日志量。实践证明,bees-agent 的稳固行、健壮性、丰盛的性能、性能与正当的资源状况,都合乎最开始设计的预期,本文的设计思路的也一再被证实卓有成效。