工作中再次使用到了Flume,记录下flume配置过程中的一些问题。
涉及到的知识点:
flume,exec source,file channel,avro sink,cumtom sink开发
一、引言
首先,为什么使用flume肯定不需要再说明了,成熟的技术框架,
各大公司都采用的技术解决方案。现在的FlumeNG是个轻量级的
工具,脱离hadoop,用作别的日志收集,实时计算也是很好用。
相关用户文档:https://flume.apache.org/FlumeUserGuide.html
二、基本概念
Flume中有以下几个核心概念:
Event,一个数据单元,带有可选消息头,如果自己写custom source
或者sink的话会接触到。
Flow:Event从源点到目的点的迁移抽象
Agent:一个独立的Flume进程,包含Source,Channel,Sink组件
Source:用来消费传递给它的Events,它是整个flume的入口
Channel:中转Event的临时存储,保存Source传递过来的数据,并
导入到Sink
Sink:负责移除Events,收集数据或者转发到下一个流动环节中去。必然是重点。
Flume的可靠性设计:
sink只有在event被转发或者被存储到指定的容器中时才会移除channel中的数据
所有的操作都是事务操作。所以只要放入到Flume中的数据,可以确保数据能够
从Flume中流出。 针对异常恢复,Channel可以支持文件管道,通过文件对channel
的数据进行暂存。
下面介绍自己项目过程中使用的技术方案:
NginxLog--->Flume Agent-->Collector-->RabbitMQ/HDFS
web服务器集群中部署Agent,读取日志,然后传递到下一个采集中转机中
中转机再将日志发送给消息队列RabbitMQ和写入HDFS存储。
中转机使用load_balance配置。
对于Source,选用了execSource,
当然为了容错考虑,用网上那种tail -F肯定是不行的
设计时需要考虑,程序宕机了怎么恢复。所以采用的技术方案是用python实现一个
更好的日志文件监控,把处理的进度写到一个文件存下来,每次启动时
加载配置文件看处理到哪了,然后继续处理。
代码是在github上找的一个改的
#coding:utf-8
#!/usr/bin/python
import sys, os, glob, time
import json
import traceback
from stat import *
from urllib import unquote
import hashlib
def sha1_dict(d):
j = json.dumps(d, sort_keys=True)
return hashlib.sha1(j).hexdigest()
def parseLineLogic(line):
listparam=line.split('##')
if len(listparam) != 6:
return
dict={}
dict['logtime']=listparam[0]
dict['ip']=listparam[1]
(dict['type'],dict['url'],httpversion)=listparam[3].split(' ')
dict['agent']=listparam[4]
param = listparam[5]
if (len(param) < 4):
param = dict['url'].split('?')[1]
paramdict = {}
paramlist = param.split('&')
for val in paramlist:
(key,value) = val.split('=')
paramdict[key] = value
if 'data' in paramdict:
paramdict['data'] = json.loads(unquote(paramdict['data']))
dict['param'] = paramdict
dict['hashid'] = sha1_dict(dict)
print json.dumps(dict)
class WatchedFile:
def __init__(self, path, inode, length):
self.path = path
self.inode = inode
self.length = length
self.fd = open(path, "r")
self.newdata = True
self.offset = 0
self.data = bytearray("")
def close(self):
self.fd.close()
def reset(self):
self.close()
self.fd = open(self.path, "r")
self.offset = 0
self.length = 0
self.data = bytearray("")
def __repr__(self):
return "WatchedFile(path = %s, inode = %d, offset = %d, length = %d, newdata = '%s')" \
% (self.path, self.inode, self.offset, self.length, str(self.newdata))
statusFile = 'tail.status'
def loadWatchFilesStatus():
FileStatus = {}
try:
statusF = open(statusFile,'r')
for line in statusF.readlines():
FileObj = json.loads(line)
#print json.dumps(FileObj)
watchFile = WatchedFile(path=FileObj['path'], inode=FileObj['inode'], length=FileObj['length'])
watchFile.newdata = True if FileObj['offset'] < FileObj['length'] else False
watchFile.offset = FileObj['offset']
watchFile.fd.seek(watchFile.offset)
FileStatus[FileObj['inode']] = watchFile
except:
#traceback.print_exc()
pass
#print 'files load:%d' %len(FileStatus)
return FileStatus
def saveWatchedFilesStatus(Files):
statusF = open(statusFile,'w')
for obFile in Files.values():
#print obFile
dt = {}
dt['inode'] = obFile.inode
dt['path'] = obFile.path
dt['offset'] = obFile.offset
dt['length'] = obFile.length
dt['newdata'] = obFile.newdata
statusF.write(json.dumps(dt)+'\n')
statusF.close()
def tail(pattern, processor):
watchedFiles = loadWatchFilesStatus()
#watchedFiles = {}
while True:
# file all items matching the pattern
for path in glob.iglob(pattern):
try:
stat = os.stat(path)
# only watch regular files
if S_ISREG(stat.st_mode):
if stat.st_ino in watchedFiles:
#print 'in watchedFies'
# update length for files already being watched
#print stat
watchedFile = watchedFiles[stat.st_ino]
if stat.st_size > watchedFile.length:
watchedFile.newdata = True
elif stat.st_size < watchedFile.length:
watchedFile.reset()
if stat.st_size > 0:
watchedFile.newdata = True
watchedFile.length = stat.st_size
#print watchedFile
else:
#print 'add watch file',path,stat.st_ino,stat.st_size
watchedFiles[stat.st_ino] = WatchedFile(path, stat.st_ino, stat.st_size)
except OSError:
# thrown by either os.stat or open
pass
# for watchedFile in watchedFiles.values():
# if not watchedFile.newdata:
for watchedFile in watchedFiles.values():
if watchedFile.newdata:
length = watchedFile.length - watchedFile.offset
if length > 0:
data = watchedFile.fd.read(length)
if data:
watchedFile.data += bytearray(data)
watchedFile.offset += processor(watchedFile.path, watchedFile.data)
watchedFile.newdata = False
# remove files which no longer exist
inodes = watchedFiles.keys()
for inode in inodes:
watchedFile = watchedFiles[inode]
if not os.path.isfile(watchedFile.path):
watchedFile.close()
del watchedFiles[inode]
saveWatchedFilesStatus(watchedFiles)
try:
time.sleep(1)
except KeyboardInterrupt:
sys.exit(0)
#break
def line_processor(path, buff):
offset = 1
bytesRead = 0
#print buff
while offset > 0:
offset = buff.find("\n")
if offset > 0:
offset += 1 # include \n
line = buff[:offset]
del buff[:offset]
#print "%s=%s" % (line.strip(), path)
try:
parseLineLogic(str(line))
except:
# traceback.print_exc()
pass
bytesRead += offset
return bytesRead
if __name__ == '__main__':
statusFile = sys.argv[2]
tail(sys.argv[1], line_processor)
class MQConstants {
public final static String CONFIG_GroupName = "msgGroupName";
public final static String CONFIG_Topic = "msgTopic";
public final static String CONFIG_Tag = "msgTag";
public final static String CONFIG_Key = "msgKey";
}
//继承自AbstractSink接口,并支持配置
public class LogSink extends AbstractSink implements Configurable {
private static final Logger log = LoggerFactory.getLogger(LogSink.class);
private String _GroupName;
private String _Topic;
private String _Tag;
private String _Key;
public LogSink(){
}
//读取配置
@Override
public void configure(Context context) {
_GroupName = context.getString(MQConstants.CONFIG_GroupName,"gateDataProducerGroup");
_Topic = context.getString(MQConstants.CONFIG_Topic, "tradeDataTopic");
_Tag = context.getString(MQConstants.CONFIG_Tag, "gateData");
_Key = context.getString(MQConstants.CONFIG_Key, "gateData");
}
@Override
public synchronized void stop() {
super.stop();
}
private void resetConnection(){
}
//实现Process过程,从代码可以看出,sink的处理都是在事务中的
@Override
public Status process() throws EventDeliveryException {
Transaction tx = getChannel().getTransaction();
try {
tx.begin();
Event e = getChannel().take();
if(e==null){
tx.rollback();
return Status.BACKOFF;
}
try {
ProducerFactory.send(_GroupName,_Topic,_Tag,_Key,e.getBody());
tx.commit();
} catch(Exception ex){
log.error(this.getName() + " - Exception while publishing...", ex);
throw ex;
}
return Status.READY;
} catch (Exception ex) {
tx.rollback();
if(log.isErrorEnabled())
log.error(this.getName() + " - Exception while publishing...", ex);
return Status.BACKOFF;
} finally {
tx.close();
}
}
}
ProducerFactory.send(_GroupName,_Topic,_Tag,_Key,e.getBody());
工程依赖的jar包为 flume-core-x.x.x.jar
附:过程中遇到的hdfsSink找不到class的问题,由于自己的flume使用的是官网下的,
hadoop使用的cdh版本,运行的时候发现找不到hadoop的相关class,这里最简单的
做法就是把hadoop share里common的jar包复制到flume的lib目录下。
当然指定flume-env.sh里面指定FLUME_CLASSPATH也是可以的
参考文章:http://www.cnblogs.com/cswuyg/p/4498804.html
flume的一些思考:
本人在设计flume过程中对一些概念的理解吧
source层:
avrosource 监听一个网络端口,通过avro协议来收取数据。通常用于flumeagent之间传输
Thriftsource 监听一个网络接口用Thrift协议来传输数据,thrift是fb开源的rpc工具,适用性就更
广泛了,之前有在百度使用过用来接收别人传输的数据。
ExecSource 这个用来监听单个文件,或者自己生产数据,抓取网络数据也可以用。
但是exec有个严重的问题就是运行的程序无法知道数据是否被channel接收了。
所以更好的方案就是自己自定义customsource来实现对应的方法。
spooling directory source这个自己一开始也比较关注,但是使用条件要求太高
要实时性就要平凡的动日志文件,而且出错了就停止了,很蛋疼
所以说考虑要收集的源头,如果是文件就用exec,spoolingdirectory,或者custom source
如果是网络数据就用thriftsource,netcatsource,或者httpsource,avrosource
sink层:
sink是持久化,或者消费数据,或者转发数据的环节,这里没啥好选的,
用了什么技术就用什么sink,没有的可以去github上搜搜别人的代码
channel层
这里主要考虑filechannel和memorychannel,为了安全肯定使用filechannel好些
但是memory速度快。调试的时候一个ctrl+c数据就拜拜
Flume监控的各个字段的含义
网上相关资料太少,只能通过源码来了解相关字段的意义
SinkCounter的使用:
这里参照hdfsSink中官方的写法
在configure的时候初始化
sinkCounter = new SinkCounter(getName());
在start的时候
调用sinkCounter.start();
结束的时候调用sinkCounter.stop();
然后可以给这个几个计数器加值
private static final String COUNTER_CONNECTION_CREATED = "sink.connection.creation.count"; //打开链接(hdfs在第一次传输数据时会调用)
private static final String COUNTER_CONNECTION_CLOSED = "sink.connection.closed.count"; //关闭链接
private static final String COUNTER_CONNECTION_FAILED = "sink.connection.failed.count";//链接失败,超时
private static final String COUNTER_BATCH_EMPTY = "sink.batch.empty"; //批量处理数据时,没有拿到消息
private static final String COUNTER_BATCH_UNDERFLOW = "sink.batch.underflow"//批量处理输出时,没有超过设置的batchSize;
private static final String COUNTER_BATCH_COMPLETE = "sink.batch.complete";//批量处理数据时,取到了batchSize的数据量
private static final String COUNTER_EVENT_DRAIN_ATTEMPT = "sink.event.drain.attempt";尝试写入数据
private static final String COUNTER_EVENT_DRAIN_SUCCESS = "sink.event.drain.sucess"; //成功写入数据条数