kafka原理

概述

Kakfa起初是由LinkedIn公司开发的一个分布式的消息系统,后成为Apache的一部分,它使用Scala和java编写,以可水平扩展和高吞吐率而被广泛使用。

特性

  • 高吞吐、低延迟:kakfa 最大的特点就是收发消息非常快,kafka 单机每秒可以处理几十万条消息,它的最低延迟只有几毫秒。
  • 高伸缩性: 每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中。
  • 持久性、可靠性: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,Kafka 底层的数据存储是基于 Zookeeper 存储的,Zookeeper 我们知道它的数据能够持久存储。
  • 容错性: 允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作
  • 高并发: 支持数千个客户端同时读写

为什么要使用消息系统

  • 解耦在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。

假如你现在系统A,这个系统会产出一个核心数据,下游系统 B和 C 都需要这个数据。

那么我们平常做的就是直接调用系统 B 和系统 C,发送数据过去。

过程如下:

kafka原理_第1张图片

 

过几天,其他的业务系统D、E 也需要这个数据,然后成了这样

kafka原理_第2张图片

 

如果后续还有系统要呢。。。

这种情况系统耦合非常的严重,如果你发送一个系统的调用失败了怎么整?

针对上面的问题,我们可以使用消息队列来实现系统解藕。

系统A 把数据发送到消息队列中,其他的系统,谁需要,自己去 消息队列 取就完了。

kafka原理_第3张图片

链接:https://juejin.im/post/5cd58d686fb9a0323416652d

  • 冗余有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的 " 插入 - 获取 - 删除 " 范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
  • 扩展性因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。
  • 灵活性 & 峰值处理能力在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。

假设你有一个系统,平时正常的时候每秒可能就几百个请求,系统部署在8核16G的机器的上,正常处理都是ok的,每秒几百请求是可以轻松抗住的。

但是如下图所示,在高峰期一下子来了每秒钟几千请求,瞬时出现了流量高峰,此时你的选择是要搞10台机器,抗住每秒几千请求的瞬时高峰吗?

那如果瞬时高峰每天就那么半个小时,接着直接就降低为了每秒就几百请求,如果你线上部署了很多台机器,那么每台机器就处理每秒几十个请求就可以了,这不是有点浪费机器资源吗?

大部分时候,每秒几百请求,一台机器就足够了,但是为了抗那每天瞬时的高峰,硬是部署了10台机器,每天就那半个小时有用,别的时候都是浪费资源的。

此时我们就可以使用消息队列来帮忙了,进行消峰。所有机器前面部署一层MQ,平时每秒几百请求大家都可以轻松接收消息。

一旦到了瞬时高峰期,一下涌入每秒几千的请求,就可以积压在MQ里面,然后那一台机器慢慢的处理和消费。

等高峰期过了,再消费一段时间,MQ里积压的数据就消费完毕了。

  • 可恢复性系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
  • 顺序保证在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka 保证一个 Partition 内的消息的有序性。
  • 缓冲在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。
  • 异步通信很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

假设你有一个系统调用的链路,系统A 调用系统B 耗时 50ms,系统B 调用系统C 又需要200ms ,系统C 调用系统 D ,需要做比较超时的操作,需要 2s,如下图所示:

kafka原理_第4张图片

 

现在上面最大的问题在于:一个用户请求过来,整个链路的调用时间是 50ms + 200ms + 2000ms = 2250ms,也就是2秒多。

而事实上,调用链路中,系统A 调用系统 B,系统B 调用系统 C 总共加起来也才 250ms,但是系统C调用系统D 却用了 2S。

正是加入系统C调用系统D 这个链路,导致系统响应时间 从 250ms 增加到了 2250 ms,足足慢了 10倍。

如果说,我们把系统D 从链路中抽离出去,让 C 系统异步调用D,那么在 B系统调用 C,C 处理完成自己的逻辑,发送一个异步的请求去调用D系统,不用阻赛等到 D系统响应了再返回。这是不是好很多了呢?

举一个例子,就以我们平常点外卖为例:

我们平常点完餐,付完款,系统然后平给账户扣款、创建订单、通知商家准备菜品。

接着,是不是需要找个骑手给你送餐?那这个找骑手的过程,是需要一套复杂算法来实现调度的,比较耗时。

那么我们是不是就可以把找骑手给你送餐的这个步骤从链路中抽离出去,做成异步化的,哪怕延迟个几十秒,但是只要在一定时间范围内给你找到一个骑手去送餐就可以了。

这样是不是就可以让你下订单点外卖的速度变得超快?支付成功之后,直接创建好订单、账户扣款、通知商家立马给你准备做菜就ok了,这个过程可能就几百毫秒。然后后台异步化的耗费可能几十秒通过调度算法给你找到一个骑手去送餐,但是这个步骤不影响我们快速下订单。

所以上面的链路也是同理,如果业务流程支持异步化的话,是不是就可以考虑把系统C对系统D的调用抽离出去做成异步化的,不要放在链路中同步依次调用。

整个过程如下:

kafka原理_第5张图片

链接:https://juejin.im/post/5cd58d686fb9a0323416652d

体系架构

kafka原理_第6张图片

 

如上图所示,一个典型的Kafka体系架构包括:

  • 若干Producer(可以是服务器日志,业务数据,页面前端产生的page view等等),Producer使用push(推)模式将消息发布到broker
  • 若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高)
  • 若干Consumer (Group),Consumer使用pull模式从broker订阅并消费消息。

 作为一个messaging system,Kafka遵循了传统的方式,选择由producer向broker push消息并由consumer从broker pull消息。一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,采用非常不同的push模式。事实上,push模式和pull模式各有优劣。
  push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。

  • 一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在consumer group发生变化时进行rebalance。

名词解释

  • Broker

Kafka集群中的服务器,一个kafka节点就是一个broker,Broker接收Producer和Consumer的请求,为消息设置偏移量,并把Message持久化到本地磁盘。每个kafka集群中会选举出一个Broker来担任集群Controller,负责处理Partition的Leader选举,协调Partition迁移等工作。

  • Topic

kafka使用topic对消息进行分类,每条消息都必须有一个topic。kafka的每条消息记录中包含一个key,一个value和一个timestamp(时间戳)。

  • Partition

一个topic可以有多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序。

kafka原理_第7张图片

Kafka以partition作为分配单位。每个partition在物理上对应一个broker上的文件夹,用户存储该partition中的消息和索引文件。例如,创建两个topic,topic1中存在5个partition,topic2中存在10个partition,则整个集群上会相应生成5+10=15个文件夹。

 partition(分区)有以下几个用途。第一,当日志大小超过了单台服务器的限制,允许日志进行扩展。每个单独的分区都必须受限于主机的文件限制,不过一个主题可能有多个分区,因此可以处理无限量的数据。第二,可以作为并行的单元集

  • Replica

消息partition 的副本,用于保障 partition 的高可用.理论上replica值越大,partition 高可用率越高,消息冗余越高。但同时会降低整体吞吐。Kafka 定义了两类副本:领导者副本(Leader Replica) 和 追随者副本(Follower Replica)。每个Partition都可以配置至少1个Replication(当仅1个Replication时即仅该Partition本身)。默认为3个副本。1个leader 副本,2个follower 副本 。

每个分区都有一台 server 作为 “leader”,零台或者多台server作为 follwers 。leader server 处理一切对 partition (分区)的读写请求,而follwers只需被动的同步leader上的数据。当leader宕机了,followers 中的一台服务器会自动成为新的 leader。每台 server 都会成为某些分区的 leader 和某些分区的 follower,因此集群的负载是平衡的。

  • ISR(In-Sync Replica)  

是Replicas的一个子集,表示目前Alive且与Leader能够“Catch-up”(可以理解为离得不远,可以追上leader的数据进度)的Replicas集合。由于读写都是首先落到Leader上,所以一般来说通过同步机制从Leader上拉取数据的Replica都会和Leader有一些延迟(包括了延迟时间和延迟条数两个维度),任意一个超过阈值都会把该Replica踢出ISR。每个Partition都有它自己独立的ISR。

在zk中会保存AR(Assigned Replicas)列表,其中包含了分区所有的副本,其中 AR = ISR+OSR

  • ISR(in sync replica):是kafka动态维护的一组同步副本,在ISR中有成员存活时,只有这个组的成员才可以成为leader,内部保存的为每次提交信息时必须同步的副本(acks = all时),每当leader挂掉时,在ISR集合中选举出一个follower作为leader提供服务,当ISR中的副本被认为坏掉的时候,会被踢出ISR,当重新跟上leader的消息数据时,重新进入ISR。
  • OSR(out sync replica): 保存的副本不必保证必须同步完成才进行确认,OSR内的副本是否同步了leader的数据,不影响数据的提交,OSR内的follower尽力的去同步leader,可能数据版本会落后。
  • Offset

消息在Partition中的编号,编号顺序不跨Partition。这个offset不是该Message在partition数据文件中的实际存储位置,而是逻辑上一个值,它唯一确定了partition中的一条Message

  • Producer

生产者负责将记录分配到topic的哪一个 partition(分区)中

  • Consumer

消费者,从broker中读取消息的客户端

  • Consumer group

消费者分组,每个consumer必须属于一个consumer group。(若不指定则属于默认的group)每条消息能被多个consumer group消费,但只能被该group中的一个consumer消费。consumer group消费全量的消息,每个consummer消费其中一部分partition

  • Consumer Lag

Kafka通过Offset标记消息发送和消费的位置。两者的差值即为Consumer Lag。 可以反应目前该Consumer Group 整体消费滞后情况,作为pub/sub消息系统,Consumer Lag 是具体到每个partition 的,每个partition 的consumer lag 总和作为整体一个consumer group 的消费滞后,每个consumer group 相互独立。

  • Log Segment

partition是分段的,每个段叫Log Segment,包括了一个.log数据文件和一个.index索引文件。数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件。数据文件是实际业务生产的消息的存储文件。默认数据文件是1G大小,超过1G后会roll out出一个新的数据文件。

  • Zookeeper

用于存储集群的meta数据、进行leader选举、故障容错等 

  • Rebalance

重平衡,消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。

kafka的数据模型

从两个维度来说明:

  • 数据分片模型
  • 多副本模型

数据分片模型(Partition)

Kafka按Topic进行数据的组织和隔离,topic按partition进行分区,partition散落存储在各个Broker上。Topic相当于一个分布式队列,Producer指定向某个Topic发送数据,消息会落到其中一个partition上,从而达到了数据分布式存储的目的。如上图所示,数据按Topic的Partition分布在Broker上,Topic 1有两个partition,分别在Broker 1和Broker 3上。

Producer生产的数据会根据一定的分区策略发送到特定的partition,Low Level 的consumer可以通过指定topic和某个partition来进行消费。一个Partition的数据写入是有序的,Partition之间是并行写入的,不保证数据的顺序性。在消费数据的时候,除了用户直接指定topic和partition来消费外,Kafka还提供了一种High Level的API,通过Consumer Group进行订阅topic来消费topic的消息。

  • Consumer Group 与Partition

    上图表示Kafka集群中有一个Topic包含4个Partition,被两个Consumer Group A和Consumer Group B消费,每个Consumer Croup包含多个Consumer。 每个Consumer Group都会消费一个Topic全量的数据,彼此之间互不干扰。同一个Consumer Group下的Consumer只能消费到一个Topic的部分数据,因此可以使用同一个Consumer Group启动多个Consumer进行并行消费,提高并行消费的能力。

    • 如果consumer数量比partition数量多,多余的consumer处于空闲状态,因为kafka的设计是在一个partition上是不允许并发的,所以consumer数量不要大于partition数量
    • 如果consumer数量比partition数量少,一个consumer会消费多个partitions,这里主要合理分配consumer数和partition数,否则会导致partition里面的数据被取的不均匀.
    • partition数量是consumer 数量的整数倍会比较好

多副本模型(Replica)

为了支持高可用,Kafka支持对一个topic的partition提供多副本,同一个partition的不同副本会存放在不同的broker上,这样当同时不可用的kafka broker数量小于副本的数目时,对应的partition仍然可以被读取数据。Kafka的多副本模型如下图描述。

一个topic的partition的多个副本中只有一个是leader,其他的都是follower,客户端的读写请求都是由leader 副本进行处理,follower从leader那里主动拉取数据进行数据同步到本地,如果follower副本超过设置的最大时间(默认为10s) 都没有向leader发出拉取请求,该follower副本都会被踢出ISR 列表,但是还是会一直同步leader副本数据,直至满足条件后再被加入。

kafka消息结构

kafka原理_第8张图片

  • offset: 消息的偏移。表明了消息在partition中的位置 
  • length:整个消息的长度,=key length + value length
  • CRC32: 消息校验码,用来判断消息的完整性
  • Magic:值是一个固定数字,用来判断这个消息是不是kafka的消息,如果不是kafka消息,则不接收
  • attributes:一些可选的属性
  • timestamp:当前消息的时间戳
  • key:无长度限制
  • value: 无长度限制

2.1 Topic & Partition

一个topic可以认为一个一类消息,每个topic将被分成多个partition,每个partition在存储层面是append log文件。任何发布到此partition的消息都会被追加到log文件的尾部,每条消息在文件中的位置称为offset(偏移量),offset为一个long型的数字,它唯一标记一条消息。每条消息都被append到partition中,是顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高???这是Kafka高吞吐率的一个很重要的保证)。

partition 是一个有序的 message 序列,这些 message 按顺序添加到一个叫做 commit log 的文件中。每个 partition 中的消息都有一个唯一的编号,称之为 offset,用来唯一标示某个分区中的message。

partition 支持消息位移读取,消息位移由消费者自身管理,比如下图:

 

kafka原理_第9张图片

 

由上图可以看出,不同消费者对同一分区的消息读取互不干扰,消费者可以通过设置消息位移(offset)来控制自己想要获取的数据,比如:可以从头读取,最新数据读取,重读读取等功能

kafka根据什么来把一个topic类型的消息分为多个partition的?

每一条消息被发送到broker中,会根据partition规则选择被存储到哪一个partition。如果partition规则设置的合理,所有消息可以均匀分布到不同的partition里,这样就实现了水平扩展。(如果一个topic对应一个文件,那这个文件所在的机器I/O将会成为这个topic的性能瓶颈,而partition解决了这个问题)。在创建topic时可以在$KAFKA_HOME/config/server.properties中指定这个partition的数量(如下所示),当然可以在topic创建之后去修改partition的数量

# The default number of log partitions per topic. More partitions allow greater
# parallelism for consumption, but this will also result in more files across
# the brokers.
num.partitions=3

对于传统的 message queue 而言,一般会删除已经被消费的消息,而 Kafka 集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要),因此 Kafka 提供两种策略删除旧数据。一是基于时间,二是基于 Partition 文件大小。例如可以通过配置 $KAFKA_HOME/config/server.properties,让 Kafka 删除一周前的数据,也可在 Partition 文件超过 1GB 时删除旧数据,配置如下所示。

	
# The minimum age of a log file to be eligible for deletion
log.retention.hours=168
# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824
# The interval at which log segments are checked to see if they can be deleted according to the retention policies
log.retention.check.interval.ms=300000
# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction.
log.cleaner.enable=false

这里要注意,因为 Kafka 读取特定消息的时间复杂度为 O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。选择怎样的删除策略只与磁盘以及具体的需求有关。另外,Kafka 会为每一个 Consumer Group 保留一些 metadata 信息——当前消费的消息的 position,也即 offset。这个 offset 由 Consumer 控制。正常情况下 Consumer 会在消费完一条消息后递增该 offset。当然,Consumer 也可将 offset 设成一个较小的值,重新消费一些消息。因为 offet 由 Consumer 控制,所以 Kafka broker 是无状态的,它不需要标记哪些消息被哪些消费过,也不需要通过 broker 去保证同一个 Consumer Group 只有一个 Consumer 能消费某一条消息,因此也就不需要锁机制,这也为 Kafka 的高吞吐率提供了有力保障。 

3 高可靠性存储分析

Kafka的高可靠性的保障来源于其健壮的副本(replication)策略。通过调节其副本相关参数,可以使得Kafka在性能和可靠性之间运转的游刃有余。Kafka从0.8.x版本开始提供partition级别的复制,replication的数量可以在$KAFKA_HOME/config/server.properties中配置(default.replication.refactor)。2.2版本设置如下:

############################# Internal Topic Settings  #############################
# The replication factor for the group metadata internal topics "__consumer_offsets" and "__transaction_state"
# For anything other than development testing, a value greater than 1 is recommended for to ensure availability such as 3.
offsets.topic.replication.factor=1

该 Replication与leader election配合提供了自动的failover机制。replication对Kafka的吞吐率是有一定影响的,但极大的增强了可用性。默认情况下,Kafka的replication数量为1。  每个partition都有一个唯一的leader,所有的读写操作都在leader上完成,leader批量从leader上pull数据。一般情况下partition的数量大于等于broker的数量,并且所有partition的leader均匀分布在broker上。follower上的日志和其leader上的完全一样。
  和大部分分布式系统一样,Kakfa处理失败需要明确定义一个broker是否alive。对于Kafka而言,Kafka存活包含两个条件,一是它必须维护与Zookeeper的session(这个通过Zookeeper的heartbeat机制来实现)。二是follower必须能够及时将leader的writing复制过来,不能“落后太多”。
  leader会track“in sync”的node list。如果一个follower宕机,或者落后太多,leader将把它从”in sync” list中移除。这里所描述的“落后太多”指follower复制的消息落后于leader后的条数超过预定值,该值可在$KAFKA_HOME/config/server.properties中配置

 

3.1 Kafka文件存储机制

Kafka中消息是以topic进行分类的,生产者通过topic向Kafka broker发送消息,消费者通过topic读取数据。然而topic在物理层面又能以partition为分组,一个topic可以分成若干个partition,那么topic以及partition又是怎么存储的呢?partition还可以细分为segment,一个partition物理上由多个segment组成,那么这些segment又是什么呢?下面我们来一一揭晓。

为了便于说明问题,假设这里只有一个Kafka集群,且这个集群只有一个Kafka broker,即只有一台物理机。在这个Kafka broker中配置($KAFKA_HOME/config/server.properties中)log.dirs=/tmp/kafka-logs,

############################# Log Basics #############################

# A comma separated list of directories under which to store log files
log.dirs=/tmp/kafka-logs

以此来设置Kafka消息文件存储目录,与此同时创建一个topic:topic_zzh_test,partition的数量为4($KAFKA_HOME/bin/kafka-topics.sh –create –zookeeper localhost:2181 –partitions 4 –topic topic_vms_test –replication-factor 4)。那么我们此时可以在/tmp/kafka-logs目录中可以看到生成了4个目录:

[root@slave1] /usr/local/kafka_2.12-2.2.0$ ll  -d /tmp/kafka-logs/topic_vms_test*
drwxr-xr-x 2 root root 141 May 24 11:21 /tmp/kafka-logs/topic_vms_test-0
drwxr-xr-x 2 root root 141 May 24 11:21 /tmp/kafka-logs/topic_vms_test-1
drwxr-xr-x 2 root root 141 May 24 11:21 /tmp/kafka-logs/topic_vms_test-2
drwxr-xr-x 2 root root 141 May 24 11:21 /tmp/kafka-logs/topic_vms_test-3

目录中的内容如下 

[root@slave1] /usr/local/kafka_2.12-2.2.0$ ll  /tmp/kafka-logs/topic_vms_test*
/tmp/kafka-logs/topic_vms_test-0:
total 4
-rw-r--r-- 1 root root 10485760 May 24 11:21 00000000000000000000.index
-rw-r--r-- 1 root root        0 May 24 11:21 00000000000000000000.log
-rw-r--r-- 1 root root 10485756 May 24 11:21 00000000000000000000.timeindex
-rw-r--r-- 1 root root        8 May 24 11:21 leader-epoch-checkpoint

/tmp/kafka-logs/topic_vms_test-1:
total 4
-rw-r--r-- 1 root root 10485760 May 24 11:21 00000000000000000000.index
-rw-r--r-- 1 root root        0 May 24 11:21 00000000000000000000.log
-rw-r--r-- 1 root root 10485756 May 24 11:21 00000000000000000000.timeindex
-rw-r--r-- 1 root root        8 May 24 11:21 leader-epoch-checkpoint

/tmp/kafka-logs/topic_vms_test-2:
total 4
-rw-r--r-- 1 root root 10485760 May 24 11:21 00000000000000000000.index
-rw-r--r-- 1 root root        0 May 24 11:21 00000000000000000000.log
-rw-r--r-- 1 root root 10485756 May 24 11:21 00000000000000000000.timeindex
-rw-r--r-- 1 root root        8 May 24 11:21 leader-epoch-checkpoint

/tmp/kafka-logs/topic_vms_test-3:
total 4
-rw-r--r-- 1 root root 10485760 May 24 11:21 00000000000000000000.index
-rw-r--r-- 1 root root        0 May 24 11:21 00000000000000000000.log
-rw-r--r-- 1 root root 10485756 May 24 11:21 00000000000000000000.timeindex
-rw-r--r-- 1 root root        8 May 24 11:21 leader-epoch-checkpoint

在Kafka文件存储中,同一个topic下有多个不同的partition,每个partiton为一个目录,partition的名称规则为:topic名称+有序序号,第一个序号从0开始计,最大的序号为partition数量减1,partition是实际物理上的概念,而topic是逻辑上的概念。

上面提到partition还可以细分为segment,这个segment又是什么?如果就以partition为最小存储单位,我们可以想象当Kafka producer不断发送消息,必然会引起partition文件的无限扩张,这样对于消息文件的维护以及已经被消费的消息的清理带来严重的影响,所以这里以segment为单位又将partition细分。每个partition(目录)相当于一个巨型文件被平均分配到多个大小相等的segment(段)数据文件中(每个segment 文件中消息数量不一定相等)这种特性也方便old segment的删除,即方便已被消费的消息的清理,提高磁盘的利用率。每个partition只需要支持顺序读写就行,segment的文件生命周期由服务端配置参数(log.segment.bytes,log.roll.{ms,hours}等若干参数)决定。

# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824

segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值,数值大小为64位,20位数字字符长度,没有数字用0填充,如下: 

00000000000000000000.index
00000000000000000000.log
00000000000000170410.index
00000000000000170410.log
00000000000000239430.index
00000000000000239430.log

以上面的segment文件为例,展示出segment:00000000000000170410的“.index”文件和“.log”文件的对应的关系,如下图: 

如上图,“.index”索引文件存储大量的元数据,“.log”数据文件存储大量的消息,索引文件中的元数据指向对应数据文件中message的物理偏移地址。其中以“.index”索引文件中的元数据[3, 348]为例,在“.log”数据文件表示第3个消息,即在全局partition中表示170410+3=170413个消息,该消息的物理偏移地址为348。

那么如何从partition中通过offset查找message呢?

以上图为例,读取offset=170418的消息,首先查找segment文件,其中00000000000000000000.index为最开始的文件,第二个文件为00000000000000170410.index(起始偏移为170410+1=170411),而第三个文件为00000000000000239430.index(起始偏移为239430+1=239431),所以这个offset=170418就落到了第二个文件之中。其他后续文件可以依次类推,以其实偏移量命名并排列这些文件,然后根据二分查找法就可以快速定位到具体文件位置。其次根据00000000000000170410.index文件中的[8,1325]定位到00000000000000170410.log文件中的1325的位置进行读取。

要是读取offset=170418的消息,从00000000000000170410.log文件中的1325的位置进行读取,那么怎么知道何时读完本条消息,否则就读到下一条消息的内容了?
这个就需要联系到消息的物理结构了,消息都具有固定的物理结构,包括:offset(8 Bytes)、消息体的大小(4 Bytes)、crc32(4 Bytes)、magic(1 Byte)、attributes(1 Byte)、key length(4 Bytes)、key(K Bytes)、payload(N Bytes)等等字段,可以确定一条消息的大小,即读取到哪里截止。

3.2 复制原理和同步方式

Kafka中topic的每个partition有一个预写式的日志文件,虽然partition可以继续细分为若干个segment文件,但是对于上层应用来说可以将partition看成最小的存储单元(一个有多个segment文件拼接的“巨型”文件),每个partition都由一些列有序的、不可变的消息组成,这些消息被连续的追加到partition中。

上图中有两个新名词:HW和LEO。这里先介绍下LEO,LogEndOffset的缩写,表示每个partition的log最后一条Message的位置。HW是HighWatermark的缩写,是指consumer能够看到的此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集群中有4个broker, 某topic有3个partition,且复制因子即副本个数也为3:

 Kafka提供了数据复制算法保证,如果leader发生故障或挂掉,一个新leader被选举并被接受客户端的消息成功写入。Kafka确保从同步副本列表中选举一个副本为leader,或者说follower追赶leader数据。leader负责维护和跟踪ISR(In-Sync Replicas的缩写,表示副本同步队列,具体可参考下节)中所有follower滞后的状态。当producer发送一条消息到broker后,leader写入消息并复制到所有follower。消息提交之后才被成功复制到所有的同步副本。消息复制延迟受最慢的follower限制,重要的是快速检测慢副本,如果follower“落后”太多或者失效,leader将会把它从ISR中删除。

############################# Internal Topic Settings  #############################
# The replication factor for the group metadata internal topics "__consumer_offsets" and "__transaction_state"
# For anything other than development testing, a value greater than 1 is recommended for to ensure availability such as 3.
offsets.topic.replication.factor=1
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1

3.3 ISR

上节我们涉及到ISR (In-Sync Replicas),这个是指副本同步队列。副本数对Kafka的吞吐率是有一定的影响,但极大的增强了可用性。默认情况下Kafka的replica数量为1,即每个partition都有一个唯一的leader,为了确保消息的可靠性,通常应用中将其值(由broker的参数offsets.topic.replication.factor指定)大小设置为大于1,比如3。 所有的副本(replicas)统称为Assigned Replicas,即AR。ISR是AR中的一个子集,由leader维护ISR列表,follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。

Kafka 0.10.x版本后移除了replica.lag.max.messages参数,只保留了replica.lag.time.max.ms作为ISR中副本管理的参数。为什么这样做呢?replica.lag.max.messages表示当前某个副本落后leaeder的消息数量超过了这个参数的值,那么leader就会把follower从ISR中删除。假设设置replica.lag.max.messages=4,那么如果producer一次传送至broker的消息数量都小于4条时,因为在leader接受到producer发送的消息之后而follower副本开始拉取这些消息之前,follower落后leader的消息数不会超过4条消息,故此没有follower移出ISR,所以这时候replica.lag.max.message的设置似乎是合理的。但是producer发起瞬时高峰流量,producer一次发送的消息超过4条时,也就是超过replica.lag.max.messages,此时follower都会被认为是与leader副本不同步了,从而被踢出了ISR。但实际上这些follower都是存活状态的且没有性能问题。那么在之后追上leader,并被重新加入了ISR。于是就会出现它们不断地剔出ISR然后重新回归ISR,这无疑增加了无谓的性能损耗。而且这个参数是broker全局的。设置太大了,影响真正“落后”follower的移除;设置的太小了,导致follower的频繁进出。无法给定一个合适的replica.lag.max.messages的值,故此,新版本的Kafka移除了这个参数。

注:ISR中包括:leader和follower。

上面一节还涉及到一个概念,即HW。HW俗称高水位,HighWatermark的缩写,取一个partition对应的ISR中最小的LEO作为HW,consumer最多只能消费到HW所在的位置。另外每个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的方式则很好的均衡了确保数据不丢失以及吞吐率。

Kafka的ISR的管理最终都会反馈到Zookeeper节点上。具体位置为:/brokers/topics/[topic]/partitions/[partition]/state。目前有两个地方会对这个Zookeeper的节点进行维护:

  1. Controller来维护:Kafka集群中的其中一个Broker会被选举为Controller,主要负责Partition管理和副本状态管理,也会执行类似于重分配partition之类的管理任务。在符合某些特定条件下,Controller下的LeaderSelector会选举新的leader,ISR和新的leader_epoch及controller_epoch写入Zookeeper的相关节点中。同时发起LeaderAndIsrRequest通知所有的replicas。
  2. leader来维护:leader有单独的线程定期检测ISR中follower是否脱离ISR, 如果发现ISR变化,则会将新的ISR的信息返回到Zookeeper的相关节点中。

3.4 数据可靠性和持久性保证

当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:

  • 1(默认):这意味着producer在ISR中的leader已成功收到的数据并得到确认后发送下一条message。如果leader宕机了,则会丢失数据。
  • 0:这意味着producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
  • -1:producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当ISR中只有leader时(前面ISR那一节讲到,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的两种情况进行详细分析:

1. request.required.acks=1

producer发送数据到leader,leader写本地日志成功,返回客户端成功;此时ISR中的副本还没有来得及拉取该消息,leader就宕机了,那么此次发送的消息就会丢失。

2. request.required.acks=-1

同步(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此时挂掉。比如follower1h和follower2都有可能变成新的leader, producer端会得到返回异常,producer端会重新发送数据,数据可能会重复。

当然上图中如果在leader crash的时候,follower2还没有同步到任何数据,而且follower2被选举为新的leader的话,这样消息就不会重复。

注:Kafka只处理fail/recover问题,不处理Byzantine问题。

3.5 关于HW的进一步探讨

考虑上图(即acks=-1,部分ISR副本同步)中的另一种情况,如果在Leader挂掉的时候,follower1同步了消息4,5,follower2同步了消息4,与此同时follower2被选举为leader,那么此时follower1中的多出的消息5该做如何处理呢?

这里就需要HW的协同配合了。如前所述,一个partition中的ISR列表中,leader的HW是所有ISR列表里副本中最小的那个的LEO。类似于木桶原理,水位取决于最低那块短板。

如上图,某个topic的某partition有三个副本,分别为A、B、C。A作为leader肯定是LEO最高,B紧随其后,C机器由于配置比较低,网络比较差,故而同步最慢。这个时候A机器宕机,这时候如果B成为leader,假如没有HW,在A重新恢复之后会做同步(makeFollower)操作,在宕机时log文件之后直接做追加操作,而假如B的LEO已经达到了A的LEO,会产生数据不一致的情况,所以使用HW来避免这种情况。

A在做同步操作的时候,先将log文件截断到之前自己的HW的位置,即3,之后再从B中拉取消息进行同步。

如果失败的follower恢复过来,它首先将自己的log文件截断到上次checkpointed时刻的HW的位置,之后再从leader中同步消息。leader挂掉会重新选举,新的leader会发送“指令”让其余的follower截断至自身的HW的位置然后再拉取新的消息。

当ISR中的个副本的LEO不一致时,如果此时leader挂掉,选举新的leader时并不是按照LEO的高低进行选举,而是按照ISR中的顺序选举。

3.6 Leader选举

一条消息只有被ISR中的所有follower都从leader复制过去才会被认为已提交。这样就避免了部分数据被写进了leader,还没来得及被任何follower复制就宕机了,而造成数据丢失。而对于producer而言,它可以选择是否等待消息commit,这可以通过request.required.acks来设置。这种机制确保了只要ISR中有一个或者以上的follower,一条被commit的消息就不会丢失。

有一个很重要的问题是当leader宕机了,怎样在follower中选举出新的leader,因为follower可能落后很多或者直接crash了,所以必须确保选择“最新”的follower作为新的leader。一个基本的原则就是,如果leader不在了,新的leader必须拥有原来的leader commit的所有消息。这就需要做一个折中,如果leader在表名一个消息被commit前等待更多的follower确认,那么在它挂掉之后就有更多的follower可以成为新的leader,但这也会造成吞吐率的下降。

一种非常常用的选举leader的方式是“少数服从多数”,Kafka并不是采用这种方式。这种模式下,如果我们有2f+1个副本,那么在commit之前必须保证有f+1个replica复制完消息,同时为了保证能正确选举出新的leader,失败的副本数不能超过f个。这种方式有个很大的优势,系统的延迟取决于最快的几台机器,也就是说比如副本数为3,那么延迟就取决于最快的那个follower而不是最慢的那个。“少数服从多数”的方式也有一些劣势,为了保证leader选举的正常进行,它所能容忍的失败的follower数比较少,如果要容忍1个follower挂掉,那么至少要3个以上的副本,如果要容忍2个follower挂掉,必须要有5个以上的副本。也就是说,在生产环境下为了保证较高的容错率,必须要有大量的副本,而大量的副本又会在大数据量下导致性能的急剧下降。这种算法更多用在Zookeeper这种共享集群配置的系统中而很少在需要大量数据的系统中使用的原因。HDFS的HA功能也是基于“少数服从多数”的方式,但是其数据存储并不是采用这样的方式。

实际上,leader选举的算法非常多,比如Zookeeper的Zab、Raft以及Viewstamped Replication。而Kafka所使用的leader选举算法更像是微软的PacificA算法。

Kafka在Zookeeper中为每一个partition动态的维护了一个ISR,这个ISR里的所有replica都跟上了leader,只有ISR里的成员才能有被选为leader的可能(unclean.leader.election.enable=false)。在这种模式下,对于f+1个副本,一个Kafka topic能在保证不丢失已经commit消息的前提下容忍f个副本的失败,在大多数使用场景下,这种模式是十分有利的。事实上,为了容忍f个副本的失败,“少数服从多数”的方式和ISR在commit前需要等待的副本的数量是一样的,但是ISR需要的总的副本的个数几乎是“少数服从多数”的方式的一半。

上文提到,在ISR中至少有一个follower时,Kafka可以确保已经commit的数据不丢失,但如果某一个partition的所有replica都挂了,就无法保证数据不丢失了。这种情况下有两种可行的方案:

  1. 等待ISR中任意一个replica“活”过来,并且选它作为leader
  2. 选择第一个“活”过来的replica(并不一定是在ISR中)作为leader

这就需要在可用性和一致性当中作出一个简单的抉择。如果一定要等待ISR中的replica“活”过来,那不可用的时间就可能会相对较长。而且如果ISR中所有的replica都无法“活”过来了,或者数据丢失了,这个partition将永远不可用。选择第一个“活”过来的replica作为leader,而这个replica不是ISR中的replica,那即使它并不保障已经包含了所有已commit的消息,它也会成为leader而作为consumer的数据源。默认情况下,Kafka采用第二种策略,即unclean.leader.election.enable=true,也可以将此参数设置为false来启用第一种策略。

unclean.leader.election.enable这个参数对于leader的选举、系统的可用性以及数据的可靠性都有至关重要的影响。下面我们来分析下几种典型的场景。

如果上图所示,假设某个partition中的副本数为3,replica-0, replica-1, replica-2分别存放在broker0, broker1和broker2中。AR=(0,1,2),ISR=(0,1)。

设置request.required.acks=-1, min.insync.replicas=2,unclean.leader.election.enable=false。这里讲broker0中的副本也称之为broker0起初broker0为leader,broker1为follower。

1. 当ISR中的replica-0出现crash的情况时,broker1选举为新的leader[ISR=(1)],因为受min.insync.replicas=2影响,write不能服务,但是read能继续正常服务。此种情况恢复方案:

  • 尝试恢复(重启)replica-0,如果能起来,系统正常;
  • 如果replica-0不能恢复,需要将min.insync.replicas设置为1,恢复write功能。

2. 当ISR中的replica-0出现crash,紧接着replica-1也出现了crash, 此时[ISR=(1),leader=-1],不能对外提供服务,此种情况恢复方案:

  • 尝试恢复replica-0和replica-1,如果都能起来,则系统恢复正常;
  • 如果replica-0起来,而replica-1不能起来,这时候仍然不能选出leader,因为当设置unclean.leader.election.enable=false时,leader只能从ISR中选举,当ISR中所有副本都失效之后,需要ISR中最后失效的那个副本能恢复之后才能选举leader, 即replica-0先失效,replica-1后失效,需要replica-1恢复后才能选举leader。保守的方案建议把unclean.leader.election.enable设置为true,但是这样会有丢失数据的情况发生,这样可以恢复read服务。同样需要将min.insync.replicas设置为1,恢复write功能;
  • replica-1恢复,replica-0不能恢复,这个情况上面遇到过,read服务可用,需要将min.insync.replicas设置为1,恢复write功能;
  • replica-0和replica-1都不能恢复,这种情况可以参考情形2.

3. 当ISR中的replica-0, replica-1同时宕机,此时[ISR=(0,1)],不能对外提供服务,此种情况恢复方案:尝试恢复replica-0和replica-1,当其中任意一个副本恢复正常时,对外可以提供read服务。直到2个副本恢复正常,write功能才能恢复,或者将将min.insync.replicas设置为1。

3.7 Kafka的发送模式

Kafka的发送模式由producer端的配置参数producer.type来设置,这个参数指定了在后台线程中消息的发送方式是同步的还是异步的,默认是同步的方式,即producer.type=sync。如果设置成异步的模式,即producer.type=async,可以是producer以batch的形式push数据,这样会极大的提高broker的性能,但是这样会增加丢失数据的风险。如果需要确保消息的可靠性,必须要将producer.type设置为sync。

对于异步模式,还有4个配套的参数,如下:

以batch的方式推送数据可以极大的提高处理效率,kafka producer可以将消息在内存中累计到一定数量后作为一个batch发送请求。batch的数量大小可以通过producer的参数(batch.num.messages)控制。通过增加batch的大小,可以减少网络请求和磁盘IO的次数,当然具体参数设置需要在效率和时效性方面做一个权衡。在比较新的版本中还有batch.size这个参数。

4 高可靠性使用分析

4.1 消息传输保障

前面已经介绍了Kafka如何进行有效的存储,以及了解了producer和consumer如何工作。接下来讨论的是Kafka如何确保消息在producer和consumer之间传输。有以下三种可能的传输保障(delivery guarantee):

  • At most once: 消息可能会丢,但绝不会重复传输,要传就只传一次,要不传就一次都别传。
  • At least once:消息绝不会丢,但可能会重复传输,最少会被传一次。
  • Exactly once:每条消息肯定会被传输一次且仅传输一次。这是我们期望的状态。

Kafka的消息传输保障机制非常直观。当producer向broker发送消息时,一旦这条消息被commit,由于副本机制(replication)的存在,它就不会丢失。但是如果producer发送数据给broker后,遇到的网络问题而造成通信中断,那producer就无法判断该条消息是否已经提交(commit)。虽然Kafka无法确定网络故障期间发生了什么,但是producer可以retry多次,确保消息已经正确传输到broker中,所以目前Kafka实现的是at least once。

consumer从broker中读取消息后,可以选择commit,该操作会在Zookeeper中存下该consumer在该partition下读取的消息的offset。该consumer下一次再读该partition时会从下一条开始读取。如未commit,下一次读取的开始位置会跟上一次commit之后的开始位置相同。当然也可以将consumer设置为autocommit,即consumer一旦读取到数据立即自动commit。如果只讨论这一读取消息的过程,那Kafka是确保了exactly once, 但是如果由于前面producer与broker之间的某种原因导致消息的重复,那么这里就是at least once。

考虑这样一种情况,当consumer读完消息之后先commit再处理消息,在这种模式下,如果consumer在commit后还没来得及处理消息就crash了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于at most once了。

读完消息先处理再commit。这种模式下,如果处理完了消息在commit之前consumer crash了,下次重新开始工作时还会处理刚刚未commit的消息,实际上该消息已经被处理过了,这就对应于at least once。

要做到exactly once就需要引入消息去重机制。

4.2 消息去重

如上一节所述,Kafka在producer端和consumer端都会出现消息的重复,这就需要去重处理。

Kafka文档中提及GUID(Globally Unique Identifier)的概念,通过客户端生成算法得到每个消息的unique id,同时可映射至broker上存储的地址,即通过GUID便可查询提取消息内容,也便于发送方的幂等性保证,需要在broker上提供此去重处理模块,目前版本尚不支持。

针对GUID, 如果从客户端的角度去重,那么需要引入集中式缓存,必然会增加依赖复杂度,另外缓存的大小难以界定。

不只是Kafka, 类似RabbitMQ以及RocketMQ这类商业级中间件也只保障at least once, 且也无法从自身去进行消息去重。所以我们建议业务方根据自身的业务特点进行去重,比如业务消息本身具备幂等性,或者借助Redis等其他产品进行去重处理。

4.3 高可靠性配置

Kafka提供了很高的数据冗余弹性,对于需要数据高可靠性的场景,我们可以增加数据冗余备份数(replication.factor),调高最小写入副本数的个数(min.insync.replicas)等等,但是这样会影响性能。反之,性能提高而可靠性则降低,用户需要自身业务特性在彼此之间做一些权衡性选择。

要保证数据写入到Kafka是安全的,高可靠的,需要如下的配置:

  • topic的配置:replication.factor>=3,即副本数至少是3个;2<=min.insync.replicas<=replication.factor
  • broker的配置:leader的选举条件unclean.leader.election.enable=false
  • producer的配置:request.required.acks=-1(all),producer.type=sync

业务场景

  • 适合的业务场景:
    • 需要使用消息中间件,实现上下游系统的解耦,并需要使用发布、订阅模式,需要多次订阅重复消费的业务
    • 消息量非常大,数据可靠性要求没有非常严格,可以接受极端情况下消息丢失、重复的业务
  • 不适合的业务场景:
    • 对消息可靠性非常高,不允许任何消息丢失的关键业务系统; 例如: 支付,订单;建议选择ActiveMQ, RocketMQ

http://www.jasongj.com/2015/01/02/Kafka%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90/

kafka是如何保证消息的高可靠性的?

  • replication多副本冗余
  • ISR副本间同步
  • leader选举

kafka是怎么实现高吞吐率的?

Kafka是分布式消息系统,需要处理海量的消息,Kafka的设计是把所有的消息都写入速度低容量大的硬盘,以此来换取更强的存储能力,但实际上,使用硬盘并没有带来过多的性能损失。

kafka主要使用了以下几个方式实现了超高的吞吐率:

  • 顺序读写

kafka的消息是不断追加到文件中的,这个特性使kafka可以充分利用磁盘的顺序读写性能

顺序读写不需要硬盘磁头的寻道时间,只需很少的扇区旋转时间,所以速度远快于随机读写

Kafka官方给出了测试数据(Raid-5,7200rpm):

顺序 I/O: 600MB/s

随机 I/O: 100KB/s

  • 零拷贝(zero-copy)

先简单了解下文件系统的操作流程,例如一个程序要把文件内容发送到网络

这个程序是工作在用户空间,文件和网络socket属于硬件资源,两者之间有一个内核空间

在操作系统内部,整个过程为:

kafka原理_第10张图片

在Linux kernel2.2 之后出现了一种叫做"零拷贝(zero-copy)"系统调用机制,就是跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区”

系统上下文切换减少为2次,可以提升一倍的性能

kafka原理_第11张图片

  • 文件分段

kafka的队列topic被分为了多个区partition,每个partition又分为多个段segment,所以一个队列中的消息实际上是保存在N多个片段文件中

kafka原理_第12张图片

通过分段的方式,每次文件操作都是对一个小文件的操作,非常轻便,同时也增加了并行处理能力

  • 批量发送

Kafka允许进行批量发送消息,先将消息缓存在内存中,然后一次请求批量发送出去

比如可以指定缓存的消息达到某个量的时候就发出去,或者缓存了固定的时间后就发送出去

如100条消息就发送,或者每5秒发送一次

这种策略将大大减少服务端的I/O次数

  • 数据压缩

Kafka还支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩

压缩的好处就是减少传输的数据量,减轻对网络传输的压力

Producer压缩之后,在Consumer需进行解压,虽然增加了CPU的工作,但在对大数据处理上,瓶颈在网络上而不是CPU,所以这个成本很值得

Web管理界面

KafkaOffsetMonitor

常见消息队列比较

  • RabbitMQ
    RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。

  • Redis
    Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。

  • ZeroMQ
    ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演了这个服务角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但是ZeroMQ仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter的Storm 0.9.0以前的版本中默认使用ZeroMQ作为数据流的传输(Storm从0.9版本开始同时支持ZeroMQ和Netty作为传输模块)。

  • ActiveMQ
    ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。

  • Kafka/Jafka
    Kafka是Apache下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡;支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制来统一了在线和离线的消息处理。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。

kafka集群安装

环境

  • slave1 192.168.255.121
  • slave2 192.168.255.122
  • slave3 192.168.255.123
  • java环境
  • (可选)zookeeper环境

 安装步骤 

1、下载解压

wget  http://mirrors.tuna.tsinghua.edu.cn/apache/kafka/1.0.1/kafka_2.12-1.0.1.tgz -P /opt/software

[root@slave1] /usr/local$ tar -zxvf kafka_2.12-1.0.1.tgz -C /usr/local

[root@slave1] /usr/local$ ln -s kafka_2.12-1.0.1 kafka

2、修改zookeeper配置

kafka依赖zookeeper,所以先建立zk集群(部署zk集群,节点数最少为3个),直接使用kafka自带的zookeeper建立zk集群,

修改zookeeper.properties文件,所有节点配置相同

vim  /usr/local/kafka/config/zookeeper.properties

dataDir=/usr/local/kafka/zookeeper  #数据文件路径,存储myid文件,需要手动创建
dataLogDir=/var/log/kafka/zookeeper  #日志路径,需要手动创建
# the port at which the clients will connect
clientPort=2181
# disable the per-ip limit on the number of connections since this is a non-production config
maxClientCnxns=0

server.1=192.168.255.121:2888:3888
server.2=192.168.255.122:2888:3888
server.3=192.168.255.123:2888:3888

 创建myid文件,进入/usr/local/kafka/zookeeper,创建myid文件,将三个节点的myid文件分别写入1,2,3,(myid是zk集群用来发现彼此的标识,必须创建,且不能相同)  

[root@slave1] /usr/local/kafka$ mkdir zookeeper
[root@slave1] /usr/local/kafka$ cat myid 
1

3、修改kafka配置

vim /server.properties ,分别修改各个节点的不同的值

broker.id=0        #每个实例不一样,从0依次分配
port=9092         #将listeners改为端口
host.name=192.168.255.121     #将advertised.host.name改为host.name
num.network.threads=3
num.io.threads=8
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
socket.request.max.bytes=104857600
log.dirs=/var/log/kafka                                  #需手动创建,kafka并不会根据配置文件自动创建
num.partitions=1
num.recovery.threads.per.data.dir=1
log.retention.hours=168
log.segment.bytes=1073741824
log.retention.check.interval.ms=300000
zookeeper.connect=192.168.255.121:2181,192.168.255.122:2181,192.168.255.123:2181         
zookeeper.connection.timeout.ms=6000
delete.topic.enable=true
auto.create.topics.enable=false

4、启动

(1)先启动zookeeper,所有节点都要执行启动,这里注意一下,启动第一台和第二台节点的时候,可能会报错"Cannot open channel",这是正常情况,因为其他节点还没有启动。

bin/zookeeper-server-start.sh config/zookeeper.properties

[2018-11-13 07:57:41,263] INFO Reading configuration from: /usr/local/kafka/config/zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig)
[2018-11-13 07:57:41,285] INFO Resolved hostname: 192.168.255.123 to address: /192.168.255.123 (org.apache.zookeeper.server.quorum.QuorumPeer)
[2018-11-13 07:57:41,285] INFO Resolved hostname: 192.168.255.122 to address: /192.168.255.122 (org.apache.zookeeper.server.quorum.QuorumPeer)
[2018-11-13 07:57:41,286] INFO Resolved hostname: 192.168.255.121 to address: 

......

[2018-11-13 07:57:53,719] INFO Getting a diff from the leader 0x400000036 (org.apache.zookeeper.server.quorum.Learner)
[2018-11-13 07:57:55,889] INFO Received connection request /192.168.255.123:50880 (org.apache.zookeeper.server.quorum.QuorumCnxManager)
[2018-11-13 07:57:55,896] INFO Notification: 1 (message format version), 3 (n.leader), 0x400000036 (n.zxid), 0x1 (n.round), LOOKING (n.state), 3 (n.sid), 0x4 (n.peerEpoch) FOLLOWING (my state) (org.apache.zookeeper.server.quorum.FastLeaderElection)
[2018-11-13 07:57:55,903] INFO Notification: 1 (message format version), 2 (n.leader), 0x400000036 (n.zxid), 0x3 (n.round), LOOKING (n.state), 3 (n.sid), 0x5 (n.peerEpoch) FOLLOWING (my state) (org.apache.zookeeper.server.quorum.FastLeaderElection)
[2018-11-13 07:58:53,549] WARN Got zxid 0x600000001 expected 0x1 (org.apache.zookeeper.server.quorum.Learner)
[2018-11-13 07:58:53,549] INFO Creating new log file: log.600000001 (org.apache.zookeeper.server.persistence.FileTxnLog)

(2)再启动kafka,所有节点都要执行启动

[root@slave1] /var/log/kafka$ nohup /usr/local/kafka/bin/kafka-server-start.sh  /usr/local/kafka/config/server.properties > /var/log/kafka/startup.log 2>1 &
[1] 41595
[root@slave1] /var/log/kafka$ cat startup.log 
[2018-11-13 08:27:02,738] INFO KafkaConfig values: 
	advertised.host.name = null
	advertised.listeners = null
	advertised.port = null
	alter.config.policy.class.name = null
	authorizer.class.name = 

	......

[2018-11-13 08:27:04,121] INFO [ProducerId Manager 0]: Acquired new producerId block (brokerId:0,blockStartProducerId:0,blockEndProducerId:999) by writing to Zk with path version 1 (kafka.coordinator.transaction.ProducerIdManager)
[2018-11-13 08:27:04,162] INFO [TransactionCoordinator id=0] Starting up. (kafka.coordinator.transaction.TransactionCoordinator)
[2018-11-13 08:27:04,261] INFO [TransactionCoordinator id=0] Startup complete. (kafka.coordinator.transaction.TransactionCoordinator)
[2018-11-13 08:27:04,274] INFO [Transaction Marker Channel Manager 0]: Starting (kafka.coordinator.transaction.TransactionMarkerChannelManager)
[2018-11-13 08:27:04,490] INFO Creating /brokers/ids/0 (is it secure? false) (kafka.utils.ZKCheckedEphemeral)
[2018-11-13 08:27:04,500] INFO Result of znode creation is: OK (kafka.utils.ZKCheckedEphemeral)
[2018-11-13 08:27:04,502] INFO Registered broker 0 at path /brokers/ids/0 with addresses: EndPoint(192.168.255.121,9092,ListenerName(PLAINTEXT),PLAINTEXT) (kafka.utils.ZkUtils)
[2018-11-13 08:27:04,503] WARN No meta.properties file under dir /var/log/kafka/meta.properties (kafka.server.BrokerMetadataCheckpoint)
[2018-11-13 08:27:04,580] INFO Kafka version : 1.0.1 (org.apache.kafka.common.utils.AppInfoParser)
[2018-11-13 08:27:04,580] INFO Kafka commitId : c0518aa65f25317e (org.apache.kafka.common.utils.AppInfoParser)
[2018-11-13 08:27:04,597] INFO [KafkaServer id=0] started (kafka.server.KafkaServer)

 

5、测试

(1)、在slave1创建一个名为test的topic

[root@slave1] ~$ /usr/local/kafka/bin/kafka-topics.sh --create --zookeeper 192.168.255.121:2181,192.168.255.122:2181,192.168.255.123:2181 --replication-factor 3 --partitions 3 --topic test
Created topic "test".

(2)、查看topic

[root@slave1] /usr/local/kafka$ bin/kafka-topics.sh --list --zookeeper 192.168.255.121:2181
test

(3)、slave2和slave3上创建consumer

[root@slave2] /usr/local/kafka$ bin/kafka-console-consumer.sh --zookeeper 192.168.255.121:2181 --topic test --from-beginning
Using the ConsoleConsumer with old consumer is deprecated and will be removed in a future major release. Consider using the new consumer by passing [bootstrap-server] instead of [zookeeper].


[root@slave3] /usr/local/kafka$ bin/kafka-console-consumer.sh --zookeeper 192.168.255.121:2181 --topic test --from-beginning
Using the ConsoleConsumer with old consumer is deprecated and will be removed in a future major release. Consider using the new consumer by passing [bootstrap-server] instead of [zookeeper].


(4)、slave1 上创建producer,并输入一些消息

[root@slave1] /usr/local/kafka$ bin/kafka-console-producer.sh --broker-list 192.168.255.121:9092 --topic test
>111
>222
>

(5)、在消费者节点上可以看到消息已传输过来

如下图

[root@slave2] /usr/local/kafka$ bin/kafka-console-consumer.sh --zookeeper 192.168.255.121:2181 --topic test --from-beginning
Using the ConsoleConsumer with old consumer is deprecated and will be removed in a future major release. Consider using the new consumer by passing [bootstrap-server] instead of [zookeeper].
111
222


[root@slave3] /usr/local/kafka$ bin/kafka-console-consumer.sh --zookeeper 192.168.255.121:2181 --topic test --from-beginning
Using the ConsoleConsumer with old consumer is deprecated and will be removed in a future major release. Consider using the new consumer by passing [bootstrap-server] instead of [zookeeper].
111
222

Using the ConsoleConsumer with old consumer is deprecated and will be removed in a future major release. Consider using the new consumer by passing [bootstrap-server] instead of [zookeeper].

这句话的意思:

现在正在使用的ConsoleConsumer已废弃,在未来的版本将会被删除。考虑通过[bootstrap-server]代替[zookeeper]来使用新的consumer,即创建消费者时,用以下命令:

bin/kafka-console-consumer.sh --bootstrap-server 192.168.255.120:2181 --topic test --from-beginning

其他常用命令

停止kafka

/usr/local/kafka_2.12-1.0.1/bin/kafka-server-stop.sh

删除topic

报错排查

1、启动时报错:failed; error='Cannot allocate memory' (errno=12) 

原因:内存不够

解决办法:

  • 加内存
  • 杀死占用内存高的进程
  • 修改kafka-server-start.sh
  •  将export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" 改为 export KAFKA_HEAP_OPTS="-Xmx256M -Xms128M"

2、创建topic时,Replication factor: 3 larger than available brokers: 0.

解决办法: 

查看创建命令中zookeeper的路径是否正确,必须与配置文件中zookeeper.connect项相同

参考

http://kafka.apache.org/documentation/

你可能感兴趣的:(大数据)