记录我与kafka的一次邂逅_生产者-往kafka中写入数据

kafka生产者是一个向kafka集群发布记录的客户端。下图为kafka生产者组件图,图中展示了kafka发送消息的主要步骤:

在这里插入图片描述

  • 发送消息之前,首先创建一个ProducerRecord对象,该对象包含目标主题、指定键和分区以及要发送的内容;
  • 在发送ProducerRecord对象前,生产者要把键和值对象序列化成字节数组,才能在网络上传输;
  • 然后数据传送到分区器;如果在ProducerRecord对象中指定了分区,那么分区器将什么也不做;如果没有指定分区,那么分区器将会根据ProducerRecord对象中的键来选择一个分区;分区好后,分区器就知道该往哪个主题和分区发送这条记录;
  • 服务器在接收到消息时会返回一个响应;如果消息成功写入kafka,就返回一个RecordMetaData对象(对象包含主题和分区信息、偏移量);如果过没有成功写入,将会继续尝试重新发送消息,如果几次之后还是失败则返回错误信息。

要想往kafka中写入消息,就需要创建一个生产者对象并设置一些属性,这其中有三个属性是必须设置的:

  1. bootstrap.servers:指定broker的地址清单,格式:host:port;不需要列出所有的broker地址,但推荐至少提供两个;
  2. key.serializer:指定一个实现了org.apache.kafka.common.serialization.Serializer接口的类,使得生产者可以使用该类将java对象转换为字节数组;这是因为broker接收的键和值都是字节数组;
  3. value.serializer:与key.serializer一样,也需要将值序列化为字节数组;

创建kafka生产者

创建一个kafka生产者,只需要实例化KafkaProducer对象,然后配置对象的属性即可。

/* 创建一个properties对象,设置一些必要的属性,这里只指定了必要属性 */
Properties props = new Properties();
props.put("bootstrap.servers", "node01:9092,node02:9092,node03:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

/* 创建kafkaProducer对象 */
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(props);

实例化生产者对象后,就可以开始发送消息了,发送消息有两种方式,一种是同步发送消息,一种是异步发送消息。

同步发送消息

kafkaProducer.send()方法先返回一个Futurn对象,该对象调用get()方法等待kafka响应,如果服务器没有返回错误则会得到一个RecordMetadata对象,该对象可以获取消息的偏移量;如果返回错误,则get()方法会抛出异常。

ProducerRecord<String, String> record = new ProducerRecord<>("test", "Precision Products", "France");

try {
    kafkaProducer.send(record).get();
} catch (Exception e) {
    e.printStackTrace();
}

异步发送消息

当我们发送消息的时候并不需要等待响应,尽管kafka会将目标主题、分区信息和偏移量发送回来。但当消息发送不成功时,我们就需要抛出异常、记录错误日志以便于分析;为了在异步发送消息的同时能够对异常情况进行处理,生产者提供了回调支持。

接下来用一个完整示例来演示如何创建一个新的生产者,采用异步发送数据的方式,将数据发送到名叫“test”的topic中去:

public class MyProducer {

  public static void main(String[] args) {

    /* 创建一个properties对象,设置一些必要的属性,这里只指定了必要属性 */
    Properties props = new Properties();
    props.put("bootstrap.servers", "node01:9092,node02:9092,node03:9092");
    props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
    props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
  
    /* 创建kafkaProducer对象 */
    KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(props);

    kafkaProducer.send(new ProducerRecord<>("CustomerCountry","Biomedical Materials","USA"),new DemoProducerCallback());
  
    /* 关闭生产者 */
    kafkaProducer.close();
  }

  private static class DemoProducerCallback implements Callback{

    @Override
    public void onCompletion(RecordMetadata metadata, Exception exception) {
      if ( exception != null){
        exception.printStackTrace();
      }
    }
  }
}

生产者其它配置

除了上述三个必要的配置参数外,下面介绍几个在内存使用、性能和可靠性方面对生产者影响较大的参数。

  1. acks

    指定必须有几个分区副本收到消息,生产者才会认为消息写入是成功的。

    • acks=0:不等待服务器响应,消息丢失无法得知;能够支持最大速度发送消息,达到高吞吐量
    • acks=1:集群leader收到消息,则会返回成功响应;leader崩溃或新的leader还未选出,则生产者会收到错误响应;吞吐量取决于同步发送还是异步发送
    • acks=all:只有当所有参与复制的节点收到消息时,生产者才会收到成功响应;该模式最安全,但延迟高
  2. buffer.memory

    用来设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。

  3. compression.type

    指定消息发送到broker之前使用哪一种压缩算法进行压缩,参数可设置为:snappygziplz4

  4. retries

    设置生产者可以重发消息的次数,超过该次数生产者会放弃重试并返回错误。建议在设置重试次数和重试时间间隔之前,测试一下恢复崩溃节点需要的时间,让总的重试时间大于kafka集群从崩溃节点中恢复的时间,防止生产者过早的放弃重试。

  5. batch.size

    当有多个消息需要被发送到同一个分区时,生产者会将它们放在同一个批次里。该参数指定一个批次可以使用的内存大小。

  6. linger.ms

    指定生产者在发送批次之前等待更多消息加入批次的时间。

  7. client.id

    任意字符串,用于服务器识别消息来源。

  8. max.in.flight.requests.per.connection

    指定生产者在收到服务器响应之前可以发送多少个消息。设为1可以保证消息是按照发送的顺序写入服务器的。

  9. timeout.ms、request.timeout.ms 和 metadata.fetch.timeout.ms

    • timeout.ms:指定broker等待同步副本返回消息确认的时间
    • request.timeout.ms:指定了生产者在发送数据时等待服务器返回响应的时间
    • metadata.fetch.timeout.ms:指定了生产者在获取元数据时等待服务器返回响应的时间
  10. max.block.ms

    指定了在调用send()方法或使用partitionsFor()方法获取元数据时生产者的阻塞时间。达到设置时间时,会抛出超时异常。

  11. max.request.size

    用于控制生产者发送的请求大小。

  12. receive.buffer.bytes 和 send.buffer.bytes

    分别指定了TCP socket接收和发送数据包的缓冲区大小,设为-1,则使用操作系统默认值。

序列化器

在创建一个生产者对象时,必须指定序列化器。kafka默认提供了ByteArraySerializer、StringSerializer、IntegerSerializer三个序列化器,但是这并不能满足大部分的应用场景,所以需要开发自定义的序列化器;但是不推荐使用自定义序列化器,推荐使用已有的序列化器和反序列化器,如:JSON、Avro、Thrift或Protobuf。

接下来写一个自定义序列化器来了解序列化器工作原理:

  • 首先创建一个类来表示一个客户:
public class Customer {

    private int customerID;
    private String customerName;

    public Customer(int customerID, String customerName) {
        this.customerID = customerID;
        this.customerName = customerName;
    }

    public int getCustomerID() {
        return customerID;
    }

    public String getCustomerName() {
        return customerName;
    }
}
  • 为这个类创建一个序列化器:
import org.apache.kafka.common.serialization.Serializer;
import java.nio.ByteBuffer;
import java.util.Map;

public class CustomerSerializer implements Serializer<Customer> {
    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        // 不做配置
    }

    /**
     * Customer对象被序列化成:
     *   表示customerID的4字节整数
     *   表示customerName长度的4字节整数(customerName为null,则为0)
     *   表示customerName的N个字节
     * */
    @Override
    public byte[] serialize(String topic, Customer data) {
        try{
            byte[] serializerName;
            int stringSize;

            if (data == null) {
                return null;
            } else {
                if (data.getCustomerName() != null){
                    serializerName = data.getCustomerName().getBytes("UTF-8");
                    stringSize = serializerName.length;
                } else {
                    serializerName = new byte[0];
                    stringSize = 0;
                }
            }

            ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + stringSize);
            buffer.putInt(data.getCustomerID());
            buffer.putInt(stringSize);
            buffer.put(serializerName);
            
            return buffer.array();
        } catch (Exception e){
            e.printStackTrace();
        }
        
        return new byte[0];
    }

    @Override
    public void close() {
        // 不需要关闭任何东西
    }
}

分区

kafka消息是一个个键值对,ProducerRecord对象可以只包含目标主题和值,键可以设置为默认null;键有两个用途:

  1. 作为消息的附加信息
  2. 用来决定消息该被写到主题的哪个分区

kafka有两种默认的分区机制:

  1. 当键值为null,且使用了默认分区器;那么分区器将使用轮询算法将消息均衡的发布到各个分区上;
  2. 当键值不为null,且使用了默认分区器;那么kafka会对键进行散列,然后根据散列值把消息映射到特定的分区上;只有在不改变主题分区数量的情况下,键与分区之间的映射才能保持不变;

有时候我们也需要对数据进行不一样的分区,这时候就需要通过自定义分区策略来实现:

public class BananaPartitioner implements Partitioner {
    
    @Override
    public void configure(Map<String, ?> configs) {
    }
    
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, 
                         Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        
        if ((keyBytes == null) || (!(key instanceof String)))
            throw new InvalidRecordException("We expect all messages to hava customer name as key");
        
        if (key.equals("Banana"))
            return numPartitions; // Banana总是被分配到最后一个分区
        
        // 其他记录被散列到其他分区
        return (Math.abs(Utils.murmur2(keyBytes)) % (numPartitions - 1));
    }

    @Override
    public void close() {
    }
}

参考资料

Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2018.1

https://dy.163.com/article/EVG4T30T0511FQO9.html

https://blog.csdn.net/m0_37809146/article/details/91126212

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 下面是一个简单的 Flink 应用程序,将 Kafka 主题数据读取并写入到另一个 Kafka 主题: ```java import org.apache.flink.api.common.serialization.SimpleStringSchema; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer; import org.apache.flink.streaming.connectors.kafka.KafkaSerializationSchema; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringSerializer; import java.util.Properties; public class KafkaToFlinkToFlinkKafka { public static void main(String[] args) throws Exception { // set up the execution environment final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // configure Kafka consumer Properties consumerProps = new Properties(); consumerProps.setProperty("bootstrap.servers", "localhost:9092"); consumerProps.setProperty("group.id", "test-group"); // create a Kafka consumer data source FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>("input-topic", new SimpleStringSchema(), consumerProps); // read from Kafka DataStream<String> stream = env.addSource(consumer); // configure Kafka producer Properties producerProps = new Properties(); producerProps.setProperty("bootstrap.servers", "localhost:9092"); // create a Kafka producer sink FlinkKafkaProducer<String> producer = new FlinkKafkaProducer<>("output-topic", new KafkaSerializationSchema<String>() { @Override public ProducerRecord<byte[], byte[]> serialize(String s, Long aLong) { return new ProducerRecord<>("output-topic", s.getBytes()); } }, producerProps, FlinkKafkaProducer.Semantic.EXACTLY_ONCE); // write to Kafka stream.addSink(producer); // execute the Flink job env.execute("Kafka to Flink to Kafka"); } } ``` 在上面的代码,我们首先创建了一个 `StreamExecutionEnvironment` 对象,然后使用 Kafka 的消费者API读取一个名为 `input-topic` 的 Kafka 主题数据,并将其转换为 Flink 数据流。接下来,我们创建一个 Kafka 生产者,并将数据写入名为 `output-topic` 的 Kafka 主题。最后,我们调用 `env.execute()` 来启动 Flink 作业。 请注意,上面的代码只是一个简单的入门级示例。在生产环境,您可能需要更复杂的逻辑来处理数据并将其写入 Kafka 主题。 ### 回答2: Flink是一个流处理框架,它可以处理流式数据,并且可以与Kafka等消息队列相结合,实现数据的实时处理和分析。下面是一个使用Flink将Kafka数据写入Kafka的入门级示例。 首先,我们需要引入Flink和Kafka的相关依赖包,并创建一个Flink程序入口。 ``` import org.apache.flink.api.common.serialization.SimpleStringSchema; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.ProducerConfig; import java.util.Properties; public class KafkaToKafkaDemo { public static void main(String[] args) throws Exception { // 创建Flink程序入口 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 设置Kafka的相关配置 Properties properties = new Properties(); properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka1:9092,kafka2:9092,kafka3:9092"); properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "flink-consumer"); properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka1:9092,kafka2:9092,kafka3:9092"); // 创建Kafka消费者 FlinkKafkaConsumer<String> kafkaConsumer = new FlinkKafkaConsumer<>("input-topic", new SimpleStringSchema(), properties); // 添加Kafka消费者到Flink程序 DataStream<String> input = env.addSource(kafkaConsumer); // 创建Kafka生产者 FlinkKafkaProducer<String> kafkaProducer = new FlinkKafkaProducer<>("output-topic", new SimpleStringSchema(), properties); // 将输入数据写入Kafka input.addSink(kafkaProducer); // 执行Flink程序 env.execute("Kafka to Kafka Demo"); } } ``` 上述代码,我们首先创建了一个Flink程序入口,然后设置了连接Kafka所需的配置信息。之后,我们创建了一个Kafka消费者,并将其添加到Flink程序。接着,我们创建了一个Kafka生产者,并将输入的数据写入Kafka。最后,我们执行了Flink程序。 需要注意的是,在上述代码,我们需要将`kafka1:9092,kafka2:9092,kafka3:9092`替换为实际的Kafka集群地址,`input-topic`和`output-topic`替换为实际的输入和输出主题名称。 这是一个简单的入门级示例,演示了如何使用Flink将Kafka数据写入Kafka。你可以根据自己的需求,在此基础上进行更复杂的流处理操作。 ### 回答3: Flink是一个流计算引擎,可以用来处理大规模的实时数据流。而Kafka是一种高吞吐量的分布式消息队列,常用于构建数据流处理平台。那么如果想要将Kafka数据写入到另一个Kafka集群,可以使用Flink来实现。下面是一个入门级的示例代码,演示了如何使用Java编写一个简单的Flink作业来实现将Kafka数据写入到另一个Kafka集群。 首先,需要在项目的pom.xml文件添加Flink和Kafka相关的依赖: ```xml <dependencies> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-java_2.12</artifactId> <version>1.11.2</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <<artifactId>flink-connector-kafka_2.12</artifactId> <version>1.11.2</version> </dependency> </dependencies> ``` 接下来,可以编写一个简单的Flink作业,该作业从一个Kafka主题消费数据,并将其写入到另一个Kafka主题: ```java import org.apache.flink.api.common.serialization.SimpleStringSchema; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer; import org.apache.flink.streaming.connectors.kafka.internals.KafkaSerializationSchemaWrapper; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import java.util.Properties; public class KafkaToFlinkToFlinkKafkaDemo { public static void main(String[] args) throws Exception { // 创建执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 设置Kafka消费者的配置 Properties consumerProps = new Properties(); consumerProps.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka_source:9092"); consumerProps.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "flink-consumer"); // 创建一个从Kafka读取数据数据流 DataStream<String> kafkaSource = env .addSource(new FlinkKafkaConsumer<>("input_topic", new SimpleStringSchema(), consumerProps)); // 设置Kafka生产者的配置 Properties producerProps = new Properties(); producerProps.setProperty("bootstrap.servers", "kafka_target:9092"); // 创建一个写入Kafka数据流 // 注意:这里将消息序列化为字符串,所以消费者也需要使用相同的字符串序列化器来读取数据 DataStream<String> kafkaSink = kafkaSource .addSink(new FlinkKafkaProducer<>("output_topic", new KafkaSerializationSchemaWrapper<>(new SimpleStringSchema()), producerProps)); // 执行作业并等待任务完成 env.execute("Kafka to Flink to Kafka Demo"); } } ``` 在上述代码,首先通过FlinkKafkaConsumer创建一个从Kafka读取数据数据流,然后通过FlinkKafkaProducer创建一个将数据写入Kafka数据流。注意,需要为消费者和生产者配置正确的Kafka集群地址和主题名称。 以上就是一个简单的使用Flink将Kafka数据写入到另一个Kafka集群的示例。当然,实际应用还需要考虑更多的业务需求和数据转换操作。希望对您有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栖溪阁晓生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值