Kafka起源
Kafka起源于领英,现在由Apache软件基金会维护,它由scala编写,是一个高吞吐量的分布式发布订阅消息系统。关于Kafka的起源,有幸听过饶军(Confluent联合创始人)的一次分享,领英当时有一个“People You May Know”的应用,这个应用从各个系统获取用户行为,为用户推荐可能认识的人。在没有Kafka前,通常处理这个问题的架构是点对点的:
他们思考一个理想的架构应当是这样的:
于是Kafka的最初版本就诞生了,所以从这里也能看到Kafka的最大价值所在。除去点对点的架构,而是要承接大量的不同系统的数据生产消费,所以Kafka在设计之初,高吞吐量就是一个重要的考量,当然后续版本中,Kafka设计增加了副本,保证高可用等等其它分布式系统的指标。
Kafka整体架构
上图是Kafka的整体拓扑结构,中间是若干个Kafka节点组成的kafka集群,生产者消费者分别是生产数据和消费数据的各个系统,ZooKeeper负责管理集群配置,注册生产者消费者,选举leader等功能。
在逻辑架构上,生产者消费者都是以Topic为逻辑单位,为了提升吞吐,利用分布式集群的优点,Topic会有多个分片partition,逻辑架构如下图:
这是一个包含三个partition的Topic(生产与消费是类似的,这里从消费端说明),一个Topic允许注册若干个消费组,每个消费组都是相互独立的不会相互影响。每个消费组会有若干个消费实例,每个partition都会被分配给一个消费实例(partition不会分配给多个实例,为了保证有序性),也就是每个消费实例会被分配0个,1个,或多个partition,一个partition只会被分配给一个消费实例。图中没有体现的是,一个消费实例当然可以由多个线程来消费,但是多个线程就不能保证消息有序性。
partition也是一个逻辑概念,组成partition的是物理上的log文件,其中index文件记录log偏移量,log文件是记录真实数据的文件(可以把partition看作文件夹,log是文件,index是索引)
Kafka采用简单实用的日志存储方式处理消息数据,这也是Kafka能够实现多消费组,保证高吞吐的关键所在
生产者在尾部生产数据,消费者从头部消费数据,通过记录和控制偏移量来保证消息顺序的消费和生产。上图中:
- Last Commited Offset:是上一次已经确认消费完的数据偏移量。
- Current Position:消费实例一次拉取的消息[Last Commited Offset+1, Current Position],只是拉取还未提交,等待消费者提交Commit信号,则Current Position更新为Last Commited Offset。注意每次拉取都是拉取上一次提交之后的消息,即使已经拉取过。(实际上这也是kafka跟一些其它消息队列不同的地方,commit信息由消费者控制)
- High Watermark:是当前能够拉取的最高位置,在这个点之前的消息都是确认安全不会丢失的(同步到副本),HW->Log End之间是不安全的,不允许拉取。
- Log End Offset:是生产者写入的点。(生产者只能从主分片写,不能写从分片)
概念 | 解释 |
---|---|
Kafka集群 | 多个Kafka实例组成的集群 |
Broker | 运行Kafka实例的机器,可以是物理机,虚拟机,容器等,通常一个Broker运行一个实例 |
Topic | 逻辑概念,生产者和消费者订阅发布的逻辑实体,一个Tpoic可以有多个消费组订阅,消费组之间完全隔离,互不影响 |
Partition | 消息分片,实际上Partiton也是一个逻辑概念,每个Topic会分为多个partiton,每个partition在一个Kafka实例上。partition内消息是有序的 |
Log文件 | 物理概念,实际存储消息的文件,它由index文件索引,log文件连接在一起组成partition。Log文件不对生产者消费者直接暴露 |
Producer | 生产者,负责发布消息 |
Consumer Group | 消费组,订阅消息的单位。不同消费组之间相互独立,互不影响 |
Consumer | 消费者实例,组成消费组。每个消费者实例会被分配到0个,1个或多个partition |
Consumer Thread | 一个消费者实例内部可以开启多个线程消费,但是多个线程不能保证消息有序性 |
Kafka消费客户端
早期版本的Kafka提供了High Level和Low Level两个客户端。High Level Consumer API围绕着Consumer Group这个逻辑概念展开,它屏蔽了每个Topic的每个Partition的Offset管理(自动读取zookeeper中该Consumer group的last offset )、Broker失败转移以及增减Partition、Consumer时的负载均衡(当Partition和Consumer增减时,Kafka自动进行负载均衡)。Low Level Consumer API,作为底层的Consumer API,提供了消费Kafka Message更大的控制,使客户端可以重复读,跳读等等操作,当然Low Level Consumer API提供更大灵活控制是以复杂性为代价的。
后来Kafka发布了0.9版本的Kafka Consumer Client,这个客户端提供了像High Level Consumer API的简洁性,同时可以像Low Level Consumer一样构建自己的消费策略(API更简洁),并且新的Kafka客户端用Java编写,不再依赖Scala。
下面通过一个示例,介绍如何使用,也能加深对Kafka结构的理解。
初始化Consumer实例
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "consumer-tutorial");
props.put("key.deserializer", StringDeserializer.class.getName());
props.put("value.deserializer", StringDeserializer.class.getName());
KafkaConsumer consumer = new KafkaConsumer<>(props);
订阅Topic
consumer.subscribe(Arrays.asList(“foo”, “bar”));
拉取消息消费,有两种方式:
1.基本的poll循环:
try {
while (running) {
ConsumerRecords records = consumer.poll(1000);
for (ConsumerRecord record : records)
System.out.println(record.offset() + ": " + record.value());
}
} finally {
consumer.close();
}
上面poll的参数是等待时间,如果有消息立即返回,如果没有消息,则等待1000ms返回,这样running可以控制消费和停止。poll也可以像下面一样传一个Long.MAX_VALUE,这样没有消息会一直阻塞在这里,等待消息到来,替代的,需要调用consumer.weakup()唤醒来控制消费实例停止。
public class ConsumerLoop implements Runnable {
private final KafkaConsumer consumer;
private final List topics;
private final int id;
public ConsumerLoop(int id,
String groupId,
List topics) {
this.id = id;
this.topics = topics;
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put(“group.id”, groupId);
props.put(“key.deserializer”, StringDeserializer.class.getName());
props.put(“value.deserializer”, StringDeserializer.class.getName());
this.consumer = new KafkaConsumer<>(props);
}
@Override
public void run() {
try {
consumer.subscribe(topics);
while (true) {
ConsumerRecords records = consumer.poll(Long.MAX_VALUE);
for (ConsumerRecord record : records) {
Map data = new HashMap<>();
data.put("partition", record.partition());
data.put("offset", record.offset());
data.put("value", record.value());
System.out.println(this.id + ": " + data);
}
}
} catch (WakeupException e) {
// ignore for shutdown
} finally {
consumer.close();
}
}
public void shutdown() {
consumer.wakeup();
}
}
public static void main(String[] args) {
int numConsumers = 3;
String groupId = "consumer-tutorial-group"
List topics = Arrays.asList("consumer-tutorial");
ExecutorService executor = Executors.newFixedThreadPool(numConsumers);
final List consumers = new ArrayList<>();
for (int i = 0; i < numConsumers; i++) {
ConsumerLoop consumer = new ConsumerLoop(i, groupId, topics);
consumers.add(consumer);
executor.submit(consumer);
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
for (ConsumerLoop consumer : consumers) {
consumer.shutdown();
}
executor.shutdown();
try {
executor.awaitTermination(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace;
}
}
});
}