kafka系列教程2(设计构造及原理1)

时间:2021-09-21 16:30:29
kafka采用了一些非主流(unconventional)并经过实践的设计使其高效和可扩展。在实际使用中kafka显示出了相对于常见流行的消息系统的优越性。并且每天能够处理上百GB的新的数据。
类似收集实时数据来获得查询、推荐、广告方感兴趣的内容时,需要计算大量细粒度的点击率,还包括那些没有点击的页面。在facebook
大约6TB日志记录用户行为事件,中国移动大约生成5-8TB日志为通话记录。早期处理这些数据都是将日志离线获取并抓取日志(scraping log)后进行处理。最近一些分布式的日志收集产生了,如facebook的Scribe,雅虎的Data
Highway,Cloudera的Flume。这些系统主要被设计用来收集及装载数据到warehouse或hadoop进行离线消费处理。但是在我们LinkdIn(社交关系网)有时候需要一些额外的需求,不同于离线分析,我们需要一些实时的和上面类似的应用却仅有几秒钟的延迟。于是我们做了一个kafka,将传统的日志收集和消息系统的优点于一身。一方面kafka是分布式且可扩展的,并提供了高的吞吐量。另一方面。kafka提供了和消息系统类似的api并且支持应用来实时的消费数据。在LinkedIn生产环境中已成功运行超过6个月。
相关的信息
传统的消息系统用来异步处理数据流。但是都不太适合用于日志处理。原因如下:
1.传统企业级消息系统存在不匹配的功能:它们重点是关注消息递送的保证性(比如websphere
MQ支持事务来允许应用自动插入多个队列,JMS规范允许每个独立的消息都能够被acknowledged,这样潜在的就存在消息非顺序性。)。这些保证往往杀伤力太大,对于日志收集来讲,偶尔的事件丢失无关紧要。
2.大多数系统并不关注高吞吐量。例如JMS没有api来可以在一次请求中明确地批量生产数据。
3.这些系统对于分布式支持都比较弱。没有号的方法进行分区和存储消息到多个集群中。
4.大部分系统都假设消息都会即刻被消费。队列中未消费的消息通常都很少。一旦消息堆积(accumulate)则性能将显著降低。
大量的专门日志收集器也有一段时间了,如facebook的Scribe。Yahoo的data
highway工程。Cloudera的Flume是相对较新的日志搜集器支持可扩展的pipes和sinks使得日志流数据可以非常灵活,同时也支持分布式。但是大多数使用它离线消费,并且暴露了一些不必要的实现细节(如miniute文件)。另外大多数使用“推”方式使broker将消息推给消费者。这在LinkedIn中不太适用(消费者希望最快的检索,避免“推”的太多导致性能消耗大于消息处理的消耗)。同时,拉也有利于“倒带”(多次获取相同时间段的数据)。最近Yahoo开发了pub/sub系统叫HedWig支持扩展和可靠性,持久化保证。这趋于用在存储数据存储系统的提交日志上。
kafka构造及原理
 
我们采用了大量方式保证系统的高效
1.简单存储。
kafka有非常简单的存储设计。每个分区对应一个逻辑日志。物理上一个日志是由多个文件组成。每个文件包含日志的一个片段(估计约有1GB)。每次生产者发布消息到分区,broker简单的将消息追加到最后的文件片段中。为了更好的性能,我们在消息量达到指定数量或时间超过某个值后flush一次文件片段。只有flush过的消息才能够被消费者看的。不同于其他典型的消息系统,存储在kafka的消息没有明确的消息id,事实上,每个消息被指定在一个log中逻辑的offset位置。这可以降低维护,密集查询,随机存储等大量访问index结构(用于map消息id到实际的消息位置的数据结构)的开销。虽然我们消息ids是增长的,但不是连贯的。为了获得下一个消息的id,我们必须添加当前消息的长度到自己的id上。所以我们的消息id和offset实际上可以相互转换。
通常消费者序列的消费一个分区,如果消费者ack了一个分区的offset,这表明消费者收到了所有在offset之前的消息。在这背后(Under
the
covers)实际上是消费者异步拉取请求broker获得一个buffer并准备好给应用程序消费。每个拉取请求中包含消息的起始offset及可接受的字节数(注意不是消息数)。每个broker在内存中有一份已排序的offsets索引列表,包含每个分片文件中的第一个消息的offset。broker指示请求中具体消息在那个片段文件中,然后发送回消息给消费者。当消费者收到消息,则计算下一个消息的offset并消费和作为下次的请求参数。有关kafka日志及内存索引描绘如下图:
kafka系列教程2(设计构造及原理1)
2.有效的传输
我们非常关注消息在kafka的传入传出。我们说过生产者可以提交一批消息作为一个请求。虽然消费者api在遍历消息是一个一个的,而在背后消费者也是获取一批消息作为一个请求的。
另一个非主流的选择是我们避免明确的缓存消息在kafka层。事实上我们依赖于底层的文件系统页缓存。这有助于避免双重缓存,及即消息只缓存了一份在页缓存中。同时这在kafka重启后保持缓存warm也有额外的优势。因此kafka根本不缓存消息在进程中。故gc开销也就很小。开发基于VM语言高效的实现也是可行的。既然消费者和生产者都是序列的操作文件片段,消费者稍微落后于生产者,那么通常操作系统的启发式缓存(write-through caching and readahead)就能起到很好的效果。我们发现无论消费还是生产始终保持着线性的数据大小,无论多少TB的数据量。
另外我们优化了消费者的网络访问。kafka是支持多个消费者同时访问相同消息的。一般一个典型的从本地文件到远程socket的消息发送由以下几步完成:1.os从磁盘读数据到页文件。2.复制页文件中数据到应用缓存。3.复制应用缓存到另一个核心缓存。4.发送核心缓存到socket。这包含了3个数据复制和两次系统调用。事实上很多系统存在一个sendfile api,能够直接将数据佛纳甘一个文件channel到socket
channel.这避免了2次的复制和一次系统调用(即第2及第3步)。kafka利用sendfile
api来有效的发送数据从文件片段到消费者。
3.无状态的broker
不同于其他消息系统。有关消费者处理了多少消息等状态并没有被broker维护。而是由消费者自己维护。该设计降低了大量的复杂操作及开销。但是这也导致消息的删除比较棘手。由于broker不清楚是否所有的订阅者都消费了消息。故kafka采用简单的服务端确定的基于时间(time-base
SLA)保留策略。即当保留了一定时期后消息自动被删除(通常7day)。而且事实上在实践中运行的很好。kafka不会因大量的数据文件太大导致性能降低。另一个方面这也有益于消费者重复获取和消费已消费过的数据。这有悖于通常的队列的概念。但是被证实在很多消费者中是一个必要的功能。比如消费者端当出现逻辑错误时,应用程序可以回放一些消息直到错误被修正。该机制在ETL数据加载到数据仓库或hadoop系统中尤为重要。再举个例子,消费者可能定期的flush消息到持久化存储中。如歌消费者挂了,未flush的消息将丢失。如果是这种情况,消费者可以设置一个检查点来记录最小的未flush的offset。一旦消费者重启,则可以从这个检查点开始继续消费。我们注意到“倒带”这种机制采用拉比推更简单。
分布式协调
 
kafka有个消费组的概念。每个消费者由一个或多个消费者共同的消费topics。举例说明:一个消息只能被消费组中一个消费者拿到,而位于不同的消费组中的消费者则能拿到全部的消息并且不需要在消费组间进行协调。同一个消费组的消费者可以位于不同的进程甚至不同的机器。我们的目的是把这些消费者划分不同的broker。同时不需要太多的协调用的开销。
第一个决定就是在同一时间,每个topic中的不同分区只能被消费组中的一个消费者消费。如果是多个消费者消费相同的分区时会导致额外的开销(如要协调哪个消费者消费哪个消息,还有锁及状态的开销)。在我们设计中消费进程只需要在重新负载(重新指定哪些分区分配给哪些消费者消费)时进行一次协调(这种协调不是经常性的,故可以忽略开销)。为了能够保证负载均衡的有效性。我们要求分区数要大于消费组中的消费者数。
第二个决定是没有中心节点,而是让消费者们自己协调(去中心化的风格)。以避免因担心中心节点挂掉导致各种复杂操作。为此我们引入了zookeeper。kafka使用zookeeper做以下事情。1.探测broker和consumer的添加或移除。2.当1发生时触发每个消费者进程的重新负载。3.维护消费关系和追踪消费者在分区消费的消息的offset。特别的,当每个broker或consumer起来时,则会注册相应的信息到zookeeper中。如broker的hostname,port,有哪些topic,partition位置,consumer位于哪个消费组,哪些topic在消费等。每个消费组是一个所有者注册单位,包含了每个订阅的分区,内容中海油哪个消费者当前在消费哪个分区(即消费者是该分区的所有者),已消费的offset的位置。broker,消费者,所有者的注册路径是ephemeral(临时路径)。而offset的注册是持久化路径。如果broker挂了,所有在上面的分区注册信息都会被移除。如果一个消费者挂了将失去消费者注册信息及所拥有的分区。每个消费者既要关注broker注册信息还要关注消费者注册信息。如果broker或消费组内容发送变化都要受到通知。当消费者启动或收到broker或其他消费者数发生变化的通知后,消费者初始化重新负载处理器来决定哪些新的分区被该消费者所有。处理算法见下图:
kafka系列教程2(设计构造及原理1)
通过读取zookeeoer中的broker及消费者注册信息,获取剩余可用的分区集合Pt及当前存在的消费者集合Ct,然后按Pt/Ct分区数分成几块。由当前消费者在Ct中的位置来确定属于哪块,然后在自己写入信息到自己相关的注册信息中。最后,消费者启动一个线程从对应的分区中拉取数据。offset来自offset注册信息中。当消息被拉取后,消费者定期的更新offset到offset注册信息中。当有多个消费者在同一个消费组时,broker或消费者数的变化都将使每个消费者都收到通知,这样每个消费者收到通知的时间可能有点不同。这就存在一个消费者所拥有的分区可能也是另一个消费者所拥有的分区。如果出现这种情况,第一个消费者简单释放所拥有的分区,等待一会儿然后再次重新负载均衡。实践中,几次重试后重新负载即可稳定。
当消费组创建好时,offset注册信息中是没有数据的。这时候消费者会选择从最小或者最大的offset位置上开始读(配置决定)。该offset数据可以通过我们提供的api调用broker获得。
交付保证
 
通常情况下kafka只保证至少一次性交付(at-least-once
delivery)。确切讲一次性交付(once
delivery)通常需要两阶段提交,这不是我们应用中所必须的。大部分情况,消息也确实是一次性交付给了每个消费组。但是一旦消费者进程挂掉而又没有清理,其他消费者接管分区后按照最后成功提交到zookeeoer中的offset开始读取消息,这就很可能重复读到一些原消费者未成功提交最新offset的消息。如果应用程序在意这种情况,则需要自己写去重逻辑。可以依靠我们提供的offset或者在消息中带上唯一key。这种途径比采用两阶段提交要划算。
kafka保证来自相同的分区的消息投递到消费者是顺序的。但是不保证消息来自不同分区的顺序性。为避免日志腐蚀,kafka存储CRC到每个消息的日志中。一旦broker出现IO错误,kafka运行恢复进程来移除那些与CRC匹配不一致的消息。使用CRC也允许我们在生产和消费时检查网络异常。
一旦broker挂掉,所有未消费的消息将不可用。如果持久化的数据永久破坏掉时,那些未消费的消息也就永远丢失了。在将来我们计划给kafka添加内建冗余(redundantly)复本在多个broker中(实际上kafka现在版本已经支持复本)。
kafka在linkedin中的运用
 
下图显示了最初开发版的情形。我们有一个kafka集群在数据中心与user facing
service一起运行。frontend服务生成了大量的log并批量的发送到broker中。我们依赖硬件load-balancer来分发发布请求。线上的消费者消费kafka数据也是在相同的数据中心里。我们也部署了一个kafka集群在另一个数据中心进行离线分析。与hadoot集群及其他数据仓库距离很近。这里的kafka实例运行内嵌的消费者从kafka拉取数据到现场数据中心。我们然后云溪数据加载进程将消息从该副本到hadoop和其他数据仓库中。同时我们还运行了很多报表进程和分析这些数据,我们还是用kafka集群进行原型分析(prototyping)和运行小的脚本处理原始事件流用于临时查询。无需太多的调节就可以让端对端的延迟在10秒左右,已经满足我们的需求。
kafka系列教程2(设计构造及原理1)
对比试验
我们进行了一些实验对比,主要比较activemq(使用KahaDB持久化),RabbitMQ与kafka的性能。我们尽可能的给每个系统于最佳配置。
测试机器:2台linux机器,配置为8个2GHz核,16GB内存
6个Raid10磁盘。使用1Gb网络。一个机器用于服务器,而另一个机器用于生产者和消费者。
生产者测试:结果如下图:
kafka系列教程2(设计构造及原理1)
对比数值kafka至少是RabbitMQ的两倍,是ActiveMQ的一个数量级。一个是因为kafka无需ack,另一个是因为kafka消息头很小,9bytes,而ActiveMQ由于JMS协议使其头较大144bytes。
消费者测试,结果如图所示:
kafka系列教程2(设计构造及原理1)
我们用一个消费者消费者这1千万的消息。同时设置RabbitMq和activeMq的acknowledge为自动。从平均值来看,kafka消费大约是ActiveMQ和RabbitMQ的4倍。这由于以下原因:kafka的存储格式更有效,传输数据少,另一个原因是ActiveMQ和RabbitMQ都需要维护每个消息发送后的交付状态。