Kafka源码解析(1)--概述

一. 介绍

1.1 什么是消息队列

首先我们先说说什么是消息队列?下面是维基百科的解释:

队列提供了一种异步通信协议,这意味着消息的发送者和接收者不需要同时与消息保持联系,发送者发送的消息会存储在队列中,直到接收者拿到它。

消息:指的是通信的基本单位,也可以是进程间通信的具体内容。
生产者和消费者:在消息传递过程中, 产生和发布消息的一端称为生产者,接收和消费消息的一端称为消费者。
我们注意到概念中的“异步”二字,因为生产者和消费者往往在处理速度上不对等,如果生产者与消费者始终保持同步,必然存在有一方存在空等情况,浪费时间。因此消息队列让生产者将消息放入队列中,不用等待消费者,而继续执行自己的工作,消费者则直接去队列中取走。如果消费者效率低于消费者,消费者也不需要一直监听等待,可以做自己的事儿,只要有消息到了就直接通知消费者。

根据消费队列实现方式,可以分为两种:

  1. 队列模式:在队列中,一群Consumer从一个Server读取数据,每条消息被其中一个Consumer读取。 队列模式的优点是你可以在多个消费者实例上分配数据处理,从而允许你对程序进行“伸缩”,各进程可以并行消费多个消息,而不是多个消费者都在消费同一个消息。缺点是其容错性比较差,如果这个消费者宕机了,这个消费者正在消费的消息就不能被其他消费者消费了,造成消息丢失。

  2. 发布-订阅模式。 在该模式中,一旦有新的消息发布就会广播通知所有的消费者。解决了容错性问题,但是因为所有消费者都在处理同一个消息导致“伸缩”能力下降,影响性能。

本文我们会介绍Kafka是如何实现的,如何解决“伸缩性”和“容错性”的问题。

1.2 为什么要用消息队列

(1)解耦

消息队列使得消费者与生产者之间的通信机制由同步改成异步,不用相互等待,也不需要相互交互,通过队列将两者解耦开。

(2)冗余,避免数据丢失

有时候生产者传输的数据没有被消费者正确处理而导致丢失,当消费者重启以后无法再处理这条未处理完成的数据。提供消息队列可以存储未处理的消息,避免数据丢失风险。在被许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理过程明确的指出该消息已经被处理完毕,确保你的数据被安全的保存直到你使用完毕, 保证了消息能被实际的处理.

(3)提高处理性能

通过消息队列解耦生产者和消费者,使得两者无需相互等待,提高各自的处理效率和消息传递频率。

(4)缓冲能力–提升系统稳定性

在访问量剧增的情况下,上游将数据写入消息队列中,下游无法即使处理大批量的访问数据,因为队列的存在帮下游顶住了突发的访问压力,下游不会因为因为突发的超负荷的请求而完全崩溃。下游可以按正常处理速度从kakfa中读取数据进行消费。也不会造成大量的请求丢失,这些请求数据存在队列中,只是处理延迟。系统限流、降低等方案可以考虑加入消息队列。

另外,在任何重要的系统中,都会有需要不同的处理时间的元素。消息队列通过一个缓冲层来帮助任务最高效率的执行–写入队列的处理会尽可能的快速,而不受从队列读的预备处理的约束。该缓冲有助于控制和优化数据流经过系统的速度

(5)可恢复性

消息队列降低了进程间的耦合度,所以即使消费者挂掉,也不会影响到整个系统,加入队列中的消息仍然可以在消费者系统恢复后被处理。而这种允许重试或者延后处理请求的能力通常是造就一个略感不便的用户和一个沮丧透顶的用户之间的区别。

(6)顺序保证

消息队列是按顺序传递消息,能够保证消费者获得的数据是有序的。

(7)异步通信

队列提供了一种异步通信协议,这意味着消息的发送者和接收者不需要同时与消息保持联系,发送者发送的消息会存储在队列中,直到接收者拿到它。

1.3 Kafka

Kafka是一个消息队列(MQ), 同时也是一个分布式的流框架,最初设计用于应对LinkedIn庞大的活动流数据(登录、浏览、点击、分享、喜欢等)和系统处理数据(CPU、负载、用户请求数等)。其具备分布式、高吞吐量、容错性、多副本、流式数据处理、多订阅者的特性。Linkedin于2010年12月份开源的消息系统,目前被大量用作消息队列、流式处理(一般结合storm)、日志聚合。

相关术语

  • Broker:消息中间件处理结点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。
  • Topic:一类消息,是消息的抽象分类概念。每个消息在发送的时候都会指定topic.
  • Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。
  • Segment:partition物理上由多个segment组成。
  • offset:每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息。
  • Producer:负责发布消息到Kafka broker。
  • Consumer:消息消费者,向Kafka broker读取消息的客户端。
  • Consumer Group:每个Consumer属于一个特定的Consumer Group。

Topic和Partition

Topic(主题)可以看做是消息的一个抽象分类概念,每一类消息称为Topic(主题)。Kafka中的Topics一般是多订阅者(消费者)的,也就是一个Topic可以有0个或多个Consumer订阅它的数据。Topic又可以分为多个partition(区),将该Topic中的消息均匀分配到多个区中。任何一个partition有且只能被一个消费者消费。partition的设计提供了分布式的基础。

每个分区是一个有序的,以不可变的记录顺序追加的Commit Log。分区中的每个记录都有一个连续的ID,称为Offset,唯一标识分区内的记录。同一个分区内部保证有序,但分区之间不保证数据有序

分区日志有几个目的:

  • 第一,使服务器能承载日志的大小,每个分区的日志必须可以被保存在单个服务器上,但是一个Topic可以拥有多个分区,那么它可以处理任意大小的数据量。
  • 第二,它们作为并行度的单位(更多的是这点的考虑)
  • 提高并行处理能力,生产者可以同时向多个partition写入数据,消费者也可以同时从多个partition消费数据。
  • 容错性,这样可以将一个topic分配到不同的机器上,在使用副本机制时更加容易。

Producer

发布消息的对象称之为生产者(Kafka topic producer),生产者往某个Topic上发布消息。生产者也负责选择发布到Topic上的哪一个分区:

  • 最简单的方式是采用轮询算法(round-robin)来实现负载均衡,即从分区列表中轮流选择一个Partition(分区)发送。也可以根据某种算法依照权重选择分区。这部分由开发者负责如何选择分区的算法。
  • 如果用户指定了消息key, 那么发消息的时候先根据key计算hash值,然后再将hash值与partition数据量取余得到对应的partition.

Consumer

订阅消息并处理发布的消息的对象称之为消费者(consumers)。Kafka还有一个消费者组的概念,Consumer使用一个group name来标识自己属于哪个组。消费者组有以下特点:

  • 如果所有的Consumer实例都是属于一个group的,那么所有的消息将被均衡的分发给每个实例。那么一条消息只能被同一个组里的一个消费者Consumer消费。
  • 一条消息会发送个所有的消费组

如上图存在Group A和Group B. 消息P0、P1、P2、P3都会发布给所有组,即都会发送给Group A, Group B. 在同一个组里均衡分担消息,只能被一个消费者消费。那么P1、P2被C2消费,P0、P3被C1消费。

Kafka保证稳定状态下每一个consumer实例只会消费某一个或多个特定partition的数据,而某个partition的数据只会被某一个特定的consumer实例所消费。Kafka会随着consumer group中consumer数量的变化,而动态调整partion的分配,使得partion尽可能均匀地分配给consumer,但这过程始终保持该特性。这种设计方式的劣势是无法让同一个consumer group里的consumer均匀消费数据:

  • 存在一种特殊情况,信息的key都一致,那么这些消息都会打在同一个partition上,供一个conscumer消费。
  • 当同一个consumer group里的consumer数量小于partition数量,那么导致在partition分配上不平衡,也导致消息分配不平衡。
  • 如果consumer group里的consumer数量大于partition数量,那么必然有一个consumer没有分配到partition,没有数据可以消费。所以部署的时候最好的选择是consumer小于partition,避免机器浪费。

    这样的设计也带来一些优势:

  1. 消息分配简单,直接交付给partition对应的消费者
  2. partition不需要与多个消费者建立连接与通信,减少了通信的开销
  3. 因为一个partition只有一个消费者消费,也保证了partition上的消息能够被消费者顺序消费

上文提到了消息队列两种实现方式:队列和发布-订阅方式的优缺点,Kafka利用了消费组的概念很好结合了两者的优点。同时具有“伸缩性”和“容错性”。

消费者会保存一个元数据是消费者的消费进度,即消费日志的偏移量(Offset)。 用来记录消费者订阅的Partion已经消费情况,通过这个方法便于消费者自己去控制消息的读取进度,保证消息可以被消费者消费。如果消费者A在消费过程中因特殊原因宕机了,这是读取到了offset=9,但是生产者还在不断的想Kafka写数据。通过消费者保存的offset很快能将宕机后未读取的消息都读取完成。

消费者提交offest以后会保存到该消费组的元数据中,目前版本这些数据是保持在broker中,即协调者之中。协调者根据消费者宕机情况重新分配partition, 分配到该partition的消费者则会从这个offest位置开始消费。

Broker

已发布的消息保存在一组服务器中,称之为Kafka集群。集群中的每一个服务器都是一个代理(Broker). 消费者可以订阅一个或多个主题(topic),并从Broker拉数据,从而消费这些已发布的消息。

四个核心API

Kafka有4个核心API:

  • Producer API:用于应用程序将数据流发送到一个或多个Kafka topics
  • Consumer API:用于应用程序订阅一个或多个topics并处理被发送到这些topics中的数据
  • Streams API:允许应用程序作为流处理器,处理来自一个或多个topics的数据并将处理结果发送到一个或多个topics中,有效的将输入流转化为输出流
  • Connector API:用于构建和运行将Kafka topics和现有应用或数据系统连接的可重用的produers和consumers。例如,如链接到关系数据库的连接器可能会捕获某个表所有的变更

二. 整体设计思路

Kafka最初被LinkedIn设计来处理活动流数据(activity stream data)和系统处理数据(operaitonal data)。这两种数据都属于日志数据的范畴,虽然现有的消息队列系统非常适合这种实时性要求高的场景,但是由于它们都是在内存中维护消息队列,所以处理数据的大小就受到了限制。因此Kafka的出现是为了解决以上两个问题,将offline的大数据分析和online的实时数据分析都可以通过该系统实现。

根据该设计目的我们可以看到Kakfa具备了流式处理能力(适应实时和离线数据分析)和存储能力(将消息存储到磁盘上)。

2.1 Kafka架构

一个典型的kafka集群中包含若干producer、若干broker、若干consumer group,以及一个Zookeeper集群。四个部分主要协作关系如下:

  • Producer集群通过zookeeper(实际中写的是broker list)获取所写topic对应的partition列表,然后顺序发送消息(支持自己实现分发策略)。
  • broker集群负责消息的存储和传递,支持Master Slaver模型,可分布式扩展一般broker数量越多,集群吞吐率越高;
  • Consumer集群从zookeeper上获取topic所在的partition列表,然后消费,一个partition只能被一个consumer消费。

2.2 Push vs. Pull

Kafka遵循了传统的方式,选择由producer向broker push消息并由consumer从broker pull消息。

一个消息如何算投递(push)成功,Kafka提供了三种模式:

  • 第一种发送出去就当作成功,ask=0,这种情况当然不能保证消息成功投递到broker;
  • 第二种是对于Master Slave模型,ask=all, 只有当Master和所有Slave都接收到消息时,才算投递成功,这种模型提供了最高的投递可靠性,但是损伤了性能;
  • 第三种模型,ask=1, 即只要Master确认收到消息就算投递成功;实际使用时,根据应用特性选择,绝大多数情况下都会中和可靠性和性能选择第三种模型。

对于消息的消费,ActiveMQ使用PUSH模型,而Kafka使用PULL模型,两者各有利弊:

  • 对于PUSH,broker很难控制数据发送给不同消费者的速度. 同时需要在服务端保存各个消费者进度,增加服务端负载
  • 而PULL可以由消费者自己控制,但是PULL模型可能造成消费者在没有消息的情况下盲等,这种情况下可以通过long polling机制缓解,而对于几乎每时每刻都有消息传递的流式系统,这种影响可以忽略。

2.3 Topic & Partition

Topic在逻辑上可以被认为是一个queue。每条消费都必须指定它的topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以水平扩展,物理上把topic分成一个或多个partition,每个partition在物理上对应一个文件夹,该文件夹下存储这个partition的所有消息和索引文件。
  
每条消息都被append到该partition中,是顺序写磁盘,因此效率非常高(顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。 

2.4 副本和选举机制

replication对Kafka的吞吐率是有一定影响的,但极大的增强了可用性。默认情况下,Kafka的replication数量为1。

每个partition都有一个唯一的leader,所有的读写操作都在leader上完成,leader批量从leader上pull数据。一般情况下partition的数量大于等于broker的数量,并且所有partition的leader均匀分布在broker上。follower上的日志和其leader上的完全一样。

  

三. 与其他消息队列对比

3.1 RabbitMQ

待补充

性能比较

待补充

参考

0%