一. 数据持久化

直接操作文件系统
操作系统对所有的磁盘操作,都经过缓存,由内存开辟空间;
进程也会缓存一份数据;
这等于进行了两次缓存;
jvm问题:

  • Java对象占用空间是非常大的,差不多是要存储的数据的两倍甚至更高
  • 随着堆中数据量的增加,垃圾回收回变的越来越困难

kafka为了避免两倍的jvm堆内存,以及困难的垃圾回收;
把内存作为一个很大的缓存,在开机时10G内存大约消耗10分钟加载;
使用文件系统,重启了也不需要刷新数据,简化了数据一致性逻辑;

对比:
传统方式:将数据缓存在内存中然后刷到硬盘
kafka:直接将数据写到了文件系统的日志中

常量时间的操作效率
操作硬盘传统的方式,提供B树,时间复杂度O(log N);
两个问题:1、磁盘一次搜索10ms,同一时间只能进行一次搜索,并发上有问题;2、树结构随着数据增长,性能会线性下降;
日志处理的消息系统,数据的持久化可以简单的通过将数据追加到文件中实现,这样做的好处是读和写都是 O(1) 的,并且读操作不会阻塞写操作和其他操作;
磁盘读写问题: 带来的特性:Kafka却可以将消息保存一段时间,不是消费后立即删除,带来了灵活性;

二. 消息传输事务

三种级别:

  1. 最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输。
  2. 最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
    3。 精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的。
    这个级别指的是consumer对于消息的处理

当发布消息时,Kafka有一个“committed”的概念,一旦消息被提交了,只要消息被写入的分区的所在的副本broker是活动的,数据就不会丢失,暂时假设broker不会down;
如果producer发布消息时发生了网络错误,但又不确定是在提交之前发生的还是提交之后发生的,这种情况虽然不常见,但是必须考虑进去,暂时并没有解决;
Kafka允许producer灵活的指定级别。比如producer可以指定必须等待消息被提交的通知,或者完全的异步发送消息而不等待任何通知,或者仅仅等待leader声明它拿到了消息;
所有的副本都有相同的日志文件和相同的offset,consumer维护自己消费的消息的offset,如果consumer不会崩溃当然可以在内存中保存这个值,当然谁也不能保证这点。如果consumer崩溃了,会有另外一个consumer接着消费消息,它需要从一个合适的offset继续处理。这种情况下可以有以下选择:

  • consumer可以先读取消息,然后将offset写入日志文件中,然后再处理消息。这存在一种可能就是在存储offset后还没处理消息就crash了,新的consumer继续从这个offset处理,那么就会有些消息永远不会被处理,这就是上面说的“最多一次”。
  • consumer可以先读取消息,处理消息,最后记录offset,当然如果在记录offset之前就crash了,新的consumer会重复的消费一些消息,这就是上面说的“最少一次”。
  • “精确一次”可以通过将提交分为两个阶段来解决:保存了offset后提交一次,消息处理成功之后再提交一次。但是还有个更简单的做法:将消息的offset和消息被处理后的结果保存在一起。比如用Hadoop ETL处理消息时,将处理后的结果和offset同时保存在HDFS中,这样就能保证消息和offser同时被处理了。

三. 性能优化

Kafka的一个主要使用场景是处理网站活动日志,吞吐量是非常大的;
写方面:主要使用场景是处理网站活动日志,每个页面都会产生好多次写操作;
读方面:假设每个消息只被消费一次,读的量的也是很大的;
线性读写的情况下影响磁盘性能问题大约有两个方面:
太多的琐碎的I/O操作和太多的字节拷贝
I/O问题发生在客户端和服务端之间,也发生在服务端内部的持久化的操作中

消息集
将消息组织到一起,作为处理的单位
Producer把消息集一块发送给服务端,而不是一条条的发送
服务端把消息集一次性的追加到日志文件中,这样减少了琐碎的I/O操作
consumer也可以一次性的请求一个消息集

二进制
另外一个性能优化是在字节拷贝方面。在低负载的情况下这不是问题,但是在高负载的情况下它的影响还是很大的。为了避免这个问题,Kafka使用了标准的二进制消息格式,这个格式可以在producer,broker和producer之间共享而无需做任何改动。

zero copy
标准二进制格式,在producer、consumer、broker之间共享
可以从一个很重要的点进行优化:消息在网络上的传递
将数据从页面缓存发送到socket的系统函数,比如linux系统的sendfile
一般数据文件发送到socket的过程
一般将数据从文件发送到socket的数据流向:

  • 操作系统把数据从文件拷贝内核中的页缓存中
  • 应用程序从页缓存从把数据拷贝自己的内存缓存中
  • 应用程序将数据写入到内核中socket缓存中
  • 操作系统把数据从socket缓存中拷贝到网卡接口缓存,从这里发送到网络上

有4次拷贝和2次系统调用,使得这过程很低效;
Sendfile通过直接将数据从页面缓存发送网卡接口缓存,避免了重复拷贝,大大的优化了性能;
在一个多consumers的场景里,数据仅仅被拷贝到页面缓存一次而不是每次消费消息的时候都重复的进行拷贝;
这使得消息以近乎网络带宽的速率发送出去。这样在磁盘层面你几乎看不到任何的读操作,因为数据都是从页面缓存中直接发送到网络上去了;
这里说明zero-copy技术:
https://www.ibm.com/developerworks/linux/library/j-zerocopy/

数据压缩
瓶颈一般在磁盘和带宽
Kafka采用了端到端的压缩,即在producer端以数据集的方式压缩,在broker中追加,发送到consumer端再解压,叫做“端到端的压缩”。
Kafka支持GZIP和Snappy压缩协议
https://cwiki.apache.org/confluence/display/KAFKA/Compression

四. Producer和Consumer

Producer
producer把数据发送给leader节点,不需要在多个节点进行分发;
所有的Kafka节点都可以及时的告知:哪些节点是活动的,目标topic目标分区的leader在哪。
这样producer就可以直接将消息发送到目的地了。

客户端控制消息将被分发到哪个分区,可以通过负载均衡随机的选择,或者使用分区函数;Kafka允许用户实现分区函数,指定分区的key,将消息hash到不同的分区上(当然有需要的话,也可以覆盖这个分区函数自己实现逻辑).比如如果你指定的key是user id,那么同一个用户发送的消息都被发送到同一个分区上。经过分区之后,consumer就可以有目的的消费某个分区的消息。

异步发送
Kafka producer的异步发送模式允许进行批量发送,先将消息缓存在内存中,然后一次请求批量发送出去;
这个策略可以配置的,比如可以指定缓存的消息达到某个量的时候就发出去,或者缓存了固定的时间后就发送出去,这样可以减少io量;
既然缓存是在producer端进行的,那么当producer崩溃时,这些消息就会丢失;
0.8.1不支持回调,在0.9之后支持回调处理,参考:https://cwiki.apache.org/confluence/display/KAFKA/Client+Rewrite#ClientRewrite-ProposedProducerAPI

Consumer
Kafa consumer消费消息时,向broker发出"fetch"请求去消费特定分区的消息;
consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息;
customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的;

推还是拉
Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息
一些消息系统使用push,比如:Scribe、Flume
好处:消息系统都致力于让consumer以最大的速率最快速的消费消息
坏处:当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了

pull模式好处:
consumer可以自主决定是否批量的从broker拉取数据;
Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略。

pull模式缺点:
broker没有可供消费的消息,将导致consumer不断在循环中轮询;为了避免这点,
Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发送)。

消费状态跟踪
大部分消息系统,都在broker端维护消费记录:
一个消息被分发到consumer后broker就马上进行标记或者等待customer的通知后进行标记。这样也可以在消息在消费后立马就删除以减少空间占用。
问题:

1.如果一条消息发送出去之后就立即被标记为消费过的,一旦consumer处理消息时失败了(比如程序崩溃)消息就丢失了。
解决方式:当消息被发送出去之后仅仅被标记为已发送状态,当接到consumer已经消费成功的通知后才标记为已被消费的状态。
2.产生两个新问题:
首先如果consumer处理消息成功了但是向broker发送响应时失败了,这条消息将被消费两次。
第二个问题时,broker必须维护每条消息的状态,并且每次都要先锁住消息然后更改状态然后释放锁。
进而:维护大量数据,而且如果消息发送出去但没有收到消费成功的通知,这条消息将一直处于被锁定的状态

Kafka的策略
Topic被分成了若干分区,每个分区在同一时间只被一个consumer消费;
因此每个分区被消费的消息在日志中的位置仅仅是一个简单的整数:offset;
消费状态在分区中,只是一个整数,容易跟踪;
好处:consumer可以把offset调成一个较老的值,去重新消费老的消息

离线处理消息
数据持久化允许consumer每个隔一段时间批量的将数据加载到线下系统中比如Hadoop或者数据仓库;
Hadoop可以将加载任务分拆,拆成每个broker或每个topic或每个分区一个加载任务。Hadoop具有任务管理功能,当一个任务失败了就可以重启而不用担心数据被重新加载,只要从上次加载的位置继续加载消息就可以了。

五. 主从同步

Kafka允许topic的分区拥有若干副本,这个数量是可以配置的,你可以为每个topci配置副本的数量。Kafka会自动在每个个副本上备份数据,所以当一个节点down掉时数据依然是可用的。
Kafka的副本功能不是必须的,你可以配置只有一个副本,这样其实就相当于只有一份数据。
创建副本的单位是topic的分区,每个分区都有一个leader和零或多个followers.
所有的读写操作都由leader处理,一般分区的数量都比broker的数量多的多,各分区的leader均匀的分布在brokers中,最好是broker的整数倍;
所有的followers都复制leader的日志,日志中的消息和顺序都和leader中的一致。
flowers向普通的consumer那样从leader那里拉取消息并保存在自己的日志文件中。

判断节点是否alive:

  • 节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接。
  • 如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久。

这两个条件是in sync 超时参数由replica.lag.max.messages决定
卡住了由replica.lag.time.max.ms决定
消息被所有的副本加入日志才算:committed,只有committed的消息才会发送给consumer,因此这样不必担心leader down掉数据会丢失,Producer也可以选择是否等待消息被提交的通知,由参数request.required.acks决定的;
Kafka保证只要有一个“同步中”的节点,“committed”的消息就不会丢失。

Leader的选择
Kafka的核心是日志文件
如果leader down掉,必须需选择高质量的follower
需要保证:一旦一个消息被提交了,但是leader down掉了,新选出的leader必须可以提供这条消息

大部分的分布式系统采用了多数投票法则选择新的leader,动态选择,但是kafka不是这样;

Kafaka动态维护了一个同步状态的副本的集合(a set of in-sync replicas),简称ISR;
这个集合中的节点,和leader保持高度一致,任何一条消息必须被这个集合中的每个节点读取并追加到日志中了,才回通知外部这个消息已经被提交了。因此这个集合中的任何一个节点随时都可以被选为leader.ISR在ZooKeeper中维护。ISR中有f+1个节点,就可以允许在f个节点down掉的情况下不会丢失消息并正常提供服。ISR的成员是动态的,如果一个节点被淘汰了,当它重新达到“同步中”的状态时,他可以重新加入ISR.这种leader的选择方式是非常快速的,适合kafka的应用场景。

当所有的副本都down掉选择

  • 等待ISR中的任何一个节点恢复并担任leader。
  • 选择所有节点中(不只是ISR)第一个恢复的节点作为leader

这是一个在可用性和连续性之间的权衡。如果等待ISR中的节点恢复,一旦ISR中的节点起不起来或者数据都是了,那集群就永远恢复不了了。如果等待ISR意外的节点恢复,这个节点的数据就会被作为线上数据,有可能和真实的数据有所出入,因为有些数据它可能还没同步到。Kafka目前选择了第二种策略,在未来的版本中将使这个策略的选择可配置,可以根据场景灵活的选择。
这种窘境不只Kafka会遇到,几乎所有的分布式数据系统都会遇到。

副本管理
以上仅仅以一个topic一个分区为例子进行了讨论,但实际上一个Kafka将会管理成千上万的topic分区.Kafka尽量的使所有分区均匀的分布到集群所有的节点上而不是集中在某些节点上,另外主从关系也尽量均衡这样每个几点都会担任一定比例的分区的leader.
优化leader的选择过程也是很重要的,它决定了系统发生故障时的空窗期有多久。Kafka选择一个节点作为“controller”,当发现有节点down掉的时候它负责在游泳分区的所有节点中选择新的leader,这使得Kafka可以批量的高效的管理所有分区节点的主从关系。如果controller down掉了,活着的节点中的一个会备切换为新的controller.