服务框架(分布式系统六)

1、从一个小的网站逐渐变大,应用越来越复杂,怎么解决呢?

(1)把应用进行拆分,原来有三个应用,现在可以拆成6个。


问题:一方面数据库的连接数的压力还在,另一方面在系统之间会存在一些重复代码。

(2)服务化方案:在原来的应用和底层数据库、缓存系统、文件系统增加了服务层。下图只是简单实例。真正实施中服务可能是多层的,服务之间也会有相互访问。

2、服务框架的设计和实现

在没有服务化前,应用都是通过本地调用的方式使用其他组件的,服务化使得原来一些本地调用变为远程调用。那么一个服务框架要解决什么问题呢?


从图可以看出,原来单机中单个进程的一个方法调用分散到两个节点上要经过好几个步骤。单机单进程的方法调用只需要把程序计数器指向响应的入口地址,而在多机之间,需要对请求信息进行编码,然后传给远程节点,解码后进行真正的调用,这是上图中编码/解码过程,寻址路由是用来让调用方法确定哪个实例被调用,实例定位是指在被调用的机器上找到对应的实例来调用。

(1)服务调用端的具体工作


(2)从代码看如何使用服务框架

大多是使用java开发系统都会使用Spring作为组件的容器,所以通过spring方式引入是一个常见的方式,而作为服务框架,在请求发起端会提供通用的Bean。

例如有个计算器的服务,名字是org.vanadies.CalaculatorImpl。SpringBean最简单的配置:


上面给出最简单版本服务框架的配置,通过配置看出,实现了一个ConsumerBean,它是一个通用的对象,是完成本地和远程服务的桥梁,因为Java有了动态代理的支持,所以在完成远程调用时,使用一个通用的对象就可以解决了。

三个相对基础的属性:

interfaceName:接口名称,通过名字可以知道这个属性设置的就是接口名字,在开发中都是通过接口来调用相应的方法的,那么在远程通信时ConsumerBean必须知道被调用的接口是哪一个,然后才能生成对这个接口的代理,以供本地调用,所以这是一个必备属性。

version:版本号。实际中,接口是存在变化的可能的,例如修改已有接口参数列表或返回值,一种是新增方法,不过会导致代码臃肿,另一个种是通过版本号区分隔离,版本号就是解决这个问题的。

group:分组。对于同一个接口的远程服务有很多机器,可以把这些远程服务的机器归组,然后调用者可以选择不同的分组来调用,这样可以将不同调用者对于同一服务的调用进行隔离了。

3、运行期服务框架与应用和容器的关系

实际中有两个重要问题需要解决:一是服务框架自身的部署方式问题,而是实现自己服务框架所依赖的一些外部jar包和应用自身依赖jar包之间的冲突问题。

先看第一个:服务框架自身部署方式。一种方案是把服务框架作为应用的一个依赖包并与应用一起打包,通过这种方式,服务框架就变为了应用的一个库,并随应用启动。带来的问题是:如果要升级服务框架,就需要更新应用本身,因为服务框架是与应用打包放在一起的,并且服务框架没有办法接管classloader,也就不能做一些隔离以及包的实现替换工作。

另一种方案是把服务框架作为容器的一部分,这是针对web应用来说的,web应用一般用JBoss、Tomcat、Jetty作为容器,服务框架可以作为web应用的一个依赖包。


服务框架作为web容器的扩展而存在,也可以看出它与web应用关系。


下面是不使用问容器,而把服务框架作为容器来部署应用。



下面看下jar包冲突,一般应用中会有多个jar包,可能是第三发(写公共的库)提供的,也可能是自己做的,这些jar包本身又会依赖另一些jar包,可能会出现直接、间接依赖的jar包导致应用里面同一个jar包有不同版本,例如打印日志有log4j,可能有两个版本。ClassLoader用户自定义的ClassLoader有多个,并且是有机会进行隔离的,也可以采用类似方式:将服务框架自身用的类与应用用到的类都控制在User-Defined Class Loader级别,这样就实现了相互间的隔离。(如果出现同一级别的冲突,解决方案:在pom文件中通过  <exclusion><groupId>log4j</groupId>  <artifactId>log4j</artifactId>  </exclusion>来排除包冲突


4.服务调用者和服务提供者之间通信方式的选择


实际张,提供某种服务的机器一定是多台的,是一个集群,而且调用服务的机器也是集群的。

解决:以采用透明代理与调用者、服务提供者直连的解决方案

5.服务端的流控处理

一般有两种方式控制。一种是0-1开关,即完全打开不进行流控,另一种就是设定一个固定的值,表示每秒可以进行的请求次数,超过这个请求数的话就拒绝对远程的请求了,那些被流量控制拒绝的请求,可以直接返回个调用者,也可以进行排队。

那么基于什么维度进行控制呢?一般从两个,根据服务端自身的接口、方法做控制。二是根据来源做控制

6、网络通信实现选择

通信方式有BIO、NIO、AIO模式。BIO是阻塞模式,一个连接要消耗一个线程。NIO客户端和服务端的连接时可以复用的,而不是每一个请求独占一个连接。




上图增加了IO线程,数据队列,通信对象队列和定时任务4部分,IO专门和socket连接打交道,进行数据的收发。需要发送的数据都会进入数据队列。这样,每个请求线程就不需要直接和socket连接打交道了,这也就为socket复用提供了可能,数据队列长度是需要关注的,因为它能内存溢出。通信对象队列是保存了多个线程使用的通信对象,这个通信对象主要是为了阻塞请求线程,请求线程把数据放入数据队列中后会生成一个通信对象,它会进入通信对象并且在通信对象队列上等待。通信对象用于唤醒请求线程,如果在远程调用超时前有执行结果返回,那么IO线程就会通知通信对象,通信对象就会结束请求线程的等待,并把结果传给请求线程,以进行后续处理。此外,还有定时任务负责检查通信对象队列中的哪些通信对象已经超时了,然后这些通信对象会通知请求线程已经超时的事实。

6、支持多种异步服务调用方式

使用NIO能够完成连接复用以及对调用者的不同调用支持,除同步调用外,还要支持如下几种调用方式。

第一种是oneway,oneway是一种只管发送请求而不关心结果的方式。在NIO方式下使用oneway会比前面同步调用简单。


oneway方式很简单,只需要把发送数据放入数据队列,然后就可以继续处理后续的任务了,而IO线程也只需要从数据队列中读到数据,然后通过socket连接送出去就好了,oneway方式不关心对方是否收到了数据,也不关心对方收到数据后做什么或有什么返回,类似于一个不保证可靠送达的通知。

第二种方式是callback,这种方式下请求方发送请求后会继续执行自己的操作,等对方有响应时进行一个回调。


请求者设置了回调对象,把数据写入数据队列后就继续自己的处理了,后面IO线程的通信方式与前面类似,只是当收到服务提供者的返回后,IO线程会通知回调对象,这时就执行回调的方法了,如果需要支持超时,同样可以通过定时任务的方式来完成。

第三种Future方式。


先把Future放入队列,然后把数据放入队列,接着就在线程中进行处理,等到请求线程的其他工作处理结束后,就通过Future来获取通信结果并直接控制超时,IO线程仍然从数据队列中得到数据后再进行通信,得到结果后会把它传给Future。

第四种方式是可靠异步。可靠异步要保证异步请求能够在远程被执行,一般是通过消息中间件来完成这个保证的。

上面四种常见的异步远程通信方式,oneway是一个单向的通知,callback则是回调,是一种很被动的方式,callback的执行不是在原请求线程中,而Future是一种能够主动控制超时、获取结果的方式,并且它的执行仍然在原请求线程中,可靠异步方式能够保证异步请求在远程被执行。

7、服务提供端的设计和实现


(1)如何暴露远程服务

服务端工作有两部分,一是对本地服务的注册管理,二是根据进来的请求定位服务。


与上面请求调用端配置非常类似,但在服务提供端使用的是ProviderBean对象,而请求调用端使用的是ConsumerBean对象,此外,服务提供端增加了一个属性target。这个属性是要表明具体执行服务的SpringBean,因为ProviderBean本身并不执行具体服务,只是起到调用端代码存根的作用,所以我们需要知道真正执行服务的SpringBean是哪个,其他的例如interface、version、group等属性,与请求调用端的同名属性的含义相同。

ProviderBean的职能是什么呢?服务需要注册到服务注册查找中心后才能被服务调用者发现,所以ProviderBean需要将自己所代表的服务注册到服务注册查找中心。另外当请求调用端定位到提供服务的机器并且请求被送到提供服务的机器上后,在本机也需要有一个服务与具体对象的对应关系。ProviderBean也需要在本地注册服务和对应服务实例的关系。

(2)服务端对请求处理的流程

无论服务框架以什么方式与应用集成在一起,在启动时都需要监听服务端口,当服务注册都已完成,而且监听端口也准备好,就只需等着服务调用端的请求进来了。

服务端的通信部分同样也不能用BIO来实现,而要采用NIO方式。接到请求后,通过协议解析以及反序列化,可以得到请求发送端调用服务方法的具体信息,根据其中的服务名称、版本号找到本地提供服务的具体对象,然后在用传过来的参数调用相关对象的方法就可以了。

上面在请求调用端介绍过路由做法,提到了引入服务、方法、参数的路由、并且通过这样的方式解除了调用慢服务对于其他服务的影响,在服务提供端,我们有另外方法来解决这个问题。


上面流程会涉及两个问题:第一在网络通信层,IO线程会进行通信的处理(一般是多个IO线程),在收到完整的数据包、完成协议解析得到的序列化后的请求数据时,反序列化在什么线程进行是需要考虑的。第二得到反序列化后的信息并定位服务后,调用服务在什么线程进行也是需要考虑的。一般,调用服务一定是在工作线程(非IO线程)进行的,反序列化的工作取决于具体的实现,在IO线程或工作线程中进行的方式都有。

(3)执行不同服务的线程池隔离

服务提供端的工作线程是一个线程池,路由到本地的服务请求会被放入这个线程池执行,如果客户端没有通过接口或方法进行路由,我们可以在服务提供端进行控制,也就是进行服务端线程池隔离。具体做法类似请求调用方法根据接口、参数、方法进行的路由。在服务提供端,工作线程池不是一个,而是多个,当定位服务后,根据服务名称、方法、参数来确定具体执行服务调用的线程池是哪个。这样不同线程池之间就是隔离的,不会出现争抢线程资源情况,就好像把服务提供者的机器隔开一样。


(8)服务的升级

一旦开始使用服务框架意味着有非常多的服务落地,也意味着必须做服务的升级,对于服务升级,会遇到两种情况,一种是接口不变,只是代码本身进行完善,这样比较简单,采用灰度发布的方式验证然后全部发布就可以了,第二种是要修改原有接口。有两种:一是增加方法,比较简单,二是对接口某些方法修改调用参数列表。解决办法就是通过版本号进行解决。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值