一、环境说明

组件

版本

Kafka

Kafka-0.10.2.0

Spark

spark-2.2

IDEA

idea64-2017

Zookeeper

zookeeper-3.4.5

 

二、Kafka自动管理偏移量

       1.管理kafka的偏移量,有两个重要的参数:auto.offset.reset 和 enable.auto.commit 

参数

可选值

解释

enable.auto.commit

true

控制kafka是否自动提交偏移量。

false

auto.offset.reset

latest

当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据。

earliest

当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费。

none

当各分区下都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常。

 

        2.Kafka自动提交的偏移量保存位置

在kafka-0.10.1.x版本之前,offset保存在zookeeper的/consumers/<group.id>/offsets/<topic>/<partitionId>中。随着Kafka              consumer在实际场景的不断应用,发现旧版本consumer把偏移量提交到ZooKeeper的做法不合适。ZooKeeper本质上只是           一个协调服务组件,它并不适合作为偏移量信息的存储组件,毕竟频繁高并发的读写操作并不是ZooKeeper擅长的事情。            所以在kafka-0.10.1.x版本之后,偏移量信息会保存在kafka的__consumer_offsets主题下,该主题有50个分区,每个分区有          3个副本。

        3.自动获取偏移量的java 代码

KafkaUtils.createDirectStream(
        ssc,
        LocationStrategies.PreferConsistent(),  
        ConsumerStrategies.<String,String>Subscribe(topicsSet,kafkaParams)
);

        

三、kafka手动管理偏移量

所谓的手动管理偏移量就是用户自行确定消息何时被真正处理完并可以提交偏移量,用户可以确保只有消息被真正处理完成后再提交偏移量。所以需要我们在代码逻辑中得到实时的偏移量,并且保证“任务处理完成之后再提交偏移量”这种时序性。

    手动管理kafka偏移量有以下优点:

    a)一般情况下,保证数据不丢失,不重复被消费

    b)可以方便地查看offset信息

    c)无需修改groupId即可从指定位置读取消息

    

    1.得到RDD对应的kafka偏移量

HasOffsetRanges hasOffsetRanges = (HasOffsetRanges) (rdd.rdd());
StringBuilder sb = new StringBuilder();
for (OffsetRange of : hasOffsetRanges.offsetRanges()) {
  sb.append(of.topic()).append("-").append(of.partition()).append("=").append(of.untilOffset()).append("\n");
}
//do something (保存偏移量)

   通过代码可以看到。OffsetRange这个类有untilOffset()方法,这个方法就是得到该批次数据,一个分区对应的终偏移量。另外       还有fromOffset()方法,对应起始偏移量。

   2.将偏移量保存到本地

   如果程序以yarn-client的模式运行,即每次运行driver的节点都是可以确定的,那么可以选择将偏移量信息保存在本地目录下。  保存在driver节点的硬盘之后,我们可以很方便的查看,但是需要注意的是,数据只有一份,并不能保证高可用性。创建目录代码略。

保存数据到本地目录的主要代码如下:
File f = new File(checkpoint_path, file_name);
FileWriter fw = new FileWriter(f);
fw.write(checkpoint_data);
fw.close();
从本地目录读取数据的主要代码如下:
     
FileInputStream fin = new FileInputStream(new File(checkpoint_path, file_name));
Properties properties = new Properties();
properties.load(fin);
for (Object o : properties.keySet()) {
        //do something
}

  读取数据之后,需要我们返回Map<TopicPartition, Long>,以供KafkaUtils创建DStream。在下文会有相关的说明。

3.将偏移量保存在zookeeper

如果程序以yarn-cluster的模式运行,每次运行时的driver节点不可以确定,所以需要一个yarn节点都可以读到的目录来保存偏移量信息。符合条件而且高可用的存储方式有很多,比如:zookeeper、hdfs、elasticsearch等等。这里选择zookeeper来实现保存偏移量的方法。

创建节点目录代码如下:
String []nodes = checkPointPath.split("/");
StringBuffer sb = new StringBuffer();
for(String node : nodes){
        if(!node.equals("")){
           sb.append("/").append(node);
           Stat stat= zk.exists(sb.toString(),false);
           if(stat == null){
              zk.create(sb.toString(),"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
           }
        }
}

保存数据到zookeeper的主要代码如下:
Stat stat = zk.exists(checkPointPath+"/"+node_name , false);
if(stat == null){
        zk.create(checkPointPath+"/"+node_name , "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
zk.setData(checkPointPath + "/" + node_name, data.getBytes(), -1);

从zookeeper读取数据的主要代码如下:
Map<TopicAndPartition, Long> topicMap = new HashMap<>();
byte[] bytes = zk.getData(checkPointPath + "/" + datasourceid,false,null);
String data = new String(bytes);
String []lines = data.split("\n");
for(String line:lines){
       //do something
}
return topicMap;

4.得到指定topics的最早偏移量或者最新偏移量

得到topic的元数据的关键代码:
consumer = new SimpleConsumer(host, port, 100000, 64 * 1024, clientId);
List<String> topics = Collections.singletonList(topic);
TopicMetadataRequest req = new TopicMetadataRequest(topics);
TopicMetadataResponse resp = consumer.send(req);
TopicMetadata tm = resp.topicsMetadata().get(0);

得到topic中各分区的最新偏移量关键代码:

private static Map<TopicAndPartition, Long> getLatestOffSets(TopicMetadata tm, String topic, String clientId) {
     Map<TopicAndPartition, Long> topicMap = new HashMap<>();
     Multimap<String, TopicAndPartition> requests = ArrayListMultimap.create();
     for (PartitionMetadata pm : tm.partitionsMetadata()) {
         TopicAndPartition topicAndPartition = new TopicAndPartition(topic, pm.partitionId());
         requests.put(pm.leader().host() + ":" + pm.leader().port(), topicAndPartition);
     }
     for (String key : requests.keySet()) {
        Collection<TopicAndPartition> tps = requests.get(key);
        Map<TopicAndPartition, PartitionOffsetRequestInfo> requestInfo = new HashMap<>();
        for (TopicAndPartition tap : tps) {
            requestInfo.put(tap, new PartitionOffsetRequestInfo(kafka.api.OffsetRequest.LatestTime(), 1));
        }
            String keys[] = key.split(":");
            SimpleConsumer consumer = new SimpleConsumer(keys[0], Integer.parseInt(keys[1]), 100000, 64 * 1024, clientId);
            OffsetRequest rs = new OffsetRequest(requestInfo, kafka.api.OffsetRequest.CurrentVersion(), clientId);
            OffsetResponse rp = consumer.getOffsetsBefore(rs);
            for (TopicAndPartition tap : tps) {
               topicMap.put(tap, rp.offsets(topic, tap.partition())[0]);
            }
        }
        return topicMap;
}
得到topic中各分区的最早偏移量关键代码,与得到最新偏移量的代码非常相似,只是在初始化PartitionOffsetRequestInfo对象时,参数不一样。代码如下:
private static Map<TopicAndPartition, Long> getEalistOffSets(TopicMetadata tm, String topic, String clientId) {
       .
       .
       .
       requestInfo.put(tap, new PartitionOffsetRequestInfo(kafka.api.OffsetRequest.EarliestTime(), 1));
       .  
       .
       .
       return topicMap;
}
最后,再将Map<TopicAndPartition, Long>转换为Map<TopicPartition, Long>即可。
Map<TopicPartition, Long> newVersionOffset = new HashMap<>();
for (TopicAndPartition topicAndPartition : oldVersionOffset.keySet()) {
     newVersionOffset.put(new TopicPartition(topicAndPartition.topic(),
                                topicAndPartition.partition()), oldVersionOffset.get(topicAndPartition));
}

四、根据手动得到的偏移量,创建DirectStream,返回值是JavaDStream<String>。

KafkaUtils.createDirectStream(
    ssc,
    String.class,
    String.class,
    StringDecoder.class,
    StringDecoder.class,
    String.class,
    kafkaParams,
    offsets,
    new Function<MessageAndMetadata<String, String>, String>() {
        @Override
        public String call(MessageAndMetadata<String, String> mmd) throws Exception {
            return mmd.message();
        }
    }
)