最全kafka从头到尾学习--结合书籍《深入理解Kafka》

 Kafka为什么这么快:

  • partition 并行处理

  • 顺序写磁盘,充分利用磁盘特性

  • 利用了现代操作系统分页存储 Page Cache 来利用内存提高 I/O 效率

  • 采用了零拷贝技术

    • Producer 生产的数据持久化到 broker,采用 mmap 文件映射,实现顺序的快速写入

    • Customer 从 broker 读取数据,采用 sendfile,将磁盘文件读到 OS 内核缓冲区后,转到 NIO buffer进行网络发送,减少 CPU 消耗

public class KafkaProducerAnalysis
{
	public static final String brokerList = "localhost:9092";
	public static final String topic = "topic-demo";
	public static Properties initConfig()
	{
		Properties props = new Properties();
		props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
		props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
				StringSerializer.class.getName());
		props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
				StringSerializer.class.getName());
		props.put("client.id", "producer.client.id.demo");
		return props;
	}

	public static void main(String[] args)
	{
		Properties props = initConfig();
		//创建kafka实例
		KafkaProducer<String, String> producer = new KafkaProducer<>(props);
		//创建消息
		ProducerRecord<String, String> record =
				new ProducerRecord<String, String>(topic,"Hello Kafka");//该对象还有其他参数  如  分区号,消息头部,消息的时间戳
		try
		{
			producer.send(record);
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}
}

生产者 

kafka发送消息有三种模式:

1.发后即忘(fire-and-forger)2.同步(sync)3.异步(async)

 上面代码就是用的发后即忘的方式,它只管往kafka发送消息而并不关心消息是否正确到达。

但是这种发送模式会造成数据丢失,比如发生不可重试异常时。

send方法其实并不是void返回,而是Future<RecordMetadata>类型,send方法有两个重载方法:

public Future<RecordMetadata> send(ProducerRecord<K,V> record)

public Future<RecordMetadata> send(ProducerRecord<K,V> record, Callback callback)

要实现同步的发送方式,可以利用返回的Future对象实现,如下

producer.send(record).get();

KafkaProducer一般会发生两种异常:可重试的异常和不可重试异常

对于可重试的异常,如果配置了retries参数,那么只要在规定的重试次数自行恢复了,就不会抛出异常。retries参数的默认值为0 ,配置方式如下:

props.put(ProducerConfig.RETRIES_CONFIG, 10);

但是同步的这种方式不好的点在于性能不行,需要阻塞等待一条消息发送完成之后才能发送下一条。

异步发送的方式:

一般是在send()方法里面指定一个Callback的回调函数,kafka在返回响应时调用该函数来实现异步的发送确认。

producer.send(record, new Callback()
			{

				@Override
				public void onCompletion(RecordMetadata metadata, Exception exception)
				{
					// TODO Auto-generated method stub
					if(exception!=null){
						exception.printStackTrace();
					}else{
						//打印出具体信息
						System.out.println(metadata.topic()+"-"+
								metadata.partition()+":"+metadata.offset());
					}
				}
			});

序列化:

什么叫序列化? 生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给kafka,而对端的消费者需要用反序列化器(Deserializer)把从kafka中收到的字节数组转成相应的对象。

分区器:

消息通过send发送到broker的过程中,有可能经过拦截器、序列化器、分区器。

消息经过序列化之后就要确定它发往的分区,如果消息ProducerRecord中指定了partition字段的话,就不需要分区器了,因为partition代表的就是所要发往的分区号。

反之,没有指定partition的话就需要依赖分区器了。根据key这个字段来计算partition的值。

分区器的作用就是为消息分配片区。

kafka中提供的默认的分区器是DefaultPartitioner实现的是Partitioner接口

生产者拦截器:

作用:用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的信息,修改消息的内容。

如何使用?

自定义实现ProducerInterceptor接口,这个接口里面有三个方法:onSend    onAcknowledgement()    close()

kafka整体架构:

整个生产者客户端是由两个线程协同运行。分别是主线程和sender线程。

主线程由KafkaProducer创建消息,然后通过可能的拦截器、序列化器、和分区器的作用之后缓存到消息累加器RecordAccumulator。

sender线程从消息累加器中获取消息将其发送到kafka中。

RecordAccumulator的作用是用来缓存消息以便Sender线程批量发送消息,进而减少网络传输的资源消耗以提升性能。

这个缓存的大小可以通过生产者客户端参数buffer.memory配置,默认值是32MB,如果生产者发送消息的速度超过发送到服务器的速度,则会导致生产者空间不足,

这个时候KafkaProducer的send方法调用要么被阻塞,要么抛异常。取决于max.block.ms参数,默认值60s

该参数指定了在调用 send() 方法或使用 partitionsFor() 方法获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法就会阻塞。在阻塞时间达到 max.block.ms 时,生产者会抛出超时异常。

主线程发送过来的消息会被追加到RecordAccumulator的某个双端队列中(尾部)。  RecordAccumulator内部每个分区都维护了一个双端队列,队列内容就是ProducerBatch。即Deque<ProducerBatch>。

ProducerBatch和ProducerRecord区别:ProducerBatch包含多个ProducerRecord。

发送消息之前需要创建一块内存区域来保存对应的消息。通过java.io.ByteBuffer实现消息内存的创建和释放。但是频繁的创建和释放比较消耗资源,RecordAccumulator内部还有一个BufferPool来实现ByteBuffer的复用。以实现缓存的高效利用。但是BufferPool只针对特定大小的ByteBuffer进行管理。大小由batch.size参数指定。默认时16KB。

还有一些重要参数:

     acks: 此参数用来指定分区中必须要有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入的。

     acks = 1 : 默认值,属于字符串类型。生产者发送消息之后,只要分区的leader副本成功写入消息,那么它就会收到来自服务端的成功响应。如果消息由于一些原因无法写入leader副本,那么生产者就会收到一个错误响应,为了避免消息丢失,生产者可以选择重发消息。

     acks = 0 : 生产者发送消息之后不需要等待任何服务端的响应。消息会丢失,但是可以达到最大的吞吐量。

     acks = -1 或 acks = all : 生产者发送消息之后,需要等待ISR中的所有副本都成功写入消息之后才能收到来自服务端的成功响应。可靠性强。但这并不意味着消息的绝对可靠性,因为ISR中可能之后一个leader副本,这样就退化成acks=1的情况。要获得更高的消息可靠性需要配合min.insync.replicas等参数的联动。

max.request.size : 这个参数用来限制生产者客户端能发送的消息的最大值,默认值是1MB。尽量不用修改。

retries和retry.backoff.ms: 前者用来配置生产者重试的次数,默认值为0,后者是用来设定重试之间的时间间隔,避免无效的频繁重试,默认值100。

消费者

消费者和消费组的关系:

每个消费者都有一个消费组,对于topic中多个分区,一个分区只能被一个消费组中一个消费者消费。如下:

对于消息中间件而言,一般有两种消息投递方式:点对点模式    发布/订阅模式。

kafka支持这两种方式。

原因:得益于消费者与消费组模型的契合。

  1. 如果所有的消费者属于同一个消费组,那么所有的消息都会被均衡的投递给每一个消费者,即每条消息只会被一个消费者所消费,这就相当于点对点模式的应用。
  2. 如果所有的消费者属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者消费,这就相当于发布/订阅模式的应用。

消费者在消费前需要指定其所属消费组的名称,这个可以通过消费者客户端参数group.id来配置,默认值是空字符串。

同一个消费组内的消费者既可以部署在同一台机器上,也可以部署在不同的机器上。

消费客户端开发

一个正常的消费逻辑需要具备以下几个步骤:

  1. 配置消费者客户端参数以及创建相应的消费者实例
  2. 订阅主题
  3. 拉取消息并消费
  4. 提交消息位移
  5. 关闭消费者实例
public class KafkaConsumerAnalysis
{
	public static final String brokerList = "localhost:9092";
	public static final String topic = "topic-demo";
	public static final String groupId = "group.demo";
	public static final AtomicBoolean isRunning = new AtomicBoolean(true);

	public static Properties initConfig(){
		Properties props = new Properties();
		props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
				StringDeserializer.class.getName());
		props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
				StringDeserializer.class.getName());
		props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
				brokerList);
		props.put(ConsumerConfig.GROUP_ID_CONFIG,
				groupId);
		props.put(ConsumerConfig.CLIENT_ID_CONFIG, "client.id.demo");
		return props;
	}

	public static void main(String[] args)
	{
		Properties props = initConfig();
		KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		consumer.subscribe(Arrays.asList(topic));
        //如果只订阅特定分区 如topic-demo主题中分区编号为0的分区
		//consumer.assign(Arrays.asList(new TopicPartition("topic-demo", 0)));
		try
		{
			while(isRunning.get()){
				ConsumerRecords<String, String> records =
						consumer.poll(1000);
				for(ConsumerRecord<String, String> record : records){
					System.out.println("topic = " + record.topic() +
							",partion = "+record.partition() + ",offset = "+
							record.offset());
					System.out.println("key = "+record.key()+", value = "+record.value());
					//。。。。
				}

			}
		}
		catch (Exception e)
		{
			// TODO: handle exception
		}
		finally {
			consumer.close();
		}
	}
}

订阅主题与分区:

一个消费者可以订阅一个或者多个主题  consumer.subscribe(Arrays.asList(topic))

也可以直接订阅分区: assign()方法

消息丢失和重复消费:

对于同步提交消费位移:consumer.commitSync();

                                                                       

  1. poll拉取的消息集为[x+2,x+7],x+2表示上次提交的消费位移,也就是之前已经完成了x+1前的所有消息的消费,包含x+1。x+5表示正在处理的位置,如果拉取消息之后就进行了位移提交,也就是提交了x+8,那么当消费x+5的时候遇到了异常,在故障恢复之后,我们重新拉取的消息是从x+8开始的,那么x+5到x+7之间的消息就没处理到,丢失了。(消息丢失)
  2. 另外一种情形,位移提交的动作是在消费完所有拉取到的消息之后才执行,那么当消费x+5的位置遇到异常,在故障恢复之后,重新拉取消息就是从x+2开始,那么x+2到x+4之间的消息就会又消费了一遍。(重复消费)

在kafka中默认的消费位移的提交方式是自动提交,消费者客户端参数enable.auto.commit配置,默认是true。这个默认的自动提交不是每消费一条就提交一次,而是定期提交。定期的周期时间由客户端参数auto.commit.interval.ms配置,默认是5s。同样自动提交也存在消息丢失和重复消费。

 

 

 

 

 

 

 

 

 

 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值