Kafka(二)基本应用以及消息处理的原理

基本应用以及消息处理的原理

  • java使用kafka进行通信
    • 原生clients
    • 基础配置解析
    • springboot和kafka整合
  • 消息处理原理
    • topic和partition
    • 生产者分发消息&分区策略
    • 消费者消费消息&分区策略
    • 谁来管理消费者-corrdinator
    • 消息持久化
    • 消费位置

这一篇主要介绍
1、kafka的应用:kafka-clients和与spring的结合
2、kafka消息处理的原理

消息中间件能做什么

主要解决分布式系统间消息传递的问题,它能屏蔽不同平台和协议之间的特性,实现程序之间的协同。
例如将一个流程中相互独立的子操作拆开来,实现异步化,类似于多线程并行处理。
如何实现这一功能,多线程也是可以,最好的方法是用第三方消息中间件:分布式消息队列。

总之,三个字:解耦;异步;削峰

java使用kafka进行通信

原生clients

maven依赖


	org.apache.kafka
	kafka-clients
	2.0.0

发送端代码

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(topic,msg)).get();


                //异步获取返回结果
//                kafkaProducer.send(new ProducerRecord(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时,也不能总是等着不发送,所以达到间隔时间时还是会发送的
二者都配置了的情况下,触发一个就发送消息

发送端架构:
Kafka(二)基本应用以及消息处理的原理_第1张图片

消费端代码

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整合

依赖


org.springframework.kafka
spring-kafka
2.2.0.RELEASE

发送端

@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(二)基本应用以及消息处理的原理_第2张图片
消费组保存到哪个分区的:Kafka 如何读取offset topic内容 (__consumer_offsets)

你可能感兴趣的:(Kafka)