kafka笔记

kafka操作命令合集

https://juejin.cn/post/7022145069851361311

基本概念

Broker

每个Broker相当于一个服务器,多个Broker构成了一个kafka集群

Topic

主题做消息分类,一个Broker可以包含多个Topic

Partition

分区,一个Topic包含多个分区,分区有leader和follower的区分,由于follower一般起到备份的作用,所以leader和follower一般不在同一台服务器上

kafka架构

0.9之前,offset消费偏移量存储在zk中
0.9之后,存储在broker的内部topic中,每个consumer会定期将自己消费分区的offset提交给kafka内部topic:__consumer_offsets,提交过去的时候,key是consumerGroupId+topic+分区号,value就是当前offset的值

三个特征

1、一个分区只能被一个消费值组内的一个消费者消费,如果一个消费者组中有消费者消费了该分区,那么同一消费者组中的其他消费者不可以再消费该分区(基于此,消费者组中的消费者个数不应大于分区数)
2、消费者消费消息是以分区为单位的,一次消费一个分区

kafka消息写入

写入消息时,有key、partition、value三个参数,
如果只指定value,消息会轮询写入分区,
如果指定key,则会根据key值哈希写入对应的分区

写入流程

分区副本

配置:default.replication.factor=N
此配置可以指定分区的副本(follower)的数量,producer和consumer只与leader分区进行交互

broker保证幂等性

Producer端配置:

  • acks参数:在Producer配置中,确保acks参数设置为"all"(或者-1),这将要求所有ISR(In-Sync Replicas)都确认消息的接收,以确保消息至少被复制到ISR中的所有副本。这有助于防止重复消息。

  • 消息ID:Producer可以为每条消息分配一个唯一的消息ID,这可以通过设置消息的key或自定义消息头来实现。

  • 幂等性配置:启用Producer的幂等性配置,可以设置enable.idempotence为true,这将确保Producer在发送消息时进行适当的幂等性检查。这会自动设置acks为"all"。

Broker端配置:

  • Unclean Leader选项:确保Kafka Broker的unclean.leader.election.enable配置设置为false,这将阻止非同步副本成为领袖,从而确保只有ISR中的副本可以处理消息。

kafka事务

// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 提交事务
void commitTransaction() throws ProducerFencedException;
// 4 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;

api

生产者

public class KafkaProducerTest {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //kafka服务端的主机名和端口号
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        /*  发出消息持久化机制参数
        (1)acks=0: 表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消息。
        (2)acks=1: 至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一 22 条消息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
        (3)acks=‐1或all: 需要等待 min.insync.replicas(默认为1,推荐配置大于等于2) 这个参数配置的副本个数都成功写入日志,这种策 略会保证 24 只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。
         */
        properties.put(ProducerConfig.ACKS_CONFIG, "all");
        //发送消息重试次数
        properties.put(ProducerConfig.RETRIES_CONFIG, "0");
        //重试间隔设置
        properties.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
        //一批消息处理大小
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        //消息不足16k时,间隔10ms也发送一次消息
        properties.put(ProducerConfig.LINGER_MS_CONFIG, 10);
        //请求延时
        properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        //发送缓存区内存大小
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
        //序列化器
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringSerializer");
        //注册拦截器
        properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.morning.kafka.MyInterceptor");

        KafkaProducer kafkaProducer = new KafkaProducer(properties);

        //同步发送
        // public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers)
        ProducerRecord record =
                new ProducerRecord<String, Object>("testInfoTopic", null, System.currentTimeMillis(), null,
                        "value");
        kafkaProducer.send(record);

        /**
         * 异步回调发送
         */
        kafkaProducer.send(record, (metadata, exception) -> {
            if (metadata != null) {
                System.err.println(metadata.partition() + " : " + metadata.offset());
            }

        });
        kafkaProducer.close();

    }
}

消费者

public class KafkaConsumerTest {
    public static String TOPIC_NAME = "myTopic";
    public static void main(String[] args) {
        Properties properties = new Properties();
        //kafka服务端的主机名和端口号
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        //消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "group");
        //是否自动确认offset(自动提交偏移量,有可能存在重复消费的问题,可以使用手动提交)
        //properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        //自动确认offset的时间间隔
        //properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

        /*
            当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在,那么应该如何消费
            latest(默认) :只消费自己启动之后发送到主题的消息
            earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都从头开始消费)
        */
        //properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

        /*
          consumer给broker发送心跳的间隔时间,broker接收到心跳如果此时有rebalance发生会通过心跳响应将
          rebalance方案下发给consumer,这个时间可以稍微短一点
        */
        properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
        /*
            服务端broker多久感知不到一个consumer心跳就认为他故障了,会将其踢出消费组,
            对应的Partition也会被重新分配给其他consumer,默认是10秒
        */
        properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10000);

        //一次poll最大拉取消息的条数,如果消费者处理速度很快,可以设置大点,如果处理速度一般,可以设置小点
        properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
        /*
            如果两次poll操作间隔超过了这个时间,broker就会认为这个consumer处理能力太弱,
            会将其踢出消费组,将分区分配给别的consumer消费
        */
        properties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
        // 从头开始消费
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        //序列化器
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");

        //定义consumer
        KafkaConsumer<Object, Object> kafkaConsumer = new KafkaConsumer<>(properties);
        //订阅topic
        kafkaConsumer.subscribe(Arrays.asList(TOPIC_NAME));

        // 消费指定分区
        //kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
        //消息回溯消费(从头开始消费)
        //kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
        //kafkaConsumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
        //指定offset消费
        //kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
        //kafkaConsumer.seek(new TopicPartition(TOPIC_NAME, 0), 10);

        //从指定时间点开始消费(先找出改时间戳的offset,再根据offset消费)
        /*List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
        //从1小时前开始消费
        long fetchDataTime = new Date().getTime() - 1000 * 60 * 60;
        Map<TopicPartition, Long> map = new HashMap<>();
        for (PartitionInfo par : topicPartitions) {
            map.put(new TopicPartition(topicName, par.partition()), fetchDataTime);
        }
        Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
        for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet()) {
            TopicPartition key = entry.getKey();
            OffsetAndTimestamp value = entry.getValue();
            if (key == null || value == null) continue;
            Long offset = value.offset();
            System.out.println("partition-" + key.partition() + "|offset-" + offset);
            System.out.println();
            //根据消费里的timestamp确定offset
            if (value != null) {
                consumer.assign(Arrays.asList(key));
                consumer.seek(key, offset);
            }
        }*/

        //当停机的时候释放资源
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            if (kafkaConsumer != null) {
                kafkaConsumer.close();
            }
        }));

        while (true) {
            //拉取消息长轮询,不满一秒就会持续拉取,拉取到就返回,满一秒没拉取到就返回空
            ConsumerRecords<Object, Object> poll = kafkaConsumer.poll(1000);
            //读取的是一批数据,需要遍历
            for (ConsumerRecord<Object, Object> record : poll) {
                System.err.println(record.offset() + "--" + record.key() + "--" + record.value());
            }
            if (poll.count() > 0) {
                //手动同步提交offset,当前线程会阻塞直到offset提交成功,一般使用同步提交,因为提交之后一般也没有什么逻辑代码了
                kafkaConsumer.commitSync();

                //异步提交
//                kafkaConsumer.commitAsync((offsets, e) -> {
//                    if (e != null) {
//                        System.err.println("commit failed for: " + offsets);
//                        System.err.println("commit failed exception: " + Arrays.toString(
//                                e.getStackTrace()));
//                    }
//                });
            }
        }
    }
}

拦截器api

自定义拦截器

@Slf4j
public class MyInterceptor implements ProducerInterceptor {
    private int successCount = 0;
    private int failCount = 0;

    @Override public ProducerRecord onSend(ProducerRecord record) {
        //消息发送前预处理,一般不会修改原有的分区和主题
        return new ProducerRecord<>(record.topic(), null, record.timestamp(), record.key(),
                record.value());
    }

    @Override public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
        /*
            该方法会在消息被应答或消息发送失败时调用,并且通常都是在producer 回调逻辑触发之前。
            onAcknowledgement 运行在producer 的I0线程中,因此不要在该方法中放入很重
            的逻辑,否则会拖慢producer的消息发送效率。
         */
        if (e == null) {
            successCount++;
        }else {
            failCount++;
        }
    }

    @Override public void close() {
        log.info("成功发送消息数:{}, 失败发送消息数: {}", successCount, failCount);
    }

    @Override public void configure(Map<String, ?> map) {

    }
}

注册拦截器

//注册拦截器(在创建producer对象之前,注册拦截器)
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.morning.kafka.MyInterceptor");

spring整合kafka

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

配置

spring:
  kafka:
    bootstrap-servers: 172.0.0.1:9092
    producer: # 生产者
      retries: 3 # 设置大于0的值,则客户端会将发送失败的记录重新发送
      batch-size: 16384
      buffer-memory: 33554432
      acks: 1
      # 指定消息key和消息体的编解码方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      # 默认消费组
      group-id: default-group
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    listener:
      # 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
      # RECORD
      # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
      # BATCH
      # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
      # TIME
      # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
      # COUNT
      # TIME | COUNT 有一个条件满足时提交
      # COUNT_TIME
      # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
      # MANUAL
      # 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
      # MANUAL_IMMEDIATE
      ack-mode: manual_immediate

生产者api

public class SpringKafkaProducerTest {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    private static String TOPIC_NAME = "topic_name";

    public void send() {
        kafkaTemplate.send(TOPIC_NAME, 0, "key", "value");
    }
}

消费者api

public class SpringKafkaConsumerTest {

    /**
     * @KafkaListener(groupId = "testGroup", topicPartitions = {
     *             @TopicPartition(topic = "topic1", partitions = {"0", "1"}),
     *             @TopicPartition(topic = "topic2", partitions = "0",
     *                     partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
     *     },concurrency = "6")
     *  //concurrency就是同组下的消费者个数,就是并发消费数,必须小于等于分区总数
     * @param record
     */
    @KafkaListener(topics = "my-replicated-topic",groupId = "testGroup")
    public void listenTestGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        //手动提交offset
        ack.acknowledge();
    }
}

Kafka设计原理

https://note.youdao.com/ynoteshare/index.html?id=d9fed88c81ff75e6c0e6364012d19fef&type=note&_time=1675050740229

Kafka线上环境规划

线上kafka环境规划

JVM参数设置

kafka是scala语言开发,运行在JVM上,需要对JVM参数合理设置,参看JVM调优专题
修改bin/kafka-start-server.sh中的jvm设置,假设机器是32G内存,可以如下设置:

export KAFKA_HEAP_OPTS="-Xmx16G -Xms16G -Xmn10G -XX:MetaspaceSize=256M -XX:+UseG1GC -XX:MaxGCPauseMillis=50 
-XX:G1HeapRegionSize=16M"

这种大内存的情况一般都要用G1垃圾收集器,因为年轻代内存比较大,用G1可以设置GC最大停顿时间,不至于一次minor gc就花费太长时间,当然,因为像kafka,rocketmq,es这些中间件,写数据到磁盘会用到操作系统的page cache,所以JVM内存不宜分配过大,需要给操作系统的缓存留出几个G。

Kafka线上问题总结优化

1、消息丢失情况:

消息发送端:
(1)acks=0: 表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消息。大数据统计报表场景,对性能要求很高,对数据丢失不敏感的情况可以用这种。
(2)acks=1: 至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一条消息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
(3)acks=-1或all: 这意味着leader需要等待所有备份(min.insync.replicas配置的备份个数)都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。当然如果min.insync.replicas配置的是1则也可能丢消息,跟acks=1情况类似。
消息消费端:
如果消费这边配置的是自动提交,万一消费到数据还没处理完,就自动提交offset了,但是此时你consumer直接宕机了,未处理完的数据丢失了,下次也消费不到了。

2、消息重复消费

消息发送端:
发送消息如果配置了重试机制,比如网络抖动时间过长导致发送端发送超时,实际broker可能已经接收到消息,但发送方会重新发送消息
消息消费端:
如果消费这边配置的是自动提交,刚拉取了一批数据处理了一部分,但还没来得及提交,服务挂了,下次重启又会拉取相同的一批数据重复处理
一般消费端都是要做消费幂等处理的。

3、消息乱序

如果发送端配置了重试机制,kafka不会等之前那条消息完全发送成功才去发送下一条消息,这样可能会出现,发送了1,2,3条消息,第一条超时了,后面两条发送成功,再重试发送第1条消息,这时消息在broker端的顺序就是2,3,1了
所以,是否一定要配置重试要根据业务情况而定。也可以用同步发送的模式去发消息,当然acks不能设置为0,这样也能保证消息发送的有序。
kafka保证全链路消息顺序消费,需要从发送端开始,将所有有序消息发送到同一个分区,然后用一个消费者去消费,但是这种性能比较低,可以在消费者端接收到消息后将需要保证顺序消费的几条消费发到内存队列(可以搞多个),一个内存队列开启一个线程顺序处理消息。(也可以考虑发送消息时携带时间戳,消费消息时根据时间戳排序消费)

4、消息积压

1)线上有时因为发送方发送消息速度过快,或者消费方处理消息过慢,可能会导致broker积压大量未消费消息。
此种情况如果积压了上百万未消费消息需要紧急处理,可以修改消费端程序,让其将收到的消息快速转发到其他topic(可以设置很多分区),然后再启动多个消费者同时消费新主题的不同分区。

2)由于消息数据格式变动或消费者程序有bug,导致消费者一直消费不成功,也可能导致broker积压大量未消费消息。
此种情况可以将这些消费不成功的消息转发到其它队列里去(类似死信队列),后面再慢慢分析死信队列里的消息处理问题。

5、延时队列

延时队列存储的对象是延时消息。所谓的“延时消息”是指消息被发送以后,并不想让消费者立刻获取,而是等待特定的时间后,消费者才能获取这个消息进行消费,延时队列的使用场景有很多, 比如 :
1)在订单系统中, 一个用户下单之后通常有 30 分钟的时间进行支付,如果 30 分钟之内没有支付成功,那么这个订单将进行异常处理,这时就可以使用延时队列来处理这些订单了。
2)订单完成1小时后通知用户进行评价。

实现思路:发送延时消息时先把消息按照不同的延迟时间段发送到指定的队列中(topic_1s,topic_5s,topic_10s,...topic_2h,这个一般不能支持任意时间段的延时),然后通过定时器进行轮训消费这些topic,查看消息是否到期,如果到期就把这个消息发送到具体业务处理的topic中,队列中消息越靠前的到期时间越早,具体来说就是定时器在一次消费过程中,对消息的发送时间做判断,看下是否延迟到对应时间了,如果到了就转发,如果还没到这一次定时任务就可以提前结束了。

6、消息回溯

如果某段时间对已消费消息计算的结果觉得有问题,可能是由于程序bug导致的计算错误,当程序bug修复后,这时可能需要对之前已消费的消息重新消费,可以指定从多久之前的消息回溯消费,这种可以用consumer的offsetsForTimes、seek等方法指定从某个offset偏移的消息开始消费.

7、分区数越多吞吐量越高吗

可以用kafka压测工具自己测试分区数不同,各种情况下的吞吐量

# 往test里发送一百万消息,每条设置1KB
# throughput 用来进行限流控制,当设定的值小于 0 时不限流,当设定的值大于 0 时,当发送的吞吐量大于该值时就会被阻塞一段时间
bin/kafka-producer-perf-test.sh --topic test --num-records 1000000 --record-size 1024 --throughput -1 --producer-props bootstrap.servers=192.168.65.60:9092 acks=1

Kafka高性能原理剖析

  • 磁盘顺序读写:kafka消息不能修改以及不会从文件中间删除保证了磁盘顺序读,kafka的消息写入文件都是追加在文件末尾,不会写入文件中的某个位置(随机写)保证了磁盘顺序写。
  • 数据传输的零拷贝
  • 读写数据的批量batch处理以及压缩传输
    数据传输零拷贝原理:
    数据传输零拷贝原理

kakfa 常用命令

查询所有topic

./bin/kafka-topics.sh --bootstrap-server localhost:9092 --list

创建topic

# 创建一个myTopic 3个分区,且一个副本,并且注册中心为 localhost:2181
./bin/kafka-topics.sh --create --zookeeper localhost:2181 --topic yourTopic --replication-factor 1 --partitions 3

查看topic详细信息

# 查询yourTopic主题的详细信息
./bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic yourTopic

开启生产者

./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic yourTopic

创建一个 Consumer(消费者)

./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic yourTopic --from-beginning

删除topic

./bin/kafka-topics.sh --zookeeper localhost:2181 --delete --topic myTopic2
posted @ 2022-11-20 23:04  MorningBell  阅读(143)  评论(0编辑  收藏  举报