渡码,阿里巴巴码农,公众号:渡码 作者,专注大数据开发、数据分析和Python技术。
关注公众号 渡码 回复关键字 manis,可获取电子书、各章节和完整源代码,并且可加入读者群一起交流问题。
19年上半年,我阅读了Hadoop RPC模块的源代码,读完后发现这个模块设计的非常好,与其他模块无耦合,完全可以独立出来当成一个独立的框架。为了总结学到的编程知识,同时也为了学习Apache顶级开源项目的代码是如何编写的,我便把它做成了电子书,共350页,从写代码到做成电子书共花了6个月的时间。本来想做成付费专栏赚点小钱,并且已经到了上架阶段了,但后来决定把它免费开放出来,让更多的人能够学习到优秀的实战项目。
当然我们这本书并不是源码分析类教程,而是强调动手能力。在这里我会带着大家按照 Hadoop RPC 源码从 0 到 1 完整敲一遍,代码量在 4600 行左右。为了让不熟悉 Hadoop 或 RPC 的朋友也能够学习,我将 Hadoop RPC 稍微做了一点改造,赋予了新的业务含义,也有自己的名字,叫 Manis。Mnias 源码相比于 Hadoop RPC源码还原度为90%。为什么不是100%呢?一方面为了突出重点,我会把不太重要、不是很核心的技术舍弃掉。另一方面为了符合新的业务定义,我会做一些改进,而不是照搬完全 Hadoop RPC。
虽然这个项目是实现 RPC 功能,但我觉得我们关注的重点不应该过多地放在 RPC 本身,而应该重点学习编写 RPC 过程中所涉及的系统设计、面向对象设计思想和原则、工程/代码规范、客户端开发、服务端开发、网络编程、多线程、并发编程、设计模式、异常处理等核心知识,可以说是麻雀虽小五脏俱全。尤其是对于刚学习 Java 还没有接触线上实战项目的朋友,这是一次很好的练兵机会。
学习开源项目的一个优势在于它是经过线上检验的,Hadoop集群规模最大达到上万台服务端,足以证明它的 RPC 模块是优秀的。另外一个好处是可以积累顶级开源项目的开发经验,大到架构设计,小到设计模式、代码规范,说不定日后就能为开源社区贡献代码了。所以,学会了 Manis 后,不但有编写实战项目的经验,同时也有能力阅读 Hadoop RPC 的源码,这也算是面试的加分项。
下面我们来介绍一下 Manis 中涉及的核心技术点。作为一个 RPC 框架,最关键的几个模块是客户端、网络模块和服务端
作为客户端来说,它的职责非常明确,以数据库客户端为例,它的职责就是向用户提供增删改查接口,并将相应的请求发送给服务端,接收结果并返回给用户。由于客户端职责边界是非常明确的,所以我们从设计上就要将其与网络模块、与服务端解耦,解耦的方式就要用到设计模式中的代理模式。也就说客户端只需要定义好它需要提供的功能(接口)并提供给用户,具体如何实现就交给代理,至于代理是通过网络发送给服务端还是通过其他什么方式客户端就不需要关心了,客户端只关心调用代理并拿结果。这样做的好处是客户端与其他模块解耦,提高了系统扩展性。当然,代理模式还有个容易被忽略的好处是它天然地适合用在 RPC 场景。
Manis 中支持多种序列化/反序列化方式,每种序列化方式对应一个类,它们都继承共同的基类。我们在设计时需要做到不同序列化方式之间的代码是解耦的,且序列化/反序列化模块与客户端模块、与网络模块是解耦的,这样才能做到任意地增加新的新的序列化方式以及删除老的序列化方式。为了实现客户端与序列化/反序列化模块的松耦合,我们需要用到一些设计模式,比如,用适配器模式将客户端定义的请求接口适配到不同序列化协议定义的请求接口。这样做几乎不需要修改现有的代码,符合面向对象的开闭原则。
下面再来说说网络模块。
由于客户端的请求可能来自不同的序列化协议,但的目的是相同的,都是为了通过网络模块的服务端,可以说是殊途同归。这样的话,我们就有必要在网络这一层定义一个统一的协议(接口),让不同序列化方式都遵循相同的协议(接口),那么网络模块就可以对它们“一视同仁”,编写一套代码就可以了。就好比,不管你用U盘还是硬盘,只要是 USB 接口,那都能插到电脑的同一个接口进行相同的读写逻辑。对于服务端的返回值也是采用同样的处理逻辑。
网络模块必不可少的功能就是发送网络请求,当然除了这个还有一个更核心的功能是管理网络资源。听起来有点抽象,如果用面向对象的思想来理解,其实就是创建一个类代表网络连接,比如就叫Connection
类,每次创建一个网络连接其实就是创建一个Connection
对象。当然,我们知道网络资源比较宝贵且创建成本较高,当系统客户端请求量非常大的时候,我们不可能为每次请求都创建一个网络连接,所以,需要建立一个网络连接池,以达到复用网络资源的目的。我们可以再定义一个类ConnectionId
,每个ConnectionId
对象都唯一代表Connection
对象,ConnectionId
的属性包含服务端地址和请求网络的一些参数,所以我们可以认为客户端请求服务端的地址和参数相同的话,就可以复用同一个网络连接。当然,这里还有一个很关键的问题不容忽视,网络连接池是公共资源,为了保证线程安全,在对资源池读写时需要加锁,也是从这里开始本书加大了对并发编程的相关讲解。刚刚介绍的这部分在 Manis 中是自主实现的。
建立网络连接的过程中还会涉及发送请求头、请求上下文,装饰网络输入、输出流等功能,这些比较偏业务,这里就不再赘述了。
发送网络请求时,为了将业务代码与发送请求代码剥离,在 Manis 创建了一个建线程池,将发送发送请求的代码封装成线程,丢到线程池中等待执行。所以,这里又涉及到三部分知识
最后,网络模块要实现的是等待服务端返回的结果。由于网络模块同一时间会接收大量客户端网络请求,所以,我们可以创建一个单独的线程,每隔一定时间轮询是否有服务端的返回。
对于服务端来说,我们最关心的是性能问题。因为大量的客户端请求最终都会汇总到服务端一个节点来处理。所以最原始的单线程+while循环的方式肯定满足不了性能要求。所以比较最容易想到的改进点是多线程,虽然在一定程度上能解决第一种方式带来的问题,但这种方式也有很大的缺点:频繁创建线程成本比较大,并且线程之间的切换也需要一定的开销,当线程数过多时显然会降低服务端的性能。目前比较常用的解决方案是Reactor模式,Reactor模式也分为单线程Reactor、多线程Reactor和多Reactor。这几种的区别在书里都有具体说明,这里我就不再介绍了。Reactor模式的优势按照我自己的理解就四个字——各司其职。Manis 中使用的是多Reactor模式,设计图如下:
简单介绍一下图中几个线程的功能
够各司其职吧。那它们之间怎么联系呢?从图上可以看到是消息队列,消息队列可以很好地实现组件间的解耦。
虽然服务端的职责也比较明确、清晰,但涉及的内容一点不少,包括注册不同的序列化方式,解析并调用相应的请求。最关键的是服务端线程是最多的,并且需要线程之间需要高度协调的,所以对并发编程的要求也更高,这块书中也有重点讲解。
最后我们看看Manis中核心组件的时序图
由于 Manis 在设计上是足够优秀的,所以开发的时候这三个模块可以并行进行。有点像近几年web开发比较火的前后端分离架构,只要各个模块把协议定义好了后,开发就可以并行进行而不需要依赖彼此。至此,Manis 的核心技术就介绍完了,当然这只是冰山一角,毕竟 4600 行代码。
关注公众号 渡码 回复关键字 manis,可获取电子书+源码+读者交流群。
在讲解相册内容同时,大部分章节都加入了课外拓展,针对每一节涉及的基础知识,如:设计模式、序列化/反序列化基础、单例测试、源码分析、并发编程以及Hadoop源码分析等内容都有拓展讲解。力求让零基础的朋友也能跟上本书节奏,从0到1独立完成一个项目。
希望你学完本书后不只学会了某项技术,而是提高了设计实现整个系统的能力。