Kafka是有LinkedIn公司基于Scala语言开发的一个分布式消息系统,现已开源。
Kafka 的标语已经从“一个高吞吐量,分布式的消息系统”改为"一个分布式流平台"
Kafka与传统的消息系统的区别在于:
- 分布式:Kafka是一个分布式系统,易于向外拓展
- 高吞吐,低延迟:能同时为生产者和消费者提供高吞吐
- 可扩展:kafka集群支持热扩展
- 持久性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
- 容错性:允许集群中节点失败(若副本数量为n,则允许失败的节点个数为n-1)
Kafka之所以受到越来越多的青睐,与它所“扮演”的三大角色是分不开的:
- 消息系统: Kafka和传统的消息系统(也称作消息中间件) 都具备系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。与此同时,Kafka还提供了大多数消息系统难以实现的消息顺序性保障及回溯消费的功能。
- 存储系统: Kafka 把消息持久化到磁盘,相比于其他基于内存存储的系统而言,有效地降低了数据丢失的风险。也正是得益于Kafka的消息持久化功能和多副本机制,我们可以把KafRa作为长期的数据存储系统来使用,只需要把对应的数据保留策略设置为“永久”或启用主题的日志压缩功能即可。
- 流式处理平台: Kalka 不仅为每个流行的流式处理框果提供了可靠的数据来源,还提供了一个完整的流式处理类库,比如窗口、连接、变换和聚合等各类操作
一个topic可以认为一个一类消息,每个topic将被分成多个partition,每个partition在存储层面是append log文件。
任何发布到此partition的消息都会被追加到log文件的尾部,每条消息在文件中的位置称为offset(偏移量),offset为一个long型的数字,
它唯一标记一条消息。每条消息都被append到partition中,是顺序写磁盘,因此效率非常高。
每一条消息被发送到broker中,会根据partition规则选择被存储到哪一个partition。如果partition规则设置的合理,
所有消息可以均匀分布到不同的partition里,这样就实现了水平扩展。
在创建topic时可以在KAFKA_HOME/config/server.properties中指定这个partition的数量,
当然可以在topic创建之后去修改partition的数量,partition的数量只能增加不能减少。
客户端调用producer.send()进行消息发送,有如下几个步骤:
和kafka交互获取到broker的元数据信息(往哪个broker发),获取不到就抛出异常
判断消息包大小是否符合,默认为1MB,如果超过该大小则抛异常,修改配置需要和broker一起配合修改。
往客户端内部的双端队列send消息,队列默认大小为32M,当大小不够时则会阻塞,阻塞时间可配,当阻塞时间过后则会抛出异常给客户端。
同时有一个Sender线程会实时批量拉取双端队列内消息往kafka集群发送。如果发送成功,则进入回调函数,返回RecordMetadate对象。如果发送失败,
则进行重试,重试次数可配,当重试次数用完,则进入客户端定义的回调函数由客户端处理(这里失败情况下,是否需要退出进程?)
Phoenix使用Producer相关重要配置如下:
// 只有当所有参与复制的节点**全部都收到消息时,生产者才会收到一个来自服务器的成功响应
props.put(ProducerConfig.ACKS_CONFIG, "-1");
// 失败重试的次数(每次间隔100ms重试次数用完需要10多年)
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
//限制客户端在单个连接上能够发送的未响应请求的个数。设置此值是1表示kafka broker在响应请求之前client不能再向同一个broker发送请求
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);
// 缓冲区已满或元数据不可用时的阻塞时间
props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 1000);
ProducerConfig.ACKS_CONFIG
指定必须有多少个分区副本收到消息,生产者才会认为消息写入是成功的。程序中设置为-1
ack=0:生产者不会等待任何来自服务器的响应。
如果当中出现问题,导致服务器没有收到消息,那么生产者无从得知,会造成消息丢失
由于生产者不需要等待服务器的响应所以可以以网络能够支持的最大速度发送消息,从而达到很高的吞吐量
acks=1(默认值):只要集群的Leader节点收到消息,生产者就会收到一个来自服务器的成功响应
如果消息无法到达Leader节点(例如Leader节点崩溃,新的Leader节点还没有被选举出来)生产者就会收到一个错误响应,为了避免数据丢失,
生产者会重发消息
如果一个没有收到消息的节点成为新Leader,消息还是会丢失
此时的吞吐量主要取决于使用的是同步发送还是异步发送,吞吐量还受到发送中消息数量的限制,例如生产者在收到服务器响应之前可以发送多少个消息
acks=-1:只有当所有参与复制的节点全部都收到消息时,生产者才会收到一个来自服务器的成功响应
这种模式是最安全的,可以保证不止一个服务器收到消息,就算有服务器发生崩溃,整个集群依然可以运行
延时比acks=1更高,因为要等待不止一个服务器节点接收消息
ProducerConfig.RETRIES_CONFIG
生产者从服务器收到临时性错误(如分区找不到Leader)时,retries参数决定了生产者可以重发消息的次数,程序中设置为 Integer.MAX
默认情况下,生产者会在每次重试之间等待100ms,控制参数为retry.backoff.ms
可以先测试一下恢复一个崩溃节点需要多少时间,假设为T,让生产者总的重试时间比T长,否着生产者会过早地放弃重试有些错误不是临时性错误,
没办法通过重试来解决(例如消息太大),这个可以通过配置来改变producer,consumer以及broker可接受的的消息大小。
(默认可接受的单个消息大小为1MB)一般情况下,因为生产者会自动进行重试。当出现不可重试的错误或者重试次数超过上限的情况时,
现在的处理逻辑是打印错误信息,继续重试。client.id可以是任意字符串,服务器会用它来识别消息的来源,还可以用在日志和配额指标里。
ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION
Producer的另一个问题是消息的乱序问题。假设客户端代码依次执行下面的语句将两条消息发到相同的分区
producer.send(record1);
producer.send(record2);
如果此时由于某些原因(比如瞬时的网络抖动)导致record1没有成功发送,同时Kafka又配置了重试机制和max.in.flight.requests.per.connection大于1
(默认值是5,本来就是大于1的),那么重试record1成功后,record1在分区中就在record2之后,从而造成消息的乱序。
ProducerConfig.MAX_BLOCK_MS_CONFIG
缓冲队列已满或元数据不可用时的阻塞时间,超出该阻塞时间则send/forPartitions调用会抛出异常
消费者,也就是接收消息的一方。消费者连接到kafka上并接收消息。
消息订阅
kafka的订阅机制,最小粒度只到partition。不支持broker端按照消息进行过滤。现在的实现方案是针对不同的应用拉取所有的消息,
获取到消息之后再进行过滤。
在构建消费者时发现消息的订阅方式有两种,subscirbe和assign 。assign的方法不能和subscribe方法同时使用。
subscirbe:订阅指定的topic列表 assign:assign方法指定consumer实例消费topic下的具体分区,
assign的consumer不会拥有kafka的group management机制,也就是当group内消费者数量变化的时候不会有reblance行为发生。
assign的方法不能和subscribe方法同时使用。
// consumer 订阅的topic及partition
topicPartition = new TopicPartition(this.topic, this.partitionId);
this.partitions = Arrays.asList(topicPartition);
consumer.assign(partitions);
消息拉取
Kafka对外暴露了一个非常简洁的poll方法,其内部实现了协作、分区重平衡、心跳、数据拉取等功能,但使用时这些细节都被隐藏了。
poll()方法返回记录的列表,每条记录包含key/value以及主题、分区、位移信息。
消费者对象不是线程安全的,也就是不能够多个线程同时使用一个消费者对象;而且也不能够一个线程有多个消费者对象。
简而言之,一个线程一个消费者,如果需要多个消费者那么请使用多线程来进行一一对应。
位移提交
当我们调用poll()时,该方法会返回我们没有消费的消息。当消息从broker返回消费者时,broker并不跟踪这些消息是否被消费者接收到;
Kafka让消费者自身来管理消费的位移,并向消费者提供更新位移的接口。可通过consumer的配置设置自动提交,也可以手动提交。
当前采用的是手动提交的方式。手动提交分为同步和异步两种,当前采用的是同步提交的方式。
消息批量拉取
kafka中消费是基于拉模式的。可通过传入超时时间timeout来控制poll()方法的阻塞时间。
可以通过不同的配置指定消费者每次拉取的最大/最小的数据量(byte)或消息个数。
相关配置
session.timeout.ms
默认值 int 10000,当使用kafka group 管理工具时,用来决定 消费超时失败的时间。 消费者周期性的发送心跳确定,来确保对于 broker 来说,
消费者是活跃的。如果在 session 超时之前, broker 没有收到心跳请求,broker 会移除 该消费者 并 导致一次 调整 rebalance。
注意:该值必须在 broker 配置的 min.session.timeout.ms和max.session.timeout.ms 之内。
enable.auto.commit
是否允许自动提交offset,这里设置为false,采用手动提交的方式
简而言之:一个kafka集群一般包括一个或者多个kafka服务实例,一般一个kafka实例部署在一台服务器上,这样的一台服务器被称为一个Broker,
即一个节点
每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。Topic是一个抽象概念(物理上不同 Topic 的消息分开存储,
逻辑上一个 Topic 的消息虽然保存于一个或多个 broker 上,但用户只需指定消息的 Topic 即可生产或消费数据而不必关心数据存于何处)
Partition 是物理上的概念,每个 Topic 包含一个或多个 Partition。为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上。
kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序。
kafka采用顺序写的方式保证同一partition内部数据有序性,但是不能保证topic级别的消息有序。
每个 Consumer 属于一个特定的 Consumer Group(可为每个 Consumer 指定 group name,若不指定 group name 则属于默认的 group)。
这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个 topic可以有多个CG。
topic的消息会复制(不是真的复制,是概念上的)到所有的CG,但每个CG只会把消息发给该CG中的一个 consumer。如果需要实现广播,
只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。
用CG还 可以将consumer进行自由的分组而不需要多次发送消息到不同的topic
Kafka的高可靠性的保障来源于其健壮的副本(replication)机制。kafka从0.8.x版本开始为分区引入了多副本机制,
通过增加副本数量来提升数据容灾能力。同时,kafka通过多副本机制实现故障自动转移,
在kafka集群中某个broker节点失效的情况下仍然保证服务可用。在分析副本机制之前我们需要先了解几个基本概念。
分区中的所有副本统称为AR(Assigned Replicas),所有与leader副本保持一定程度的同步的副本(包括leader副本在内)组成ISR(In-Sync-Replicas),
ISR集合是AR集合中的一个子集。消息会先发送到leader副本,然后follower副本再从leader副本中拉取消息进行同步,
同步期间内follower副本相对于leader副本会有一定程度的滞后
(”一定程度“指的是可忍受的滞后范围,这个范围是可配置的,配置包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages,
0.10.x中只支持replica.lag.time.max.ms这个配置)。与leader副本同步滞后过多的副本(不包括leader副本)组成OSR(Out-of-Sync Replicas)。
AR = ISR+OSR
leader 副本负责维护和跟踪ISR集合中所有follower副本的滞后状态,当follower副本本落后太多或失效时,leader 副本会把它从ISR集合中剔除。如果OSR集合中有follower副本“追上了leader 副本,那么leader副本会把它从OSR集合转移至ISR集合。默认情况下,当leader副本发生故障时,只有在ISR集合中的副本才有资格被选举为新的leader, 而在OSR集合中的本则没有任何机会(不过这个原则也可以通过修改相应的参数配置来改变)。
如果某一个partition的所有replica都挂了,就无法保证数据不丢失了。这种情况下有两种可行的方案:
#1 false 等待ISR中任意一个replica“活”过来,并且选它作为leader
#2 true(默认)选择第一个“活”过来的replica(并不一定是在ISR中)作为leader
--config unclean.leader.election.enable=false
HW是High Watermark的缩写,俗称高水位。他表示一个特定的偏移量(offset),消费者只能拉取到这个offset之前的消息
如图,他表示一个日志文件,这个日志文件中有9条消息,第一条消息的offset(LogStartOffset)为0,最后一条消息的offset为8,
offset为9的消息用虚线框表示,代表下一条待写入的消息。日志文件的HW为6,表示消费者只能拉取到offset在0-5之间的消息,
而offset为6的消息对消费者而言是不可见的。
LEO是Log End Offset的缩写。他表示当前日志文件中下一条待写入消息的offset,如上图offset为9的位置,即为当前日志文件的LEO。
LEO的大小相当于当前日志文件中最后一条消息的offset增加1.分区ISR集合中的每一个副本都会维护自身的LEO,
而ISR集合中最小的LEO即为分区的HW,对消费者而言只能消费HW之前的消息
Kafka中topic的每个partition有一个预写式的日志文件,每个partition都由一些列有序的、不可变的消息组成,这些消息被连续的追加到partition中。
为了提高消息的可靠性,Kafka每个topic的partition有N个副本(replicas),其中N(大于等于1)是topic的复制因子(replica fator)的个数。
Kafka通过多副本机制实现故障自动转移,当Kafka集群中一个broker失效情况下仍然保证服务可用。
在Kafka中发生复制时确保partition的日志能有序地写到其他节点上,N个replicas中,其中一个replica为leader,
其他都为follower, leader处理partition的所有读写请求,与此同时,follower会被动定期地去复制leader上的数据。
Kafka提供了数据复制算法保证,如果leader发生故障或挂掉,一个新leader被选举并被接受客户端的消息成功写入
。Kafka确保从同步副本列表中选举一个副本为leader,或者说follower追赶leader数据。leader负责维护和跟踪ISR中所有follower滞后的状态。
当producer发送一条消息到broker后,leader写入消息并复制到所有follower。消息提交之后才被成功复制到所有的同步副本。
消息复制延迟受最慢的follower限制,重要的是快速检测慢副本,如果follower“落后”太多或者失效,leader将会把它从ISR中删除。
每个replica都有HW,leader和follower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,
leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费。这样就保证了如果leader所在的broker失效,
该消息仍然可以从新选举的leader中获取。对于来自内部broker的读取请求,没有HW的限制。
下图详细的说明了当producer生产消息至broker后,ISR以及HW和LEO的流转过程:
由此可见,Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的follower都复制完,
这条消息才会被commit,这种复制方式极大的影响了吞吐率。而异步复制方式下,follower异步的从leader复制数据,
数据只要被leader写入log就被认为已经commit,这种情况下如果follower都还没有复制完,落后于leader时,突然leader宕机,
则会丢失数据。而Kafka的这种使用ISR的方式则很好的均衡了确保数据不丢失以及吞吐率
当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:
1(默认):这意味着producer在ISR中的leader已成功收到数据并得到确认。如果leader宕机了,则会丢失数据。
0:这意味着producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
-1/all :producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,
比如当ISR中只有leader时,这样就变成了acks=1的情况。
如果要提高数据的可靠性,在设置request.required.acks=-1的同时,也要min.insync.replicas这个参数(可以在broker或者topic层面进行设置)的配合,
这样才能发挥最大的功效。min.insync.replicas这个参数设定ISR中的最小副本数是多少,默认值为1,当且仅当request.required.acks参数设置为-1时,
此参数才生效。如果ISR中的副本数少于min.insync.replicas配置的数量时,
客户端会返回异常:org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。
下面分别分析一下acks=1和-1的两种情况:
producer发送数据到leader,leader写本地日志成功,返回客户端成功;此时ISR中的副本还没有来得及拉取该消息,leader就宕机了,
那么此次发送的消息就会丢失。
同步(Kafka默认为同步,即producer.type=sync)的发送模式,replication.factor>=2且min.insync.replicas>=2的情况下,不会丢失数据。
有两种典型情况。acks=-1的情况下(如无特殊说明,以下acks都表示为参数request.required.acks),
数据发送到leader, ISR的follower全部完成数据同步后,leader此时挂掉,那么会选举出新的leader,数据不会丢失。
acks=-1的情况下,数据发送到leader后 ,部分ISR的副本同步,leader此时挂掉。比如follower1和follower2都有可能变成新的leader,
producer端会得到返回异常,producer端会重新发送数据,数据可能会重复。
接下来需要修改broker的配置文件$KAFKA-HOME/config/server.properties。主要关注以下几个配置参数即可。
broker.id = 0
listeners=PLAINTEXT://localhost:9092
log.dirs=…/kafka-logs
zookeeper.connect=127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002(样例)
log.cleanup.policy=compact
log.retention.hours=168
auto.create.topics.enable=false
如果是单机模式,那么修改完上述配置参数之后就可以启动服务。如果是集群模式,那么只需要对单机模式的配置文件做相应的修改即可:确保集群中每个broker的broker.id配置参数的值不一样,以及listeners配置参数也需要修改为与broker对应的IP地址或域名,之后就可以各自启动服务。注意,在启动Kafka 服务之前同样需要确保zookeeper.connect参数所配置的ZooKeeper服务已经正确启动。
启动
启动zookeeper
启动kafka
$nohup ./bin/kafka-server-start.sh config/server.properties >/dev/null 2>&1 &
节点数量
为保证消息可靠性和高可用性,推荐部署3个或以上节点
Kafka集群运维
bin/kafka-topics.sh --create --zookeeper ip:2181 --replication-factor 3 --partitions 1 --config unclean.leader.election.enable=false --config min.insync.replicas=2 --topic name
bin/kafka-console-producer.sh --broker-list ip:9092 --topic topic-name
bin/kafka-console-consumer.sh --bootstrap-server ip:9092 --from-beginning --topic topic-name
bin/kafka-topics.sh --describe --zookeeper ip:2181 --topic topic_name
bin/kafka-topics.sh --delete --zookeeper ip:2181 --topic topic_name
bin/kafka-topics.sh --alter --zookeeper ip:2181 --topic topic_name --partitions 3
bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list ip:9092 -topic command_topic --time -1
参数解析:
–relication-factor 3
–partitions 1
– config min.insync.replilcas=2
#1 false 等待ISR中任意一个replica“活”过来,并且选它作为leader
#2 true(默认)选择第一个“活”过来的replica(并不一定是在ISR中)作为leader
–config unclean.leader.election.enable=false