基本应用以及消息处理的原理
这一篇主要介绍
1、kafka的应用:kafka-clients和与spring的结合
2、kafka消息处理的原理
消息中间件能做什么
主要解决分布式系统间消息传递的问题,它能屏蔽不同平台和协议之间的特性,实现程序之间的协同。
例如将一个流程中相互独立的子操作拆开来,实现异步化,类似于多线程并行处理。
如何实现这一功能,多线程也是可以,最好的方法是用第三方消息中间件:分布式消息队列。
总之,三个字:解耦;异步;削峰
java使用kafka进行通信
原生clients
maven依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.0.0</version>
</dependency>
发送端代码
public class GPkafkaProduce extends Thread
{
private KafkaProducer<Integer,String> kafkaProducer;
private String topic;
//初始化kafka信息
public GPkafkaProduce(String topic)
{
Properties properties=new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.116.137:9092");
properties.put(ProducerConfig.CLIENT_ID_CONFIG,"practice-producer");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
IntegerSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
kafkaProducer = new KafkaProducer<Integer, String>(properties);
this.topic=topic;
}
@Override
public void run()
{
//发送50个消息
int index = 0;
while(index<50)
{
String msg="pratice test message:"+index;
try
{
//同步获得返回结果
//这里的offset是该partition下一个被消费消息的指向地址
RecordMetadata recordMetadata = kafkaProducer.send(new ProducerRecord<Integer,String>(topic,msg)).get();
System.out.println(recordMetadata.offset()+"->"+recordMetadata.partition()+"->"+recordMetadata.topic());
//kafkaProducer.send(new ProducerRecord<Integer,String>(topic,msg)).get();
//异步获取返回结果
// kafkaProducer.send(new ProducerRecord<Integer, String>(topic, msg), new Callback()
// {
// @Override
// public void onCompletion(RecordMetadata recordMetadata, Exception e)
// {
// System.out.println("这是异步返回消息"+recordMetadata.offset()+"->"+recordMetadata.partition()+"->"+recordMetadata.topic());
// }
// });
TimeUnit.SECONDS.sleep(2);
index++;
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)
{
new GPkafkaProduce("test").start();
}
}
kafka对消息的发送支持同步和异步,本质上来说kafka用的是异步的方式发送到broker的,实际上kafka把消息是发送在一个队列中,由后台的线程不断的取消息发送,发送成功后调用callback。
kafka客户端会积累一定量的消息组装成一个批量消息发送出去,触发条件是
batch.size和linger.ms
batch.size 批量发送大小
生产者批量发送消息,为了减少性能消耗,会等待消息达到一定数量才发送消息,默认的字节数大小是16k,一批消息大小达到16k时会立即发送
linger.ms 发送的间隔时间
当消息迟迟不满16k时,也不能总是等着不发送,所以达到间隔时间时还是会发送的
二者都配置了的情况下,触发一个就发送消息
发送端架构:
消费端代码
public class GPkafkaConsumer extends Thread
{
//消费端也要初始化
private KafkaConsumer<Integer,String> consumer;
private String topic;
//初始化kafka信息
public GPkafkaConsumer(String topic)
{
Properties properties=new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.116.137:9092");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "practice-consumer");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");//设置offset自动提交
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");//自动提交间隔时间
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.IntegerDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");//对于当前groupid来说,消息的offset从最早的消息开始消费
consumer= new KafkaConsumer<Integer,String>(properties);
this.topic=topic;
;
}
@Override
public void run()
{
//消费端写个死循环去处理
while(true)
{
//阻塞
consumer.subscribe(Collections.singleton(topic));
ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofSeconds(1));
records.forEach(record -> {
System.out.println(record.key() + " " + record.value() + " -> offset:" + record.offset());
});
}
}
public static void main(String[] args)
{
new GPkafkaConsumer("test").start();
}
}
基础配置解析
group.id
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "practice-consumer");
consumer group是消费者的一个分类,一个group下有一个多个consumer,group.id是相同的
一个分区只能有一个group内的一个consumer来消费
enable.auto.commit
//设置offset自动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
前面说了消息的消费状态是consumer控制的,消费者得保证自己不重复消费一条消息
消费者消费一条消息后将offset指向该分区内的下一条消息
消费者消费消息以后自动提交,只有当消息提交以后,该消息才不会被再次接收到。
注意:一般都是用手动提交的多
还可以配合
auto.commit.interval.ms控制自动提交的频率。
//自动提交间隔时间
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
auto.offset.reset
//对于当前groupid来说,消息的offset从最早的消息开始消费
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
这个配置有三个参数
latest、earliest、none
都是针对于新的groupid的消费者而言的
earliest 是最早的topic消息都消费
latest 新的消费者从其他消费者最后的offset开始往下消费
none 新的消费者加入以后,由于之前不存在offset,则会直接抛出异常
max.poll.records
此设置限制每次调用poll返回的消息数,这样可以更容易的预测每次poll间隔要处理的最大值。通过调整此值,可以减少poll间隔
springboot和kafka整合
依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
发送端
@Component
public class Producer
{
@Autowired
private KafkaTemplate kafkaTemplate;
public void send()
{
kafkaTemplate.send("test","msgKey","msgData");
}
}
消费端
@Component
public class Consumer
{
@KafkaListener(topics = {"test"})
public void listener(ConsumerRecord record)
{
Optional<?> msg=Optional.ofNullable(record.value());
if(msg.isPresent()){
System.out.println(msg.get());
}
}
}
测试类
public class MainKafka
{
public static void main(String[] args)
{
//以springboot方式启动
ConfigurableApplicationContext context=SpringApplication.run
(KafkaPracticeApplication.class, args);
Producer kafkaProducer=context.getBean(Producer.class);
for(int i=0;i<3;i++){
kafkaProducer.send();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消息处理原理
topic和partition
topic是kafka存储消息的,可以认为是一个消息集合。每条消息发送到kafka都有一个类别
从物理上来说,不同的topic消息是分开存储的。
partition
每个topic分为一个或多个partition,同一topic下不同partition下的消息是不同
每个消息分配到partition时都会分配一个offset,是消息在该分区内的唯一标识,kafka通过offset保证下次再分区内的顺序性,offset的顺序不跨分区,kafka只保证在同一个分区内是有顺序的。
partition是以文件的形式存储在文件系统中
sh kafka-topics.sh --create --zookeeper 192.168.116.137:2181 --replication-factor 1 --partitions 3 --topic firstTopic
生产者分发消息&分区策略
在kafka中,一条消息是由key和value组成的,producer会根据key和partition的分发机制来决定把消息发到哪个partition中,可以自定义partition机制
默认的消息分发机制
kafka默认采用hash取模的分区算法,如果key为null,则随机分配分区。
metadata,是topic/partition和broker的映射关系,每一个topic每一个分区
都需要知道对应的broker列表是什么,leader是谁,foller是谁。都存在这里
自定义partition分发规则
public class MyPartitioner implements Partitioner
{
@Override
public int partition(String topic, Object key, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster)
{
System.out.println("enter MyPartitioner");
List<PartitionInfo> list = cluster.partitionsForTopic(topic);
int length = list.size();
if(key==null)
{
Random random=new Random();
return random.nextInt(length);
}
return Math.abs(key.hashCode())%length;
}
@Override
public void close()
{
}
@Override
public void configure(Map<String, ?> map)
{
}
}
发送端配置自动应以partitioner
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.gupaoedu.kafka.MyPartitioner");
消费端如何消费指定分区
代码控制
//消费指定分区的时候,不需要再订阅
//kafkaConsumer.subscribe(Collections.singletonList(topic));
//消费指定的分区
TopicPartition topicPartition=new TopicPartition(topic,0);
kafkaConsumer.assign(Arrays.asList(topicPartition));
消费者消费消息&分区策略
同一组消费者只有一个消费者能消费一个分区
borker上的数据分片提升了io性能,如何合理负载均衡呢
需要合理设置消费者和分区的数量
1、consumer数不要大于分区数
2、分区数最好是consumer的整数倍
3、kafka只保证在一个分区上数据有序,从多个分区读取数据,顺序不同
4、consumer、broker、partition数量变化回导致rebalance。
消费组新加了消费者
消费者挂了
topic新增了分区
消费者消费分区规则
1、RangeAssignor(范围分区)
n=分区数/消费者数 取模
m=分区数%消费者数 取余数
前m个消费者是n+1个分区,后(消费者数-m)分配n个分区
比如,10个分区,三个消费者,c1,c2,c3
n=3
m=1
前m个消费者(c1)消费4个(n+1),后面俩(3-1)消费3(n)
11个分区
n=3
m=2
c1 消费 4个
c2 消费 4个
c3 消费 3个
如果有两个主题,分别10个分区,c1就消费了 8个,其他的都是6个,好惨
2、RoundRobinAssignor(轮询分区)
按照hashcode排序,最后轮询分配分区给消费者
3、StrickyAssignor (粘滞策略)
简单的来说就俩
1、尽可能分配均匀
2、尽可能维持上次的分配相同
2个条件冲突时,1优先于2。
谁来管理消费者-corrdinator
coordinator管理消费者组。第一个consumer启动后向kafka确定谁是
coordinator,后续组员都跟这个coordinnator协调。
kafka服务端返回负载最小的broker节点作为coordinator
rebalance 过程
1、join
选一个consumer作为leader,一般是第一个来的cnsumer,如果leader
没了,随机算法选举一个leader
coordinatro接收各消费者分分区消费策略
统计所有消费者的消费策略票数,用票数多的消费策略
2、sync
一般是由leader将消费者对应的分区分配方案同步给所有的consumer
总结
rebalance过程
coordinator 管理消费组
coordinator 在zk上增加watcher,消费者变化时,触发rebalance
开始rebalance
消费者向coordinator发起join请求,coordinator选一个leader。所有消费者
收到coordinator的返回消息,知道谁是leader
消费者向coordinator发sync请求,leader去分区分配,把结果童子所有comsumer
消息持久化
消息保存格式
生产者发送的消息可以永久保存在broker上,是以文件的形式保存的
默认路径是:/tmp/kafka-logs/topic_partition
eg: /tmp/kafka-logs/test_0
消息是分段保存的,n条记录后就生成新的文件;
index文件记录索引;log文件记录真实数据
如果第一个log最后一个offset是多少,下一个分段文件的命名就从这个值开始。
sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/mytopic-0/00000000000000000000.log --print-data-log
通过offset查找message
1、根据offset查找index索引文件,文件是以上一个文件最后一个offset命名的,所以使用二分查找快速丁文索引文件
2、找到索引文件,根据offset,定位,找出索引位置
3、得到position后,到log表中查找offset对应的消息
log文件格式:
createTime表示创建时间、keysize和valuesize表示key和 value的大小
compresscodec表示压缩编码、payload:表示消息的具体内容
日志清除或者压缩
清除策略:
1、根据保留时间,定期清除
2、根据topic存储数据大小,超过了,就开始清除旧的日志。kafka有后台线程,定期检查
通过log.retention.bytes和log.retention.hours设置
压缩策略:
kafka开启日志压缩功能;将key值相同的value合并,新value替换老的value。
磁盘存储的性能优化
零拷贝
页缓存
有时间可以了解下
消费位置
offset
每个topic有一个多个分区,每个分区数据不同。
每个消息分配到分区时,贴一个offset,是这个消息在此分区中的唯一表示
kafka通过offset保证在该分区内有序,
offset在哪里维护
老版本kafka中是保存在zk上,但是zk不适合做大量的读写操作,于是kafka自己来维护了
kafka提供一个topic,把offset写到这个topic中
这个topic保存了每个消费组的某个时段的提交的offset
根据自带计算公式推断出在哪个分区
消费组保存到哪个分区的:Kafka 如何读取offset topic内容 (__consumer_offsets)