参考文档:
http://kafka.apache.org/documentation/#introduction
http://kafka.apachecn.org/documentation.html
https://www.jianshu.com/p/d3e963ff8b70
1、JMS是什么
(1)JMS的基础
JMS是什么
JMS是Java提供的一套技术规范(Java Message Service)
JMS作用
用来异构系统,集成通信,环节系统瓶颈,提供系统的伸缩性、增强系统用户体验,使得系统模块化和组件化变得可行并更加灵活。
通过什么方式
生产消费这模式(生产者、服务器、消费者)
实现类似模式的有:
JDK、kafka、ActiveMQ
(2)JMS消息传输模型
i、点对点模式
一对一、消费者主动拉取数据,消息收到后消息清除。点对点模式通常是一个给予拉取或者轮询的消息传送模型,这种模型从队列中请求信息,而不是将消息推送到客户端。这个模型的特点是发送到队列的消息被一个且只有一个接收者处理,即使有多个消息监听者也是如此;
ii、发布/订阅模式
一对多,数据生产后,推送给所有订阅者。发布订阅模式则是一个给予推送的消息传送模型,发布订阅模式可以有多种不同的订阅者,临时订阅者旨在主动监听主题时才接收消息,而持久订阅者则监听主题的所有消息,即使当前订阅者不可用,处于离线状态。
kafka实现区别在不直接推送数据,而是放在队列中,消费者自己来拉取数据
生产数据:queue.put(object)
消费数据:queue.take(object)
(3)JMS核心组件
Destination
消息发送的目的地,也就是前面说的Queue和Topic
Message
从字面上就可以看到被发送的消息
Producer
消息的生产者
MessageConsumer
与生产者对应,这是消息的消费者或接收者,通过它来接收一个消息;
(4)常见的JMS消息服务器
JMS消息服务器ActiveMQ
分布式消息中间件Metamorphosis
分布式消息中间件RocketMQ
其他MQ
2、Kafka是什么
kafka是一个分布式消息队列。具有高性能、持久化、多副本备份、横向扩展能力。生产者往队列里写消息,消费者从队列里取消息进行业务逻辑。一般在架构设计中起到解耦、削峰、异步处理的作用。
(1)Apache Kafka是一个开源的消息系统,由Scala编程,是由Apache软件基金会开发的一个开源消息系统项目
(2)Kafka最初由LinkedIn开发,并于2011年初开源。2012年10月从Apache Incubator毕业。该项目的目标是为处理实时数据提供一个统一、高通量、低等待的平台;
(3)Kafka是一个分布式消息队列:生产者、消费者功能。它提供类似于JMS的特性,但是在设计实现上完全不同,此外它并不是JMS规范的实现。
(4)kafka对消息保存时根据topic进行归类,发送者成为producer,消息接收者成为consumer,此外kafka集群有多个kafka实例组成,每个实例(server)称为broker
(5)无论是Kafka集群,还是producer和consumer都依赖于Zookeeper集群保存一些meta信息,来保证系统可靠性;
Apache Kafka与传统的消息系统相比,有以下不同:
(1)它被设计成一个分布式系统,易于向外扩展;
(2)它同时为发布和订阅提供高吞吐量;
(3)它支持多订阅者,当失败时能自动平衡消费者;
(4)它将消息持久化到磁盘,因此可用于批量消费,例如ETL,以及实时应用程序。
架构图:
生产者是能够发布消息到话题的任何对象,已发布的消息保存在一组服务器中,它们被成为代理(broker)或kafka集群;消费者可以订阅一个或多个话题,并从Broker拉取数据,从而消费这些已发布的消息;话题(Topic)是特定类型的消息流,消息是字节的有效负载(Payload),话题是消息的分类或种子(Feed)名。
kafka作为分布式消息系统,提供了生产者,缓冲区,消费者的模型
broker:kafka集群的一个节点就是一个broker,存储消息,集群由多个broker组成
topic:话题是kafka给消息提供的分类模式,broker用于存储不同话题的消息数据;
partition:topic物理上的分组,一个topic可以分成多个partition,每一个partition是一个有序队列;kafka对外使用topic的概念,生产者往topic里写消息,消费者从读消息。为了做到水平扩展,一个topic实际是由多个partition组成的,遇到瓶颈时,可以通过增加partition的数量来进行横向扩容。单个parition内是保证消息有序。
segment:partition物理上有多个segment组成;
message:消息,生产者通信过程中产生的数据流就是消息;
producer:生产者,往broker中的某个话题里面生产数据的逻辑实体,生产者向kafka生产数据流的时候需要指定话题;
consumer:消费者,从kafka中读取数据的逻辑实体;
设计思想
kafka将所有消息组成多个topic的形式进行存储,每一个topic可以被分成多个partition(分区),每一个partition由多个消息组成,每一个消息都被表示了一个offset,每个消息是按顺序存储在partition中(速度块的原因之一:每条消息都包含一个键一个值和一个时间戳);
消息是以一个个id的方式组合起来(offset),consumer会选择一个topic,通过id指定从哪个位置开始消费消息(数据),消费完成之后会保留id(offset)下次可以直接从这个id继续进行消费,当然也可以从任意位置开始消费。消费者可以根据需求,指定offset进行消费;消息以partition为单位,分配到多个server中,并以partition为单位进行备份(副本数),通过zk进行数据的备份。
消息不变性,对并发的消息提供了线程安全的保证,每一个consumer都保存自己的消费的偏移量,相互之间互不干扰,不存在线程安全问题;
消息访问的并行高效性,每一个topic中的消费被组成多个partition,partition被均匀分配到集群的server中,消费消息的时候会路由到指定的partition中,减少每一个consumer的竞争。
消息系统的伸缩性,每一个话题中保留的消息非常庞大,通过partition将消息切分成多个子消息,并通过负载均衡策略将partition分配到不同的server中,当机器负载重的时候通过扩容集群将消息重新分配。
消息可靠性,消息被消费完成之后不会被删除,可以通过重置偏移量重新消费,保证消息不会丢失。
消息持久化,通过指定时间段来保存消息,节省了broker存储空间。
对于每个主题,Kafka群集都维护一个分区日志,如下所示:
每个分区都是一个有序的,不可变的记录序列,不断附加到结构化的提交日志中。分区中的记录每个都被分配一个称为偏移的顺序ID号,它唯一地标识分区中的每个记录。 Kafka集群持久地保留所有已发布的记录 - 无论它们是否已被消耗 - 使用可配置的保留期。例如,如果保留策略设置为两天,则在发布记录后的两天内,它可供使用,之后将被丢弃以释放空间。Kafka的性能在数据大小方面实际上是恒定的,因此长时间存储数据不是问题。
生产者参数:
(1)topic:指定往哪个topic产生消息
(2)partition:往哪个partition产生消息
(3)key:根据key将消息分到不同的partition中
(4)message:消息
消费者
消费者使用消费者组名称标记自己,并且发布到主题的每个记录被传递到每个订阅消费者组中的一个消费者实例。消费者实例可以在单独的进程中,也可以在不同的机器如果所有使用者实例具有相同的使用者组,则记录将有效地在使用者实例上进行负载平衡。如果所有消费者实例具有不同的消费者组,则每个记录将广播到所有消费者进程。
两个服务器Kafka群集,托管四个分区(P0-P3),包含两个使用者组。消费者组A有两个消费者实例,B组有四个消费者实例。
然而,更常见的是,我们发现主题具有少量的消费者群体,每个“逻辑订户”一个。每个组由许多用于可伸缩性和容错的消费者实例组成。这只不过是发布 - 订阅语义,其中订阅者是消费者群集而不是单个进程。
在Kafka中实现消费的方式是通过在消费者实例上划分日志中的分区,以便每个实例在任何时间点都是分配的“公平份额”的独占消费者。维护组中成员资格的过程由Kafka协议动态处理。如果新实例加入该组,他们将从该组的其他成员接管一些分区; 如果实例死亡,其分区将分发给其余实例。
Kafka仅提供分区内记录的总订单,而不是主题中不同分区之间的记录。对于大多数应用程序而言,按分区排序与按键分区数据的能力相结合就足够了。但是,如果您需要对记录进行总订单,则可以使用仅包含一个分区的主题来实现,但这将意味着每个使用者组只有一个使用者进程。
3、kafka特点
(1)支持高throughput(高吞吐量)的应用,源自多分区
(2)无需停止即可扩展机器
(3)持久化:通过数据持久化到硬盘以及replication防止数据丢失
(4)支持online(实时消费)和offline(离线消息-如按天消费)的场景
(5)依赖Zookeeper集群,状态信息都写在Zookeeper集群中;
4、kafka数据流程图
、
大概用法就是,Producers往Brokers里面的指定Topic中写消息,Consumers从Brokers里面拉去指定Topic的消息,然后进行业务处理。
图中有两个topic,topic 0有两个partition,topic 1有一个partition,三副本备份。可以看到consumer gourp 1中的consumer 2没有分到partition处理,这是有可能出现的,下面会讲到。
关于broker、topics、partitions的一些元信息用zk来存,监控和路由啥的也都会用到zk。
生产
基本流程是这样的:
创建一条记录,记录中一个要指定对应的topic和value,key和partition可选。 先序列化,然后按照topic和partition,放进对应的发送队列中。kafka produce都是批量请求,会积攒一批,然后一起发送,不是调send()就进行立刻进行网络发包。
如果partition没填,那么情况会是这样的:
1. key有填
按照key进行哈希,相同key去一个partition。(如果扩展了partition的数量那么就不能保证了)
2. key没填
round-robin来选partition
这些要发往同一个partition的请求按照配置,攒一波,然后由一个单独的线程一次性发过去。
API
有high level api,替我们把很多事情都干了,offset,路由啥都替我们干了,用以来很简单。
还有simple api,offset啥的都是要我们自己记录。
partition
当存在多副本的情况下,会尽量把多个副本,分配到不同的broker上。kafka会为partition选出一个leader,之后所有该partition的请求,实际操作的都是leader,然后再同步到其他的follower。当一个broker歇菜后,所有leader在该broker上的partition都会重新选举,选出一个leader。(这里不像分布式文件存储系统那样会自动进行复制保持副本数)
然后这里就涉及两个细节:怎么分配partition,怎么选leader。
关于partition的分配,还有leader的选举,总得有个执行者。在kafka中,这个执行者就叫controller。kafka使用zk在broker中选出一个controller,用于partition分配和leader选举。
partition的分配
1. 将所有Broker(假设共n个Broker)和待分配的Partition排序
2. 将第i个Partition分配到第(i mod n)个Broker上 (这个就是leader)
3. 将第i个Partition的第j个Replica分配到第((i + j) mode n)个Broker上
leader容灾
controller会在Zookeeper的/brokers/ids节点上注册Watch,一旦有broker宕机,它就能知道。当broker宕机后,controller就会给受到影响的partition选出新leader。controller从zk的/brokers/topics/[topic]/partitions/[partition]/state中,读取对应partition的ISR(in-sync replica已同步的副本)列表,选一个出来做leader。
选出leader后,更新zk,然后发送LeaderAndISRRequest给受影响的broker,让它们改变知道这事。为什么这里不是使用zk通知,而是直接给broker发送rpc请求,我的理解可能是这样做zk有性能问题吧。
如果ISR列表是空,那么会根据配置,随便选一个replica做leader,或者干脆这个partition就是歇菜。如果ISR列表的有机器,但是也歇菜了,那么还可以等ISR的机器活过来。
多副本同步
这里的策略,服务端这边的处理是follower从leader批量拉取数据来同步。但是具体的可靠性,是由生产者来决定的。
生产者生产消息的时候,通过request.required.acks参数来设置数据的可靠性。
acks | what happen |
0 | which means that the producer never waits for an acknowledgement from the broker.发过去就完事了,不关心broker是否处理成功,可能丢数据。 |
1 | which means that the producer gets an acknowledgement after the leader replica has received the data. 当写Leader成功后就返回,其他的replica都是通过fetcher去同步的,所以kafka是异步写,主备切换可能丢数据。 |
-1 | which means that the producer gets an acknowledgement after all in-sync replicas have received the data. 要等到isr里所有机器同步成功,才能返回成功,延时取决于最慢的机器。强一致,不会丢数据。 |
在acks=-1的时候,如果ISR少于min.insync.replicas指定的数目,那么就会返回不可用。
这里ISR列表中的机器是会变化的,根据配置replica.lag.time.max.ms,多久没同步,就会从ISR列表中剔除。以前还有根据落后多少条消息就踢出ISR,在1.0版本后就去掉了,因为这个值很难取,在高峰的时候很容易出现节点不断的进出ISR列表。
从ISA中选出leader后,follower会从把自己日志中上一个高水位后面的记录去掉,然后去和leader拿新的数据。因为新的leader选出来后,follower上面的数据,可能比新leader多,所以要截取。这里高水位的意思,对于partition和leader,就是所有ISR中都有的最新一条记录。消费者最多只能读到高水位;
从leader的角度来说高水位的更新会延迟一轮,例如写入了一条新消息,ISR中的broker都fetch到了,但是ISR中的broker只有在下一轮的fetch中才能告诉leader。
也正是由于这个高水位延迟一轮,在一些情况下,kafka会出现丢数据和主备数据不一致的情况,0.11开始,使用leader epoch来代替高水位。
思考:
当acks=-1时
1. 是follwers都来fetch就返回成功,还是等follwers第二轮fetch?(lixx的理解:fetch到并不一定成功,所以一定等下一轮上报上一轮高水位后确定所有机器都已经同步完成)
2. leader已经写入本地,但是ISR中有些机器失败,那么怎么处理呢?(lixx的理解:等一下次follower同步并且写成功且在下一次更新上报高税后才能给客户端返回成功!)
消费
订阅topic是以一个消费组来订阅的,一个消费组里面可以有多个消费者。同一个消费组中的两个消费者,不会同时消费一个partition。换句话来说,就是一个partition,只能被消费组里的一个消费者消费,但是可以同时被多个消费组消费。因此,如果消费组内的消费者如果比partition多的话,那么就会有个别消费者一直空闲。
API
订阅topic时,可以用正则表达式,如果有新topic匹配上,那能自动订阅上。
offset的保存
一个消费组消费partition,需要保存offset记录消费到哪,以前保存在zk中,由于zk的写性能不好,以前的解决方法都是consumer每隔一分钟上报一次。这里zk的性能严重影响了消费的速度,而且很容易出现重复消费。
在0.10版本后,kafka把这个offset的保存,从zk总剥离,保存在一个名叫__consumeroffsets topic的topic中。写进消息的key由groupid、topic、partition组成,value是偏移量offset。topic配置的清理策略是compact。总是保留最新的key,其余删掉。一般情况下,每个key的offset都是缓存在内存中,查询的时候不用遍历partition,如果没有缓存,第一次就会遍历partition建立缓存,然后查询返回。
确定consumer group位移信息写入__consumers_offsets的哪个partition,具体计算公式:
//groupMetadataTopicPartitionCount由offsets.topic.num.partitions指定,默认是50个分区。
__consumers_offsets partition = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)
思考:
如果正在跑的服务,修改了offsets.topic.num.partitions,那么offset的保存是不是就乱套了?
分配partition--reblance
生产过程中broker要分配partition,消费过程这里,也要分配partition给消费者。类似broker中选了一个controller出来,消费也要从broker中选一个coordinator,用于分配partition。
下面从顶向下,分别阐述一下
1. 怎么选coordinator。
2. 交互流程。
3. reblance的流程。
选coordinator
1. 看offset保存在那个partition
2. 该partition leader所在的broker就是被选定的coordinator
这里我们可以看到,consumer group的coordinator,和保存consumer group offset的partition leader是同一台机器。
交互流程
把coordinator选出来之后,就是要分配了
整个流程是这样的:
1. consumer启动、或者coordinator宕机了,consumer会任意请求一个broker,发送ConsumerMetadataRequest请求,broker会按照上面说的方法,选出这个consumer对应coordinator的地址。
2. consumer 发送heartbeat请求给coordinator,返回IllegalGeneration的话,就说明consumer的信息是旧的了,需要重新加入进来,进行reblance。返回成功,那么consumer就从上次分配的partition中继续执行
reblance流程
1. consumer给coordinator发送JoinGroupRequest请求。
2. 这时其他consumer发heartbeat请求过来时,coordinator会告诉他们,要reblance了。
3. 其他consumer发送JoinGroupRequest请求。
4. 所有记录在册的consumer都发了JoinGroupRequest请求之后,coordinator就会在这里consumer中随便选一个leader。然后回JoinGroupRespone,这会告诉consumer你是follower还是leader,对于leader,还会把follower的信息带给它,让它根据这些信息去分配partition
5、consumer向coordinator发送SyncGroupRequest,其中leader的SyncGroupRequest会包含分配的情况。
6、coordinator回包,把分配的情况告诉consumer,包括leader。
当partition或者消费者的数量发生变化时,都得进行reblance。
列举一下会reblance的情况:
1. 增加partition
2. 增加消费者
3. 消费者主动关闭
4. 消费者宕机了
5. coordinator自己也宕机了
消息投递语义
kafka支持3种消息投递语义
At most once:最多一次,消息可能会丢失,但不会重复
At least once:最少一次,消息不会丢失,可能会重复
Exactly once:只且一次,消息不丢失不重复,只且消费一次(0.11中实现,仅限于下游也是kafka)
在业务中,常常都是使用At least once的模型,如果需要可重入的话,往往是业务自己实现。
At least once
先获取数据,再进行业务处理,业务处理成功后commit offset。
1、生产者生产消息异常,消息是否成功写入不确定,重做,可能写入重复的消息
2、消费者处理消息,业务处理成功后,更新offset失败,消费者重启的话,会重复消费
At most once
先获取数据,再commit offset,最后进行业务处理。
1、生产者生产消息异常,不管,生产下一个消息,消息就丢了
2、消费者处理消息,先更新offset,再做业务处理,做业务处理失败,消费者重启,消息就丢了
Exactly once
思路是这样的,首先要保证消息不丢,再去保证不重复。所以盯着At least once的原因来搞。 首先想出来的:
1. 生产者重做导致重复写入消息----生产保证幂等性
2. 消费者重复消费---消灭重复消费,或者业务接口保证幂等性重复消费也没问题
由于业务接口是否幂等,不是kafka能保证的,所以kafka这里提供的exactly once是有限制的,消费者的下游也必须是kafka。所以一下讨论的,没特殊说明,消费者的下游系统都是kafka(注:使用kafka conector,它对部分系统做了适配,实现了exactly once)。
生产者幂等性好做,没啥问题。
解决重复消费有两个方法:
1. 下游系统保证幂等性,重复消费也不会导致多条记录。
2. 把commit offset和业务处理绑定成一个事务。
本来exactly once实现第1点就ok了。
但是在一些使用场景下,我们的数据源可能是多个topic,处理后输出到多个topic,这时我们会希望输出时要么全部成功,要么全部失败。这就需要实现事务性。既然要做事务,那么干脆把重复消费的问题从根源上解决,把commit offset和输出到其他topic绑定成一个事务。
生产幂等性
思路是这样的,为每个producer分配一个pid,作为该producer的唯一标识。producer会为每一个<topic,partition>维护一个单调递增的seq。类似的,broker也会为每个<pid,topic,partition>记录下最新的seq。当req_seq == broker_seq+1时,broker才会接受该消息。因为:
1. 消息的seq比broker的seq大超过1时,说明中间有数据还没写入,即乱序了。
2. 消息的seq不比broker的seq小,那么说明该消息已被保存。
事务性/原子性广播
场景是这样的:
1. 先从多个源topic中获取数据。
2. 做业务处理,写到下游的多个目的topic。
3. 更新多个源topic的offset。
其中第2、3点作为一个事务,要么全成功,要么全失败。这里得益与offset实际上是用特殊的topic去保存,这两点都归一为写多个topic的事务性处理。
基本思路是这样的:
引入tid(transaction id),和pid不同,这个id是应用程序提供的,用于标识事务,和producer是谁并没关系。就是任何producer都可以使用这个tid去做事务,这样进行到一半就死掉的事务,可以由另一个producer去恢复。
同时为了记录事务的状态,类似对offset的处理,引入transaction coordinator用于记录transaction log。在集群中会有多个transaction coordinator,每个tid对应唯一一个transaction coordinator。
注:transaction log删除策略是compact,已完成的事务会标记成null,compact后不保留。
做事务时,先标记开启事务,写入数据,全部成功就在transaction log中记录为prepare commit状态,否则写入prepare abort的状态。之后再去给每个相关的partition写入一条marker(commit或者abort)消息,标记这个事务的message可以被读取或已经废弃。成功后在transaction log记录下commit/abort状态,至此事务结束。
1. 首先使用tid请求任意一个broker(代码中写的是负载最小的broker),找到对应的transaction coordinator。
2. 请求transaction coordinator获取到对应的pid,和pid对应的epoch,这个epoch用于防止僵死进程复活导致消息错乱,当消息的epoch比当前维护的epoch小时,拒绝掉。tid和pid有一一对应的关系,这样对于同一个tid会返回相同的pid。
4. client先请求transaction coordinator记录<topic,partition>的事务状态,初始状态是BEGIN,如果是该事务中第一个到达的<topic,partition>,同时会对事务进行计时;client输出数据到相关的partition中;client再请求transaction coordinator记录offset的<topic,partition>事务状态;client发送offset commit到对应offset partition。
5. client发送commit请求,transaction coordinator记录prepare commit/abort,然后发送marker给相关的partition。全部成功后,记录commit/abort的状态,最后这个记录不需要等待其他replica的ack,因为prepare不丢就能保证最终的正确性了。
这里prepare的状态主要是用于事务恢复,例如给相关的partition发送控制消息,没发完就宕机了,备机起来后,producer发送请求获取pid时,会把未完成的事务接着完成。
当partition中写入commit的marker后,相关的消息就可被读取。所以kafka事务在prepare commit到commit这个时间段内,消息是逐渐可见的,而不是同一时刻可见。
文件组织
kafka的数据,实际上是以文件的形式存储在文件系统的。topic下有partition,partition下有segment,segment是实际的一个个文件,topic和partition都是抽象概念。
在目录/${topicName}-{$partitionid}/下,存储着实际的log文件(即segment),还有对应的索引文件。
每个segment文件大小相等,文件名以这个segment中最小的offset命名,文件扩展名是.log;segment对应的索引的文件名字一样,扩展名是.index。有两个index文件,一个是offset index用于按offset去查message,一个是time index用于按照时间去查,其实这里可以优化合到一起,下面只说offset index。总体的组织是这样的:
为了减少索引文件的大小,降低空间使用,方便直接加载进内存中,这里的索引使用稀疏矩阵,不会每一个message都记录下具体位置,而是每隔一定的字节数,再建立一条索引。 索引包含两部分,分别是baseOffset,还有position。
baseOffset:意思是这条索引对应segment文件中的第几条message。这样做方便使用数值压缩算法来节省空间。例如kafka使用的是varint。
position:在segment中的绝对位置。
查找offset对应的记录时,会先用二分法,找出对应的offset在哪个segment中,然后使用索引,在定位出offset在segment中的大概位置,再遍历查找message。
常用配置项
broker配置
配置项 | 作用 |
broker.id | broker的唯一标识 |
auto.create.topics.auto | 设置成true,就是遇到没有的topic自动创建topic。 |
log.dirs | log的目录数,目录里面放partition,当生成新的partition时,会挑目录里partition数最少的目录放。 |
topic配置
配置项 | 作用 |
num.partitions | 新建一个topic,会有几个partition。 |
log.retention.ms | 对应的还有minutes,hours的单位。日志保留时间,因为删除是文件维度而不是消息维度,看的是日志文件的mtime。 |
log.retention.bytes | partion最大的容量,超过就清理老的。注意这个是partion维度,就是说如果你的topic有8个partition,配置1G,那么平均分配下,topic理论最大值8G。 |
log.segment.bytes | 一个segment的大小。超过了就滚动。 |
log.segment.ms | 一个segment的打开时间,超过了就滚动。 |
message.max.bytes | message最大多大 |
关于日志清理,默认当前正在写的日志,是怎么也不会清理掉的。
还有0.10之前的版本,时间看的是日志文件的mtime,但这个指是不准确的,有可能文件被touch一下,mtime就变了。因此在0.10版本开始,改为使用该文件最新一条消息的时间来判断。按大小清理这里也要注意,Kafka在定时任务中尝试比较当前日志量总大小是否超过阈值至少一个日志段的大小。如果超过但是没超过一个日志段,那么就不会删除。
5、Kafka和其他主流分布式消息系统的对比 (错误kafka由Scala编写)
kafka为什么快?
Kafka会把收到的消息都写入到硬盘中,它绝对不会丢失数据。为了优化写入速度Kafak采用了两个技术,顺序写入和MMFile
(1)顺序写入
因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最“讨厌”随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。
(2)Memory Mapped Files
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。Memory Mapped Files(后面简称mmap)也被翻译成内存映射文件,在64位操作系统中一般可以表示20G的数据文件,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
(3)消费者(读取数据)
Kafka使用磁盘文件还想快速?这是我看到Kafka之后的第一个疑问,ZeroMQ完全没有任何服务器节点,也不会使用硬盘,按照道理说它应该比Kafka快。可是实际测试下来它的速度还是被Kafka“吊打”。“一个用硬盘的比用内存的快”,这绝对违反常识;如果这种事情发生说明——它作弊了。没错,Kafka“作弊”。无论是顺序写入还是mmap其实都是作弊的准备工作
如何提高Web Server静态文件的速度
仔细想一下,一个Web Server传送一个静态文件,如何优化?答案是zero copy。传统模式下我们从硬盘读取一个文件是这样的,先复制到内核空间(read是系统调用,放到了DMA,所以用内核空间),然后复制到用户空间;从用户空间重新复制到内核空间(你用的socket是系统调用,所以它也有自己的内核空间),最后发送给网卡。
Zero Copy中直接从内核空间(DMA的)到内核空间(Socket的),然后发送网卡。这个技术非常普遍,The C10K problem 里面也有很详细的介绍,Nginx也是用的这种技术。
Kafka把所有的消息都存放在一个一个的文件中,当消费者需要数据的时候Kafka直接把“文件”发送给消费者。这就是秘诀所在,比如:10W的消息组合在一起是10MB的数据量,然后Kafka用类似于发文件的方式直接扔出去了,如果消费者和生产者之间的网络非常好(只要网络稍微正常一点10MB根本不是事。。。家里上网都是100Mbps的带宽了),10MB可能只需要1s。所以答案是——10W的TPS,Kafka每秒钟处理了10W条消息。
可能你说:不可能把整个文件发出去吧?里面还有一些不需要的消息呢?是的,Kafka作为一个“高级作弊分子”自然要把作弊做的有逼格。Zero Copy对应的是sendfile这个函数(以Linux为例),这个函数接受
out_fd作为输出(一般及时socket的句柄)
in_fd作为输入文件句柄
off_t表示in_fd的偏移(从哪里开始读取)
size_t表示读取多少个
没错,Kafka是用mmap作为文件读写方式的,它就是一个文件句柄,所以直接把它传给sendfile;偏移也好解决,用户会自己保持这个offset,每次请求都会发送这个offset。(还记得吗?放在zookeeper中的);数据量更容易解决了,如果消费者想要更快,就全部扔给消费者。如果这样做一般情况下消费者肯定直接就被压死了;所以Kafka提供了的两种方式——Push,我全部扔给你了,你死了不管我的事情;Pull,好吧你告诉我你需要多少个,我给你多少个。
总结
Kafka速度的秘诀在于,它把所有的消息都变成一个的文件。通过mmap提高I/O速度,写入数据的时候它是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。阿里的RocketMQ也是这种模式,只不过是用Java写的。
快来成为我的朋友或合作伙伴,一起交流,一起进步!
QQ群:961179337
微信:lixiang6153
邮箱:lixx2048@163.com
公众号:IT技术快餐
更多资料等你来拿!