Kafka学习总结(三)——kafka通信协议

此博客总结学习以及借鉴其他博文;参考资料:译文:点击打开链接
                                                                      资料:API方式调用Kafka各种协议的方法点击打开链接
                                                                                         

概述


Kafka的Producer、Broker和Consumer之间采用的是一套自行设计的基于TCP层的协议。Kafka的这套协议完全是为了Kafka自身的业务需求而定制的,而非要实现一套类似于Protocol Buffer的通用协议

卡夫卡协议是相当简单的,只有六个核心的客户端请求的API

  1. 元数据(Metadata) – 描述可用的brokers,包括他们的主机和端口信息,并给出了每个broker上分别存有哪些分区;
  2. 发送(Send) – 发送消息到broker;
  3. 获取(Fetch) – 从broker获取消息,其中,一个获取数据,一个获取集群的元数据,还有一个获取topic的偏移量信息;
  4. 偏移量(Offsets) – 获取给定topic的分区的可用偏移量信息;
  5. 偏移量提交(Offset Commit) – 提交消费者组(Comsumer Group)的一组偏移量;
  6. 偏移量获取(Offset Fetch) – 获取一个消费者组的一组偏移量;

上述的API都将在下面详细说明。此外,从0.9版本开始,Kafka支持为消费者和Kafka连接进行分组管理。客户端API包括五个请求:

  1. 分组协调者(GroupCoordinator) – 用来定位一个分组当前的协调者。
  2. 加入分组(JoinGroup) – 成为某一个分组的一个成员,当分组不存在(没有一个成员时)创建分组。
  3. 同步分组(SyncGroup) – 同步分组中所有成员的状态(例如分发分区分配信息(Partition Assignments)到各个组员)。
  4. 心跳(Heartbeat) – 保持组内成员的活跃状态。
  5. 离开分组(LeaveGroup) – 直接离开一个组。

最后,有几个管理API,可用于监控/管理的卡夫卡集群(KIP-4完成时,这个列表将增长):

  1. 描述消费者组(DescribeGroups) – 用于检查一组群体的当前状态(如:查看消费者分区分配)。 
    列出组(ListGroups) – 列出某一个broker当前管理的所有组
  2. 基本数据类型

    1. 定长数据类型:int8,int16,int32和int64,对应到Java中就是byte, short, int和long。
    2. 变长数据类型:bytes和string。变长的数据类型由两部分组成,分别是一个有符号整数N(表示内容的长度)和N个字节的内容。其中,N为-1表示内容为null。bytes的长度由int32表示,string的长度由int16表示。
    3. 数组:数组由两部分组成,分别是一个由int32类型的数字表示的数组长度N和N个元素

    Request和Response的基本结构

    Kafka中两个角色之间通讯的基本单位是Request/Response,Request和Response的基本结构如下:

    RequestOrResponse => MessageSize (RequestMessage | ResponseMessage)
    

    其中各字段的含义为:

    名称 类型 描述
    MessageSize int32 表示RequestMessage或者ResponseMessage的长度
    RequestMessage/ResponseMessage - 表示Request或者Response的内容,在下面将会介绍其具体格式。

    这个结构定义了通讯双方交换数据的基本结构。通讯的过程可以简单地表示为:客户端打开与服务器端的Socket,然后往Socket写入一个int32的数字表示这次发送的Request有多少字节,然后继续往Socket中写入对应字节数的数据。服务器端先读出一个int32的整数从而获取这次Request的大小,然后读取对应字节数的数据从而得到Request的具体内容。服务器端处理了请求后,也用同样的方式来发送响应。

    RequestMessage的结构

    RequestMessage的结构如下:

    RequestMessage => ApiKey ApiVersion CorrelationId ClientId Request
    
    名称 类型 描述
    ApiKey int16 表示这次请求的API编号
    ApiVersion int16 表示请求的API的版本,有了版本后就可以做到后向兼容
    CorrelationId int32 由客户端指定的一个数字唯一标示这次请求的id,服务器端在处理完请求后也会把同样的CorrelationId写到Response中,这样客户端就能把某个请求和响应对应起来了。
    ClientId string 客户端指定的用来描述客户端的字符串,会被用来记录日志和监控,它唯一标示一个客户端。
    Request - Request的具体内容。

    ResponseMessage的结构

    ResponseMessage的结构如下:

    ResponseMessage => CorrelationId Response
    
    名称 类型 描述
    CorrelationId int32 对应Request的CorrelationId。
    Response - 对应Request的Response,不同的Request的Response的字段是不一样的。

    Message

    Kafka是一个分布式消息系统,Producer生产消息并推送(Push)给Broker,然后Consumer再从Broker那里取走(Pull)消息。Producer生产的消息就是由Message来表示的,对用户来讲,它就是键-值对,来看看它的结构。

    Message => Crc MagicByte Attributes Key Value
    
    名称 类型 描述
    CRC int32 表示这条消息(不包括CRC字段本身)的校验码
    MagicByte int8 表示消息格式的版本,用来做后向兼容,目前值为0
    Attributes int8 表示这条消息的元数据,目前最低两位用来表示压缩格式
    Key bytes 表示这条消息的Key,可以为null
    Value bytes 表示这条消息的Value。Kafka支持消息嵌套,也就是把一条消息作为Value放到另外一条消息里面。

    MessageSet

    MessageSet用来组合多条Message,它在每条Message的基础上加上了Offset和MessageSize,其结构是:

    MessageSet => [Offset MessageSize Message]
    

    它的含义是MessageSet是个数组,数组的每个元素由三部分组成,分别是Offset,MessageSize和Message,它们的含义分别是:

    名称 类型 描述
    Offset int64 它用来作为log中的序列号,Producer在生产消息的时候还不知道具体的值是什么,可以随便填个数字进去
    MessageSize int32 表示这条Message的大小
    Message - 表示这条Message的具体内容,其格式见上一小节。

    Message的压缩

    Kafka支持下面几种压缩方式,

    压缩方式 编码
    不压缩 0
    Gzip 1
    Snappy 2
    LZ4 3

    其中编码就是Message的Attribute的最低两位的值。

    因为单条消息中重复内容可能不多,所以通常把多条消息放在一起组成MessageSet,然后再把MessageSet放到一条Message里面去,从而提高压缩比率。

    Request/Respone和Message/MessageSet的关系

    • Request/Response是通讯层的结构,和网络的7层模型对比的话,它类似于TCP层。
    • Message/MessageSet定义的是业务层的结构,类似于网络7层模型中的HTTP层。Message/MessageSet只是Request/Response的payload中的一种数据结构。

  3. 网络

    Kafka使用基于TCP的二进制协议。该协议定义了所有API的请求及响应消息。所有消息都是有长度限制的,并且由后面描述的基本类型组成。

    客户端启动的socket连接,并且写入请求的消息序列和读回相应的响应消息。连接和断开时均不需要握手消息。如果保持你保持长连接,那么TCP协议本身将会节省很多TCP握手时间,但如果真的重新建立连接,那么代价也相当小。

    客户可能需要维持到多个broker的连接,因为数据是被分区的,而客户端需要和存储这些分区的broker服务器进行通讯。当然,一般而言,不需要为单个服务端和单个客户端间维护多个连接(即连接池技术)。

    服务器的保证单一的TCP连接中,请求将被顺序处理,响应也将按该顺序返回。为保证broker的处理请求的顺序,单个连接同时也只会处理一个请求指令。请注意,客户端可以(也应该)使用非阻塞IO实现请求流水线,从而实现更高的吞吐量。也就是说,客户可以在等待上次请求应答的同时发送下个请求,因为待完成的请求将会在底层操作系统套接字缓冲区进行缓冲。除非特别说明,所有的请求是由客户端启动,并从服务器获取到相应的响应消息。

    服务器能够配置请求大小的最大限制,超过这个限制将导致socket连接被断开。

    分区和引导(Partitioning and bootstrapping)

    Kafka是一个分区系统,所以不是所有的服务器都具有完整的数据集。主题(Topic)被分为P(预先定义的分区数量)个分区,每个分区被复制N(复制因子)份,Topic Partition根据顺序在“提交日志”中编号为0,1,…,P。

    所有具有这种特性的系统都有一个如何制定某个特定数据应该被分配给哪个特定的分区的问题。Kafka中它由客户端直接控制分配策略,broker则没有特别的语义来决定消息发布到哪个分区。相反,生产者直接将消息发送到一个特定的分区,提取消息时,消费者也直接从某个特定的分区获取。如果两个生产者要使用相同的分区方案,那么他们必须用同样的方法来计算Key到分区映射关系。

    这些发布或获取数据的请求必须发送到指定分区中作为leader的broker。此条件同时也会由broker保证,发送到不正确的broker的请求将会返回NotLeaderForPartition错误代码(后文所描述的)。

    那么客户端如何找出哪些主题存在,他们有什么分区,以及这些分区被哪些broker存取,以便它可以直接将请求发送到所在的主机?这个信息是动态的,因此你不能只是提供每个客户端一些静态映射文件。所有的Kafka broker都可以回答这个描述集群的当前状态的数据请求:有哪些主题,这些主题都有多少分区,哪个broker是这些分区的Leader,以及这些broker主机的地址和端口信息。

    换句话说,客户端只需要找到一个broker,broker将会告知客户端所有其他存在的broker,以及这些broker上面的所有分区。这个broker本身也可能会掉线,因此客户端实现的最佳做法是保存两个或三个broker地址,从而来引导列表。用户可以选择使用负载均衡器或只是静态地配置两个或三个客户的Kafka主机。

    客户并不需要轮询地查看集群是否已经改变;它可以等到它接收到所用的元数据是过时的错误信息时一次性更新元数据。这中错误有两种形式:(1)一个套接字错误指示客户端不能与特定的broker进行通信,(2)请求响应表明该broker不再是其请求数据分区的Leader的错误。

    1. 轮询“起始”Kafka的URL列表,直到我们找到一个我们可以连接到的broker。获取集群元数据。
    2. 处理获取数据或者存储消息请求,根据这些请求所发送的主题和分区,将这些请求发送到合适的broker。
    3. 如果我们得到一个适当的错误(显示元数据已经过时时),刷新元数据,然后再试一次。

    分区策略(Partitioning Strategies)

    上面提到消息的分区分配是由生产者客户端控制,那么,为什么要把这个功能被暴露给最终用户?

    在Kafka中,这样分区有两个目的:

    1. 它平衡了broker的数据和请求负载
    2. 它允许多个消费者之间处理分发消息的同时,能够维护本地状态,并且在分区中维持消息的顺序。我们称这种语义的分区(semantic partitioning)。

    对于给定的使用场景下,你可能只关心其中的一个或两个。

    为了实现简单的负载均衡,一个简单的策略是客户端发布消息是对所有broker进行轮询请求(round robin requests)。另一种选择,在那些生产者比消费者多的场景下,给每个客户机随机选择并发布消息到该分区。后一种的策略能够使用少得多的TCP连接。

    语义分区是指使用关键字(key)来决定消息分配的分区。例如,如果你正在处理一个点击消息流时,可能需要通过用户ID来划分流,使得特定用户的所有数据会被单个消费者消费。要做到这一点,客户端可以采取与消息相关联的关键字,并使用关键字的某个Hash值来选择的传送的分区。

    批处理(Batching)

    我们的API鼓励将小的请求批量处理以提高效率。我们发现这能非常显著地提升性能。我们两个用来发送消息和获取消息的API,总是以一连串的消息工作,而不是单一的消息,从而鼓励批处理操作。聪明的客户端可以利用这一点,并支持“异步”操作模式,以此进行批处理哪些单独发送的消息,并把它们以较大的块进行发送。我们再进一步允许跨多个主题和分区的批处理,所以生产请求可能包含追加到许多分区的数据,一个读取请求可以一次性从多个分区提取数据的。

    当然,如果他们喜欢,客户端实现者可以选择忽略这一点,所有消息一次都发送一个。

    版本和兼容性(Versioning and Compatibility)

    该协议的目的要达到在向后兼容的基础上渐进演化。我们的版本是基于每个API基础之上,每个版本包括一个请求和响应对。每个请求包含API Key,里面包含了被调用的API标识,以及表示这些请求和响应格式的版本号。

    这样做的目的是允许客户端执行相应特定版本的请求。目标主要是为了在不允许停机的环境下进行更新,这种环境下,客户端和服务器不能一次性都切换所使用的API。

    服务器将拒绝它不支持的版本的请求,并始终返回它期望收到的能够完成请求响应的版本的协议格式。预期的升级路径方式是,新功能将首先部署到服务器(老客户端无法完全利用他们的新功能),然后随着新的客户端的部署,这些新功能将逐步被利用。

    目前,所有版本基线为0,当我们演进这些API时,我们将分别显示每个版本的格式

    4 通讯协议(The Protocol)

    协议基本数据类型(Protocol Primitive Types)

    The protocol is built out of the following primitive types. 
    该协议是建立在下列基本类型之上。

    • 定长基本类型(Fixed Width Primitives) 
      int8, int16, int32, int64 – 不同精度(以bit数区分)的带符号整数,以大端(Big Endiam)方式存储.
    • 变长基本类型(Variable Length Primitives) 
      bytes, string – 这些类型由一个表示长度的带符号整数N以及后续N字节的内容组成。长度如果为-1表示空(null). string 使用int16表示长度,bytes使用int32.
    • 数组(Arrays) 
      这个类型用来处理重复的结构体数据。他们总是由一个代表元素个数int32整数N,以及后续的N个重复结构体组成,这些结构体自身是有其他的基本数据类型组成。我们后面会用BNF语法展示一个foo的结构体数组[foo]

    请求格式语法要点(Notes on reading the request format grammars)

    后面的BNF确切地以上下文无关的语法展示了请求和响应的二进制格式。每个API都会一起给出请求和响应,以及所有的子定义(sub-definitions)。BNF使用没有经过缩写的便于阅读的名称(比如我使用一个符号化了得名称来定义了一个生产者错误码,即便它只是int16整数)。一般在BNF中,一个序列表示一个连接,所以下面给出的MetadataRequest将是一个含有VersionId,然后clientId,然后TopicNames的阵列(每一个都有其自身的定义)。自定义类型一般使用驼峰法拼写,基本类型使用全小写方式乒协。当存在多中可能的自定义类型时,使用’|’符号分割,并且用括号表示分组。顶级定义不缩进,后续的子部分会被缩进

    等等,其他要点案例请看译文:http://www.360doc.com/content/16/1118/21/37466175_607641263.shtml


猜你喜欢

转载自blog.csdn.net/gududedabai/article/details/80002088
今日推荐