Kafka生产者

向Kafka写入数据

  • Kafka生产者组件
    Kafka生产者_第1张图片

从创建一个ProducerRecord对象开始,ProducerRecord对象需要包含目标主题和要发送的内容。我们可以指定键或分区。在发送ProducerRecord对象时,生产者要先把键和值对象序列化成字节数组,这样才能在网络上传输。
接下来,数据被传送给分区起,如果之前在ProducerRecord对象里指定了分区,则分区器不会再做任何事情,直接把指定的分区返回。如果没有指定分区,则分区器会根据ProducerRecord对象的键来选择一个分区。选好分区后,生产者就知道该往哪个主题和分区发送这条记录了。紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的broker上。

创建Kafka生产者

Kafka生产者有3个必选属性

bootstrap.servers: 指定broker地址清单,地址格式为host:port。为了可靠性,至少提供两个broker信息,生产者可以从给定的broker里查找其他broker信息

key.serializer: 必须是一个实现了org.apache.kafka.xxx.Serializer接口的类,生产者使用这个类把键对象序列化成字节数组。

value.serializer: 与key.serializer一样,value.serializer指定的类会将值序列化。如果键和值都是字符串,可以使用与key.serializer一样的序列化器。如若不同则使用不同的序列化器。

发送消息到Kafka

  • 3种发送消息的方式

发送并忘记:发送消息到服务器,但并不关心是否正常到达;

同步发送:使用send()发送消息,返回一个Future对象,调用get()方法进行等待,就可以知道消息是否发送成功;

异步发送:调用send()方法,并指定一个回调函数,服务器在返回响应时调用该函数。

示例:

// 需要目标主题和要发送的键和值的对象 的构造函数
ProducerRecord record = new ProducerRecord<>("CustomerCountry", "Precision Producers", "France");
try {
  // 如果不关心发送结果,则可以使用这种发送方式
  producer.send(record);
} catch (Exception e) {
  // 发送消息之前,生产者可能发生其他异常,可能是序列化消息失败、缓冲区已满又或者是发送线程被中断
  e.printStackTrace();
}
  • 同步发送消息
    同步发送消息方式示例:
ProducerRecord record = new ProducerRecord<>("CustomerCountry", "Precision Products", "France");
try {
  // producer.send()方法先返回一个Future对象,然后调用Future对象的get()方法等待Kafka响应。如果服务器返回错误,get()方法会抛出异常,如果没有,则会得到一个RecordMetadata对象,可以用它获取消息的偏移量
  producer.send(record).get();
} catch (Exception e) {
  // 发送消息之前或发送过程中,比如broker返回了一个不允许重发消息的异常或者已经超过了重发的次数
  e.printStackTrace();
}

KafkaProducer一般会发生两类错误。其中一类是可重试错误,这类错误可以通过重发消息来解决,比如连接错误,可以通过再次建立连接来解决,"no leader"错误则可以通过重新为分区选举首领来解决;另一类错误无法通过重试解决,比如"消息太大"异常,此时直接抛出异常。

  • 异步发送消息
    为了在异步发送消息的同时能够对异常情况进行处理,生产者提供了回调支持。下面是使用回调的一个例子:
// 为了使用回调,需要实现一个org.apache.kafka.xxx.Callback接口的类,这个接口只有一个onCompletion方法
private class DemoProducerCallback implements Callback {
  @Override
  public void onCompletion(RecordMetadata recordMetadata, Exception e) {
    if (e != null) {
      // 若Kafka返回一个错误,onCompletion方法会抛出一个非空(non null)异常
      e.printStackTrace();
    }
  }
}

// 记录与之前的一样
ProducerRecord record = new ProducerRecord<>("CustomerCountry", "Biomedical Materials", "USA");
// 在发送消息时传进去一个回调对象
producer.send(record, new DemoProducerCallback());

生产者的配置

在内存使用、性能和可靠性方面对生产者影响比较大的参数的说明

1.acks:

acks参数制定了必须要有多少个分区副本接收到消息,生产者才会认为消息写入是成功的。此参数对消息丢失的可能性有重要影响
acks=0: 生产者在成功写入消息之前不会等待任何来自服务器的响应。即生产者不关注消息是否丢失,不等待服务器响应,只管发送消息,所以能达到很高的吞吐量。
acks=1: 只要集群leader节点收到消息,生产者就会收到一个来自服务器的成功响应。
acks=all: 只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。此时最安全,不过延迟最高,因为要等待不止一个服务节点的响应。

2.buffer.memory:

用来设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。若应用程序发送消息的速度超过发送到服务器的速度,会导致生产者空间不足。此时,send()方法调用要么阻塞,要么抛出异常,取决于如何设置block.on.buffer.full/max.block.ms参数

3.compression.type

默认情况下,消息发送时不会被压缩,该参数可以设置为snappy、gzip或lz4,它指定了消息被发送给broker之前使用哪一种压缩算法进行压缩。使用压缩可以降低Kafka的网络传输开销和存储开销。

4.retries

生产者从服务器收到的错误有可能是临时性的错误(比如分区找不到leader)。这种情况下,retries参数的值决定了生产者可以重发消息的次数,若达到这个次数,生产者会放弃重试并返回错误。默认情况下,生产者会在每次重试之间等待100ms,也可以通过retry.backoff.ms参数来改变这个时间间隔。

5.batch.size

当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算(而不是消息个数)。当批次被填满,批次里的所有消息会被发送出去。不过生产者并不一定都会等到批次被填满才发送,半满的批次,甚至只包含一个消息的批次也有可能被发送。

6.linger.ms

指定生产者在发送批次之前等待更多消息加入到批次的时间。KafkaProducer会在批次填满或linger.ms达到上限时把批次发送出去。默认情况,只要有可用线程,生产者就会把消息发送出去,就算批次里只有一个消息。虽然会增加延迟,但也会提升吞吐量。

7.client.id

可以是任意字符串,服务器会用它来识别消息的来源,还可以用在日志和配额指标里。

8.max.in.flight.requests.per.connnection

指定生产者在收到服务器响应之前可以发送多少个消息。值越高,就会占用越多的内存,不过也会提升吞吐量。设为1可以保证消息是按照发送的顺序写入服务器的,即使放生了重试。

9.timeout.ms、request.timeout.ms和metadata.fetch.timeout.ms

request.timeout.ms指定生产者在发送数据时等待服务器返回响应的时间,metadata.fetch.timeout.ms指定生产者在获取元数据(比如目标分区的leader是谁)时等待服务器返回响应时间。timeout.ms指定了broker等待同步副本返回消息确认的时间,与asks的配置相匹配——若指定时间内没收到同步副本的确认,则broker就会返回一个错误。

10.max.block.ms

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

11.max.request.size

控制生产者发送的请求大小。它可以指能发送的单个消息的最大值,也可以指单个请求里所有消息总的大小。broker对可接收的消息最大值也有自己的限制(message.max.bytes),所以两边的配置最好可以匹配,避免生产者发送的消息被broker拒绝。

12.receive.buffer.bytes和send.buffer.bytes

分别指定TCP socket接收和发送数据包的缓冲区大小。如果它们被设为-1,就使用操作系统的默认值。若生产者和消费者与broker处于不同的数据中心,则可以适当增大这些值,因为跨网络一般都有较高延迟和较低的带宽。

顺序保证

一般来说,若某些场景要求消息是有序的,则消息是否写入成功也是很关键的,所以不建议把retries设为0。可以把max.in.flight.requests.per.connection设为1,这样生产者尝试发送第一批消息时,就不会有其他的消息发送给broker。但是这样会严重影响生产者的吞吐量,所以只有在对消息的顺序有严格要求的情况下才能这么做。

序列化器

  • 在Kafka里使用Avro

Avro的数据文件里包含了整个schema,不过这样的开销是可接受的。但是如果在每条Kafka记录里都嵌入schema,会让记录的大小成倍地增加。我们遵循通用的结构模式并使用"schema注册表"来达到目的。schema注册表并不属于Kafka,可以使用一些开源的schema注册表来实现,例如:Confluent Schema Registry。

Avro记录的序列化和反序列化流程图
Kafka生产者_第2张图片

生产者将所有写入数据需要用到的schema保存在注册表里,然后在记录里引用schema标识符。负责读取数据的应用程序使用标识符从注册表里拉取schema来反序列化记录。序列化器和反序列化器分别负责处理schema的注册和拉取。

示例:把生成的Avro对象发送到Kafka

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");
// schema.registry.url是一个新的参数,指向schema的存储位置
props.put("schema.registry.url", schemaUrl);

String topic = "customerContacts";

// Customer是生成的对象。我们会告诉生产者Customer对象就是记录的值
Producer producer = new KafkaProducer(props);

// 不断生成事件,直到有人按下Ctrl+C组合键
while(true) {
  Customer customer = CustomerGenerator.getNext();
  System.out.println("xxxx" + customer.toString());
  ProducerRecord record = new ProducerRecord<>(topic, customer.getId(), customer);
  producer.send(record);
}

示例:使用一般的Avro对象而非生成的AVro对象

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");
// schema.registry.url是一个新的参数,指向schema的存储位置
props.put("schema.registry.url", schemaUrl);

// 这里需要提供Avro schema,因为我们没有使用Avro生成的对象
String schemaString = "{\n" +
        "      \"namespace\": \"customerManagement.avro\",\n" +
        "      \"type\": \"record\",\n" +
        "      \"name\": \"Customer\",\n" +
        "      \"fields\": [\n" +
        "        {\"name\": \"id\", \"type\": \"int\"},\n" +
        "        {\"name\": \"name\", \"type\": \"string\"},\n" +
        "        {\"name\": \"email\", \"type\":  [\"null\", \"string\"], \"default\": \"null\"}\n" +
        "      ]\n" +
        "    }";

// 对象类型是Avro GenericRecord,我们通过schema和需要写入的数据来初始化它
Producer producer = new KafkaProducer(props);
Schema.Parser parser = new Schema.Parser();
Schema schema = parser.parse(schemaString);

for (int nCustomers = 0; nCustomers < customers; nCustomers++) {
  String name = "exampleCustomer" + nCustomers;
  String email = "example" + nCustomers + "@example.com";
  
  GenericRecord customer = new GenericData.Record(schema);
  customer.put("id", nCustomers);
  customer.put("name", name);
  customer.put("email", email);
  
  ProducerRecord data = new ProducerRecord("customerContacts", name, customer);
  
  producer.send(data);
}

分区

Kafka消息是一个个键值对,ProducerRecord对象可以只包含目标主题和值,键可以设置为默认的null,不过大多数应用程序会用到键。键有两个用途:可以作为消息的附加信息,也可以用来决定消息该被写到主题的哪个分区。要创建一个包含键值的记录,只需像下面这样创建ProducerRecord对象:

ProducerRecord record = new ProducerRecord<>("CustomerCountry", "Laboratory Equipment", "USA");

如果要创建键为null的消息,不指定键就可以了:

// 这里键被设为null
ProducerRecord record = new ProducerRecord<>("CustomeCountry", "USA");

附录

  • 参考文章
  1. 《Kafka权威指南》

你可能感兴趣的:(Kafka)