RPC
RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务
背景
- 单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。 - 垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。 - 分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。 - 流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。
基本原理
客户端(Client):服务的调用方。
服务端(Server):真正的服务提供者。
客户端存根:存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
服务端存根:接收客户端发送过来的消息,将消息解包,并调用本地的方法。
过程:
- 要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpoint URI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。
- 要解决通讯的问题,主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
- 当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给B服务器。
- B服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后找到对应的方法(寻址的一部分)进行本地调用,然后得到返回值。
- 返回值还要发送回服务器A上的应用,也要经过序列化的方式发送,服务器A接到后,再反序列化,恢复为内存中的表达方式,交给A服务器上的应用
如何分析一个RPC框架
- 服务治理
- 编码协议(IDL)
- 传输(通信)协议
- 线程模型
RPC框架
Dubbo
架构
Dubbo 架构具有连通性、健壮性、伸缩性、以及向未来架构的升级性几个特点。
- Provider为服务提供方,提供 Java 服务接口的实现,并将其元信息注册到 Dubbo 注册中心(过程 1.register 所示)
- Consumer为服务消费端,从 Dubbo 注册中心检索订阅的 Java 服务接口的元信息(过程 2.subscribe 所示),通过框架处理后,生成代理程序执行远程方法调用(过程 4.invoke 所示)
- Registry为注册中心,属于注册元信息中心化基础设施(如 Apache Zookeeper 或 Alibaba Nacos),为 Provider 提供注册通道,为 Cosumer 提供订阅渠道。同时,注册中心支持注册元信息变更通知,通知 Consumer 上游 Provider 节点的变化(如扩容或缩容)。而注册元信息均以 Dubbo URL 的形式存储
- Monitor为服务治理平台,提供开发和运维人员服务查询、路由规则、服务 Mock 和测试等治理能力
传输协议:TCP+基于长链接的NIO框架
编码协议:定制的Hessian2框架/PB/HTTP等
线程模型:Reactor
Dubbo解决的问题
- 当服务越来越多时,服务 URL 配置管理变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。此时需要一个服务注册中心,动态地注册和发现服务,使服务的位置透明。并通过在消费方获取服务提供方地址列表,实现软负载均衡和 Failover,降低对 F5 硬件负载均衡器的依赖,也能减少部分成本。
- 服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。这时,需要自动画出应用间的依赖关系图,以帮助架构师理清关系。
- 服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器?为了解决这些问题,第一步,要将服务现在每天的调用量,响应时间,都统计出来,作为容量规划的参考指标。其次,要可以动态调整权重,在线上,将某台机器的权重一直加大,并在加大的过程中记录响应时间的变化,直到响应时间到达阈值,记录此时的访问量,再以此访问量乘以机器数反推总容量。
Spring Cloud
Spring Cloud为开发人员提供了快速构建分布式系统中的一些通用模式(例如配置管理,服务发现,断路器,智能路由,微代理,控制总线,一次性令牌,全局锁,领导选举,分布式 会话,群集状态)。 分布式系统的协调引出样板模式(boiler plate patterns),并且使用Spring Cloud开发人员可以快速地实现这些模式来启动服务和应用程序。
交互流程
Spring Cloud 微服务架构是由多个组件一起组成的,各个组件的交互流程如下。
- 请求统一通过 API 网关 Zuul 来访问内部服务,先经过 Token 进行安全认证。
- 通过安全认证后,网关 Zuul 从注册中心 Eureka 获取可用服务节点列表。
- 从可用服务节点中选取一个可用节点,然后把请求分发到这个节点。
- 整个请求过程中,Hystrix 组件负责处理服务超时熔断,Turbine 组件负责监控服务间的调用和熔断相关指标,Sleuth 组件负责调用链监控,ELK 负责日志分析。
优点
- 社区活跃
- 标准化的将微服务的成熟产品和框架结合一起,Spring Cloud 提供整套的微服务解决方案,开发成本较低,且风险较小。
- 基于 Spring Boot,具有简单配置、快速开发、轻松部署、方便测试的特点。
- 支持 REST 服务调用,相比于 RPC,更加轻量化和灵活(服务之间只依赖一纸契约,不存在代码级别的强依赖),有利于跨语言服务的实现,以及服务的发布部署。另外,结合 Swagger,也使得服务的文档一体化。
对比Dubbo
gRPC
它的原理是通过 IDL(Interface Definition Language)文件定义服务接口的参数和返回值类型,然后通过代码生成程序生成服务端和客户端的具体实现代码,这样在 gRPC 里,客户端应用可以像调用本地对象一样调用另一台服务器上对应的方法。
它的主要特性包括三个方面。
- 通信协议采用了 HTTP/2,因为 HTTP/2 提供了连接复用、双向流、服务器推送、请求优先级、首部压缩等机制。Netty 4.1 提供了 HTTP/2 底层协议栈,通过 Http2ConnectionHandler 及其依赖的其它类库,实现了 HTTP/2 消息的统一接入和处理。
- IDL 使用了ProtoBuf,ProtoBuf 是由 Google 开发的一种数据序列化协议,它的压缩和传输效率极高,语法也简单
- 多语言支持,能够基于多种语言自动生成对应语言的客户端和服务端的代码。
Thrift
Thrift 是一种轻量级的跨语言 RPC 通信方案,支持多达 25 种编程语言。为了支持多种语言,跟 gRPC 一样,Thrift 也有一套自己的接口定义语言 IDL,可以通过代码生成器,生成各种编程语言的 Client 端和 Server 端的 SDK 代码,这样就保证了不同语言之间可以相互通信。它的架构图可以用下图来描述。
网络栈结构
TProtocol层
支持多种序列化格式:如 Binary、Compact、JSON、Thrift 等。
TTransport层
支持多种通信方式:如 Socket、Framed、File、Memory、zlib 等。
Service模型
服务端支持多种处理方式:如 Simple 、Thread Pool、Non-Blocking 等。
- TSimpleServer: 简单的单线程服务模型,常用于测试;
- TThreadPoolServer: 多线程服务模型,使用标准的阻塞式IO;
- TNonBlockingServer: 多线程服务模型,使用非阻塞式IO(需要使用TFramedTransport数据传输方式);
- THsHaServer: THsHa引入了线程池去处理,其模型读写任务放到线程池去处理,Half-sync/Half-async处理模式,Half-async是在处理IO事件上(accept/read/write io),Half-sync用于handler对rpc的同步处理;
对比
框架 | 语言 | 服务治理 | 多种序列化 | 注册中心 | 管理中心 | 跨语言 |
---|---|---|---|---|---|---|
Dubbo | Java | 支持 | 支持 | 支持 | 支持 | 不支持 |
Spring Cloud | Java | 支持 | 支持 | 支持 | 支持 | 不支持 |
gRPC | 跨语言 | 不支持 | only pb | 不支持 | 不支持 | 支持 |
Thrift | 跨语言 | 不支持 | only thrift | 不支持 | 不支持 | 支持 |
RPC中的网络传输与线程模型
基础网络模型
Linux IO模式及 select、poll、epoll详解
几种IO模型
- BIO,同步阻塞IO,阻塞整个步骤,如果连接少,他的延迟是最低的,因为一个线程只处理一个连接,适用于少连接且延迟低的场景,比如说数据库连接。
- NIO,同步非阻塞IO,阻塞业务处理但不阻塞数据接收,适用于高并发且处理简单的场景,比如聊天软件。
- 多路复用IO,他的两个步骤处理是分开的,也就是说,一个连接可能他的数据接收是线程a完成的,数据处理是线程b完成的,他比BIO能处理更多请求。
- 信号驱动IO,这种IO模型主要用在嵌入式开发,不参与讨论。
- 异步IO,他的数据请求和数据处理都是异步的,数据请求一次返回一次,适用于长连接的业务场景。
NIO
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
多路复用
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
Netty
概述
Netty采用典型的三层网络架构进行开发和设计,主要涵盖Reactor通信调度层,责任链ChannelPipeline和业务逻辑编排层(Service ChannelHandler)。
Reactor通信调度层:该层主要包含NioSocketChannel(客户端异步非阻塞通道)/NioServerSocketChannel(服务端异步非阻塞通道),Eventloop,ByteBuffer和Task。该层的主要职责是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后出发各种网络事件,例如连接,读/写等事件,将这些事件出发到pipeline中,由pipeline管理的职责链来进行后续处理。
职责链ChannelPipeline:它负责事件在职责链中的有序传播,同时负责动态地编排职责链。不同应用的Handler 节点的功能也不同,通常情况下,往往会开发编解码Hanlder 用于消息的编解码,它可以将外部的协议消息转换成内部的POJO 对象,这样上层业务则只需要关心处理业务逻辑即可,不需要感知底层的协议差异和线程模型差异,实现了架构层面的分层隔离。
业务逻辑编排层:业务逻辑编排层通常有两类:一类是纯粹的业务逻辑编排,还有一类是其他的应用层协议插件,用于特定协议相关的会话和链路管理。例如CMPP 协议,用于管理和中国移动短信系统的对接。
Reactor通信调度层
该层主要包含NioSocketChannel(客户端异步非阻塞通道)/NioServerSocketChannel(服务端异步非阻塞通道),Eventloop,ByteBuffer和Task。该层的主要职责是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后出发各种网络事件,例如连接,读/写等事件,将这些事件出发到pipeline中,由pipeline管理的职责链来进行后续处理。
- Reactor主线程 MainReactor 对象通过select 监听连接事件, 收到事件后,通过Acceptor 处理连接事件
- 当 Acceptor 处理连接事件后,MainReactor 将连接分配给SubReactor
- subReactor 将连接加入到连接队列进行监听,并创建handler进行各种事件处理
- 当有新事件发生时, subreactor 就会调用对应的handler处理
- handler 通过read 读取数据,分发给后面的worker 线程处理
- worker 线程池分配独立的worker 线程进行业务处理,并返回结果
- handler 收到响应的结果后,再通过send 将结果返回给client
- Reactor 主线程可以对应多个Reactor 子线程, 即MainRecator 可以关联多个SubReactor
Netty Reactor
Channel
Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:
- 当前网络连接的通道的状态(例如是否打开?是否已连接?)
- 网络连接的配置参数 (例如接收缓冲区大小)
- 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。
- 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
- 支持关联 I/O 操作与对应的处理程序。
不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。下面是一些常用的 Channel 类型:
- NioSocketChannel,异步的客户端 TCP Socket 连接。
- NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
- NioDatagramChannel,异步的 UDP 连接。
- NioSctpChannel,异步的客户端 Sctp 连接。
- NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
ChannelPipeline
Netty将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipline中流动和传递。ChannelPipeline持有I/O事件拦截器ChannelHandler的双向链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便的新增和删除ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持
ChannelHandler
它是一个接口,用于处理I/O事件或拦截I/O事件,并将其转发给对应的channelPipeline中的下一个处理程序。
ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:
ChannelInboundHandler 用于处理入站 I/O 事件。
ChannelOutboundHandler 用于处理出站 I/O 操作。
NioEventLoop
NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:
- I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
- 非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。
两种任务的执行时间比由变量 ioRatio 控制,默认为 50,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。
特点
- 零拷贝
- 数据从内存发到网络中,存在两次拷贝,先是从用户空间拷贝到内核空间,再从内核空间拷贝到网络IO
- NIO提供的ByteBuffer可以使用Direct Buffer模式
- 直接开辟一个非堆物理内存,不需要进行字节缓冲区的二次拷贝,可以直接将数据写入到内核空间
- 可扩展性
基于Netty的基础NIO框架,可以方便地进行应用层协议定制,例如HTTP协议栈、Thrift协议栈、FTP协议栈等。这些扩展不需要修改Netty的源码,直接基于Netty的二进制类库即可实现协议的扩展和定制。 - 高可靠
路有效性检测,内存保护机制,优雅停机
RPC性能优化
- 客户端 获取到 目标接口的 client stub
- 客户端 调用目标方法
- 客户端 获取到 请求方法 和 请求数据
- 客户端 把 请求方法 和 请求数据 序列化为 传输数据
- 进行网络传输
- 服务端 获取到 传输数据
- 服务端 反序列化获取到 请求方法 和 请求数据
- 服务端 获取到 Invoker
- 服务端 调用 具体实现 获取到 响应结果
- 服务端 把 响应结果 序列化为 传输数据
- 进行网络传输
- 客户端 接收到 传输数据
- 客户端 反序列化获取到 响应结果
- 客户端 返回 响应结果
整个流程中对性能影响比较大的环节有:序列化[4, 7, 10, 13],方法调用[2, 3, 8, 9, 14],网络传输[5, 6, 11, 12]
RPC性能取决于
- IO模型。优化点:结合多路复用器(select,epoll)实现非阻塞IO。
- 线程模型。优化点:使用Reactor线程模型
- 序列化方式。优化点:pb,thrift或者自定义协议
- 池化技术,减少对象创建和销毁
- 使用Direct Buffer
- 定制报文。优化点:不使用HTTP,使应用层的头尽可能小
- 连接优化。优化点:在合适的地方使用长链接
kRPC
https://wiki.corp.kuaishou.com/pages/viewpage.action?pageId=351636938