kafka持久化原理

Topic在逻辑上可以被认为是一个queue。每条消费都必须指定它的topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以水平扩展,物理上把topic分成一个或多个partition,每个partition在物理上对应一个文件夹,该文件夹下存储这个partition的所有消息和索引文件

 

  每个日志文件都是“log entries”序列,每一个log entry包含一个4字节整型数(值为N),其后跟N个字节的消息体。每条消息都有一个当前partition下唯一的64字节的offset,它指明了这条消息的起始位置。磁盘上存储的消息格式如下:

  message length : 4 bytes (value: 1+4+n)

  “magic” value : 1 byte

  crc : 4 bytes

  payload : n bytes

  这个“log entries”并非由一个文件构成,而是分成多个segment,每个segment名为该segment第一条消息的offset和“.kafka”组成。另外会有一个索引文件,它标明了每个segment下包含的log entry的offset范围,如下图所示

kafka持久化原理_第1张图片

为数据文件建索引:

  稀疏存储,每隔一定字节的数据建立一条索引(这样的目的是为了减少索引文件的大小)。

  下图为一个partition的索引示意图:

kafka持久化原理_第2张图片

注:

     1.现在对6.和8建立了索引,如果要查找7,则会先查找到8然后,再找到8后的一个索引6,然后两个索引之间做二分法,找到7的位置


通过调用kafka自带的工具,可以看到日志下的数据信息

 
   
  1. /bin/bash kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test_8-0/00000000000000000350.log --print-data-log --verify-index-only
  2. Dumping /tmp/kafka-logs/test_8-0/00000000000000000350.log
  3. Starting offset: 350
  4. offset: 350 position: 0 isvalid: true payloadsize: 1 magic: 0 compresscodec: NoCompressionCodec crc: 3860515739 keysize: 1 key: 0 payload: 0
  5. offset: 351 position: 28 isvalid: true payloadsize: 1 magic: 0 compresscodec: NoCompressionCodec crc: 1172048828 keysize: 1 key: 2 payload: 2
  6. offset: 352 position: 56 isvalid: true payloadsize: 1 magic: 0 compresscodec: NoCompressionCodec crc: 2061913492 keysize: 1 key: 4 payload: 4
  7. offset: 353 position: 84 isvalid: true payloadsize: 1 magic: 0 compresscodec: NoCompressionCodec crc: 3642789299 keysize: 1 key: 6 payload: 6
  8. offset: 354 position: 112 isvalid: true payloadsize: 1 magic: 0 compresscodec: NoCompressionCodec crc: 76726724 keysize: 1 key: 8 payload: 8
kafka日志分为index与log,两个成对出现;index文件存储元数据( 用来描述数据的数据,这也可能是为什么index文件这么大的原因了),log存储消息。索引文件元数据指向对应log文件中message的迁移地址;例如2,128指log文件的第2条数据,偏移地址为128;而物理地址(在index文件中指定)+ 偏移地址可以定位到消息。

因为每条消息都被append到该partition中,是顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。

kafka持久化原理_第3张图片

  每一条消息被发送到broker时,会根据paritition规则选择被存储到哪一个partition。如果partition规则设置的合理,所有消息可以均匀分布到不同的partition里,这样就实现了水平扩展。(如果一个topic对应一个文件,那这个文件所在的机器I/O将会成为这个topic的性能瓶颈,而partition解决了这个问题)。在创建topic时可在$KAFKA_HOME/config/server.properties中指定这个partition的数量(如下所示),当然也可以在topic创建之后去修改parition数量。

 
   
  1. # The default number of log partitions per topic. More partitions allow greater parallelism for consumption, but this
  2. # will also result in more files across the brokers.
  3. num.partitions=3

  在发送一条消息时,可以指定这条消息的key,producer根据这个keypartition机制来判断将这条消息发送到哪个parition。paritition机制可以通过指定producer的paritition. class这一参数来指定,该class必须实kafka.producer.Partitioner接口。本例中如果key可以被解析为整数则将对应的整数与partition总数取余,该消息会被发送到该数对应的partition。(每个parition都会有个序号)

 
   
  1. import kafka.producer.Partitioner;
  2. import kafka.utils.VerifiableProperties;
  3. public class JasonPartitioner<T> implements Partitioner {
  4. public JasonPartitioner(VerifiableProperties verifiableProperties) {}
  5. @Override
  6. public int partition(Object key, int numPartitions) {
  7. try {
  8. int partitionNum = Integer.parseInt((String) key);
  9. return Math.abs(Integer.parseInt((String) key) % numPartitions);
  10. } catch (Exception e) {
  11. return Math.abs(key.hashCode() % numPartitions);
  12. }
  13. }
  14. }

  如果将上例中的class作为partitioner.class,并通过如下代码发送20条消息(key分别为0,1,2,3)至topic2(包含4个partition)。

 
   
  1. public void sendMessage() throws InterruptedException{
  2.   for(int i = 1; i <= 5; i++){
  3.    List messageList = new ArrayList<KeyedMessage<String, String>>();
  4.    for(int j = 0; j < 4; j++){
  5.    messageList.add(new KeyedMessage<String, String>("topic2", j+"", "The " + i + " message for key " + j));
  6.    }
  7.    producer.send(messageList);
  8. }
  9.   producer.close();
  10. }

  则key相同的消息会被发送并存储到同一个partition里,而且key的序号正好和partition序号相同。(partition序号从0开始,本例中的key也正好从0开始)。


  对于传统的message queue而言,一般会删除已经被消费的消息,而Kafka集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要),因此Kafka提供两种策略去删除旧数据。一是基于时间,二是基于partition文件大小。例如可以通过配置$KAFKA_HOME/config/server.properties,让Kafka删除一周前的数据,也可通过配置让Kafka在partition文件超过1GB时删除旧数据,如下所示。

 
   
  1.  ############################# Log Retention Policy #############################
  2. # The following configurations control the disposal of log segments. The policy can
  3. # be set to delete segments after a period of time, or after a given size has accumulated.
  4. # A segment will be deleted whenever *either* of these criteria are met. Deletion always happens
  5. # from the end of the log.
  6. # The minimum age of a log file to be eligible for deletion
  7. log.retention.hours=168
  8. # A size-based retention policy for logs. Segments are pruned from the log as long as the remaining
  9. # segments don't drop below log.retention.bytes.
  10. #log.retention.bytes=1073741824
  11. # The maximum size of a log segment file. When this size is reached a new log segment will be created.
  12. log.segment.bytes=1073741824
  13. # The interval at which log segments are checked to see if they can be deleted according
  14. # to the retention policies
  15. log.retention.check.interval.ms=300000
  16. # By default the log cleaner is disabled and the log retention policy will default to
  17. #just delete segments after their retention expires.
  18. # If log.cleaner.enable=true is set the cleaner will be enabled and individual logs
  19. #can then be marked for log compaction.
  20. log.cleaner.enable=false

  这里要注意,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除文件与Kafka性能无关,选择怎样的删除策略只与磁盘以及具体的需求有关。另外,Kafka会为每一个consumer group保留一些metadata信息–当前消费的消息的position,也即offset。这个offset由consumer控制。正常情况下consumer会在消费完一条消息后线性增加这个offset。当然,consumer也可将offset设成一个较小的值,重新消费一些消息。因为offet由consumer控制,所以Kafka broker是无状态的,它不需要标记哪些消息被哪些consumer过,不需要通过broker去保证同一个consumer group只有一个consumer能消费某一条消息,因此也就不需要锁机制,这也为Kafka的高吞吐率提供了有力保障。      

你可能感兴趣的:(Kafka)