RPC
优点<br>
简单、直接、开发方便。<br>
<b><font color="#9c27b0">长链接</font></b>,不必每次通信都要像http一样去3次握手什么的,减少了网络开销
RPC框架一般都有注册中心,有丰富的监控管理
发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作
RPC一般配合netty框架、spring自定义注解来编写轻量级框架,<br>较新的jdk的IO一般是NIO,即非阻塞IO,在高并发网站中,RPC的优势会很明显<br>
组件<br>
客户端(Client)<br>
客户端存根(Client Stub)
存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端
服务端存根(Server Stub)
接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理
服务端(Server)<br>
具体调用过程
<ol><li>服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务;</li><li>客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体;</li><li>客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端;、</li><li>服务端存根(server stub)收到消息后进行解码(反序列化操作);</li><li>服务端存根(server stub)根据解码结果调用本地的服务进行相关处理;</li><li>本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub);</li><li>服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方;</li><li>客户端存根(client stub)接收到消息,并进行解码(反序列化);</li><li>服务消费方得到最终结果;</li></ol>
目标
RPC框架的实现目标则是将上面的第2-10步完好地封装起来,<br>也就是<b><u>把调用、编码/解码的过程给封装起来</u></b>,让用户感觉上像调用本地服务一样的调用远程服务<br>
需要解决的问题
1、如何确定客户端和服务端之间的通信协议?<br>2、如何更高效地进行网络通信?<br>3、服务端提供的服务如何暴露给客户端?<br>4、客户端如何发现这些暴露的服务?<br>5、如何更高效地对请求对象和响应结果进行序列化和反序列化操作?
1、需要有非常高效的网络通信,比如一般选择Netty作为网络通信框架;<br>2、需要有比较高效的序列化框架,比如谷歌的Protobuf序列化框架;<br>3、可靠的寻址方式(主要是提供服务的发现),比如可以使用Zookeeper来注册服务等等;<br>4、如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能;
关键技术<br>
动态代理
序列化与反序列化
NIO通信<br>
注册中心<br>
其他主流RPC框架<br>
RMI<br>
Hessian<br>
potobuf-rpc-pro
Thrift<br>
<span style="color: rgb(62, 62, 62); font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; letter-spacing: 0.544px; text-align: justify;">Finagle </span>
Avro<br>
Dubbo<br>
实现<br>
建立通信<br>
是通过在客户端和服务器之间建立TCP连接
连接分类<br>
长连接<br>
客户端和服务器建立起连接之后保持长期持有,<br>不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效;<br><b><font color="#ba68c8">多个远程过程调用共享同一个连接</font></b>。<br>
服务寻址
采用Redis或者Zookeeper来注册服务
服务提供者角度<br>
将自己提供的服务注册到指定的注册中心<br>
各种原因致使提供的服务停止时,需要向注册中心注销停止的服务;
服务的提供者需要定期向服务注册中心发送心跳检测,<br>服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉<br>
调用者角度<br>
消费的服务上线或者下线的时候,注册中心会告知该服务的调用者
服务调用者下线的时候,则取消订阅
网络传输<br>
序列化
传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输
反序列化<br>
将二进制信息恢复为内存中的表达方式,<br>然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用,通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。<br>
服务调用<br>
SPI
Dubbo SPI
<ol><li>对 Dubbo 进行扩展,不需要改动 Dubbo 的源码</li><li><font color="#b71c1c">按需加载</font>:延迟加载,可以一次只加载自己想要加载的扩展实现。</li><li><font color="#b71c1c">增加了对扩展点 IOC 和 AOP 的支持</font>,一个扩展点可以直接 setter 注入其它扩展点。</li><li>Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。</li></ol>
依靠SPI机制实现插件化功能
Java SPI
JDK 标准的 SPI 会一次性加载所有的扩展实现,<br>如果有的扩展很耗时,但也没用上,很浪费资源。<br>所以只希望加载某个的实现,就不现实了<br>
实现方法
需要一个目录META/services 在classpath下面
目录下放置配置文件
文件名为要扩展的接口全名<br>
文件内容为要实现的接口的实现方法
文件必须为UTF-8编码
使用方法
ServiceLoader<接口> searchServiceLoader = ServiceLoader.load(接口.class);<br> Iterator<Search> iterable = searchServiceLoader.iterator();
解释:<b><u>SPI ,全称为 Service Provider Interface,是一种服务发现机制</u></b>
运维管理<br>
通过接口版本号兼容老接口
可以利用 telnet 命令进行调试、管理。<br><u><b>Dubbo2.0.5 以上版本服务提供端口支持 telnet 命令</b></u><br>
参考资料:<br><font color="#31a8e0">https://www.cnblogs.com/suger43894/p/9372123.html<br>https://www.jianshu.com/p/bbb1c92cb113</font><br>
服务降级
1.在监控管理界面处理<br>
2.通过代码
mock=force:return+null
不发起远程调用看,直接放回null。用来屏蔽不重要服务不可用时对调用方的影响
mock=fail:return+null
在远程调用失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
mock=“return null”
dubbo:reference 中设置 mock=“return null”。<br>mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,<br>命名规则是 “<font color="#f44336"><b>接口名称+Mock</b></font>” 后缀。<font color="#f15a23"><b>mock实现需要保证有无参的构造方法</b></font>。<br>然后在 Mock 类里实现自己的降级逻辑<br>
3.集成Hystric
优雅停机<br>
通过 JDK 的 S<font color="#f15a23">hutdownHook 来完成优雅停机</font>的,<br>如果使用kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过<font color="#f15a23"> kill PID</font> 时,才会执行。<br>
其他<br>
调用失败默认重试两次
不支持事务,需要集成第三方<br>
缓存
为了提高数据访问的速度。Dubbo 提供了<font color="#f15a23"><font color="#c41230">声明式缓存</font>,以减少用户加缓存的工作量<dubbo:reference cache=“true”</font> />
支持的序列化方式<br>
Hessian(默认)<br>
Dubbo序列化
FastJson<br>
Java自带序列化<br>
安全措施<br>
通过 Token 令牌防止用户绕过注册中心直连,然后在注册中心上管理授权。
提供服务黑白名单,来控制服务所允许的调用方。
<font color="#f15a23">Zookeeper 的注册可以添加用户权限认证</font>
是否阻塞调用<br>
<font color="#f15a23"><b>默认是阻塞的,可以<font color="#c41230">异步调用</font></b></font>,没有返回值的可以这么做。<br>Dubbo 是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象。<br>
服务时效踢出原理<br>
基于 <font color="#f15a23">zookeeper 的临时节点</font>原理。
Dubbo 允许配置多协议,在不同服务上支持不同协议或者同一服务上同时支持多种协议。
当<b><u>一个接口有多种实现时,可以用 group 属性来分组</u></b>,服务提供方和消费方都指定同一个 group 即可。
可以用版本号(version)过渡,多个不同版本的服务注册到注册中心,版本号不同的服务相互间不引用。这个和服务分组的概念有一点类似。
Dubbo 的设计目的是为了<b style=""><font color="#f57c00">满足高并发</font><font color="#f44336">小数据量</font><font color="#f57c00">的 rpc 调用</font></b>,在<u>大数据量</u>下的性能表现并不好,<u>建议使用 rmi 或 http 协议。</u>
<b>管理控制台</b>主要包含:<b>路由规则</b>,<b>动态配置</b>,<b>服务降级</b>,<b>访问控制</b>,<b>权重调整</b>,<b>负载均衡</b>,等管理功能。
Dubbo <u><b>默认使用 Netty 框架</b></u>,也是推荐的选择,另外内容还集成有<u>Mina</u>、<u>Grizzly</u>。
注册中心<br>
Multicast 注册中心
Multicast 注册中心不需要任何中心节点,只要广播地址,就能进行服务注册和发现,基于网络中组播传输实现。
组播受网络结构限制,只适合小规模应用或开发阶段使用
Provider 和 Consumer 和 Registry 不能跨机房(路由)
Zookeeper注册中心<br>
基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更。
Redis注册中心<br>
基于 Redis 实现,采用 key/map 存储,<br>key 存储服务名和类型,<br>map 中 key 存储服务 url,value 服务过期时间。基于 Redis 的发布/订阅模式通知数据变更。<br>
通过心跳的方式检测脏数据,<u>服务器时间必须相同</u><br><b>脏数据由监控中心删除。</b>(注意:<b>服务器时间必需同步</b>,否则过期检测会不准确)<br>
Simple注册中心<br>
注册中心<b>本身就是</b>一个普通的<b>Dubbo服务</b>,可以减少第三方依赖,使整体通讯方式一致。<br>SimpleRegistryService只是简单实现,<b>不支持集群</b>,可作为自定义注册中心的参考,但不适合直接用于生产环境<br>
参考:<br><font color="#31a8e0">https://zhuanlan.zhihu.com/p/73949200<br>https://www.cnblogs.com/twoheads/p/10119251.html</font><br>
架构设计
核心组件<br>
Provider<br>
Consumer
Registry<br>
Monitor
注册发现流程<br>
1.服务提供者启动时,向注册中心注册自己提供的服务;<br>2.Consumer在启动时,向注册中心获取自己所需的服务;<br>3.注册中心返回提供者注测的消费者所需接口给消费者;<b style=""><font color="#2196f3">如果有变更,基于</font><font color="#ff0000">长连接</font><font color="#2196f3">通知消费者</font></b>;<br>4.消费者从服务提供者地址列表中,基于<font color="#00a650">软负载均衡</font>,先取一个进行调用,如果失败再调用另一台;<br>
服务提供者和消费者在内存中记录调用次数和时间,定时<font color="#f15a23"><u><b>每分钟</b></u></font>发送一次统计数据到监控中心Monitor;<br>
框架设计分层<br>
配置层(Config)
用于扩展实现consumer/provider配置信息,以ServiceConfig和ReferenceConfig为中心,可以直接new配置类,也可以通过Sping解析配置生成配置类
服务代理层(Proxy)
使用动态代理的方式为接口创建代理类,Proxy 层最主要的接口就是 ProxyFactory。其默认的扩展点有:<b>stub</b>、<b>jdk</b>、<b>javassist</b>。jdk 使用反射的方式创建代理类,javassist 通过拼接字符串然后编译的方式创建代理类。<br>对于服务提供者,代理的对象是接口的真实实现。<br>对于服务消费者,代理的对象是远程服务的 invoker 对象。
服务注册层(Registry)
注册层主要负责的就是服务的注册发现。这层的主要接口就是 RegistryFactory,默认的扩展实现有:<br><ul><li><b>zookeeper</b></li><li><b>redis</b></li><li><b>multicast(广播模式)</b></li><li><b>内存</b></li></ul>
集群层(Cluster)
对多提供者调用场景的抽象,是 Dubbo 整个集群容错的抽象层。主要的扩展点有:<br><ul><li><b>容错(Cluster)</b>、</li><li><b>路由(RouterFactory)、</b></li><li><b>负载均衡(LoadBalance)。</b></li></ul>
监控层(Monitor)
RPC调用次数和调用时间监控,以Statistics为中心,扩展接口为MonitorFactory,Monitor和MonitorService
远程调用层(Protocol)<br>
在这一层发起服务暴露(protocol.export)和服务引用(protocol.refer)。<br>Dubbo 提供的协议有:<br><ul><li><b>Dubbo (默认);</b></li><li><b>injvm;</b></li><li><b>rmi;</b></li><li><b>http;</b></li><li><b>hessian;</b></li><li><b>thrift。</b></li></ul>
信息交换层(Exchange)
封装 Request/Response 对象。
网络传输层(Transport)
数据传输就是在传输层发生的,所以这层包含 <b>Transport(传输)</b>、<b>Dispatcher(分派)</b>、<b>Codec2(编解码)</b>、<b>ThreadPool(线程池)</b>,这几个接口。<br>数据传输发生在传输层,所以传输层一定需要具备数据传输的能力,也就是 Transport 和 Codec2,其中 Transport 就是 Netty 等网络传输的接口,编解码不必说了,传输肯定需要;<br>除了传输能力,数据接收之后的处理,Dispatcher 和 ThreadPool,也就是分派任务和任务提交给线程池处理,也是必不可少的。<br>
数据序列化层(Serialize)
序列化的作用是把对象转化为二进制流,然后在网络中传输。Dubbo 提供的序列化方式有:<br><b>Hessian2(默认);<br>Fastjson;<br>fst;<br>Java;<br>Kayo;<br>protobuff。</b>
Monitor实现原理<br>
Consumer 端在发起调用之前会先走<b> filter 链</b>;<br>provider 端在接收到请求时也是先走<b> filter 链</b>,<br>然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和provider 的 filter 链中都会有 Monitorfilter。<br>
流程<br>
1、MonitorFilter 向 DubboMonitor 发送数据
2、DubboMonitor 将数据进行聚合后(默认聚合 <b><font color="#ff0000">1min</font> </b>中的统计数据)暂存到<b>ConcurrentMap<Statistics, AtomicReference> statisticsMap</b>,然后使用一个含有 <b>3 个线程</b>(线程名字:DubboMonitorSendTimer)<b>的线程池</b>每隔 <b>1min</b> 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference
3、SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000)
4、SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写)
5、SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔<font color="#ff0000"> <b>5min</b></font> 钟,将文件中的统计数据画成图表
集群
负载均衡策略
<b>随机</b>选取提供策略(Random loadBalance)<b><font color="#c41230">(默认)</font></b>
有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀。
<b>循环选取</b>提供策略(RoundRobin loadBalance)<br>
平均分布,但是存在请求累积的问题。
<b>量少活跃</b>调用策略(LeastActive loadBalance)<br>
解决慢提供者接收更少的请求。
<b>一致性哈希策</b>略(Constant loadBalance)<br>
使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提<br>供者的剧烈变动。
容错策略<br>(消费者,cluster属性设置)<br>
失败切换(Failover)<br>
当出现失败,重试其它服务器。<b>通常用于读操作</b>,但重试会带来更长延迟。
快速失败(FailFaster)
只发起一次调用,失败立即报错。通常用于<b>非幂等性的写操作</b>,比如新增记录。
失败安全(FailSafe)
出现异常时,<b>直接忽略</b>。通常用于写入审计日志等操作。
失败自动恢复(FailBack)<br>
后台记录失败请求,<b>定时重发</b>。通常用于消息通知操作。
并行调用多个服务器(Forking)<br>
<b>只要一个成功即返回</b>。通常用于<b>实时性要求较高的读操作</b>,但需要浪费更多服务资源。<br>可通过 forks=”2″ 来设置最大并行数。<br>
广播调用所有提供者(Broadcast)<br>
<b>逐个调用,任意一台报错则报错 </b>。通常用于<u>通知所有提供者更新缓存或日志等本地资源信息</u>。