一、consumer消费逻辑

如有描述错误或者不准确的地方,欢迎指出

1.发送消息

  • 发送消息
    采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障kafka吞吐率)。如果有多个分区的情况,默认会根据key选择分区,即hash(key) % numPartitions。相同的key将会在相同的分区内。
  • producer写入消息流程如下:
    a. producer先从zookeeper的 "/brokers/…/state"节点找到该partition的leader
    b. producer将消息发送给该leader
    c. leader将消息写入本地log
    d. followers从leader pull消息,写入本地log后向leader发送ACK
    a. leader收到所有ISR中的replication的ACK后,增加HW(high watermark,最后commit 的offset)并向producer发送ACK

2.保存策略

消费者负责存储consumer的上次阅读位置,且不会删除原来的已读数据,不过用户可以通过设置保留时间来清理过期的数据,比如,设置保留策略为两天。那么,在消息发布之后,它可以被不同的消费者消费,在两天之后,过期的消息就会自动清理掉。

3.消费者消费消息

在kafka中的逻辑消费者是一个消费组。消费组是由多个消费者实例组成,后面说到的消费者都是单一的消费者实例。

如果一个消费组中有多个消费者实例,消费记录会负载平衡到每个消费者实例。

如果有多个消费组同时订阅了topic,那么每条消息会广播到所有消费者进程。

怎么提高kafka消费者的消费速度 kafka 消费流程_数据


在上图中kafka集群中有两个server,且每个server中有两个分区,有一个topic在中转数据。该topic同时被两个group订阅,所以该topic的每条消息都会广播给两个消费组。

在消费组A中,在消费消息过程中,4个分区被均匀非配给两个消费者实例去消费消息。即C1处理分区P0、P3的数据,C2处理P1、P2的数据。每个分区只能有一个消费者实例在处理消息,而每个消费者实例可以同时处理几个分区的消息。
在消费组B中,4个消费者实例分别负责一个分区的消息。
所以在一个消费组中分区和消费者实例之间的关系是n->1.
当消费者数量小于分区数,一个消费者去消费多个分区的数据。
当消费者数量等于分区数,消费者与分区一一对应。
当消费者数量大于分区数,将会有消费者处于空闲中。

Kafka 只保证分区内的记录是有序的,而不保证主题中不同分区的顺序。如果需要整体的数据都是有序的,那么可以只保留一个分区来处理消息,由于在一个消费组中,一个分区的消息只能被一个消费者实例处理,所以这也意味着消费组中将只有一个消费者实例。

具体的消费策略如下:
Kafka采用拉取模型,由消费者自己记录消费状态,每个消费者互相独立地顺序读取每个分区的消息。若有两个消费者(不同消费者组)拉取同一个主题的消息,消费者A的消费进度是3,消费者B的消费进度是6。消费者拉取的最大上限通过最高水位(watermark)控制,生产者最新写入的消息如果还没有达到备份数量,对消费者是不可见的。这种由消费者控制偏移量的优点是:**消费者可以按照任意的顺序消费消息。比如,消费者可以重置到旧的偏移量,重新处理之前已经消费过的消息;或者直接跳到最近的位置,从当前的时刻开始消费。

如图所示,topic1 有两个partition,消息在partition里面是依次排序的

  • 第一次 消息发到msg5,consumer1消费到msg5
    在partition1里面总offset=3,consumer1消费offset是3
    在partition2里面总offset=2,consumer1消费offset是2
    lag=partition1总offset=3+partition2总offset-(partition1consumer消费offset+consumer消费offset)=0
  • 第二次再次发送消息msg6和msg7,由于某原因,consumer1没有继续消费数据

lag=partition1总offset=3+partition2总offset-(partition1consumer1消费offset+consumer消费offset)=(4+3)-(3+2)= 2

怎么提高kafka消费者的消费速度 kafka 消费流程_zookeeper_02

二、本地单点测试

1.kafka配置安装(2.5.0)

brew install kafka    
brew install zookeeper

修改配置信息

cd /usr/local/etc/kafka/
vim server.properties

找到listeners=PLAINTEXT://:9092 那一行,把注释取消掉
2.启动kafka

brew services start zookeeper
brew services start kafka

备注: 停止命令 brew services stop zookeeper brew services stop kafka

kafka自带的启动命令,进入目录先启动zookeeper,再启动kafka。

cd /usr/local/Cellar/kafka/2.6.0/libexec
bin/zookeeper-server-start.sh config/zookeeper.properties
bin/kafka-server-start.sh config/server.properties

3.查看broker信息
这里通过zookeeper查看broker信息

➜ /usr/local/Cellar/zookeeper/3.6.2/bin
➜ ll
total 48
-r-xr-xr-x  1 user  staff   129B 10  9 20:44 zkCleanup
-r-xr-xr-x  1 user  staff   125B 10  9 20:44 zkCli
-r-xr-xr-x  1 user  staff   128B 10  9 20:44 zkServer
-r-xr-xr-x  1 user  staff   139B 10  9 20:44 zkServer-initialize
-r-xr-xr-x  1 user  staff   137B 10  9 20:44 zkSnapShotToolkit
-r-xr-xr-x  1 user  staff   135B 10  9 20:44 zkTxnLogToolkit
➜ zkCli
/usr/bin/java
Connecting to localhost:2181
Welcome to ZooKeeper!
JLine support is enabled

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
[zk: localhost:2181(CONNECTED) 0] ls /brokers/ids
[2]
[zk: localhost:2181(CONNECTED) 1] get /brokers/ids[1]
org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /brokers/ids[1]
[zk: localhost:2181(CONNECTED) 2] get /brokers/ids[2]
org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /brokers/ids[2]

4.发收消息

  • 生产者发消息
    执行该目录下下脚本/usr/local/Cellar/kafka/2.5.0/bin(不必非在该目录下执行,brew install的时候自动配置了环境变量)
kafka-console-producer --broker-list localhost:9092 --topic test1
  • 消费者消费消息
kafka-console-consumer --bootstrap-server localhost:9092 --topic test1 --from-beginning

指定group

kafka-console-consumer --bootstrap-server localhost:9092 --topic test1 -group aaaa  --from-beginning

不指定group(kafka会自己生成group id)

➜  libexec bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
aaaa
console-consumer-34543

5.查看group信息
所有group列表

cd /usr/local/Cellar/kafka/2.5.0/libexec
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list

查看某个group信息

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group aaaa

GROUP           TOPIC           PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             CONSUMER-ID                                          HOST            CLIENT-ID
aaaa            test1           0          37              37              0               consumer-aaaa-1-77fbf9e2-e9af-42aa-af44-9fef6f04e2c7 /127.0.0.1      consumer-aaaa-1
  1. 删除group
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --delete --group aaaa

命令参考

三、scala代码实现

1.maven依赖

<dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>1.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>1.1.0</version>
        </dependency>

1.生产者创建数据

package kafka
import java.util.Properties
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord, RecordMetadata}

/**
  * 实现producer
  */
object Producer {
  def main(args: Array[String]): Unit = {
    val prop = new Properties
    // 指定请求的kafka集群列表
    prop.put("bootstrap.servers", "localhost:9092")// 指定响应方式
    //prop.put("acks", "0")
    prop.put("acks", "all")
    // 请求失败重试次数
    //prop.put("retries", "3")
    // 指定key的序列化方式, key是用于存放数据对应的offset
    prop.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
    // 指定value的序列化方式
    prop.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer")
    // 配置超时时间
    prop.put("request.timeout.ms", "60000")
    //prop.put("batch.size", "16384")
    //prop.put("linger.ms", "1")
    //prop.put("buffer.memory", "33554432")

    // 得到生产者的实例
    val producer = new KafkaProducer[String, String](prop)

    // 模拟一些数据并发送给kafka
    for (i <- 1 to 100) {
      val msg = s"${i}: this is a linys ${i} kafka data"
      println("send -->" + msg)
      // 得到返回值
      val rmd: RecordMetadata = producer.send(new ProducerRecord[String, String]("linys", msg)).get()
      println(rmd.toString)
      Thread.sleep(500)
    }

    producer.close()
  }
}

2.消费者消费数据

package kafka


import java.util.{Collections, Properties}
import org.apache.kafka.clients.consumer.{ConsumerRecords, KafkaConsumer}
/**
  * 实现consumer
  */
object Consumer {
  def main(args: Array[String]): Unit = {
    // 配置信息
    val prop = new Properties
    prop.put("bootstrap.servers", "localhost:9092")
    // 指定消费者组
    prop.put("group.id", "group01")
    // 指定消费位置: earliest/latest/none
    prop.put("auto.offset.reset", "earliest")
    // 指定消费的key的反序列化方式
    prop.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    // 指定消费的value的反序列化方式
    prop.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    prop.put("enable.auto.commit", "true")
    prop.put("session.timeout.ms", "30000")
    // 得到Consumer实例
    val kafkaConsumer = new KafkaConsumer[String, String](prop)
    // 首先需要订阅topic
    kafkaConsumer.subscribe(Collections.singletonList("linys"))
    // 开始消费数据
    while (true) {
      // 如果Kafak中没有消息,会隔timeout这个值读一次。比如上面代码设置了2秒,也是就2秒后会查一次。
      // 如果Kafka中还有消息没有消费的话,会马上去读,而不需要等待。
      val msgs: ConsumerRecords[String, String] = kafkaConsumer.poll(2000)
      // println(msgs.count())
      val it = msgs.iterator()
      while (it.hasNext) {
        val msg = it.next()
        println(s"partition: ${msg.partition()}, offset: ${msg.offset()}, key: ${msg.key()}, value: ${msg.value()}")
      }
    }
  }
}

3.消费状态lag获取

def getKafkaConsumerLag(consumer: KafkaConsumer[Bytes, Bytes]): Long = {

    val topicPartitions = consumer.partitionsFor(topic).asScala.map(p => new TopicPartition(p.topic, p.partition))
    val consumerOffset = topicPartitions.map(consumer.committed(_).offset).sum
    val totalOffset = consumer.endOffsets(topicPartitions.asJava).asScala.values.map(_.toLong).sum

    totalOffset - consumerOffset
  }