MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kafka 核心架构深度剖析

2021-10-102.3k 阅读

Kafka 核心架构概述

Kafka是一个分布式流处理平台,被广泛应用于大数据领域,用于处理高吞吐量的实时数据。它的核心架构由多个关键组件构成,这些组件协同工作,实现了数据的高效生产、存储与消费。

1. 生产者(Producer)

生产者负责将消息发送到Kafka集群。在Kafka中,生产者将消息发送到特定的主题(Topic)。生产者可以是各种类型的应用程序,比如Web应用、移动应用后端,或者是其他数据生成系统。

生产者在发送消息时,需要指定消息的目标主题、分区(Partition)等信息。分区是Kafka中对主题数据进行物理划分的单位,通过合理的分区策略,可以实现消息的负载均衡和并行处理。

以下是一个简单的Java生产者代码示例:

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;

public class KafkaProducerExample {
    public static void main(String[] args) {
        // 设置生产者属性
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 创建生产者实例
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

        // 发送消息
        for (int i = 0; i < 10; i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>("test-topic", "key-" + i, "message-" + i);
            producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception != null) {
                        exception.printStackTrace();
                    } else {
                        System.out.println("Message sent to partition: " + metadata.partition() + " at offset: " + metadata.offset());
                    }
                }
            });
        }

        // 关闭生产者
        producer.close();
    }
}

在上述代码中,我们首先配置了Kafka生产者的属性,包括Kafka集群的地址、键和值的序列化器。然后创建了一个生产者实例,并通过循环发送10条消息到名为“test - topic”的主题。每条消息都有一个键和一个值,并且我们通过回调函数来处理消息发送的结果。

2. 消费者(Consumer)

消费者从Kafka集群中读取消息。消费者可以订阅一个或多个主题,并按照一定的顺序消费这些主题中的消息。Kafka的消费者是以消费者组(Consumer Group)的形式工作的。

消费者组内的消费者共同消费主题中的消息,每个分区只能被组内的一个消费者消费,这样可以实现消息的并行消费。当有新的消费者加入组或者有消费者离开组时,Kafka会自动进行再平衡(Rebalance)操作,重新分配分区给各个消费者。

以下是一个简单的Java消费者代码示例:

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;

public class KafkaConsumerExample {
    public static void main(String[] args) {
        // 设置消费者属性
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // 创建消费者实例
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        // 订阅主题
        consumer.subscribe(Collections.singletonList("test-topic"));

        while (true) {
            // 拉取消息
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println("Received message: key = " + record.key() + ", value = " + record.value() + ", partition = " + record.partition() + ", offset = " + record.offset());
            }
        }
    }
}

在这个代码示例中,我们配置了Kafka消费者的属性,包括Kafka集群地址、消费者组ID、键和值的反序列化器。然后创建了一个消费者实例,并订阅了“test - topic”主题。通过一个无限循环,消费者不断从Kafka集群拉取消息并进行处理。

Kafka 主题(Topic)与分区(Partition)

1. 主题(Topic)

主题是Kafka中消息的逻辑分类。生产者将消息发送到主题,消费者从主题中读取消息。一个Kafka集群可以包含多个主题,每个主题可以有不同的用途,比如“user - activity”主题可以用于记录用户的行为数据,“system - logs”主题可以用于记录系统日志。

主题在创建时可以指定一些参数,比如分区数、副本因子等。分区数决定了主题数据在物理上的划分数量,副本因子决定了每个分区的副本数量,用于数据的冗余和高可用性。

2. 分区(Partition)

分区是Kafka主题数据的物理划分单位。每个主题可以包含一个或多个分区。分区的设计使得Kafka能够实现高吞吐量和水平扩展。

在生产者发送消息时,如果没有指定分区,Kafka会根据消息的键(Key)通过哈希算法来决定将消息发送到哪个分区。这样可以保证具有相同键的消息会被发送到同一个分区,对于需要顺序处理的消息非常有用。

例如,假设我们有一个“user - transactions”主题,并且希望按照用户ID来对交易消息进行分区,以便同一个用户的所有交易消息都在同一个分区中,这样可以保证按照顺序处理这些消息。我们可以在生产者发送消息时,将用户ID作为消息的键。

分区在Kafka集群中分布在不同的Broker节点上。每个分区都有一个领导者(Leader)副本和零个或多个追随者(Follower)副本。领导者副本负责处理生产者和消费者的读写请求,追随者副本则从领导者副本同步数据,用于数据冗余和故障恢复。

Kafka Broker

1. Broker 概述

Broker是Kafka集群中的节点。每个Broker节点负责存储和管理一部分主题的分区。当Kafka集群规模扩大时,可以通过添加更多的Broker节点来实现水平扩展。

Broker在启动时会向Zookeeper注册自己,并获取集群的元数据信息,包括主题、分区、副本等信息。Zookeeper在Kafka集群中起着协调和管理的作用,虽然在Kafka 2.8.0及更高版本中,Kafka可以在无Zookeeper的情况下运行,但Zookeeper仍然在早期版本以及一些复杂场景中具有重要意义。

2. 副本机制

如前文所述,每个分区都有一个领导者副本和零个或多个追随者副本。追随者副本通过向领导者副本发送FETCH请求来同步数据。当领导者副本发生故障时,Kafka会从追随者副本中选举出一个新的领导者副本,以保证服务的可用性。

Kafka使用ISR(In - Sync Replicas)机制来管理副本。ISR是一组与领导者副本保持同步的追随者副本集合。只有ISR中的副本才有资格被选举为新的领导者。如果一个追随者副本长时间没有从领导者副本同步数据,它将被移出ISR。

例如,假设我们有一个包含3个副本的分区,其中副本1是领导者,副本2和副本3是追随者。如果副本2因为网络故障等原因长时间没有从领导者副本同步数据,Kafka会将副本2移出ISR。当领导者副本1发生故障时,Kafka会从ISR中的副本3中选举出新的领导者。

Kafka 日志存储

1. 日志结构

Kafka的日志是以分区为单位进行存储的。每个分区在磁盘上对应一个目录,目录名是分区的ID。在分区目录中,包含一系列的日志段(Log Segment)文件。

每个日志段文件由一个索引文件和一个数据文件组成。数据文件存储实际的消息内容,索引文件则用于快速定位消息在数据文件中的位置。日志段文件有一定的大小限制,当一个日志段文件达到配置的大小上限时,Kafka会创建一个新的日志段文件。

2. 日志清理策略

Kafka提供了两种日志清理策略:删除(Delete)策略和压缩(Compact)策略。

删除策略:按照一定的时间或大小限制,删除旧的日志段文件。例如,可以配置Kafka只保留最近7天的日志,或者当日志文件总大小超过一定限制时,删除最早的日志段。

压缩策略:对于具有相同键的消息,只保留最新的消息。这种策略适用于一些需要保留最新状态的场景,比如用户的最新信息、设备的最新状态等。

以下是如何在Kafka主题创建时设置日志清理策略的示例:

bin/kafka - topics.sh --create --bootstrap - servers localhost:9092 --replication - factor 1 --partitions 1 --topic test - topic --config cleanup.policy=compact

在上述命令中,我们通过--config cleanup.policy=compact设置了“test - topic”主题的日志清理策略为压缩策略。

Kafka 控制器(Controller)

1. 控制器作用

Kafka控制器是Kafka集群中的一个特殊Broker节点,负责管理集群的元数据,比如主题的创建、删除,分区的分配、副本的选举等操作。

当一个新的Broker节点加入集群或者有Broker节点离开集群时,控制器会负责协调集群的状态变更,保证集群的正常运行。

2. 控制器选举

Kafka控制器是通过Zookeeper进行选举产生的。在Kafka集群启动时,每个Broker节点都会尝试在Zookeeper中创建一个临时节点(/controller)。第一个成功创建该节点的Broker将成为控制器。

如果当前控制器发生故障,Zookeeper会检测到并删除/controller节点,此时其他Broker节点会再次竞争创建该节点,从而选举出新的控制器。

Kafka 高级特性

1. 事务

Kafka从0.11.0版本开始支持事务。事务允许生产者将一组消息作为一个原子操作发送到多个主题和分区,要么全部成功,要么全部失败。

在使用事务时,生产者需要先初始化事务,然后开始事务,在事务中发送消息,最后提交事务或回滚事务。

以下是一个使用事务的Java生产者代码示例:

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;

public class KafkaTransactionalProducerExample {
    public static void main(String[] args) {
        // 设置生产者属性
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "my - transactional - id");

        // 创建生产者实例
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

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

        try {
            // 开始事务
            producer.beginTransaction();

            // 发送消息到不同主题
            ProducerRecord<String, String> record1 = new ProducerRecord<>("topic1", "key1", "message1");
            ProducerRecord<String, String> record2 = new ProducerRecord<>("topic2", "key2", "message2");
            producer.send(record1);
            producer.send(record2);

            // 提交事务
            producer.commitTransaction();
        } catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
            // 处理事务异常,回滚事务
            producer.abortTransaction();
            e.printStackTrace();
        } finally {
            // 关闭生产者
            producer.close();
        }
    }
}

在上述代码中,我们通过设置ProducerConfig.TRANSACTIONAL_ID_CONFIG来启用事务,并使用initTransactions()方法初始化事务。在try - catch块中,我们开始事务、发送消息,最后根据情况提交或回滚事务。

2. 流处理

Kafka Streams是Kafka提供的一个轻量级流处理库,用于在Kafka上进行实时流处理。它基于Kafka的生产者和消费者API构建,使得开发者可以方便地对Kafka中的数据流进行转换、聚合等操作。

例如,我们可以使用Kafka Streams对一个包含用户点击事件的数据流进行处理,统计每个用户的点击次数。

以下是一个简单的Kafka Streams示例代码:

import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.Materialized;
import org.apache.kafka.streams.kstream.Produced;
import java.util.Properties;

public class KafkaStreamsExample {
    public static void main(String[] args) {
        // 设置Streams配置
        Properties props = new Properties();
        props.put(StreamsConfig.APPLICATION_ID_CONFIG, "click - counter - app");
        props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
        props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());

        // 创建StreamsBuilder
        StreamsBuilder builder = new StreamsBuilder();

        // 读取输入主题
        KStream<String, String> clicks = builder.stream("user - clicks - topic");

        // 统计每个用户的点击次数
        clicks.groupByKey()
              .count(Materialized.as("click - counts - store"))
              .toStream()
              .to("user - click - counts - topic", Produced.with(Serdes.String(), Serdes.Long()));

        // 构建拓扑
        Topology topology = builder.build();

        // 创建并启动Kafka Streams实例
        KafkaStreams streams = new KafkaStreams(topology, props);
        streams.start();

        // 添加关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(streams::close));
    }
}

在上述代码中,我们首先配置了Kafka Streams的属性,然后通过StreamsBuilder构建了一个拓扑。我们从“user - clicks - topic”主题读取用户点击事件,按照用户ID(键)进行分组,统计每个用户的点击次数,并将结果输出到“user - click - counts - topic”主题。

Kafka 与其他系统的集成

1. Kafka 与 Spark

Kafka与Spark的集成非常常见。Spark Streaming可以作为Kafka的消费者,从Kafka主题中读取数据进行实时处理。

例如,我们可以使用Spark Streaming从Kafka主题中读取日志数据,并进行实时的数据分析,如统计不同类型日志的数量。

以下是一个简单的Spark Streaming与Kafka集成的代码示例:

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.KafkaUtils
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent

object KafkaSparkStreamingExample {
    def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf().setAppName("KafkaSparkStreamingExample").setMaster("local[2]")
        val ssc = new StreamingContext(sparkConf, Seconds(5))

        val kafkaParams = Map[String, Object](
            "bootstrap.servers" -> "localhost:9092",
            "key.deserializer" -> classOf[StringDeserializer],
            "value.deserializer" -> classOf[StringDeserializer],
            "group.id" -> "spark - group",
            "auto.offset.reset" -> "earliest",
            "enable.auto.commit" -> (false: java.lang.Boolean)
        )

        val topics = Array("system - logs - topic")
        val stream = KafkaUtils.createDirectStream[String, String](
            ssc,
            PreferConsistent,
            Subscribe[String, String](topics, kafkaParams)
        )

        val logTypeCounts = stream.map(_.value())
              .map(log => (log.split(" ")(0), 1))
              .reduceByKey(_ + _)

        logTypeCounts.print()

        ssc.start()
        ssc.awaitTermination()
    }
}

在上述代码中,我们首先创建了一个Spark Streaming上下文,然后配置了Kafka消费者的参数。通过KafkaUtils.createDirectStream方法从“system - logs - topic”主题读取日志数据,对日志数据进行处理,统计不同类型日志的数量,并打印结果。

2. Kafka 与 Flink

Flink也可以与Kafka很好地集成。Flink可以作为Kafka的消费者或生产者,实现复杂的流处理逻辑。

例如,我们可以使用Flink从Kafka主题中读取传感器数据,实时计算传感器的平均值。

以下是一个简单的Flink与Kafka集成的代码示例:

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import java.util.Properties;

public class FlinkKafkaExample {
    public static void main(String[] args) throws Exception {
        // 创建Flink执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // 设置Kafka消费者属性
        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers", "localhost:9092");
        properties.setProperty("group.id", "flink - group");
        properties.setProperty("auto.offset.reset", "earliest");

        // 创建Kafka消费者
        DataStreamSource<String> stream = env.addSource(new FlinkKafkaConsumer<>("sensor - data - topic", new SimpleStringSchema(), properties));

        // 处理传感器数据,计算平均值
        // 这里假设传感器数据格式为 "sensorId,value"
        stream.map(value -> {
            String[] parts = value.split(",");
            return new SensorReading(parts[0], Double.parseDouble(parts[1]));
        })
              .keyBy(SensorReading::getSensorId)
              .average("value")
              .print();

        // 执行Flink作业
        env.execute("Flink Kafka Example");
    }

    public static class SensorReading {
        private String sensorId;
        private double value;

        public SensorReading() {}

        public SensorReading(String sensorId, double value) {
            this.sensorId = sensorId;
            this.value = value;
        }

        public String getSensorId() {
            return sensorId;
        }

        public double getValue() {
            return value;
        }

        @Override
        public String toString() {
            return "SensorReading{" +
                    "sensorId='" + sensorId + '\'' +
                    ", value=" + value +
                    '}';
        }
    }
}

在上述代码中,我们首先创建了Flink的执行环境,然后配置了Kafka消费者的属性。通过env.addSource方法从“sensor - data - topic”主题读取传感器数据,将数据转换为SensorReading对象,按照传感器ID进行分组,计算每个传感器数据的平均值并打印。

通过对Kafka核心架构各个方面的深入剖析,以及代码示例的展示,相信读者对Kafka有了更全面和深入的理解,能够在实际项目中更好地应用Kafka来构建高效、可靠的分布式数据处理系统。无论是处理高吞吐量的实时数据,还是与其他大数据框架进行集成,Kafka都提供了强大的功能和灵活的架构支持。