本文假设你已经了解了什么是服务化,只阐述针对现有的服务框架所存在的问题,如何根据实际需求去考量并解决对应问题。
首先,我们先来聊一聊服务框架的核心RPC!
RPC
RPC全称是Remote Procedure Call,即远程过程调用。它和HTTP不是同级的概念。HTTP是协议,而RPC是一种远程调用方式。它可以基于HTTP来实现,也可以基于TCP来实现。
乍看之下RPC不就是个客户端调用服务端吗?实际上它确实就是客户端调用服务端而已,只不过,在大部分情况下,RPC在调用时看起来像是在调用本地方法/接口。
注意看上面的Consumer Stub和Provider Stub,这里可以理解为是代理,即
- Consumer和Consumer Stub交互
- Consumer Stub通过RPCLibrary将调用内容序列化,通过网络传输给Provider端
- Provider端反序列化,然后根据传递的内容去调用实际的方法
- 接着再将结果序列化后,通过网络回传给Consumer端
- Consumer端接收到数据后,再反序列化获取到结果,然后进行后续的操作
一个具体的例子可以看SpringCloud套件中Feign的使用。
如下图所示,这里通过Feign定义了一个接口
可以直接通过方法调用的方式来调用,看起来和调用普通方法一样没有任何区别。
这里实际上就是进行了上面所述流程的操作,具体的源码流程,可以自行梳理,并不复杂。如果比较懒,可以等我后面SpringCloud的源码梳理,不过时间未定!
解释完RPC,我们就来看一看服务化框架。
服务化框架
上面所说的RPC还远远达不到框架的程度!
首先,Consumer是需要知道Provider的地址信息的,否则它无法将消息发送给Provider。这会导致什么问题呢?所有的Consumer都需要维护Provider地址信息。假设Provider地址信息发生了变化,则所有的Consumer都需要修改对应的配置信息。
其次,Consumer和Provider之间是直连的,假设Provider多了以后,则Consumer与Provider之间的连接将会很多,要人工维护会非常的麻烦。
另外,还有一个问题,Provider地址信息是配置到Consumer中的,那如果Provider挂了该怎么办?如何去除Provider的单点问题呢?
最后,Provider升级后,Consumer该如何处理?
如果你熟悉分布式系统的话,那答案是显而易见的,就是引入注册中心。
- Provider启动后将自己的信息注册到注册中心,注册中心接收到消息后,将新的Provider信息列表推送到已经连接上来的Consumer
- 同时Consumer启动时从注册中心获取Provider注册信息
- Consumer内部实现了负载均衡,通过负载均衡算法,来选择合适的Provider来发送请求
服务框架的功能
上图的Consumer,Provider和Registry实际上就是一个服务框架所需要的三个必要组件。而一个较为完善的服务框架,需要至少提供如下功能:
实现
当初编写这个服务框架实际上是为了解决几个问题:
- 一般服务框架的升级问题
- 服务元数据管理问题
- 模块化服务开发
我们从「服务升级」这个场景开始!服务升级应该算是服务化里最基本的功能了,我们以dubbo为例,看看使用dubbo框架的情况下,如何进行服务升级?
一般服务升级可以分为两种情况:
- 服务修改保持兼容,也就是说接口不变,实现改变。
- 另外一种情况是服务修改不兼容,即接口也改变了。
我们先看第一种情况,当服务保持兼容的情况下,我们的升级流程大致是什么样的!以下图为例,我们有三个服务节点和两个客户端:
- 首先,停止一个服务节点,发布新服务,启动服务节点
- 然后,再停止一个服务节点,发布服务,启动服务节点
- 直到所有的服务节点都更新完成
而当服务不兼容的情况下,升级流程大致是这个样子:
- 停止一个服务节点,发布新服务,启动服务节点。并保证当前客户端无法访问到这个新服务
- 升级一个客户端到新版本,并将调用指向新服务
- 重复如上步骤,直至全部发完
从上面的流程,我们可以发现如下几个问题:
- 频繁重启容器,累积耗时很长:服务发布需要重启容器,而容器中一般不会只有一个服务,发布一个服务需要启动整个容器,耗时会比较长
- 对同一服务节点下的其他服务有影响:上面提到一个容器下一般不会只有一个服务,当重启这个容器时,该容器内的其它服务也无法对外服务
- 在进行升级的过程中,会导致服务容量减少,相对的负载增加:在服务节点停止后,无法对外服务,原本路由到该节点上的请求被分配到了其他节点上,变相增加了其他服务的负载压力
对于这个问题,我们该如何解决呢?
其实很简单,
- 重启容器会使发布时间变长,那我们就不重启容器,直接发布服务,也就是「热部署」。
- 而对于不兼容服务的发布,我们可以通过版本管理来处理。
- 另外,我们希望能模块化的开发服务,使得服务能像积木一样的组合
对于如上的问题和需求,我们如何解决和实现呢?
如果你熟悉JVM的话,其实第一个想到的方法应该是自定义ClassLoader!而在我们服务框架的早期版本里,是使用OSGi来实现的。
现在应该很少有人了解OSGi了!在JAVA模块化出来之前,OSGi是Java模块化编程事实上的标准。eclipse就是基于OSGi来实现插件化的。
OSGi默认就提供了多版本管理,动态部署,模块化管理等功能(看起来很契合我们的需求)。它也是通过ClassLoader来实现的,不过和我们已经很熟悉Java的双亲委托模型不同,OSGi自己实现了一套网状的ClassLoader结构,通过这样的结构OSGi实现了上述功能。
我们来看下,在基于OSGi实现的服务框架下,服务发布的流程变成了什么样:
假设当前服务版本是1.0.0,欲发布2.0.0版本
- 直接将2.0.0版本的服务添加到服务容器中,自动启动、注册、推送
- 客户端目前版本为1.0.0,服务端目前包含了1.0.0和2.0.0版本的服务
- 客户端会查找最符合要求的服务进行调用:
- 如果版本号一致,则调用版本号一致的服务,
- 否则调用版本号最新的服务。目前客户端会调用1.0.0版本的服务
- 如果服务是兼容的,则可以直接卸载1.0.0版本的服务
- 而不管服务兼容不兼容,通过将客户端升级到2.0.0版本即可完成服务升级.1.0.0版本的服务可删可不删,因为目前没有请求会调用它了
最终服务框架的架构如下,客户端,注册中心,服务端和其它框架无异,主要区别在使用了OSGi来作为服务发布的容器。
新的问题
这样看起来很完美,OSGi完美的实现了我们的需求。但是实际上,在实际使用过程中还有很多新问题:
- OSGi增加开发复杂度
前面说了OSGi提供了多版本管理,动态部署,模块化管理等功能。而相应的带来了开发复杂度的提高。OSGi通过METAINF.MF文件来配置相关信息,而这些信息在Java中是静态的,通过这些信息,OSGi在运行时处理版本、导入导出等信息。导致的问题就是,开发修改了METAINF.MF文件后,不部署到OSGi容器内,无法确认配置是否正确。而每次发布都要打包,大大降低了开发效率。且由于需要发布到OSGi容器,所以只能远程调试。
如何解决这个问题呢?是继续为了使用OSGi带来的便利而忍受其带来的开发复杂度,还是放弃OSGi呢?
其实我们使用OSGi,需要的是它的多版本管理和热部署功能,模块化并不是强制需求,是我们的美好愿望,为了这个美好愿望牺牲开发效率值不值得呢?这是个问题。
最终我们放弃了OSGi,而使用自定义ClassLoader的方式来实现版本管理和热部署。
大致结构并不复杂,主要在ApplicationClassLoader下增加了BaseContainer和ServiceContainer,其中ApplicationClassLoader加载服务节点应用,主要负责通信,BaseContainer加载一些公共服务,例如日志,数据源等服务。ServiceContainer加载业务服务,在ServiceContainer加载的服务中,有一个比较特殊的服务,我们称为系统服务,它的作用是用来发布其他服务,具体作用后面再说。
- 耗时服务导致超时
我们使用netty作为服务框架的通信框架,实现的是从Reactor模型,在服务端获取到客户端请求后,会首先将其丢到一个队列中,由消费线程去异步处理请求,如果某个服务耗时比较长,则会导致队列阻塞,影响其它的服务响应。这个问题在其他的服务化框架中应该也是存在的。在新版服务框架中,我们可以将耗时服务独立到一个队列中进行执行,使该服务不会影响其他服务的正常调用。具体操作只需要在提供的界面做些配置就可以动态调整了。
- 服务发布分散
在我们发布服务时,我们需要登录每个需要发布服务的机器,进行发布服务的操作。公司的运维平台提供了界面可以操作发布,不过它是个运维平台,而不是业务平台,发布完成后如何确认服务都注册成功了呢?新版服务框架中提供了管理中心,实现在一处发布所有的服务。流程如下:
- 管理中心通知需要发布服务的节点,将需要发布的服务信息发送给节点
- 节点收到信息后,从Maven仓库下载需要发布的服务,进行部署
- 缺少完善的服务治理
在服务发布完成后,如何确认服务都注册成功了呢?
举个例子:我们发布服务A,注册中心可以看到其下的服务接口1和服务接口2,当我们更新服务A后,注册中心只能看到服务接口1,那么请问,消失的服务接口2是注册失败?还是版本更新给删除了?这个问题目前的服务框架都没有处理。
在我们新版服务框架中,提供了基于元数据的服务监控与提醒,可以配置监控重要的服务接口,如果发布后服务接口消失,则通过监控来进行及时的提醒。
除了上面所说的问题,新版服务框架还提供了完善的服务治理,权重的动态调整,服务自动升降级,服务端管理,客户端管理等操作。
新版服务框架的架构如下:
- 主要将注册中心升级为了管理中心
- 管理中心包含了注册中心,一个WebApp提供给相关人员进行服务管理操作,一个LocalClient来进行通信
- 这个LocalClient是个特殊的客户端,主要就是和上面提到的系统服务进行通信,来进行相关的管理操作
在新版服务框架下,所有的操作都可以在管理中心执行,结合公司管理平台提供的部署与监控,可以实现服务开发、部署、发布的闭环,大致流程如下:
- 通过管理平台发布客户端,注册中心,服务端。通过相应的配置,发布完成后三者即可通信
- 在管理中心服务页面,选择需要发布或更新服务的节点,进行发布或更新操作
- 相应节点接收到消息后,从Maven仓库下载需要发布或更新的服务,进行发布或更新
- 管理平台中可以查看服务调用的相关监控,同时如果出现了相关问题,例如上面提到的服务未注册成功,会及时通过管理平台通知相关人员
总结
本文主要是对之前开发的服务化框架的设计过程的一个梳理总结,并对一些特殊的考量做了特别说明。