1. 基本概念
1.1 RPC - Remote Procedure Calls 远程函数调用
RPC(Remote Procedure Call)是一种计算机通信技术,用于在分布式系统中实现远程方法调用。它允许在不同的计算机或进程之间请求和执行像本地方法一样的函数或过程。RPC使得开发人员可以编写像调用本地函数一样调用远程函数,从而隐藏了底层通信细节。
在RPC中,客户端应用程序通过发送请求消息到远程服务器来调用远程函数,服务器收到请求后执行相应的操作,然后将结果返回给客户端。这使得分布式系统中的不同部分可以通过函数调用来交互,就像它们是在同一台计算机上运行一样。
RPC可以用于不同的编程语言和平台之间,允许开发人员在不同环境中构建分布式应用程序。
一些常见的RPC框架和协议包括:
- gRPC: 由Google开发的高性能、开源的RPC框架,支持多种编程语言。
- XML-RPC: 使用XML格式的数据进行通信的RPC协议。
- JSON-RPC: 使用JSON格式的数据进行通信的RPC协议。
- CORBA: 通用对象请求代理架构,用于不同语言和平台之间的分布式对象通信。
- Java RMI: Java远程方法调用,用于在Java应用程序之间实现远程调用。
RPC技术在构建微服务架构、分布式系统和跨网络通信时非常有用,它简化了不同部分之间的通信,提高了系统的可维护性和可扩展性。
1.2 需要解决的问题
-
函数映射
在RPC(Remote Procedure Call)中,函数映射是指将远程服务器上的函数或方法与本地客户端应用程序中的函数建立对应关系,以便客户端可以像调用本地函数一样调用远程函数。
函数映射通常涉及以下几个方面:
- 函数名称: 客户端需要知道要调用的远程函数的名称。通常,远程函数的名称和本地函数名称相同,以便使调用过程更加直观。
- 参数传递: 客户端需要将函数所需的参数传递给远程服务器。这可能涉及将参数进行序列化(如将参数转换为字节流或JSON格式),以便在网络上进行传输。
- 结果返回: 远程服务器执行函数后会产生结果,客户端需要接收并解析这些结果。类似地,结果可能需要被序列化以在网络上传输。
- 异常处理: 如果在远程函数调用期间发生错误,客户端需要能够处理这些异常情况。这可能涉及识别不同类型的异常并采取相应的操作。
- 数据类型映射: 如果客户端和服务器使用不同的编程语言或数据类型系统,可能需要进行数据类型的映射。这确保了跨语言和平台的正确数据传递。
- 协议和序列化: RPC系统通常会使用特定的协议来管理通信,例如gRPC使用HTTP/2协议。同时,数据的序列化和反序列化是必要的,以便将参数和结果在网络上传输。
- Stub(存根)生成: 在一些RPC框架中,客户端和服务器之间的通信需要一些中间代码,通常称为存根(Stub)。这些存根负责处理通信细节,例如序列化、协议处理等。
总之,函数映射是将分布式系统中远程函数与本地函数关联起来的重要概念,它使得远程过程调用对于开发人员来说更加方便和透明,从而促进了分布式应用程序的开发和维护。不同的RPC框架和协议提供不同的方式来处理这种函数映射。
-
数据转化成字节流
在RPC(Remote Procedure Call)中,数据通常需要在网络上传输,因此需要将数据从高级的数据结构(如函数参数、返回值等)转换为字节流的形式,以便在网络中传递。这个过程称为序列化(Serialization)。
序列化的过程涉及将数据转换为字节流,这使得数据可以在网络上传输,并在接收端进行反序列化以还原为原始数据。序列化和反序列化是实现RPC通信的关键步骤之一。
不同的RPC框架和协议使用不同的序列化技术和格式。一些常见的序列化格式包括:
- Binary(二进制)序列化: 将数据直接转换为字节流的形式。这种方式效率较高,但字节流通常难以读懂和调试。
- JSON(JavaScript Object Notation)序列化: 使用JSON格式将数据序列化为字符串,然后再将字符串转换为字节流。JSON格式具有人类可读性,适合用于Web应用和跨语言通信。
- XML(eXtensible Markup Language)序列化: 使用XML格式将数据序列化为字符串,然后再将字符串转换为字节流。XML具有结构化的特点,但相对于JSON,其格式较为繁琐。
- Protocol Buffers(protobuf): 这是一种由Google开发的二进制序列化格式,具有高效性能和紧凑的数据表示,适用于高性能的RPC通信。
- MessagePack: 一种紧凑的二进制序列化格式,类似于JSON,但在大小和性能上更优。
-
网络传输
RPC(Remote Procedure Call)使用不同的网络传输方式来在客户端和服务器之间进行通信。以下是一些常见的RPC网络传输方式:
- HTTP/HTTPS: 许多RPC框架使用HTTP或HTTPS协议作为底层的网络传输协议。HTTP是一种广泛使用的应用层协议,可以通过Web浏览器和Web服务器进行通信。RPC调用可以通过HTTP请求和响应进行传输,使得RPC可以穿越防火墙和代理服务器。gRPC是一个使用HTTP/2协议的RPC框架,它支持多种编程语言和平台。
- TCP/IP: Transmission Control Protocol(TCP)和Internet Protocol(IP)组合成了TCP/IP协议栈,这是一种传输层协议,用于在计算机网络中进行数据传输。一些RPC框架使用TCP/IP作为底层传输,以提供可靠的、面向连接的通信。
- UDP: User Datagram Protocol(UDP)是一种无连接的、面向消息的传输协议。尽管UDP不像TCP那样提供可靠性,但它具有较低的延迟和更高的吞吐量。一些需要低延迟的RPC系统可能会选择使用UDP作为底层传输。
- WebSocket: WebSocket是一种在单个TCP连接上实现全双工通信的协议,通常用于Web应用程序。一些RPC框架可以使用WebSocket来实现双向通信,从而支持服务器主动向客户端推送消息。
- AMQP: Advanced Message Queuing Protocol(AMQP)是一种用于消息中间件的开放标准协议。它支持消息传输、队列和发布/订阅模型,可以用于实现分布式系统中的RPC通信。
- Custom Protocols: 一些RPC系统可能会使用自定义的网络传输协议,以满足特定的需求和性能要求。这些协议可能基于底层的TCP或UDP,但在通信过程中加入了自定义的控制和数据结构。
1.3 RPC概念模型
1.4 一次完整的RPC过程
- IDL文件:中立的接口文件
- 生成代码:通过编译工具吧IDL文件转化为语言对应的静态库
- 编解码
- 通信协议:规范了数据在网络中的传输内容和格式。包含请求/响应参数和与元数据
- 网络传输:通常基于成熟的网络库走TCP/UDP传输
1.5 RPC的好处
- 单一职责,有利于分工协作和运维开发
- 可扩展性强,资源使用率更优
- 故障隔离,服务的整体可靠性更高
2. 分层设计
2.1 分层设计
2.2 编解码层 - 生成代码
2.3 - 数据格式 - 二进制编码
TLV编码
- Tag :标签
- Lenght:长度
- Value:值
扩展性好,但是新增了很多的冗余部分
2.4 协议层 - 概念、构造
2.5 协议层 - 协议解析
2.6 网络层
SOCKET-API
3. 关键指标
3.1 稳定性 - 保障策略
- 熔断:保护调用方,防止被调用的服务出现问题影响整个链路
- 限流:保护被调用方,防止大流量压垮服务
- 超时控制:避免资源浪费
3.2 稳定性 - 请求成功率
- 负载均衡
- 重试
3.3 稳定性 - 长尾请求
3.4 稳定性 - 注册中间件
3.5 易用性
- 开箱即用
- 周边工具
3.6 扩展性
3.7 观测性
3.8 高性能
4. 简单的RPC实践
- 定义共享的函数:
首先,需要定义将在客户端和服务器之间共享的函数。这些函数必须满足一些要求,例如函数名必须是导出的(首字母大写),并且参数和返回值的类型必须是Go支持的类型。
package main
import (
"errors"
)
type Arith int
func (a *Arith) Multiply(args Args, reply *int) error {
*reply = args.A * args.B
return nil
}
type Args struct {
A, B int
}
2. 创建服务器:
在服务器端,需要将共享的函数注册到rpc包中,并侦听某个网络地址以等待客户端连接。
package main
import (
"fmt"
"net"
"net/rpc"
)
func main() {
arith := new(Arith)
rpc.Register(arith)
listener, err := net.Listen("tcp", ":1234")
if err != nil {
fmt.Println("Error starting server:", err)
return
}
defer listener.Close()
fmt.Println("Server started. Listening on :1234")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting connection:", err)
continue
}
go rpc.ServeConn(conn)
}
}
3. 创建客户端:
客户端需要与服务器建立连接,并通过RPC调用远程函数。
package main
import (
"fmt"
"net/rpc"
)
func main() {
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
fmt.Println("Error connecting to server:", err)
return
}
defer client.Close()
args := Args{A: 5, B: 3}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
fmt.Println("Error calling remote function:", err)
return
}
fmt.Printf("Result: %d * %d = %d\n", args.A, args.B, reply)
}