Kafka:用于日志处理的分布式消息系统
周末躺不平,摆不烂,卷不动,随便读一篇paper吧
原文:Kafka: a Distributed Messaging System for Log Processing
作者:Jay Kreps / Neha Narkhede / Jun Rao
这三尊神就是当初在LinkedIn开发Kafka的大佬
摘要
日志处理已经成为了当下互联网公司数据管道(data pipeline)的重要组成部分。我们引入了Kafka——一个我们设计的用于低延迟的收集、传送大规模日志数据分布式消息系统。我们的系统包含了现存的日志聚合系统和消息系统的idea,并且适用于离线和在线日志消费场景。在Kafka中,我们做了一些现在还不太常见的但实用的设计选择来让我们的系统更加高效、易扩展。根据我们的实验结果,Kafka和当前的两个流行的消息系统相比有着优秀的性能。我们已经将Kafka投入生产一段时间了,它每天都要处理几百GB的新数据。
1. 介绍
在任何具有一定规模的互联网公司都有大量的日志数据被生成,这些数据通常包括:
- 用户活动事件,如:登陆、查看页面、点击、点赞、分享、评论以及查询搜索
- 每台机器上的操作指标,如:服务调用栈、调用延迟、错误以及系统指标:CPU、内存、网络、磁盘利用率
日志数据长期以来一直是一个用于跟踪用户粘度、系统利用率以及其他指标的分析组件。然而,互联网应用的最新趋势已使这些活动数据成为直接用于网站功能的生产数据管道的一部分,包括:
- 搜索相关度
- 由在活动流中的流行程度驱动的推荐
- 广告投送和报告
- 防止滥用行为的安全应用
- 聚合用户的状态更新以及动作,给它们的朋友看
这些日志数据的实时应用给数据系统带来了新的挑战,因为它们的规模是真实数据的几倍大,举个例子,搜索、推荐以及广告经常需要计算细粒度的点击率,这不仅仅是为每次用户点击生成日志记录,对于那些用户没点击的数据也要生成。每天,中国移动收集5到8TB的电话记录,Facebook手机大约6TB的用户活动事件。
很多早期的处理这种数据的系统依赖物理的从生产服务器中抓取数据来分析,而近几年,很多专用的分布式日志聚合系统出现了,比如Facebook的Scribe、Yahoo的Data Highway以及Cloudera的Flume。这些系统主要被设计成用于将日志数据加载到数据仓库或Hadoop以用于离线消费,在LinkedIn(一个社交网络网站),我们发现除了传统的离线分析外,我们还需要支持上面提到的大多数实时应用,并且延迟要不超过几秒钟。
我们构建了一个新的用于日志处理的消息系统——Kafka,它具有传统日志聚合系统和消息系统的优点。一方面,Kafka是分布式的,易于扩展,并且提供高吞吐量,另一方面,Kafka提供了一套类似消息系统的API,允许应用实时消费日志事件。Kafka已经开源,并且在LinkedIn投入生产超过6个月,它很好的简化了我们的基础设施,因为我们可以利用一个单独的软件处理全部类型日志数据的离线和在线消费。本篇paper的剩余部分组织如下:
- 第二节中我们探讨传统的日志系统和日志聚合系统
- 第三节中我们描述Kafka的结构以及它的关键设计原则
- 第四节中我们介绍LinkedIn中Kafka的应用
- 第五节中我们介绍Kafka的性能结果
- 第六节中我们讨论未来的工作以及结论
2. 相关工作
传统的企业级消息系统已经存在很长时间了,它们通常作为处理异步数据流的事件总线这一关键角色。然而,有一些原因使得它们不善于进行日志处理。首先,这些系统总是专注于提供一个丰富的传递保证,举个例子,IBM的Websphere MQ具有事务消息支持,允许程序原子的插入消息到多个队列中;JMS规范允许每一个独立的消息在消费后被确认(ack),这可能会出现消息顺序混乱的情况。这种递送保证对于收集日志数据来说有点过了,举个例子,偶然丢了少量的页面访问事件世界也不会炸了,这些我们不需要的特性增加了这些系统的API和底层实现的复杂性。第二,很多系统并不将高吞吐量作为它们的主要设计约束,比如,JMS没有允许生产者显式在一个请求中批量多条消息的API,这意味着每一个消息都需要一个完整的TCP/IP往返,这不符合我们的业务对吞吐量的需求。第三,这些系统缺乏分布式支持,没有一个简单的方式来分区并存储消息在多台机器上。最后,很多消息系统假设消息立即会被消费,所以,未消费消息的队列总是很小,当允许消息积累时,它们的性能就会显著下降,而在如数据仓库应用的离线消费场景中,偏向周期性的大负载而不是持续消费。
最近几年,很多专用的日志聚合系统被构建出来,Facebook使用的系统叫Scribe,每一个前端机器可以使用socket发送日志数据到一系列Scribe机器上,每一个Scribe机器聚合日志条目,周期性导出它们到HDFS或NFS设备上。Yahoo到Data Highway项目有着相似的数据流,一系列机器从客户端聚合事件,推分钟文件(roll out “minute” files),然后将它们添加到HDFS中。Flume是一个由Cloudera开发的相对新的日志聚合器,它支持可扩展的“pipes”和“sinks”,这使得传递日志数据非常灵活(make streaming log data very flexible),它也有很多集成的分布式支持。然而,大多数这种系统都是为了消费离线日志数据设计的,并且通常给客户端暴露了不必要的实现细节(比如“minute files”),另外,它们大多数使用推模型,broker将数据转发到consumer上。在LinkedIn,我们发现拉模型更加适合我们的应用,因为每一个消费者可以用它的最大速率来获取消息,并且可以避免消息推送超过它可以处理的速率。拉模型也让回溯消费者变得容易,我们将在3.2节的末尾讨论这个好处。
Yahoo Research开发了一个新的pub/sub系统,称作——HedWig。HedWig高度可扩展、高可用,提供强持久保证,然而,它主要用于存储数据商店的提交日志。
3. Kafka结构以及设计原则
由于现存系统的限制,我们开发了一个新的基于消息的日志聚合器——Kafka!我们首先介绍Kafka的基本概念,一个特定类型的消息流被定义成一个topic,一个生产者可以向一个topic发送消息,发送的消息被存储在一组被称做broker的机器上,消费者可以从broker订阅某一个或多个topic,通过从broker拉取数据来消费订阅的消息。
消息传递在概念上很简单,我们尝试让Kafka的API一样简单。我们不会展示实际的API,而是放一些样本代码来展示API如何被使用。producer的样本代码在下面,一个消息被定义成只包含字节的载荷,用户可以选择自己喜欢的序列化方式来编码消息,为了高效,用户可以在一个发布请求中发送一个消息集合。
为了订阅topic,一个消费者首先创建一个或多个topic的消息流,发送到这个topic上的消息将被均匀的分发到这些订阅流上。Kafka分发消息的细节将在3.2中介绍。每一个消息流提供一个迭代器接口,来获取被生产的消息的连续消息流。消费者迭代每一个流中的消息,处理消息的载荷。不同于传统的迭代器,消息流迭代器永远不会停下,如果当前没有更多消息可以消费,迭代器就会阻塞直到有消息发布到topic中。我们支持点对点到传送模型,为了多个消费者共同消费一个topic中所有消息的单独拷贝,就像发布/订阅模型一样,每一个consumer都会获得它独有的主题的copy。
来获取连续的消息流
Kafka的总体设计如图1,因为Kafka天生就是分布式的,一个Kafka集群通常包含多个broker,为了负载均衡,一个topic被分割成多个partition,每一个broker存储一个或多个partition。多个生产者和消费者可以同时发布以及获取消息。在3.1节中,我们将介绍broker上的单一分区的布局以及我们的一些让分区访问更高效的设计选择。在3.2小节中,我们将介绍生产者和消费者如何在分布式场景下通过多个broker交互。我们将在3.3小节中讨论Kafka的递送保证。
3.1 单一Partition的效率
我们在Kafka中做了一些决策让系统更高效。
简单的存储:Kafka有一个非常简单的存储布局,topic的每一个分区对应一个逻辑日志。物理上,一个日志被实现成一系列大约相同大小的段文件(比如1GB),每次一个生产者发布一条消息到partition(后文称分区)中时,broker只是简单的将消息追加给给最后一个段文件。为了更好的性能,我们只在指定的可配置的数量的消息或指定的时间到达后才会将段文件刷新到磁盘,一个消息只有在它被刷到磁盘上后才会暴露给消费者。
与典型的消息系统不同,存储在Kafka中的消息没有明确的消息ID,取而代之的是,每一个消息可以通过它在日志中的逻辑offset被寻址,这避免了维护用于将message id映射到实际消息位置的辅助的,搜索密集的随机访问索引结构。我们的消息id是递增的,但不是连续的,为了计算下一条消息的id,我们必须用当前消息的id加上当前消息的长度,从现在开始,我们会交替的使用消息id和offset这两个术语。
消费者总是从一个特定的分区顺序的消费消息,如果一个消费者确认(ack)一个特定的消息offset,这意味着消费者已经接受到了该分区该offset之前的所有消息。消费者会提交异步的拉请求到borker,以为程序准备好要消费的数据缓冲区。每一个拉请求包含消费开始的消息offset,以及一个可接受的拉取字节数,每一个broker在内存中存储一个offset但排序集合,broker通过搜索offset列表来定位请求的消息所在的段文件,并将数据快发送给消费者。在消费者收到消息之后,它计算出下一个消息的偏移量,并在下一次请求中使用它。Kafka日志的布局以及内存中的索引结构在图2中描述,每一个方块展示了消息的offset。
高效的传输:我们非常小心的向Kafka传入和传出数据。之前,我们已经展示了生产者可以在一个请求中提交一个消息集合,即使端的消费者API每次迭代一条消息,在底层,每一个消费者的拉请求都获取至多到一个确切大小的多条消息,通常是几百KB。
另一个非常规的选择是,避免显式的在Kafka层面的内存中缓存消息,我们依赖底层的文件系统的page cache。避免双缓冲有两个主要好处——消息只在page cache中被缓存,即使broker进程重启缓存也还是热的;另外因为kafka没有在进程中缓存消息,所以对于GC只有很小的损耗,这让在基于VM的语言中的高效实现成为可能,最后,因为生产者和消费者只会顺序的访问段文件,消费者总是落后于生产者一小点儿,常规的操作系统缓存的启发式方法将非常有效(特别是write-through缓存和read-ahead)我们发现生产和消费都具有和数据大小相关的线性性能,最大可达几TB的数据。
另外,我们优化消费者的网络访问。Kafka是一个多消费者的系统,一个消息可能被不同的消费者程序消费多次,一个典型的从本地文件发送字节到远端套接字的方式引入如下几步:
- 从存储介质读数据到OS page cache
- 从page cache复制数据到应用buffer
- 复制应用buffer到另一个kernel buffer
- 发送kernel buffer到socket
这引入了4次数据复制以及2次系统调用,在Linux和其它Unix操作系统中,有sendfile
API,可以直接从一个文件通道将数据传输到socket通道,这通常会避免在2和3步中引入的2次拷贝和1次系统调用。Kafka利用sendfile
API高效的从broker传送日志段文件的字节到消费者。
无状态的broker:不像其它消息系统,Kafka中,每一个消费者消费了多少不存在于broker中,而是在消费者自己那里。这种设计减少了大量的复杂性和broker上的开销。然而,这让删除消息变得很棘手,因为broker不知道是否所有订阅者都已经消费了消息。Kafka通过使用基于时间的SLA做保留策略。一个消息若已经在broker上保留超过指定时间周期,它就会被自动删除,通常是7天。这个解决办法在实践中很棒。大多数消费者,包括离线的,都会在一天、一小时或实时的完成消费,Kafka不会因为很大的数据大小而降低性能的事实让这种持久保存可以实现。
这个设计有一个重要的好处,消费者可以任意回退到一个老的offset,并且重新消费数据。这违反了队列的常见约定,但提供了一个对很多消费者至关重要的特性。比如,如果消费者的应用逻辑中有一个异常,应用可以在错误被修复后重放特定的消息,这对于将ETL数据加载到我们的数据仓库或Hadoop系统尤为重要。另一个例子,消费过的数据可能周期性的被刷新到一个持久层中(比如一个全文检索器),如果消费者crash,未刷入的数据丢失了,在这种情况下,消费者可以检查最小的未刷新消息offset,并在重启时从那个offset重新消费。我们发现让消费者支持重放在拉模型中比推模型中更简单。
3.2 分布式协作
现在我们描述了生产者和消费者在分布式场景中的行为,每一个生产者可以投递一条消息到任意一个随机选择的分区或者通过分区key或一个分区函数选择。我们将要关注消费者如何与broker交互。
Kafka具有消费者组的概念,每一个消费者组包含一个或多个消费者,共同消费一系列订阅的topic,举个例子,每一个消息只会被传送到组内的一个消费者上。不同的消费者组独立的消费订阅消息的整个集合,并且消费者组之间不需要协作。同一个消费者组的消费者可以在不同进程或不同机器上,我们的目标是均匀的在消费之间分割在broker上存储的消息,而不需要引入过多的协作开销。
我们的第一个决策是让topic中的一个分区作为最小的并行单元。这意味着在任何时间,在一个分区上的所有消息只能被一个消费者组中的一个消费者消费,如果我们允许多个消费者同时消费一个单一分区,那么我们就必须协调谁消费了哪些消息,这需要锁以及状态维护的开销。相反,在我们的设计中,消费进程只需要在消费者重平衡负载时协调,这甚至是很少见的。为了负载真正均衡,我们需要比每个组中的消费者个数多得多的分区,我们可以轻易的通过为一个topic分区做到这一点。
第二个决策,我们没有中央的“master”节点,我们让消费者以一种去中心化的方式协作。添加一个master会让系统变得复杂,因为我们必须进一步思考master failure。为了便于协作,我们使用一个高可用的共识服务Zookeeper(译者:貌似现在的Kafka已经摆脱了zk)。Zookeeper有一个非常简单的,类文件系统的API,你可以创建路径,设置路径的值,读取路径的值,删除一个路径,列出路径的子路径。还有一些有趣的:
- 你可以在路径上注册一个watcher,当路径的子路径或值发生变更你就会得到通知
- 一个path可以被创建为临时的(相反就是持久的)这意味着如果创建它的客户端不见了,这个路径将自动被Zookeeper服务器移除
- Zookeeper在它的多个服务器之间复制数据,这让数据高可靠、高可用
Kafka使用Zookeeper做如下任务:
- 检测broker和consumer的添加和移除
- 在上面的事件发生时触发每个消费者上的重平衡进程
- 维护消费关系,跟踪每个分区的消费offset
特别地,当每一个broker或consumer启动,它在Zookeeper中的broker或consumer registry中保存自己的信息。broker registry包含broker的主机名和端口,以及存储在上面的一系列主题和分区。consumer registry包含消费者属于的消费者组以及它订阅的topic集合。每一个消费者组都关联到Zookeeper上的一个ownership registry,以及一个offset registry,ownership registry为每一个订阅的分区都有一个路径,它的值是当前从这个分区消费的consumer的id(我们使用术语“consumer拥有这个分区”)。offset registry保存每一个被订阅的分区的最后消费消息的offset。
broker、consumer以及ownership registry在zk中创建的path是临时的,offset registry是永久的。如果broker fail,所有在上面的分区都会自动从broker registry中移除,消费者fail将导致它丢掉自己在consumer registry中的条目,以及在ownership registry中所有它拥有的分区。每一个消费者都会在broker registry和consumer regisry上注册一个zk的watcher,当broker集合或消费者组变动发生时会得到通知。
在消费者初始化启动期间,或者消费者通过watcher注意到broker/consumer发生变化,小嫩王者就会初始化一个重平衡程序来决定它应该消费的新的分区子集。这个程序在算法1中描述。通过读取zk上的broker和consumer registry,消费者首先计算每个被订阅的主题\(T\)的可用分区集合\((P_T)\)以及所有订阅主题\(T\)的消费者集合\((C_T)\),然后它将\(P_T\)分成\(|C_T|\)个块,确定地选择一块来own,对于消费者选择的每一个分区,它在ownership registry中将自己写成新的分区所有者,最终,消费者开始一个新线程去从每一个所有的分区拉数据。一旦消息从分区中拉出,消费者在offset registry中周期性的更新最近的消费offset。
当组内有多个消费者时,每一个都会注意到broker和consumer的变化,然而消费者处理的时间可能略有不同,所以有可能一个消费者尝试获取一个仍然被其它消费者拥有的分区的所有权。当这种事情发生,第一个消费者简单的放弃它当前拥有的所有分区,等待一会,然后重试重平衡程程序。在实践中,重平衡程序总会在几次少量的重试后稳定。
当一个新的消费者组被创建,offset registry中没有可用的offset,这种情况下,消费者将从每一个被订阅的partition上可用的最小或者最大的offset开始(取决于配置),通过一个我们在broker上提供的API来获取。
3.3 递送保证
通常情况下,Kafka之保证至少一次交付。精确一次交付通常需要两阶段提交,这对我们的程序来说是不必要的。大多数时候,一个消息被精确的投递到每个消费者组一次,然而,在消费者进程崩溃,并且没有在shutdown时做清理,消费者进程可能会结果哪些被failed的消费者拥有的分区,这种情况下会获取到一些在最后一个offset被成功提交到zk之后的重复消息。如果一个应用害怕重复,他必须有自己的去重逻辑,或者是使用我们返回给consumer的offset,或者是一些消息中其它的唯一key,这通常是比使用两阶段提交更高效的方式。
Kafka保证从一个单一分区的消息会按顺序被偷送到消费者,然而,来自其它分区的消息顺序则没有保证。
为了避免日志损坏,Kafka为每一个消息在日志中存储了一个CRC。如果在broker上有任何I/O异常,Kafka运行一个回复程序来移除哪些具有不正确CRC的消息。在消息层面拥有CRC也允许我们在消息被生产或消费后检查网络error。
如果一个broker down掉,任何存储在上面的尚未消费的消息将会不可用,如果broker上的存储系统永久损坏,任何未消费的消息将永久消失。未来,我们计划在Kafka中添加内建的备份机制去在多个broker上冗余存储每条消息。
4. Kafka在LinkedIn的应用
在这个部分,我们将介绍在LinkedIn,我们如何使用Kafka。图3展示了一个我们的部署的一个简单版本,我们有一个Kafka集群与运行面向用户的服务运行在同一个数据中心,前端服务生成多种log数据并批量发布到它本地的Kafka broker,我们依赖一个硬件负载均衡来均匀地将发布请求分发到一系列Kafka broker上。在线的Kafka消费者运行在相同数据中心的服务中。
我们也为离线分析在一个单独的数据中心部署了一个Kafka集群,在地理上靠近我们的Hadoop集群以及其他的数据仓库基础设施。这个Kafka实例运行一系列内置的消费者,它们从在线的数据中心的Kafka实例中拉数据,然后,我们运行数据装载任务从这个Kafka备份集群拉取数据到Hadoop以及我们的数据仓库,那是我们在数据上运行多个上报任务以及分析进程的地方。
未完,后面我不太关心了,看下原文