Kafka
定义
Kafka 是一种高吞吐量、分布式、基于发布/订阅的消息系统,主要用于大数据实时处理领域。最初由 LinkedIn 公司开发,使用 Scala 语言编写,目前是 Apache 的开源项目。
消息队列作用
- 解耦:允许独立的拓展或者修改两边的处理过程,只要确保它们遵守同样的接口
- 可恢复性:即时系统中处理消息的进程挂掉,消息队列中的消息也不会丢失,可以在系统恢复后被继续处理
- 缓冲:有助于控制和优化数据流经过系统的速度,解决生产者和消费者处理速度不一致的问题,一般生产大于消费需要解决
- 削峰
- 异步通信
消息队列两种模式
- 点对点模式:消费者主动获取数据
- 发布/订阅模式:订阅模式,消费者被动的收取数据或主动的拉取数据,而kafka基于消费者主动拉取数据
Kafka中的相关概念
Kafka中发布订阅的对象是topic。可以为每类数据创建一个topic,把向topic发布消息的客户端称作producer,从topic订阅消息的客户端称作consumer。Producers和consumers可以同时从多个topic读写数据。一个kafka集群由一个或多个broker服务器组成,它负责持久化和备份具体的kafka消息。
-
Broker(代理者 ):Kafka集群中的机器 /服务被称为broker, 是一个物理概念。一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。注意:一台节点上可以有多个broker,这和HDFS上一个节点是一个namenode或者datanode有所区别。一台机器上的broker数量由server.properties的数量决定。
-
Topic:一类消息,消息存放的目录即主题,例如page view日志、click日志等都可以以topic的形式存在,Kafka集群能够同时负责多个topic的分发。一个broker可以容纳多个topic,因此在一台broker上一个topic可以分leader和follower,follower负责备份。
-
Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列,producer端发送的message必须指定是发送到哪个topic,但是不需要指定topic下的哪个partition,因为kafka会把收到的message进行load balance,均匀的分布在这个topic下的不同的partition上。每个分区只能被同一个消费者组里的一个消费者消费。在数据的产生和消费过程中,不需要关注具体存储的Partition在哪个Broker上,只需要指定 Topic 即可,由 Kafka负责将数据和对应的 Partition关联上。 一般来说,一个Topic的Partition数量大于等于Broker的数量,可以提高吞吐率;同一个Partition的Replica尽量分散到不同的机器,做到高可用。
-
Partition Replica:每个partition可以在其他的Kafka broker节点上存副本,以便某个Kafka broker节点宕机不会影响这个Kafka集群。存replica副本的方式是按照Kafka broker的顺序存。例如有5个Kafka broker节点,某个topic有3个partition,每个partition存2个副本,那么partition1存broker1,broker2,partition2存broker2,broker3。。。以此类推(replica副本数目不能大于Kafka broker节点的数目,否则报错。这里的replica数其实就是partition的副本总数,其中包括一个leader,其他的就是copy副本)。这样如果某个broker宕机,其实整个Kafka内数据依然是完整的。但是,replica副本数越高,系统虽然越稳定,但是回来带资源和性能上的下降;replica副本少的话,也会造成系统丢数据的风险。
-
Segment:partition物理上由多个segment组成,每个Segment存着message信息,文件分为index和log,通过index文件可以定位log中数据的信息
-
Message (消息 ):传递的数据对象,主要由四部分构成(offset(偏移量 )、key、value、 timestamp(插入时间 ),消息和数据不是一个概念,消息是对数据的封装,其中的value部分是可以看成是数据。
-
Producer : 生产message发送到topic
-
Consumer : 订阅topic消费message, consumer作为一个线程来消费
-
Consumer Group:一个Consumer Group包含多个consumer, 这个是预先在配置文件中配置好的。各个consumer(consumer 线程)可以组成一个组(Consumer group ),partition中的每个message只能被组(Consumer group ) 中的一个consumer(consumer 线程 )消费,如果一个message可以被多个consumer(consumer 线程 ) 消费的话,那么这些consumer必须在不同的组。Kafka不支持一个partition中的message由两个或两个以上的consumer thread来处理,即便是来自不同的consumer group的也不行。如果觉得效率不高的时候,可以加partition的数量来横向扩展,那么再加新的consumer thread去消费。这样没有锁竞争,充分发挥了横向的扩展性,吞吐量极高。这也就形成了分布式消费的概念。
Kafka命令
- 启动:
kafka-server-start.sh [-daemon] config/server.properties
- 创建topic:
kafka-topics.sh --create --zookeeper zk地址 --topic 名称 --partitions 分区数 --replication-factor 副本数
- 查看topic:
kafka-topics.sh --list --zookeeper zk地址 --topic 名称
- 删除topic:
kafka-topics.sh --delete --zookeeper zk地址 --topic 名称
- 查看topic详细信息:
kafka-topics.sh --describe --topic 名称 --zookeeper zk地址
- 命令行启动生产者:
bin/kafka-console-producer.sh --broker-list kafka地址:9092 --topic 名称
- 命令行启动消费者:
bin/kafka-console-consumer.sh --topic 名称 --zookeeper zk地址 [--from-beginning]
- 命令行启动消费者:
bin/kafka-console-consumer.sh --topic 名称 --bootstrap-server 生产者地址 [--from-beginning]
ISR
leader会维护一个与其基本保持同步的Replica列表,该列表称为ISR(in-sync Replica),每个Partition都会有一个ISR,而且是由leader动态维护。如果一个flower比一个leader落后太多,或者超过一定时间未发起数据复制请求,则leader将其重ISR中移除。
生产者
Kafka producer 发送message不用维护message的offsite信息,因为这个时候,offsite就相当于一个自增id,producer就尽管发送message就好了。而且Kafka与AMQ不同,AMQ大都用在处理业务逻辑上,而Kafka大都是日志,所以Kafka的producer一般都是大批量的batch发送message,向这个topic一次性发送一大批message,load balance到一个partition上,一起插进去,offsite作为自增id自己增加就好。并且producer是先把message发送到partition leader,再由leader发送给其他partition follower。
-
生产者分区策略:默认是将key的哈希值与分区数取余得到分区号,但代码中可以指定分区号。
-
数据可靠性保证:为保证producer发送的数据能可靠的到达指定的topic,topic的每个partition收到producer发送的消息后,都需要向producer发送ack,如果producer收到ack就会进行下一轮的发送。多少个副本同步完成后发送ack?kafka选择了下面的第二种,但是问题是如果有一台机器很慢甚至挂掉的话那么leader需要一直等待同步完成之后才能返回ack,因此就需要ISR。当ISR中所有Replica都向Leader发送ACK时,leader才commit。
方案 | 优点 | 缺点 |
---|---|---|
半数以上完成同步就发送ack | 延迟低 | 选举新leader时,容忍n台节点故障,需要2n+1个副本 |
全部同步完成,才能发送ack | 选举新leader,容忍n台节点的故障,需要n+1个副本 | 延迟高 |
- ack应答机制:对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR中的follower全部接收成功。Kafka提供了三种可靠性级别,用户可以根据可靠性和延迟的要求进行权衡,选择设置acks来配置。
值 | 说明 |
---|---|
0 | 生产者不等待broker的ack,可能丢失数据,但不会重复 |
1 | 等待broker的partition的leader落盘后回复ack,如果follower未同步之前leader发生故障,会造成数据丢失 |
2 | leader和其他一个follower成功的时候,broker就返回成功,无论其他的partition follower是否写成功 |
-1 | 等待follower落盘后回复ack,如果同步完成但是leader未回复ack之前会造成数据重复,但是kafka后续版本引入了幂等性,共同形成了Exactly Once语义,因此不会重复。 |
发送流程
Kafka的producer发送消息采用的是异步的方式。消息发送的过程中,涉及到了两个线程——main线程和Sender线程,以及一个线程共享变量——recordAccumulator。main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker,但是只有数据积累到batch.size后才会一起发送,如果数据在设定的时间linger.time内未达到batch.size,sender会发送数据。
代码
public static void main(String[] args) {
Properties properties=new Properties();
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("bootstrap.servers","kafka服务器的地址");
properties.put("acks","all"); //应答机制
properties.put("retries",3); //重试次数
properties.put("batch.size",16384); //sender发送拉取的范围
properties.put("linger.ms",1); //sender等待时间
properties.put("buffer.memory",33554432);//recordAccumulator缓冲区大小
properties.put("partition.class","com.lk.partition。myPartition");//recordAccumulator缓冲区大小
//创建KafkaProducer 实例
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(properties);
//构建待发送的消息,可以设置分区等
ProducerRecord<String,String> record=new ProducerRecord<String, String>("topic","hello Kafka!");
try {
//尝试发送消息
producer.send(record);
//打印发送成功
System.out.println("send success from producer");
} catch (Exception e) {
e.printStackTrace();
}finally {
//关闭生产者客户端实例
producer.close();
}
}
自定义partition
class myPartition implements Partitioner{
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
int size = partitionInfos.size();
return key.toString().hashCode()%size;
}
}
拦截器:可以对发送者的消息进行处理后再发送到broker
class MyInterceptor implements ProducerInterceptor<String,String>{
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
//编写处理的逻辑
return null;
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
//发送到broker后的后续逻辑,比如获取成功或失败的条数
}
@Override
public void close() {
//关闭时的逻辑
}
@Override
public void configure(Map<String, ?> map) {}
}
引入拦截器只需要在配置文件中引入:prop.put("interceptor.classes",自定义的拦截器列表)
消费者
kafka的消费者采用pull的模式,不足之处是如果kafka没有数据,消费者可能一直循环,一直返回空数据。针对这一点,kafka的消费者会在消费数据时传入一个时间,如果当前没有数据,则会在一定时间后再获取。Consumer处理partition里面的message的时候是o(1)顺序读取的。所以必须维护着上一次读到哪里的offsite信息。high level API,offset存于Zookeeper中,low level API的offset由自己维护。一般来说都是使用high level api的。其次,Kafka默认是读完message先commmit再处理message,autocommit默认是true,这时候就会更新offsite+1,一旦处理失败,offsite已经+1,这个时候就会丢message;也可以配置成读完消息处理再commit,这种情况下consumer端的响应就会比较慢的,需要等处理完才行。
分区分配策略:一种是RoundRobin、一种是range。RoundRobin不适用于消费者组中的消费者订阅了不同topic的情况,range是首先选出一个消费者组中订阅了该topic的消费者,然后给这些消费者按批分发消息,比如一共7条消息分给三个消费者,那么第一个消费者获得3条信息,第二个和第三个消费者获取2条信息,但range会有分配不均衡的问题。
- Consumer Rebalance的触发条件:(1)Consumer增加或删除会触发 Consumer Group的Rebalance(2)Broker的增加或者减少都会触发 Consumer Rebalance
消费策略:当启动一个consumer group去消费一个topic的时候,无论topic里面有多个少个partition,无论我们consumer group里面配置了多少个consumer thread,这个consumer group下面的所有consumer thread一定会消费全部的partition;即便这个consumer group下只有一个consumer thread,那么这个consumer thread也会去消费所有的partition。因此,最优的设计就是,consumer group下的consumer thread的数量等于partition数量,这样效率是最高的。我们在设定consumer group的时候,只需要指明里面有几个consumer数量即可,无需指定对应的消费partition序号,consumer会自动进行rebalance。
代码
public static void main(String[] args) {
/*判断是否指定消费的topic*/
if(args.length == 0){
System.out.println("Enter topic name");
return;
}
String topicName = args[0].toString();
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.12.34:9092");
props.put("acks", "all");
props.put("retries", 0);
/*将单个消费者分配给组*/
props.put("group.id", "test");
//如果值为true,则为偏移启用自动提交,否则不提交。也可以手动提交,分为同步或异步。
props.put("enable.auto.commit", "true");
/*自动提交的时间间隔*/
props.put("auto.commit.interval.ms", "1000");
/*反序列化器接口的键*/
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
/*反序列化器接口的值*/
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
// latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,不从头开始消费
// earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
// earliest 加上props.put("group.id", UUID.randomUUID().toString());可以实现 --from-beginning 功能
props.put("auto.offset.reset", "earliest");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
/*Kafka Consumer subscribes list of topics here.Kafka使用者在这里订阅主题列表。*/
consumer.subscribe(Arrays.asList(topicName));
/*print the topic name 打印topic 名称*/
System.out.println("Subscribed to topic " + topicName);
/*长时间监听和消费*/
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
/* print the offset,key and value for the consumer records.打印消费者记录的偏移量、键和值。*/
System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), record.value());
}
}
}
自定义存储offset
consumer.subscribe(Arrays.asList(topicName), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> collection) {}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> collection) {}
});
Message
- message状态:在Kafka中,消息的状态被保存在consumer中,broker不会关心哪个消息被消费了被谁消费了,只记录一个offset值(指向partition中下一个要被消费的消息位置),这就意味着如果consumer处理不好的话,broker上的一个消息可能会被消费多次。
- message持久化:Kafka中会把消息持久化到本地文件系统中,并且保持o(1)极高的效率。我们众所周知IO读取是非常耗资源的性能也是最慢的,这就是为了数据库的瓶颈经常在IO上,需要换SSD硬盘的原因。但是Kafka作为吞吐量极高的MQ,却可以非常高效的message持久化到文件。这是因为Kafka是顺序写入o(1)的时间复杂度,速度非常快。也是高吞吐量的原因。由于message的写入持久化是顺序写入的,因此message在被消费的时候也是按顺序被消费的,保证partition的message是顺序消费的。一般的机器,单机每秒100k条数据。
- message有效期:Kafka会长久保留其中的消息,以便consumer可以多次消费,当然其中很多细节是可配置的。
Kafka提供3种消息传输一致性语义:最多1次,最少1次,恰好1次。
- 最少1次 At least once:可能会重传数据,有可能出现数据被重复处理的情况;
- 最多1次 At most once:可能会出现数据丢失情况;
- 恰好1次 Exactly once:并不是指真正只传输1次,只不过有一个机制。确保不会出现“数据被重复处理”和“数据丢失”的情况。
Kafka的leader选举
Kakfa Broker集群受Zookeeper管理。所有的Kafka Broker节点一起去Zookeeper上注册一个临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Kafka Broker会成为Kafka Broker Controller,其他的Kafka broker叫Kafka Broker follower。(这个过程叫Controller在ZooKeeper注册Watch)。这个Controller会监听其他的Kafka Broker的所有信息,如果这个kafka broker controller宕机了,在zookeeper上面的那个临时节点就会消失,此时所有的kafka broker又会一起去Zookeeper上注册一个临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Kafka Broker会成为Kafka Broker Controller,其他的Kafka broker叫Kafka Broker follower。例如:一旦有一个broker宕机了,这个kafka broker controller会读取该宕机broker上所有的partition在zookeeper上的状态,并选取ISR列表中的一个replica作为partition leader(如果ISR列表中的replica全挂,选一个幸存的replica作为leader; 如果该partition的所有的replica都宕机了,则将新的leader设置为-1,等待恢复,等待ISR中的任一个Replica“活”过来,并且选它作为Leader;或选择第一个“活”过来的Replica(不一定是ISR中的)作为Leader),这个broker宕机的事情,kafka controller也会通知zookeeper,zookeeper就会通知其他的kafka broker。
Kafka 事务机制
Kafka 从 0.11 版本开始支持了事务机制。Kafka 事务机制支持了跨分区的消息原子写功能。具体来说,Kafka 生产者在同一个事务内提交到多个分区的消息,要么同时成功,要么同时失败。这一保证在生产者运行时出现异常甚至宕机重启之后仍然成立。
- producer的事务:为了实现跨分区跨会话的事务,需要引入一个全局唯一的transactionID,并将producer获得的pid和transactionID绑定。这样producer重启后就可以通过正在进行的transactionID获得原来的pid。为了管理transaction,Kafka引入了新的组件transaction coordinator。Producer就是通过和transaction coordinator交互获得了transactionID对应的任务状态。transaction coordinator还负责将事务所有写入Kafka的一个内部topic,这样即使整个服务重启,由于事务状态得以保存,事务还可以恢复继续进行。
Kafka监控
Eagle略