通常来讲,消息模型可以分为两种:队列和发布-订阅式。队列的处理方式是一组消费者从服务器读取消息,一条消息只有其中的一个消费者来处理。在发布-订阅模型中,消息被广播给所有的消费者,接收到消息的消费者都可以处理此消息。Kafka为这两种模型提供了单一的消费者抽象模型: 消费者组(consumer group)。消费者用一个消费者组名标记自己。
一个发布在Topic上消息被分发给此消费者组中的一个消费者。假如所有的消费者都在一个组中,那么这就变成了queue模型。假如所有的消费者都在不同的组中,那么就完全变成了发布-订阅模型。更通用的, 我们可以创建一些消费者组作为逻辑上的订阅者。每个组包含数目不等的消费者,一个组内多个消费者可以用来扩展性能和容错。
并且,kafka能够保证生产者发送到一个特定的Topic的分区上,消息将会按照它们发送的顺序依次加入,也就是说,如果一个消息M1和M2使用相同的producer发送,M1先发送,那么M1将比M2的offset低,并且优先的出现在日志中。消费者收到的消息也是此顺序。如果一个Topic配置了复制因子(replication facto)为N,那么可以允许N-1服务器宕机而不丢失任何已经提交(committed)的消息。此特性说明kafka有比传统的消息系统更强的顺序保证。但是,相同的消费者组中不能有比分区更多的消费者,否则多出的消费者一直处于空等待,不会收到消息。
Kafka 起初是由 Linkedin 发展而来. 总的看来, 它有点像一个消息队列系统, 并做了一些调整使其能够支持发布/订阅, 在多个服务器上进行扩展, 对消息进行重放 (或者说, “重复消费”).
当你想要采用响应式编程 (reactive programming) 而非命令式编程 (imperative programming) 时, 下面注意以下一些点:
命令式编程, 其实我们开始学习编程时的编程类型就是命令式编程. 当一个事件发生时, 代码里就会对该事件进行通知. 比如说, 用户点击一个按钮时, 你就会在代码中处理该事件. 或许是想要保存记录到一个数据库中, 调用另一个服务, 发送文件, 或是所有的这些操作. 这里的关键点在于, 事件直接与具体发生的动作相关联.
响应式编程允许你对发生的事件进行响应, 而这通常是以流 (stream) 的形式. 多个关注点可以订阅同一个事件, 并且无论在其他作用域中发生了什么, 仅仅让事件作用于自己的域 (domain) 中. 换句话说, 它允许松散耦合的代码, 能够轻松扩展更多功能. 在不同堆栈中编码的各种大型下行系统都可能受到一个事件, 甚至是一些在云端某处运行的无状态函数的影响。
为了理解 Kafka 能够给整个架构带来什么, 让我们先从消息队列谈起. 因为我们会谈到消息队列的一些不足, 并且看到 Kafka 是如何应对这些缺点.
一个消息队列允许多个订阅者从队列尾部抓取一个或是多个消息. 在拉取消息时, 消息队列通常允许进行某种级别的事务处理,以确保在删除消息之前, 所需的操作已经得到执行。
并非所有的队列系统都具有相同的功能,但是一旦消息被处理后,它就会被从队列中删除。仔细想一下,它非常类似于命令式编程,当事件发生时,原始系统就决定了在下游系统中应该执行的某个操作。
即使你可以在队列上对多个消费者进行扩展,但它们都包含相同的功能,这仅仅是为了并行加载和处理消息,换句话说,它不允许你基于同一事件启动多个独立操作。队列消息的所有处理程序 (processor) 都将在同一问题域 (problem domain) 中执行相同类型的逻辑 (same logic)。这也意味着队列中的消息实际上是命令 (command) (适用于命令式编程),而不是一个事件(event) (适用于响应式编程)。
(使用队列时, 对于队列中的每个消息你会在同一个域中执行同样的逻辑.)
不同的是,Kafka 则将消息(message)/事件(event)发布到主题(topic),并会将它们进行持久化。当消费者接收到消息/事件后,它们不会被删除。这允许您重放消息,但更重要的是,它允许大量的消费者基于相同的消息/事件处理(不同的)逻辑。
这样一来, 虽然仍然可以在同一个域中进行扩展来并行处理,但实际更重要的是,你可以添加不同类型的消费者,让它们基于相同的事件执行不同的逻辑 (different logic)。换句话说,在 Kafka 中,您可以采用一个响应式的发布/订阅架构。
(使用 Kafka, 可以基于同一个事件在不同的系统上执行不同的逻辑.)
由于 Kafka 信息保留和消费者组 (consumer group) 的概念, 这是完全可能的。Kafka 的消费者组在向某个话题询问信息时,会向 Kafka 告知自己的身份。consumer group 消费完消息以后, Kafka 将会记录每个 consumer group 消费信息的偏移量 (offset),以便它不会重复消费。
(Hadoop, Stream Processing, Databases 等 consumer group 有着各自的 offset1)
实际情况要复杂得多,因为有一大堆的配置选项可以用来控制细节,但我们并不需要完全了解这些选项, 也可以在高层次地理解 Kafka 的工作机制。
简单的说,Kafka是由Linkedin开发的一个分布式的消息队列系统(Message Queue)
kafka开发的主要初衷目标是构建一个用来处理海量日志,用户行为和网站运营统计等的数据处理框架。在结合了数据挖掘,行为分析,运营监控等需求的情况下,需要能够满足各种实时在线和批量离线处理应用场合对低延迟和批量吞吐性能的要求。从需求的根本上来说,高吞吐率是第一要求,其次是实时性和持久性。
既有的消息队列框架或者对消息传送的可靠性提供了较高的保证,由此带来较大的负担,不能满足海量高吞吐率的要求;或者完全面向实时消息处理系统,对于批量离线处理的场合无法提供足够的缓存和持久性要求。
而多数针对大数据开发应用的日志收集处理系统(e.g. scribe, flume)则通常更适合批量离线处理场合,对实时在线处理的场合支持不够。
总体而言,kafka试图提供一个同时满足在线和离线处理海量数据的消息派发系统。
kafka的集群有多个Broker服务器组成,每个类型的消息被定义为topic,同一topic内部的消息按照一定的key和算法被分区(partition)存储在不同的Broker上,消息生产者producer和消费者consumer可以在多个Broker上生产/消费topic
核心思想
以高效率作为第一设计原则,kafka的结构设计在很多方面都做了激进的取舍。
=极简的数据结构和应用模式 =
消息队列是以log文件的形式存储,消息生产者只能将消息添加到既有的文件尾部,没有任何ID信息用于消息的定位,完全依靠文件内的位移,因此消息的使用者只能依靠文件位移顺序读取消息,这样也就不需要维护复杂的支持随即读取的索引结构。
kafka broker完全不维护和协调多用户使用消息的行为模式,用户自己维护位移用来索引消息。
最小的并发访问单位就是partition分区,同一用户组内的所有用户(可以理解为同一个应用的所有并发进程)只能有一个访问同一分区,同时分区的个数是固定的,不支持动态调整。这样最大简化了多进程/分布式client之间对消息处理访问的并发控制的复杂度,当然也带来一定的使用模式上的限制(比如最大并发度完全取决于预先规划的partition的个数)
此外分区也带来一个问题就是消息只是分区内部有序而不是全局有序的。如果需要全局有序,应用需要自己靠别的机制来保证。
使用Pull模式派发消息,消息的使用情况,比如是否还有consumer没有读取,是否重复读取(改进中)等,在Broker端也完全不跟踪维护,消息的过期处理简单的由定时器定时删除(比如保留7天),由此简化各种消息跟踪维护的开销。
=采取各种方式最大化数据传输效率 =
比如生产者和消费者可以批量读写消息减少RPC开销
使用Zero Copy方式在内核层直接将文件内容传送给网络Socket,避免应用层数据拷贝
使用合理的压缩格式等
=激进的内存管理模式 =
基本的意思就是不管理。。。kafka不在JVM进程内部维护消息Cache,消息直接从文件中读写,完全依赖操作系统在文件系统层面的cache,避免在JVM中管理Cache带来的额外数据结构开销和GC带来的性能代价。基于批量处理和顺序读写的应用模式,最大化利用文件系统的Cache机制和规避文件读写相对内存读写的性能代价。
= HA =
kafka在0.8之前message是没有备份容错机制的,producer的工作模式是fire and forget,如果一个broker失效,那么相关topic分区的相关消息也就丢失了。这种设计的原因在于最初的应用模式,如日志/用户行为等消息的处理,对数据的健壮性方面要求不高,可以容忍部分数据的缺失。采用fire and forget 模式,不需要等待Broker ack,有利于提高producer的吞吐率。
不过在0.8版本中,添加了数据replica的机制,一个消息分区的多个replica分布在不同的Broker上,由leader replica负责日常读写,通过zookeeper监督failover,不同的分区的leader replica均衡负载到不同的Broker上。在这种情况下,producer可以选择不等待leader replica的Ack,部分Ack,或者完全备份完毕后Ack等不同的ack机制。这三种机制,性能依次递减 (producer吞吐量降低1-3倍),数据健壮性则依次递增。