net/rpc中一个函数调用的旁白

身份介绍

大家好,我的名字叫小R,大家也可以叫我的全名:RPC请求,我从出生开始,就背负着全村人的希望,但我知道等待我的将会是一条漫长的链路,虽然在整个链路之中,会有多名长辈(Call,Method,Client等等)为我保驾护航,但我仍旧会经历许多的磨练(序列化+反序列化)直到将我的精神(请求参数)传达到中央(服务端)。此时我的任务就结束了,但我知道一定会有来者(响应),将我的躯体转化成果实(响应结果)传回我的村子里(client)。无论如何,我都将在这里叙述我的故事。

正题

RPC,全称 remote procedure call,中文名叫远程函数调用,做的事情非常简单,就是远程的,这里的远程可以指的是本地其他的函数,或者是其他机器、集群、机房上的,但是归根结底,就是客户端和服务端的一次通信。这样的通信还有很多,比如数据库设计中的主从设计、计算存储分离,都和RPC大同小异。

收益

RPC实际上最大的收益就是:由于客户端不需要关心服务端的函数如何实现(整个过程对于客户端来说是黑盒的),从扩容的角度来看,在服务端测可以支持无限的水平扩容,从业务侧开发的角度来看,也极大地减少了业务侧开发的成本。

坏处

上面说到客户端不需要关心服务端如何实现,特别是在企业级项目中,RPC经常被用作微服务架构的通信基础,那么实际上服务端要想保证高可用+高稳定性,就必须要做到如下几点:

  • 强大的负载均衡+高效的服务发现
  • 相应的熔断、限流措施,优雅的重试机制
  • 弹性扩容、缩容机制
    相比于单体架构微服务架构会将上述的几点的影响转化到最大,相应的也就增加了底层负载均衡等微服务水平结构的开发难度。

net/RPC 源码剖析

主要名词解释

  • Client:
    • Request:是链表结构,本质上是一次请求头部写入,这里仅仅用来记录调用方法名、seq(seq类似一个时间戳的概念,每一次调用都会分配一个seq number,且后调用RPC的seq number永远大于先调用的)以及下一个request。和数据库中的log有点类似,这里的作用是可以用来追溯请求记录,自定义上报埋点,或者分析网络状况等等
    • ClientCodec:Client-Server 链路传输中的编码解码库,会讲整合后的RPC编码成相应的流写入RPC Session,以便server读取,同时对服务端返回的响应体进行解码,供Client读取。
    • Call:正在执行的一次RPC调用。
  • Server:
    • serviceMap:一个线程安全的map,后来记录不同的服务名与服务本身的映射。
    • freeReq:同Client端的Request解释。
    • freeResp:和Request作用基本相同,不赘述了。

下面我将用一张图来讲述一下链路中的RPC调用函数的执行流程
在这里插入图片描述

客户端流程讲解:

  1. 调用Call 函数:func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error,call函数是对外暴露给客户端调用的接口
    serviceMethod 服务+方法名,为字符串类型,有大小写检查,例如我想调用Service A中的Get方法,那么就是A.Get
    args 调用RPC方法参数,一般为结构体
    reply 一般为返回,根据预期的返回值创建相应的结构体
  2. Call函数会调用Go函数,主要作用是创建channel,保证请求执行的有序性(这里可能还有一个目的是可以通过设置channel的大小来防范针对RPC的洪泛攻击),channel的类型是Call类型,默认大小是1,主要作用是传递Call和接收从服务端返回的Call
    在Go()这个方法里,主要做了这么几件事
    • serviceMethod, args, reply, channel 封装进Call里面
    • 调用send(call *Call)
  3. func (client *Client) send(call *Call)
    Send() 主要作用是
    1. 进一步的创建request,用作链路追踪和回溯 以及
    2. 将request和args写入session

至此为止,调用一次远程调用的request的全部信息已经组装完成,写入session的操作是:调用flush()函数将组装的的request+call写入I/O writer,等待服务端读取,相应步骤涉及到的函数如下:
* err := client.codec.WriteRequest(&client.request, call.Args)
* c.encBuf.Flush() // Flush writes any buffered data to the underlying io.Writer.

服务端流程讲解:

初始化

我们会在代码里手动初始服务端监听线程(这个过程可能在本地,也可以是利用http协议开放ip+端口的形式启动)

Register(new(Service))
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":1234")
if e != nil {
	log.Fatal("listen error:", e)
}
go http.Serve(l, nil)

所以实际上很清楚(low)了,RPC虽然启用的是tcp的协议,但是本质上还是套了一个http的壳,创建TCP连接,利用http的调度去监听TCP的listener,所以个人感觉RPC实际上是一种特殊的http,或者可以说RPC的application layer是基于http的

启动

我们的远端RPC调用通常是按这样的顺序去执行的

  1. func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request)
  2. func (server *Server) ServeConn(conn io.ReadWriteCloser)
  3. func (server *Server) ServeCodec(codec ServerCodec)
  4. func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec)

调用ServeHTTP的时候会创建一个connection(顾名思义,用来接受http报文的连接),这个connection会被当作入参传入到ServeConn中,在ServeConn中会创建一个gobServerCodec的结构体,并作为入参传入到ServeCodecServeCodec主要是为了解码request和编码response而生的。解码request之后,会调用Call函数去执行相应的代码,并返回Response,Response同客户端中Request的编码写入逻辑类似,就不再赘述了。

从设计模式的角度理解net/rpc源码的设计

假如说让我从0开始,不借助任何的资料,我是万万设计不出这样高内聚低耦合的代码,其实看别人的代码能理解只是第一步,最重要的是要学习这个代码的设计思路,有时候可能好的代码能比你只多传一个参数,但少写几十行兼容
下面我来列举一下我任何设计的比较好的代码块。
同时也希望各位大佬能在留言区讨论

  1. ServeCodeCServeConn的拆分:之所以这样拆分,是保证了对于上层ServeConn来说,不需要关心底层的解码编码逻辑,同时如果底层换了一套这样的逻辑,我们也不需要对上层有过多的更改,总结起来:在设计代码的时候,我们需要考虑,如果下层代码执行逻辑发生了颠覆性变更的时候,改动上层的成本有多少,耦合度越低的代码,改动成本一定越小。
  2. Request 的设计:request的设计我起初以为是一种在链路传输中的基本单元,后来发现不是,链表结构的Request更多的作用是链路的一个监控和追踪,能够更好的监控网路中的状况,有了这个Request,我们可以根据其做更多的自定义监控机制,为上层业务以及容灾容错设计提供了一种思路和基础。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值