Kafka的消息存储

为了便于说明问题,假设这里只有一个单节点的伪分布式Kafka集群。在这个Kafka broker实例的$KAFKA_HOME/config/server.properties中配置 log.dirs=/tmp/kafka-logs,以此来设置 Kafka 消息文件存储目录。并通过命令:

$KAFKA_HOME/bin/kafka-topics.sh --create --zookeeper localhost:2181 --partitions 4 --topic topic_test --replication-factor 1

创建一个 topic:topic_test,partition的数量配置为4。接下来可以在 /tmp/kafka-logs 目录中可以看到生成了 4 个partition目录:

drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_test-0
drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_test-1
drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_test-2
drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_test-3

在Kafka文件存储中,同一个topic下有多个不同的partition,每个partiton为一个目录,partition的名称规则为topic名称+有序序号,第一个序号从0开始计,最大的序号为partition数量减1,类似数组的索引, partition是实际物理上的概念,而topic是逻辑上的概念,更多表象是一个消息的类别,当然,partition还可以细分为segment(文件),一个partition物理上由多个segment组成.

为什么不能以partition 作为存储单位?

如果就以partition 为最小存储单位,可以想象,当Kafka producer不断发送消息,必然会引起partition文件的无限扩张,将对消息文件的维护以及已消费的消息的清理带来严重的影响,新数据是在文件尾部追加的,不论文件数据文件有多大,这个操作永远都是 O(1)的查找,再者,查找某个offset的Message是顺序查找的。因此,如果数据文件很大的话,查找的效率就低. 因此,需以segment 为单位将partition进一步细分。每个partition相当于一个巨型文件被平均分配到多个大小相等的segment数据文件中(每个segment文件中消息数量不一定相等)这种特性也方便old segment的删除,即方便已被消费的消息的清理,提高磁盘的利用率。每个partition只需要支持顺序读写就行.segment的文件生命周期由服务端配置参数log.segment.byteslog.roll.{ms,hours} 等相关参数决定。

segment文件由两部分组成,分别为 .index文件和.log文件,分别表示为segment索引文件和数据文件。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值,数值大小为64位,20位数字字符长度,没有数字用0填充,如下:

00000000000000000000.index
00000000000000000000.log
00000000000000000000.timeindex
00000000000000170410.index
00000000000000170410.log
00000000000000170410.timeindex
00000000000000239430.index
00000000000000239430.log
00000000000000239430.timeindex

以上面的segment文件为例,展示出segment中的00000000000000170410.index文件和00000000000000170410.log文件是存在对应关系的,.index索引文件存储大量的元数据,.log数据文件存储大量的消息,索引文件中的元数据指向对应数据文件中message的物理偏移地址(也就是实际的偏移地址,因为会涉及到segment文件清理)。其中以.index索引文件中的元数据[3, 348]为例,在.log数据文件表示第3个消息,该消息的物理偏移地址为348。

如何保证消息消费的有序性呢?

举个例子,比如说生产者生产了25个订单,订单假设分为创建-提交-付款-发货4个步骤,那么消费者在消费的时候按照0到100这个从小到大的顺序消费,那么kafka如何保证这种有序性呢?

难度就在于,生产者生产出0到100这100条数据之后,通过一定的分组策略存储到broker的partition中的时候,比如0到10这10条消息被存到了这个partition中,10到20这10条消息被存到了那个partition中,这样的话,消息在分组存到partition中的时候就已经被分组策略搞得无序了。

那么能否做到消费者在消费消息的时候全局有序呢?遇到这个问题,我们可以回答,在大多数情况下是做不到全局有序的。但在某些情况下是可以做到的。比如我的partition只有一个,这种情况下是可以全局有序的。

那么可能有人又有疑问了,只有一个partition的话,哪里来的分布式呢?哪里来的负载均衡呢?所以说,全局有序是一个伪命题!让订单全局有序根本没有办法在kafka内部做到。但是我们只能保证当前这个partition内部消息消费的有序性。

针对一个topic里面的数据,只能做到partition内部有序,不能做到全局有序。

当然不使用一个partition我们可以从代码层面解决.

如何从partition中通过offset查找message?

以上图为例,读取offset=170418的消息,首先查找segment文件,其中00000000000000000000.index为最开始的文件,第二个文件为00000000000000170410.index(起始偏移为170410),而第三个文件为00000000000000239430.index(起始偏移为239430),所以这个offset=170418就落到了第二个文件之中。其他后续文件可以依次类推,以其实偏移量命名并排列这些文件,然后根据二分查找法就可以快速定位到具体文件位置。其次根据00000000000000170410.index文件中的[8,1325]定位到00000000000000170410.log文件中的1325的位置进行读取。

怎么知道何时读完本条消息,否则就读到下一条消息的内容了?
这个问题就得引出kafka的消息结构,如下图所示:

file

消息都具有固定的物理结构,包括:offset(8 Bytes)、消息体的大小(4 Bytes)、crc32(4 Bytes)、magic(1 Byte)、attributes(1 Byte)、key length(4 Bytes)、key(K Bytes)、payload(N Bytes)等等字段,可以确定一条消息的大小,即读取到哪里截止。

总结,offset的查找方机制是建立在offset是有序的,索引文件被映射到内存中,所以查找的速度还是很快的。

另外,Kafka的Message存储采用了分区(partition),Segment和index这几个手段来达到了高效性。

Views: 602

Kafka如何保证无消息丢失

配置Kafka无消息丢失

Kafka 只对“已提交”的消息(committed message)做有限度的持久化保证。Kafka 的 一个Broker或多个Broker 成功地接收到一条消息并写入到日志文件后,它们会告诉生产者程序这条消息已成功提交,具体是一个Broker还是多个Broker取决ack参数的配置。

要想要消息不丢失,假如你的消息保存在 N 个 Kafka Broker 上,那么这个前提条件就是这 N 个 Broker 中至少有 1 个存活。

目前 Kafka Producer 的send方法是异步发送消息的,也就是说如果你调用的是 producer.send(msg) 这个 API,那么它通常会立即返回,消息会异步继续发送到Broker中,在这个过程中是有可能失败的,比如网络抖动导致发送失败,比如消息格式不正确等导致Broker拒绝消息。

解决方法:Producer 要使用带有回调通知的发送 API,也就是说不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。

通过callback(回调),它能准确地告诉你消息是否真的提交成功了。一旦出现消息提交失败的情况,你就可以有针对性地进行处理。举例来说,如果是因为那些瞬时错误,那么仅仅让 Producer 重试就可以了;如果是消息不合格造成的,那么可以调整消息格式后再次发送。总之,处理发送失败的责任在 Producer 端而非 Broker 端。

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class ProducerWithCallBackExample {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        final Logger logger = LoggerFactory.getLogger(ProducerWithCallBackExample.class);

        //Create producer properties
        Properties properties = new Properties();
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop000:9092");
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //Create the Producer
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);

        //Send 10 messages
        for (int i = 0; i < 10; i++) {
            //Create a Producer Record
            ProducerRecord<String, String> record = new ProducerRecord<String, String>("my-topic", "id_" + i, "Hello Kakfka" + i);

            logger.info("Key :" + "id_" + i); //Log the Key
            //send data
            producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null) {
                        logger.info("Received new metadata. \n" +
                                "Topic:" + recordMetadata.topic() + "\n" +
                                "Pratition:" + recordMetadata.partition() + "\n" +
                                "Offeset:" + recordMetadata.offset() + "\n" +
                                "Timestamp" + recordMetadata.timestamp());
                    } else {
                        logger.error("Error while producing", e);
                    }
                }
            });
           producer.flush();
        }
        producer.close();
    }
}

最佳实践

分享一下 Kafka 无消息丢失的配置的最佳实践:

  1. 不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。记住,一定要使用带有回调通知的 send 方法。
  2. 设置 acks = all。acks 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。
  3. 设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。
  4. 设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。
  5. 设置 replication.factor >= 3。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。
  6. 设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。
  7. 确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1
  8. 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关重要的。

Views: 303

Storm DRCP应用(计算推特Reach值)

需求

针对twitter网站上的一篇推文的接触用户(也叫REACH值)进行统计。

Reach值让你了解推文的真实覆盖到的用户群体, 要计算一个推文URL的Reach值,需要以下4步:

  1. 根据推文的URL查询数据库获取全部直接接触用户(转发的用户)
  2. 再根据接触用户通过查询数据库获取每个用户的全部粉丝
  3. 对粉丝集合中的用户进行去重处理
  4. 最后统计去重后的用户数, 即这个推文的Reach值

拓扑定义

一个单独的Reach计算在计算期间可能涉及到数千次数据库访问和数千万的粉丝记录查询,可能是一个非常耗时的计算。在storm上实现这个功能非常简单。在一台机器上,Reach计算可能花费数分钟,而在storm集群,最难计算Reach的URL也只需数秒。

Storm-starter 项目中有一个计算Reach样例,Reach拓扑定义如下所示:

LinearDRPCTopologyBuilder builder = new LinearDRPCTopologyBuilder("reach"); 

builder.addBolt(new GetTweeters(), 3); 
builder.addBolt(new GetFollowers(), 2) .shuffleGrouping(); 
builder.addBolt(new PartialUniquer(), 3) .fieldsGrouping(new Fields("id", "follower")); 
builder.addBolt(new CountAggregator()) .fieldsGrouping(new Fields("id")); 

过程分析

以计算https://tech.backtype.com/blog/123的Reach值为例进行说明:

image-20211024202541029

这个拓扑以4个步骤的形式执行, 因此一共有4个Bolt:

  1. GetTweeters 从数据库获取给定URL对应的用户并发射出去
  2. GetFollowers 从数据库获取每个用户对应的粉丝并发射出去
  3. PartialUniquer按粉丝进行字段分组并利用Set集合进行去重处理, 将去重后的粉丝数量进行局部累加, 并把结果发送出去
  4. 最后,CountAggregator从每个PartialUniquer任务接收计数并对累加求和作为返回值给DRPC客户端。

项目代码

为了方便项目中数据库使用Map集合进行伪造, 代码如下:

package storm.example.drpc;

/**
 * 计算推文的REACH值
 * storm.example.drpc.AdvanceDRPCTopology
 */

import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.LocalDRPC;
import org.apache.storm.StormSubmitter;
import org.apache.storm.coordination.BatchOutputCollector;
import org.apache.storm.drpc.LinearDRPCTopologyBuilder;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.BasicOutputCollector;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseBasicBolt;
import org.apache.storm.topology.base.BaseBatchBolt;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;
import org.apache.storm.utils.Utils;

import java.util.*;

// 计算一篇推文的REACH值
public class AdvanceDRPCTopology {

    // MAP: URL -> USER
    public static Map<String, List<String>> TWEETERS_DB = new HashMap<String, List<String>>() {{
        put("foo.com/blog/1", Arrays.asList("sally", "bob", "tim", "george", "nathan"));
        put("engineering.twitter.com/blog/5", Arrays.asList("adam", "david", "sally", "nathan"));
        put("tech.backtype.com/blog/123", Arrays.asList("tim", "mike", "john"));
    }};

    // MAP: USER -> FOLLOWERS
    public static Map<String, List<String>> FOLLOWERS_DB = new HashMap<String, List<String>>() {{
        put("sally", Arrays.asList("bob", "tim", "alice", "adam", "jim", "chris", "jai"));
        put("bob", Arrays.asList("sally", "nathan", "jim", "mary", "david", "vivian"));
        put("tim", Arrays.asList("alex"));
        put("nathan", Arrays.asList("sally", "bob", "adam", "harry", "chris", "vivian", "emily", "jordan"));
        put("adam", Arrays.asList("david", "carissa"));
        put("mike", Arrays.asList("john", "bob"));
        put("john", Arrays.asList("alice", "nathan", "jim", "mike", "bob"));
    }};

    public static LinearDRPCTopologyBuilder construct() {

        LinearDRPCTopologyBuilder builder = new LinearDRPCTopologyBuilder("reach");

        builder.addBolt(new GetTweeters(), 3);
        builder.addBolt(new GetFollowers(), 2).shuffleGrouping();
        builder.addBolt(new PartialUniquer(), 3).fieldsGrouping(new Fields("id", "follower"));
        builder.addBolt(new CountAggregator(), 1).fieldsGrouping(new Fields("id"));

        return builder;
    }

    public static void main(String[] args) throws Exception {

        LinearDRPCTopologyBuilder builder = construct();
        Config conf = new Config();

        if (args == null || args.length == 0) {
            // conf.setMaxTaskParallelism(3);
            LocalDRPC drpc = new LocalDRPC();
            LocalCluster cluster = new LocalCluster();
            cluster.submitTopology("reach-drpc", conf, builder.createLocalTopology(drpc));

            String[] urlsToTry = new String[]{"foo.com/blog/1", "engineering.twitter.com/blog/5", "notaurl.com", "tech.backtype.com/blog/123"};

            for (String url : urlsToTry) {
                System.err.println("Thread[" + Thread.currentThread().getName() + "] " +"Reach of " + url + ": " + drpc.execute("reach", url));
                Utils.sleep(10000);
            }
            cluster.shutdown();
            drpc.shutdown();
        } else {
            conf.setNumWorkers(1); //6
            StormSubmitter.submitTopology(args[0], conf, builder.createRemoteTopology());
        }
    }

    // BOLT 1: 根据URL发送推特用户到数据流中
    public static class GetTweeters extends BaseBasicBolt {

        @Override
        public void execute(Tuple tuple, BasicOutputCollector collector) {

            Object id = tuple.getValue(0);
            String url = tuple.getString(1);
            List<String> tweeters = TWEETERS_DB.get(url);

            if (tweeters != null) {
                for (String tweeter : tweeters) {
                    collector.emit(new Values(id, tweeter));
                }
            }
        }

        @Override
        public void declareOutputFields(OutputFieldsDeclarer declarer) {

            declarer.declare(new Fields("id", "tweeter"));
        }

    }

    // BOLT 2: 根据推特用户发送对应的粉丝到数据流中
    public static class GetFollowers extends BaseBasicBolt {

        @Override
        public void execute(Tuple tuple, BasicOutputCollector collector) {

            Object id = tuple.getValue(0);
            String tweeter = tuple.getString(1);
            List<String> followers = FOLLOWERS_DB.get(tweeter);

            if (followers != null) {
                for (String follower : followers) {
                    System.err.println("Thread[" + Thread.currentThread().getName() + "] " + "request-id[" + id + "]: " + tweeter + "'s follower: " + follower);
                    collector.emit(new Values(id, follower));
                }
            }
        }

        @Override
        public void declareOutputFields(OutputFieldsDeclarer declarer) {

            declarer.declare(new Fields("id", "follower"));
        }
    }

    // BOLT 3: 对粉丝去重后的计数发送的数据流中
    public static class PartialUniquer extends BaseBatchBolt {

        BatchOutputCollector collector;
        Object id;
        Set<String> followers = new HashSet<String>();

        @Override
        public void prepare(Map conf, TopologyContext context, BatchOutputCollector collector, Object id) {
            this.collector = collector;
            this.id = id;
        }

        @Override
        public void execute(Tuple tuple) {
            followers.add(tuple.getString(1));
        }

        @Override
        public void finishBatch() {
            System.err.println("Thread[" + Thread.currentThread().getName() + "] " +"request-id[" + id + "]: " + "followers.size(): " + followers.size() + followers);
            collector.emit(new Values(id, followers.size()));
        }

        @Override
        public void declareOutputFields(OutputFieldsDeclarer declarer) {
            declarer.declare(new Fields("id", "partial-count"));
        }
    }

    // BOLT 4:将所有计数累加的结果发送的数据流中
    public static class CountAggregator extends BaseBatchBolt {

        BatchOutputCollector collector;

        Object id;
        int reach = 0;

        @Override
        public void prepare(Map conf, TopologyContext context, BatchOutputCollector collector, Object id) {
            this.collector = collector;
            this.id = id;
        }

        @Override
        public void execute(Tuple tuple) {
            int partiaLReach = tuple.getInteger(1);
            System.err.println("Thread[" + Thread.currentThread().getName() + "] " +"request-id[" + id + "]: " + "Partial reach is " + partiaLReach);
            reach += tuple.getInteger(1);
        }

        @Override
        public void finishBatch() {
            System.err.println("Thread[" + Thread.currentThread().getName() + "] " +"request-id[" + id + "]: " + "Global reach is " + reach);
            collector.emit(new Values(id, reach));
        }

        @Override
        public void declareOutputFields(OutputFieldsDeclarer declarer) {
            declarer.declare(new Fields("id", "reach"));
        }
    }
}

Reach计算的每一单步都是可以并行执行的,而且定义一个DRPC拓扑也非常简单。

需要注意的是, CountAggregator 只有在 PartialUniquer 并行度大于1个时候才有意义.

另外CountAggregatorPartialUniquer 这两个Bolt都是继承的BaseBatchBolt,这种Bolt发射数据流的时机是在相同id的数据都处理完成后, 才会触发(类似事务).

谢谢!

Views: 356