Preface
在实现Lambda架构的时候,我以Kafka作为系统的输入,同时需要将数据批量从Kafka导入到HDFS存储起来,以备Batch layer批处理计算。
而从Kafka到HDFS的数据传输,Linkedin已经有一个开源的项目,即Linkedin Camus。Camus是Kafka到HDFS的管道,它实际上是向Hadoop提交一个作业,并从Kafka获取指定topic的消息,存储到HDFS中。
实际上在使用Camus的时候,只有3件事是我们关注的。
确定MessageDecoder
确定RecordWriterProvider
确定Kafka话题
MessageDecoder
MessageDecoder 是对Kafka的消息进行解析的解析器,比如Camus自带了几个Decoder:KafkaAvroMessageDecoder,JsonStringMessageDecoder等。
我们也可以自行设计符合自己业务需求的Decoder,而消息解析后的内容,会作为输出后期被存储到HDFS(当然还会加上压缩)
我在系统中实现了一个简单的字符串解析器,基本保持消息原文,也就是我只是要直接存储消息原文即可:
package com.linkedin.camus.etl.kafka.coders;
import com.linkedin.camus.coders.CamusWrapper;
import com.linkedin.camus.coders.MessageDecoder;
import org.apache.log4j.Logger;
import java.util.Properties;
/**
* MessageDecoder class that will convert the payload into a String object,
* System.currentTimeMillis() will be used to set CamusWrapper's
* timestamp property
* This MessageDecoder returns a CamusWrapper that works with Strings payloads,
*/
public class StringMessageDecoder extends MessageDecoder<byte[], String> {
private static final Logger log = Logger.getLogger(StringMessageDecoder.class);
@Override
public void init(Properties props, String topicName) {
this.props = props;
this.topicName = topicName;
}
@Override
public CamusWrapper<String> decode(byte[] payload) {
long timestamp = 0;
String payloadString;
payloadString = new String(payload);
timestamp = System.currentTimeMillis();
return new CamusWrapper<String>(payloadString, timestamp);
}
}
简单说一下,该类继承于com.linkedin.camus.coders.MessageDecoder,输入的类型为byte[],解析后的类型为String。重点是实现decode方法,返回一个CamusWrapper对象,该对象包含了解析后的数据。
RecordWriterProvider
相比MessageDecoder在Camus的输入端发挥作用,RecordWriterProvider则在输出端发挥作用,确定Camus如何把数据写到HDFS中。
Camus同样有实现好的,比如AvroRecordWriterProvider,这也是Camus默认的,因为Camus默认是支持Avro格式,而我只需要纯文本即可,所以我使用的是同样Camus自带的com.linkedin.camus.etl.kafka.common.StringRecordWriterProvider,代码在Camus源码中可以找到。
既然是RecordWriter的Provider,其核心功能当然是提供一个RecordWriter,这是Hadoop的一个类,规定了Job如何输出到文件系统的,只要实现其中的write方法即可。
另外,关于输出还有一个压缩的问题,比如在StringRecordWriterProvider中根据配置文件确定是否压缩以及压缩算法,并确定输出时的行为,部分代码如下:
isCompressed = FileOutputFormat.getCompressOutput(context);
if (isCompressed) {
Class<? extends CompressionCodec> codecClass = null;
if ("snappy".equals(EtlMultiOutputFormat.getEtlOutputCodec(context))) {
codecClass = SnappyCodec.class;
} else if ("gzip".equals((EtlMultiOutputFormat.getEtlOutputCodec(context)))) {
codecClass = GzipCodec.class;
} else {
codecClass = DefaultCodec.class;
}
codec = ReflectionUtils.newInstance(codecClass, conf);
extension = codec.getDefaultExtension();
}
以及:
FileSystem fs = path.getFileSystem(context.getConfiguration());
if (!isCompressed) {
FSDataOutputStream fileOut = fs.create(path, false);
return new ByteRecordWriter(fileOut, recordDelimiter);
} else {
FSDataOutputStream fileOut = fs.create(path, false);
return new ByteRecordWriter(new DataOutputStream(codec.createOutputStream(fileOut)), recordDelimiter);
}
配置
以上说到的内容,都是通过配置文件在构建的时候整合到camus中。
camus.message.decoder.class 用来配置MessageDecoder
etl.record.writer.provider.class 用来配置RecordWriterProvider
关于压缩,可以参照下面配置:
mapred.output.compress=true #告诉Hadoop需不需要压缩
etl.output.codec=deflate #配置压缩算法为deflate
etl.deflate.level=6
另外,配置一些HDFS路径
etl.destination.path 是文件最终输出的文件夹
etl.execution.base.path 和 etl.execution.history.path分别是Camus运行信息临时存放和已完成的Job的运行信息存放的地方
kafka.whitelist.topics和kafka.blacklist.topics分别是Kafka话题的白名单和黑名单,将要订阅的topics以逗号分隔的形式赋值给whitelist即可。