Kafka生产者
客户端开发
客户端开发一般包含以下几个基本流程:
- 配置生产者客户端参数及创建生产者实例;
- 构建待发送的消息;
- 发送消息;
- 关闭生产者实例。
示例代码:
public class KafkaProducerAnalysis {
public static final String brokerList = "localhost:9092";
public static final String topic = "topic-demo";
//初始化生产者配置
public static Properties initConfig() {
Properties pros = new Properties();
props.put(“bootstrap.servers", brokerList);
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("client.id", "producer.client.id.demo");
return props;
}
public static void main() {
Properties props = initConfig();
//创建生产者实例
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
//构建待发送的消息
ProducerRecord<String, String> record =
new ProducerRecord<>(topic, "hello, Kafka!");
try {
//发送消息
producer.send(record);
} catch (Exception e) {
e.printStackTrace();
}
}
}
消息的格式
-
topic:主题
-
partition:分区号
-
headers:消息头
-
key:键,不仅是消息的附加信息,而且可以用来计算分区号以发送消息至特定的分区
-
value:值,消息体,一般不为空,为空则表示特定的消息-墓碑消息。
-
timestamp:消息的时间戳,包括CreateTime和LogAppendTime两种类型,分别表示消息的创建时间和消息追加到日志文件的时间。
发送消息
- ProducerRecord的创建有一系列重载的方法,最简单和最常用的是:
public ProducerRecord(String top, V value);
使用这种方法相当于将其他参数都置为null。
- 发送消息有2个重载的方法,如下:
public Future<RecordMetadata> send(ProducerRecord<K, V> record)
public Future<RecordMetadata> send(ProducerRecord<K, V> record,
Callback callback)
- Kafka有以下几种消息发送方式
-
发后即忘
-
同步发送消息
-
异步发送消息
发后即忘
上文中用法就是发后即忘,生产者只负责发送消息而不关心消息是否到达。通常消息可以顺利发送,不过在某些异常情况下会有消息丢失的情况。这种消息发送方式性能最高,但可靠性也最差。
同步发送消息
注意KafkaProducer的send()方法返回值是Future类型,则可以利用Futrure对象实现同步发送消息,示例如下:
try { producer.send(record).get(); } catch (ExcutionException | InterruptedException e) { e.pringStackTrace(); }
send()方法本身是异步的,其返回值Future对象可以使调用方稍后获得发送结果,例子中调用了get()方法来阻塞等待Kafka的响应,直到消息发送成功或发生异常。
同步发送方式可靠性高,消息要么发送成功,要么发生异常,如果发生异常,可以在捕获后进行相应处理,不会像发后即忘方式出现消息丢失。但是同步发送的缺点也很明显,性能较差,因为消息的发送是阻塞的,需要等待上一条消息发送完才能发送下一条。
异步发送消息
异步发送消息一般使用第二个重载的方法,入参中指定一个Callback的回调函数,Kafka在返回响应时调用该函数来实现异步的发送确认,要么发送成功,要么发生异常。示例如下:
producer.send(record, new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if (exception != null) { //处理异常 ... } else { System.out.println(metadata.topic() + "-" + metadata.partition() + ":" + metadata.offset()); } } })
另外需要注意,对于同一个分区而言,如果消息1在消息2之前发送,那么Kafka可以保证callback1在callback2之前调用,即:回调函数可以保证消息的分区有序性。
producer.send(record1, callback1); producer.send(record2, callback2);
-
关闭生产者
Kafka提供close()方法来关闭生产者以释放资源。
public void close(); public void close(long timeout, TimeUnit timeUnit);
close()方法会阻塞等待之前所有的请求完成后再关闭生产者。调用带超时时间的close()方法会等待timeout时间,用来完成请求,然后强行退出。一般无参的close()方法使用较多。
消息通过send()方法发往broker过程中,通常会经过拦截器、序列化器和分区器等。
![3xSly6.png 3xSly6.png](https://i-blog.csdnimg.cn/blog_migrate/9b6a852510e49f7ce6db03fcd80c18ba.png)
分区器
若消息中没有指定partition字段,则需要依赖分区器,根据key字段来计算partition的值。
Kafka中提供的默认分区器是:org.apache.kafka.producer.internals.DefaultPartitioner,它实现了org.apache.kafka.client.producer.Partitioner 接口,该接口中定义了两个方法:
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster);
public void close();
-
轮询策略
也成Round-robin策略,即顺序分配消息到各个分区。如一个主题下有3个分区,那么第一条消息发送到分区0,第二条消息发送到分区1,第三条消息发送到分区2,以此类推。
这就是所谓的轮序策略。轮询策略是Kafka的Java生产者API默认提供的分区策略。
轮询策略有非常优秀的负载均衡表现,它总能保证消息最大限度地被平均分配到所有分区上,所以它是默认情况下最合理的分区策略,这也是最常用的分区策略 。
-
按消息键保序策略
Kafka允许为每条消息定义消息键,简称为key。一旦消息被定义了key,那么就可以保证同一个key的所有消息进入相同的分区中,由于每个分区中的消息都是有顺序的,所以该策略为按消息键保序策略。
该策略实现方法也相对简单,只需两行代码:
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic); return Math.abs(key.hashCode()) % partitions.size();
Kafka的默认分区策略实际上同时实现了两种策略 :
-
如果指定了key,那么默认实现按消息键保序策略;
-
如果没有指定key,则使用轮询策略。
-
-
自定义分区策略
用户也可以根据自身的业务需要,自定义自己的分区策略,比如一个大体量的数据中心需要大规模的Kafka集群来进行跨城市、跨地域的数据交换,针对这种场景,可以使用基于地理位置的分区策略。
比如针对南方用户产生的消息和针对北方用户产生的消息需要分别投放到相应的Broker中,我们就可以根据Broker所在的IP地址实现定制化的分区策略,如以下代码:
List<PartitionInfo> parititons = Cluster.partitionForTopic(topic); return partitions.stream() .filter(p -> isSouth(p.leader().host())) .map(Partition::partition) .findAny() .get();
小结
分区是实现负载均衡以及高吞吐量的关键,故在生产者这一端就要仔细分析合适的分区策略,避免造成消息数据的“倾斜”,使得某些分区成为性能瓶颈,进而引发下游数据消费的性能下降。
生产者拦截器
生产者拦截器主要可以提供以下几种功能:
- 在消息发送前做一些准备工作,如按照某个规则过滤部分消息、修改消息内容等;
- 在发送回调消息前做些定制化需求,比如统计类工作。
使用生产者拦截器,需要实现org.apache.kafka.clients.producer.ProducerInterceptor接口,该接口包含3个方法:
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
public void close();
- onSend()方法会在消息序列化和计算分区之前被调用,可以在该方法中对消息进行定制化操作。
- onAcknowledgement()方法会在消息成功提交或发送失败之后被调用。且该调用要早于用户设定的callback。注意:该方法和onSend()方法不在一个线程里,而是运行在Producer的I/O线程中,所以实现逻辑应该越简单越好,否则会影响消息发送速度。
主要适用场景:生产者拦截器可以应用于包括客户端监控、端到端系统性能检测、消息审计等多种功能在内的场景。
总结
- 首先,我们了解了生产者在客户端开发时的基本步骤;
- 在罗列了Kafka消息的基本字段属性后,介绍了消息发送的几种方式:发后即忘、同步发送和异步发送,并介绍了各自的优缺点;
- 最后我们根据消息发送流程分别介绍了分区器与生产者拦截器,知晓了默认分区策略以及其他几种常用分区策略。在消息发送前或返回提交时做相应处理,可以依靠生产者拦截器。
以上就是关于Kafka生产者的相关基础知识,至此,各位小伙伴们应该对Kafka生产者有了初步了解,希望可以帮助到大家,谢谢!
参考资料:
《深入理解Kafka核心设计与实践原理》
极客时间-《Kafka核心技术与实战》