对于 kafka 的架构原理我们先提出几个问题?
1.Kafka 的 topic 和分区内部是如何存储的, 有什么特点?
2. 与传统的消息系统相比, Kafka 的消费模型有什么优点?
3.Kafka 如何实现分布式的数据存储与数据读取?
Kafka 架构图
1.kafka 名词解释
在一套 kafka 架构中有多个 Producer, 多个 Broker, 多个 Consumer, 每个 Producer 可以对应多个 Topic, 每个 Consumer 只能对应一个 ConsumerGroup.
整个 Kafka 架构对应一个 ZK 集群, 通过 ZK 管理集群配置, 选举 Leader, 以及在 consumer group 发生变化时进行 rebalance.
名称解释
Broker
消息中间件处理节点, 一个 Kafka 节点就是一个 broker, 一个或者多个 Broker 可以组成一个 Kafka 集群
Topic
主题, Kafka 根据 topic 对消息进行归类, 发布到 Kafka 集群的每条消息都需要指定一个 topic
Producer
消息生产者, 向 Broker 发送消息的客户端
Consumer
消息消费者, 从 Broker 读取消息的客户端
ConsumerGroup
每个 Consumer 属于一个特定的 Consumer Group, 一条消息可以发送到多个不同的 Consumer Group, 但是一个 Consumer Group 中只能有一个 Consumer 能够消费该消息
Partition
物理上的概念, 一个 topic 可以分为多个 partition, 每个 partition 内部是有序的
2.Topic 和 Partition
在 Kafka 中的每一条消息都有一个 topic. 一般来说在我们应用中产生不同类型的数据, 都可以设置不同的主题. 一个主题一般会有多个消息的订阅者, 当生产者发布消息到某个主题时, 订阅了这个主题的消费者都可以接收到生产者写入的新消息.
kafka 为每个主题维护了分布式的分区 (partition) 日志文件, 每个 partition 在 kafka 存储层面是 append log. 任何发布到此 partition 的消息都会被追加到 log 文件的尾部, 在分区中的每条消息都会按照时间顺序分配到一个单调递增的顺序编号, 也就是我们的 offset,offset 是一个 long 型的数字, 我们通过这个 offset 可以确定一条在该 partition 下的唯一消息. 在 partition 下面是保证了有序性, 但是在 topic 下面没有保证有序性.
在上图中在我们的生产者会决定发送到哪个 Partition.
如果没有 Key 值则进行轮询发送.
如果有 Key 值, 对 Key 值进行 Hash, 然后对分区数量取余, 保证了同一个 Key 值的会被路由到同一个分区, 如果想队列的强顺序一致性, 可以让所有的消息都设置为同一个 Key.
3. 消费模型
消息由生产者发送到 kafka 集群后, 会被消费者消费. 一般来说我们的消费模型有两种: 推送模型 (psuh) 和拉取模型(pull)
基于推送模型的消息系统, 由消息代理记录消费状态. 消息代理将消息推送到消费者后, 标记这条消息为已经被消费, 但是这种方式无法很好地保证消费的处理语义. 比如当我们把已经把消息发送给消费者之后, 由于消费进程挂掉或者由于网络原因没有收到这条消息, 如果我们在消费代理将其标记为已消费, 这个消息就永久丢失了. 如果我们利用生产者收到消息后回复这种方法, 消息代理需要记录消费状态, 这种不可取. 如果采用 push, 消息消费的速率就完全由消费代理控制, 一旦消费者发生阻塞, 就会出现问题.
Kafka 采取拉取模型(poll), 由自己控制消费速度, 以及消费的进度, 消费者可以按照任意的偏移量进行消费. 比如消费者可以消费已经消费过的消息进行重新处理, 或者消费最近的消息等等.
4. 网络模型
4.1 KafkaClient -- 单线程 Selector
单线程模式适用于并发链接数小, 逻辑简单, 数据量小.
在 kafka 中, consumer 和 producer 都是使用的上面的单线程模式. 这种模式不适合 kafka 的服务端, 在服务端中请求处理过程比较复杂, 会造成线程阻塞, 一旦出现后续请求就会无法处理, 会造成大量请求超时, 引起雪崩. 而在服务器中应该充分利用多线程来处理执行逻辑.
4.2 Kafka--server -- 多线程 Selector
在 kafka 服务端采用的是多线程的 Selector 模型, Acceptor 运行在一个单独的线程中, 对于读取操作的线程池中的线程都会在 selector 注册 read 事件, 负责服务端读取请求的逻辑. 成功读取后, 将请求放入 message queue 共享队列中. 然后在写线程池中, 取出这个请求, 对其进行逻辑处理, 即使某个请求线程阻塞了, 还有后续的县城从消息队列中获取请求并进行处理, 在写线程中处理完逻辑处理, 由于注册了 OP_WIRTE 事件, 所以还需要对其发送响应.
5. 高可靠分布式存储模型
在 Kafka 中保证高可靠模型的依靠的是副本机制, 有了副本机制之后, 就算机器宕机也不会发生数据丢失.
5.1 高性能的日志存储
kafka 一个 topic 下面的所有消息都是以 partition 的方式分布式的存储在多个节点上. 同时在 kafka 的机器上, 每个 Partition 其实都会对应一个日志目录, 在目录下面会对应多个日志分段(LogSegment).LogSegment 文件由两部分组成, 分别为 ".index" 文件和 ".log" 文件, 分别表示为 segment 索引文件和数据文件. 这两个文件的命令规则为: partition 全局的第一个 segment 从 0 开始, 后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值, 数值大小为 64 位, 20 位数字字符长度, 没有数字用 0 填充, 如下, 假设有 1000 条消息, 每个 LogSegment 大小为 100, 下面展现了 900-1000 的索引和 Log:
由于 kafka 消息数据太大, 如果全部建立索引, 即占了空间又增加了耗时, 所以 kafka 选择了稀疏索引的方式, 这样的话索引可以直接进入内存, 加快偏查询速度.
简单介绍一下如何读取数据, 如果我们要读取第 911 条数据首先第一步, 找到他是属于哪一段的, 根据二分法查找到他属于的文件, 找到 0000900.index 和 00000900.log 之后, 然后去 index 中去查找 (911-900) =11 这个索引或者小于 11 最近的索引, 在这里通过二分法我们找到了索引是 [10,1367] 然后我们通过这条索引的物理位置 1367, 开始往后找, 直到找到 911 条数据.
上面讲的是如果要找某个 offset 的流程, 但是我们大多数时候并不需要查找某个 offset, 只需要按照顺序读即可, 而在顺序读中, 操作系统会对内存和磁盘之间添加 page cahe, 也就是我们平常见到的预读操作, 所以我们的顺序读操作时速度很快. 但是 kafka 有个问题, 如果分区过多, 那么日志分段也会很多, 写的时候由于是批量写, 其实就会变成随机写了, 随机 I/O 这个时候对性能影响很大. 所以一般来说 Kafka 不能有太多的 partition. 针对这一点, RocketMQ 把所有的日志都写在一个文件里面, 就能变成顺序写, 通过一定优化, 读也能接近于顺序读.
可以思考一下:
1. 为什么需要分区, 也就是说主题只有一个分区, 难道不行吗? 2. 日志为什么需要分段
5.2 副本机制
Kafka 的副本机制是多个服务端节点对其他节点的主题分区的日志进行复制. 当集群中的某个节点出现故障, 访问故障节点的请求会被转移到其他正常节点(这一过程通常叫 Reblance),kafka 每个主题的每个分区都有一个主副本以及 0 个或者多个副本, 副本保持和主副本的数据同步, 当主副本出故障时就会被替代.
在 Kafka 中并不是所有的副本都能被拿来替代主副本, 所以在 kafka 的 leader 节点中维护着一个 ISR(In sync Replicas)集合, 翻译过来也叫正在同步中集合, 在这个集合中的需要满足两个条件:
节点必须和 ZK 保持连接
在同步的过程中这个副本不能落后主副本太多
另外还有个 AR(Assigned Replicas)用来标识副本的全集, OSR 用来表示由于落后被剔除的副本集合, 所以公式如下: ISR = leader + 没有落后太多的副本; AR = OSR+ ISR;
这里先要说下两个名词: HW(高水位)是 consumer 能够看到的此 partition 的位置, LEO 是每个 partition 的 log 最后一条 Message 的位置. HW 能保证 leader 所在的 broker 失效, 该消息仍然可以从新选举的 leader 中获取, 不会造成消息丢失.
当 producer 向 leader 发送数据时, 可以通过 request.required.acks 参数来设置数据可靠性的级别:
1(默认): 这意味着 producer 在 ISR 中的 leader 已成功收到的数据并得到确认后发送下一条 message. 如果 leader 宕机了, 则会丢失数据.
0: 这意味着 producer 无需等待来自 broker 的确认而继续发送下一批消息. 这种情况下数据传输效率最高, 但是数据可靠性确是最低的.
-1:producer 需要等待 ISR 中的所有 follower 都确认接收到数据后才算一次发送完成, 可靠性最高. 但是这样也不能保证数据不丢失, 比如当 ISR 中只有 leader 时(其他节点都和 zk 断开连接, 或者都没追上), 这样就变成了 acks=1 的情况.
来源: http://www.jianshu.com/p/0e40f140d4d5