Kafka中的消息序列化和反序列化

Kafka生产者中的配置项key.serializervalue.serializer指示如何将用户通过其ProducerRecord提供的键和值对象转换为字节。对于简单的字符串或字节类型,可以使用包含的ByteArraySerializerStringSerializer进行序列化操作。

kafka在发送或者接收消息的时候实际是使用byte[]字节型数组进行传输的。但是我们平常使用的时候,不但可以使用byte[],还可以使用int、short、long、float、double、String等数据类型,这是因为在我们使用这些数据类型的时候,kafka根据我们指定的序列化和反序列化方式转成byte[]类型之后再进行传输来提高传输效率。

通常我们在使用kakfa发送或者接受消息的时候都需要指定消息的key和value序列化方式,如生产者我们可以设置value.serializerorg.apache.kafka.common.serialization.StringSerializer来设置value的序列化方式为字符串,即我们可以发送string类型的消息。目前kafka原生支持的序列化和反序列化方式如下两表所示,这些原生的序列化和反序列化的类都是在org.apache.kafka.common.serialization包之下:

kafka序列化方式表

序列化方式 对应java数据类型 说明
ByteArraySerializer byte[] 原生类型
ByteBufferSerializer ByteBuffer 关于ByteBuffer
IntegerSerializer Interger
ShortSerializer Short
LongSerializer Long
DoubleSerializer Double
StringSerializer String

kafka反序列化方式表

序列化方式 对应java数据类型 说明
ByteArrayDeserializer byte[] 原生类型
ByteBufferDeserializer ByteBuffer 关于ByteBuffer
IntegerDeserializer Interger
ShortDeserializer Short
LongDeserializer Long
DoubleDeserializer Double
StringDeserializer String

Java原生类型的序列化和反序列化

上面我们了解一些关于kafka原生的一些序列化和反序列化方式。它们究竟是如实现的呢?以string类型为例子,我们看一下,kafka如何实现序列化/反序列化的。

kafka序列化/反序列化方式的实现代码在org.apache.kafka.common.serialization包下。

String 类型的序列化类

我们查看org.apache.kafka.common.serialization.StringSerializer这个类。

package org.apache.kafka.common.serialization;

import org.apache.kafka.common.errors.SerializationException;

import java.io.UnsupportedEncodingException;
import java.util.Map;

/**
 *  String encoding defaults to UTF8 and can be customized by setting the property key.serializer.encoding,
 *  value.serializer.encoding or serializer.encoding. The first two take precedence over the last.
 */
public class StringSerializer implements Serializer<String> {
    private String encoding = "UTF8";

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        String propertyName = isKey ? "key.serializer.encoding" : "value.serializer.encoding";
        Object encodingValue = configs.get(propertyName);
        if (encodingValue == null)
            encodingValue = configs.get("serializer.encoding");
        if (encodingValue instanceof String)
            encoding = (String) encodingValue;
    }

    @Override
    public byte[] serialize(String topic, String data) {
        try {
            if (data == null)
                return null;
            else
                return data.getBytes(encoding);
        } catch (UnsupportedEncodingException e) {
            throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + encoding);
        }
    }

    @Override
    public void close() {
        // nothing to do
    }
}

由上面的代码我们可以看出:

  • String的序列化类是继承了Serializer接口,指定<String>泛型,然后实现的Serializer接口的configure()serialize()close()方法。代码重点的实现是在serialize(),可以看出这个方法将我们传入的String类型的数据,简单的通过data.getBytes(encoding)方法进行了序列化。

String 类新的反序列化类

我们查看org.apache.kafka.common.serialization.StringDeserializer这个类。

package org.apache.kafka.common.serialization;

import org.apache.kafka.common.errors.SerializationException;

import java.io.UnsupportedEncodingException;
import java.util.Map;

/**
 *  String encoding defaults to UTF8 and can be customized by setting the property key.deserializer.encoding,
 *  value.deserializer.encoding or deserializer.encoding. The first two take precedence over the last.
 */
public class StringDeserializer implements Deserializer<String> {
    private String encoding = "UTF8";

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        String propertyName = isKey ? "key.deserializer.encoding" : "value.deserializer.encoding";
        Object encodingValue = configs.get(propertyName);
        if (encodingValue == null)
            encodingValue = configs.get("deserializer.encoding");
        if (encodingValue instanceof String)
            encoding = (String) encodingValue;
    }

    @Override
    public String deserialize(String topic, byte[] data) {
        try {
            if (data == null)
                return null;
            else
                return new String(data, encoding);
        } catch (UnsupportedEncodingException e) {
            throw new SerializationException("Error when deserializing byte[] to string due to unsupported encoding " + encoding);
        }
    }

    @Override
    public void close() {
        // nothing to do
    }
}

同样,由上面的代码我们可以看出:

  • String类型的反序列化类是继承了Deserializer接口,指定<String>泛型,然后实现的Deserializer接口的configure()deserialize()close()方法。代码重点的实现是在deserialize(),可以看出这个方法将我们传入的byte[]类型的数据,简单的通过return new String(data, encoding)方法进行了反序列化得到了String类型的数据。

复杂Java对象的序列化及反序列化

通过上面,我们对kafka原生序列化/反序列化方式的了解,我们可以看出,kafka实现序列化/反序列化可以简单的总结为两步,第一步实现序列化Serializer或者反序列化Deserializer接口。第二步实现接口方法,将指定类型序列化成byte[]或者将byte[]反序列化成指定数据类型(String)。

graph LR
    生产者(String)--序列化-->传输和持久("Byte[]")--反序列化-->消费者(String)

由于原生类型不支持复杂对象类型,所以接下来,我们来实现对复杂对象类型的自定义序列化/反序列化方式。

这里我们介绍两种方式:

利用Buffer缓冲区

问题陈述:

Chapter 4 Activity 4.2

Kafka 消息也可以传递复杂对象类型。通常,这些对象将有多个字段。例如供应商对象。

实体类 Supplier

@Data
public class Supplier {
    private final int supplierId;
    private final String supplierName;
}

如果希望发送此类自定义对象或类型结构,则需要实现自定义序列化器和反序列化器。实现需要自定义序列化器和反序列化器。

解决方案 为了实现上述要求,利用缓冲区(Buffer,内存中预留指定大小的存储空间)用来对输入/输出(I/O)的数据作临时存储,这里由于需要保存字节数据所以我们使用ByteBuffer

自定义序列化类 SupplierSerializer

public class SupplierSerializer implements Serializer<Supplier> {
    private final String encoding = "UTF8";

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {

    }
    @Override
    public byte[] serialize(String topic, Supplier data) {

        int sizeOfName;
        byte[] serializedName;

        try {
            if (data == null) {
                return null;
            }
           // 根据 字符编码 转换成字节数组
            serializedName = data.getName().getBytes(encoding);
            // 通过sizeOfName可以帮助在反序列化的时候
            // 可以知道要取多少个字符来根据指定编码格式转换成回字符串
            sizeOfName = serializedName.length;
            ByteBuffer buf = ByteBuffer.allocate(4 + 4 + sizeOfName);

            buf.putInt(data.getID()); // supplierId 是int类型占4 bytes
            buf.putInt(sizeOfName);   // supplierName 长度
            buf.put(serializedName);  // supplierName 内容

            return buf.array();

        } catch (Exception e) {
            throw new SerializationException("Error when serializing Supplier to byte[]");
        }
    }

    @Override
    public void close() {
        // nothing to do
    }
}

自定义反序列化类 SupplierDeserializer

public class SupplierDeserializer implements Deserializer<Supplier> {
    private final String encoding = "UTF8";

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        //Nothing to configure
    }

    @Override
    public Supplier deserialize(String topic, byte[] data) {

        try {
            if (data == null) {
                System.out.println("Null recieved at deserialize");
                return null;
            }

            ByteBuffer buf = ByteBuffer.wrap(data);
            int id = buf.getInt(); // 获取 supplierId的 int类型的值

            // 获取 supplierName长度
            int sizeOfName = buf.getInt();
            // 创建相同长度的字节数组
            byte[] nameBytes = new byte[sizeOfName]; 

            // get(nameBytes) :
            //  从当前位置开始相对读,读nameBytes.length个byte,
            //  并写入dst下标从offset到offset+length的区域
            buf.get(nameBytes); // 根据获取 supplierName
            String deserializedName = new String(nameBytes, encoding);

            return new Supplier(id, deserializedName);

        } catch (Exception e) {
            throw new SerializationException("Error when deserializing byte[] to Supplier");
        }
    }

    @Override
    public void close() {
        // nothing to do
    }
}

JSON格式的序列化及反序列化

实体类 JsonData

import java.util.Date;
import java.util.Map;

// lombok
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonData {
    @JsonProperty("lng")
    private double longitude;
    @JsonProperty("lat")
    private double latitude;
    private double weight;
    private Date timestamp;
}

什么是 JSON ?

  • JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation)
  • JSON 是轻量级的文本数据交换格式,类似 XML
  • JSON 比 XML 更小、更快,更易解析。
  • JSON 独立于语言:JSON 使用 Javascript语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。 目前非常多的动态(PHP,JSP,.NET)编程语言都支持JSON。
  • JSON 具有自我描述性,更易理解

JSON 实例

{
    "sites": [    
        { "name":"NIIT" , "url":"www.niit.com.cn" },     
        { "name":"Google" , "url":"www.google.com" },     
        { "name":"WeiBo" , "url":"www.weibo.com" }    
    ]
}

这个 sites 对象是包含 3 个站点记录(对象)的数组。

语法:

JSON 键必须是字符串,字符串必须使用双引号包裹。

JSON 值可以是:

  • 数字(整数或浮点数)
  • 字符串(在双引号中)
  • 逻辑值(true 或 false)
  • 数组(在中括号中)
  • 对象(在大括号中)
  • null

JSON的Java解析库 - Jackson

市面有很多用于解析JSON的用Java编写的第三方库,比如Jackson(fasterxml,可靠、灵活、可定制,使用广泛),Gson(Google, 轻量、简洁),国内比较著名的有FastJson(Alibaba个人开源,特点是快,虽然很有多历史漏洞,但是作为国人还是要支持一下,可以越来越好),这些库的用法比较类似,这里的案例使用的是Jackson

Jackson的maven依赖

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.11.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.11.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.11.0</version>
</dependency>

基于Jackson的一个用于解析Json的工具类 JsonSerializerUtil

public class JsonSerializerUtil {

    /**
     * JSON序列化
     *
     * @param object 对象
     * @return JSON字符串
     */
    public static String serialize(Object object) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return "";
        }
    }

    /**
     * JSON字符串反序列化
     *
     * @param jsonStr JSON字符串
     * @return a Map
     */
    public static Map deserialize(String jsonStr) {
        try {
            return deserialize(jsonStr, Map.class);
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap();
        }
    }

    public static <T> T deserialize(String jsonStr, Class<T> classType) throws Exception {
        return new ObjectMapper().readValue(jsonStr, classType);
    }
}

自定义序列化类 JsonDataSerializer

public class JsonDataSerializer implements Serializer<JsonData> {

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {

    }

    @Override
    public byte[] serialize(String topic, JsonData data) {
        return JsonSerializerUtil.serialize(data).getBytes();
    }

    @Override
    public void close() {
        // nothing to do
    }
}

自定义反序列化类 JsonDataDeserializer

public class JsonDataDeserializer implements Deserializer<JsonData> {
    private final String encoding = "UTF8";

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        //Nothing to configure
    }

    @Override
    public JsonData deserialize(String topic, byte[] data) {

        if (data == null) {
            return null;
        }

        JsonData jsonData = null;

        try {
            String jsonString = new String(data, encoding);
            jsonData =  JsonSerializerUtil.deserialize(jsonString, JsonData.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return jsonData;
    }

    @Override
    public void close() {
        // nothing to do
    }
}

作业

LG Activity Exer 1

需要将书籍(Book) 的信息通过Kafka传输, 书籍的相关字段有 书名 (name), 订购数量(quantityOrdered),单价(unitPrice),请编写一个生产者发送书籍消息, 在编写一个消费者消费书籍消息。

可以自行选择序列化的方式。

总结

实现序列化还有很多比较成熟的第三方序列化库可以使用(如avro,protoBuff等),关于采用什么样的方式去序列化数据还需要根据业务场景自己去定义。

Views: 569

Kafka Consumer API

高级API

在控制台创建发送者

$ bin/kafka-console-producer.sh \
--broker-list hadoop000:9092 --topic first

>hello world

创建消费者(过时API)

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;

public class CustomConsumer {

  @SuppressWarnings("deprecation")
  public static void main(String[] args) {
     Properties properties = new Properties();
     properties.put("zookeeper.connect", "hadoop000:2181");
     properties.put("group.id", "g1");
     properties.put("zookeeper.session.timeout.ms", "500");
     properties.put("zookeeper.sync.time.ms", "250");
     properties.put("auto.commit.interval.ms", "1000");

     // 创建消费者连接器
     ConsumerConnector consumer = Consumer.createJavaConsumerConnector(new ConsumerConfig(properties));

     HashMap<String, Integer> topicCount = new HashMap<>();
     topicCount.put("first", 1);

     Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer.createMessageStreams(topicCount);

     KafkaStream<byte[], byte[]> stream = consumerMap.get("first").get(0);

     ConsumerIterator<byte[], byte[]> it = stream.iterator();

     while (it.hasNext()) {
       System.out.println(new String(it.next().message()));
     }
  }
}

官方提供案例(自动维护消费情况, 新API)

高级消费者和简单的消费者有以下的区别。

1.自动/隐藏偏移管理(Offset Management )

2.自动(简单)分区分配

3.Broker 故障转移 => 自动重新平衡

4.Consumer 故障转移 => 自动重新平衡

import java.util.Arrays;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

public class CustomNewConsumer {

  public static void main(String[] args) {

     Properties props = new Properties();
     // 定义kakfa 服务的地址,不需要将所有broker指定上 
     props.put("bootstrap.servers", "hadoop000:9092");
     // 制定consumer group 
     props.put("group.id", "test");
     // 是否自动确认offset 
     props.put("enable.auto.commit", "true");
     // 自动确认offset的时间间隔 
     props.put("auto.commit.interval.ms", "1000");
     // key的序列化类
     props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
     // value的序列化类 
     props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
     // 定义consumer 
     KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
     // 消费者订阅的topic, 可同时订阅多个 
     consumer.subscribe(Arrays.asList("first", "second","third"));

     while (true) {
       // 读取数据,读取超时时间为100ms 
       ConsumerRecords<String, String> records = consumer.poll(100);

       for (ConsumerRecord<String, String> record : records)
         System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
     }
  }
}

低级API

也叫Simple Consumer, 实际使用起来并不简单.

实现使用低级API读取指定topic,指定partition,指定offset的数据。

1)消费者使用低级API 的主要步骤:

步骤 主要工作
1 根据指定的分区从主题元数据中找到主副本
2 获取分区最新的消费进度
3 从主副本拉取分区的消息
4 识别主副本的变化,重试

2)方法描述:

findLeader() 客户端向种子节点发送主题元数据,将副本集加入备用节点
getLastOffset() 消费者客户端发送偏移量请求,获取分区最近的偏移量
run() 消费者低级AP I拉取消息的主要方法
findNewLeader() 当分区的主副本节点发生故障,客户将要找出新的主副本

3)代码:

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import kafka.api.FetchRequest;
import kafka.api.FetchRequestBuilder;
import kafka.api.PartitionOffsetRequestInfo;
import kafka.cluster.BrokerEndPoint;
import kafka.common.ErrorMapping;
import kafka.common.TopicAndPartition;
import kafka.javaapi.FetchResponse;
import kafka.javaapi.OffsetResponse;
import kafka.javaapi.PartitionMetadata;
import kafka.javaapi.TopicMetadata;
import kafka.javaapi.TopicMetadataRequest;
import kafka.javaapi.consumer.SimpleConsumer;
import kafka.message.MessageAndOffset;

public class SimpleExample {
  private List<String> m_replicaBrokers = new ArrayList<>();

  public SimpleExample() {
    m_replicaBrokers = new ArrayList<>();
  }

  public static void main(String args[]) {
    SimpleExample example = new SimpleExample();
    // 最大读取消息数量
    long maxReads = Long.parseLong("3");
    // 要订阅的topic
    String topic = "test1";
    // 要查找的分区
    int partition = Integer.parseInt("0");
    // broker节点的ip
    List<String> seeds = new ArrayList<>();
    seeds.add("192.168.9.102");
    seeds.add("192.168.9.103");
    seeds.add("192.168.9.104");
    // 端口
    int port = Integer.parseInt("9092");
    try {
      example.run(maxReads, topic, partition, seeds, port);
    } catch (Exception e) {
      System.out.println("Oops:" + e);
      e.printStackTrace();
    }
  }

  public void run(long a_maxReads, String a_topic, int a_partition, List<String> a_seedBrokers, int a_port) throws Exception {

    // 获取指定Topic partition的元数据
    PartitionMetadata metadata = findLeader(a_seedBrokers, a_port, a_topic, a_partition);

    if (metadata == null) {
      System.out.println("Can't find metadata for Topic and Partition. Exiting");
      return;
    }

    if (metadata.leader() == null) {
      System.out.println("Can't find Leader for Topic and Partition. Exiting");
      return;
    }

    String leadBroker = metadata.leader().host();
    String clientName = "Client" + a_topic + "" + a_partition;

    SimpleConsumer consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName);
    long readOffset = getLastOffset(consumer, a_topic, a_partition, kafka.api.OffsetRequest.EarliestTime(), clientName);
    int numErrors = 0;

    while (a_maxReads > 0) {
      if (consumer == null) {
        consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName);

      }

      FetchRequest req = new FetchRequestBuilder().clientId(clientName).addFetch(a_topic, a_partition, readOffset, 100000).build();

      FetchResponse fetchResponse = consumer.fetch(req);

      if (fetchResponse.hasError()) {
        numErrors++;
        // Something went wrong!
        short code = fetchResponse.errorCode(a_topic, a_partition);
        System.out.println("Error fetching data from the Broker:" + leadBroker + " Reason: " + code);

        if (numErrors > 5)
          break;
        if (code == ErrorMapping.OffsetOutOfRangeCode()) {
          // We asked for an invalid offset. For simple case ask for
          // the last element to reset
          readOffset = getLastOffset(consumer, a_topic, a_partition, kafka.api.OffsetRequest.LatestTime(), clientName);
          continue;
        }

        consumer.close();
        consumer = null;
        leadBroker = findNewLeader(leadBroker, a_topic, a_partition, a_port);
        continue;
      }

      numErrors = 0;

      long numRead = 0;
      for (MessageAndOffset messageAndOffset : fetchResponse.messageSet(a_topic, a_partition)) {
        long currentOffset = messageAndOffset.offset();
        if (currentOffset < readOffset) {
          System.out.println("Found an old offset: " + currentOffset + " Expecting: " + readOffset);
          continue;
        }

        readOffset = messageAndOffset.nextOffset();
        ByteBuffer payload = messageAndOffset.message().payload();

        byte[] bytes = new byte[payload.limit()];
        payload.get(bytes);
        System.out.println(String.valueOf(messageAndOffset.offset()) + ": " + new String(bytes, "UTF-8"));
        numRead++;
        a_maxReads--;
      }

      if (numRead == 0) {
        try {
          Thread.sleep(1000);
        } catch (InterruptedException ie) {
        }
      }
    }

    if (consumer != null)
      consumer.close();
  }

  public static long getLastOffset(SimpleConsumer consumer, String topic, int partition, long whichTime, String clientName) {

    TopicAndPartition topicAndPartition = new TopicAndPartition(topic, partition);

    Map<TopicAndPartition, PartitionOffsetRequestInfo> requestInfo = new HashMap<TopicAndPartition, PartitionOffsetRequestInfo>();

    requestInfo.put(topicAndPartition, new PartitionOffsetRequestInfo(whichTime, 1));

    kafka.javaapi.OffsetRequest request = new kafka.javaapi.OffsetRequest(requestInfo, kafka.api.OffsetRequest.CurrentVersion(), clientName);

    OffsetResponse response = consumer.getOffsetsBefore(request);

    if (response.hasError()) {
      System.out.println("Error fetching data Offset Data the Broker. Reason: " + response.errorCode(topic, partition));
      return 0;
    }
    long[] offsets = response.offsets(topic, partition);
    return offsets[0];
  }

  private String findNewLeader(String a_oldLeader, String a_topic, int a_partition, int a_port) throws Exception {

    for (int i = 0; i < 3; i++) {
      boolean goToSleep = false;
      PartitionMetadata metadata = findLeader(m_replicaBrokers, a_port, a_topic, a_partition);
      if (metadata == null) {
        goToSleep = true;
      } else if (metadata.leader() == null) {
        goToSleep = true;
      } else if (a_oldLeader.equalsIgnoreCase(metadata.leader().host()) && i == 0) {
        // first time through if the leader hasn't changed give
        // ZooKeeper a second to recover
        // second time, assume the broker did recover before failover,
        // or it was a non-Broker issue

        goToSleep = true;
      } else {
        return metadata.leader().host();
      }

      if (goToSleep) {
           Thread.sleep(1000);
      }
    }
    System.out.println("Unable to find new leader after Broker failure. Exiting");
    throw new Exception("Unable to find new leader after Broker failure. Exiting");
  }

  private PartitionMetadata findLeader(List<String> a_seedBrokers, int a_port, String a_topic, int a_partition) {
    PartitionMetadata returnMetaData = null;

    loop:
    for (String seed : a_seedBrokers) {
      SimpleConsumer consumer = null;

      try {
        consumer = new SimpleConsumer(seed, a_port, 100000, 64 * 1024, "leaderLookup");
        List<String> topics = Collections.singletonList(a_topic);
        TopicMetadataRequest req = new TopicMetadataRequest(topics);
        kafka.javaapi.TopicMetadataResponse resp = consumer.send(req);
        List<TopicMetadata> metaData = resp.topicsMetadata();

        for (TopicMetadata item : metaData) {
          for (PartitionMetadata part : item.partitionsMetadata()) {
            if (part.partitionId() == a_partition) {
              returnMetaData = part;
               break loop;
            }
          }
        }
      } catch (Exception e) {
        System.out.println("Error communicating with Broker [" + seed + "] to find Leader for [" + a_topic + ", " + a_partition + "] Reason: " + e);
      } finally {
        if (consumer != null)
          consumer.close();
      }
    }

    if (returnMetaData != null) {
      m_replicaBrokers.clear();
      for (BrokerEndPoint replica : returnMetaData.replicas()) {
        m_replicaBrokers.add(replica.host());
      }
    }
    return returnMetaData;
  }
}

Views: 569

Kafka的消息存储

为了便于说明问题,假设这里只有一个单节点的伪分布式Kafka集群。在这个Kafka broker实例的$KAFKA_HOME/config/server.properties中配置 log.dirs=/tmp/kafka-logs,以此来设置 Kafka 消息文件存储目录。并通过命令:

$KAFKA_HOME/bin/kafka-topics.sh --create --zookeeper localhost:2181 --partitions 4 --topic topic_test --replication-factor 1

创建一个 topic:topic_test,partition的数量配置为4。接下来可以在 /tmp/kafka-logs 目录中可以看到生成了 4 个partition目录:

drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_test-0
drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_test-1
drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_test-2
drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_test-3

在Kafka文件存储中,同一个topic下有多个不同的partition,每个partiton为一个目录,partition的名称规则为topic名称+有序序号,第一个序号从0开始计,最大的序号为partition数量减1,类似数组的索引, partition是实际物理上的概念,而topic是逻辑上的概念,更多表象是一个消息的类别,当然,partition还可以细分为segment(文件),一个partition物理上由多个segment组成.

为什么不能以partition 作为存储单位?

如果就以partition 为最小存储单位,可以想象,当Kafka producer不断发送消息,必然会引起partition文件的无限扩张,将对消息文件的维护以及已消费的消息的清理带来严重的影响,新数据是在文件尾部追加的,不论文件数据文件有多大,这个操作永远都是 O(1)的查找,再者,查找某个offset的Message是顺序查找的。因此,如果数据文件很大的话,查找的效率就低. 因此,需以segment 为单位将partition进一步细分。每个partition相当于一个巨型文件被平均分配到多个大小相等的segment数据文件中(每个segment文件中消息数量不一定相等)这种特性也方便old segment的删除,即方便已被消费的消息的清理,提高磁盘的利用率。每个partition只需要支持顺序读写就行.segment的文件生命周期由服务端配置参数log.segment.byteslog.roll.{ms,hours} 等相关参数决定。

segment文件由两部分组成,分别为 .index文件和.log文件,分别表示为segment索引文件和数据文件。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值,数值大小为64位,20位数字字符长度,没有数字用0填充,如下:

00000000000000000000.index
00000000000000000000.log
00000000000000000000.timeindex
00000000000000170410.index
00000000000000170410.log
00000000000000170410.timeindex
00000000000000239430.index
00000000000000239430.log
00000000000000239430.timeindex

以上面的segment文件为例,展示出segment中的00000000000000170410.index文件和00000000000000170410.log文件是存在对应关系的,.index索引文件存储大量的元数据,.log数据文件存储大量的消息,索引文件中的元数据指向对应数据文件中message的物理偏移地址(也就是实际的偏移地址,因为会涉及到segment文件清理)。其中以.index索引文件中的元数据[3, 348]为例,在.log数据文件表示第3个消息,该消息的物理偏移地址为348。

如何保证消息消费的有序性呢?

举个例子,比如说生产者生产了25个订单,订单假设分为创建-提交-付款-发货4个步骤,那么消费者在消费的时候按照0到100这个从小到大的顺序消费,那么kafka如何保证这种有序性呢?

难度就在于,生产者生产出0到100这100条数据之后,通过一定的分组策略存储到broker的partition中的时候,比如0到10这10条消息被存到了这个partition中,10到20这10条消息被存到了那个partition中,这样的话,消息在分组存到partition中的时候就已经被分组策略搞得无序了。

那么能否做到消费者在消费消息的时候全局有序呢?遇到这个问题,我们可以回答,在大多数情况下是做不到全局有序的。但在某些情况下是可以做到的。比如我的partition只有一个,这种情况下是可以全局有序的。

那么可能有人又有疑问了,只有一个partition的话,哪里来的分布式呢?哪里来的负载均衡呢?所以说,全局有序是一个伪命题!让订单全局有序根本没有办法在kafka内部做到。但是我们只能保证当前这个partition内部消息消费的有序性。

针对一个topic里面的数据,只能做到partition内部有序,不能做到全局有序。

当然不使用一个partition我们可以从代码层面解决.

如何从partition中通过offset查找message?

以上图为例,读取offset=170418的消息,首先查找segment文件,其中00000000000000000000.index为最开始的文件,第二个文件为00000000000000170410.index(起始偏移为170410),而第三个文件为00000000000000239430.index(起始偏移为239430),所以这个offset=170418就落到了第二个文件之中。其他后续文件可以依次类推,以其实偏移量命名并排列这些文件,然后根据二分查找法就可以快速定位到具体文件位置。其次根据00000000000000170410.index文件中的[8,1325]定位到00000000000000170410.log文件中的1325的位置进行读取。

怎么知道何时读完本条消息,否则就读到下一条消息的内容了?
这个问题就得引出kafka的消息结构,如下图所示:

file

消息都具有固定的物理结构,包括:offset(8 Bytes)、消息体的大小(4 Bytes)、crc32(4 Bytes)、magic(1 Byte)、attributes(1 Byte)、key length(4 Bytes)、key(K Bytes)、payload(N Bytes)等等字段,可以确定一条消息的大小,即读取到哪里截止。

总结,offset的查找方机制是建立在offset是有序的,索引文件被映射到内存中,所以查找的速度还是很快的。

另外,Kafka的Message存储采用了分区(partition),Segment和index这几个手段来达到了高效性。

Views: 602

Kafka如何保证无消息丢失

配置Kafka无消息丢失

Kafka 只对“已提交”的消息(committed message)做有限度的持久化保证。Kafka 的 一个Broker或多个Broker 成功地接收到一条消息并写入到日志文件后,它们会告诉生产者程序这条消息已成功提交,具体是一个Broker还是多个Broker取决ack参数的配置。

要想要消息不丢失,假如你的消息保存在 N 个 Kafka Broker 上,那么这个前提条件就是这 N 个 Broker 中至少有 1 个存活。

目前 Kafka Producer 的send方法是异步发送消息的,也就是说如果你调用的是 producer.send(msg) 这个 API,那么它通常会立即返回,消息会异步继续发送到Broker中,在这个过程中是有可能失败的,比如网络抖动导致发送失败,比如消息格式不正确等导致Broker拒绝消息。

解决方法:Producer 要使用带有回调通知的发送 API,也就是说不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。

通过callback(回调),它能准确地告诉你消息是否真的提交成功了。一旦出现消息提交失败的情况,你就可以有针对性地进行处理。举例来说,如果是因为那些瞬时错误,那么仅仅让 Producer 重试就可以了;如果是消息不合格造成的,那么可以调整消息格式后再次发送。总之,处理发送失败的责任在 Producer 端而非 Broker 端。

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class ProducerWithCallBackExample {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        final Logger logger = LoggerFactory.getLogger(ProducerWithCallBackExample.class);

        //Create producer properties
        Properties properties = new Properties();
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop000:9092");
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //Create the Producer
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);

        //Send 10 messages
        for (int i = 0; i < 10; i++) {
            //Create a Producer Record
            ProducerRecord<String, String> record = new ProducerRecord<String, String>("my-topic", "id_" + i, "Hello Kakfka" + i);

            logger.info("Key :" + "id_" + i); //Log the Key
            //send data
            producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null) {
                        logger.info("Received new metadata. \n" +
                                "Topic:" + recordMetadata.topic() + "\n" +
                                "Pratition:" + recordMetadata.partition() + "\n" +
                                "Offeset:" + recordMetadata.offset() + "\n" +
                                "Timestamp" + recordMetadata.timestamp());
                    } else {
                        logger.error("Error while producing", e);
                    }
                }
            });
           producer.flush();
        }
        producer.close();
    }
}

最佳实践

分享一下 Kafka 无消息丢失的配置的最佳实践:

  1. 不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。记住,一定要使用带有回调通知的 send 方法。
  2. 设置 acks = all。acks 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。
  3. 设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。
  4. 设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。
  5. 设置 replication.factor >= 3。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。
  6. 设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。
  7. 确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1
  8. 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关重要的。

Views: 303

Kafka Producer API

http://kafka.apache.org/25/documentation.html#api

Kafka 架构回顾

image-20211011145945791

1)Producer :消息生产者,就是向kafka broker发消息的客户端;

2)Consumer :消息消费者,向kafka broker取消息的客户端;

3)Topic :可以理解为一个队列;

4) Consumer Group (CG):这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制(不是真的复制,是概念上的)到所有的CG,但每个partion只会把消息发给该CG中的一个consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic;

5)Broker :一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic;

6)Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序;

7)Offset:kafka的存储文件都是按照offset.kafka来命名,用offset做名字的好处是方便查找。例如你想找位于2049的位置,只要找到2048.kafka的文件即可。当然the first offset就是00000000000.kafka。

添加 maven 依赖

Producer API可以让应用程序发送数据流到Kafka集群的消息主题中。

Producer的使用可以查看官方文档 javadocs.

http://kafka.apache.org/25/javadoc/index.html?org/apache/kafka/clients/producer/KafkaProducer.html

要使用Producer可以添加以下Maven依赖:

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.5.0</version>
</dependency>

Producer的简单使用

public class KafkaProducer<K, V> implements Producer<K, V>

KafkaProducer是Kafka客户端,用于将一条消息(记录)发布到Kafka集群。

KafkaProducer是线程安全的,跨线程共享单个Producer实例通常比拥有多个Producer实例更快。

下面是一个使用Producer发送消息的简单示例,该记录使用包含0到99的数字序列的字符串作为键/值对。

 Properties props = new Properties();
 props.put("bootstrap.servers", "localhost:9092");
 props.put("acks", "all");
 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
 props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

 Producer<String, String> producer = new KafkaProducer<>(props);
 for (int i = 0; i < 100; i++)
     producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));

 producer.close();

注意这里send()发送消息是异步的.

Producer 生产过程

写入方式

producer采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障kafka吞吐率)。

分区(Partition)

消息发送时都被发送到一个topic,其本质就是一个目录,而topic是由一些Partition Logs(分区日志)组成,其组织结构如下图所示:

image-20211011150538034

我们可以看到,每个Partition中的消息都是有序的,生产的消息被不断追加到Partition log上,其中的每一个消息都被赋予了一个唯一的offset值。

如何唯一标识一个消息: 主题->分区->偏移量

1)分区的原因

(1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;

(2)可以提高并发,因为可以以Partition为单位读写了。

2)分区的原则

(1)指定了patition,则直接使用;

(2)未指定patition但指定key,通过对key的value进行hash出一个patition;

(3)patition和key都未指定,使用轮询选出一个patition。

DefaultPartitioner class {

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) {

      int nextValue = nextValue(topic);

      List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);

      if (availablePartitions.size() > 0) {

        int part = Utils.toPositive(nextValue) % availablePartitions.size();

        return availablePartitions.get(part).partition();

      } else {

        // no partitions are available, give a non-available partition

        return Utils.toPositive(nextValue) % numPartitions;

      }

    } else {

      // hash the keyBytes to choose a partition

      return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;

    }

  }

}

副本(Replication)

同一个partition可能会有多个replication(对应 server.properties 配置中的 default.replication.factor=N)。没有replication的情况下,一旦broker 宕机,其上所有 patition 的数据都不可被消费,同时producer也不能再将数据存于其上的patition。引入replication之后,同一个partition可能会有多个replication,而这时需要在这些replication之间选出一个leader,producer和consumer只与这个leader交互,其它replication作为follower从leader 中复制数据。

消息写入流程

image-20211011150947554

producer写入消息流程如下:

1)producer先从zookeeper的 "/brokers/.../state"节点找到该partition的leader

2)producer将消息发送给该leader

3)leader将消息写入本地log

4)followers从leader pull消息,写入本地log后向leader发送ACK

5)leader收到所有ISR中的replication的ACK后,增加HW(high watermark,最后commit 的offset)并向producer发送ACK

Broker 保存消息

存储方式

物理上把topic分成一个或多个patition(对应 server.properties 中的num.partitions=3配置),每个patition物理上对应一个文件夹(该文件夹存储该patition的所有消息和索引文件),如下:

[hadoop@hadoop100 logs]$ ll

drwxrwxr-x. 2 hadoop hadoop 4096 8月  6 14:37 first-0
drwxrwxr-x. 2 hadoop hadoop 4096 8月  6 14:35 first-1
drwxrwxr-x. 2 hadoop hadoop 4096 8月  6 14:37 first-2

[hadoop@hadoop100 logs]$ cd first-0

[hadoop@hadoop100 first-0]$ ll

-rw-rw-r--. 1 hadoop hadoop 10485760 8月  6 14:33 00000000000000000000.index
-rw-rw-r--. 1 hadoop hadoop   219 8月  6 15:07 00000000000000000000.log
-rw-rw-r--. 1 hadoop hadoop 10485756 8月  6 14:33 00000000000000000000.timeindex
-rw-rw-r--. 1 hadoop hadoop    8 8月  6 14:37 leader-epoch-checkpoint

存储策略

无论消息是否被消费,kafka都会保留所有消息。有两种策略可以删除旧数据:

1)基于时间:log.retention.hours=168

2)基于大小:log.retention.bytes=1073741824

需要注意的是,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。

Zookeeper存储结构

image-20211018163259464

注意:producer不在zk中注册,消费者在zk中注册。

producer 包括一个保存尚未传输到服务器的消息(record)的缓冲区空间池,以及一个负责将这些记录转换为请求并将其传输到集群的后台I/O线程。producer使用后未及时关闭将导致这些资源泄漏。

send()方法是异步的,调用send()方法会将待发送消息(record)添加到缓冲区等待发送,并立即返回。生产者会积攒一批消息后批量发送以提高效率。

acks配置项用来控制请求如何被认为已处理完成。我们指定的“all”设置将导致阻塞全部记录提交,这是最慢但最持久的设置。

如果请求失败,生产者可以自动重试,除非配置了retries配置项为0。启用重试可能导致出现重复消息的可能性(有关消息传递语义的详细信息,请参阅文档)。

Producer 在每个分区维都维护未发送记录的缓冲区。这些缓冲区的大小由batch.size配置项进行配置,增大这个配置的值可以允许一个批次处理更多消息,但需要更多的内存(因为我们通常会为每个活动分区拥有一个这样的缓冲区)。

默认情况下,即使缓冲区中有额外的未使用空间,缓冲数据也是可以立即发送出去的。然而如果你想减少请求的数量可以设置linger.ms为一个大于0的数, 这可以让Producer发送请求之前等待该毫秒数,这样可以有更多消息在一个批次中批量发送。

在上面的代码片段中,可能会在一个请求中发送所有100条记录,因为我们将延迟时间设置为1毫秒。但是,如果我们没有填满缓冲区,这个设置会给请求增加1毫秒的延迟,以等待更多的记录填充到缓冲区。因此在负载大的情况下,无论是否配置linger.ms,消息都是通过批处理发送的;将linger.ms的值设置为大于0的值的意义在于,在没有处于最大负载的情况下可以有效减少请求数量, 代价是增加一点点延迟。

配置项buffer.memory 用来控制Producer用于缓冲区的可用内存总量。如果消息发送到服务器的速度大于缓冲区耗尽的速度,当缓冲区空间耗尽时,其他消息的发送将被阻塞,阻塞时间的阈值由max.block决定。之后,它会抛出一个TimeoutException异常。

配置项 key.serializervalue.serializer 用来配置键值对象的序列化类, 对于简单的字符串或字节类型,可以使用Kafka自带的ByteArraySerializer或StringSerializer。

从Kafka 0.11这个版本开始,KafkaProducer支持另外两种模式:幂等Producer和事务Producer。幂等Producer加强了卡夫卡的交付语义,从至少一次交付(at least once delivery)加强为准确地一次交付(exactly once delivery), 幂等Producer的消息无论失败重试多少次都不会导致数据重复统计。而事务Producer是以原子操作将消息发送到多个分区(和主题!),即这些操作要么同时成功, 要么同时失败!.

若要启用等幂性(idempotence),则 enable.idempotence配置必须设置为true。此时retries配置值将默认为Integer.MAX_VALUE, 而acks配置默认为all。使用幂等性Producer在API上没有变化,因此不需要修改现有应用程序。

一旦启用了幂等性(idempotence),建议不要配置重试(retries)参数,因为它默认值为Integer.MAX_VALUE。另外,如果send(ProducerRecord)方法在进行无限次重试之后仍然返回错误(例如消息在缓冲区中过期),那么建议关闭Producer并检查最后生成的消息的内容,以确保没有重复消息。最后,生产者只能保证在单个会话中发送的消息的幂等性。

过时的Producer API

一些公司可能因为历史原因, 仍然在使用旧的(过时的)API.

pom.xml

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka_2.11</artifactId>
    <version>0.10.0.1</version>
</dependency>

简单代码示例

BasicConfigurator.configure();
Properties properties = new Properties();
properties.put("metadata.broker.list", "hadoop000:9092");
properties.put("request.required.acks", "1");
properties.put("serializer.class", "kafka.serializer.StringEncoder");

Producer<Integer, String> producer = new Producer<>(new ProducerConfig(properties));

KeyedMessage<Integer, String> message = new KeyedMessage<>("t1", "hello world");
producer.send(message);

过时的生产者API不支持幂等性和事务.

复杂代码示例

自定义调度器代码

package com.niit.kafka.producer.newapi;

/**
 * @Author: deLucia
 * @Date: 2021/10/7
 * @Version: 1.0
 * @Description:
 * 使用更多配置项的生产者
 * 启动自定义分区: 不管有多少分区,只写到分区0
 * 先开启消费者, 运行程序, 并测试 tail -F my-topic-3-0/00000000000000000000.log
 */

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

import java.util.Map;

public class CustomPartitioner 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) {
        // 控制分区
        return 0;
    }

    @Override
    public void close() {

    }
}

生产者代码


/**
 * @Author: deLucia
 * @Date: 2021/10/6
 * @Version: 1.0
 * @Description: old api from kafka_2.11
 */

import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
import org.apache.kafka.common.utils.Utils;
import org.apache.log4j.BasicConfigurator;

import java.util.Properties;

public class DetailedProducerExample {

    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        BasicConfigurator.configure();
        Properties props = new Properties();
        // Kafka服务端的主机名和端口号
        props.put("metadata.broker.list", "hadoop100:9092");
        // 等待所有副本节点的应答
        props.put("request.required.acks", "0");
        // 序列化类
        props.put("serializer.class", "kafka.serializer.StringEncoder");
        // partitioner
        //      - default: kafka.producer.DefaultPartitioner
        //          - hash(key)%partitionNum)
        //      - custom partitioner:
        //          - send to partition #0
        props.put("partitioner.class", CustomPartitioner.class.getName());

        Producer<String, String> producer = new Producer<>(new ProducerConfig(props));

        for (int i = 0; i < 10; i++) {
            KeyedMessage<String, String> message = new KeyedMessage<>("my-topic-3", "hello world " + i);
            producer.send(message);
            Utils.sleep(1000);
        }
        producer.close();
    }
}

Producer 配置参数

所有producer的配置参阅官网

同步发送结合自定义调度器

自定义调度器

package com.niit.kafka.producer.newapi;

/**
 * @Author: deLucia
 * @Date: 2021/10/7
 * @Version: 1.0
 * @Description:
 * 使用更多配置项的生产者
 * 启动自定义分区: 不管有多少分区,只写到分区0
 * 先开启消费者, 运行程序, 并测试 tail -F my-topic-3-0/00000000000000000000.log
 */

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

import java.util.Map;

public class CustomPartitioner 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) {
        // 控制分区
        return 0;
    }

    @Override
    public void close() {

    }
}

生产者代码

package com.niit.kafka.producer.newapi;

/**
 * @Author: deLucia
 * @Date: 2020/8/22
 * @Version: 1.0
 * @Description: new Producer APi from kafka_client
 * STEPS:
    create topic t1 with multiple partitioners.
    bin/kafka-console-consumer.sh --bootstrap-server hadoop000:9092,hadoop000:9093,hadoop000:9094 --topic t1
    $ tail -F t1-0/0000000000000000000X.log
 */

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.log4j.BasicConfigurator;
import org.apache.kafka.clients.producer.internals.DefaultPartitioner;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class DetailedProducerExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        BasicConfigurator.configure();

        // Create producer properties
        Properties properties = new Properties();
        //      - kafka clustger
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop000:9092,hadoop000:9093,hadoop000:9094");
        //      - ack level
        //              no wait = 0
        //              wait leader  = 1
        //              wait all     = -1
        properties.setProperty(ProducerConfig.ACKS_CONFIG, "all");
        //      - retries
        properties.setProperty(ProducerConfig.RETRIES_CONFIG, "0");
        //      - batch.size
        properties.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "16384");
        //      - linger.ms
        properties.setProperty(ProducerConfig.LINGER_MS_CONFIG, "1");
        //      - buffer.memory
        properties.setProperty(ProducerConfig.BUFFER_MEMORY_CONFIG, "33554421");
        //      - KV serializer classes
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //      - Defauilt Partitioner
        //           default: org.apache.kafka.clients.producer.internals.DefaultPartitioner;
        //              - 如果指定分区则使用指定的分区
        //              - 如果没有指定分区但是指定key, 则计算哈希取模得到分区索引
        //              - 如果没有指定分区也没有指定key, 则采用轮询算法
        //      - Custom Partitioner
        //              - 总是发送到分区0上
        properties.setProperty(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomPartitioner.class.getName());
        // Create the Producer
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);

        // send data
        for (int i = 0; i < 10; i++) {
            // Create a Producer Record
            ProducerRecord<String, String> record = new ProducerRecord<>("my-topic-3", String.valueOf(i));
            RecordMetadata recordMetadata = producer.send(record).get();// 同步发送
            int partition = recordMetadata.partition();
            long offset = recordMetadata.offset();
            System.out.println("Partition[" + partition + "], offset[" + offset + "], message[" + i + "]");

// producer.send(record); // 异步发送
            producer.flush();
        }

        producer.close();
    }
}

异步发送结合回调

这里回调采用使用匿名内部类的方式:

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class ProducerWithCallBackExample {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        final Logger logger = LoggerFactory.getLogger(ProducerWithCallBackExample.class);

        //Create producer properties
        Properties properties = new Properties();
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop000:9092");
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //Create the Producer
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);

        //Send 10 messages
        for (int i = 0; i < 10; i++) {
            //Create a Producer Record
            ProducerRecord<String, String> record = new ProducerRecord<String, String>("my-topic", "id_" + i, "Hello Kakfka" + i);

            logger.info("Key :" + "id_" + i); //Log the Key
            //send data
            producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null) {
                        logger.info("Received new metadata. \n" +
                                "Topic:" + recordMetadata.topic() + "\n" +
                                "Pratition:" + recordMetadata.partition() + "\n" +
                                "Offeset:" + recordMetadata.offset() + "\n" +
                                "Timestamp" + recordMetadata.timestamp());
                    } else {
                        logger.error("Error while producing", e);
                    }
                }
            });
           producer.flush();
        }
        producer.close();
    }
}

事务Producer

客户端能与之交互的Broker上安装Kafka版本要至少0.10.0。有些代理服务器可能不支持某些客户端特性。例如,事务api需要Kafka版本至少0.11.0。当使用的代码和实际版本不符合时将收到unsupportedVersionException异常。

要使用事务Producer API,必须设置 transactional.id 属性, 设置transactional.id可用来在单个Producer的实例跨多个会话时方便进行事务的恢复(回滚操作)。只要 transactional.id 设置好后,幂等性所依赖的所有相关的Producer配置就会自动生效。此外,事务中包含的主题应该做好持久性设置。特别是replication.factor 应该至少是3,主题的min.insync.replicas 应设为2。最后,为了从端到端实现事务保证,还必须将使用者的事务隔离级别配置为只读取提交(read only committed messages)。

所有新的事务api都是阻塞的,一旦失败就会抛出异常。下面的示例说明了如何使用新的api。它与上面的示例类似,只是100条消息都是属于一个事务(要么同时成功,要么同时失败)。

 Properties props = new Properties();
 props.put("bootstrap.servers", "localhost:9092");
 props.put("transactional.id", "my-transactional-id");
 Producer<String, String> producer = new KafkaProducer<>(props, new StringSerializer(), new StringSerializer());

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

 try {
     // 事务开始
     producer.beginTransaction();
     for (int i = 0; i < 100; i++)
         producer.send(new ProducerRecord<>("my-topic", Integer.toString(i), Integer.toString(i)));
     producer.commitTransaction();
 } catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
     // We can't recover from these exceptions, so our only option is to close the producer and exit.
     producer.close();
 } catch (KafkaException e) {
     // For all other exceptions, just abort the transaction and try again.
     // 退出事务
     producer.abortTransaction();
 }
 // 关闭生产者
 producer.close();

正如示例中所暗示的,每个生产者只能有一个开放的事务。所有在beginTransaction()commitTransaction()调用之间发送的消息都是单个事务的一部分。如果指定了事务id,则生产者发送的所有消息必须是事务的一部分。

事务Producer通过抛出异常来传递错误状态。因此不需要为producer.send()指定回调,也不需要调用producer.send().get()来返回将来执行的结果. 如果任何producer.send()或事务调用在事务期间遇到不可恢复的错误,就会抛出KafkaException。有关从事务发送中检测错误的详细信息,请参阅文档 send(ProducerRecord) 。通过在接收到KafkaException时调用producer.abortTransaction(),从而保证事务性。

Views: 498

Index