1 目标
kafka在数据缓冲、异步通信、汇集日志、系统解耦等方面有广泛的应用,旨在了解kafka基础原理。
2 ** 基本概念**
2.1 生产者
Producer:用于往Broker中发送/生产Message。
2.2 消费者
Consumer消息消费者,向 Kafka broker 读取消息的客户端。
Consumer Group每个 Consumer 属于一个特定的 Consumer Group(可为每个 Consumer 指定 group name,若不指定 group name 则属于默认的 group)。
Offset:消息者的消费进度,在Partition中的编号,编号顺序不跨Partition。
2.3 队列及备份
Broker:已发布的消息保存在一组服务器中,它们被称为代理(Broker)或Kafka集群。
Topic:用于划分Message的逻辑概念,一个Topic可以分布在多个Broker上。
Partition:是Kafka中横向扩展和一切并行化的基础,每个Topic都至少被切分为1个Partition。
逻辑log:一个Partition对应一个逻辑log
Segment:一个partition有多个segment ,写总是对应最新的segment,删总是对应最旧的segment,读哪一个segment根据consumer的offset来决定
Replication:Kafka支持以Partition为单位对Message进行冗余备份,每个Partition都可以配置至少1个Replication(当仅1个Replication时即仅该Partition本身)。
Leader:每个Replication集合中的Partition都会选出一个唯一的Leader,所有的读写请求都由Leader处理。其他Replicas从Leader处把数据更新同步到本地,过程类似大家熟悉的MySQL中的Binlog同步。
follower:Replication中的一个角色,从 leader 中复制数据。
controller:kafka 集群中的其中一个服务器,用来进行 leader election 以及 各种 failover。
3 生产机制及策略
3.1 生产策略
生产者直接向某topic的某partition发送数据。leader负责主备策略,写入数据,发送ack。
3.2 消息路由
1. 指定了 patition,则直接使用;
2. 未指定 patition 但指定 key,通过对 key 的 value 进行hash 选出一个 patition
3. patition 和 key 都未指定,使用轮询选出一个 patition。
3.3 写入流程
1. producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader
2. producer 将消息发送给该 leader
3. leader 将消息写入本地 log
4. followers 从 leader pull 消息,写入本地 log 后 leader 发送 ACK
5. leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset) 并向 producer 发送 ACK
producer 写入消息序列图如下所示:
3.4 存储
物理上把 topic 分成一个或多个 patition(对应 server.properties 中的 num.partitions=3 配置),每个 patition 物理上对应一个文件夹(该文件夹存储该 patition 的所有消息和索引文件),如下:
无论消息是否被消费,kafka 都会保留所有消息。有两种策略可以删除旧数据:
1. 基于时间:log.retention.hours=168
2. 基于大小:log.retention.bytes=1073741824
需要注意的是,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。
3.5 存储结构
每个日志文件都是一个 log entrie 序列,每个 log entrie 包含一个 4 字节整型数值(值为 N+5),1 个字节的 "magic value",4 个字节的 CRC 校验码,其后跟 N 个字节的消息体。每条消息都有一个当前 Partition 下唯一的 64 字节的 offset,它指明了这条消息的起始位置。
这个 log entries 并非由一个文件构成,而是分成多个 segment,每个 segment 以该 segment 第一条消息的 offset 命名并以“.kafka”为后缀。另外会有一个索引文件,它标明了每个 segment 下包含的 log entry 的 offset 范围,如下图所示。
4 消费机制及策略
4.1 消费策略
一个topic 可以配置几个partition,produce发送的消息分发到不同的partition中,consumer接受数据的时候是按照group来接受,kafka确保每个partition只能同一个group中的同一个consumer消费,如果想要重复消费,那么需要其他的组来消费。Zookeerper中保存这每个topic下的每个partition在每个group中消费的offset
新版kafka把这个offsert保存到了一个__consumer_offsert的topic下
这个__consumer_offsert 有50个分区,通过将group的id哈希值%50的值来确定要保存到那一个分区. 这样也是为了考虑到zookeeper不擅长大量读写的原因。
所以,如果要一个group用几个consumer来同时读取的话,需要多线程来读取,一个线程相当于一个consumer实例。当consumer的数量大于分区的数量的时候,有的consumer线程会读取不到数据。
假设一个topic test 被groupA消费了,现在启动另外一个新的groupB来消费test,默认test-groupB的offset不是0,而是没有新建立,除非当test有数据的时候,groupB会收到该数据,该条数据也是第一条数据,groupB的offset也是刚初始化的ofsert, 除非用显式的用–from-beginnging 来获取从0开始数据
消费者:消费者使用fetch的方式拉取数据。kafkaServer不直接负责每个consumer的当前消费到了哪里,所以需要client端和zk联合维护每个partition读到了哪里,即Offset。
所以这样看上去,kafkaServer在一定程度上更像是一个大部分为顺序读取的,基于文件的日志系统。
消费语义:对比其他MQ的多播,等语义,Kafka看上去略显单薄,其主要通过User Group的概念实现消费语义。而UserGroup实际对应的就是Offset的更改策略。
User1,User2同属一个userGroup时,即表示二者共用一套Offset。因每个partition 的offset只能由一个线程维护,因此注定了每个UserGroup里只能有一个消费线程对一个partition进行消费。
同样,如果希望实现多播,那就User1和User2用两个userGroup。
4.2 高吞吐率
Kafka 会为每一个 Consumer Group 保留一些 metadata 信息——当前消费的消息的 position,也即 offset。这个 offset 由 Consumer 控制。正常情况下 Consumer 会在消费完一条消息后递增该 offset。当然,Consumer 也可将 offset 设成一个较小的值,重新消费一些消息。因为 offet 由 Consumer 控制,所以 Kafka broker 是无状态的,它不需要标记哪些消息被哪些消费过,也不需要通过 broker 去保证同一个 Consumer Group 只有一个 Consumer 能消费某一条消息,因此也就不需要锁机制,这也为 Kafka 的高吞吐率提供了有力保障。
5 ** 控制**
Broker:Kafka中使用Broker来接受Producer和Consumer的请求,并把Message持久化到本地磁盘。每个Cluster当中会选举出一个Broker来担任Controller,负责处理Partition的Leader选举,协调Partition迁移等工作。
ISR(In-Sync Replica):是Replicas的一个子集,表示目前Alive且与Leader能够“Catch-up”的Replicas集合。由于读写都是首先落到Leader上,所以一般来说通过同步机制从Leader上拉取数据的Replica都会和Leader有一些延迟(包括了延迟时间和延迟条数两个维度),任意一个超过阈值都会把该Replica踢出ISR。每个Partition都有它自己独立的ISR。
6 传递担保
At most once 消息可能会丢,但绝不会重复传输
At least one 消息绝不会丢,但可能会重复传输
Exactly once 每条消息肯定会被传输一次且仅传输一次,很多时候这是用户所想要的。
当 Producer 向 broker 发送消息时,一旦这条消息被 commit,因数 replication 的存在,它就不会丢。但是如果 Producer 发送数据给 broker 后,遇到网络问题而造成通信中断,那 Producer 就无法判断该条消息是否已经 commit。虽然 Kafka 无法确定网络故障期间发生了什么,但是 Producer 可以生成一种类似于主键的东西,发生故障时幂等性的重试多次,这样就做到了 Exactly once。截止到目前 (Kafka 0.8.2 版本,2015-03-04),这一 Feature 还并未实现,有希望在 Kafka 未来的版本中实现。(所以目前默认情况下一条消息从 Producer 到 broker 是确保了 At least once,可通过设置 Producer 异步发送实现 At most once)。
接下来讨论的是消息从 broker 到 Consumer 的 delivery guarantee 语义。(仅针对 Kafka consumer high level API)。Consumer 在从 broker 读取消息后,可以选择 commit,该操作会在 Zookeeper 中保存该 Consumer 在该 Partition 中读取的消息的 offset。该 Consumer 下一次再读该 Partition 时会从下一条开始读取。如未 commit,下一次读取的开始位置会跟上一次 commit 之后的开始位置相同。当然可以将 Consumer 设置为 autocommit,即 Consumer 一旦读到数据立即自动 commit。如果只讨论这一读取消息的过程,那 Kafka 是确保了 Exactly once。但实际使用中应用程序并非在 Consumer 读取完数据就结束了,而是要进行进一步处理,而数据处理与 commit 的顺序在很大程度上决定了消息从 broker 和 consumer 的 delivery guarantee semantic。
读完消息先 commit 再处理消息。这种模式下,如果 Consumer 在 commit 后还没来得及处理消息就 crash 了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于 At most once
读完消息先处理再 commit。这种模式下,如果在处理完消息之后 commit 之前 Consumer crash 了,下次重新开始工作时还会处理刚刚未 commit 的消息,实际上该消息已经被处理过了。这就对应于 At least once。在很多使用场景下,消息都有一个主键,所以消息的处理往往具有幂等性,即多次处理这一条消息跟只处理一次是等效的,那就可以认为是 Exactly once。(笔者认为这种说法比较牵强,毕竟它不是 Kafka 本身提供的机制,主键本身也并不能完全保证操作的幂等性。而且实际上我们说 delivery guarantee 语义是讨论被处理多少次,而非处理结果怎样,因为处理方式多种多样,我们不应该把处理过程的特性——如是否幂等性,当成 Kafka 本身的 Feature)
如果一定要做到 Exactly once,就需要协调 offset 和实际操作的输出。精典的做法是引入两阶段提交。如果能让 offset 和操作输入存在同一个地方,会更简洁和通用。这种方式可能更好,因为许多输出系统可能不支持两阶段提交。比如,Consumer 拿到数据后可能把数据放到 HDFS,如果把最新的 offset 和数据本身一起写到 HDFS,那就可以保证数据的输出和 offset 的更新要么都完成,要么都不完成,间接实现 Exactly once。(目前就 high level API 而言,offset 是存于 Zookeeper 中的,无法存于 HDFS,而 low level API 的 offset 是由自己去维护的,可以将之存于 HDFS 中)
总之,Kafka 默认保证 At least once,并且允许通过设置 Producer 异步提交来实现 At most once。而 Exactly once 要求与外部存储系统协作,幸运的是 Kafka 提供的 offset 可以非常直接非常容易得使用这种方式。