1.Kafka 简介
Kafka 是一个高吞吐、分布式、基于发布订阅的消息系统,利用 Kafka 技术可在廉价 PCServer 上搭建起大规模消息系统。
Kafka 和其他组件比较,具有消息持久化、高吞吐、分布式、多客户端支持、实时等特性,适用于离线和在线的消息消费,如常规的消息收集、网站活性跟踪、聚合统计系统运营数据(监控数据)、日志收集等大量数据的互联网服务的数据收集场景。
下面介绍先大体介绍一下 Kafka 的主要设计思想,可以让相关人员在短时间内了解到 kafka 相关特性,如果想深入研究,后面会对其中每一个特性都做详细介绍。(1) Consumergroup:各个 consumer 可以组成一个组,每个消息只能被组中的一个 consumer 消费,如果一个消息可以被多个 consumer 消费的话,那么这些 consumer 必须在不同的组。
(2) 消息状态:在 Kafka 中,消息的状态被保存在 consumer 中,broker 不会关心哪个消息被消费了被谁消费了,只记录一个 offset 值(指向 partition 中下一个要被消费的消息位置),这就意味着如果 consumer 处理不好的话,broker 上的一个消息可能会被消费多次。
(3) 消息持久化:Kafka 中会把消息持久化到本地文件系统中,并且保持极高的效率。
(4) 消息有效期:Kafka 会长久保留其中的消息,以便 consumer 可以多次消费,当然其中很多细节是可配置的。
(5) 批量发送:Kafka 支持以消息集合为单位进行批量发送,以提高 push 效率。
(6) push-and-pull:Kafka 中的 Producer 和 consumer 采用的是 push-and-pull 模式,即 Producer 只管向 brokerpush 消息,consumer 只管从 brokerpull 消息,两者对消息的生产和消费是异步的。
(7) Kafka 集群中 broker 之间的关系:不是主从关系,各个 broker 在集群中地位一样,我们可以随意的增加或删除任何一个 broker 节点。
(8) 负载均衡方面:Kafka 提供了一个 metadataAPI 来管理 broker 之间的负载(对 Kafka0.8.x 而言,对于 0.7.x 主要靠 zookeeper 来实现负载均衡)。
(9) 同步异步:Producer 采用异步 push 方式,极大提高 Kafka 系统的吞吐率(可以通过参数控制是采用同步还是异步方式)。
(10)分区机制 partition:Kafka 的 broker 端支持消息分区,Producer 可以决定把消息发到哪个分区,在一个分区中消息的顺序就是 Producer 发送消息的顺序,一个主题中可以有多个分区,具体分区的数量是可配置的。分区的意义很重大,后面的内容会逐渐体现。
(11)离线数据装载:Kafka 由于对可拓展的数据持久化的支持,它也非常适合向 Hadoop 或者数据仓库中进行数据装载。
(12)插件支持:现在不少活跃的社区已经开发出不少插件来拓展 Kafka 的功能,如用来配合 Storm、Hadoop、flume 相关的插件。
2.Kafka 基本概念
一个典型的 Kafka 集群中包含若干 Producer(可以是 web 前端产生的 PageView,或者是服务器日志,系统 CPU、Memory 等),若干 Broker(Kafka 支持水平扩展,一般 broker 数量越多,集群吞吐率越高),若干 Consumer,以及一个 Zookeeper集群。Kafka 通过 Zookeeper 管理集群配置,选举 Leader,以及在 Consumer 发生变化时进行 rebalance。Producer 使用 push 模式将消息发布到 Broker, Consumer 使用 pull 模式从 Broker 订阅并消费消息。
(1) Broker:Kafka 集群包含一个或多个服务实例,这些服务实例被称为Broker
(2) Topic:每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为Topic。每条发布到 Kafka 的消息都有一个类别,这个类别被称为 Topic,也可以理解为一个存储消息的队列。例如:天气作为一个 Topic,每天的温度消息就可以存储在“天气”这个队列里。.
(3) Partition:Kafka 将 Topic 分成一个或者多个 Partition,每个 Partition在物理上对应一个文件夹,该文件夹下存储这个 Partition 的所有消息。每个 Topic 都有一个或者多个 Partitions 构成。每个 Partition 都是有序且不可变的消息队列。引入 Partition 机制,保证了 Kafka 的高吞吐能力。
Topic 的 Partition 数量可以在创建时配置。
Partition 数量决定了每个 Consumergroup 中并发消费者的最大数量。 ConsumergroupA 有两个消费者来读取 4 个 Partition 中数据;Consumergroup B 有四个消费者来读取 4 个 partition 中数据。
我们可以看到,每个 Partition 中的消息都是有序的,生产的消息被不断追加到Partitionlog 上,其中的每一个消息都被赋予了一个唯一的 offset 值。Kafka集群会保存所有的消息,不管消息有没有被消费;我们可以设定消息的过期时间,只有过期的数据才会被自动清除以释放磁盘空间。比如我们设置消息过期时间为 2 天,那么这 2 天内的所有消息都会被保存到集群中,数据只有超过了两天才会被清除。
任何发布到此 Partition 的消息都会被直接追加到 log 文件的尾部。
每条消息在文件中的位置称为 offset(偏移量),offset 是一个 long 型数字,它唯一标记一条消息。消费者通过(offset、partition、topic)跟踪记录。
Kafka 需要维持的元数据只有一个–消费消息在 Partition 中的 offset 值,Consumer 每消费一个消息,offset 就会加 1。其实消息的状态完全是由 Consumer控制的,Consumer 可以跟踪和重设这个 offset 值,这样的话 Consumer 就可以读取任意位置的消息。
把消息日志以 Partition 的形式存放有多重考虑,第一,方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 topic 又可以有多个 Partition 组成,因此整个集群就可以适应任意大小的数据了;第二就是可以提高并发,因为可以以 Partition 为单位读写了。副本以分区为单位。每个分区都有各自的主副本和从副本。主副本叫做 Leader,从副本叫做 Follower,处于同步状态的副本叫做 In-SyncReplicas(ISR)。Follower 通过拉取的方式从 Leader 中同步数据。消费者和生产者都是从 Leader 中读写数据,不与 Follower 交互。
为了提高 Kafka 的容错性,Kafka 支持 Partition 的复制策略,可以通过配置文件配置 Partition 的副本个数。Kafka 针对 Partition 的复制同样需要选出一个Leader,同时由该 Leader 负责 Partition 的读写操作,其他的副本节点只是负责数据的同步。如果 Leader 失效,那么将会有其他 follower 来接管(成为新的Leader),如果由于 Follower 自身的性能,或者网络原因导致同步的数据落后Leader 太多,那么当 Leader 失效后,就不会将这个 Follower 选为 Leader。由于 Leader 的 Server 承载了全部的请求压力,因此从集群的整体考虑,Kafka 会将 Leader 均横的分散在每个实例上,来确保整体的性能稳定。一个 Kafka 集群各个节点间可能互为 Leader 和 Flower。
Kafka 中每个 Broker 启动时都会创建一个副本管理服务(ReplicaManager),该服务负责维护 ReplicaFetcherThread 与其他 Broker 链路连接关系。该 Broker 中存在的 Followerpartitions 对应的 leaderpartitions 分布在不同的 Broker 上,这些 Broker 创建相同数量的 ReplicaFetcherThread 线程同步对应 partition 数据。Kafka 中 partition 间复制数据是由 follower(扮演 consumer
角色)主动向 leader 获取消息,follower 每次读取消息都会更新 HW 状态(HighWatermark,用于记录当前最新消息的标识)。每当 Follower 的 partitions 发生变更而影响 leader 所在 Broker 时,ReplicaManager 就会新建或销毁相应的 ReplicaFetcherThread。
(4)Producer:负责发布消息到 KafkaBroker。
(5)Consumer:消息消费者,从 KafkaBroker 读取消息的客户端。
(6)ConsumerGroup:每个 Consumer 属于一个特定的 ConsumerGroup(可为每个 Consumer 指定 groupname)。
3.Kafka 核心组件
(1) Replications、Partitions 和 Leaders
通过上面介绍的我们可以知道,kafka 中的数据是持久化的并且能够容错的。 Kafka 允许用户为每个 topic 设置副本数量,副本数量决定了有几个 broker 来存放写入的数据。如果你的副本数量设置为 3,那么一份数据就会被存放在 3 台不同的机器上,那么就允许有 2 个机器失败。一般推荐副本数量至少为 2,这样就可以保证增减、重启机器时不会影响到数据消费。如果对数据持久化有更高的要求,可以把副本数量设置为 3 或者更多。
Kafka 中的 topic 是以 partition 的形式存放的,每一个 topic 都可以设置它的partition 数量,Partition 的数量决定了组成 topic 的 log 的数量。Producer在生产数据时,会按照一定规则(这个规则是可以自定义的)把消息发布到 topic 的各个 partition 中。上面将的副本都是以 partition 为单位的,不过只有一个 partition 的副本会被选举成 leader 作为读写用。
关于如何设置 partition 值需要考虑的因素。一个 partition 只能被一个消费者消费(一个消费者可以同时消费多个 partition),因此,如果设置的 partition 的数量小于 consumer 的数量,就会有消费者消费不到数据。所以,推荐 partition 的数量一定要大于同时运行的 consumer 的数量。另外一方面,建议 partition 的数量大于集群 broker 的数量,这样 leaderpartition 就可以均匀的分布在各个 broker 中,最终使得集群负载均衡。在 Cloudera,每个 topic 都有上百个partition。需要注意的是,kafka 需要为每个 partition 分配一些内存来缓存消息数据,如果 partition 数量越大,就要为 kafka 分配更大的 heapspace。(2) Producers Producers 直接发送消息到 broker 上的 leaderpartition,不需要经过任何中介一系列的路由转发。为了实现这个特性,kafka 集群中的每个 broker 都可以响应 producer 的请求,并返回 topic 的一些元信息,这些元信息包括哪些机器是存活的,topic 的 leaderpartition 都在哪,现阶段哪些 leaderpartition 是可以直接被访问的。
Producer 客户端自己控制着消息被推送到哪些 partition。实现的方式可以是随机分配、实现一类随机负载均衡算法,或者指定一些分区算法。Kafka 提供了接口供用户实现自定义的分区,用户可以为每个消息指定一个 partitionKey,通过这个 key 来实现一些 hash 分区算法。比如,把 userid 作为 partitionkey 的话,相同 userid 的消息将会被推送到同一个分区。以 Batch 的方式推送数据可以极大的提高处理效率,kafkaProducer 可以将消息在内存中累计到一定数量后作为一个 batch 发送请求。Batch 的数量大小可以通过 Producer 的参数控制,参数值可以设置为累计的消息的数量(如 500 条)、累计的时间间隔(如 100ms)或者累计的数据大小(64KB)。通过增加 batch 的大小,可以减少网络请求和磁盘 IO 的次数,当然具体参数设置需要在效率和时效性方面做一个权衡。
Producers 可以异步的并行的向 kafka 发送消息,但是通常 producer 在发送完消息之后会得到一个 future 响应,返回的是 offset 值或者发送过程中遇到的错误。这其中有个非常重要的参数“acks”,这个参数决定了 producer 要求 leader partition 收到确认的副本个数,如果 acks 设置数量为 0,表示 producer 不会等待 broker 的响应,所以,producer 无法知道消息是否发送成功,这样有可能会导致数据丢失,但同时,acks 值为 0 会得到最大的系统吞吐量。若 acks 设置为 1,表示 producer 会在 leaderpartition 收到消息时得到 broker的一个确认,这样会有更好的可靠性,因为客户端会等待直到 broker 确认收到消息。若设置为-1,producer 会在所有备份的 partition 收到消息时得到 broker的确认,这个设置可以得到最高的可靠性保证。
Kafka 消息有一个定长的 header 和变长的字节数组组成。因为 kafka 消息支持字节数组,也就使得 kafka 可以支持任何用户自定义的序列号格式或者其它已有的格式如 ApacheAvro、protobuf 等。Kafka 没有限定单个消息的大小,但我们推荐消息大小不要超过 1MB,通常一般消息大小都在 1~10kB 之前。(3) Consumers Kafka 提供了两套 consumerapi,分为 high-levelapi 和 sample-api。Sample-api 是一个底层的 API,它维持了一个和单一 broker 的连接,并且这个 API 是完全无状态的,每次请求都需要指定 offset 值,因此,这套 API 也是最灵活的。在 kafka 中,当前读到消息的 offset 值是由 consumer 来维护的,因此,consumer可以自己决定如何读取 kafka 中的数据。比如,consumer 可以通过重设 offset 值来重新消费已消费过的数据。不管有没有被消费,kafka 会保存数据一段时间,这个时间周期是可配置的,只有到了过期时间,kafka 才会删除这些数据。High-levelAPI 封装了对集群中一系列 broker 的访问,可以透明的消费一个 topic。它自己维持了已消费消息的状态,即每次消费的都是下一个消息。High-levelAPI 还支持以组的形式消费 topic,如果 consumers 有同一个组名,那么 kafka 就相当于一个队列消息服务,而各个consumer 均衡的消费相应 partition 中的数据。若 consumers 有不同的组名,那么此时 kafka 就相当与一个广播服务,会把 topic 中的所有消息广播到每个 consumer。
4.Kafka 核心特性
(1)压缩
我们上面已经知道了 Kafka 支持以集合(batch)为单位发送消息,在此基础上,Kafka 还支持对消息集合进行压缩,Producer 端可以通过 GZIP 或 Snappy 格式对消息集合进行压缩。Producer 端进行压缩之后,在 Consumer 端需进行解压。压缩的好处就是减少传输的数据量,减轻对网络传输的压力,在对大数据处理上,瓶颈往往体现在网络上而不是 CPU(压缩和解压会耗掉部分 CPU 资源)。那么如何区分消息是压缩的还是未压缩的呢,Kafka 在消息头部添加了一个描述压缩属性字节,这个字节的后两位表示消息的压缩采用的编码,如果后两位为 0,则表示消息未被压缩。
(2)消息可靠性
在消息系统中,保证消息在生产和消费过程中的可靠性是十分重要的,在实际消息传递过程中,可能会出现如下三中情况:
•一个消息发送失败
•一个消息被发送多次
•最理想的情况:exactly-once,一个消息发送成功且仅发送了一次有许多系统声称它们实现了 exactly-once,但是它们其实忽略了生产者或消费者在生产和消费过程中有可能失败的情况。比如虽然一个 Producer 成功发送一个消息,但是消息在发送途中丢失,或者成功发送到 broker,也被 consumer 成功取走,但是这个 consumer 在处理取过来的消息时失败了。
到了消息,但却在处理过程中挂掉,此时 Consumer 可以通过这个 offset 值重新找到上一个消息再进行处理。Consumer 还有权限控制这个 offset 值,对持久化到 broker 端的消息做任意处理。
(3)备份机制
备份机制是 Kafka0.8 版本的新特性,备份机制的出现大大提高了 Kafka 集群的可靠性、稳定性。有了备份机制后,Kafka 允许集群中的节点挂掉后而不影响整个集群工作。一个备份数量为 n 的集群允许 n-1 个节点失败。在所有备份节点中,有一个节点作为 lead 节点,这个节点保存了其它备份节点列表,并维持各个备份间的状体同步。下面这幅图解释了 Kafka 的备份机制:
(4) Kafka 高效性相关设计a)消息的持久化
Kafka 高度依赖文件系统来存储和缓存消息,一般的人认为磁盘是缓慢的,这导致人们对持久化结构具有竞争性持怀疑态度。其实,磁盘远比你想象的要快或者慢,这决定于我们如何使用磁盘。一个和磁盘性能有关的关键事实是:磁盘驱动器的吞吐量跟寻到延迟是相背离的,也就是所,线性写的速度远远大于随机写。比如:在一个7200rpmSATARAID-5 的磁盘阵列上线性写的速度大概是 600M/秒,但是随
机写的速度只有 100K/秒,两者相差将近 6000 倍。线性读写在大多数应用场景下是可以预测的,因此,操作系统利用 read-ahead 和 write-behind 技术来从大的数据块中预取数据,或者将多个逻辑上的写操作组合成一个大写物理写操作中。我们发现,对磁盘的线性读在有些情况下可以比内存的随机访问要快一些。
为了补偿这个性能上的分歧,现代操作系统都会把空闲的内存用作磁盘缓存,尽管在内存回收的时候会有一点性能上的代价。所有的磁盘读写操作会在这个统一的缓存上进行。
此外,如果我们是在 JVM 的基础上构建的,熟悉 java 内存应用管理的人应该清楚以下两件事情:
基于这些事实,利用文件系统并且依靠页缓存比维护一个内存缓存或者其他结构要好——我们至少要使得可用的缓存加倍,通过自动访问可用内存,并且通过存储更紧凑的字节结构而不是一个对象,这将有可能再次加倍。这么做的结果就是在一台 32GB 的机器上,如果不考虑 GC 惩罚,将最多有 28-30GB 的缓存。此外,这些缓存将会一直存在即使服务重启,然而进程内缓存需要在内存中重构(10GB 缓存需要花费 10 分钟)或者它需要一个完全冷缓存启动(非常差的初始化性能)。它同时也简化了代码,因为现在所有的维护缓存和文件系统之间内聚的逻辑都在操作系统内部了,这使得这样做比 one-offin-processattempts 更加高效与准确。如果你的磁盘应用更加倾向于顺序读取,那么 read-ahead 在每次磁盘读取中实际上获取到这人缓存中的有用数据。
以上这些建议了一个简单的设计:不同于维护尽可能多的内存缓存并且在需要的时候刷新到文件系统中,我们换一种思路。所有的数据不需要调用刷新程序,而是立刻将它写到一个持久化的日志中。事实上,这仅仅意味着,数据将被传输到内核页缓存中并稍后被刷新。我们可以增加一个配置项以让系统的用户来控制数据在什么时候被刷新到物理硬盘上。
b)常数时间性能保证消息系统中持久化数据结构的设计通常是维护者一个和消费队列有关的
B 树或者其它能够随机存取结构的元数据信息。B 树是一个很好的结构,可以用在事务型与非事务型的语义中。但是它需要一个很高的花费,尽管 B 树的操作需要 O(logN)。通常情况下,这被认为与常数时间等价,但这对磁盘操作来说是不对的。磁盘寻道一次需要 10ms,并且一次只能寻一个,因此并行化是受限的。
直觉上来讲,一个持久化的队列可以构建在对一个文件的读和追加上,就像一般情况下的日志解决方案。尽管和 B 树相比,这种结构不能支持丰富的语义,但是它有一个优点,所有的操作都是常数时间,并且读写之间不会相互阻塞。这种设计具有极大的性能优势:最终系统性能和数据大小完全无关,服务器可以充分利用廉价的硬盘来提供高效的消息服务。
事实上还有一点,磁盘空间的无限增大而不影响性能这点,意味着我们可以提供一般消息系统无法提供的特性。比如说,消息被消费后不是立马被删除,我们可以将这些消息保留一段相对比较长的时间(比如一个星期)。
c)进一步提高效率我们已经为效率做了非常多的努力。但是有一种非常主要的应用场景是:
处理 Web 活动数据,它的特点是数据量非常大,每一次的网页浏览都会产生大量的写操作。更进一步,我们假设每一个被发布的消息都会被至少一个 consumer 消费,因此我们更要怒路让消费变得更廉价。通过上面的介绍,我们已经解决了磁盘方面的效率问题,除此之外,在此类系统中还有两类比较低效的场景:
为了减少大量小 I/O 操作的问题,kafka 的协议是围绕消息集合构建的。 Producer 一次网络请求可以发送一个消息集合,而不是每一次只发一条消息。在 server 端是以消息块的形式追加消息到 log 中的,consumer 在查询的时候也是一次查询大量的线性数据块。消息集合即 MessageSet,实现本身是一个非常简单的 API,它将一个字节数组或者文件进行打包。所以对消息的处理,这里没有分开的序列化和反序列化的上步骤,消息的字段可以按需反序列化(如果没有需要,可以不用反序列化)。另一个影响效率的问题就是字节拷贝。为了解决字节拷贝的问题,kafka设计了一种“标准字节消息”,Producer、Broker、Consumer 共享这一种消息格式。Kakfa 的 messagelog 在 broker 端就是一些目录文件,这些日志文件都是 MessageSet 按照这种“标准字节消息”格式写入到磁盘的。维持这种通用的格式对这些操作的优化尤为重要:持久化 log 块的网络传输。流行的 unix 操作系统提供了一种非常高效的途径来实现页面缓存和 socket 之间的数据传递。在 Linux 操作系统中,这种方式被称作: sendfilesystemcall ( Java 提供了访问这个系统调用的方法:FileChannel.transferToapi)。
为了理解 sendfile 的影响,需要理解一般的将数据从文件传到 socket 的路径:
5.Kafka-logs
为了使得 Kafka 的吞吐率可以线性提高,物理上把 Topic 分成一个或多个Partition,每个 Partition 在物理上对应一个文件夹,该文件夹下存储这个Partition 的所有消息和索引文件。Kafka 把 Topic 中一个 Parition 大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
segmentfile 组成:由 2 大部分组成,分别为 indexfile 和 datafile,此 2 个文件一一对应,成对出现,后缀“.index”和“.log”分别表示为 segment 索引文件、数据文件。 segment 文件命名规则:partion 全局的第一个 segment 从 0 开始,后续每个segment 文件名为上一个全局 partion 的最大 offset(偏移 message 数)。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用 0 填充。
Kafka 的存储布局非常简单。Topic 的每个分区对应一个逻辑日志。物理上,一个日志为相同大小的一组分段文件。每次生产者发布消息到一个分区,代理就将消息追加到最后一个段文件中。当发布的消息数量达到设定值或者经过一定的时间后,段文件真正写入磁盘中。写入完成后,消息公开给消费者。同一个 topic 下有不同分区,每个分区下面会划分为多个文件,只有一个当前文件在写,其他文件只读。当写满一个文件(写满的意思是达到设定值)后,新建一个空文件用来写,老的文件切换为只读。文件的命名以起始偏移量来命名。
通过索引信息可以快速定位 message。
通过将 index 元数据全部映射到 memory,可以避免 segmentfile 的 index 数据IO 磁盘操作。通过索引文件稀疏存储,可以大幅降低 index 文件元数据占用空间大小。
6.Kafka-Message
7.Producer 读写数据
(1)写数据
总体流程:
Producer 连接任意存活的 Broker,请求制定 Topic、Partition 的 Leader 元数据信息,然后直接与对应的 Broker 直接连接,发布数据。开放分区接口:
用户可以制定分区函数,使得消息可以根据 Key,发送到特定 Partition。(2)读数据
总体流程:
Consumer 连接指定 TopicPartition 所在的 LeaderBroker,用主动获取方式从Kafka 中获取消息。
8.Kafka in zookeeper
9.Kafka Cluster Mirroring
KafkaClusterMirroring 是 Kafka 跨集群数据同步方案,通过 Kafka 内置的 MirrorMaker 工具来实现。
如图,源集群向目标集群同步数据,需要目标集群建立一个 Mirror Master 进程,该进程中有两个子进程,分别为 consumer 和 producer,其中 consumer 从源集群中进行数据的读取工作,然后再通过 producer 进程将数据转存到目标集群的 Broker 进程中进行存储。其实也就相当于有一个同步进程来进行一个数据的转入转出的操作,那么转入转出还是使用的原本的 Kafka 进程中的读取和写出进程。