一 producer概念
producer设计上比consumer简单,不涉及复杂组管理操作,即每个producer都是独立进行工作的,与其他producer实例之间没有关联。
producer的功能就是向topic发送消息,在发送之前需要确认消息要发送的分区。
确认分区:
producer提供了默认的分区策略和分区器。若待发送消息指定了key,那partition会根据key哈希值选目标分区,若未指定key,partitioner会使用轮讯的方式确认目标分区,这样最大限度地确保消息在所有分区上的均匀性。
producer也赋予了用户指定分区的权力。另外,partitioner会认为具有相同key的所有消息都会被路由到相同分区中。
确认分区后:
producer要寻找分区对应leader,就是该分区leader副本所在kafka broker上(topic有多个副本,但是只有一个为leader的副本会响应client请求)。
二 构造Producer步骤
- 创建Properties对象;
- 创建KafkaProducer对象;
- 构造发送的消息对象ProducerRecord;
- KafkaProducer.send()发送消息;
- 关闭kafkaProducer。
Producer实例:
首先先引入kafka的maven依赖:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>2.3.0</version>
</dependency>
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Properties;
import java.util.concurrent.Future;
public class ProducerTest {
public static void main(String[] args){
Properties props = new Properties();
props.put("bootstrap.servers","192.168.25.70:9092,192.168.25.70:9093,192.168.25.70:9094");//必须指定
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");//必须指定
props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");//必须指定
props.put("acks","-1");
props.put("retries",3);
props.put("batch.ms",323840);
props.put("linger.ms",10);
props.put("buffer.memory",33554432);
props.put("max.block.ms",3000);
Producer<String,String> producer = new KafkaProducer<String, String>(props);
for (int i = 0; i < 100; i++){
producer.send(new ProducerRecord<>("topic12", Integer.toString(i), Integer.toString(i)));
}
producer.close();
}
}
bootstrap.servers、key.serializer、value.serializer这三个参数是必须指定的。
其次在Producer发送消息分为同步发送和异步发送。KafkaProducer.send()方法是通过Future实现了同步发送和异步发送。
发送方式 | 方法名 | 返回值 |
异步发送 | send(ProducerRecord record, Callback callback) | Future<ProducerRecord> |
同步发送 | send(ProducerRecord record) | Future<ProducerRecord> |
Producer发送消息失败时,会返回异常错误。
返回的异常分为可重试异常和不可重试异常。
可重试异常(RetriableException):
- LeaderNotAvailableException:分区leader不可用,一般出现的leader选举期间;
- NotControllerException:controller不可用;
- NetWorkException:网络不可用或网络故障。
不可重试异常:这种异常非常严重,kafka无法自行处理。
- RecordToolException:发送消息尺寸过大,超过了规定大小;
- SerializationException:序列化失败异常;
- KafkaException:其他类型异常。
开发Producer时可以配置的参数:
acks:控制producer生产消息的持久性;
- acks=0:producer不管leader broker处理结果,直接返回响应结果;
- acks=all或-1:表示当发送消息时,leader broker不仅会将消息写本地日志,同时会等ISR其他副本写本地日志成功,才返回响应结果;
- acks=1:写入leader broker成功后即返回响应结果。
buffer.memory:指定缓存消息的缓冲区大小,单位:字节。
compression.type:是否压缩消息,默认none,压缩方式有GZIP、Snappy、Lz4等,压缩消息会降低网络I/O开销,提升整体吞吐量,但会增加producer端的cpu开销,若broker端压缩参数与producer不同,也会增加broker端cpu开销。
retries:重试次数,默认0。重试会造成消息重复发送,还可能造成消息乱序,所以需要consumer进行去重。处理乱序,producer提供了max.in.flight.requests.per.connection参数,该参数设为1后,producer确保某一时刻只能发送一个请求。
producer两次重试之间会停顿一段时间,防止频繁重试对系统带来冲击,可配置两次重试的时间,参数为retry.backoff.ms,默认100ms。
batch.size:它对于调优producer吞吐量和延时有重要作用。producer会将同一分区的多条消息封装进一个batch中,当batch满后,producer会发送batch中的所有消息。producer可以不用等batch满才发消息。
linger.ms:控制消息发送时延行为,默认为0,表示消息需要被立即发送。无需关心batch是否被填满。
max.request.size:用于控制producer发送请求的大小,实际是能够发送的最大消息的大小。
request.timeout.ms:请求超时时间,默认30秒。
三 消息分区机制
1 分区策略
producer提供了分区策略和分区器,默认partitioner会尽力将相同key的消息发送相同分区,没有指定key,partitioner采用轮训方式确保消息在topic的所有分区上均匀分布。
2 自定义分区器
自定义分区器需要实现org.apache.kafka.clients.producer.Partitioner接口。
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class MyPartitioner implements Partitioner {
private Random random = new Random();
@Override
public int partition(String topic, Object keyObj, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
String key = (String) keyObj;
List<PartitionInfo> partitionInfosList = cluster.availablePartitionsForTopic(topic);
int partitionCount = partitionInfosList.size();
int auditPartition = partitionCount - 1;
return key == null || key.isEmpty() || !key.contains("audit")?random.nextInt(partitionCount-1):auditPartition;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
random = new Random();
}
}
3 消息序列化
默认的序列化方式有:
- ByteArraySerializer:本质上什么也不做,因为已经是字节数组了;
- ByteBufferSerializer:序列化ByteBuffer;
- ByteSerializer:序列化kafka的自定义的Byte类;
- DoubleSerializer:序列化double类型;
- IntegerSerializer:序列化Integer类型;
- LongSerializer:序列化Long类型;
- StringSerializer:序列化String类型。
自定义序列化器:
需要实现org.apache.kafka.common.serialization.Serializer。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.serialization.Serializer;
import java.util.Map;
public class MySerializer implements Serializer {
private ObjectMapper objectMapper;
@Override
public void configure(Map configs, boolean isKey) {
objectMapper = new ObjectMapper();
}
@Override
public byte[] serialize(String s, Object o) {
byte[] ret = null;
try {
ret = objectMapper.writeValueAsString(o).getBytes();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return ret;
}
@Override
public byte[] serialize(String topic, Headers headers, Object data) {
byte[] ret = null;
try {
ret = objectMapper.writeValueAsString(data).getBytes();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return ret;
}
@Override
public void close() {
}
}
四 Producer拦截器(0.10.0.0版本引入)
producer拦截器实现了client端定制化控制逻辑。
interceptor使用在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求。producer允许用户指定多个interceptor作用于同一条消息形成链。
实现接口:ProducerInterceptor。
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
public class TimeStampPrependerInterceptor implements ProducerInterceptor<String,String> {
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
return new ProducerRecord<String,String>(producerRecord.topic(),producerRecord.partition(),producerRecord.timestamp(),
producerRecord.key(),System.currentTimeMillis()+"," +producerRecord.value().toString());
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
五 无消息丢失设置
如何规避网络消息丢失以及消息乱序?
在producer端可以通过以下参数配置实现:
(1)block.on.buffer.full=true
0.9.0.0版本中标记为deprecate,用max.block.ms替代。作用是当内存缓冲区填满时,producer进入阻塞状态,停止接收新消息,而不是抛出异常。
(2)acks=all
等所有follower都响应了才能认为提交成功。
(3)retries=Integer.MAX_VALUE
重试次数
(4)max.in.flight.requests.per.connection=1
等1时为防止topic下同分区下消息乱序。
然而实际效果限制了producer在单个broker连接上能够发送的来响应请求的数量,设为1时,producer在broker响应前无法再给broker发新请求。
(5)使用带回调机制的send方法,这样能收到消息发送出去的返回结果
(6)callback逻辑中显示立即关闭producer,能够解决乱序,若不关闭,producer会被允许将来完成的消息发送出去。
broker端配置:
(1) unclean.leader.election.enable=false
关闭unclean leader选举。即不允许非ISR中副本被选为leader,从而避免broker端因日志水位截断而造成消息丢失
(2)replication.factor>=3
多副本保存。
(3)min.insync.replicas>1
控制某条消息至少被写入到ISR中多个副本才算成功(前提是producer端acks=-1或all)。
(4)确保replication.factor>min.insync.replicas
相等时,只要有一副本挂掉,分区就无法工作,可用性降低。
六 消息压缩(0。7版本开始支持)
降低磁盘占用或带宽占用,提升I/O密集型应用的性能。
Kafka目前支持Gzip、Snappy、Lz4算法。目前压缩算法性能Lz4>>Snappy>Gzip。
七 多线程处理
实际环境中需要构造多个线程或多个进程来同时给kafka集群发消息。
目前有两种方法:
- 多线程但KafkaProducer实例;
- 多线程多KafkaProducer实例。
KafkaProducer是线程安全的。
说明 | 优势 | 劣势 | |
单KafkaProducer实例 | 所有线程共享一个KafkaProducer实例 | 实现简单,性能好 | 所有线程共享一个内存缓冲区,可能需要较多内存;一旦producer某个线程崩溃导致KafkaProducer实例被破坏,则所有用户线程都无法工作 |
多KafkaProducer实例 | 每个线程维护自己专属的KafkaProducer实例 | 每个用户线程拥有专属的KafkaProducer实例、缓冲区空间及一组对应的配置参数,可以进行细粒度的调优;单个KafkaProducer崩溃不会影响其他Producer线程工作 | 需要较大的内存分配开销 |