服务框架 分布式服务框架

分布式服务框架

1. 前言html

几年前,我就一直想着要设计一款本身的实时通信框架,因而出来了TinySocket,她是基于微软的SocketAsyncEventArgs来实现的,因为此类提供的功能很简洁,因此当时本身实现了缓冲区处理,粘包拆包等,彼时的.net平台尚未一款成熟的即时通信框架出来,因此当这款框架出来的时候,将当时公司的商业项目的核心竞争力提高至行业前三。可是后来随着.net平台上愈来愈多的即时通信框架出来,TinySocket也是英雄暮年,通过了诸多版本迭代和诸多团队经手,她不只变得臃肿,并且也不符合潮流。总体的重构势在必行了。可是我还在等,在等一款真正的即时通信底层库出来。java

都说念念不忘,必有回响。经过不停的摸索后,我发现了netty这套底层通信库(对号入座,.net下对应的是dotnetty),凭借着以前的经验,第一感受这就是我要找寻的东西。后来写了一些demo完全印证了个人猜测,简直是欣喜若狂,想着若是早点发现这个框架,也许就不会那么被动的踩坑了。就这样,我算是开启了本身的netty之旅。linux

微言netty系列,就是个人netty之旅的一些产出,它结合了我过往的经验来产出一些对你们有用的东西,但愿不会让你们失望。redis

注:本文原理讲解并不是以某一种语言为主,可是对于具体场景分析,用的是Java,读者能够类推到其余语言。同时本文并不提供源码级别的原理性讲解,如读者有兴趣,能够自行查找实践。apache

2. 总体架构模型api

言归正传,咱们继续netty之旅吧。promise

分布式服务框架,特色在于分布式,功能在于服务提供,目标在于即时通信框架整合。因为其可以让服务端和客户端进行解耦,让调用方和被调用方处于网络的两端可是通信毫无障碍,从而可以扩充总体的业务规模。对于一些业务场景稍微大一些的公司,通常都会采用分布式服务框架。包括目前兴起的微服务设计,更是让分布式服务框架煊赫一时。那么咱们今天的目标,就是来打造一款手写的分布式服务框架TinyWhale,中文名巨小鲸(手写做品,本文讲解专用, 暂无更多精力打形成开源^_^),接下来让咱们开始吧。缓存

说道目前比较流行的分布式服务框架,朗朗上口的有Dubbo,gRpc,Spring cloud等。这些框架无一例外都有着以下图所示的总体架构模型:服务器

3cd145d7-1bad-4740-ab97-5615b04e03c5

总体流程解释以下:网络

1. 启动注册,指服务端开始启动并将服务注册到注册中心中。注册中心通常用zookeeper或者consul实现。

2. 启动并监听,指客户端启动并监听注册中心的服务列表。

3. 有变动则通知,指客户端订阅的服务列表发生改变,则将更新客户端缓存。

4. 接口调用,指客户端进行接口调用,此调用将首先会向服务端发起链接操做,而后进行鉴权,以后发起接口调用操做。

5. 客户端数据监控,指监控端会监控客户端的行为和数据并作记录。

6. 服务端数据监控,指监控端会监控服务端的行为和数据并作记录。

7. 数据分析并衍生出其余业务策略,指监控端会根据服务端和客户端调用数据,来衍生出新的业务策略,好比熔断,防刷,异地多活等。

固然,上面的流程是比较标准的分布式服务框架所涉及的环节。在实际设计过程当中,能够根据具体的使用方式进行调整,好比监控端只监控服务端数据,由于客户端我不用关心。或者客户端不设置服务地址列表缓存,每次调用前都从注册中心从新获取最新的服务地址列表等。

TinyWhale,因为设计的初衷是简单,可靠,高性能,因此这里咱们去除了监控端,因此流程5,流程6,流程7都会拿掉,若是有须要使用到监控端的,能够自行根据提供的接口来实现一套,这里将再也不对监控端作过多的赘述。

3. 即时通信框架设计涉及要素

编解码设计

编解码设计任何通信类框架,编解码处理是没法绕过的一个话题。由于网络上只能流淌字节流,因此这种特性催生了不少的框架。因为这块的工具很是多,诸如ProtoBuf,Marshalling,Msgpack等,因此喜欢用哪一个,全凭喜爱。这里我用使用ProtoStuff来做为咱们的编解码工具,缘由有二:其一是易用性,无需编写描述文件;其二是高性能,性能属于T0级别梯队。下面来具体看看吧:

首先看看咱们的编解码类:

a1c7ca52-390c-497a-964e-143e980963f7

其中serialize方法,用于将类对象编码成字节数据,而后经过本机发送出去。而deserialize方法,则用于将缓冲区中的字节数据还原为类对象。考虑到设计的简洁性,我这里并未抽象出一个公共的codecInterface和codecFactory来适配不一样的编解码工具,你们能够自行来进行设计和适配。

有了编解码的辅助类了,如何集成到Netty中呢?

在Netty中,将对象编码成字节数据的操做,咱们可使用已有的MessageToByteEncoder类来进行操做,继承自此类,而后override encode方法,便可利用本身实现的protostuff工具类来进行编码操做。

40907776-cbc0-4888-9c94-6f4e28fbdec2

一样的,将字节数据解码成对象的操做,咱们可使用已有的ByteToMessageDecoder类来进行操做,继承自此类,而后override decode方法,便可利用本身实现的protostuff工具类来进行解码操做。

粘包拆包设计

以前章节已经讲过,咱们直接拿来展现下。

粘包拆包,顾名思义,粘包,就是指数据包黏在一块了;拆包,则是指完整的数据包被拆开了。因为TCP通信过程当中,会将数据包进行合并后再发出去,因此会有这两种状况发生,可是UDP通信则不会。下面咱们以两个数据包A,B来说解具体的粘包拆包过程:

bb0a0099-2e12-4191-be8a-1d4c031552be

第一种状况,A数据包和B数据包被分别接收且都是整包状态,无粘包拆包状况发生,此种状况最佳。

f9b580f9-bd49-4dd0-b9e7-742e63f6276f

第二种状况,A数据包和B数据包在一起且一块儿被接收,此种状况,即发生了粘包现象,须要进行数据包拆分处理。

a1f0cd86-ac4d-45a7-b79f-20c8dba93fed

第三种状况,A数据包和B数据包的一部分先被接收,而后收到B数据包的剩余部分,此种状况,即发生了拆包现象,即B数据包被拆分。

a1cfac00-7f70-4183-a57e-becbb95ac9e1

第四种状况,A数据包的一部分先被接收,而后收到A数据包的剩余部分和B数据包的完整部分,此种状况,即发生了拆包现象,即A数据包被拆分。

fd5757c1-63dd-4c8b-a28d-24c9c4b88fe9

第五种状况,也是最复杂的一种,先收到A数据包的部分,而后收到A数据包剩余部分和B数据包的一部分,最后收到B数据包的剩余部分,此种状况也发生了拆包现象。

至于为何会发生这种问题,根本缘由在于缓冲区中的数据,Server端不大可能一次性的所有发出去,Client端也不大可能一次性正好把数据所有接收完毕。因此针对这些发生了粘包或者拆包的数据,咱们须要找到合适的手段来让其造成整包,以便于进行业务处理。好消息是,Netty已经为咱们准备了多种处理工具,咱们只须要简单的动动代码,就能够了,他们分别是:LineBasedFrameDecoder,StringDecoder,LengthFieldBasedFrameDecoder,DelimiterBasedFrameDecoder,FixedLengthFrameDecoder。

因为上节中,咱们讲解了其大概用法,因此这里咱们以LengthFieldBasedFrameDecoder来着重讲解其使用方式。

LengthFieldBasedFrameDecoder:顾名思义,固定长度的粘包拆包器,此解码器主要是经过消息头部附带的消息体的长度来进行粘包拆包操做的。因为其配置参数过多(maxFrameLength,lengthFieldOffset,lengthFieldLength,lengthAdjustment,initialBytesToStrip等),因此能够最大程度的保证能用消息体长度字段来进行消息的解码操做。这些不一样的配置参数能够组合出不一样的粘包拆包处理效果,在此Rpc框架的设计过程当中,个人使用方式以下:

be24597e-daed-4c17-bad8-7c78bd29bc63

是否是代码很简单?

翻阅LengthFieldBasedFrameDecoder源码,实现原理尽收眼底,因为网上讲解足够多,并且源码中的讲解也足够详细,因此这里再也不作过多阐释。具体的原理解释能够看这里:LengthFieldBasedFrameDecoder

自定义协议设计

在进行网络通信的时候,数据包从一端传输到另外一端,而后被解析,被消化。这里就涉及到一个知识点,数据包是怎样定义的,才能让另外一方识别出数据包所表明的业务含义。这就涉及到自定义传输协议的设计,咱们来看看具体怎么设计。

首先,咱们须要明确本身定义的协议须要承载哪些业务数据,通常说来包含以下的业务要点:

1. 自定义协议须要让双端识别出哪些包是心跳包

2. 自定义协议须要让双端识别出哪些包是鉴权包

3. 自定义协议须要让双端识别出哪些包是具体的业务包

4. 自定义协议须要让双端识别出哪些包是上下线包等等(本条规则适用于IM系统)

不一样的系统在设计的时候,自定义协议的设计是不同的,好比分布式服务框架,其业务包则须要包含客户端调用了哪一个方法,入参中传入了哪些参数等。物联网采集框架,其业务包则须要包含底层采集硬件上传的数据中,哪些数值表明空气温度,哪些数值表明光照强度等。一样的,IM系统则须要知道当前的聊天是谁发出的,想发给谁等等。正是因为不一样系统承载的业务不一样,因此致使自定义协议种类繁多,不一而足。性能表现也是错落有致。复杂程度更是简繁并举。

那么针对要讲解的分布式服务框架,咱们来详细看一下设计方式。

首先定义一个NettyMessage泛型类,此泛型类是一个基础类,包含了会话ID,消息类型,消息体三个字段。这三个字段是服务端和客户端进行数据交换过程当中,必传的三个字段,因此总体抽取出来,放到了这里。

98e12d4a-8932-4bd6-a2d0-420484b271e4

而后,针对客户端,定义一个NettyRequest类,包含基本的请求ID,调用的类名称,方法名称,入参类型,入参值。

e7c0174d-2858-40e7-a103-462879edc247

最后,客户端的请求传送到服务端,服务端须要反射调用方法并将结果返回,服务端的NettyResponse类,则包含了请求ID,用于识别请求来自于哪一个客户端,error错误,result结果三个字段:

78f8dee2-8f31-4a91-b1e1-99b1bd3d4950

当服务端调用完毕,就会把结果封装到此类中,而后将结果返回给客户端,客户端还原此类,便可拿出本身想要的数据来。

那么这个稍显冗杂的自定义协议就设计完毕了,有人会问,心跳包用这个协议如何识别呢?其实直接实例化NettyMessage类,而后在其type字段中塞入心跳标记值便可,相似以下:

1b082d53-92e8-4fea-8fa3-420d80d3a5c0

而上下线包和鉴权包则也是相似的构造,不通点在于,鉴权包 可能须要往body属性里面放一些鉴权用的用户token等。

鉴权设计

顾名思义,就是进行客户端登陆的认证操做。因为客户端不是随意就能链接上来的,因此须要对客户端链接的合法性进行过滤操做,不然很容易形成各类业务或者非业务类的问题,好比数据被盗窃,服务器被压垮等等。那么通常说来,如何进行鉴权设计呢?

e7876f6b-6221-4a28-9e57-2582a8d8e07e

能够看到,上面的鉴权模块里面有三个属性,一个是已登陆的用户列表clientList,一个是用户白名单whiteIP,一个是用户黑名单blackIP,在进行用户认证的时候,会经过用户token,白名单,黑名单作验证。因为不一样业务的认证方式不同,因此这里的设计方式也是五花八门。通常说来,分布式服务框架的认证方式依赖于token,也就是服务端的provider启动的时候,会给当前服务分配一个token,客户端进行请求的时候,须要附带上这个token才可以请求成功。因为我这里只是作演示效果,并未利用token进行验证,实际设计的时候,能够附带上token验证便可。

心跳包设计

传统的心跳包设计,基本上是服务端和客户端同时维护Scheduler,而后双方互相接收心跳包信息,而后重置双方的上下线状态表。此种心跳方式的设计,能够选择在主线程上进行,也能够选择在心跳线程中进行,因为在进行业务调用过程当中,此种心跳包是没有必要进行发送的,因此在必定程度上会形成资源浪费。严重的甚至会影响业务线程的操做。可是在netty中,心跳包的设计并不是按照如上的方式来进行。而是经过检测链路的空闲与否在进行的。链路分为读操做空闲检测,写操做空闲检测,读写操做空闲检测。若是一段时间没有收到客户端的信息,则会触发服务端发送心跳包到客户端,称为读操做空闲检测;若是一段时间没有向客户端发送任何消息,则称为写操做空闲检测;若是一段时间服务端和客户端没有任何的交互行为,则称为读写操做空闲检测。因为空闲检测自己只有在通道空闲的时候才进行检测,而不是固定频率的进行心跳包通信,因此能够节省网络带宽,同时对业务的影响也很小。

那么就让咱们看看在netty中,怎么实现高效的心跳检测吧。

在netty中,进行读写操做空闲检测,须要引入IdleStateHandler类,而后须要咱们实现本身的心跳处理Handler,具体设计方式以下:

首先,引入IdleStateHandler和服务端心跳处理Handler

fb267adc-dec0-4f36-9d35-055d0d960f0e

其中读空闲检测为45秒,写空闲检测为45秒,读写空闲检测为120秒,也就是说,若是服务器45秒没有收到客户端发来的消息,就会触发一个回调事件,另外两个同理。具体触发什么事件了呢?咱们来看看服务端心跳处理Handler:HeartBeatResponseHandler

fe66f758-317f-456a-8ba0-1d83fcdc188d

能够看到,检测到读空闲,会调用processReadIdle方法来处理,咱们进来看看具体处理方式:

62e323ee-003a-4aa2-a995-28649b91a5c8

能够看到,服务端发现一段时间没收到客户端消息后,就会主动给客户端发一次心跳,确认客户端是否存活。若是在第90秒内尚未收到客户端的回复心跳,则会尝试再发一条,同时在客户端上下线状态表中,将当前客户端的未响应次数加一;若是在第135后认为收到客户端的回复心跳,则会尝试重发一条,同时未响应次数再加一,当次数累积到三次的时候,则认为此客户端掉线,此时将会踢掉此客户端。若是是IM系统的话,此时服务端就能够将此客户端的信息告知其余在线用户掉线,这样其余用户就能够在本身的客户端列表中删掉掉线用户。

至于processWriteIdle和processAllIdle方法,均是如上相似原理,至于须要处理,怎么去处理,均是业务本身定制,至关灵活。

很遗憾,在翻阅不少基于Netty的源码中,并未发现此样的实现方式,这也是至关惋惜的。

断线重连设计

在实际网络通信过程当中,客户端可能因为网络缘由未能及时的响应服务端的心跳请求,从而被服务端踢下线。之全部有这种机制,一方面是为了节省服务端资源,剔除死连接;另外一方面则是出于业务要求,好比IM系统中,用户掉线了,可是服务端没有及时剔除,会致使其余用户认为此用户在线,从而可能形成误解等。

那么就须要有一种机制来保证客户端网络掉线后,可以及时的感知并进行重连,从而保证服务的可用性。以前咱们介绍了心跳包,它是专门用来保持服务端和客户端的通道链接保持的。假设当客户端由于网络缘由,被服务端踢下线后,客户端是无感知的,并不知道本身已经被服务端踢下线,因此这时候若是客户端依旧向服务端发送数据,将会失败。此时这就是断线重连应该工做的地方了。具体设计以下:

75ddab25-f90a-45c7-bf11-d456fc6f1489

能够看到,咱们依旧用了netty原生的IdleState类来检测空闲通道。当客户端一段时间没收到服务端的消息,将会首先尝试给服务端发送一次心跳,因为此时客户端已经被服务端踢掉了,因此三次心跳均未得到回应,此时,客户端忽然想明白了:“哦,我想我已经掉线了”。因而客户端将会利用ctx对象进行服务端重连操做。

此种方式简单易行,虽然不具备实时性,可是效果很好,能够有效地避免由于网络抖动等未知缘由致使的掉线问题。

以上几种特性,是设计通讯框架过程当中,基本上都绕不开。虽然不一样的通讯框架因为承载的业务不一样而形成设计上的差别,可是正是由于这些特性的存在,才能保证整个通讯过程当中的稳定性和可靠性。

接下来咱们将焦点转移到服务端和客户端的设计上来。

先说说服务端和客户端,基本上的通信模型为,服务端bind本地端口,而后进行listen监听。客户端connect服务端套接字,而后进行通信。用netty打造的双端,也绕不开这种通信模型。其实若是读者有过通讯框架的设计经验的话,将会对此十分熟悉。不过就通信方式来讲,也是很统一的,通常都是一端发送数据,另外一端接收处理,而后看具体业务再决定需不须要返回数据回去。那么这里就涉及到一个要点,由于数据的返回有同步和异步之分,通常说来同步等待数据返回的性能要比异步获取数据的性能要差一些,可是具体能差别多少,彻底由设计者本身把握。

同步等待数据返回这块,我就无需多说了,基本上就是以下示例代码:

3c1d4c35-e7cd-4897-925e-760c2eed0521

异步获取返回数据这块,则设计上要复杂一些,由于设计方式是多种多样的。有用双Queue来作异步化(任务quque和应答queue), 有用Future来作异步化,固然也有用多线程来作异步化等。TinyWhale的异步化处理,采用的是后者,在客户端讲解那块,将会作详细的解释。

再说说netty框架,因为其纯异步化模型,因此获取的各类结果对象基本上是各类Future,若是以前对这种模型接触比较少的话,将会不太习惯netty的这种设计思惟。具体的使用方式,将会在接下来的设计中进行详细讲解。

服务端设计

首先说道服务端,是指提供服务的一方,通常用来处理客户端请求。因为netty这块,已经将底层封装的特别好,因此这里无需多余设计,只须要了解netty的异步模型便可。那么何为netty的异步模型呢?

既然说到了同步异步,那么难免就会提起阻塞非阻塞,我就说下我的的理解吧。同步异步的区别,我的认为,只要不是一个时间只能作一件事儿的,都可称为异步。实现异步有多种方式,而多线程只是异步的一种实现方式而已。好比咱们用两个queue模拟生产消费行为,也能够称之为异步。阻塞非阻塞的区别,我的认为,主要体如今对资源的争抢等待上面,发生了资源争抢等待,则被阻塞,反之为非阻塞。好比http请求远程结果,阻塞等待等。我的意见,若有谬误,还请指教。接下来让咱们进入正题。

首先要从同步阻塞模型提及。

同步阻塞 

相信你们都据说过这个模型,客户端请求到服务端,服务端里面有个Acceptor接收请求,而后针对每一个请求都建立一个Thread来处理,典型的一对一通讯处理方式。看下具体的模型示意图:

32c59e26-a4ef-4c46-b192-8985c8fa5142

首先,客户端请求达到Acceptor,Acceptor接收并处理,而后Acceptor为每一个请求建立一个线程来处理。这样后续的请求处理工做就在各自的线程上进行处理了。此种方式最简便,代码也很是好写,可是带来的问题就是一个请求对应一个线程,没法作到高性能,并且因为线程开销较大,对服务器的稳定运行也有必定的影响,随时都有可能出现内存耗尽,建立线程失败等,最终的结果就是由于宕机等缘故形成生产问题。

因为上述问题,后来产生了伪异步处理模型,其实就是讲Acceptor里面为每一个请求分配一个线程,改为了线程池这种池化方式来处理,整体上性能比以前要好不少,并且机器运行也稳定不少,相对以前的模型,有了不小的提高。可是从本质上来将,此种方式和以前方式相比,并未有质的改变,之因此称为伪异步,原因在此吧。

非阻塞

同步阻塞模型因为性能很差,可靠性低,因此催生了非阻塞模型的产生。目前非阻塞模型有两种,一种是NIO,另外一种是AIO,然而AIO虽能够称得上为真正的异步非阻塞IO模型,代码也很简便,可是并未大规模的应用,料想应该有它自身的短板,因此咱们着重来说解NIO模型。首先来看看NIO模型示意图:

938a5b17-bc55-45cb-be25-87a3b550e824

上面这幅图是网上流传比较广的一幅图,由于被你们所熟知,因此这里我就直接拿来用了,这幅图的出处在这里。具体来看一下。

首先,从图中能够看出,client为客户端请求,mainReactor主要接收客户端请求,而后调用acceptor进行处理,acceptor查到已经就绪的链接,则交由subReactor进行处理。subReactor这里会负责已链接客户端的读写网络操做,也就是若是有读写操做,会反映到subReactor中来,至于业务处理部分,则直接扔给ThreadPool进行业务处理。通常说来,subReactor的个数大概和CPU的核数是一致的。从这里还能够看出mainReactor和subReactor都有派发器的意味。

因为此NIO模型使用了事件驱动,并且以linux底层做为通信支持,彻底使用了epoll高性能的特色,因此总体表现堪称完美。这里我要推荐一座金矿,大名鼎鼎的C10k问题,诸位看官若是有兴趣,能够探索一番。

而后来具体说下服务端设计吧:

  服务端源码

因为代码作了具体的注释,我这里就不针对性的进行解释了。须要注意的是,当服务启动以后,会注册两个监听器,一个绑定时间监听,一个关闭事件监听,当事件被触发的时候,会回调两个事件内部的逻辑。最后服务端正常启动,会被注册到注册中心中,以便于客户端调用。须要注意的是,通常状况下,业务Handler最好和心跳包Handler等非业务性的Handler处理分开,避免业务高峰时期,由于心跳包等Handler的处理来耗费捉襟见肘的内存资源或者CPU资源等,形成服务器性能降低。来看一下ServerHandler的具体设计:

5ca2ea97-bcac-45bb-8c22-49ca6599a2c3

从这里能够看出,咱们用了一个线程池来将业务处理进行池化,这样作就不会受到心跳包等其余非业务处理Handler的影响,最大限度的保证系统的稳定性。

更多关于同步异步,阻塞非阻塞的设计,请参见Doug Lea:Scalable IO in Java

客户端设计

再来讲说客户端,是指消费服务的一方,通常用来实现特定的消费业务。一样的,netty这块已经将底层封装的很好,因此直接编写业务便可。和编写服务端不一样的是,这里不须要分BossGroup和WorkerGroup,由于对于客户端来讲,只须要链接服务端,而后发送数据并监听便可,不存在影响性能的问题。具体的写法看看吧:

  客户端源码

因为代码中,我也作了诸多的注释,因此这里再也不一一解释。须要注意的是,和编写服务端相似,我这里添加了两个监听事件,监听链接成功事件,监听关闭事件,响应的业务场景若是触发了这两个事件,将会执行事件内部的逻辑。

这里须要提一下消息发送的场景。通常说来,客户端向服务端发送数据,而后服务端处理功能后返回给客户端,客户端接收到消息后再进行后续处理。这个流程通常有两种实现方式,一种是同步的实现方式,另外一种是异步的实现方式,具体来呈现如下:

首先是同步实现方式,顾名思义,客户端发送数据给服务端,服务端在处理完毕并返回数据以前,客户端一直处于阻塞等待状态,send方法的代码设计以下:

e0387579-7276-4d74-bd83-31c1d974ede2

来看看clientHandler里面的sendMessage方法:

4b2d29fd-55fa-4159-873c-0a9ea9cd553a

在开始发送以前,咱们先拿到当前ctx的promise句柄,而后将数据写入到缓冲区,最后将此句柄返回给send方法,send方法接收到此句柄后,将会等待promise执行完毕,如何判断promise执行完毕呢?当客户端接收到服务端返回,就能够将promise置为完成状态:

677e5ed7-63f4-4e75-a7bd-763304e1dcd4

能够看到,经过重置promise的setSuccess方法,便可将promise置为完成态,这样操做以后,send方法里面就能够正常的拿到数据并返回了。不然将会一直处于阻塞状态。

能够看到,在netty中实现阻塞的方式来接收服务端返回,处理起来仍是挺麻烦的,根本缘由在于netty彻底异步化的模型,因此只能用如上的方式来进行同步化处理。

再来讲说异步化处理吧, 这也是netty很推崇的方式。

首先来看看send方法:

9d017228-d265-4b58-b735-9618856758a5

从上面代码中能够看到,当咱们将消息发送出去后,会当即得到一个TinyWhaleFuture的句柄,不会再有阻塞等待的场景。咱们看看clientHandler.sendMessage的具体实现:

2974eedd-4e1c-46c0-96a7-1735a4ad1916

能够看到,只是单纯的将数据推送到缓冲区而已。

还记得咱们的TinyWhaleFuture句柄吗?既然返回给咱们了这个句柄,那么咱们确定是能够今后句柄中取出咱们想要获取的数据的,咱们看看客户端若是收到服务端的返回结果,该如何处理呢?

50149181-14be-4cce-9fdc-1393e0cd70b3

能够看到,这里利用了一个Map来保存用户每一个发送请求,一旦当服务端返回数据后,就会将请求置为完成态,同时从Map中将已完成的操做删掉。这样,客户端拿到TinyWhaleFuture句柄后,经过提供的get方法便可在想获取结果的地方来获取返回结果。这样作,是不会阻塞其余业务执行的。

其实不只仅是netty中,在设计其余框架的时候,也能够利用此思想来实现真正意义上的异步执行逻辑。固然,可以实现这种执行逻辑的方式有不少种,至于更好的实现方式,还请君细细斟酌吧,这里只起到抛砖引玉的做用。

4. 动态调用设计

服务注册和服务发现

先来上个大体的类设计图,ServerCache接口提供基础的本地缓存操做;ServerBase提供基础的链接注册中心,关闭注册中心链接操做;ServerRegistry为服务注册类;ServerDiscovery为服务发现类,下面是类UML图,咱们来具体的说一说:

33f8e329-fe57-4223-8fb9-8e61865ce163

首先是注册中心,这个就没必要说了,通常都是使用zookeeper或者consul等框架来实现,这里咱们使用zookeeper。可是咱们这里并非用原生的zookeeper sdk来操做,而是使用curator来操做,curator是什么呢?在其介绍页面有句很经典的话:Guava is to Java what Curator is to Zookeeper,至关的简洁明了吧。来看下具体的使用方式吧。

首先定义用于加载注册中心服务套接字的共享缓存,客户端启动的时候,此共享缓存会从注册中心拉取服务器列表到本地保存:

1bbffe83-acca-4c28-962b-20c9ea286b3a

而后,定义服务治理的公共操做类:

8074c8cc-fb0c-44ca-9941-95ba65d32c19

能够看到,此基类中,open方法和close方法,用于链接zk服务器,关闭和zk服务器的链接。以后即是对接口中操做本地缓存的实现。

因为服务治理这块包含了服务注册和服务发现功能,因此这里,咱们分别定义ServerRegistry类和ServerDiscovery类来进行处理。

ServerRegistry类,顾名思义,表示服务注册,也就是当咱们的服务端启动以后,绑定了本机端口以后,会将承载的服务注册到zk中。

44220ee6-9d3b-44af-b06f-546ea2771065

ServerDiscovery类,顾名思义,服务发现,那么此类中的discovery方法则就是根据用户传入的接口名称来找到对应的服务器,而后将结果返回。须要注意的是,服务发现的过程,须要涉及到负载均衡,之因此涉及到这个,主要是为了让每台服务器收到的请求均匀一些,以达到均衡的目的,这样就不会由于请求打的不均匀致使有些服务器负载太大,有些服务器负载几乎没有的状况。负载均衡,我将在后面的章节讲解,先继续看看服务发现这块:

43f13e2c-9f5e-4bd5-89a9-8998f9f92923

能够看到,我用了一个watchNode方法来检测节点的改动,此方法内部设置了一个Listener,只要有节点的改动,都会推送到此Listener中,而后我就能够根据改动的类型来决定是否对本地缓存进行更新操做。

更具体的服务注册和服务发现使用方式,能够参考curator官网:Service register and Service discovery

负载均衡

前面说到了服务治理这块,因为里面涉及到负载均衡这块,这里就详细说一下。

通常说来,有三种负载均衡模型是绕不开的,分别是一致性哈希,此模型可让带有业务标记的请求每次请求都会导向到指定的服务器上。轮询,此模型主要是对服务器列表进行顺序访问。随机,此模型主要是随机获取服务器并返回。其余的模型还有不少,能够根据具体的业务进行衍生,这里不作一一的展现。

首先来看看负载均衡基类:

6db1acf2-c315-48a9-b11a-599193ee965b

而后看看三种模型的实现:

一致性哈希实现,直接对服务端的size进行取余操做:

92cd52ab-0d2d-44ec-960a-0cbd383eb652

轮询实现,对访问过的服务器进行计数累加,而后把此计数做为下标并获取元素返回:

f3915660-1dbe-4346-9fb8-e2592e58be9c

随机实现,对服务器进行随机选取:

9fc58dcf-bcd2-4458-96b3-080c7ad5a66a

你也许会问,为何你设计的负载均衡里面没有权重操做呢?其实若是愿意,也是能够加上权重操做的,这样就会衍生出来其余的负载均衡模型,好比服务访问不一样,权重-10,服务能访问通,权重+1,这样就能够经过权重,选取一些权重较高的服务器优先返回,而对那些权重较低的服务器,能够少分一些请求,让其慢慢恢复到正常状态以后,再多分配一些请求过来等等。

总之,你能够在此基础上进行本身的设计,可是大致思想就是让服务器得到的负载越均衡越好。

容灾处理

此处整合Hystrix进行的设计,能够对请求作FailFast处理,RetryOnece处理,RetryTwice处理等, 具体细节能够翻看Hystrix设计便可。这里就不详解(哈哈哈,实际上是由于写着写着,写的懒了,这块就不想讲了,毕竟基本上都是Hystrix那套)。

反射调用

最后要说的部分就是反射调用这块了。咱们知道,当客户端发送待调用的方法发送给服务端,服务端接收后,须要经过反射调用方法,而后将结果返回给客户端。首先来看看服务端业务处理Handler:

c04f7664-506d-470b-b2cd-56d1d934ae6a

能够看到,此业务处理handler会读取客户端的请求,而后分析数据包内容,最后利用反射来调用相应的方法获取结果并压入缓冲区中,以后发送给客户端。

再来看看handle方法是如何进行反射调用并获得结果的:

a3e6bb50-4421-43c6-be3b-413cec9424f8

能够看到,很经典的反射调用场景,这里就不细说了。

从这里,咱们能够看出,服务端的处理方式如上,很是的简单。可是客户端是怎么发送请求消息给服务端,又是如何接收服务端的返回数据的呢?

3acc13f4-28cc-4de4-965d-336074b6a6f5

从上面能够看出,咱们用了javassist组件的反射(java自身的反射也是相似的使用方式)来构建完整的类对象,而后利用callback回调来发送请求给服务端获取数据,而后获取服务端返回的数据,最后将返回的数据拆解后,返回给客户端。若是用java自带的反射来实现,编码也是差很少的:

00ff2b8e-0261-4cc4-896e-ab7ea42ee7a1

这里须要注意的是,此处用了动态反射的功能来实现,性能并非特别好,若是能用上字节码技术,性能会再提高一个台阶。具体的字节码实现方式,能够参见我后续的文章。

5. 跑起来吧!!

好了,咱们终于把一切都准备好了,那么就让咱们运行起来吧。

在服务端,首先能够看到以下的注册中心上线日志:

068cefb2-70c4-44ac-8176-17e7d7b48963

而后能够看到客户端登陆日志:

57981f83-d3a9-429b-a956-75c7ab4a8861

在客户端,咱们能够看到以下的日志:

2f6f67b2-4a52-4d78-b235-c9381fe23cdf

能够看到,客户端链接上来后,先发送鉴权请求,鉴权成功后,将会发送服务调用请求,调用完毕后,获得返回结果,整个过程耗费18ms,最后客户端退出。

当咱们在客户端调用的时候,加上Thread.Sleep来触发心跳探活,能够看到以下的检测结果:

b2eca78b-c48b-4a06-b5f3-a5b7b3b81da7

能够看到,每隔5秒钟,咱们都能收到客户端的心跳,而后咱们模拟网络差,客户端掉线,看看服务端如何检测:

ee8a82c9-902e-4d54-9b8f-2ee0727ceabf

能够看到客户端被踢掉了,此时咱们再去看看客户端日志,能够看出来,客户端确实被服务端踢掉线了:

d7498e9b-8f60-4d72-b52b-7596b126c8f6

最后,东西作完后,补一个benchmark吧,因为个人机器性能比较差,并且测试是直接开启IDEA这个IDE来测试的,因此性能并不见得很好:

57366a4e-c94a-41b7-a8dc-9224f7c71654

而后来看看benchmark结果吧:

0f003fc7-3e98-4488-8037-6bf5dc50c02a

性能并非特别好,关键有如下几个地方是耗时大户:编解码,反射,同步等待服务端返回

编解码这个只能找性能比较好的组件来解决

反射能够经过字节码来实现,性能会再提高一个档次,可是难度也会提高很多。

同步等待服务返回,能够经过彻底异步化实现来解决,那么刚刚展现的

a5b71565-ff20-4f5f-bafb-d4271f48a0c0

调用方式,会被改变成:

71bc7cea-5640-411b-a8a9-a75ac57ce63f

虽然这样速度会快不少,可是用户可否接受这种调用方式,则是另外一个头疼的问题。性能和易用,自己就具备相悖性,因此只能在进退之间作平衡了。

写到这里,总体介绍差很少了,可是还有不少东西没有接入,譬如说kafka,mq,redis等。若是能把这些东西接入,则会让其总体显得更加丰满,同时功能也更丰富,应用场景也会更广阔一些。

6.总结

写到这里,利用netty打造分布式服务框架的要点就基本上完结了。通篇看来,知识点不少,可是都是咱们耳熟能详的东西,能把它们串在一块儿,组成一个能够用的框架,则须要必定的思考。

文中全部内容基本上为原创,如需转载,请标明 转载自博客园程序诗人 字样,算是对本家付出的辛苦的一点尊重吧。

相关文章
相关标签/搜索