这是我参与「第五届青训营 」伴学笔记创作活动的第4天
RPC框架
远程过程调用(Remote Procedure Call, RPC), 是分布式系统或微服务中一种常见的通信方法, 允许程序调用远程的过程或函数.
RPC框架通过参数传递的方式调用远程的方法, 并且回隐藏底层的通讯细节, 并且RPC隐藏了本地调用和远程调用的区别, 使程序员可以像调用本地方法一样调用远程方法.
Call ID
在远程调用中, 因为两个进程的地址空间不一, 所以在RPC框架中, 每个函数都必须有一个自己的ID, 这个ID在所有进程中都是唯一确定的, 而客户端在远程调用中, 需要使用这个ID确定想要那个要调用的方法. 并且需要在client和server分别维护一个{函数名:Call ID}的表, 通过这个对应表, 客户端确定方法的ID, 服务端通过ID查找方法.
序列化
在远程调用中, Client需要将参数转化为字节流, 传递给server, 接着server将其进行反序列化,
而在本地调用中, 至于要将参数放入栈中, 方法即可自己进行读取.
网络传输
在远程传输中, 所有的数据都需要网络进行传输, 主要进行Call ID和序列化后的的参数字节流的传输, 而server返回的结果也会进行序列化后再次传输给client.
KiteX
server
当Client需要进行远程调用时, 就需要server的接口, 传入参数以及返回值分别是什么样的, 为此, 通过IDL约定server和client双方的协议.
IDL的优势只要在于定义清晰, 并且能够辅助进行代码生成, 在团队项目中, 通过IDL整合不同服务的接口.
namespace go api
struct Request{
1 : string message
}
struct Response{
1 : string message
}
service Echo{
Response echo(1: Request req)
}
定义完成IDL之后, 可以通过kitex命令
$ kitex -module example -service example echo.thrift
生成server端代码.
生成代码的相关格式如下所示
.
|-- build.sh
|-- echo.thrift
|-- handler.go
|-- kitex_gen
| `-- api
| |-- echo
| | |-- client.go
| | |-- echo.go
| | |-- invoker.go
| | `-- server.go
| |-- echo.go
| `-- k-echo.go
|-- main.go
`-- script
|-- bootstrap.sh
`-- settings.py
其中, build.sh为构建脚本, kitex_gen为IDL内容相关的代码, 主要包含server/client以及struct和编解码的优化, handler.go则需要用户子啊其中实现IDL service中定义的方法, 即实现echo(1: Requeset)的逻辑
kitex默认监听8888端口, 当方法逻辑较为复杂时, 推荐按照MVC等模式对代码进行分层, 尽量保证handler.go中存放的是较为简单的逻辑.
func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
return &api.Response{Message: req.Message}, nil
}
client
kitex执行的协议主要为Thrift, Protobuf以及gRPC, 只要满足其支持的协议, 客户端请求甚至可以由其他语言框架发起.
创建client
import "example/kitex_gen/api/echo"
import "github.com/cloudwego/kitex/client"
...
c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888"))
if err != nil {
log.Fatal(err)
}
echo.NewClient用于创建client, "example"处应当填入服务名, 第二个参数可选, 而其中client.WithHostPorts("x.x.x.x:xxxx")用于指定服务端地址, 当指定服务端地址时, kitex的服务发现将会失效
发起请求
import "example/kitex_gen/api"
...
req := &api.Request{Message: "my request"}
resp, err := c.Echo(context.Background(), req, callopt.WithRPCTimeout(3*time.Second))
if err != nil {
log.Fatal(err)
}
log.Println(resp)
服务发现与注册
kitex的服务发现与注册对接了主流的服务注册与发现中心, 如ETCD, Nacos等.
当我们使用微服务时, 更多地是使用服务发现与注册去调用下游服务.
通过将server注册到注册中心, client去注册中心获取数据, 以此实现负载均衡.
通过服务注册与发现的性能将会高于使用多层代理的方式, 因为实现了ip直连, 减少了多次代理带来的网络延时.
服务注册-server端
type HelloImpl struct{}
func (h *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
return &api.Response{Message: req.Message}, nil
}
func main(){
r,err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"})
if err != nil{
log.Fatal(err)
}
server := hello.NewServer(new(HelloImpl), server.WithRegistry(r), server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{
Service : "Hello",
}))
err = server.Run()
if err!=nil{
log.Fatal(err)
}
}
服务发现-client
func main() {
r,err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"})
if err != nil {
log.Fatal(err)
}
//传递服务名, 使用服务名进行注册过滤
client := hello.MustNewClient("Hello",client.WithResolver(r))
for {
ctx,cancel := contex.WithTimeout(contex.BackGround(), time.Second*3)
resp,err := client.Echo(ctx, &api.Request{Message: "Hello"})
cancel()
if err!= nil{
log.Fatal(err)
}
log.Println(resp)
time.Sleep(time.Second)
}
}
为了避免每次获取示例数据都前往服务注册与发现中心(IO时间), kitex在此处使用缓存, 并且在后台使用异步方法更新删除对象.