1. 为什么Kafka要分区
在了解分区策略之前,我们有必要先了解一下为什么要分区。其实分区的作用就是提供负载均衡的能力或者说对数据进行分区的主要原因就是为了实现系统的高伸缩性。不同的分区能够被放到不同的节点机器上,而数据的读写操作也是针对分区这个粒度进行的,使用分区之后每个节点的机器都能独立地执行各自分区的读写请求处理并且,我们还能通过添加新的节点机器来增加系统整体的吞吐量。
从图中可以看出,分区属于主题,消息属于分区。
2. Kafka常见分区策略
分区策略是决定生产者将消息发送到哪一个分区的算法,Kafka不仅仅提供了默认的分区策略,同时它也支持你自定义分区策略。
2.1 轮询策略
轮询策略也称Round-robin策略,即顺序分配。比如一个主题下有2个分区,那第一条消息会发送到分区0,第二条被发到分区1,第三条被发送到分区0,以此类推。Kafka的默认分区策略就是轮询策略,如下图所示。
轮询策略是Kafka Java生产者API默认提供的分区策略。如果没有指定分区策略,则会默认使用轮询。 轮询策略有着非常优秀的负载均衡表现,它总是能保证消息最大限度平均分配到所有分区上,所以一般情况下它是最合理的分区策略,也是我们常用的分区策略之一。
2.2 随机策略
随机策略就是我们随机的将消息放到任意一个分区上,如下图所示。
本质上,随机策略也是力求将数据均匀地打散到各个分区,但是从实际表现来看,它要逊于轮询策略,所以如果追求数据的均匀分布,轮询策略比随机策略优秀。
2.3 Key-Ordering策略
Kafka允许为每条消息定义key。这个key可以是一个有着明确业务含义的字符串,也可以用来表征消息元数据。一旦消息被定义了Key,那就可以保证同一个Key的所有消息都被发送到相同的分区中。对于一些需求是严格要求执行顺序的,可以采用这种方法进行发送消息。这样就能保证消息有序(消费同一个分组中的数据是有序的)
Kafka默认的分区策略实际上有两种,在没有指定key的情况下,使用轮询。如果指定了Key,则默认按照key-ordering策略
3. 三种分区策略在Spring Boot中的实现
3.1 轮询分区策略
轮询分区策略是Kafka默认的分区策略,因此在使用该策略的时候无需特殊配制,代码如下:
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
@Service
public class KafkaConsumerService {
// 使用 @KafkaListener 注解来定义 Kafka 消费者
@KafkaListener(
topics = "my-topic", // 指定要订阅的主题
groupId = "my-consumer-group" // 指定消费者组,会自动按照轮询分区策略在消费者组中进行负载均衡
)
public void consumeMessage(String message) {
// 在这里处理接收到的消息
System.out.println("Received message: " + message);
}
}
3.2 随机分区策略
首先,在你的Spring Boot应用的配置文件(如application.properties或application.yml)中,添加Kafka消费者的相关配置,包括消费者组、监听的主题和Kafka服务器的地址:
Kafka Consumer Configuration
spring.kafka.consumer.group-id=your-consumer-group
spring.kafka.consumer.bootstrap-servers=your-bootstrap-servers
接下来,创建一个Kafka消费者服务类,你可以在这个类中指定分区分配策略:
@Service
public class RandomPartitionConsumerService {
@Value("${spring.kafka.consumer.group-id}")
private String groupId;
@Value("${spring.kafka.consumer.bootstrap-servers}")
private String bootstrapServers;
@KafkaListener(topics = "your-topic")
public void consumeMessage(String message) {
System.out.println("Received message: " + message);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(3); // 设置并发消费者的数量
factory.getContainerProperties().setPollTimeout(3000); // 设置超时时间
factory.getContainerProperties().setPartitionAssignor(new RandomPartitionAssignor()); // 设置随机分区分配策略
return factory;
}
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class.getName());
props.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
return new DefaultKafkaConsumerFactory<>(props);
}
}
在上面的示例中,我们通过 setPartitionAssignor 方法设置了随机分区分配策略,使用自定义的 RandomPartitionAssignor 类。你需要自己实现 RandomPartitionAssignor 类来定义随机分区分配的逻辑。
3.3 Key-Ordering策略
首先,创建自定义分区器,该自定义分区器根据指定的key值分配到相同的Partition中:
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class KeyOrderingPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
if (key == null) {
// 如果键为null,则使用随机分区策略
return (int) (Math.random() * cluster.partitionCountForTopic(topic));
} else {
// 根据键的hashCode分配分区
return Math.abs(key.hashCode()) % cluster.partitionCountForTopic(topic);
}
}
@Override
public void close() {
// 关闭资源
}
@Override
public void configure(Map<String, ?> configs) {
// 配置分区器
}
}
在Spring Boot中配置Kafka生产者并制定使用自定义分区器:
@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "your-kafka-broker");
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 指定自定义分区器
configProps.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, KeyOrderingPartitioner.class.getName());
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
4. 总结
分区是实现负载均衡和高吞吐的关键,所以在生产者端一定要使用适合业务的分区策略,避免造成消息数据的“倾斜”,使某些分区成为性能瓶颈。 常见的分区策略有:
- 轮询:在不指定key的情况下默认的分区策略,能够最大限度实现数据均匀发送到每一个分区
- 随机:本质也是想要实现数据均匀发送
- key-ording:在指定key时默认的分区策略,能够在某些严格要求执行顺序的业务上发挥Kafka的多分区带来的性能提升