Kafka设计思想

Kafka存在的原因

kafka主要是用来处理Linkedin的大面积活跃数据流处理(activity stream).

此类的数据经常用来反映网站的一些有用的信息,比如PV,页面展示给哪些用户访问,用户搜索什么关键字最多,这类信息经常被log到文件里,然后线下且周期性的去分析这些数据。现在这种用户活跃数据已经成为互联网公司重要的一部分,所以必须构建一个更轻量且更精炼的基础架构。

活跃数据 使用案列
对用户的好友行为的进行广播。
对某一事物相关性进行裁决,通过进行快速的统计用户投票,点击。
防止用户对网站进行无限制的抓取数据,以及超限制的使用API,辨别垃圾。
对网站进行全方位的实时监控,从而得到实时有效的性能数据,并且即使的发成警告。
批量的导入数据到数据仓库,对数据进行离线分析,从而得到有价值的商业信息。(0.6可以直接将数据导入Hadoop)

活跃数据的特点
高流量的活跃数据是无法确定其大小的,因为他可能随时的变化,比如商家可能促销,节假日打折,突然又冒出一个跳楼价等等。所有的数据可能是数量级的往上递增。
传统日志分析方式都是需要离线,而且操作起来比较复杂,根本无法满足实时的分析。另一方面,现有的消息队列系统只能达到近似实时的分析,因为无法消费大量的持久化在队列系统上的信息。这对Hadoop而言就是个大延迟,因为hadoop集群对消息的处理是非常的迅速的。Kafka的目标就是能够成为一个高效的队列平台,无论是处理离线的信息还是在线的信息。

部署

上图是单个Kafka集群处理所有的来自不同方面的信息。Kafka提供一个单一的数据通道给所有的在线或离线的消费者,根据kafka的设计,消息并不缓存在kafka里,所有这些消息生产者也可以认为消息的缓存者,kafka也可以复制信息给另一个数据处理中心去消费产生的信息。

主要设计思想
kafka 与传统的消息队列设计思想并不一样。
1.可持久化消息.(这里的持久化可能与传统的消息持久不一样)
2.目标是高流量处理而不是花俏的功能
3.消费状态信息并不保存在服务端,而是保存在消费端
4.支持分布式

Kafka 与消息生产者消费者(一个或多个)组成一个逻辑上的小集群组。这里消费者可能有点特殊,每一个消息处理者都属于特定的消息者(一个消费者集群),因此一个消费组消费信息必须看起来像是一个单一的消费者在处理这条信息。这里消费组的概念可以类比JMS里的队列,主题.
比如队列的概念可以理解为:我们将所有的消费者组织成一个消费组。然后每条消息都会被组里的单一的消费者接受处理.
主题的组织形式与上面的类似,只不过组里的每一个消费者都会接受到消息。
在LinkedIn里,更常用的场景是组织多个逻辑上的消费组,每一个组都由消息者集群机器组成,他们的行为犹如单一的消费者。kafka在大数据上的处理由着天然的好处,瞬间存储若干个消费者,主题,消息. PS:主要是因为与传统的消息队列设计上的不一样

Kafka本质上依赖磁盘的文件系统做消息缓存,即使大部分人认为磁盘是非常慢的。实际磁盘的快慢取决于你如何使用,如果用的好的话可以达到网络的传输速度 PS:个人认为应该运用了zero-copy技术。

磁盘读取数据与写数据方式是不同的。6块 7200RPM SATA RAID5阵列的写速度差不多是300MB/sec.但是随机读取的速度差不多是50/kec 差不多10000倍的差距,这种线性的读写模式是最容易预见到的。因此可以很好的通过操作系统来这种技术。(read-ahead and write-hehind: 预先从多个组块获取大数据的,小面积数据写入放在后面).这样磁盘的顺序读的速度可以媲美内存随机读。

现代操作系统的都会转移所有的空闲内存给磁盘做缓存(即使内存重新申请时会有一点点性能折损),这样磁盘读写都会经过这块内存缓冲区。这块区域不会轻易的被回收,除非直接使用I/O操作。所以即使一个进程维护一个处理中的缓存数据,这些数据可能被复制了一份到操作系统的页缓存里。这样等于是将数据存储了两次。

由于Kafka基于JVM,所以必须认识到下面两点
1.对象的内存开销非常高,基本上是两倍或两倍以上的要存储的数据大小。
2.垃圾回收机制变得很难调优。
我们需要2倍的可用缓存才能访问可用的内存(Pagecache 与 进程中的数据),存储压缩的过的对象同样也需要两倍的空间,为此在一个32GB的内存机器上,我们可能会消耗掉28-30GB的内存空间,但是,无需担心GC问题,进一步的好处是,这些缓存可以一直保持着,即使计算机重启奥,(重启后依旧在的数据,实际上是进程数据的拷贝)进程中的数据需要重建到内存中。重建速度也是非常快的。
这种设计的意义在于,与其保存如此多的数据到内存中,然后flush到磁盘上,不如反过来玩,所有的数据先持久化到文件系统中,无需等待flush命令。实际上,就是把数据放入OS cache上,然后让OS flush他到磁盘上。我们只提供一个可配置的flush策略就行了。

可以达到O(1)级别的读取速度。这个完成取决于队列这种应用形式,有别于其他的队列数据结构BTree,Kafka的读写不会互相阻塞(只是简单的将消息保存到log.),所以可以无限制的被多台机器读取,而且即使数据已经被消费者消费掉,数据也可以在kafka里保存很长的时间,比如一周。
消息队列重点优化应该在C端,而不是P端,影响C端主要有两个因素,大量的Request以及大量的copy. Kafka API里提供了message set抽象类,为的使消息打成一个Set而不是一次次的去分发,从而造成不必要的网络开销与磁盘I/O开销。

消息日志由broker自己保存,因此哪怕是一个字节也可以被共享给其他的broker 或者 C ,甚至是P.(这些保存在磁盘上的消息直接通过zero-copy到socket).

消费状态
在Kafka里,需要监控的关键因素是潜在的引起磁盘随机读。这一点与传统的消息队列不同,传统的消息队列关注单位时间内多少消息被消费。因为消息出去之后队列就不需要在管他了,队列里的数据越少越好。OK,让我们看看一些特殊情况,比如断网怎么办。或者消费者只是接受了消息,但是并没有成功的处理消息,而且队列只是记录了这个消息已经成功的被C接受了。并不知道C是否处理完消息,这里会引发多次重复消费消息,(因为网络原因没有及时的收到回应。)

Kafka针对元数据做了两件不一样的事情。
数据流在broker里被分割到不同的区域里。也就是说,producer指明了哪一块区域属于这次产生的消息。消息按顺序存储到每一块区域里。然后以同样的顺序发送给customer.这里没有为每一条信息存储元数据,我们只需要针对消费者,主题,区域的组合存储高位标志(high water mark).因此消费者总元数据的总结状态信息是非常的小。
Kafka也对客户端暴露其消息的消费状态,这样带来的好处非常明显。因为Kafka只记录其消费的状态信息,即元数据,比如客户端提交的是删除数据库里一条信息,当消息消费者处理完后,可以更改其状态值,这样客户端就能够感知到。另外对于搜索服务提交的数据,我们可以通过消费状态的来记录其提交的数据状态,这样当服务发生崩溃的时候,可以根据消息记录的数据偏移位置,继续读取消息。
另外通过这种特性,kafka可以随意的回滚到指定的偏移位,再次读取数据,貌似这种机制很不符合队列的规则,但是,假设消费者有bug,没有读到那一部分数据,这是你就可以使用这一个trick.直到bug被修复. [当然也可以再次的提交数据]

消息从P端PUSH到broker,然后被C端PULL,如果将消息PUSH到C端,会导致C端被淹没,而且这种PUSH方式不能确定C的消费能力。
对比一下C主动PULL,这种方式可能会有一定的延迟,因为他是根据自己的消费能力去PULL数据处理.所以C不能被充分的利用。

有关分布式
Kafka集群主要是基于Zookeeper,借助zk。C端与broker可以插拔自如,因为所有的一致性以及集群改变都可以通过zk的watch广播出去。此外Kafka在C端与Broker之间实现了负载均衡。主要是在zk里注册了自己的一些状态信息作为元数据放在zk里,但是Broker与P端并没有实现负载均衡,目前LinkedIn是通过硬件负责均衡工具去分发数据到不同的Broker上。将来也许会考虑加入这一功能(将所有消息都发到一个特定的broker,由此broker通过Id来实现producer的负载);

此类集群带来的好处如下
1.消费者可以更加有序的处理消息。因为所有的对一个特别的区域更新都被有序的处理成一个单一的流
2.强一致的性的集群均衡,确保每一个broker可以被消费者消费。
3.所有的执行都不会互相牵连,除非集群了新加了一个Broker或者C,引起ZK的广播。
如果这样的话,我们只是简单的针对某一个C锁了部分的消息.然后等到拓扑结构改变完成后,再互相更新元数据.再让被锁的C解锁.

网络层
NIOserver,单线程接受连接请求,多个处理者线程处理固定大小的连接请求。(Reactor模式)

消息
消息由一个固定大小的头部和可变长度的body组成.header里包含了以个版本格式信息以及CRC32校验码。剩下字节都是要传输的数据,Kafka没有固定然后组织这部分数据,Kafka只是简单的把剩下的数据直接发到NIO的channel上以字节流的方式。(这里消息体必须自己定义,可以使用google protocal buffer)

日志
日志文件里记录了主题的message.他会被分配到两个目录里且会以数字结尾加以区别。比如主题名称叫my_tpoic会被复制成my_topic_0,my_topic_1.日志的文件名称是一个连续的4字节整型,所有消息以字节的方式存储在这个文件里。每个消息都被一个64位整型标识着消息的起始位置偏移.(Kafka的消息可以保存很久,所以消息即使发送出去了,也会一直保存着。)磁盘上的日志文件名称是由第一个消息所包含的偏移量来确定的。所以第一个文件是000000000000.kafka 然后额外添加的文件也有一个数字整型,大致上是接着上个文件的最大字节数延伸下来.

消息内容是一个被版本化且维护成一个标准接口,所以消息可以在P,B,C端随便的转发。且不需要复制或者做转换.
On-disk format of a message
message length : 4 bytes (value: 1+4+n)
“magic” value : 1 byte
crc : 4 bytes
payload : n bytes

 

用message的偏移量作为其id,这个貌似很少见。Kafka原本想在P端生成GUID,然后根据GUID与偏移量维护对应到每个broker的映射关系。但是因为C必须维护一个ID对应所有的Server,所以全局的唯一ID没有价值。此外这种随机GUID相对于kafka的偏移量记录设计而言是一个很重的设计(必须与磁盘做同步,这样会涉及到磁盘随机访问,从而失去磁盘O(1)特殊设计).因此想出了这种特别的设计:区域原子计数器。可以轻松的根据区域ID与节点ID去匹配到一个消息ID.(PS:这重区域计数器,有点类似concurrentHashMap思想。)这样一旦我们确定了一个计数器的开始,对于每一块区域都是其唯一性的单调递增增长的整数。这里的偏移量对C端的API是隐藏的。

Kafka的读写删设计

日志文件都是按顺序向文件尾部添加的。当文件达到一定的大小的时候(此参数可配置)的时候会新建一个文件。这里有两个很重要的参数。
一个M代表在当消息达到多少条的时候强行让OS执行flush.
另一个参数S 表示强行flush后,避免多久再次flush.
这里两个参数可以保存在系统崩溃的时候最多多少条消息或者最多多少秒内的消息可能会丢失。

读数据是通过一个消息的64位的偏移量加上需要读取的最大字节数来实现的。这样会得到一个包含指定字节数的消息迭代器,其需要的数据已经包含在迭代器里。这里的需要的字节数应该尽可能的大于单个消息,在处理一个其大无比的消息时,可以进行多次读取,但每次都会是两倍于缓存的字节数,直到消息成功的读完。通过设定最大消息数目和缓存大小可以让服务端拒绝某些过大的消息,从而让客户端确定他必须读完一个完整的有效的消息。这个很像read buffer可以通过判断一个终结符来确定消息的结尾 (通过已经确定的消息的大小)。

实际上读取过程是通过一个定位偏移量起始位置到已经存储在磁盘上的数据来开始的。实质上是从全局偏移量上得出该文件的偏移量。搜索过程是一个二分查找,而不是内存查找。

日志提供最近的可读取的消息给客户端去及时订阅,基本可以达到及时性。而且这对因为出错而不能消费自己的消息的C端来说也是很有用的,你可以指定什么时候再去读取。这样如果C端尝试消费不存在或者越界的消息,则可以重置自己的偏移量或者给一个比较和谐的错误给用户。

下面是给C端的消息格式。
MessageSetSend (fetch result)

total length : 4 bytes
error code : 2 bytes
message 1 : x bytes

message n : x bytes
MultiMessageSetSend (multiFetch result)

total length : 4 bytes
error code : 2 bytes
messageSetSend 1

messageSetSend n

一次只能删除log的上的部分数据,kafka上存在一个日志管理策略,允许指定删除策略,目前的策略是删除多少天之前的数据。为了避免读取准备删除的数据,kafka使用了 写时复制(copy-on-write) 策略,提供一个数据快照给二分查找去搜索要读的数据,这样即使在进行删除数据,也可以保证能够正常的进行。【搜索到要读的数据可能不存在了,怎么办?】

意外关机恢复消息检查时,kafka会迭代检查最新的log上的每条消息,通过检查文件大小和偏移量是否小于log文件以及消息 payload部分的CRC值来确定此消息是否有效。

这里存在两种不完整消息错误需要被处理,一种是因为服务器崩溃导致未写的数据块丢失。另一种是因为无意义的数据块被添加到文件里(这种情况主要产生的原因是因为操作系统没有办法保障写入的顺序刚好是文件索引节点与数据块相对应,如果索引节点刚更新其缓存大小,其在块消息写入其文件时发生了故障,那么缓存指向的区域是脏数据区。)这里通过CRC检验避免此类事情发生。即使未写的数据已经丢失。

分布式

上面已经说了Kafka的分布式是broker与consumer之间的分布式协调。然后通过ZooKeeper的来维护一致性。基本的zk里的符号代表先简单说一下。[xyz]代表一个某路径下的不定的名称,如/topics/[topic] 其意思是说节点目录topics下可能有个子目录,然后[0...5]代表子目录范围0,1,2,3,4 箭头->代表一种包含关系 /hello->world 代表节点目录/hello 包含一个world子目录。

Broker节点注册
/brokers/ids/[0...N] –> host:port (ephemeral node)
上面的目录结构表示所有broker节点的上的id,此id对consumer是可见的。所以不能重复,而且是可以配置的。在启动的时候,broker节点会创建一个id放到brokers/ids下,通过ZK,broker可以随时的换物理机器只要其id跟着走就行了。此注册方式是 ephemeral,所以断开或者broker发生故障此broker会在集群里丢失。

Broker主题注册
/brokers/topics/[topic]/[0...N] –> nPartions (ephemeral node)
这个应该能看懂了,每个broker注册自己到/brokers/topics下,且持有一定的消息分区在[topic]下

消费者与消费者组
主题消费者也把自己注册到ZK里,这样可以负载消息消费并且broker可以记录消息在每块区域的偏移量.多个消费者组成一个消费组,此组的消费者只消费一次主题,每个组里的consumer都有一个共享的grouo_id.比如某一个消费过程需要横跨三台物理机器,这样你可以为这个组里的consumer定义一个group_id.这个id可以配置在consumer的配置文件里,这样可以以你的方式告诉conumer它属于哪一个group. consumer会尽可能的公平的划分成组,每个组里只有一个consumer去处理消息.

Consumer Id注册
组里的consumer有一个短暂的唯一的id(通过hostname:uuid)通过如下方式注册
/consumers/[group_id]/ids/[consumer_id] –> {“topic1″: #streams, …, “topicN”: #streams} (ephemeral node)
每个consumer在指定的组里注册一个节点以consumer_id做为标识。此节点包含一个map结构的数据,这里consumer_id节点在zk里也是临时的。所以节点出故障就会消失。

定位consumer偏移
consumer会记录他在每一个分区里最大消息偏移量,在zk里存储结构如下
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id] –> offset_counter_value ((persistent node)

消息分区拥有者注册
每个broker的消息分区只会被消息组里的一个唯一的consumer消费,所以必须在消费行为产生之前,consumer必须与给定的消息分区建立关系。主要是通过把consumer_id写到指定的消息分区下面(也是建立一个临时的节点)。
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id] –> consumer_node_id (ephemeral node)

Broker节点注册
Broker节点基本上是独立的。他们只发布他所拥有的消息,当一个新的broker加入进来的时候他只是简单的把自己的主机名称与端口写到broker注册的节点目录下。broker上会动态的添加新建立的topic,同时其消息分区也会记录在该broker的主题下

Consumer注册算法

一个起来的consumer主要做下面4件事情
1.把自己的id注册到指定的组里
2.在consumer id 节点上注册一个watch,当有新的consumer加入或者已经存在的consumer离开,都会重新负载该consumer组里的所有consumers.
3.在broker id节点上注册一个watch,当有新的broker加入或者旧的离开,会重新调配各个组里的consumers.
4.consumer自我平衡在组里的消费能力。

Consumer rebalancing 算法
首先consumer rebalancing 只发生在broker的变动,以及自身组里变动.会让所有的consumers达成一致使之确定一个consumer在其消费区域消费。消息,消费组,以及消息区域都是公平的划分给组里的每个consumer.一个消息区域总是被一个consumer消费,如果发生consumer多于消息分区,那么会有一些consumer永远消费不到消息。kafka会通过减少broker节点数目来阻止其consumer的连接,这样就会平衡消息分区与consumer之间的对应关系。

重新平衡期间,consumer会做以下的事情。
遍历每个consumer订阅的主题
生成者P(T) 将会有的分区都产生相关的主题 T
让Consumer (G)把这同一个组里所有consumer使之看成像一个Consumer(i)去消费主题 T
排序P(T)
排序C(G)
让 i 成为该组里的代表者Consumer(i)的索引位置,设置N = size(PT) / size(CG)
给Consumer(i)分配区域范围 i*N 到 (i+1)*N-1
从分区所有者的注册下删除Consumer(i)当前拥有的条目
添加新的分区到分区所有者注册的节点上
不停的尝试上面的步骤直到原来的分区拥有者释放掉其原来的关系

 

转载于:https://my.oschina.net/weiweiblog/blog/475972

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值