一个Kafka体系架构包括Producer,Broker,Consumer,ZooKeeper
ZooKeeper:Kafka用来负责集群元数据的管理,控制器的选举
Producer将消息发送到Broker,Broker将消息持久化到磁盘中
Consumer负责从Broker订阅并消费消息(消费者采用pull模式订阅并消费)
Kafka术语:
1.Producer:生产者,创建消息并发送
2.Consumer:消费者,接收消息。消费者连接Kafka并接收消息做相应逻辑处理
3.Broker:服务代理节点。对于Kafka来说,Broker可看作一个独立的Kafka服务节点或Kafka服务实例。一个或多个Broker组成一个Kafka集群。
4.主题Topic(逻辑概念)与分区Partition。消息以主题为单位进行归类
一个分区只属于单个主题,同一主题下的不同分区包含的消息是不同的,分区在存储层面可看作一个可追加的日志Log文件,消息在被追加到分区日志文件的时候都会分配一个特定的偏移量offset。
offset是消息在分区中的唯一标识,Kafka通过它来保证消息在分区内的顺序性,但offset不跨越分区。即Kafka保证的是分区有序而不是主题有序
Kafka中的分区可以分布在不同的服务器broker上,即一个主题可以横跨多个broker
若一个主题只对应一个文件,则该文件所在及其I/O将会成为这个主题的瓶颈
通过增加分区的数量实现水平扩展
Kafka为分区引入多副本Replica机制:增加副本数量提升容灾能力
同一分区的不同副本中保存相同的消息,副本之间是一主多从关系,其中leader副本负责处理读写请求,follower副本只负责与leader副本消息同步。
副本处于不同的broker中,当leader副本故障,从follower副本中重新选举新的leader副本(副本信息存ZooKeeper,重新选举利用ZooKeeper)
1个主题3个分区,且副本因子为2,如此每个分区有1个leader副本和1个follower副本
Topic: topic1 PartitionCount:3 ReplicationFactor:2 Configs:
Topic: topic1 Partition: 0 Leader: 0 Replicas: 0,2 Isr: 0,2
Topic: topic1 Partition: 1 Leader: 1 Replicas: 1,0 Isr: 1,0
Topic: topic1 Partition: 2 Leader: 2 Replicas: 2,1 Isr: 2,1
分区2 leader副本因子在broker2 follower副本在1
分区中所有副本统称AR。
所有与leader副本保持一定程度同步的副本,包括leader副本组成ISR,为AR的子集
与leader副本同步滞后过多的副本,不包括leader副本,组成OSR,AR=ISR+OSR
默认leader副本故障,只有ISR集合中的副本有资格被选举为新的leader,配置可改
ISR与HW和LEO也有紧密的关系。HW是High Watermark的缩写,俗称高水位,它标识了一个特定的消息偏移量offset,消费者只能拉取到这个offset之前的消息
LEO (Log End Offset),标识当前日志文件中下一条待写入消息的offset
HW= MIN(LEO1,LEO2...)
分区ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW,消费者只能消费HW之前的消息(保证选举同步)
创建主题或修改分区
新增partitions
bin/kafka-topics.sh --zookeeper 127.0.0.1:2181 --alter --topic PushNotice --partitions 3
新增topics 分片不能超过broker数量
bin/kafka-topics.sh --zookeeper 127.0.0.1:2181 --create --topic topic-demo1 --replication-factor 1 --partitions 4
查看主题 -describe
查看
bin/kafka-topics.sh --zookeeper 127.0.0.1:2181 --topic PushNotice --describe
显示
Topic: topic1 PartitionCount:3 ReplicationFactor:2 Configs:
Topic: topic1 Partition: 0 Leader: 0 Replicas: 0,2 Isr: 0,2
Topic: topic1 Partition: 1 Leader: 1 Replicas: 1,0 Isr: 1,0
Topic: topic1 Partition: 2 Leader: 2 Replicas: 2,1 Isr: 2,1
Partition分区 Leader 副本中的领导在哪个broker-id节点中 Replicas分区的副本存在哪几个broker-id中
Topic: topic1 Partition: 2 Leader: 2 Replicas: 2,1 Isr: 2,1
标识topic 分区2有2个副本, 副本broker-id=2的副本为领导节点支持读写 副本1为follow节点
Partition分区值 其他的数字是brokerid的值
控制台消费
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test3
--bootstrap-server 指定了连接的Kafka集群地址 --topic指定了消费者订阅的主题
控制台生产
kafka-console-producer.sh --broker-list localhost:9092 --topic test3
--broker-list指定了连接的Kafka集群地址 --topic指定了发送消息时的主题
org.apache.kafka
KafkaProducer 生产者客户端
ProducerRecord 需要发送的消息
KafkaConsumer 消费者客户端
ConsumerRecord 需要消费的消息
kafka目录下config下server.properties配置文件参数
1.zookeeper.connect:broker要连接的ZooKeeper集群的服务地址,包含端口号如 localhost:2181,localhost2:2181
可以指定chroot路径 localhost3:2181/kafka 若不指定,默认使用ZooKeeper的根路径
2.listeners:指明broker监听客户端连接的地址列表,即为客户端要连接broker的入口地址列表,protocoll://hostname1:port1,主机名0.0.0.0绑定所有网卡,127.0.0.1,这样无法对外服务。与此参数关联的还有advertised.listeners,作用和listeners类似,默认值也为null,若公有云上的及其通常配备多块网卡,即包含私网网卡和公网网卡,对于这种情况,可以设置advertised.listeners参数绑定公网ip供外部客户端使用,而配置listeners参数来绑定私网ip地址供broker间通信使用
3.broker.id:指定Kafka集群中broker的唯一标识,默认值为-1.与meta.properties文件及服务端参数broker.id.generation.enable和reserved.broker.max.id有关
4.log.dir log.dirs:保存消息日志文件到磁盘路径
5.message.max.bytes:指定broker所能接收消息的最大值默认976.6kb,若修改此参数 会影响客户端参数max.request.size,topic端参数max.message.bytes
一般的生产者逻辑:
配置生产者客户端参数,创建生产者实例
构建待发送的消息
发送消息
关闭实例
ProducerRecord:消息
public class ProducerRecord {
private final String topic; // 主题
private final Integer partition; // 分区号
private final Headers headers; // 消息头部
private final K key; // 键
private final V value; // 值
private final Long timestamp; // 消息的时间戳
...
}
key用来计算分区号进而让消息发往特定的分区。消息以主题为单位进行归类,key让消息再进行二次归类,同一个key的消息会被划分到同一个分区中
demo:
public class KafkaProducerAnalysis {
public static final String brokerList = "localhost:9092";
public static final String topic = "topic-demo";
public static Properties initPerferConfig() {
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(ProducerConfig.PARTITIONER_CLASS_CONFIG, DemoPartitioner.class.getName());
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptorPrefix.class.getName() + "," + ProducerInterceptor2.class.getName() + "," + ProducerInterceptorPrefix3.class.getName());
return props;
}
===================================================
Properties props = initPerferConfig();
KafkaProducer producer = new KafkaProducer<>(props);
kafka生产者参数:
bootstrap.servers:指定生产者客户端连接Kafka集群所需的broker地址清单,格式host1:port1,host2:port2, 注意这里并非需要所有的broker地址,因为生产者会从给定的broker里查找到其他broker的信息
key.serializer,value.serializer:broker端接收的消息必须以字节数组byte[]的形式存在,再发送消息时序列化key和value,serializer指定序列化方式
client.id:设定KafkaProducer对应的客户端id,默认值""。若客户端不设置生成为字符串producer-数字的形式
KafkaProducer的参数可在ProducerConfig类中找到
KafkaProducer是线程安全的,可在多个线程中共享单个KafkaProducer实例,可将KafkaProducer实例池化供其他线程调用
消息的创建
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable headers)
ProducerRecord消息类其topic和value是必填 其余选填
创建生产者实例和构建消息后,才可以发送消息,发送消息主要有三种模式:发后即忘fire-and-forget,同步sync,异步async
发送的两个重载方法
public Future send(ProducerRecord record)
public Future send(ProducerRecord record, Callback callback)
发后即忘:
producer.send(record);
同步:利用Future对象,要么发送成功,要么抛出异常
producer.send(record).get();
or
Future future = producer.send(record);
RecordMetadata metadata = future.get();
其中RecordMetadata可以获得更多信息 如下
public final class RecordMetadata {
public static final int UNKNOWN_PARTITION = -1;
private final long offset;
private final long timestamp;
private final int serializedKeySize;
private final int serializedValueSize;
private final TopicPartition topicPartition;
private volatile Long checksum;
}
异步:send方法中指定一个Callback的回调函数,Kafka有响应时就会回调
producer.send(record, new Callback() {
// 成功metadata不为空,失败exception不为空
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.println(metadata.partition() + ":" + metadata.offset());
}
}
});
回调函数的执行顺序保证分区有序,record1先于record2发送则callback1先于callback2调用
KafkaProducer一般会发生两种类型的异常:可重试的异常和不可重试的异常,若配置了retries参数,那么只要在规定的重试次数内自行恢复,就不会抛出异常,该参数默认0
KafkaProducer的close方法会阻塞等待之前所有的发送请求完成后再关闭
生产者需要用序列化器Serializer把对象转换成字节数组才能通过网络发送给Kafka。而消费者需要用反序列化起Deserializer将Kafka中收到的字节数组转换成相应的对象
Serializer接口:
配置当前类
void configure(Map var1, boolean var2);
执行序列化操作
byte[] serialize(String var1, T var2);
关闭序列化器
void close()
生产者使用的序列化器和消费者使用的反序列化器必须一一对应
StringSerializer:
public class StringSerializer implements Serializer{
private String encoding = "UTF8";
// 创建KafkaProducer实例时调用,确定编码类型
@Override
public void configure(Map configs, boolean isKey) {
String propertyName = isKey ? "key.serializer.encoding" : "value.serializer.encoding";
Object encodingValue = configs.get(propertyName);
if (encodingValue == null)
encodingValue = configs.get("serializer.encoding");
if (encodingValue instanceof String)
encoding = (String) encodingValue;
}
// 将String类型转成byte[]
@Override
public byte[] serialize(String topic, String data) {
try {
if (data == null)
return null;
else
return data.getBytes(encoding);
} catch (UnsupportedEncodingException e) {
throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + encoding);
}
}
@Override
public void close() {
// nothing to do
}
}
自定义序列化器需要实现Serializer接口
消息通过send方法发往broker时,可能会经过拦截器Interceptor,序列化器Serializer和分区器Partitioner之后才能发往broker。拦截器可选,序列化器必需,若消息ProducerRecord指定了partition字段则无需分区器的作用
默认分区器DefaultPartitioner
public class DefaultPartitioner implements Partitioner {
private final ConcurrentMap topicCounterMap = new ConcurrentHashMap();
public DefaultPartitioner() {
}
// Partitioner接口有一个父接口Configurable 只有一个方法如下:用来获取配置信息及初始化数据
public void configure(Map configs) {
}
// partition方法用来计算分区号 key不为null通过哈希算法确定 为null随机
// topic主题 key value keyBytes序列化后的键 valueBytes序列化后的值 cluster集群元数据信息
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) {
int nextValue = this.nextValue(topic);
List availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return ((PartitionInfo)availablePartitions.get(part)).partition();
} else {
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
private int nextValue(String topic) {
AtomicInteger counter = (AtomicInteger)this.topicCounterMap.get(topic);
if (null == counter) {
counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
AtomicInteger currentCounter = (AtomicInteger)this.topicCounterMap.putIfAbsent(topic, counter);
if (currentCounter != null) {
counter = currentCounter;
}
}
return counter.getAndIncrement();
}
public void close() {
}
}
自定义分区器,需要实现Partitioner接口,实现之后需要通过配置参数partitioner.class显式指定此分区器
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomizePartitioner.class.getName())
kafka有生产者拦截器和消费者拦截器
生产者拦截器自定义实现ProducerInterceptor接口
KafkaProducer在将消息序列化和计算分区前会调用生产者拦截器的onSend方法来对消息进行定制化操作
public interface ProducerInterceptor extends Configurable {
ProducerRecord onSend(ProducerRecord var1);
void onAcknowledgement(RecordMetadata var1, Exception var2);
void close();
}
KafkaProducer会在消息被应答Acknowledgement之前或消息发送失败时调用生产者拦截器的onAcknowledgement方法,优先于用户设定的Callback之前执行。此方法运行在Producer的I/O线程中,逻辑多会影响消息发送的速度
以上三个方法的异常只会被捕获记录,并不会再向上传递
自定义拦截器:
public class ProducerInterceptor2 implements ProducerInterceptor {
@Override
public ProducerRecord onSend(ProducerRecord record) {
System.out.println("生产者拦截器");
String modifiedValue = "prefix2-" + record.value();
return new ProducerRecord<>(record.topic(),
record.partition(), record.timestamp(),
record.key(), modifiedValue, record.headers());
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
}
@Override
public void close() {
}
@Override
public void configure(Map map) {
}
}
实现自定义的拦截器后,需要在KafkaProducer的配置参数interceptor.classes中指定这个拦截器
prop.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptorTest.class.getName())
KafkaProducer可以指定多个拦截器形成拦截器链,拦截器链按照interceptor.classes参数配置的拦截器的顺序来一一执行
在拦截器链中,若某个拦截器执行失败,那么下一个拦截器会接着从上一个执行成功的拦截器继续执行
整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和Sender线程
生产者整体架构:
主线程:
1.KafkaProducer
2.经过拦截器
3.经过序列化器
4.经过分区器
消息累加器RecordAccumulator:
RecordAccumulator中每个分区都有一个deque,里面存的ProducerBatch对象
RecordAccumulator为每个分区都维护了一个双端队列,即Deque,消息写入缓存时,追加到双端队列的尾部,Sender线程从双端队列的头部读取
ProducerBatch不是ProducerRecord,ProducerBatch中可以包含多个ProducerRecord,为一个消息批次
消息经过主线程继而缓存到消息累加器中,之后Sender线程负责从RecordAccumlator中获取消息并将其发送到Kafka中
RecordAccumulator主要用来缓存消息,RecordAccumulator缓存的大小通过生产者客户端参数buffer.memory配置,默认值32MB。
若生产者发送消息的速度超过发送到服务器的速度,则会导致生产者空间不足,表现为KafkaProducer的send方法调用要么被阻塞,要么抛异常,这个取决于参数max.block.ms的配置,默认60000,60秒
消息在网络中以字节的形式传输,发送之前需要创建一块内存区域来保存对应的消息,对应于java的ByteBuffer的创建和释放,RecordAccumulator中有一个BufferPool实现ByteBuffer的复用,但BufferPool只针对特定大小的ByteBuffer进行管理,而其他大小的ByteBuffer不会缓存仅BufferPool中,这个特定大小由batch.size参数指定,默认16KB
ProducerBatch的大小和batch.size参数也有关系,当ProducerRecord写入RecordAccumulator时,会寻找分区队列末尾的ProducerBatch是否可以写入,可以则写入,不可以则创建一个新的ProducerBatch,新建ProducerBatch时评估这条消息的大小是否超过batch.size参数的大小,若不超过,那么就以batch.size参数的大小来创建ProducerBatch,如此使用完这段内存区域后,可通过BufferPool复用;若超过,那么就以评估的大小创建ProducerBatch,这段内存区域不会被复用
Sender线程:
Sender从RecordAccumulator中获取缓存的消息后,会将原本的<分区,Deque>的保存形式转变成>的形式,其中Node表示Kafka集群的broker节点
对于网络连接来说,生产者客户端是与具体的broker节点建立的连接,也就是向具体的broker节点发送消息,而并不关心消息的分区,而对于KafkaProducer的应用逻辑只关心哪个分区发消息,所以这有应用逻辑层到网络I/O层面的转换
对于> Sender会将其进一步封装成的形式,这样就可以将Request请求发送各个Node了
请求在从Sender线程发往Kafka之前还会保存到InFlightRequests中,InFlightRequests保存对象的形式为Map>,它的主要作用是缓存了已经发出去但还没有收到响应的请求
通过参数max.in.flight.requests.per.connection 默认5 可以限制每个连接(客户端与Node之间的连接)最多缓存的连接数,超过这个参数后就不能再向这个连接发送更多的请求
若某NodeId对应Deque的size大于以上配置参数,说明这个Node负载较大或网络连接有问题
客户端元数据:
客户端拉取远程服务器更新元数据通过Sender线程 (挑选出leastLoadedNode)向Node发送MetadataRequest请求来获取具体的元数据信息,在创建完MetadataRequest之后同样会存入InFlightRequests
客户端没有需要使用的元数据信息时,或者超过metadata.max.age.ms时间没有更新元数据都会引起元数据的更新操作。默认值5分钟
生产者客户端参数:
1.acks:指定分区中必须要有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入。
acks参数有3种类型的值 字符串类型
acks = 1,默认值。生产者发送消息后,只要分区的leader副本成功写入消息,即收到成功响应
acks = 0。生产者发送消息之后不需要等待任何服务端的响应。最大的吞吐量
acks = -1 or acks = all。生产者在消息发送后,需要等待ISR中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应
2.max.request.size:这个参数用来限制生产者客户端能发送的消息的最大值,默认1MB
3.retries和retry.backoff.ms:
retries参数配置生产者重试的次数,默认0
retry.backoff.ms指定两次重试的时间间隔 默认100ms
对于重试,若acks参数配置为非零值,且max.in.flight.requests.per.connection参数配置为大于1的值,那么就会出现错序的现象
在需要保证消息顺序的场合建议把参数max.in.flight.requests.per.connection配置为1
4.compression.type:指定消息的压缩方式 默认值none,可选gzip,snappy,lz4 ;时间换空间
5.connections.max.idle.ms:指定多久之后关闭闲置的连接,默认9分钟
6.linger.ms:指定生产者发送ProducerBatch前等待更多消息ProducerRecord的时间,默认0。生产者客户端会在ProducerBatch被填满或等待时间超过linger.ms值时发送出去
7.receive.buffer.bytes:设置Socket接收消息缓冲区SO_RECBUF的大小,默认值32KB。设置-1,则使用操作系统的默认值
8.send.buffer.bytes:设置Socket发送消息缓存区SO_SNDBUF的大小,默认值128KB
9.request.timeout.ms:配置Producer等待请求响应的最长时间,默认值30000ms。超时可以选择进行重试。这个参数需要比broker端参数replica.lag.time.max.ms的值要大,如此减少因客户端重试而引起的消息重复地概率