最近一直在关注阿里的一个开源项目:OpenMessaging

OpenMessaging, which includes the establishment of industry guidelines and messaging, streaming specifications to provide a common framework for finance, e-commerce, IoT and big-data area. The design principles are the cloud-oriented, simplicity, flexibility, and language independent in distributed heterogeneous environments. Conformance to these specifications will make it possible to develop a heterogeneous messaging applications across all major platforms and operating systems.

这是OpenMessaging-Java项目GitHub上的一段介绍,大致是说OpenMessaging项目致力于建立MQ领域的标准。

看了OpenMessaging-Java项目的源码,定义了:

  • Message接口
  • Producer接口
  • Consumer接口
  • 消费方式:Pull、Push
  • 各种异常

确实是在朝着建立一套MQ的接口标准。

这引发了我的一个思考:MQ目前确实没有一套标准的接口,如果我们尝试从更高的层次看自己的项目,即我们希望它成为行业标准,那么现在项目中接口的定义合适吗?是否够通用、简洁、易用、合理?

带着这样的疑问,最近把Kafka Consumer部分的源码读了一遍,因为:

  1. Kafka应该是业界最著名的一个开源MQ了(RocketMQ最初也是参考了Kafka去实现的)
  2. 希望通过读Kafka源码能找到一些定义MQ接口的想法

但是在读完Kafka Consumer部分的源码后稍稍有一些失望,因为它并没有给我代码我想要的,反而在读完后觉得接口设计和源码实现上相对于Kafka的盛名有一些名不副实的感觉。

接口定义

Kafka在消费部分只提供了一个接口,即Consumer接口。

Consumer接口如下:

  • Set<TopicPartition> assignment();
  • Set<String> subscription();
  • void subscribe(Collection<String> topics);
  • void subscribe(Collection<String> topics, ConsumerRebalanceListener callback);
  • void assign(Collection<TopicPartition> partitions);
  • void subscribe(Pattern pattern, ConsumerRebalanceListener callback);
  • void subscribe(Pattern pattern);
  • void unsubscribe();
  • ConsumerRecords<k, v=""> poll(long timeout);
  • void commitSync();
  • void commitSync(Map<TopicPartition, OffsetAndMetadata> offsets);
  • void commitAsync();
  • void commitAsync(OffsetCommitCallback callback);
  • void commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback);
  • void seek(TopicPartition partition, long offset);
  • void seekToBeginning(Collection<TopicPartition> partitions);
  • void seekToEnd(Collection<TopicPartition> partitions);
  • long position(TopicPartition partition);
  • OffsetAndMetadata committed(TopicPartition partition);
  • Map<MetricName, ? extends Metric> metrics();
  • List<PartitionInfo> partitionsFor(String topic);
  • Map<String, List> listTopics();
  • Set<TopicPartition> paused();
  • void pause(Collection<TopicPartition> partitions);
  • void resume(Collection<TopicPartition> partitions);
  • Map<TopicPartition, OffsetAndTimestamp> offsetsForTimes(Map<TopicPartition, Long> timestampsToSearch);
  • Map<TopicPartition, Long> beginningOffsets(Collection partitions);
  • Map<TopicPartition, Long> endOffsets(Collection partitions);
  • ...

(读源码时光看完这部分接口我就已经晕了)

上面的方法大致可以分为四类:

  1. 订阅相关:subscribe、unsubscribe
  2. 消费相关:assign、poll、commit
  3. 元数据相关:搜索、设置、获取offset信息;partition信息
  4. 生命周期相关:pause、resume、close等

看完这个接口的第一个感觉就是灵活有余易用不足。

Kafka几乎暴露了所有的操作API,这样的好处是足够灵活,但是带来的问题就是易用性下降,哪怕用户只是希望简单的获取消息并处理也需要关心offset的提交和管理以及commit等等。

另外功能上也并没有提供用户更多的选择,比如只提供了poll模式去获取消息,而没有提供类似push的模式。

线程模型部分

看完接口之后,第二步看了Kafka Consumer部分的线程模型,即尝试将Consumer部分的线程模型梳理清楚:Consumer部分有哪些线程,线程间的交互等。

Consumer部分包含以下几个模块:

  1. Consuming

    • Consumer、ConsumerConfig、ConsumerProtocol
    • Fetcher
  2. 分布式协调
    • AbstractCoordinator、ConsumerCoordinator
  3. 分区分配和负载均衡
    • Assignor
    • ReblanceListener
  4. 网络组件
    • NetClient
    • Future
    • FutureListener
  5. 异常
    • NoAvailableBrokersException、CommitFailedExceptin、...
  6. 元数据和数据
    • ConsumerRecord、ConsumerRecords
    • TopicPartition
  7. 统计及其他

通过分布式系统组件及分区分配策略,每个Consumer可以拿到自己消费的分区。之后通过Fetcher来执行获取消息的操作,而底层通过网络组件NetworkClient和Broker完成交互。

通过阅读源码和注释发现,Kafka Consumer并没有去管理线程,而是所有的操作都在用户线程中完成。

所以线程模型就非常简单,Consumer非线程安全,同时只能有一个线程执行操作,且所有的操作都在用户的线程中执行。

Consumer通过一个AtomicLong的CAS操作来保证只能有一个线程操作(多线程的情况下会报出异常)

部分代码实现解读

ConsumerRecords<k, v=""> poll(long timeout)

poll应该是Consumer的核心接口了,因为到这里才真正执行了和获取消息相关的逻辑。

首先是校验逻辑,在poll之前如果没有进行topic的订阅或分区的分配,poll操作将抛出异常。

接着是poll的核心逻辑:

  • 在一个循环体中执行获取数据的逻辑,跳出循环的条件是超时或者获取到数据

从代码中可以看出pollOnce应该是真正的执行一次获取消息的操作。而代码中注释的部分是poll的核心:

  • fetcher#sendFetches方法给有需要的Server节点发送获取消息的请求

    • 这么做的目的是在用户下一次进行poll操作之前先将获取消息的请求发送出去
    • 这样网络操作和就可以和用户处理消息的逻辑并行,降低延迟
  • client#hasPendingRequests判断是否还有未从客户端发送出去的请求
  • client#pollNoWakeup执行网络真正的网络IO操作

从这段注释和代码中可以看出,poll时如果拿到数据了,会将剩余的请求发送出去来实现pipelining的目的。

所以对应的pollOnce内的逻辑必然有从缓存中(即上一次poll请求中获取的数据)获取数据的操作。

pollOnce对目标分区执行一路poll请求,大致流程如下:

  1. coordinator#poll确保Consumer在Coordinator的管理之中

    • ensure coordinator
    • ensure active group(将Consumer加入到group中)
    • 发送heartbeat
  2. 更新positions
  3. 从fetcher中获取消息,如果已经拿到消息则返回结果,调用结束
  4. 对分区执行poll请求
  5. 阻塞等待至少一个fetch操作完成
  6. 判断是否操作期间元数据进行了变更,如果变更了,丢弃获取的数据
  7. 返回获取结果

读上面的代码,第一个感觉就是可读性比较差,比较难懂。

比如pollOnce中,fetcher#sendFetches从字面上看会理解成发送fetch请求:

  • 如果是同步的,那么应该获取它的结果
  • 如果是异步的,应该通过Future获取最终的结果

而实际上fetcher#sendFetches只是去构建了请求,并且将请求保存在NetworkClient中(NetworkClient会有数据结构保存每个Node对应的请求:类似这样的数据结构Map<Node,Queue<Request>>)。

在client#poll中才将通过fetcher构造的请求真正的写出去,并且阻塞的等待fetch的结果,从实现上感觉将代码变的复杂了。

NetworkClient提供了异步的网络操作,且是非线程安全的。

NetworkClient只有poll会真正的去执行IO操作,而其中的send只是将send数据保存在channel上,直到执行poll时将它写到网络中。

总结

在读完Kafka Consumer部分的源码后,稍稍有些失望:

  1. 只提供了poll模式,没有提供给用户更多的选择,比如push模式

    • openmessaging在这块分别提供了PullConsumer和PushConsumer接口
    • 而我们自己的项目则是提供了ListenConsumer、StreamConsumer等(Listen模式用户只提供回调接口,我们管理线程,而Stream模式将消费线程交给用户自己管理),继续还会提供基础的PullConsumer等
  2. Consumer接口的灵活性由于,易用性不足
    • 暴露了太多的接口,对于一个指向简单获取消息处理的使用方来说心智负担太重
  3. 代码的实现上复杂化了,比如提供了Fetcher和NetworkClient的实现非常复杂

总体上Consumer的代码有一些乱,比如下面是Kafka源码中Consumer部分的包组织和我自己读源码使对它的整理:

右边是Kafka源码Consumer部分的包结构,所有的类分了两块,内部的在internals中。右边是自己读源码时根据各个模块对Consumer的类进行划分。

私以为将各个类按照不同的模块分开会更加清晰,读起来也会更加舒服。

读Kafka Consumer源码的更多相关文章

  1. 实战录 | Kafka-0.10 Consumer源码解析

    <实战录>导语 前方高能!请注意本期攻城狮幽默细胞爆表,坐地铁的拉好把手,喝水的就建议暂时先别喝了:)本期分享人为云端卫士大数据工程师韩宝君,将带来Kafka-0.10 Consumer源 ...

  2. Kakfa揭秘 Day6 Consumer源码解密

    Kakfa揭秘 Day6 Consumer源码解密 今天主要分析下Consumer是怎么来工作的,今天主要是例子出发,对整个过程进行刨析. 简单例子 Example中Consumer.java是一个简 ...

  3. Kafka Eagle 源码解读

    1.概述 在<Kafka 消息监控 - Kafka Eagle>一文中,简单的介绍了 Kafka Eagle这款监控工具的作用,截图预览,以及使用详情.今天笔者通过其源码来解读实现细节.目 ...

  4. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  5. 如何读懂Framework源码?如何从应用深入到Framework?

    如何读懂Framework源码? 首先,我也是一个应用层开发者,我想大部分有"如何读懂Framework源码?"这个疑问的,应该大都是应用层开发. 那对于我们来讲,读源码最大的问题 ...

  6. 读源码【读mybatis的源码的思路】

    ✿ 需要掌握的编译器知识 ★ 编译器为eclipse为例子 调试准备工作(步骤:Window -> Show View ->...): □ 打开调试断点Breakpoint: □ 打开变量 ...

  7. Kafka的Producer和Consumer源码学习

    先解释下两个概念: high watermark (HW) 它表示已经被commited的最后一个message offset(所谓commited, 应该是ISR中所有replica都已写入),HW ...

  8. KClient——kafka消息中间件源码解读

    目录 kclient消息中间件 kclient-processor top.ninwoo.kclient.app.KClientApplication top.ninwoo.kclient.app.K ...

  9. Kafka Broker源码:网络层设计

    一.整体架构 1.1 核心逻辑 1个Acceptor线程+N个Processor线程(network.threads)+M个Request Handle线程(io threads) 多线程多React ...

随机推荐

  1. C语言指针的那些坑

    那些年把我们坑惨的指针 一.引言 当我们使用c语言的时候,不可避免的就得用到指针,然后对于刚刚接触C语言的猿兄们,可能会有点不适应,特别是刚刚从python等离硬件很远的语言转过来的. 下面我为大家总 ...

  2. 记录一个Unity播放器插件的开发

    背景 公司最近在做VR直播平台,VR开发我们用到了Unity,而在Unity中播放视频就需要一款视频插件,我们调研了几个视频插件,记录两个,如下: Unity视频插件调研 网上搜了搜,最流行的有以下两 ...

  3. MongoDB自动增长

    MongoDB 没有像SQL一样有自动增长的功能,如果我们需要实现ObjectId自动增长功能,可以通过编程的方式来实现.步骤如下: 1. 创建counters集合: db.createCollect ...

  4. PHP设计模式一:工厂方法设计模式

    一.什么是工厂方法模式 作为一种创建型设计模式,工厂方法模式就是要创建“某种东西”.对于工厂方法,要创建的“东西”是一个产品,这个产品与创建它的类之间不存在绑定. 实际上,为了保持这种松耦合,客户会通 ...

  5. Python Fabric远程自动部署简介

    Fabric是一个Python(2.5-2.7)库,用于简化使用SSH的应用程序部署或系统管理任务. 它提供的操作包括:执行本地或远程shell命令,上传/下载文件,以及其他辅助功能,如提示用户输入. ...

  6. iOS之 Auto Layout

    1. 动画 // 修改从 StoryBoard 绑定到类的约束的值 self.boxView.constant += 80 // 在动画闭包里对其父级进行 layoutIfNeeded() UIVie ...

  7. (11.06)Java小知识

    最近由于课程变化,学习计划也跟着改动,留给我写博客的时间也越来越少.今天晚上没有课,抽空过来图书馆写一写,许久不写感觉都有点陌生了! 今天要和大季家分享的衔接了上一篇博客,是关于方法的嵌套调用与递归调 ...

  8. 【IDEA】向IntelliJ IDEA创建的项目导入Jar包的两种方式

    转载请注明出处:http://blog.csdn.net/qq_26525215 本文源自[大学之旅_谙忆的博客] 今天用IDEA,需要导入一个Jar包,因为以前都是用eclipse的,所以对这个id ...

  9. T-SQL 删除重复数据SQL

    WITH cte AS (   SELECT roleid,permissionid,      row_number() OVER(PARTITION BY roleid,permissionid ...

  10. c语言学习笔记 —— 数组

    1. 数组的内存分配原理. 数组定义  int a[3] = {1,3,11}; 如下写法是错误的: int number; int a[number] = {1,7,9}; 原因是:没有定长的数组初 ...