服务调用

标签: 服务调用

服务调用:除了常用的同步服务调用之外,分布式服务框架还需要支持其他几种形式的服务调用,下面将详细介绍。

1、常见误区

    因惯性思维,很多人会将传统MVC架构或者RPC框架的做法带入到分布式服务框架的架构设计中,其中有些思想存在误区,或者已过时,它们会破坏分布式服务架构的架构品质,下面将纠正这些误区。

    1.1、NIO就是异步服务

        实际上,通信框架基于NIO实现,并不意味着服务框架就支持异步服务调用,两者本质上不是同一个层面的事情。

        在分布式服务框架中,引入NIO带来的好处是显而易见的,各种I/O对比如表1-1所示:

                                                                表1-1  几种I/O模型的功能和特性对比

 同步阻塞I/O(BIO)伪异步I/O非阻塞I/O(NIO)异步I/O(AIO)
客户端个数:I/O线程1:1M:N(其中M可以大于N)M:1(1个I/O线程处理多个客户端连接)

M:0(不需要启动额外的I/O线程,被动回调)

I/O类型(阻塞)阻塞(I/O)阻塞(I/O)非阻塞(I/O)非阻塞(I/O)
I/O类型(同步)同步(I/O)同步(I/O)同步(I/O)(I/O多路复用)异步(I/O)
API使用难度简单简单非常复杂复杂
调试难度简单简单复杂复杂
可靠性非常差

        引入NIO的优点归纳如下:

           √ 所有的I/O操作都是非阻塞的,避免有限的I/O线程因为网络、对方处理慢等原因被阻塞。

           √ 多路复用的Reactor线程模式:基于Linux的epoll和Selector,一个I/O线程可以并行处理成百上千条链路,解决了传统同步I/O通信线程膨胀的问题。

        NIO只解决了通信层面的异步问题,跟服务调用的异步没有必然关系,也就是说,即便采用BIO通信,依然可以实现异步服务调用,只不过通信效率和可靠性比较差而已。

        下面对异步服务调用和通信框架的关系进行说明,如图1-1所示:
        
                                            图1-1   服务调用和通信框架的关系

        用户发起远程调用之后,经历了层层业务逻辑处理、消息编码,最总序列化后的消息会被放入到通信框架的消息队列中。业务线程可以选择同步等待、也可以选择直接返回,通过消息队列的方式实现业务层和通信层的分离是比较成熟、典型的做法,现代的RPC框架或者Web服务器很少直接使用业务线程进行网络读写。

        通过图1-1可以看出,采用BIO还是NIO对上层的业务是不可见的,双方的汇聚点是消息队列,在Java实现中它通常就是个Queue。业务线程将消息放入到发送队列中,可以选择主动等待或者立即返回,跟通信系统是否是NIO没有任何关系。

    1.2、服务调用模式

            √ OneWay模式: 只有请求,没有应答,例如通知消息。

            √ 请求-应答模式:一请求,一应答的模式,这种模式最常见。

        OneWay模式的服务调用由于不需要返回应答,因此很容易被设计为异步的:消费者发起远程服务调用之后,立即返回,不需要同步阻塞等待应答。

        对于请求-应答模式,一般的观点都认为消费者必须要等待服务端响应,拿到结果后才能返回,否则结果从哪里取?即便业务线程不阻塞,没有获取到结果流程还是无法继续执行下去。

        从逻辑上看,上述观点没有问题。但是实际中,同步阻塞等待应答并非是唯一的技术选择,我们也可以利用Java的Future-Listener机制来实现异步服务调用。从业务角度看,它的效果与同步等待等价,但是从技术层面看,却是个很大的进步,他可以保证业务线程在不同步阻塞的情况下实现同步等待的效果,服务执行效率更高。

    1.3、异步服务调用性能更高

        通常在实验室环境中测试,由于网络时延小、模拟业务又通常比较简单,所以异步服务调用并不一定性能更高,但是在生产环境中,异步服务调用往往性能更高、可靠性也更好。主要原因是网络环境相对恶劣,真时的服务调用耗时更多等,这种恶劣的运行环境正好能够发挥异步服务调用的优势。

2、服务调用方式

    服务框架支持多种形式的服务调用,本节将对这集中服务调用的原理和设计进行讲解。

    2.1、同步服务调用

       它的工作原理如下:客户端发起远程服务调用请求,用户线程完成消息序列化之后将消息投递到通信框架,然后同步阻塞,等待通信框架发送请求并接受应答之后,唤醒同步等待的用户线程,用户线程获取到应答之后返回。

        它的工作原理如图1-2所示:
        
                                                    图1-2  同步服务调用

        1)、消费者调用服务端发布的接口,接口调用由分布式服务框架包装成动态代理,发起远程服务调用。

        2)、消费者线程调用通信框架的消息发送接口之后,直接或者间接调用wait()方法,同步阻塞等待应答。

        3)、通信框架的I/O线程通过网络将请求消息发送给服务端。

        4)、服务端返回应答消息给消费者,由通信框架负责应答消息的反序列化。

        5)、I/O线程获取到应答消息之后,根据消息上下文找到之前同步阻塞的业务线程,notify()阻塞的业务线程,返回应答给消费者,完成服务调用。

        为了防止服务端长时间不返回应答消息导致客户端用户线程挂死,用户线程等待的时候需要设置超时时间,这个超时时间与服务端或者客户端配置的超时时间对应。

    2.2、异步服务调用

        基于JDK的Future机制,可以非常方便地实现异步服务调用。

        JDK原生的Future主要用于异步操作,它代表了异步执行的结果,用户可以通过调用它的get方法获取结果。如果当前操作没有执行完,get操作将阻塞调用线程。

        在实际项目中,往往会扩展JDK的Future,提供Future-Listener机制,它支持主动获取和被动异步回调通知两种模式,适用于不同的业务场景。

        以Netty的Future接口定义为例,新增了监听器管理接口,监听器主要用于异步通知回调。

        异步服务调用工作原理如图1-3所示:
        
                                    图1-3   异步服务调用工作原理

        1)、消费者调用服务端发布的接口,接口调用由分布式服务框架包装成动态代理,发起远程服务调用。

        2)、通信框架异步发送请求消息,如果没有发生I/O异常,返回。

        3)、请求消息发送成功之后,I/O线程构造Future对象,设置到RPC上下文中。

        4)、用户线程通过RPC上下文获取Future对象。

        5)、构造Listener对象,将其添加到Future中,用于服务端应答异步回调通知。

        6)、用户线程返回,不阻塞等待应答。

        7)、服务端返回应答消息,通信框架负责反序列化。

        8)、I/O线程将应答消息,设置到Future对象的操作结果中。

        9)、Future对象扫描注册的监听器列表,循环调用监听器的operationComplete方法,将结果通知给监听器,监听器获取到结果之后,继续后续业务逻辑的执行,异步服务调用结束。

    需要指出的是,还有另外一种异步服务调用形式,就是不添加Listener,用户连接发起N次服务调用,然后依次从RPC上下文中获取Future对象,最终再主动get结果,业务线程阻塞,相比于老的同步服务调用,它的阻塞时间更短,其工作原理如图1-4:

                           图1-4   异步服务调用主动get结果原理图

    异步服务调用的代码实例如下:

xxxService1.xxxMethod(Req);
Future f1 = RpcContext.getContext().getFuture();
xxxService2.xxxMethod(Req);
Future f2 = RpcContext.getContex().getFuture();
Object xxResult1 = f1.get(3000);
Object xxResult2 = f2.get(3000);

    假如xxxService1和xxxService2发布成异步服务,则调用xxxMethod方法之后当前业务线程不阻塞,立即返回null,用户不能直接使用它的返回值,而是通过当前线程上下文RpcContext获取异步操作结果Future。获取到Future之后继续发起其他异步服务调用,然后获取另一个Future....最后,通过Future的get方法集中获取结果。无论是多少Future,采用此种方法用户线程最长阻塞时间为耗时最长的Future,即T = Max(t(future*));如果是同步调用,用户线程阻塞时间T = t(future1) + t(future2)+... ...+t(futureN)。

    异步服务调用相比于同步服务调用有两个优点:

        √ 化串行为并行,提升服务调用效率,减少业务线程阻塞时间。

        √ 化同步为异步,避免业务线程阻塞。

    异步服务调用效果如图1-5所示:
    
                                                          图1-5  异步服务调用场景

     采用异步服务调用模式,最后调用三个服务异步操作结果Future的get方法同步等待应答,他的总执行时间T=Max(T1,T2,T3),相对于同步服务调用,性能提升效果非常明显。

       第二种基于Future-Listener的纯异步服务调用,它的代码示例如下:

xxxService1.xxxMethod(Req);
Future f1 = RpcContext.getContext().getFuture();
Listener l = new xxxListener();
f1.addListener(1);
......后续代码省略

        基于Future-Listener的异步服务调用相比于Future-get模式更好,但是实际使用中有一定的局限性。

    2.3、并行服务调用

        在大多数业务应用中,服务总是被串行地调用和执行,例如A调用B服务,B服务调用C服务,最后形成一个串行服务调用链:A-->B服务-->C服务-->......

        串行服务调用比较简单,但是一些业务场景中,需要采用并行服务调用来降低E2E的时延。

            √ 多个服务之间逻辑上不存在互相依赖关系,执行先后顺序没有严格的要求,逻辑上可以被并行执行。

            √ 长流程业务,调用多个业务,对时延比较敏感,其中有部分服务逻辑上无上下文关联,可以并行调用。

        并行服务调用的目标主要有两个:

            1)、降低业务E2E时延。

            2)、提升这个系统的吞吐量。

        要解决串行调用效率低的问题,有两个解决对策:

            1)、异步服务调用。

            2)、并行服务调用。

        并行服务调用的原理:一次同时发起多个服务调用,先做流程的Fork,在利用Future等主动等待获取结果,进行结果汇聚Join。实现并行服务调用的集中技术方案:

            √ JDK7的Fork/Join,可以实现子任务的并行执行和结果汇聚。

            √ BPM的Paraller Gateway(并行网关)。

            √ 批量串行服务调用。

    JDK7的Fork/Join底层会开启多个线程来分解任务,在服务框架中使用会导致依赖线程上下文传递的变量丢失、线程膨胀不可控等问题,因此在并行服务调用时不适合使用JDK的Fork/Join并行执行框架。

    BPM流程引擎支持并行流程(子流程)调用,它的执行示意图如图1-6所示:
    
                          图1-6  BPM Parallerl GateWay 工作流程

     Paraller Gateway(并行网管)能在一个流程里用来对并发建模。在一个流程模型里引入并发最直接的网关就是并行网管(Parallel Gateway),它允许Fork执行多个路径,或者Join多个执行的到达路径。

      并行网管的功能基于即将到达的和即将离开的流程顺序流。

        √ Fork:所有即将离开的顺序流将将以并行方式,为每个顺序流程建立一个并发执行器。

        √ Join:所有的并发执行达到并行网关,在网关里面等待直到每个来到的顺序流的执行到达,条件满足后流程继续通过合并网关。

        从技术上看,不同的BPM流程引擎具体实现细节也不同,但大多数都支持:通过创建子线程的方式实现并行调用、通过批量调用的方式实现伪异步并行调用。对于服务框架而言,BPM Parallel Gateway的功能可以满足需求,但是为了并行服务调用引入BPM流程引擎显然是得不偿失,我们可以参考Parallel Gateway的伪异步并行调用来实现服务框架的并行服务调用。

        下面对批量串行服务调用实现并行服务调用的原理进行说明,如图1-7所示:
        
                                                    图1-7  批量服务调用原理图

        1)、服务框架提供批量服务调用接口供消费者使用,他的定义样例如下:parallelService.invoke(serviceName[], methodName[],args[])

           2)、平台的并行服务调用器创建并行Future,缓存批量服务调用上下文信息。

           3)、并行服务调用器循环调用普通的Invoker,通过循环的方式执行单个服务调用,获取到单个服务的Future之后设置到Parallel Future中。

           4)、返回Parallel Future给消费者。

           5)、普通Invoker调用通信框架的消息发送接口,发送远程服务调用。

           6)、服务端返回应答,通信框架对报文做反序列化,转换成业务对象更新Parallel Future的结果列表。

           7)、消费者调用Parallel Future的get(timeout)方法,同步阻塞,等待所有结果都返回。

           8)、Parallel Future通过对结果集进行判断,看所有服务调用是否都已经完成(包括成功、失败和异常)

           9)、所有批量服务调用结果都已经返回,Notify消费者线程,消费者获取到结果列表,完成批量服务调用,流程继续执行。

        通过批量服务调用+Future机制,我们实现了并行服务调用,而且没有创建新的线程,用户不用担心依赖线程上下文的功能出异常。该方案唯一的缺点就是用户需要调用平台提供的并行服务调用接口,这个会导致API层面的依赖,对于努力构建零依赖的服务框架而言不是最优的选择。但是零依赖事实是不存在的,即便100%XML配置也是一种配置依赖,所以在设计过程中要能够识别并抓主要矛盾点,做到有所舍,否则设计工作将步履维艰。

    2.4、泛化调用

        泛化调用通常包含两种模式:泛化引用和泛化实现。泛化引用主要用于客户端没有API接口及数据模型的场景,参数及返回值中的所有POJO均用Map表示,通常用于框架继承,比如实现一个通用的服务测试框架。泛化实现主要用于服务端没有API接口及数据模型的场景,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如实现一个通用的远程服务Mock框架。

        泛化调用的设计要点如下:

           1)、分布式服务框架提供泛化接口,供服务提供者实现和消费者引用,它的参数定义如下:

      public interface GenService{
         Object invoke (String methodName, String[] paramTypes, Object[] args);
      }

           2)、消费者引用泛化接口,则直接将请求参数转换成Map,应答消息也自动转换成Map。

           3)、服务提供者如果使用泛化实现发布服务,则自动将请求参数转换成Map,调用GenService的泛化实现类,应答消息自动包装成Map返回。

        泛化调用由于比较灵活,没有服务契约,在实际项目中慎用,它通常用于测试集成、系统上线之后的回声测试等。

总结:服务框架往往支持多种形式的调用,我们在设计服务调用时,需要充分考虑用户的使用习惯以及业务面临的主要挑战,在矛盾中做出平衡和取舍,这是一个优秀架构师的基本功。

版权声明:本文为zhengzhaoyang122原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zhengzhaoyang122/article/details/80981991