Kafka基础-生产者发送消息

无论你是使用Kafka作为队列,消息总线还是数据存储平台,你都会用到生产者,用于发送数据到Kafka。下文介绍如何使用Java来发送消息到Kafka。

1. 发送消息的主要步骤

Kafka基础-生产者发送消息_第1张图片

  • 首先创建ProducerRecord对象,此对象除了包括需要发送的数据value之外还必须指定topic,另外也可以指定key和分区。当发送ProducerRecord的时候,生产者做的第一件事就是把key和value序列化为ByteArrays,以便它们可以通过网络发送。
  • 接下来,数据会被发送到分区器。如果在ProducerRecord中指定了一个分区,那么分区器会直接返回指定的分区;否则,分区器通常会基于ProducerRecord的key值计算出一个分区。一旦分区被确定,生产者就知道数据会被发送到哪个topic和分区。然后数据会被添加到同一批发送到相同topic和分区的数据里面,一个单独的线程会负责把那些批数据发送到对应的brokers。
  • 当broker接收到数据的时候,如果数据已被成功写入到Kafka,会返回一个包含topic、分区和偏移量offset的RecordMetadata对象;如果broker写入数据失败,会返回一个异常信息给生产者。当生产者接收到异常信息时会尝试重新发送数据,如果尝试失败则抛出异常。

2. 创建生产者

发送数据到Kafka的第一步是创建一个生产者,必须指定以下三个属性:

  • bootstrap.servers:生产者用于与Kafka集群建立初始连接的主机和端口的列表。该列表不需要包括所有的brokers信息,因为生产者在建立连接后能够获取所有brokers的信息。但建议至少包含两个,防止一个broker宕机,生产者仍然能够通过另外一个broker连接到群集。
  • key.serializer:用于序列化keys的类名。Kafka brokers期待key和value的类型为byte数组,但是也允许使用参数化的Java对象作为key和value。这使得代码非常易读,但也意味着生产者必须知道如何把这些对象转换为byte数组。key.serializer应设为实现了org.apache.kafka.common.serialization.Serializer接口的类名,生产者将会使用这个类来把key对象序列化为byte数组。Kafka内置实现了ByteArraySerializer、StringSerializer和IntegerSerializer。注意,即使生产者发送的数据没有指定key,也必须设置key.serializer这个属性。
  • value.serializer:用于序列化value的类名。类似于key.serializer,生产者将会使用指定的类来把value对象序列化为byte数组。

下面是创建生产者的代码示例:

Properties kafkaProps = new Properties();
kafkaProps.put("bootstrap.servers", "broker1:9092,broker2:9092");
kafkaProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
kafkaProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer(kafkaProps);

3. 发送消息

发送消息主要有以下三种方法:

3.1 Fire-and-forget

发送消息后不需要关心是否发送成功。因为Kafka是高可用的,而且生产者会自动重新发送,所以大多数情况都会成功,但是有时也会失败。

下面是代码示例:

ProducerRecord record = new ProducerRecord("CustomerCountry",
		"Precision Products", "France");
try {
	producer.send(record);
} catch (Exception e) {
	e.printStackTrace();
}

ProducerRecord有多个构造器,这里使用了三个参数的,topic、key、value。

在发送消息之前有可能会发生异常,例如是序列化消息失败的SerializationException、缓冲区满的BufferExhaustedException、发送超时的TimeoutException或者发送的线程被中断的InterruptException。

3.2 Synchronous send

同步发送,调用send()方法后返回一个Future对象,再调用get()方法会等待直到结果返回,根据返回的结果可以判断是否发送成功。

简单的使用下面的代码替换上面try里面的一行代码:

producer.send(record).get();

在调用send()方法后再调用get()方法等待结果返回。如果发送失败会抛出异常,如果发送成功会返回一个RecordMetadata对象,然后可以调用offset()方法获取该消息在当前分区的偏移量。

KafkaProducer有两种类型的异常,第一种是可以重试的Retriable,该类异常可以通过重新发送消息解决。例如是连接异常后重新连接、“no leader”异常后重新选取新的leader。KafkaProducer可以配置为遇到该类异常后自动重新发送消息直到超过重试次数。第二类是不可重试的,例如是“message size too large”(消息太大),该类异常会马上返回错误。

3.3 Asynchronous send

异步发送,在调用send()方法的时候指定一个callback函数,当broker接收到返回的时候,该callback函数会被触发执行。

class DemoProducerCallback implements Callback {
	@Override
	public void onCompletion(RecordMetadata recordMetadata, Exception e) {
		if (e != null) {
			e.printStackTrace();
		}
	}
}

producer.send(record, new DemoProducerCallback());

要使用callback函数,先要实现org.apache.kafka.clients.producer.Callback接口,该接口只有一个onCompletion方法。如果发送异常,onCompletion的参数Exception e会为非空。

4. 生产者配置属性

生产者有很多配置属性,除了上述的三个之外,下面是一些比较重要的属性:

4.1 acks

此配置设置在生产者可以认为发送请求完成之前,有多少分区副本必须接收到数据。此选项对消息可能丢失的可能性有重大影响,此配置有三个允许的值,默认为1:

  • acks=0,生产者不会等待broker的任何确认,消息会被立即添加到缓冲区并被认为已经发送。在这种情况下,不能保证服务器已经收到消息,并且重试配置不会生效(因为客户端通常不会知道任何异常),每条消息返回的偏移量始终设置为-1。由于生产者不等待broker的任何确认,因此它可以以网络支持的最快速度发送消息,所以这个配置适用于实现非常高的吞吐量。
  • acks=1,在leader服务器的副本收到消息的同一时间,生产者会接收到broker的确认。如果消息不能写入leader的副本(例如,如果leader宕机并且还没有选出新的leader),生产者将接收到异常响应,然后可以重新发送消息,避免丢失数据。如果leader宕机并且消息没有被写入到新的leader(通过不确定的leader选举),该消息仍然会丢失。在这种情况下,吞吐量取决于消息是同步还是异步发送。如果我们的客户端等待服务器的回复(通过上述的调用发送消息时返回的Future对象的get()方法),它明显会显著地增加延迟(至少通过网络往返)。如果客户端使用callback,则延迟不会那么明显,但吞吐量将受到正在发送消息数量的限制(例如,在接收到响应之前生产者将会发送多少消息)。
  • acks=all(或-1),一旦所有的同步副本接收到消息,生产者才会接收到broker的确认。这是最安全的模式,因为可以确保多于一个的broker接收到该消息,即使在宕机的情况下,该消息也能被保存。然而,延迟性会比acks=1的时候更高,因为需要等待所有broker接收到消息。

4.2 buffer.memory

此配置设置生产者可用于缓冲等待发送给brokers消息的总内存字节数,默认为33554432=32MB。如果消息发送到缓存区的速度比发送到broker的速度快,那么生产者会被阻塞(根据max.block.ms配置的时间,默认为60000ms=1分钟,在0.9.0.0版本之前使用block.on.buffer.full配置),之后会抛出异常。

4.3 compression.type

生产者对生成的所有数据使用的压缩类型,默认值是none(即不压缩),有效值为none,gzip,snappy或lz4。Snappy压缩技术是Google开发的,它可以在提供较好的压缩比的同时,减少对CPU的使用率并保证好的性能,所以建议在同时考虑性能和带宽的情况下使用。Gzip压缩技术通常会使用更多的CPU和时间,但会产生更好的压缩比,所以建议在网络带宽更受限制的情况下使用。通过启用压缩功能,可以减少网络利用率和存储空间,这往往是向Kafka发送消息的瓶颈。

4.4 retries

默认值为0,当设置为大于零的值,客户端会重新发送任何发送失败的消息。注意,此重试与客户端收到错误时重新发送消息是没有区别的。在配置max.in.flight.requests.per.connection不等于1的情况下,允许重试可能会改变消息的顺序,因为如果两个批次的消息被发送到同一个分区,第一批消息发送失败但第二批成功,而第一批消息会被重新发送,则第二批消息会先被写入。

4.5 batch.size

当多个消息被发送到同一个分区时,生产者会把它们一起处理。此配置设置用于每批处理使用的内存字节数,默认为16384=16KB。当使用的内存满的时候,生产者会发送当前批次的所有消息。但是,这并不意味着生产者会一直等待使用的内存变满,根据下面linger.ms配置的时间也会触发消息发送。设置较小的值会增加发送的频率,从而可能会减少吞吐量;设置较大的值会使用较多的内存,设置为0会关闭批处理的功能。

4.6 linger.ms

此配置设置在发送当前批次消息之前等待新消息的时间量,默认值为0。KafkaProducer会在当前批次使用的内存已满或等待时间到达linger.ms配置时间的时候发送消息。当linger.ms>0时,延时性会增加,但会提高吞吐量,因为会减少消息发送频率。

4.7 client.id

用于标识发送消息的客户端,通常用于日志和性能指标以及配额。

4.8 max.in.flight.requests.per.connection

此配置设置客户端在单个连接上能够发送的未确认请求的最大数量,默认为5,超过此数量会造成阻塞。设置大的值可以提高吞吐量但会增加内存使用,但是需要注意的是,当设置值大于1而且发送失败时,如果启用了重试配置,有可能会改变消息的顺序。设置为1时,即使重新发送消息,也可以保证发送的顺序和写入的顺序一致。

4.9 request.timeout.ms

此配置设置客户端等待请求响应的最长时间,默认为30000ms=30秒,如果在这个时间内没有收到响应,客户端将重发请求,如果超过重试次数将抛异常。此配置应该比replica.lag.time.max.ms(broker配置,默认10秒)大,以减少由于生产者不必要的重试造成消息重复的可能性。

4.10 max.block.ms

当发送缓冲区已满或者元数据不可用时,生产者调用send()和partitionsFor()方法会被阻塞,默认阻塞时间为60000ms=1分钟。由于使用用户自定义的序列化器和分区器造成的阻塞将不会计入此时间。

4.11 max.request.size

此配置设置生产者在单个请求中能够发送的最大字节数,默认为1048576字节=1MB。例如,你可以发送单个大小为1MB的消息或者1000个大小为1KB的消息。注意,broker也有接收消息的大小限制,使用的配置是message.max.bytes=1000012字节(好奇怪的数字,约等于1MB)。

4.12 receive.buffer.bytes和send.buffer.bytes

  • receive.buffer.bytes:读取数据时使用的TCP接收缓冲区(SO_RCVBUF)的大小,默认值为32768字节=32KB。如果设置为-1,则将使用操作系统的默认值。
  • send.buffer.bytes:发送数据时使用的TCP发送缓冲区(SO_SNDBUF)的大小,默认值为131072字节=128KB。如果设置为-1,则将使用操作系统的默认值。

5. 序列化器

根据之前的代码示例,生产者配置必须指定序列化器。除了默认提供的序列化器之外还可以实现自定义的序列化器。

5.1 自定义序列化器

当发送给Kafka的消息不是简单的字符串或整数时,可以使用像JSON、Avro、Thrift或Protobuf这样的通用序列化库,也可以实现自定义的序列化库,强烈建议使用通用序列化库。以下是自定义序列化器的示例代码:

创建一个简单的Customer类:

public class Customer {
	
	private int customerID;
	private String customerName;

	public Customer(int ID, String name) {
		this.customerID = ID;
		this.customerName = name;
	}

	public int getID() {
		return customerID;
	}

	public String getName() {
		return customerName;
	}
}

创建一个简单的序列化器:

import java.nio.ByteBuffer;
import java.util.Map;

import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Serializer;

public class CustomerSerializer implements Serializer {

	@Override
	public void configure(Map configs, boolean isKey) {
		// nothing to configure
	}

	@Override
	/**
	 * We are serializing Customer as:
	 * 4 byte int representing customerId
	 * 4 byte int representing length of customerName
	 *     in UTF-8 bytes (0 if name is Null)
	 * N bytes representing customerName in UTF-8
	 */
	public byte[] serialize(String topic, Customer data) {
		try {
			byte[] serializedName;
			int stringSize;
			if (data == null)
				return null;
			else {
				if (data.getName() != null) {
					serializedName = data.getName().getBytes("UTF-8");
					stringSize = serializedName.length;
				} else {
					serializedName = new byte[0];
					stringSize = 0;
				}
			}
			ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + stringSize);
			buffer.putInt(data.getID());
			buffer.putInt(stringSize);
			buffer.put(serializedName);
			return buffer.array();
		} catch (Exception e) {
			throw new SerializationException("Error when serializing Customer to byte[] " + e);
		}
	}

	@Override
	public void close() {
		// nothing to close
	}

}

实现CustomerSerializer后可以定义ProducerRecord并且直接发送Customer对象。上述例子虽然非常简单,但一般不建议使用自定义的序列化器,因为如果一旦需要修改,例如更改customerID的类型为Long,或者添加新的startDate属性,那么在维护新旧消息代码的兼容性时会遇到不同程度的问题。

5.2 Apache Avro

Apache Avro是一个数据序列化系统,它支持丰富的数据结构,提供了紧凑的,快速的,二进制的数据格式。当使用Avro读取消息时,需要先读取整个的schema。为了实现这一点,可以使用了一个名为Schema Registry的架构,Confluent Schema Registry是实现该架构的开源软件之一。

我们只需要设置schema.registry.url属性即可:

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("value.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("schema.registry.url", schemaUrl);

String topic = "customerContacts";
KafkaProducer producer = new KafkaProducer(props);
// We keep producing new events
while (true) {
	Customer customer = CustomerGenerator.getNext();
	System.out.println("Generated customer " + customer.toString());
	ProducerRecord record = new ProducerRecord<>(topic, customer.getId(), customer);
	producer.send(record);
}

6. 分区

在上述提过,在创建消息时既可以指定key也可以不指定。Key除了可以保存额外的信息之外,还用于决定消息将会写入哪个分区,也就是说具有相同key的消息都会保存在同一分区。

当key为空且使用默认的分区器时,消息会被随机发送到指定topic的其中一个可用分区,会使用round-robin算法均衡分区间的消息。

当key不为空且使用默认的分区器时,Kafka会计算该key的hash值(使用其自己的hash算法,因此当升级Java版本时hash值不会改变),并使用得到的hash值把消息映射到特定的分区。因为把一个key始终映射到同一分区是非常重要的,所以需要使用一个topic的所有分区来计算映射关系,而不仅仅是可用的分区。这意味着,如果当写入消息到一个不可用的分区时,会出现异常,但是这种情况很少见。

只要一个topic的分区数量不变,key与分区的映射关系就能保证一致。但是如果你添加一个新的分区到一个topic时,虽然存在的数据仍然会保存在原来的分区里,但具有相同key的新消息不能保证还会写入到原来的分区。所以在创建topic时最好预先定义好需要的分区数量,避免后期添加新的分区造成映射关系的不一致。

6.1 自定义分区器

在使用默认的分区器时有可能会造成数据倾斜,数据被集中写入到某个分区,因此Kafka支持自定义的分区器,实现自己的分区策略。以下是示例代码,用于把指定key的数据写到最后一个分区里,其余key对应的值按照hash算法写入到其它分区:

import java.util.List;
import java.util.Map;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.record.InvalidRecordException;
import org.apache.kafka.common.utils.Utils;

public class CustomerPartitioner implements Partitioner {
	
	private Map configs;

	@Override
	public void configure(Map configs) {
		this.configs = configs;
	}

	@Override
	public int partition(String topic, Object key, byte[] keyBytes,
			Object value, byte[] valueBytes, Cluster cluster) {
		List partitions = cluster.partitionsForTopic(topic);
		int numPartitions = partitions.size();
		if ((keyBytes == null) || (!(key instanceof String))) {
			throw new InvalidRecordException("We expect all messages to have customer name as key");
		}
		// Customer key will always go to last partition
		// Other records will get hashed to the rest of the partitions
		if (((String) key).equals(configs.get("key"))) {
			return numPartitions;
		}
		return (Math.abs(Utils.murmur2(keyBytes)) % (numPartitions - 1));
	}

	@Override
	public void close() {
		// do something here
	}

}

END O(∩_∩)O

你可能感兴趣的:(Kafka)