1、kafka生产批(优化)

producer会尝试缓冲record,实现批量发送,通过以下配置控制发送时机。

  • batch.size:当多条消息发送到一个分区时,生产者会进行批量发送,这个参数指定了批量消息的大小上限(以字节为单位)。
  • linger.ms:这个参数指定生产者在发送批量消息前等待的时间,当设置此参数后,即便没有达到批量消息的指定大小,到达时间后生产者也会发送批量消息到broker。
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,2048); // 批量发送数据上限为 2kb
properties.put(ProducerConfig.LINGER_MS_CONFIG,1000); // 批量发送最大等待时间 1s

2、消费者组

消费组实际上是用来管理组织消费者,原则:组外广播,组内负载均衡
组外广播:消费者组不同

properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g2");

组内负载均衡:消费者组相同

properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");

结论

消费组组内负载均衡是针对于分区,组内的消费者在负载均衡时,一个消费者负责Topic一个或者多个分区内数据处理

3、生产者幂等性(idempotence)

幂等:多次操作的结果和一次操作的结果相同,就称为幂等性操作。读操作一定是幂等性操作,写操作一定不是幂等性操作。

kafka批量推送是指整个kafka还是topic kafka 批量发送_幂等性


Kafka的producer和broker之间默认有应答(ack)机制,当kafka的producer发送数据给broker,如果在规定的时间没有收到应答,生产者会自动重发数据,这样的操作可能造成重复数据(at least onnce语义)的产生。

kafka支持三种语义操作:at least once、at most once、at exactly once。

幂等性使用方法:开启kafka生产者幂等性支持

properties.put(ProducerConfig.ACKS_CONFIG,"all"); // ack的机制
// 0 无需应答  1 只写入Leader分区后立即ack  -1(all) 写入到leader和follower分区后再进行应答
properties.put(ProducerConfig.RETRIES_CONFIG,3); // 重试发送record的次数,默认是int的最大值
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,3000); // 请求的超时时间 单位毫秒
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);  // 开启幂等性支持

4、kafka事务

kafka事务指的是在一个事务内多个Record发送或者处理是一个原子操作,不可分割,同时成功,或者同时失败。

kafka事务的隔离级别:读未提交(默认:read_uncommitted)、读提交(read_commmitted

4.1 仅生产者事务

Kafka生产者在一个事务内生产的Record是一个不可分割的整体,要么同时写入Kafka集群,或者某个出错,回滚撤销所有的写操作。

开启生产者事务的前提:

  • ENABLE_IDEMPOTENCE_CONFIG=true
  • 每一个生产者,需要唯一的事务编号:TransactionID
  • 事务的超时时间(可选):TRANSACTION_TIMEOUT_CONFIG
public class KafkaProducerDemo {
    public static void main(String[] args) {
        //1. 创建配置对象 指定Producer的信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class); 
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ProducerConfig.ACKS_CONFIG,"all"); // ack的机制
        properties.put(ProducerConfig.RETRIES_CONFIG,3); // 重试发送record的次数
        properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,3000); // 请求的超时时间 单位毫秒
        properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);  // 开启幂等性支持
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString()); // 生产者事务的ID
        properties.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG,1000); // 事务的超时时间

        //2. 创建Producer对象
        KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String>(properties);

        // 初始化kafka的事务
        producer.initTransactions();
        // 开启Kafka的事务
        producer.beginTransaction();

        //3. 发布消息
        try {
            for (int i = 80; i < 100; i++) {
                ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>("t1",i,"Hello World"+i);
                producer.send(record);
            }
            // 正常操作 提交Kafka事务
            producer.commitTransaction();
        } catch (ProducerFencedException e) {
            e.printStackTrace();
            // 异常操作 回滚kafka事务
            producer.abortTransaction();
        }
        //4. 提交
        producer.flush();
        producer.close();
    }
}
4.2 Consume-Transfer-Produce(消费和生产并存事务)

特点:消费kafka的A主题进行业务操作,将操作的结果写入到B主题中。如果开启consume-transfer-produce事务,读A和写B在一个事务环境中,不可分割,要么同时成功,要么同时失败。

/**
 * 消费生产并存事务
 */
public class ConsumeTransferProduce {
    public static void main(String[] args) {
        KafkaProducer<Integer, String> producer = initProducer();
        KafkaConsumer<Integer, String> consumer = initConsumer();

        //1. 消费者订阅
        consumer.subscribe(Arrays.asList("t5"));

        //2. 初始化kafka的事务
        producer.initTransactions();

        while (true) {
            //3. 开启kafka事务环境
            producer.beginTransaction();
            try {
                ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofSeconds(5));
                Map<TopicPartition, OffsetAndMetadata> commitOffset = new HashMap<>();
                for (ConsumerRecord<Integer, String> record : records) {
                    // 业务操作
                    System.out.println(record.key() + " ---> " + record.value());
                    // 人为模拟业务错误,测试事务
                    /*
                    if (record.value().equals("AA")){
                        int m = 1/0;
                    }
                    */
                    // 手动维护消费的偏移量信息
                    commitOffset.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1));

                    // 消费啥内容 发送啥内容
                    producer.send(new ProducerRecord<Integer, String>("t6", record.key(), record.value()));
                }
                //4. 将事务内的消费的偏移量提交
                producer.sendOffsetsToTransaction(commitOffset, "g1");

                //5. 提交kafka的事务
                producer.commitTransaction();
            } catch (ProducerFencedException e) {
                e.printStackTrace();
                //6. 回滚事务
                producer.abortTransaction();
            }
        }
    }

    /**
     * 初始化 生产者实例
     */
    public static KafkaProducer<Integer, String> initProducer() {
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
        properties.put(ProducerConfig.ACKS_CONFIG, "all");
        properties.put(ProducerConfig.RETRIES_CONFIG, 3);
        properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 2000);
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString()); // 事务的ID
        properties.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 1000);

        return new KafkaProducer<Integer, String>(properties);
    }

    /**
     * 初始化 消费者实例
     */
    public static KafkaConsumer<Integer, String> initConsumer() {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 手动提交消费的offset
        properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); // 读已提交 不会读到别的事务未提交的记录
        return new KafkaConsumer<Integer, String>(properties);
    }
}

5、kafka Streaming

Kafka Streams是一个用于构建应用程序和微服务的客户端库,其中的输入和输出数据存储在Kafka集群中。它结合了在客户端编写和部署标准Java和Scala应用程序的简单性,以及Kafka服务器端集群技术的优点,下面介绍一下kafka stram的概念。

Topology(拓扑):表示一个流计算任务,等价于MapReduce中的job。不同的是MapReduce的job作业最终会停止,但是Topology会一直运行在内存中,除非人工关闭该Topology

Stream:它代表了一个无限的,不断更新的Record数据集。流是有序,可重放和容错的不可变数据记录序列,其中数据记录被定义为键值对

States:用以持久化存放流计算状态结果,可以用以容错和故障恢复

Time

  • Event time(事件时间)
  • Processing time(处理时间)
  • Ingestion time(摄入时间)

注意:所谓的流处理就是通过Topology编织程序对Stream中Record元素的处理的逻辑/流程。

5.1 kafka stram架构

Kafka Streams通过构建Kafka生产者和消费者库并利用Kafka的本机功能来提供数据并行性,分布式协调,容错和操作简便性,从而简化了应用程序开发。

kafka批量推送是指整个kafka还是topic kafka 批量发送_数据_02


Kafka的消息分区用于存储和传递消息, Kafka Streams对数据进行分区以进行处理。 Kafka Streams使用Partition和Task的概念作为基于Kafka Topic分区的并行模型的逻辑单元。在并行化的背景下,Kafka Streams和Kafka之间有着密切的联系:

  • 每个stream分区都是完全有序的数据记录序列,并映射到Kafka Topic分区。
  • Stream中的数据记录映射到该Topic的Kafka消息。
  • 数据记录的key决定了Kafka和Kafka Streams中数据的分区,即数据如何路由到Topic的特定分区。

任务的并行度

Kafka Streams基于应用程序的输入流分区创建固定数量的Task,每个任务(Task)分配来自输入流的分区列表(即Kafka主题)。分区到任务的分配永远不会改变,因此每个任务都是应用程序的固定平行单元。然后,任务可以根据分配的分区实例化自己的处理器拓扑; 它们还为每个分配的分区维护一个缓冲区,并从这些记录缓冲区一次一个地处理消息。因此,流任务可以独立并行地处理,无需人工干预。

kafka批量推送是指整个kafka还是topic kafka 批量发送_幂等性_03


用户可以启动多个KafkaStream实例,这样等价启动了多个Stream Tread,每个Thread处理1~n个Task。一个Task对应一个分区,因此Kafka Stream流处理的并行度不会超越Topic的分区数。需要值得注意的是Kafka的每个Task都维护这自身的一些状态,线程之间不存在状态共享和通信。因此Kafka在实现流处理的过程中扩展是非常高效的。

容错
Kafka Streams构建于Kafka本地集成的容错功能之上。 Kafka分区具有高可用性和复制性;因此当流数据持久保存到Kafka时,即使应用程序失败并需要重新处理它也可用。 Kafka Streams中的任务利用Kafka消费者客户端提供的容错功能来处理故障。如果任务运行的计算机故障了,Kafka Streams会自动在其余一个正在运行的应用程序实例中重新启动该任务。

此外,Kafka Streams还确保local state store也很有力处理故障容错。对于每个state store,Kafka Stream维护一个带有副本changelog的Topic,在该Topic中跟踪任何状态更新。这些changelog Topic也是分区的,该分区和Task是一一对应的。如果Task在运行失败并Kafka Stream会在另一台计算机上重新启动该任务,Kafka Streams会保证在重新启动对新启动的任务的处理之前,通过重播相应的更改日志主题,将其关联的状态存储恢复到故障之前的内容。

5.2 kafka stream lowlevel api

lowlevel api作为了解即可,在实际工作中,都会使用high api。

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-streams</artifactId>
    <version>2.2.0</version>
</dependency>

案例:消费A topic的数据,进行wordCount统计,结果输入到B topic中。

public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.139.156:9092");
        properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
        properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
        properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-processor-application");

        // 创建Topology
        Topology topology = new Topology();
        topology.addSource("input_name", "Atopic");
        topology.addProcessor("wordCountProcessor_name", () -> new WordCountProcessor(), "input_name");
        topology.addSink("output_name", "Btopic", new StringSerializer(), new LongSerializer(), "wordCountProcessor_name");
        KafkaStreams streams = new KafkaStreams(topology, properties);
        streams.start();
    }
public class WordCountProcessor implements Processor<String, String> {
    private HashMap<String, Long> wordPair;
    private ProcessorContext context;


    public void init(ProcessorContext context) {
        wordPair = new HashMap<>();
        this.context = context;
        // 每隔1秒将处理的结果向下游传递
        this.context.schedule(Duration.ofSeconds(1), PunctuationType.STREAM_TIME, timestamp -> {
            wordPair.forEach((k,v) -> {
                this.context.forward(k,v);
            });
        });
    }

    public void process(String key, String value) {
        String[] words = value.split(" ");
        for (String word : words) {
            Long num = wordPair.getOrDefault(word, 0L);
            num++;
            wordPair.put(word, num);
        }
        context.commit();
    }

    public void close() {

    }
}

也可以作为数据接入,解析json使用

public class WordCountProcessor implements Processor<String, String> {
    private ProcessorContext context;
    public void init(ProcessorContext context) {
        this.context = context;
    }

    public void process(String key, String value) {
        JSONObject jsonObject = JSONObject.parseObject(value);
        StringBuilder append = new StringBuilder(jsonObject.getString("A")).append("\t").append(jsonObject.getString("B"));
        context.forward(key,append.toString());
    }

    public void close() {

    }
}

思考:,如果进行wordCount统计,当服务器出现故障或者进程丢掉的情况下,那统计的数据结果怎么办?

接下来进行一个有状态的案例:

public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.139.156:9092");
        properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
        properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
        properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-processor-application");


        // 创建state,存放状态信息
        Map<String, String> changelogConfig = new HashMap();
        /**
         *  changlog数据清除策略
         *  一、默认策略(delete) 删除超过保留期的过期数据
         *  二、compact(压实) 多个key相同的数据 使用新值覆盖旧值
         * */
        changelogConfig.put("cleanup.policy","compact");
        StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = Stores.keyValueStoreBuilder(
                Stores.persistentKeyValueStore("Counts"),
                Serdes.String(),
                Serdes.Long()).withLoggingEnabled(changelogConfig);  // 开启远程状态副本 容错故障恢复

        // 创建Topology
        Topology topology = new Topology();
        topology.addSource("input", "Atopic");
        topology.addProcessor("wordCountProcessor", () -> new WordCountProcessor(), "input");
        topology.addStateStore(countStoreSupplier,"wordCountProcessor");
        topology.addSink("output", "Btopic", new StringSerializer(), new LongSerializer(), "wordCountProcessor");

        KafkaStreams streams = new KafkaStreams(topology, properties);
        streams.start();
    }
public class WordCountProcessor implements Processor<String, String> {
    private KeyValueStore<String, Long> keyValueStore;
    private ProcessorContext context;

    public void init(ProcessorContext context) {
        keyValueStore = (KeyValueStore<String, Long>) context.getStateStore("Counts");
        this.context = context;
        // 定期向下游输出计算结果
        this.context.schedule(Duration.ofSeconds(1), PunctuationType.STREAM_TIME, timestamp -> {
            KeyValueIterator<String, Long> iterator = keyValueStore.all();
            while (iterator.hasNext()) {
                KeyValue<String, Long> entry = iterator.next();
                this.context.forward(entry.key, entry.value);
            }
            iterator.close();
        });
    }

    public void process(String key, String value) {
        String[] words = value.split(" ");
        for (String word : words) {
            Long oldValue = keyValueStore.get(word);
            if (oldValue == null) {
                keyValueStore.put(word, 1L);
            } else {
                keyValueStore.put(word, oldValue + 1L);
            }
        }
        context.commit();
    }

    public void close() {

    }
}

注意:事实StateStore本质是一个Topic,但是改topic的清除策略不在是delete,而是compact.