Kafka消息订阅发布系统设计介绍
(2012-01-18 09:07:43)
标签:
kafka消息订阅发布系统设计介绍it |
分类: 资料 |
Kafka学习总结
一、Kafaka简介
Kafka是一个分布式的消息发布-订阅系统。它的特性如下:
l
l
l
l
Kafka目标是提供一个可以完成在一个用户站点上所有动作流数据和处理的解决方案。
这种动作(网页浏览量、搜索和其他的用户行为)是在现代网站的许多社交特征中的重要组成部分。这个数据通常根据吞吐量的需要采用日志记录和专门的日志合并解决方案。这种专门的解决方案通过提供记录数据给离线分析系统,比如hadoop,是可行的,但是要想实现实时的数据处理就很局限了。Kafka的目标是通过提供一个在并行加载到hadoop的同时也要具有在一个集群上逐个分区的进行实时消费能力的机制来统一离线和线上处理。
针对动作流处理的使用让Kafaka可以与Facebook的Scribe和Cloudera的Flume相提并论,尽管这些系统的体系结构和基本实现单元都不相同,并且使得Kafaka更像是传统的消息处理系统。make Kafka more comparable to a traditional messaging system.
二、设计
1.项目背景和出发点
Kafaka是一个构建在LinkedIn(一个社交网站)作为活动流处理基础的消息系统。
活动流数据是一个分析任何一个网站浏览量的指标。动作的数据是一些比如像网页浏览量、关于什么内容被展示的信息和搜索之类的东西。这种东西通常用日志记录把相关的动作到一类文件中,并最终阶段性的整合这些文件去进行分析。
在近几年中,然而,动作数据已经成为了网站生产特征的要害部位,所以需要一个稍微复杂一些的基础架构集。
2.动态数据的用例
l
l
l
l
l
3.动作流数据的特点
因为动作流数据的体积几乎10倍甚至100倍于一个站点的下一个数据源,所以这个高吞吐量的不可变动态数据真的提出了一个巨大的计算挑战。
传统的日志文件聚合计算是一个不错的和可扩展的方法来提供类似于报表或者批处理方面的用例;但是对于实时处理它有太大的时延并且倾向于具有相当高复杂度的操作。另一方面,已存在的消息和队列系统对于实时或者准实时用例是不错的,但是在操作大量未被消费掉的队列方面很差劲,often treating persistence as an after thought。这就给向馈入hadoop一样的离线系统带来了麻烦,可能每天或者每小时只能消费一些数据源。This creates problems for feeding the data to offline systems like Hadoop that may only consume some sources once per hour or per day.Kafaka意在作一个同时支持离线和线上用例的单一队列平台。
Kafka支持相当通用的信息语义。但是没有一个与动作处理绑定,尽管那是激发我们做Kafka的用例。
4.部署
下面的这副图给出了Kafka在LinkedIn的一个简化的部署拓扑图。
注意,一个kafka集群处理来自所有不同数据源的所有动作数据。这给线上和离线处理的consumers提供一个单一的数据管道。这一层作为一个在实时活动和异步处理之间的一个缓存。我们也使用kafka来将所有的数据备份到另外一个数据中心去进行离线处理。
5.主要设计元素
这里有一些使得Kafka不同于大部分其他的消息系统的主要的设计方案:
1.
2.
3.
State
about
4.
以上的每一种设计都将在下面详细讨论。
6.基本概念
首先是一些基本的术语和概念:
Messages
Kafka是显式分布式的——producer、consumer和broker可以全部运行在一个集群
7.消息持久化和缓存
不要害怕文件系统
Kafka严重依赖于文件系统去进行messages的存储和缓存。有一个常见的观点就是磁盘是缓慢的,也使人们怀疑在磁盘上进行持久化的结构可以提供比较好的性能。事实上,磁盘或者比人们想象中的更慢或者更快,这取决于它们是被怎样使用的,一个合适的磁盘结构设计通常比网络传输还快。
为了祢补这种性能不足,现代操作系统已经倾向于使用主存作为磁盘的缓存。任何的现代操作系统将所有的空余内存作为磁盘的缓存,同时在内存回收的时候伴有一些性能损失。所有的磁盘读写将通过这个统一的缓存。这种特性如果不是直接使用I/O的话不能被轻易的关闭。所以即使是一个在进程中保存的数据,还是可能被复制到OS的页缓存中,事实上把所有的数据都存储两次。
另外我们是建立在JVM之上的,所有花了一些时间在Java内存使用的人都知道两件事:
1.
2.
作为这些因素的结果,使用文件系统和依赖于页缓存比维持一个内存的存储或者其他的结构有优势——我们至少通过自动访问所有的空闲内存使得可用的缓存加倍,而且可能通过存储一个紧凑的字节结构而不是单独的对象使得可用的缓存又增加一倍的大小。这么做将导致在在一个32GB的机器上有28到30GB的缓存,而且还不会有GC带来的损失。而且这种缓存将保持可用即使服务被重新启动,但是进程中的缓存将需要在内存中重建(这对于一个10GB的缓存将需要大概10分钟的时间)或者需要用一个完全的冷备份启动(这将是一个非常可怕的初始化过程)。它也将极大的简化了编码因为所有在缓存和文件系统里的相关维护逻辑现在都归操作系统里了,这将比在进程中的一次性尝试的效率和正确度都要高。如果你的磁盘支持一次的读取那么read-ahead
这表明了一种很简单的设计:我们不是把数据尽量多的维持在内存中并只有当需要的时候在将数据刷到文件系统,我们是反其道而行之。所有的数据不用进行任何的刷数据的调用就立刻被写入到文件系统的一个持久化的日志记录中。事实上这只是意味着转移到了内核的页缓存中,OS将在之后将它刷出。接着我们添加一个配置驱动器刷数据策略来允许系统的用户控制数据被刷入物理磁盘的频率(每多少消息或者每多少秒)来设置一个在临界磁盘崩溃时数据量的一个限制。
这种页缓存为中心的设计在一片关于Varnish的设计的文章中有描述。
8.较长时间的满足
在消息系统元数据中使用的持久化数据结构的通常是B树。B树是可以使用的最万能的数据结构,使得在消息系统中支持一个广泛的各种各样的事务性的和非事务性的语义。这样有着相当高的开销,但是B树操作的复杂度是O(log N)。正常情况下O(log N)被认为是本质上等于恒定时间,但是这对于磁盘操作来讲是不对的。磁盘寻道达到10ms ,并且一个磁盘一次只能做一个寻道,所有并行化被限制住了。因此即使是少量的磁盘寻道也将带来很大的开销。因为存储系统在物理磁盘操作中混合了快速的缓存操作,观测到的树结构的性能通常是字面上的。此外,B树要求一个复杂的页或行同步实现来避免在每一个操作中锁定整个树。实现必须要对row-locking或者有效的序列化所有的读取付出较高的代价。因为对磁盘寻道较高的依赖,不可能有效的利用驱动器的密度优势。人们被强制使用小型的(小于100GB)高转速SAS驱动器来维持一个数据寻找能力的合理度。
直观上,一个持久化队列可以建立在简单地读取和添加到文件中,就像在日志记录方案中常见到的那样。尽管这个结构不能支持丰富的B树实现的语义,但是他有一个优势就是所有的操作复杂度都是O(1)并且读和写之间不会互相阻塞。这显然是个性能优势自从性能完全和数据的大小解耦。一个服务器可以充分利用许多廉价的、低转速的1TB大小的SATA驱动器。虽然它们的寻道性能比较差,但是这些驱动器对于大的写入和读取方面有1/3的价格和3倍的容量的性能可比性。可以访问几乎没有限制的磁盘空间而不出现损失意味着我们可以提供一些在消息系统中不常见的特性。比如,在kafka中,我们可以将消息保存相当长的一段时间而不是在消费完之后立刻将它删除。
9.效率最大化
我们的假设是message量是及其之大的,甚至是一个站点的网页访问量(我们假设网页访问量是我们要处理的活动)总数的几倍。此外,我们还假设每一个发布的消息至少被读取一次(实际上可能是很多次),因此我们为consumption优化而不是为production优化。
这里有两个效率低下的原因:太多的网络请求有和过度的字节复制
为了达到高效率,API建立在一个“message set”的抽象上,这个抽象自然的将消息分组。这使得网络请求把message分组以此将网络折返的开销分摊而不是一次只请求一个消息。
MessageSet实现本身是一个小的封装了字节数组或者文件的API。因此在message处理中没有单独的序列化和反序列化的步骤,message字段是根据需要lazily deserialized的。
被broker维护的message的记录本身只是个被写入磁盘的message sets的目录。这种抽象使得一个字节格式同时被broker和consumer共享(某种程度上还有producer,虽然producer的消息在被记录之前已经被校验和验证过了)。
维护这个通用的格式允许对最重要的操作进行优化:持久化的日志块在网络中的传输。现代的Unix操作系统提供了高度优化过的编码途径将数据从页缓存中传输给一个socket。在Linux中这通过sendfile的系统调用实现。Java通过FileChannel.transferTo
1.
2.
3.
4.
这是非常低效的,这里出现了四次拷贝,两次系统调用。使用sendfile,通过允许OS将数据直接从页缓存发送到网络来避免这种重复复制。所有在这种途径中,只有最终复制到NIC的缓存中是必须的。
我们希望对一个topic对应的多个consumer有一个通用的用例。使用了上面的0拷贝优化,数据被拷贝到页缓存中一次而不是存在内存中并被每一个consumption重复利用,在每次读取的时候拷贝出内核空间。这就使得消息被以接近网络连接上限的速率consumed。
对于更多在java中对sendfile和0拷贝的支持,请参考这篇文章。
10.消费状态
11消息传递语义
很明显这里有许多种可能的消息交付保障方案来提供:
l
l
l
12.consumer状态
13.推还是拉
14.分布
Kafka通常运行在一个集群的机器上。Broker和consumer通过Zookeeper协调发现topics并且协调消费。这里没有中心的主节点,取而代之的是broker和consumer作为对等节点集中的元素互相配合。集群中的机器集是非常灵活的:broker和consumer都可以在任何时候不用任何手动配置的增加和删除。
Kafka在consumer和broker之间有一个内建的负载均衡器。要达到这种配合,每一个broker和consumer都要在Zookeeper中注册它的状态并且保存它的元数据。
这种集群感知的消费平衡有一些优点:
l
l
l
15.Producer
自动的均衡负载:
因为broker中分片的数目是对每一个topic都是你可配置的,Zookeeper的观察者在以下的事件中进行注册:
l
l
l
l
内部的,producer维持了一个灵活的到broker的连接池,一个连接到一个
broker。这个连接池保持更新建立和维护到所有的活动的broker,通过zookeeper的回调信号。当一个producer对一个特定的topic的请求到来,一个broker的分片被选出通过partitioner。连接池中可用的producer连接被用来发送数据到选定的broker分片。
16.异步传送
异步非阻塞操作是可扩展的消息系统的基石。在kafka中,producer提供了一个选项来对produce的请求进行异步的分配(producer.type=async)。这允许在一个内存的队列中缓存produce的请求并通过一个时间间隔或者预配置的批量大小来触发进行成批的发送。因为数据通常来自以不同速率产生数据的异源的机器集中,这个异步的缓存帮助生成了到达broker中的统一的通信,以达到更好的网络利用率和更高的吞吐量。
17.语义分片
考虑这样一个应用,用于统计每个成员的网友访问量。这将首先将所有对一个成员的访问事件发送到一个特定的分片,因此使得所有关于这个成员的更新出现在相同consumer线程中的相同的流中。在0.6版本中,我们给集群感知producer添加了可以从语义上将消息映射到可用的kafka节点和分片中。这许可用一些语义上的分片函数基于消息中的某些key对消息流进行分片并在broker机器上传播。那些分片函数可以通过实现kafka.producer.Partitioner
18.对Hadoop和其他批量数据加载的支持
可扩展的持久化考虑到了对批数据加载支持的可能性,比如周期性的将快照存入到一个离线系统做批量处理。我们使用这个将数据加载到我们的数据仓库和hadoop集群。
在Hadoop的案例中,我们通过将装载量分割给单独的map任务,一个对应一个node/topic/partition