kafka系列1----简单使用

1、简单使用

1、1 安装zookeepeper并启动

启动kafka自带的zookeeper(建议独立安装zookeeper并配置启动。)

zookeeper-server-start ../../config/zookeeper.properties

zookeeper.properties在kafka主目录下的config目录中

    也可以直接使用自己下载并独立安装的zookeeper

1.2 启动broker

kafka-server-start D:\development\kafka\config\server.properties

​​​​​​​1.3 创建topic

kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

​​​​​​​1.4 启动kafka自带的consumer

kafka-console-consumer --bootstrap-server localhost:9092 --topic test

​​​​​​​1.5 启动kafka自带的producer

kafka-console-producer --broker-list localhost:9092 --topic test

2、数据结构

Kafka使用 Zookeeper 保存集群的元数据信息和消费者信息。从kafka-0.9版本及以后,kafka的消费者组和offset信息就不存zookeeper了,而是存到broker服务器上。数据以日志(就是数据文件,kafka中一个分区就是一个提交日志)形式存放在broker的log .dirs配置的目录

2.1 topic与分区

kafka通过topic来组织数据。topic就好比数据库的表,或者文件系统里的文件夹。主题可以被分为若干个分区,一个分区就是一个提交日志,存储对应topic的一部分数据。一个主题的所有数据就是其所有分区的数据之和。

​​​​​​​2.2 偏移量

每个分区的数据都是有序且顺序不可变的记录集,如下图所示:

上面的序号就是偏移量,类似数据库中数据的ID,自增且有序。每个分区都有自己的偏移量,互不影响

2.3 Leader/Follower

每个分区有多个副本。那些副本被保存在broker上,存储Leader副本的节点就是Leader节点,存储Follower副本的就是Follower节点。所以后面Leader副本,Leader节点不做区分,都叫Leader。每个broker可以保存成百上千个属于不同主题和分区的副本。如下图,主题1被分为2个分区,每个分区3个副本(分区下面的数字是偏移量)

3、数据交付语义

数据交付语义有3种:

At most once——消息可能会丢失但绝不重传。

At least once——消息可以重传但绝不丢失。

Exactly once——这正是人们想要的, 每一条消息只被传递一次.

值得注意的是,这个问题被分成了两部分:发布消息的持久性保证和消费消息的保证。下面将在数据生产和消费的章节中分别介绍

4、数据生产

4.1 消息映射到分区

客户端控制消息发送数据到哪个分区,这个可以实现随机的负载均衡方式,或者使用一些特定语义的分区函数。 我们有提供特定分区的接口让用于根据指定的键值进行hash分区(当然也有选项可以重写分区函数),例如,如果使用用户ID作为key,则用户相关的所有数据都会被分发到同一个分区上。 这允许消费者在消费数据时做一些特定的本地化处理。这样的分区风格经常被设计用于一些本地处理比较敏感的消费者。

4.1.1 消息key

Kafka 的消息是一个个键值对。创建一条消息如下:

ProducerRecord<String, String> record =

new ProduceRecord<>(“CustomerCountry”,”Precision Products”,”France”);

         record即为一条消息,可以直接调用生产者的send方法进行发送。ProduceRecord的构造函数第一个参数即为topic,第二个参数是消息对应的key,第三个是消息的内容。

键有两个用途 :

  1. 作为消息的附加信息,
  2. 用来决定消息该被写到主题的哪个分区。拥有相同键的消息将被写到同一个分区。

键可以为空。如果键值为null,并且使用了默认的分区器,那么记录将被随机地发送到主题内各个可用的分区上。分区器使用轮询(Round Robin)算法将消息均衡地分布到各个分区上。同一个键总是被映射到同一个分区上,所以在进行映射时,我们会使用主题所有的分区,而不仅仅是可用的分区。这也意味着,如果写人数据的分区是不可用的,那么就会发生错误。只有在不改变主题分区数量的情况下,键与分区之间的映射才能保持不变。

4.2 数据交付语义的实现----消息持久化

从 0.11.0.0 版本开始,Kafka producer新增了幂等性的传递选项,该选项保证重传不会在 log 中产生重复条目。为实现这个目的, broker 给每个 producer 都分配了一个 ID ,并且 producer 给每条被发送的消息分配了一个序列号来避免产生重复的消息。

4.2.1 事务性producer

同样也是从 0.11.0.0 版本开始, producer 新增了使用类似事务性的语义将消息发送到多个 topic partition 的功能: 也就是说,要么所有的消息都被成功的写入到了 log,要么一个都没写进去。这种语义的主要应用场景就是 Kafka topic 之间的 exactly-once 的数据传递(如下所述)。

4.2.2 提交确认

并非所有使用场景都需要这么强的保证。对于延迟敏感的应用场景,我们允许生产者指定它需要的持久性级别。如果 producer 指定了它想要等待消息被提交,则可以使用10ms的量级。然而, producer 也可以指定它想要完全异步地执行发送,或者它只想等待直到 leader 节点拥有该消息(follower 节点有没有无所谓)。

可靠性相关的内容,参考可靠性中的复制一节

4.3 生产者API

依赖:

<dependency>

    <groupId>org.apache.kafka</groupId>

    <artifactId>kafka-clients</artifactId>

    <version>2.2.0</version>

</dependency>

参考javadoc:

http://kafka.apache.org/22/javadoc/index.html?org/apache/kafka/clients/producer/KafkaProducer.html

4.3.1 创建生产者

private Properties kafkaProps=new Properties();

//放了两个broker

kafkaProps.put(“bootsrap.servers”,”localhost:9092,127.0.0.1:9093”);

kafkaProps.put(“key . serializer“,“org . apache.kafka.common.serialization.StringSerializer”)

kafkaProps.put (“value. serializer”,org.apache.kafka.common.serialization.StringSerializer”);

Producer=new KafkaProducer <String,String>(kafkaProps);

4.3.2 发送消息send

同步发送

ProducerRecord<String, String> record =

new ProduceRecord<>(“CustomerCountry”,”Precision Products”,”France”);

try{

         //创建producer的代码略,跟前面一样。get会一直阻塞等待kafka broker响应

         producer.send(record).get();

}catch(Exception e){e.printStackTrace()}

这里ProduceRecord三个参数,分别是主题,键和消息内容

异步发送

Callback是一个接口,把回调逻辑实现在这。

producer.send(record,Callback)

4.3.3 自定义分区

查看org.apache.kafka.clients.producer.Partitioner

实现这个接口可以让生产者实现自己的分区策略,把消息发送到对应的分区

5、数据消费

5.1 pull还是push

所谓push就是broker把数据推送给consumer,而pull则是consumer自己拉取数据。

push的好处是,broker知道有数据,可以立即推送给consumer。坏处是broker不了解consumer的情况,所以broker推送的数据少了,那consumer的资源得不到充分的利用,推送得多了,consumer又可能处理不过来

所以kafka使用pull模式,由consumer自动去拉取数据。不过pull模式的缺点是当broker没有数据时,会空轮询(即不断轮询去获取数据,但每次获取的数据为空)

5.2 消费者组

消费者有group.id属性,该属性相同的消费者就算是一个组的。一个群组里的消费者订阅的是同-个主题,每个消费者接收主题一部分分区的消息。一个主题的一个分区对应组内的一个消费者。如下图所示:

这里有2个组AB在消费数据。在组1中,P0P3被C1消费,P1P2被C2消费,而在组B中,P0P3P1P2分别被C3C4C5C6消费。需要注意的是,消费者过多,可能导致部分消费者被闲置,如下所示

这里因为一个分区只能被一个消费者消费,所以消费者5因为没有分配到分区,被闲置

5.3 broker对消费者偏移量的记录是针对组的

假设消费者4挂掉,费者5顶替消费者4的位置,并且4挂掉前,分区3的偏移量为100,即消费者4最后提交的偏移量为100,然后消费者5将会从101的位置继续处理消息,因为4跟5是同一个组的。

    这里有个问题需要注意,如果4在挂掉前,数据101到107,已经被处理,但还没来得及提交就挂掉了,此时消费者5就重复处理了101到107的数据了。后面会介绍解决方案

    另外,能不能实现这样的功能:假设有两个消费者AB,同时处理同个主题同个分区的数据。当处理到50的时候,B挂掉了,A继续处理。在A处理到第100个数据时,B又回来了,这时候能不能让B继续从51开始处理?答案是可以的,只要AB不属于同一个组即可。在消息被消费时,kafka会记录组对数据分区内的消息处理的偏移量,注意这里记录的是组的处理记录,而不是消费者的处理记录。所以AB如果不同组,它们的偏移量的记录是不一样的。

5.4 数据交付语义的实现----消息被确认消费

3种语义的实现方案:

  1. at-most-once:Consumer 可以先读取消息,然后将它的位置p保存到broker的log 中,最后再对消息进行处理。如果位置p保存后,还没处理数据,consumer就挂掉了,接手的consumer也是从p+1开始处理,也就是p位置这条数据实际上没被处理
  2. at-least-once:Consumer 可以先读取消息,然后处理消息,最后再保存它的位置。如果在保存数据位置p时,consumer挂掉,则新的consumer会从p开始获取数据,所以这个方案可能导致位置p的数据被处理2次。
  3. exactly once:这里有2个场景,一种是consumer数据处理后,输出到外部系统,如数据库,另一种场景是consumer把数据处理后输出回broker。
    1. consumer数据输出回broker的场景:使用上节提到的事务型procedure,将 consumer 的offset存储为一个 topic 中的消息。此时有2个数据要提交,一个是offset,一个是consumer处理后要输出的数据,把这2个数据放到一个事务中进行提交。
    2. consumer数据输出到外部系统的场景:可以使用2阶段提交,如数据库事务,然后让数据与offset一起被提交。也可以使用kafka自带的功能:Kafka Connect连接器,它将所读取的数据和数据的 offset 一起写入到 HDFS,以保证数据和 offset 都被更新,或者两者都不被更新。 对于其它很多需要这些较强语义,并且没有主键来避免消息重复的数据系统,我们也遵循类似的模式。因此,事实上 Kafka 在Kafka Streams中支持了exactly-once 的消息交付功能,并且在 topic 之间进行数据传递和处理时,通常使用事务型 producer/consumer 提供 exactly-once 的消息交付功能。

5.4.1 常规JMS对消息确认的实现

当消息被发送到消费者,则消息标记位sent,当消费者确认后,再标记为consumed。这里有几个问题:

  1. 如果 consumer 处理了消息但在发送确认之前出错了,那么该消息就会被消费两次。
  2. 关于性能的,现在 broker 必须为每条消息保存多个状态(首先对其加锁,确保该消息只被发送一次,然后将其永久的标记为 consumed,以便将其移除)
  3. 其他:比如如何处理已经发送但一直得不到确认的消息

5.4.2 kafka的实现

通过偏移量实现:消费者在处理完其消费的消息后,会对kafka提交一个偏移量,比如3,由此kafka知道消费者处理过数据偏移量为0到3的消息。如果此时消费者崩溃,当它再次连接上kafka时,kafka就能知道它掉线之前处理过偏移量为0到3的消息,从而让消费者可以继续处理其未处理的消息。

所以当消息被消费时,Kafka不会像其他 JMS 队列那样需要得到消费者的确认,从而实现异步提交

另外这使得被消费的消息的状态信息相当少,仅仅需要一个offset,而offset也仅仅只是一个数字而已。offset还可以作为周期性的 checkpoint(checkpoint概念在日志压缩中会提到)。这以非常低的代价实现了和消息确认机制等同的效果。

这种方式还有一个附加的好处。consumer 可以回退到之前的 offset 来再次消费之前的数据

5.5 消费者API

依赖跟生产者相同,javadoc参考如下链接:

http://kafka.apache.org/22/javadoc/index.html?org/apache/kafka/clients/consumer/KafkaConsumer.html

5.5.1 创建消费者

创建消费者:

private Properties kafkaProps=new Properties();

//放了两个broker

kafkaProps.put(“bootsrap.servers”,”localhost:9092,127.0.0.1:9093”);

//配置groupid

kafkaProps.put( “group.id”,” CountryCounter”);

//传入解码器

kafkaProps.put(“key . serializer“,“org . apache.kafka.common.serialization.StringDeserializer”)

kafkaProps.put (“value. serializer”,org.apache.kafka.common.serialization.StringDeserializer”);

KafkaConsumer<String,String> consumer=new KafkaConsumerr <String,String>(kafkaProps);

5.5.2 偏移量

重置偏移量

    如果要新加入的群组处理分区,则新加入的群组,其默认的偏移量是最大的,因为其auto.offset.reset值为latest。比如分区内最大偏移量是100,即已经有100个数据了,但是新组的偏移量是101,所以此时新组不会显示数据。所以可以把auto.offset.reset设置为earliest,这时候新组就可以从头开始处理数据。旧的组可以通过重置偏移量,达到重头开始处理的目的。

从任意偏移量开始处理数据

实际上,消费者可以借助seekToBeginning,seekToEnd,seek等方法,实现从任意偏移量进行数据处理

5.5.3 订阅主题

consumer.subscribe(Collections.singletonList(“customerCountries”));

这里传入了仅有一个元素的List,可见subsribe参数是一个集合,可以订阅多个主题。

我们也可以在调用 subscribe方法时传入一个正则表达式。正则表达式可以匹配多个主题, 如果有人创建了新的主题,并且主题的名字与正则表达式匹配 ,那么会立即触发一次再均衡,消费者就可以读取新添加的主题。如果应用程序需要读取多个主题,井且可以处理不同类型的数据,那么这种订阅方式就很管用

要订阅所有与 test 相关的主题,可以这样做 :

consumer.subscribe(“test.*”)

5.5.4 轮询(consumer用完要close)

消息轮询是消费者 API 的核心,通过一个简单的轮询向服务器请求数据。一旦消费者订阅了主题,轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据,开发者只需要使用一组简单的 API 来处理从分区返回的数据。

t ry {

while (true) {

               //拉取100条数据。拉取数据有两个作用,一是拉取数据,二是心跳,一个不断拉//取数据的消费者,无疑就是存活的

                  Consumer Records<String , String> records = consumer. poll( 100 );

for(Consumer Records<String , String> record:records)

{

log.debug( "topic = %s, partition = %s, offset = %d, consumer=%s,country=%s /n”,

         //record的方法,包括分区和偏移量信息

record.topic(),record . partition(),record.offset()

record. key(),record.value());

 

int updatedCount = 1;

if (custCountryMap. containsValue(record.value()) ) {

updatedCount = custCountryMap.get(record.value()) + 1;

}

custCountryMap.put(record.value(), updatedCount )

JSONObject json = new JSONObject(cust CountryMap);

System.out.println ( json.toString(4));

}

}

} finally {

        

         consumer.close();

}

5..5.5 退出轮询

如果确定要退出循环,需要通过另一个线程调用 consumer.wakeup()方法。如果循环运行在主线程里,可以在 ShutdownHook 里调用该方法。要记住,consumer.wakeup是消费者唯一一个可以从其他线程里安全调用 的方法。

下面是运行在主线程上的消费者退出线程的代码:

Runtime.getRuntime().addShutdownHook(new Thread(){

         System . out . println(“Starting exit ...”);

consumer. wakeup()

t ry {

mainThread.join();

} catch (InterruptedExcepti.on e) {

e . printStackTace();

}

});

5.5.6 提交

自动提交

如果 enable.auto.commit 被设为 true ,那么每过5s,消费者会自动把从 poll () 方法接收到的最大偏移量提交上去。提交时间间隔由 auto.commit.interval.ms控制,默认值是 5s假设我们仍然使用默认的 5s 提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后了 3s ,所以在这 3s 内到达的消息会被重复处理

同步提交

把 auto.commit.offset 设为 false ,让应用程序决定何时提交偏移量。使用 commitSync()提交偏移量最简单也最可靠。commitSync()将会提交由 poll () 返回的最新偏移量 , 所以在处理完所有记录后要确保调用了commitSync(),否则还是会有丢失消息的风险

while (true ) {

ConsumerRecords<String,String> records = consumer. poll(100);

//处理信息

for(ConsumerRecords<String,String> record:records)

{

S ystem.out.pr intf("topic = %s, partition = %s, offset = %d,

consumer=%s,country=%s /n”,

record.topic(),record . partition(),record.offset()

record. key(),record.value());

}

try {

        //处理后同步提交

consumer.commitSync()

} catch (CoMMi.tFai.ledExcepti.on e) {

log. error(“commit failed”,e);

}

}

异步提交

同步提交有一个不足之处,在 broker 对提交请求作出回应之前,应用程序会一直阻塞

while (true ) {

ConsumerRecords<String,String> records = consumer. poll(100);

//处理信息

for(ConsumerRecords<String,String> record:records)

{

S ystem.out.pr intf("topic = %s, partition = %s, offset = %d,

consumer=%s,country=%s /n”,

record.topic(),record . partition(),record.offset()

record. key(),record.value());

}

//异步提交

consumer.commitAsync();

}

在成功提交或碰到无怯恢复的错误之前,commitSync会一直重试,但是 commitAsync()不会,这也是commitAsync 不好的一个地方。它之所以不进行重试,是因为在它收到服务器响应的时候,可能有一个更大的偏移量 已经提交成功。可以给异步提交带一个回调处理提交成功后的事

while (true ) {

ConsumerRecords<String,String> records = consumer. poll(100);

//处理信息

for(ConsumerRecords<String,String> record:records)

{

S ystem.out.pr intf("topic = %s, partition = %s, offset = %d,

consumer=%s,country=%s /n”,

record.topic(),record . partition(),record.offset()

record. key(),record.value());

}

//异步提交

consumer.commitAsync(new OffsetCommitCallback(){

    public void onComplete(Map<TopicPartition,OffsetAndMetadata> offsets ,

Exception e){

        //在这处理

}

});

}

提交特定偏移量

private Map<TopicPartition,OffsetAndMetadata> currentOffsets =

new HashMap<>();。

int count =0;

 

。。。

 

while (true ) {

ConsumerRecords<String,String> records = consumer. poll(100);

//处理信息

for(ConsumerRecords<String,String> record:records)

{

System.out.pr intf("topic = %s, partition = %s, offset = %d,

consumer=%s,country=%s /n”,

record.topic(),record . partition(),record.offset()

record. key(),record.value());

//构建偏移量信息

currentOffsets.put(

new TopicPartition(record.topic(),record.partition()),

new OffsetAndMetadata(record.offset()+l ,"no Metadata”)

);

if(count % 1000 ==0)

    //提交特定偏移量

consumer.commitAsync(currentOffsets,null) ;

count++;

}

}

5.5.7 再均衡监听器

在为消费者分配新分区或移除旧分区时,可以通过消费者API执行一些应用程序代码,在调用 subscribe方法时传进去一个ConsumerRebalanceListener实例就可以了。ConsumerRebalanceListener有两个方法:

public void onPartitionsRevoked(Collection<TopicPartition> partitions)方法会在再均衡开始之前和消费者停止读取消息之后被调用

public void onPartitionsAssigned(Collection<TopicPartition> patitions )方法会在重新分配分区之后和消费者开始读取消息之前被调用。

示例:

consumer.subscribe(topics, ConsumerRebalanceListener实例);

5.5.8 无组的消费者

使用场合:你可能只需要一个消费者从一个主题的所有分区或者某个特定的分区读取数据。这个时候就不需要消费者群组和再均衡了,只需要把主题或者分区分配给消费者,然后开始读取消息并提交偏移量。如果是这样的话,就不需要订阅主题, 取而代之的是为自己分配分区。 一个消费者可以订阅主题(井加入消费者群组),或者为自己分配分区,但不能同时做这两件事情。

示例(看注释部分即可):

Llst<PartitionInfo> partitonInfos = null;

//向集群请求主题“topic”可用的分区。如果只打算读取特定分区 ,可以跳过这一步

partitonInfos = consumer.partitionsFor(topic);

if(partitonInfos!=null){

    for(PartitonInfos partition: partitonInfos)

        partitions.add(new TopicPartition(partition.topic(),

partition. partition ()));

            //知道需要哪些分区之后,调用 assign方法

        consumer.assign(partitions)

其他无关代码略。。

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值