`
herman_liu76
  • 浏览: 96629 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

分析几个有名中间件源码的核心类的设计

阅读更多
    分析过不少中间件产品后,自己也设计过产品后,也需要在更高的高度上总结一下。本文先总结了一下核心类的定义与主要的设计特点,再进一步用druid,rocketmq,spring-jms等源码的核心类进行说明(包括启动/停止),之后还介绍了一下dubbo的核心类在哪?(它更像一个产业链结构,核心类控制采购与销售,中间可能层层外包出去)

    在讲核心类之前,其实在软件系统有之前,我们的社会组织,或者企业组织,都是这么个结构,核心类就是中央,就是总经理。当然也有与dobbo对应的组织形态,控制头尾,中间层层分包出去。

    理解了核心类,也就很容易理解整个产品的的结构设计了,另外对于应用系统,对于分布式架构,对于微服务架构设计与组织,也有思想上的启发。类比一下,核心类类似DDD(领域驱动设计)中的聚合根,类似于组合多个resposity的service,类似于微服务中的聚合服务。



## 一、什么是核心类

我们说,擒贼先擒王,研究一个东西要抓核心,那一个软件产品的核心在哪?研究过一些产品后,发现产品都有核心类,自己也应用这个思想把控组件设计。

核心类是产品功能的最主要的实现类,也是类中的老大哥,它下面的兄弟众多,它可能只负责协调调用,也可能功能需要兄弟们协助,也可能根据不同的情况选择不同的兄弟,也可能安排助手去监督兄弟们的工作...

核心类不一定是最花时间的,但一定是稳定系统结构的,显示出整体的思想与高度。有助于在万事开头难的情况下跨出第一步。现在常说API接口优先,那是实现角度,复杂系统的功能是如何进行组织的,可能是更优先的设计。



核心类的基本结构与考量如下:

  • - 自身可能是工厂产生,或者是单例对象,一般复杂的都是工厂产生的。
  • - 有自身状态,有生命周期。构造,inti,优雅的启动与关闭,在构造中组建好自己的团队成员
  • - 它持有其它功能类成员,这些功能类一般不相互引用,但它把this传给功能类,可相互引用
  • - 特殊的成员比如,通讯成员,负责对外接收发送数据,也可能对内对外协议不同有不同的通讯员。如果外对支持多协议,还要插入一个通用API层。
  • - 成员基于接口引用,所以核心类持有的成员兄弟很多可替换,如果是策略模式,加载根据运行情况,可通过spi,或者aware spring容器去取,或者配置参数用工厂来产生,或者如dubbo统一建一个extension中心。
  • - 交给一些成员重要功能时,可能会给它设置一个引用自己的回调类,让核心类感知变化,协调资源
  • - 可能包含多个线程或者线程池,可能分工作线程与自身守护线程定时运行,可能需要锁进行同步,Synchronized优化偏向锁,自旋锁,锁再升级,性能可以的
  • - 包含共享变量,或者共享数据容器,注意volitile/concurrent/atom使用
  • - 核心类自身产生多个对象各自工作,一般用享元模式存放,即static map,当然也可能有个上层进行简单管理,比如多个jms消费容器,上头有注册器统一管理。




## 二、核心类例子

### 1. rocketmq中的BrokerController

rocketmq是阿里的消息中间件,它有一个重要模块是broker,功能是接收消息并存储,索引,分队列的,也是推送消息的客户端的。这个模块的核心类是org.apache.rocketmq.broker.BrokerController,它的特点有:

- 成员众多且不直接引用
  •   1. 有几个config成员,管不同组件的配置
  •   2. 有很多Manager成员,比如管消费偏移量,管客户端
  •   3. 有Service,比如扫描客户端通道状态与变化
  •   4. 有些Listener,比如监听消息到来
  •   5. 有负责内部通讯的romotingServer
  •   6. 有处理消息的Processor,有存储负责DefaultMessageStore等,各司其职。DefaultMessageStore也是存储方面的子核心类,结构关系很相似。
  •   7. 有很多BlockingQueue与ExecutorService,它们组合起来,再与Processor交给romotingServer用于处理接收不同类型的消息。

- 构造函数与initialize初始化方法
  •   1. 构造函数中有很多类似this.topicConfigManager = new TopicConfigManager(this);的语句,即产生了成员对象,也实现了核心类与功能类对象的相互引用。如果设计不好,走“捷径”,成员之间相互引用,就乱成毛线团了。而成员由于引用了核心类对象,找谁也找的到,也就无所不能了。
  •   2. initialize中,先由一些成员load加载数据,满足条件后,再进一步产生一些成员,比如存储管理,通讯管理等成员。

- init中安排成员工作
  •   1. 比如安排通讯成员工作。在初始化中,通讯成员启动后把Processor与ExecutorService交给它,通讯成员就开始工作了,监听tcp端口,处理请求了。那请求时需要核心类帮忙怎么办?通过SendMessageProcessor sendProcessor = new SendMessageProcessor(this);,处理器可以找到核心类,那工作都好做了。
  •   2. 比如安排存储成员工作。先产生了DLedgerRoleChangeHandler传给存储成员,存储发生变化时会通知这个handler,hander由于持有核心类对象,所以也无所不能。

- 生命周期管理
  •   1. 比如start()方法,让很多成员也start(),也让自己的定时守线线程启动起来。
  •   2. shutdown()让很多成员也shutdown(),线程池也shutdown了。

- 如何启动/停止核心类
  •   1. BrokerStartup用main方法开始,调用构造函数new 一个BrokerController。
  •   2. 并在Runtime停止时,用hook关闭核心类对象。
  •   3. 另外提一下,客户端client的核心类是懒加载的,有producer了才启动。没人用,不空转。


**通过上面的总结,一个简单的消息处理过程是这样的**:通讯成员接收客户端消息,它在线程池中用处理器处理时,处理器可以找到核心类,核心类会让存储存好数据,再返回结果,通讯成员就可以回复客户端消息接收成功了。

### 2. druid中的DruidDataSource

druid是阿里的数据库连接池产品,代理了几个常用的连接,并在所有操作中切入filter得到监控数据。这的最外层核心类是:com.alibaba.druid.pool.DruidDataSource,包括它父类。它的特点有:

- 成员众多
  •   1. 它不是上面那样很多功能类,而是很多基本数据,如:long弄的Count或者AtomicLong。正好符合统计监控的需要
  •   2. 重要的数据,即一堆真实的数据库连接。DruidConnectionHolder[]
  •   3. 有两个重要线程Thread,分别用于增加与减少真实的连接数。还有logStatsThread统计线程。
  •   4. 有个CountDownLatch,用于保证init操作在两个额外线程完成后完成。
  •   5. List<Filter>用于存放将插入监控的过滤器,Init中也反持有核心类。
  •   6. 有ReentrantLock用于控制共享数据的操作,其它略。

- 构造函数与初始化
  •   1. 由于它实现了jdbc的DataSource接口,可以就当一个DB数据源使用。在getConnection获取数据库连接时,进行Init()操作。
  •   2. Init中初始化一些值,包括连接的容器外,还启动了控制pool中连接数量的两个线程。是否可以用一个?
  •   3. 过滤器filter是插入sql操作中,真正进行监控的工具,核心类用List保存它们,而在核心类的Init中有:filter.init(this);说明每个过滤器都要持有核心类来工作,至少会把数据写在核心类的统计属性中。

- 关闭数据库连接
  •   1. close()中,interrupt这些线程,connHolder中的真正连接都close(),清空容器,清空filter的list。


**通过上面的总结,一个简单的连接过程是这样的**:调用DruidDataSource获取连接方法,如果DruidDataSource没初始化就进行初始化,从DruidDataSource中再拿到filter来处理统计,再用filter反持有的核心类DruidDataSource真正做事。实际中用了过滤链模式。这个druid原理很简单,但所有操作,所有对象要包装起来,琐碎工作量还是很大的。


### 3. spring-jms中的消费容器DefaultMessageListenerContainer

与前面的核心类相比,这个org.springframework.jms.listener.DefaultMessageListenerContainer并不是很突出,不过获取消息进行消费都是在这个容器中进行的。而且每一个注解@jmsListen都产生一个,它有多层父类,其特点有:

- 它有也不少属性成员
  •   1. 包括很多参数,重要成员有:Executor,transactionManager,messageListener,messageConverter等
  •   2. 还有放置runnable对象的容器Set<AsyncMessageListenerInvoker>,可放Executor中执行,线程之间有Monitor对象,用synchronized进行同步处理
  •   3. 它持有的线程启动后,很聪明的进行自我管理,可减少,可增加,可休息

- 它受spring生命周期管理,有明确的initialize()/start()/stop()方法。
- 它受上级管理
  •   1. 虽然它做核心功能,但由于它有很多相同的对象一起工作,所以有上层的管理者JmsListenerEndpointRegistry。
  •   2. JmsListenerEndpointRegistry用工厂JmsListenerContainerFactory构造出它后,用Map<String, MessageListenerContainer> listenerContainers保存它。算享元模式。
  •   3. 上级也试图对它们进行start()/stop()控制。


由于这个工具是在spring中,启动过程就是扫描注解,按@jmsListen产生这样的真正工作的容器,并接受spring的控制。

### 4.dubbo中的核心类

与前面几个产品中的核心类不同,回忆dubbo还真不如上面的这么明显存在一个核心类。

先整理下服务端接收调用过程例子:一个包含invocation的TCP请求过来,netty从早就建好的了channel中decode出来特定应用协议msg,看消息头,如果是业务处理msg,就找相应的handler处理消息,handler会从一个map表中找到invocation对应的invoker,invoker是由service统一包装出来的,再反射调用得到结果,再封装成msg,再由netty从channel中发回去。

服务端本来只有一个个service,通过dubbo产生上面的所有相关的东西。dubbo操作的过程是:

  • - 先把一个个service变成invoker
  • - 一个个invoker以invocation为key,存放在map中
  • - 启动一个netty通讯服务端,这个过程中要产生Netty用的decoder/encoder/msgHandler等类
  • - 要给msgHandler设置一个专门处理业务的内部handler
  • - 内部handler要能从msg中拿到invocation,再从map中拿到invoker
  • - 最后调用到service,得到结果,后面不提了。


这里,被spring加工的对象就是service,结果是让外部的各种请求可以调的到它。那么是否有设计一个核心类来做上面这一系列工作呢?没有。

或者说,由于dubbo的微核心化,大多数功能的实现类都是不确定的,在运行时由参数来确定。实现类统一放在extension中存放,甚至extension本身也有两种实现。在spring中用的话,那么上面的功能实际上由两个主要的类来实现。

核心类丙个:一个是ServiceBean,一个是协议如DubboProtocol。spring启动前者开始工作,前者再根据配置启动DubboProtocol工作,当然最主要的工作还是后者来做了。而且两个核心类的角度不同,ServiceBean对应一个service,但可以用多个Protocol暴露,而一个Protocol持有多个service的invoker,就是多对多的关系。

DubboProtocol持有的成员,与工作有:

  • - 持有一个New构造的真正处理RPC请求的requestHandler
  • - 持有exporterMap,让requestHandler可以找到对应的Invoker
  • - 提供参数启动具体的传输层,并把requestHandler交给它用,由于requestHandler是内部类,不用象上面用this引用核心类,就可以使用核心类了。
  • - 传输层具体怎么做就不管了,反正参数与最终的处理对象都给了它,算是分包出去了。
  • - 传输层会根据参数产生具体的netty,产生具体的encode/decode,再产生具体的serilize对象来处理tcp包中的dubbo协议包。


虽然dubbo的核心类不清晰,但顺着业务用例的流动,可以梳理出一个核心类调用另一个核心类工作的结构。如果不是要设计的这么灵活,特定的协议下,也可以写出一个清晰的核心类来做上面的工作。

### 5. 图

看着也蛮简单的,主要还是很多细节工作要做,而且细节要考虑的很仔细。比如rocketmq主要是存储,索引,队列和高可用这样的细节工作。



## 三、总结

其实还有些小例子就不一一举例了,我在设计组件时,也基本上采取这样的设计,感觉比较清楚,再复杂都可以可以把握全局,如同现实生活中,做好业务就有一个好的公司架构,也有强大的核心。

自己设计中,就可以把相似的功能复制过来用,比如rocketmq的内部通讯可以拿来用;或者把局部好的设计抄过来用,比如线程的自我控制,通讯处理器与带有阻塞队列线程池的组合,细粒度控制;设置监听了解成员的工作;另外成员其实也是局部功能中心,它也可以是层次结构;成员可以在运行中替换,比如dubbo中动态生成适配器成员,按参数持有真正成员;成员可以统一存放,用多种方式加载,比如工厂,比如SPI...

话说现在最喜欢的源码还是rocketmq,1主多从小集合比kafka全对等好,这与看到的某支3城5中心中的10个组,以及某信抢红包的一个set,都是相同的理念;另外对文件的处理值得参考;还有通讯设计可以直接用...

了解了结构也可以更好的使用,给使用者提供简单明了的操作入口。
1
1
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics