【问题标题】:Kafka partition key not working properly‏Kafka 分区键无法正常工作
【发布时间】:2015-02-07 00:42:17
【问题描述】:

我正在为如何正确使用分区键机制而苦恼。我的逻辑是设置分区号为3,然后创建三个分区键为“0”、“1”、“2”,然后使用分区键创建三个KeyedMessage如

  • KeyedMessage(主题,“0”,消息)
  • KeyedMessage(主题,“1”,消息)
  • KeyedMessage(主题,“2”,消息)

在此之后,创建一个生产者实例以发送所有 KeyedMessage。

我希望每个 KeyedMessage 应该根据不同的分区键进入不同的分区,这意味着

  • KeyedMessage(topic, "0", message) 转到分区 0
  • KeyedMessage(topic, "1", message) 转到分区 1
  • KeyedMessage(topic, "2", message) 转到分区 2

我正在使用 Kafka-web-console 来查看主题状态,但结果与我期望的不一样。 KeyedMessage 仍然会随机进入分区,有时两个 KeyedMessage 会进入同一个分区,即使它们有不同的分区键。

为了让我的问题更清楚,我想发布一些我目前拥有的 Scala 代码,我正在使用 Kafka 0.8.2-beta 和 Scala 2.10.4

这里是生产者代码,我没有使用自定义partitioner.class

  val props = new Properties()

  val codec = if(compress) DefaultCompressionCodec.codec else NoCompressionCodec.codec

  props.put("compression.codec", codec.toString)
  props.put("producer.type", if(synchronously) "sync" else "async")
  props.put("metadata.broker.list", brokerList)
  props.put("batch.num.messages", batchSize.toString)
  props.put("message.send.max.retries", messageSendMaxRetries.toString)
  props.put("request.required.acks",requestRequiredAcks.toString)
  props.put("client.id",clientId.toString)

  val producer = new Producer[AnyRef, AnyRef](new ProducerConfig(props))

  def kafkaMesssage(message: Array[Byte], partition: Array[Byte]): KeyedMessage[AnyRef, AnyRef] = {
     if (partition == null) {
       new KeyedMessage(topic,message)
     } else {
       new KeyedMessage(topic,partition,message)
     }
  }

  def send(message: String, partition: String = null): Unit = send(message.getBytes("UTF8"), if (partition == null) null else partition.getBytes("UTF8"))

  def send(message: Array[Byte], partition: Array[Byte]): Unit = {
    try {
      producer.send(kafkaMesssage(message, partition))
    } catch {
      case e: Exception =>
        e.printStackTrace
        System.exit(1)
    }       
  }

下面是我如何使用生产者,创建一个生产者实例,然后使用这个实例发送三个消息。目前我将分区键创建为整数,然后将其转换为字节数组:

  val testMessage = UUID.randomUUID().toString
  val testTopic = "sample1"
  val groupId_1 = "testGroup"

  print("starting sample broker testing")
  val producer = new KafkaProducer(testTopic, "localhost:9092")

  val numList = List(0,1,2);
  for (a <- numList) {
    // Create a partition key as Byte Array
    var key = java.nio.ByteBuffer.allocate(4).putInt(a).array()
    //Here I give a Array[Byte] key
    //so the second "send" function of producer will be called
    producer.send(testMessage.getBytes("UTF8"), key)
  }

不确定我的逻辑是否不正确,或者我没有正确理解分区键机制。任何人都可以提供一些示例代码或解释会很棒!

【问题讨论】:

  • 我想你正在使用stealthly/scala-kafka 库?它看起来像一个错误,你能在 github 上为此打开一个问题吗?我会尽量在本周末左右解决这个问题。
  • @serejja 是的,代码来自该存储库。我猜应该定义生产者链接这个“新生产者[String,String](someConfig)”,然后内部默认分区器应该可以工作。今天我将在 github 上打开这个问题。谢谢

标签: scala apache-kafka


【解决方案1】:

人们通常认为分区是一种将业务数据按业务类别分开的方法,但这不是查看分区的正确角度。

分区直接影响这些科目:

-性能(每个分区可以与其他分区并行消耗)

-消息顺序(仅在分区级别保证消息的顺序)

我将举例说明我们如何创建分区:

你有一个话题,比如 MyMessagesToWorld

您想将此主题(所有 MyMessagesToWorld)转移给某个消费者。

您“称重”了 MyMessagesToWorld 的整个“质量”,发现这是 10 公斤。

您在“MyMessagesToWorld”中有以下“业务”类别:

-给爸爸的信息(D)

-给妈妈的信息(M)

-给sis (S)的消息

-给奶奶的信息(G)

-给老师的信息(T)

-给女朋友的信息(F)

你想,你的消费者是谁,发现你的消费者是侏儒,每个人一个小时可以消费 1 公斤的消息。

您最多可以雇佣 2 个这样的侏儒。

1 个 gnome 需要 10 个小时来消费 10 kg 消息,2 个 gnome 需要 5 个小时。

所以你决定使用所有可用的侏儒来节省时间。

要为这 2 个 gnome 创建 2 个“通道”,您需要在 Kafka 上创建该主题的 2 个分区。如果您设想更多 gnome,请创建更多分区。

您有 6 个内部业务类别和 2 个连续的独立消费者 - gnome(消费者线程)。

怎么办?

Kafka 的做法如下:

假设集群中有 2 个 kafka 实例。 (同样的例子可以,如果集群中有更多实例)

您在 Kafka 上将分区号设置为 2,例如(以 Kafka 0.8.2.1 为例):

您在 Kafka 中定义您的主题,告诉您该主题有 2 个分区:

kafka-topics.sh(.bat) --create --zookeeper localhost:2181 --replication-factor 2 --partitions 2 --topic MyMessagesToWorld 

现在 MyMessagesToWorld 主题有 2 个分区:P(0) 和 P(1)。

你选择了 2 号(分区),因为你知道,你(invision)只有 2 个消费侏儒。

您可以稍后添加更多分区,届时将使用更多的消费者 gnome。

不要将 Kafka 消费者与此类 gnome 混淆。

Kafka 消费者可以使用 N 个侏儒。 (N个并行线程)

现在您为您的消息创建 KEY。

您需要 KEYS 在分区之间分发消息。

键将是您之前定义的“业务类别”的这些字母: D、M、S、G、T、F,你觉得这样的字母可以当ID。

但在一般情况下,任何可以用作 Key 的东西: (复杂的对象和字节数组,任何东西......)

如果您不创建分区器,则将使用默认分区器。

默认的分区器有点笨。

它获取每个 KEY 的哈希码并除以可用分区数,“提醒”将定义该键的分区数。

例子:

KEY M, hashcode=12345, partition for M = 12345 % 2 = 1

您可以想象,在最好的情况下,使用这样的分区器,您可以在每个分区中登陆 3 个业务类别。

在更糟糕的情况下,您可以将所有业务类别放在 1 个分区中。

如果你有 100000 个业务类别,按这样的算法分配它们在统计上是可以的。

但是只有很少的类别,你的分布可能不是很公平。

因此,您可以重写分区程序并更明智地分配业务类别。

有一个例子:

此分区程序在可用分区之间平均分配业务类别。

public class CustomPartitioner {

private static Map<String, Integer> keyDistributionTable = new HashMap<String, Integer>();
private static AtomicInteger sequence = new AtomicInteger();
private ReentrantLock lock = new ReentrantLock();

public int partition(ProducerRecord<String, Object> record, Cluster cluster) {

    String key = record.key();
    int seq = figureSeq(key);

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

    if (availablePartitions.size() > 0) {
        int part = seq % availablePartitions.size();
        return availablePartitions.get(part).partition();
    } else {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(record.topic());
        int numPartitions = partitions.size();
        // no partitions are available, give a non-available partition
        return seq % numPartitions;
    }
}


private int figureSeq(String key) {
    int sequentualNumber = 0;
    if(keyDistributionTable.containsKey(key)){
        sequentualNumber = keyDistributionTable.get(key);
    }else{//synchronized region
        //used only for new Keys, so high waiting time for monitor expected only on start
        lock.lock();
        try{
            if(keyDistributionTable.containsKey(key)){
                sequentualNumber =  keyDistributionTable.get(key);
            }else{
                int seq = sequence.incrementAndGet();
                keyDistributionTable.put(key, seq);
                sequentualNumber =  seq;
            }
        }finally{
            lock.unlock();
        }
    }
    return sequentualNumber;
}

}

【讨论】:

    【解决方案2】:

    默认分区程序查看键(作为字节数组)并使用 (% numPartitions) 将该值转换为介于 0 和分区数 - 1 之间的整数值。结果整数决定了消息写入的分区,而不是你正在做的键的值。

    【讨论】:

    • 嗨,克里斯,就像你说的,我正在使用默认分区器,并让默认分区器根据我的硬代码键和 numPartitions 生成一个整数值。这意味着如果我更改了硬代码键,生成的结果也应该更改(numPartitions 永远不会更改,它是在配置文件中配置的)。但在我的实验中,结果并不如我所料。我猜这个问题是因为 byte[] 的哈希码是基于引用,而不是值。我应该可以将生产者绑定到 [String,Array[byte]]。
    【解决方案3】:

    遇到同样的问题 - 只需切换到 ByteArrayParitioner:

    props.put("partitioner.class", "kafka.producer.ByteArrayPartitioner")
    

    【讨论】:

    • 在 0.9.x 版本中找不到 ByteArrayPartitioner 类。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-22
    • 1970-01-01
    • 1970-01-01
    • 2017-09-19
    相关资源
    最近更新 更多