详解海量日志传输框架 Flume
什么是 Flume
本次我们来聊一聊 Flume,它是 Cloudera 提供的一个高可用、高可靠、分布式的日志收集框架,用于海量日志的采集、聚合以及传输。
Flume 在生产上使用最多的场景就是,实时读取服务器本地磁盘的数据,然后将数据写入到 HDFS。
Flume 基础架构
再来看看 Flume 的基础架构:
Agent 是一个 JVM 进程,它是 Flume 的运行实例,负责以事件的形式将数据从源端发送至目的端。然后 Agent 由三部分组成: Source、Channel、Sink,我们分别介绍。
Source
负责接收数据,并且数据可以来自于很多地方,比如日志文件,Socket 等等。
Channel
一种临时存储机制,负责在 Source 和 Sink 之间缓存数据,确保数据在被最终处理之前不会丢失。所以 Channel 有点类似消息队列,因为有了 Channel,即使 Source 和 Sink 传输数据的速率有差异也没关系。然后 Channel 还是线程安全的,可以同时处理多个 Source 的写入操作和 Sink 的读取操作。
Flume 自带两种 Channel:Memory Channel 和 File Channel。
- Memory Channel 是内存中的管道,适用于不需要关心数据丢失的场合。如果不希望数据丢失,那么 Memory Channel 就不适合了,因为程序死亡、机器宕机或者重启都会导致数据丢失。
- File Channel 将所有事件写到磁盘,因此在程序关闭或机器宕机的情况下不会丢失数据。
Sink
不断地轮询 Channel,将里面的数据发送到目的地,例如 HDFS、数据库或其它系统。
Event
Event 是 Flume 数据传输的基本单元,它由 Header 和 Body 两部分组成,其中 Header 用来存放该 Event 的一些属性(KV 结构),Body 用来存放具体数据(字节流)。
以上就是 Flume 的基础架构,可以说非常简单了。
安装 Flume
下面来安装 Flume,由于它是 Apache 的顶级项目,所以官网是 flume.apache.org,我们下载最新版本 1.11.0。下载完成后,上传到服务器,然后解压到 /opt 目录中。
Java 编写的大数据相关的框架,其目录结构基本都是类似的,bin 目录保存一些启动脚本,conf 目录保存配置文件,lib 目录保存依赖的 Jar 包。
注:如果 Flume 对接的 Hadoop 版本是 3.x,那么需要将 lib 目录中的一个 Jar 包给删掉。
# 删除该 Jar 包,兼容 Hadoop 3.x
rm -rf /opt/apache-flume-1.11.0-bin/lib/guava-11.0.2.jar
另外为了后续操作方便,建议配置一下环境变量。
Flume 简单案例
下面我们通过几个案例来学习 Flume 的用法。
Flume 监听端口数据
下面我们通过几个案例来学习 Flume 的用法,首先是监听端口数据。我们使用 Flume 监听一个端口,然后数据源向该端口发数据,Flume 收到之后再打印到控制台。
使用 Flume 时需要定义一个配置文件,在里面写上 Source、Channel、Sink 相关的配置。我们在 Flume 安装目录中创建一个 job 目录,然后在 job 目录里面创建一个 port-listener-logger.conf 文件,内容如下。
# a1 表示 Agent 的名称,在启动 Flume 的时候指定
# 创建一个 Source,名叫 r1
a1.sources = r1
# 创建一个 Channel,名叫 c1
a1.channels = c1
# 创建一个 Sink,名叫 k1
a1.sinks = k1
# 指定 a1 的输入源类型为端口类型,也就是数据要通过指定的端口发送
# 这里监听本地的 22333 端口
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 22333
# 表示 a1 的 Channel 类型是 memory,总容量是 1000 个 Event
# 并且每收集 100 个再去提交事务
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# 表示 a1 的输出是日志类型
a1.sinks.k1.type = logger
# 将 r1 和 k1 绑定到 c1 上,r1 会写入数据到 c1,然后 k1 会从 c1 读数据
a1.sources.r1.channels = c1
# 注意这里的 channel 后面没有 s,因为一个 Sink 只能绑定一个 Channel
a1.sinks.k1.channel = c1
配置文件写完之后,我们开启 Flume 监听。
# -c/--conf:指定配置文件的存储目录
# -n/--name:指定 Agent 的名称
# -f/--conf-file:指定 Flume 启动时读取的配置文件,就是我们上面手动创建的
# -Dflume.root.logger=INFO,console:-D 选项可以指定很多参数,这里指定输出位置是控制台,日志级别是 INFO
flume-ng agent -c $FLUME_HOME/conf -n a1 \
-f $FLUME_HOME/job/port-listener-logger.conf \
-Dflume.root.logger=INFO,console
执行命令:
但我们发现启动之后立马就停止了,根据提示信息可以得知 Flume 启动时找到了多个 SLF4J 绑定,SLF4J 是一个打印日志的工具。而在加载 SLF4J 对应的 Jar 包时,类名出现了冲突。
从图中可以看到有两个 Found binding in [jar:file:/opt/....../StaticLoggerBinder.class],另一个来自于 Hadoop,因为 Flume 也隶属于 Hadoop 生态圈,所以 Flume 启动之后会自动查找 HADOOP_HOME,加载相应的 Jar 包。但很明显有两个 Jar 包中都存在 StaticLoggerBinder.class,因此发生冲突了。
# 解决办法也很简单,找到 Flume 中冲突的 Jar 包,重新命个名(或者删除)就行了
# 这里是 log4j-slf4j-impl-2.18.0.jar
mv $FLUME_HOME/lib/log4j-slf4j-impl-2.18.0.jar $FLUME_HOME/lib/log4j-slf4j-impl-2.18.0.jar.bak
增加一个 .bak 后缀就可以了,然后我们重新启动。
此时进程就阻塞在这里了,我们启动 Python 编写的 Socket 程序,往 Flume 监听的 22333 端口发数据。然而问题又来了,就是数据发走了,但是却没有显示在控制台上。首先在 Flume 1.9 的时候是会正常显示的,但从 Flume 1.10 的时候,需要我们手动修改一下配置文件。
在 $FLUME_HOME/conf 目录中有一个 log4j2.xml,在 1.10 之前叫 log4j2.properties,我们打开它。
只需要增加一行,将 Console 也指定在里面,这样就会输出在控制台了。默认只有一个 LogFile,说明默认会输出到日志文件(flume.log)中。然后再重新启动 Flume,并发送数据,看看会不会显示。
显然结果没问题,消息正常打印了。当然此时不光打印在控制台,还会打印在日志文件中(默认叫 flume.log,可以通过修改 log4j2.xml 进行指定),因为 Sink 类型为 logger,并且同时指定了 Console 和 LogFile。
当然啦,Flume 收到消息数据时,一般不会输出到控制台,而是会输出到指定的存储系统中,比如本地文件、HDFD、Kafka、HBase 等等。我们看看如果要将消息输出到本地文件,该怎么做?
# 创建 Source、Channel、Sink
a1.sources = r1
a1.channels = c1
a1.sinks = k1
# 指定 Source 的类型为 netcat,监听 22333 端口
# 绑定在 c1 上,也就是将数据写入到 c1
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 22333
a1.sources.r1.channels = c1
# 设置 Channel 的类型以及容量
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# 其它地方不变,将 Sink 类型改为 file_roll,表示输出到本地文件中
a1.sinks.k1.type = file_roll
# 输出文件的存储目录,需要提前创建好
a1.sinks.k1.sink.directory = /root/flume-log
# 设置文件滚动的间隔时间(秒),为 0 表示不基于时间滚动
a1.sinks.k1.sink.rollInterval = 0
# 设置文件滚动的大小(字节)
# 这里表示当文件达到 10MB 时滚动(创建一个新的文件,老的文件封存起来)
a1.sinks.k1.sink.rollSize = 10485760
# 设置在滚动之前可接收的事件数,为 0 表示不基于事件计数滚动
a1.sinks.k1.sink.rollCount = 0
# 绑定在 c1 这个 Channel 上,也就是从 c1 读取数据
a1.sinks.k1.channel = c1
在 job 目录中创建一个文件 port-listener-local-file.conf,并将上面的配置拷贝进去,然后启动 Flume。
flume-ng agent -c $FLUME_HOME/conf -n a1 -f $FLUME_HOME/job/port-listener-local-file.conf
我们往里面 22333 端口发几条数据,看看效果。
那么 flume-log 目录中是否有内容呢?
结果没有问题,并且此时写入的不是 Event 对象,而是具体的内容。
补充:Flume 不会一直往一个文件里面输出,当满足滚动条件时,会创建一个新的文件。至于滚动条件可以基于时间、文件大小、接收的事件数进行设置,但如果 Flume 重启了,那么会直接创建一个新文件开始写入。
Flume 监控日志文件
任何一个服务在运行过程中都会产生大量的日志,这些日志会被统一收集到大数据平台进行分析。我们来模拟该场景,使用 Flume 监控一个日志文件,然后将内容输出到 HDFS 平台。
下面依旧是编写配置文件,配置好 Source、Channel、Sink,我们在 job 目录中创建一个 log-listener-hdfs.conf,写入如下内容。
a1.sources = r1
a1.channels = c1
a1.sinks = k1
# 指定 Source 的类型为 exec,表示要执行命令
a1.sources.r1.type = exec
# 执行的命令
a1.sources.r1.command = tail -f /root/test.log
# 执行命令使用的 Shell,这个一般可以不指定
a1.sources.r1.shell = /bin/bash -c
a1.sources.r1.channels = c1
# 设置 Channel 的类型以及容量
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# 指定 Sink 类型为 HDFS
a1.sinks.k1.type = hdfs
# 指定 HDFS 的地址(包含存储目录),会自动创建
a1.sinks.k1.hdfs.path = hdfs://10.0.24.11:9000/flume-log
# 是否按照时间滚动文件夹
a1.sinks.k1.hdfs.round = true
# 多少时间单位创建一个新的文件夹
a1.sinks.k1.hdfs.roundValue = 1
# 定义时间单位
a1.sinks.k1.hdfs.roundUnit = hour
# 是否使用本地时间戳,在指定存储目录时,可以使用 %Y、%m、%d、%H
# Flume 在创建目录时,会自动将占位符替换为时间,但需要将该参数指定为 true
a1.sinks.k1.hdfs.useLocalTimeStamp = true
# 每积攒(接收)多少个 Event 提交到 HDFS 一次
a1.sinks.k1.hdfs.batchSize = 100
# 设置文件类型,可支持压缩
a1.sinks.k1.hdfs.fileType = DataStream
# 设置文件滚动的间隔时间(秒),为 0 表示不基于时间滚动
a1.sinks.k1.hdfs.rollInterval = 0
# 设置文件滚动的大小(字节)
a1.sinks.k1.hdfs.rollSize = 10485760
# 设置在滚动之前可接收的事件数,为 0 表示不基于事件计数滚动
a1.sinks.k1.hdfs.rollCount = 0
# 指定输出文件的前缀,a1.sinks.k1.hdfs.Suffix 则是指定后缀
a1.sinks.k1.hdfs.filePrefix = event
# 绑定在 c1 这个 Channel 上,也就是从 c1 读取数据
a1.sinks.k1.channel = c1
然后我们创建一个 /root/test.log,每隔 1 秒钟往里面写一条数据。
# 直接在控制台中输入以下内容即可,当然你也可以创建一个 sh 文件
while true; do echo "Hello World" >> /root/test.log; sleep 1; done
然后我们使用 Flume 监控这个文件:
flume-ng agent -c $FLUME_HOME/conf -n a1 -f $FLUME_HOME/job/log-listener-hdfs.conf
执行命令开启监听,然后查看 HDFS 上面有没有数据。
显然数据已成功上传到 HDFS 中,以上就是监控日志文件,输出到 HDFS。
使用 Flume,其实就是一个不断为 Agent 编写配置文件的过程。
Flume 监控目录
Flume 除了可以监控单个日志文件外,还能监控整个目录,比如我们希望监控某个目录中出现的新文件。下面创建一个配置文件 dir-listener-hdfs.conf,里面内容如下。
a1.sources = r1
a1.channels = c1
a1.sinks = k1
# 指定 Source 的类型是 spooldir
a1.sources.r1.type = spooldir
# 指定监控的目录,会处理已存在和新增的文件
a1.sources.r1.spoolDir = /root/files
# Flume 会读取文件的内容,转成独立的 Flume 事件,然后发送到 Channel
# 一旦文件读取完成,Flume 会自动给文件增加一个 .COMPLETED 后缀,以防止重复读取
a1.sources.r1.fileSuffix = .COMPLETED
# fileHeader 设置为 true 时,Flume 会在每个事件中添加一个头部信息
# 指明该事件来源于哪个文件,这对于跟踪事件来源很有帮助
a1.sources.r1.fileHeader = true
# 指定一个正则表达式,表示忽略哪些文件,这里会忽略掉所有以 .tmp 结尾的文件
a1.sources.r1.ignorePattern = ([^ ]*\.tmp)
a1.sources.r1.channels = c1
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.path = hdfs://10.0.24.11:9000/upload-log
a1.sinks.k1.hdfs.round = true
a1.sinks.k1.hdfs.roundValue = 1
a1.sinks.k1.hdfs.roundUnit = hour
a1.sinks.k1.hdfs.useLocalTimeStamp = true
a1.sinks.k1.hdfs.batchSize = 100
a1.sinks.k1.hdfs.fileType = DataStream
a1.sinks.k1.hdfs.rollInterval = 0
a1.sinks.k1.hdfs.rollSize = 10485760
a1.sinks.k1.hdfs.rollCount = 0
a1.sinks.k1.hdfs.filePrefix = event
a1.sinks.k1.channel = c1
下面启动 Flume:
flume-ng agent -c $FLUME_HOME/conf -n a1 -f $FLUME_HOME/job/dir-listener-hdfs.conf
然后我们往 files 目录中创建几个新文件。
Flume 会每隔 500 毫秒扫描一次监控目录的文件变动,然后检测到 files 目录中新增了 a.txt 和 b.txt,并进行读取。一旦读取完毕,会自动给文件增加一个 .COMPLETED 后缀,表示读取完毕。而 c.tmp 则没有读取,因为过滤掉了以 .tmp 结尾的文件。
注意:监控目录和监控单个文件有所不同,如果是监控单个文件,那么可以不停地追加内容。但如果是监控目录,那么应该向目录中上传已经包含完整内容的文件,而不要在监控的目录中对文件进行修改。
最后再来查看一下 HDFS:
显然内容已成功上传。
Flume 监控目录(支持动态追加)
无论 Source 的类型是 exec 还是 spooldir,它们都有自己的优缺点。
当 Source 类型为 exec 时
- 优点:可以持续监控一个文件的增长,能够捕获到每次文件更新时添加的新行,并将这些新行作为独立的事件进行处理,一般和 tail -f 命令搭配。
- 缺点:exec Source 无法实现断点续传,如果 Flume 进程重启,exec Source 不会从上次停止的地方继续读取文件,而是会从执行命令那一刻的文件末尾开始读取,或者重新执行配置中的命令。这意味着在 Flume 停止和重新启动的时间里追加到文件中的数据将不会被 Flume 捕获。
当 Source 类型为 spooldir 时
- 优点:可以监控一个特定目录(称为 spooling 目录),并处理出现在该目录中的新文件。当一个新文件被放入这个目录中,Flume 将自动读取该文件的内容,并将每一行或记录转换成独立的事件,然后通过配置的 Channel 传递给 Sink 进行进一步的处理。这种方式非常适合于那些周期性生成完整文件的场景,比如批处理作业、报表生成或日志轮转(log rotation)系统,其中日志或数据文件在完成后被移动到 spooling 目录中。
- 缺点:spooldir Source 不适合对实时追加的文件进行监控,这是因为 spooldir Source 被设计用来读取一次性放入的完整文件,而不是持续写入的流式文件。一旦开始处理一个文件,它会读取文件到最后,并给文件增加一个后缀,然后等待处理下一个文件。如果文件在此期间被追加内容,这些新增的内容不会被 spooldir Source 读取或处理,除非文件重命名后重新放入监控目录中。
那么问题来了,如果我希望不仅要监控目录中的新增文件,还要监控发生变更的文件,该怎么做呢?Flume 提供了类型为 taildir 的 Source,可以同时具备 exec 和 spooldir 的功能。我们创建一个新的配置文件 taildir-listener-hdfs.conf,在里面写上如下内容。
a1.sources = r1
a1.channels = c1
a1.sinks = k1
# 监控目录中的新文件,以及变更的文件
a1.sources.r1.type = taildir
# taildir Source 支持断点续传,因为它会记录每个被监控文件的读取位置
# 这样即使 Flume 重启,它也能从上次停止的地方继续读取
# 而位置信息,便写在 a1.sources.r1.positionFile 中
a1.sources.r1.positionFile = /root/tail_dir_position.json
# 定义一个文件组,里面包含 f1 和 f2 两个成员
a1.sources.r1.filegroups = f1 f2
# 定义 f1 和 f2,指定要监控的文件路径,支持正则表达式
# 此时 /root/files/ 目录中文件名包含 file 的文件,和 /root/files2/ 目录中文件名包含 log 的文件,都会受到监控
a1.sources.r1.filegroups.f1 = /root/files1/.*file.*
a1.sources.r1.filegroups.f2 = /root/files2/.*log.*
# 当设置为 true 时,Flume 会在每个事件中添加一个头信息,表明该事件来自哪个文件
a1.sources.r1.fileHeader = true
# 如果 fileHeader 为 true,这里可以指定头信息的键名
# a1.sources.r1.fileHeaderKey = fileName
# 当文件没有新内容时,Flume 等待下一次尝试读取的时间将按此值增加
a1.sources.r1.backoffSleepIncrement = 1000
# 设置 Flume 在增加等待时间后的最大等待时间
a1.sources.r1.maxBackoffSleep = 8000
# 当首次监控文件时,如果设置为 true,Flume 将跳到文件末尾开始读取,而不是从文件开头
a1.sources.r1.skipToEnd = true
# 设置读取文件时使用的缓冲区大小
a1.sources.r1.bufferSize = 4096
a1.sources.r1.channels = c1
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.path = hdfs://10.0.24.11:9000/taildir-log
a1.sinks.k1.hdfs.round = true
a1.sinks.k1.hdfs.roundValue = 1
a1.sinks.k1.hdfs.roundUnit = hour
a1.sinks.k1.hdfs.useLocalTimeStamp = true
a1.sinks.k1.hdfs.batchSize = 100
a1.sinks.k1.hdfs.fileType = DataStream
a1.sinks.k1.hdfs.rollInterval = 0
a1.sinks.k1.hdfs.rollSize = 10485760
a1.sinks.k1.hdfs.rollCount = 0
a1.sinks.k1.hdfs.filePrefix = event
a1.sinks.k1.channel = c1
然后我们启动 Flume:
flume-ng agent -c $FLUME_HOME/conf -n a1 -f $FLUME_HOME/job/taildir-listener-hdfs.conf
启动之后创建几个文件,并写入内容。
我们写入了 5 条数据,但很明显第 3 条是不会被 Flume 处理的,因为 files1 目录中只有文件名包含 file 的文件才会受到监控。其它 4 条数据则会正常写入,我们查看 HDFS 验证一下。
结果没有问题,数据成功写入了,以上就是 taildir Source 的功能,非常强大。如果 taildir Source 的组中只定义了一个成员,比如只定义了 f1,然后将 f1 配置为某个具体的文件,那么完全可以实现 exec Source 的功能。并且相比 exec Source,taildir Source 还记录了文件的读取位置。
taildir Source 维护了一个 JSON 格式的 position File,会定期往 position File 中更新每个文件读取到的最新位置,因此能够实现断点续传。然后看一下 position File 里的内容,有一个 key 叫 inode。如果熟悉 Linux 的话,那么肯定知道这个 inode,因为 Linux 中储存文件元数据的区域就叫 inode。每个 inode 都有一个编号,Unix / Linux 系统内部不使用文件名,而是使用 inode 编号来识别文件。
Flume 的内部细节
说完了 Flume 的几个简单案例,我们再来看看 Flume 的内部细节。
事务
事务用于保证数据从源(Source)到通道(Channel)再到接收器(Sink)的过程中的可靠性和原子性。这意味着在整个数据传输过程中,每个事件要么完全成功传输,要么在失败时完全回滚,以确保数据的一致性和完整性。整个过程如下:
1)开始事务:在传输数据之前,首先会在 Channel 上开始一个新的事务,这个过程涉及准备数据传输所需的所有资源和状态。
2)写入数据:一旦事务开始,Source 会将事件写入 Channel。如果 Source 是从外部系统(例如日志文件或消息队列)读取数据,这个步骤会包括从外部系统提取数据并将其封装成 Flume 事件。
3)提交或回滚事务。
- 提交:如果事件成功地从 Source 写入 Channel,那么事务会被提交。这意味着所有在事务期间发送的事件都会被永久地保存在 Channel 中,等待 Sink 进一步处理。
- 失败:如果在写入过程中发生错误(如网络问题、数据格式错误、资源不足等),事务将被回滚。这意味着所有尝试在当前事务中写入的事件都将被丢弃,好像它们从未被发送过一样。
4)Sink 事务:类似地,当 Sink 从 Channel 读取事件进行处理时(例如写入文件系统、数据库或发送到另一个系统),它也会在一个事务中进行。如果所有事件都成功处理,Sink 会提交事务;如果处理过程中出现错误,它会回滚事务。
因此事务是确保数据可靠传输的关键机制,维护了数据管道的完整性、可靠性和一致性。
- 数据完整性:通过事务,Flume 确保数据在传输过程中不会因为任何故障而丢失或损坏。
- 可靠性:即使在系统出现故障的情况下,事务也保证了数据传输的可靠性,因为失败的事务会被重新尝试直到成功为止。
- 一致性:事务机制确保了数据的一致性,即所有数据都以原子方式处理,要么全部成功,要么全部失败。
Put 事务流程:
- doPut:将批数据先写入临时缓冲区 putList。
- doCommit:检查 Channel 内存队列是否足够合并。
- doRollback:Channel 内存队列空间不足,回滚数据。
Take 事务流程:
- doTake:将数据读取到临时缓冲区 takeList,然后发送到 HDFS。
- doCommit:如果数据全部发送成功,则清除临时缓冲区 takeList。
- doRollback:如果数据发送过程中出现异常,rollback 将临时缓冲区 takeList 中的数据归还给 Channel 内存队列。
Flume Agent 原理
Flume Agent 有三个核心组件:Source、Channel、Sink,这些组件在一个 Agent 中可以定义多个,比如:
a1.sources = r1 r2
a1.channels = c1 c2
a1.sinks = k1 k2
# 一个 Source 可以绑定多个 Channel
a1.sources.r1.channels = c1 c2
# 但是一个 Sink 只能绑定一个 Channel
a1.sinks.k1.channel = c1
那么问题来了,如果 Source 绑定了多个 Channel,发送数据的时候要发给哪一个 Channel 呢?这就涉及到 ChannelSelector 组件,它的作用是选出 Event 要被发送到哪一个 Channel,有两种类型。
- ReplicatingChannelSelector:将同一个 Event 发给所有的 Channel,默认选项。
- MultiplexingChannelSelector:会根据相应的原则,将不同的 Event 发给不同的 Channel。
当然啦,图中还少了 Sink 这一步,因为 Sink 还依赖一个叫 SinkProcessor 的组件,该组件有三种类型:
- DefaultSinkProcessor:对应单个 Sink。
- LoadBalancingSinkProcessor:对应一组 Sink,并实现负载均衡。
- FailoverSinkProcessor:对应一组 Sink,支持错误恢复。
Channel 的数据并不会直接进入 Sink,而是会先进入 SinkProcessor,然后 SinkProcessor 再将数据发给 Sink,最后 Sink 将数据导出到外部系统。
Flume 拓扑结构
前面说了,一个 Flume Agent 里面可以有多个 Source、Channel、Sink,而 Agent 也可以有多个,多个 Agent 之间串联起来,下面就来看看具体细节。
1)简单串联
这种模式是将多个 Flume 顺序连接起来了,从最初的 Source 开始到最终的 Sink。但此模式不建议桥接过多的 Flume, 因为 Flume 过多不仅会影响传输速率,而且一旦传输过程中某个节点的 Flume 宕机,还会影响整个传输系统。
2)复制和多路复用
Flume 支持将事件流向一个或多个目的地,这种模式可以将相同数据复制到多个 Channel 中,或者将不同数据分发到不同的 Channel 中,然后 sink 可以选择传送到不同的目的地。
3)负载均衡和故障转移
Flume 支持在逻辑上将多个 Sink 归为一个 Sink 组,而 Sink 组配合不同的 SinkProcessor 可以实现负载均衡和错误恢复的功能。
4)聚合
这种模式是最常见的,也非常实用。比如大型 Web 应用可以分布在上百、上千、甚至上万台服务器上,那么这些服务器产生的日志要怎么处理呢。显然用 Flume 的这种聚合方式能很好地解决这一问题,每台服务器都部署一个 Flume 负责采集本地产生的日志,然后这些 Flume 再将采集的内容传送到一个统一的中心 Flume 中,最后再由该 Flume 将日志信息上传到 HDFS、HBase 等外部存储系统。
Flume 企业复杂案例
说完了 Flume 的拓扑结构,我们实际应用起来,做几个复杂的案例巩固一下。
复制和多路复用
需求如下:使用 Flume Agent1 监控文件变动,然后将变动内容同时传递给 Agent2 和 Agent3,其中 Agent2 负责存储到 HDFS,Agent3 负责输出到本地文件系统。
下面来编写配置文件,因为有三个 Agent,所以需要编写三份。
配置 Agent1
在 $FLUME_HOME/job 中创建一个 flume-file-flume.conf,里面写上如下内容。
# Agent1 有一个 Source,两个 Channel,两个 Sink
a1.sources = r1
a1.channels = c1 c2
a1.sinks = k1 k2
# Source 类型为 exec,监控 /root/log_file.log 文件(需要提前创建好)
a1.sources.r1.type = exec
a1.sources.r1.command = tail -f /root/log_file.log
a1.sources.r1.shell = /bin/bash -c
# 将 r1 同时绑定在 c1、c2 两个 Channel 上
a1.sources.r1.channels = c1 c2
# 在发送时,每个 Channel 都要发一份
# 如果只想发给其中一个 Channel,那么类型要指定为 multiplexing
a1.sources.r1.selector.type = replicating
# Sink 类型为 avro,表示导出的目的地也是 Flume
a1.sinks.k1.type = avro
a1.sinks.k1.hostname = localhost
a1.sinks.k1.port = 4001
a1.sinks.k2.type = avro
a1.sinks.k2.hostname = localhost
a1.sinks.k2.port = 4002
# 将 k1 绑定在 c1 上,k2 绑定在 c2 上
a1.sinks.k1.channel = c1
a1.sinks.k2.channel = c2
# 设置 Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
a1.channels.c2.type = memory
a1.channels.c2.capacity = 1000
a1.channels.c2.transactionCapacity = 100
配置 Agent2
在 $FLUME_HOME/job 中创建一个 flume-file-hdfs.conf,里面写上如下内容。
# 注意:我们要在同一个节点上启动三个 Flume Agent
# 所以 Agent 的名字不能重复,这里指定为 a2
a2.sources = r1
a2.channels = c1
a2.sinks = k1
# Agent1 的 Sink 要发给 Agent2,所以 Agent1 的 Sink 和 Agent2 的 Source 都要是 avro 类型
a2.sources.r1.type = avro
a2.sources.r1.bind = localhost
a2.sources.r1.port = 4001
a2.sources.r1.channels = c1
# Channel
a2.channels.c1.type = memory
a2.channels.c1.capacity = 1000
a2.channels.c1.transactionCapacity = 100
# Sink,类型为 hdfs,这里就不设置那么多属性了
a2.sinks.k1.type = hdfs
a2.sinks.k1.hdfs.path = hdfs://10.0.24.11:9000/product-log
a2.sinks.k1.hdfs.batchSize = 100
a2.sinks.k1.hdfs.fileType = DataStream
a2.sinks.k1.hdfs.rollInterval = 0
a2.sinks.k1.hdfs.rollSize = 10485760
a2.sinks.k1.hdfs.rollCount = 0
a2.sinks.k1.channel = c1
配置 Agent3
在 $FLUME_HOME/job 中创建一个 flume-file-local-file.conf,里面写上如下内容。
a3.sources = r1
a3.channels = c1
a3.sinks = k1
# Agent1 的 Sink 要发给 Agent3,所以 Agent1 的 Sink 和 Agent3 的 Source 都要是 avro 类型
a3.sources.r1.type = avro
a3.sources.r1.bind = localhost
a3.sources.r1.port = 4002
a3.sources.r1.channels = c1
# Channel
a3.channels.c1.type = memory
a3.channels.c1.capacity = 1000
a3.channels.c1.transactionCapacity = 100
# Sink,类型为 file_roll
a3.sinks.k1.type = file_roll
a3.sinks.k1.sink.directory = /root/product-log
a3.sinks.k1.sink.rollInterval = 0
a3.sinks.k1.sink.rollSize = 10485760
a3.sinks.k1.sink.rollCount = 0
a3.sinks.k1.channel = c1
以上几个配置文件就编写完成,下面启动 Flume。
# 启动 Flume Agent2
flume-ng agent -c $FLUME_HOME/conf -n a2 -f $FLUME_HOME/job/flume-file-hdfs.conf
# 启动 Flume Agent3
flume-ng agent -c $FLUME_HOME/conf -n a3 -f $FLUME_HOME/job/flume-file-local-file.conf
# 启动 Flume Agent1
flume-ng agent -c $FLUME_HOME/conf -n a1 -f $FLUME_HOME/job/flume-file-flume.conf
这里要先启动 Agent2 和 Agent3,然后再启动 Agent1,并且由于是前台启动,所以需要开启三个终端。然后我们追加文件内容:
echo "苹果\n草莓\n香蕉\桃子" >> /root/log_file.log
echo '草莓牛奶' >> /root/log_file.log
echo '芒果啵啵' >> /root/log_file.log
查看一下效果,看看内容有没有写入 HDFS 和本地文件系统。
结果没有问题,不过需要注意:输出的本地目录必须是已存在的目录,如果目录不存在,那么不会自动创建(但 HDFS 是会的)。
负载均衡和故障转移
需求如下:使用 Flume Agent1 监听端口,然后将数据优先发给 Agent3,如果 Agent3 挂掉,那么就发给 Agent2,最后再保存到 HDFS 上。
下面来编写配置文件,同样需要编写三份。
配置 Agent1
在 $FLUME_HOME/job 中创建一个 flume-netcat-flume.conf,里面写上如下内容。
a1.sources = r1
a1.channels = c1
a1.sinkgroups = g1 # 一个 Sink 组
a1.sinks = k1 k2 # 两个 Sink
# Source 监听 22333 端口,绑定 c1
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 22333
a1.sources.r1.channels = c1
# SinkProcessor 用于控制数据如何从 Channel 流向 Sink,有如下几种类型
# default:默认处理器类型,不提供任何故障转移机制,只是单纯地将 Channel 数据发送给每一个 Sink
# failover:允许指定一系列的 Sink,如果第一个 Sink 失败,那么事件将被转发到下一个 Sink
# load_balance:允许在多个 Sink 之间分配事件,可以基于不同的策略进行负载均衡
a1.sinkgroups.g1.processor.type = failover
# Sink 组都包含哪些 Sink
a1.sinkgroups.g1.sinks = k1 k2
# 优先级,值越大优先级越高,如果两个 Sink 都可用,那么优先发给 k2
a1.sinkgroups.g1.processor.priority.k1 = 5
a1.sinkgroups.g1.processor.priority.k2 = 10
# 发送数据给 Sink 出现失败,并进行下一次尝试之前需要等待的最长时间,单位毫秒
a1.sinkgroups.g1.processor.maxpenalty = 10000
# Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# Sink
a1.sinks.k1.type = avro
a1.sinks.k1.hostname = localhost
a1.sinks.k1.port = 4001
a1.sinks.k2.type = avro
a1.sinks.k2.hostname = localhost
a1.sinks.k2.port = 4002
# k1 和 k2 都绑定在 c1 上
# 注意:一个 Sink 只能绑定一个 Channel,但一个 Channel 可以绑定多个 Sink
a1.sinks.k1.channel = c1
a1.sinks.k2.channel = c1
配置 Agent2
在 $FLUME_HOME/job 中创建一个 flume-netcat-hdfs1.conf,里面写上如下内容。
a2.sources = r1
a2.channels = c1
a2.sinks = k1
a2.sources.r1.type = avro
a2.sources.r1.bind = localhost
a2.sources.r1.port = 4001
a2.sources.r1.channels = c1
# Channel
a2.channels.c1.type = memory
a2.channels.c1.capacity = 1000
a2.channels.c1.transactionCapacity = 100
# Sink,类型为 hdfs
a2.sinks.k1.type = hdfs
a2.sinks.k1.hdfs.path = hdfs://10.0.24.11:9000/people/log1
a2.sinks.k1.hdfs.batchSize = 100
a2.sinks.k1.hdfs.fileType = DataStream
a2.sinks.k1.hdfs.rollInterval = 0
a2.sinks.k1.hdfs.rollSize = 10485760
a2.sinks.k1.hdfs.rollCount = 0
a2.sinks.k1.channel = c1
配置 Agent3
在 $FLUME_HOME/job 中创建一个 flume-netcat-hdfs2.conf,里面写上如下内容。
a3.sources = r1
a3.channels = c1
a3.sinks = k1
# Source
a3.sources.r1.type = avro
a3.sources.r1.bind = localhost
a3.sources.r1.port = 4002
a3.sources.r1.channels = c1
# Channel
a3.channels.c1.type = memory
a3.channels.c1.capacity = 1000
a3.channels.c1.transactionCapacity = 100
# Sink,类型为 hdfs
a3.sinks.k1.type = hdfs
a3.sinks.k1.hdfs.path = hdfs://10.0.24.11:9000/people/log2
a3.sinks.k1.hdfs.batchSize = 100
a3.sinks.k1.hdfs.fileType = DataStream
a3.sinks.k1.hdfs.rollInterval = 0
a3.sinks.k1.hdfs.rollSize = 10485760
a3.sinks.k1.hdfs.rollCount = 0
a3.sinks.k1.channel = c1
以上几个配置文件就编写完成,下面启动 Flume。
# 启动 Flume Agent2
flume-ng agent -c $FLUME_HOME/conf -n a2 -f $FLUME_HOME/job/flume-netcat-hdfs1.conf
# 启动 Flume Agent3
flume-ng agent -c $FLUME_HOME/conf -n a3 -f $FLUME_HOME/job/flume-netcat-hdfs2.conf
# 启动 Flume Agent1
flume-ng agent -c $FLUME_HOME/conf -n a1 -f $FLUME_HOME/job/flume-netcat-flume.conf
启动之后我们来往 22333 端口发几条数据:
没有问题,结果一切正常。需要注意的是,文件的结尾有一个 .tmp,这表示该文件还有可能被写入。如果文件写满了,或者对应的 Flume Agent 进程停止了,那么文件结尾的 .tmp 会被去掉。
然后是 people 目录里面只有一个 log2,却没有 log1,因为 k2 的优先级高于 k1,所以数据优先发给它。但如果这时候 k2 挂掉了,那么数据就会发给 k1。由于 k2 连接的是 Agent3 的 Source,我们手动将 Agent3 进程给停掉,然后再发几条数据。
k2 挂掉了,于是数据会发给 k1,而 k1 连接的是 Agent2 的 Source。Agent2 的 Source 收到数据后流向 Channel,再发给 Sink,因此会保存在 /people/log1 目录中。因此这便是负载均衡和故障转移,当然负载均衡这里其实没有体现出来,如果想实现的话,那么只需将 SinkProcessor 的类型指定为 load_balance。
聚合
需求如下:Flume Agent1 监听 22333 端口,Flume Agent2 监控某个文件,然后它们将数据统一发给 Flume Agent3。
下面来编写配置文件,同样需要编写三份。
配置 Agent1
在 $FLUME_HOME/job 中创建一个 flume1-netcat-flume.conf,里面写上如下内容。
a1.sources = r1
a1.channels = c1
a1.sinks = k1
# Source
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 22333
a1.sources.r1.channels = c1
# Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# Sink
a1.sinks.k1.type = avro
a1.sinks.k1.hostname = localhost
a1.sinks.k1.port = 4001
a1.sinks.k1.channel = c1
配置 Agent2
在 $FLUME_HOME/job 中创建一个 flume2-local-file-flume.conf,里面写上如下内容。
a2.sources = r1
a2.channels = c1
a2.sinks = k1
# Source
a2.sources.r1.type = exec
a2.sources.r1.command = tail -f girl.txt
a2.sources.r1.channels = c1
# Channel
a2.channels.c1.type = memory
a2.channels.c1.capacity = 1000
a2.channels.c1.transactionCapacity = 100
# Sink
a2.sinks.k1.type = avro
a2.sinks.k1.hostname = localhost
a2.sinks.k1.port = 4001
a2.sinks.k1.channel = c1
配置 Agent3
在 $FLUME_HOME/job 中创建一个 flume-flume-hdfs.conf,里面写上如下内容。
a3.sources = r1
a3.channels = c1
a3.sinks = k1
# Source
a3.sources.r1.type = avro
a3.sources.r1.bind = localhost
a3.sources.r1.port = 4001
a3.sources.r1.channels = c1
# Channel
a3.channels.c1.type = memory
a3.channels.c1.capacity = 1000
a3.channels.c1.transactionCapacity = 100
# Sink
a3.sinks.k1.type = hdfs
a3.sinks.k1.hdfs.path = hdfs://10.0.24.11:9000/girl
a3.sinks.k1.hdfs.batchSize = 100
a3.sinks.k1.hdfs.fileType = DataStream
a3.sinks.k1.hdfs.rollInterval = 0
a3.sinks.k1.hdfs.rollSize = 10485760
a3.sinks.k1.hdfs.rollCount = 0
a3.sinks.k1.channel = c1
Agent1 和 Agent2 的数据都会通过 4001 端口发给 Agent3,下面将它们启动起来。
# 启动 Flume Agent3
flume-ng agent -c $FLUME_HOME/conf -n a3 -f $FLUME_HOME/job/flume-flume-hdfs.conf
# 启动 Flume Agent1
flume-ng agent -c $FLUME_HOME/conf -n a1 -f $FLUME_HOME/job/flume1-netcat-flume.conf
# 启动 Flume Agent2
flume-ng agent -c $FLUME_HOME/conf -n a2 -f $FLUME_HOME/job/flume2-local-file-flume.conf
然后我们发几条数据。
可以看到端口数据和文件追加的数据,都发送到 HDFS 里面了,并且哪个先产生,哪个就先发送。
自定义 Interceptor(拦截器)
在实际开发中,一台服务器产生的日志类型可能有很多种,而不同类型的日志需要发送到不同的分析系统。
如果 a1.sources.r1.selector.type 为 replicating,那么 Source 会将事件发给所有的 Channel,但现在我们希望的是 Source 只将事件发给其中一个 Channel,那么就需要将类型从 replicating 换成 multiplexing。multiplexing 的原理是,根据 Event 中 Header 的某个 key 值,将不同的 Event 发给不同的 Channel。所以我们需要自定义一个 Interceptor,为不同类型的 Event 的 Header 中的 key 赋予不同的值。
但由于 Flume 是 Java 语言开发的,拦截器也需要用 Java 来编写,而我本人是做 Python 的,所以关于拦截器的编写就不赘述了。
自定义 Source
Source 是负责接收数据到 Channel 的组件,Source 组件可以处理各种类型、各种格式的日志数据,包括 avro、thrift、exec、jms、spooling directory、netcat、sequence generator、syslog、http、legacy。可以看到官方提供的 Source 类型已经有很多了,但有时还是不能满足实际开发当中的需求,此时我们就需要根据实际需求自定义某些 Source。
官方也提供了自定义 Source 的接口,可以查看相关文档。根据官方说明,自定义 MySource 需要继承 AbstractSource 类,并实现 Configurable 和 PollableSource 接口。所以它同样需要使用 Java 编写,这里就不赘述了。
另外 Flume 包含的 Source 其实已经足够我们用了,当然这些 Source 我们只介绍了一小部分,其它的 Source 可以通过官网查看,文档非常详细。
自定义 Sink
Sink 不断地轮询 Channel 中的事件,然后将这些事件批量写入到存储或索引系统,当然也可以发送给另一个 Flume Agent,完事之后再将数据从 Channel 中删除。Sink 是完全事务性的,在从 Channel 批量删除数据之前,每个 Sink 会用 Channel 启动一个事务,批量事件一旦成功写到存储系统或下一个 Flume Agent,Sink 就利用 Channel 提交事务。事务一旦被提交,事件就会从 Channel 的内部缓冲区中删除。
Sink 组件的目的地包括 hdfs、logger、avro、thrift、ipc、file、null、hbase、solr、kafka、自定义。官方提供的 Sink 类型已经很多,但有时候并不能满足实际开发当中的需求,此时我们就需要根据实际需求自定义某些 Sink。而官方也提供了自定义 Sink 的接口,可以查看相关文档。根据官方说明, 自定义 MySink 需要继承 AbstractSink 类并实现 Configurable 接口。所以自定义 Sink 也需要使用 Java 编写,这里不再赘述。
当然啦,Sink 的类型有很多种,我们同样只介绍了一小部分,其它的 Sink 可以通过官网查看。
企业的一些真实面试题
下面通过一些面试题,来回顾一下 Flume。
你是如何实现 Flume 数据传输的监控的
这个前面没有说,这里补充一下。有一个第三方框架 Ganglia 可以监控 Flume 的运行状态,它由三部分组成:
- gmond(Ganglia Monitoring Daemon):一种轻量级服务,安装在每台需要收集指标数据的节点上。使用 gmond,你可以很容易收集到大量的系统指标数据,如 CPU、内存、磁盘、网络和活跃进程的数据等。
- gmetad(Ganglia Meta Daemon):整合所有信息,并将其以 RRD 格式存储至磁盘的服务。
- gweb(Ganglia Web):Ganglia 可视化工具,gweb 是一种利用浏览器显示 gmetad 所存储数据的 PHP 前端,在 Web 界面中以图表方式展现集群的运行状态下收集到的多种不同指标数据。
Flume 的 Source,Sink,Channel 的作用是什么
- Source 组件是专门用来收集数据的,可以处理各种类型、各种格式的日志数据,包括 avro、thrift、exec、jms、spooling directory、netcat、sequence generator、syslog、 http、legacy 等。
- Channel 组件对采集到的数据进行缓存,可以存放在 memory 或 file 中。
- Sink 组件是用于把数据发送到指定的目的地,目的地包括 hdfs、logger、avro、 thrift、ipc、file、Hbase、solr、Kafka、自定义等。
解释一下 Flume 的 Channel Selectors 的作用
Channel Selectors 用于决定 Source 发来的数据应该进入哪一个 Channel,它有两种类型:replicating 和 multiplexing。
- replicating:返回的列表中包含所有的 Channel,也就是说从 Source 过来的数据要进入所有的 Channel。
- multiplexing:返回的列表中只包含指定的 Channel,也就是说从 Source 过来的数据只会进入指定的 Channel。
Flume 的事务机制
Flume 的事务机制(类似数据库的事务机制)是指,Flume 使用两个独立的事务分别负责从 Soucrce 到 Channel、从 Channel 到 Sink 的事件传递。
比如 spooling directory source 为文件的每一行创建一个事件,一旦事务中所有的事件全部传递到 Channel 并且提交成功,那么 Soucrce 就将该文件标记为已完成。同理,事务以类似的方式处理从 Channel 到 Sink 的传递过程,如果因为某种原因使得事件无法记录,那么事务将会回滚,并且所有的事件都会保持到 Channel 中,等待重新传递。
Flume 采集数据会丢失吗
根据 Flume 的架构原理,Flume 是不可能丢失数据的,因为其内部有完善的事务机制。Source 到 Channel 是事务性的,Channel 到 Sink 也是事务性的,因此这两个环节不会出现数据的丢失。唯一可能丢失数据的情况是 Channel 采用 memory 类型,然后 Agent 宕机导致数据丢失,或者 Channel 存储空间已满,导致 Source 不再写入,未写入的数据丢失。
注:Flume 不会丢失数据,但是有可能造成数据的重复。例如数据已经成功由 Sink 发出,但是没有接收到响应,那么 Sink 会再次发送数据,此时就可能导致数据的重复。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏