背景介绍、业务场景:
有的时候希望通过Flume将读取的文件再细分存储,
比如:source的数据按照业务类型分开存储,具体一点比如类似:将source中web、wap、media等的内容分开存储;
比如:过滤或修改一些数据。
比如在收集数据的event的hander中加入处理的时间戳,agent的主机或者IP,固定的key-valueg。
就可以考虑使用interceptors。
Flume中的拦截器(interceptor),用户Source读取events发送到Sink的时候,在events header中加入一些有用的信息,或者对events的内容进行过滤,完成初步的数据清洗。interceptors 是 source 的拦截器 。主要的作用就是对于一个source 可以指定一个或者多个interceptors 按先后的顺序对数据进行处理 。
Flume有各种自带的拦截器,如:
Timestamp 拦截器:在Event Header中添加时间戳。
Host 拦截器:在Event Header中添加agent运行机器的Host或IP。
Static 拦截器:在Event Header中添加自定义静态属性。
Remove Header拦截器:可移除Event Header中指定属性。
UUID拦截器:在Event Header中添加全局唯一UUID。
Search and Replace拦截器:基于正则搜索和替换字符串等。
Regex Filtering拦截器:基于正则过滤或反向过滤Event。
Regex Extractor拦截器:基于正则在Event Header添加指定的Key,并将匹配到的内容作为对应的Value
通过使用不同的拦截器,实现不同的功能。但是以上的这些拦截器,不能改变原有日志数据的内容或者对日志信息添加一定的处理逻辑,当一条日志信息有几十个甚至上百个字段的时候,在传统的Flume处理下,收集到的日志还是会有对应这么多的字段,也不能对你想要的字段进行对应的处理。
根据实际业务的需求,为了更好的满足数据在应用层的处理,通过自定义Flume拦截器,过滤掉不需要的字段,并对指定字段加密处理,将源数据进行预处理。减少了数据的传输量,降低了存储的开销。
一、自定义拦截器类型必须是:类全名$内部类名,其实就是内部类名称
如:zhouls.bigdata.MySearchAndReplaceInterceptor$Builder
二、为什么这样写
至于为什么这样写:是因为Interceptor接口还有一个 公共的内部接口(Builder) ,所以自定义拦截器 要是实现 Builder接口,也就是实现一个内部类(该内部类的主要作用是:获取flume-conf.properties 自定义的 参数,并将参数传递给 自定义拦截器)
内容包括:
1. 定义一个类CustomParameterInterceptor实现Interceptor接口。
2. 在CustomParameterInterceptor类中定义变量,这些变量是需要到 Flume的配置文件中进行配置使用的。每一行字段间的分隔符(fields_separator)、通过分隔符分隔后,所需要列字段的下标(indexs)、多个下标使用的分隔符(indexs_separator)、多个下标使用的分隔符(indexs_separator)。
3. 添加CustomParameterInterceptor的有参构造方法。并对相应的变量进行处理。将配置文件中传过来的unicode编码进行转换为字符串。
4. 写具体的要处理的逻辑intercept()方法,一个是单个处理的,一个是批量处理。
5. 接口中定义了一个内部接口Builder,在configure方法中,进行一些参数配置。并给出,在flume的conf中没配置一些参数时,给出其默认值。通过其builder方法,返回一个CustomParameterInterceptor对象。
6. 定义一个静态类,类中封装MD5加密方法
7. 通过以上步骤,自定义拦截器的代码开发已完成,然后打包成jar, 放到Flume的根目录下的lib中
flume自定义拦截器 - 简书
//只保留两个接口的数据
package deng.yb.flume_ng_Interceptor;
public class MyInterceptor implements Interceptor {
/**
* epp接口-request
*/
private final String EPP_REQUEST = "POST /api/sky_server_data_app/track/user_time HTTP/1.1";
/**
* app接口-request
*/
private final String APP_REQUEST = "POST /api/sky_server_data_app/code/app_code HTTP/1.1";
public void close() {
}
public void initialize() {
}
public Event intercept(Event event) {
String body = new String(event.getBody(), Charsets.UTF_8);
if (body.indexOf(EPP_REQUEST) > -1 || body.indexOf(APP_REQUEST) > -1) {
event.setBody(body.toString().getBytes());
return event;
}
return null;
}
public List<Event> intercept(List<Event> events) {
List<Event> intercepted = new ArrayList<>(events.size());
for (Event event : events) {
Event interceptedEvent = intercept(event);
if (interceptedEvent != null) {
intercepted.add(interceptedEvent);
}
}
return intercepted;
}
public static class Builder implements Interceptor.Builder {
public void configure(Context arg0) {
// TODO Auto-generated method stub
}
public Interceptor build() {
return new MyInterceptor();
}
}
}
epplog.sources.r1.interceptors=i1
epplog.sources.r1.interceptors.i1.type= deng.yb.flume_ng_Interceptor.MyInterceptor$Builder
- 把自定义程序打好jar包放进$FLUME_HOME/lib文件夹下
- 启动:bin/flume-ng agent -c conf -f conf/spool-interceptor-hdfs.conf -name a1 -Dflume.root.logger=DEBUG,console
- 这样flume到kafka的数据就是帅选的信息后的,避免了大量没用信息到kafka导致IO问题
kafka均衡负载
- 需要把消息均匀分布在不同brokers上,避免单台broker节点压力过大
解决方案:
- 向flume添加拦截器,会为每个event的head添加一个随机唯一的key,我们需要向header中写上随机的key,然后数据才会真正的向kafka分区进行随机发布
- flume的agent添加和修改以下配置
epplog.sources.r1.interceptors=i1 i2
epplog.sources.r1.interceptors.i1.type= deng.yb.flume_ng_Interceptor.MyInterceptor$Builder
epplog.sources.r1.interceptors.i2.type=org.apache.flume.sink.solr.morphline.UUIDInterceptor$Builder
epplog.sources.r1.interceptors.i2.headerName=key
epplog.sources.r1.interceptors.i2.preserveExisting=false
创建topic
#分区数需要根据brokers的数量决定,最好是brokers的整数倍
kafka-topics --create --zookeeper bi-slave1:2181,bi-slave2:2181,bi-master:2181
--replication-factor 1 --partitions 3 --topic epplog1
-
修改flume的sink的topic,重启flume
-
看到消息
-
可以看到,消息自动uuid和帅选后的信息
-
查看不同brokers该topic的分区
1分区2分区
3分区
-
分区名格式为 topic-分区索引,索引从0开始算
-
能看到,消息已经相对均匀分布在3个分区,也就是三台机器上面,从而达到kafka负载均衡
链接:https://www.jianshu.com/p/7b60f9f01d44
总结
kafka sink会自动从header中获取key的值,并以此值最为key发送到broker中。
kafka的分区规则是:
如果key为空,则随机发送到各个分区中。
key不为空,则根据Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions,类似于hash(keyBytes)对分区数取摸,来发送到对应的分区。
二、Kafka数据分类发送到不同topic,自定义拦截器
依据Kafka Sink的配置在消息头中携带了topic字段的话,该消息就会被发送到topic字段对应的topic去。那么在flume接收到消息之后,可以通过拦截器为topic加上header,即可将其进行分类。
Flume拦截器如下:
public class JudgeTestStringInterceptor implements Interceptor {
// 声明一个存放事件的List
private List<Event> allEvents;
public void initialize () {
// 初始化
allEvents = new ArrayList<Event>();
}
/**
* 单个事件拦截
* @param event
* @return
*/
public Event intercept (Event event) {
// 1、获取事件中的头信息
Map<String, String> headers = event.getHeaders();
// 2、获取事件中的body信息
String body = new String(event.getBody());
// 3、根据body中是否有“test”来决定添加怎样的头信息
// 有的话添加<topic, first>没有则添加<topic, second>
if (body.contains("test")) {
headers.put("topic", "first");
} else {
headers.put("topic", "second");
}
return event;
// 如果返回null则认为该事件无用,将会被过滤
}
/**
* 批量事件拦截
* @param list
* @return
*/
public List<Event> intercept (List<Event> list) {
// 1、清空集合
allEvents.clear();
// 2、遍历event
for (Event event : list) {
// 3、给每个事件添加头信息
allEvents.add(intercept(event));
}
return allEvents;
}
public void close () {
}
// 定义一个Builder对象
public static class Builder implements Interceptor.Builder {
public Interceptor build () {
return new JudgeTestStringInterceptor();
}
public void configure (Context context) {
}
}
}
配置文件type-kafka.conf如下:
# define
a1.sources = r1
a1.sinks = k1
a1.channels = c1
# source
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444
# interceptor
a1.sources.r1.interceptors = i1
a1.sources.r1.interceptors.i1.type = com.starnet.interceptor.JudgeTestStringInterceptor$Builder
# sink
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.bootstrap.servers = hadoop113:9092,hadoop114:9092,hadoop115:9092
a1.sinks.k1.kafka.topic = first
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.kafka.producer.acks = 1
a1.sinks.k1.kafka.producer.linger.ms = 1
# channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
启动flume,两个消费者以及nc之后结果如下:
[bd@hadoop113 ~]$ nc localhost 44444
test
hello
word
[bd@hadoop113 ~]$ kafka-console-consumer.sh --zookeeper hadoop113:2181 --topic first
test
[bd@hadoop113 ~]$ kafka-console-consumer.sh --zookeeper hadoop113:2181 --topic second
hello
word
方法2:增加自定义分区类
在自定义分区类的时候,如果和lib目录下的Kafka的版本和你自定义Kafka分区类时使用的API版本不一致,就会出现找不到对应类的异常。那么如何自定义对应版本的分区类呢?
最简单的方法就是找到你的安装的版本的Kafka的lib目录下的类似于kafka_2.10-0.10.2.1.jar的jar包,并将此jar包作为工程的lib引入,Maven工程直接添加对应版本的依赖即可,先以最新版本的Kafka版本(0.10.2.1)为例,对应Maven工程需要添加的依赖就是:
之后,实现你的jar包下的对应的Partition接口即可(推荐继承DefaultPartitioner,以免少实现了某些方法):
public class SimplePartitioner extends DefaultPartitioner {
public SimplePartitioner() {
}
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
String stringKey = String.valueOf(key);
String[] keys = stringKey.split("\\.");
System.out.println(Integer.valueOf(keys[3]) % 2);
return Integer.valueOf(keys[3]) % 2;
}
@Override
public void close() {
}
}
然后需要将你自定义的分区类的jar包以及所依赖的Kafka版本的jar包(不同版本的可能依赖的个数也不一样,最新版需要依赖两个jar包:kafka-clients-0.10.2.1.jar和kafka_2.10-0.10.2.1.jar),放到flume的安装目录的lib文件夹下,并将其他版本的Kafka的jar包删除。
最后需要提醒的是一定要注意版本一致性。你用什么版本的kafka就需要将对应版本的jar包放到对应目录下,否则有时会找不到。
# sink 1 配置
kafka_key.sinks.sink1.type = org.apache.flume.sink.kafka.KafkaSink
kafka_key.sinks.sink1.brokerList = bigdata-001:9092,bigdata-002:9092,bigdata-003:9092
kafka_key.sinks.sink1.topic = test_key
kafka_key.sinks.sink1.channel = channel1
kafka_key.sinks.sink1.batch-size = 100
kafka_key.sinks.sink1.requiredAcks = -1
# kafka_key.sinks.sink1.kafka.partitioner.class = com.lxw1234.flume17.SimplePartitioner
自定义分区实现kafka有序(示例代码)
1)Source中使用拦截器
kafka_key.sources.sources1.interceptors = i1
kafka_key.sources.sources1.interceptors.i1.type = com.bigdata.flume.MyInterceptor$Builderkafka_key.sources.sources1.interceptors = i1
kafka_key.sources.sources1.interceptors.i1.type = regex_extractor
kafka_key.sources.sources1.interceptors.i1.regex = .*?\|(.*?)\|.*
kafka_key.sources.sources1.interceptors.i1.serializers = s1
kafka_key.sources.sources1.interceptors.i1.serializers.s1.name = key
2)sink中使用自定义的partitioner类:
kafka_key.sinks.sink1.kafka.partitioner.class = com.lxw1234.flume17.SimplePartitioner
自定义的分区类:
public class BehaviorInterceptor implements Interceptor
{
@Override
public void initialize() {}
@Override
public Event intercept(Event event) {
//如果event为空过滤掉
if(event == null || event.getBody() == null || event.getBody().length == 0){
return null;
}long userId = 0;
//解析日志
try{
UserBehaviorRequestModel model = JSONUtil.json2Object(new String(event.getBody()),UserBehaviorRequestModel.class);
userId = model.getUserId();
}catch (Exception e){
e.printStackTrace();
}if(userId == 0){
return null;
}
//将userId赋值给key
event.getHeaders().put("key",userId+"");return event;
}@Override
public List<Event> intercept(List<Event> events) {List<Event> out = Lists.newArrayList();
for (Event event : events) {
Event outEvent = intercept(event);
//event 为空过滤掉
if (outEvent != null) { out.add(outEvent); }
}
return out;
}@Override
public void close() {}
//程序入口
public static class BehaviorBuilder implements Interceptor.Builder {@Override
public Interceptor build() {
return new BehaviorInterceptor();
}@Override
public void configure(Context context) {}
}
}