rpc框架之rpcx-远程调用(3)

我们一起来探寻rpcx框架,本系列会详细讲解rpcx,尽量覆盖它的所有代码,看看这款优秀的rpc框架是如何实现的。

远程调用

顾名思义,就是客户端发起请求,服务端接收并处理,会返回结果的过程。也就是我们说的remote procedure callRPC)。在这个过程中会涉及到如何做服务治理,如何序列化/反序列化

服务治理

服务治理涉及到如何处理远程调用失败的策略: FailoverFailfastFailtryBackup。以及支持什么样的路由算法随机轮询权重网络质量, 一致性哈希,地理位置

序列化/反序列化

也就是网络传输中涉及到的数据编码/解码的过程,这个会单独章节讲解。

我们再来看下远程调用的整个流程(来自于官网文档),理解起来就容易多了
在这里插入图片描述

服务的调用过程为:

  1. client调用client stub,这是一次本地过程调用
  2. client stub将参数打包成一个消息,然后发送这个消息。打包过程也叫做 marshalling
  3. client所在的系统将消息发送给server
  4. server的的系统将收到的包传给server stub
  5. server stub解包得到参数。 解包也被称作 unmarshalling
  6. 最后server stub调用服务过程. 返回结果按照相反的步骤传给client

源码

我们分几个部分来讲解源码(我们仍然以rpc框架之rpcx-简介(1)中的例子为例)

  • 客户端发起请求

    在发起请求的过程中涉及到失败策略路由算法

  • 服务端接收请求

客户端发起请求

//参数
args := &example.Args{
	A: 10,
	B: 20,
}
//返回
reply := &example.Reply{}
//发起请求
err := xclient.Call(context.Background(), "Mul", args, reply)

接下来我们来看看Call方法做了什么事情

// Call invokes the named function, waits for it to complete, and returns its error status.
// It handles errors base on FailMode.
func (c *xClient) Call(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error {
    //如果连接已经中断
	if c.isShutdown {
		return ErrXClientShutdown
	}

	if c.auth != "" {
		metadata := ctx.Value(share.ReqMetaDataKey)
		if metadata == nil {
			metadata = map[string]string{}
			ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
		}
		m := metadata.(map[string]string)
		m[share.AuthKey] = c.auth
	}

	var err error
    //1、路由算法
	k, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args)
	if err != nil {
        //“在路由发生错误的时候”,如果是最快失败策略,则直接返回,不做任何重试
		if c.failMode == Failfast {
			return err
		}
	}

	var e error
    //2、失败策略
	switch c.failMode {
	case Failtry:
		……
        //3、远程调用
        err = c.wrapCall(ctx, client, serviceMethod, args, reply)
        ……
	case Failover:
		……
        //3、远程调用
        err = c.wrapCall(ctx, client, serviceMethod, args, reply)
        ……
	case Failbackup:
		……
	default: //Failfast
		……
        //3、远程调用
        err = c.wrapCall(ctx, client, serviceMethod, args, reply)
        ……
	}
}
1、路由算法

进入到selectClient方法

// selects a client from candidates base on c.selectMode
func (c *xClient) selectClient(ctx context.Context, servicePath, serviceMethod string, args interface{}) (string, RPCClient, error) {
    //注意,这里是有锁的,路由是需要同步的
	c.mu.Lock()
    //获取路由算法
	var fn = c.selector.Select
    //用到装饰器模式,用Plugins来装饰路由器
	if c.Plugins != nil {
		fn = c.Plugins.DoWrapSelect(fn)
	}
    //选择服务节点
	k := fn(ctx, servicePath, serviceMethod, args)
	c.mu.Unlock()
	if k == "" {
		return "", nil, ErrXClientNoServer
	}
    //获取一个连接
	client, err := c.getCachedClient(k)
	return k, client, err
}

定位var fn = c.selector.Select这行代码,获取指定路由算法的Select方法,由k := fn(ctx, servicePath, serviceMethod, args)来获得服务节点。rpcx支持的路由算法有:

在这里插入图片描述
接下来,我们挨个来分析这些算法(需要注意的是:我们说的路由算法是建立在有多个服务端提供同一个服务的情况,如果只有一个服务,那就无所谓算法了,没得其他选择),比如存在三个服务节点:

在这里插入图片描述

localhost:8972,localhost:8973,localhost:8974,我们的分析就在这个3个节点上进行,也就是说s.servers=[]string{"localhost:8972","localhost:8973","localhost:8974"},servers中存放的是就是当前可用的服务节点信息

alwaysFirstSelector

只选择第一个

func (s *alwaysFirstSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
	var ss = s.servers
	if len(ss) == 0 {
		return ""
	}
	return ss[0]
}
randomSelector

随机

从配置的节点中随机选择一个节点。

最简单,但是有时候单个节点的负载比较重。这是因为随机数只能保证在大量的请求下路由的比较均匀,并不能保证在很短的时间内负载是均匀的。

func (s randomSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
	ss := s.servers
	if len(ss) == 0 {
		return ""
	}
    //随机一个数字
	i := fastrand.Uint32n(uint32(len(ss)))
	return ss[i]
}

可以看到随机算法,作者并没使用系统自带随机函数rand.Int31n(),而是使用的是:fastrand.Uint32n,库的对应地址:github.com/valyala/fastrand,为什么呢,我们它的介绍:

Fast and scalable pseudorandom generator for Go
fastrand.Uint32n scales on multiple CPUs, while rand.Int31n doesn't scale. Their performance is comparable on GOMAXPROCS=1, but fastrand.Uint32n runs 3x faster than rand.Int31n on GOMAXPROCS=2 and 10x faster than rand.Int31n on GOMAXPROCS=4.

说白了就是:更快,更稳定,有多快好省的既视感。这属于本文章范畴之外的了,之所以会拿出来讲,希望读者和我一样,看到新的东西,欣然拥抱。

roundRobinSelector

轮询

使用轮询的方式,依次调用节点,能保证每个节点都均匀的被访问。在节点的服务能力都差不多的时候适用。

func (s *roundRobinSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
	ss := s.servers
	if len(ss) == 0 {
		return ""
	}
    //循环引用下一个服务
	i := s.i
	i = i % len(ss)
	s.i = i + 1

	return ss[i]
}
geoSelector

地理位置

选择离客户端最近的服务端节点来提供服务

如果我们希望的是客户端会优先选择离它最新的节点, 比如在同一个机房。 如果客户端在北京, 服务在上海和美国硅谷,那么我们优先选择上海的机房。如果两个服务的节点的经纬度是一样的, rpcx会随机选择一个。

它要求服务在注册的时候要设置它所在的地理经纬度。

func (s geoSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
   if len(s.servers) == 0 {
      return ""
   }

   var server []string
   min := math.MaxFloat64
   //根据服务端(gs)和客户端(s)经纬度来计算距离
   for _, gs := range s.servers {
      d := getDistanceFrom(s.Latitude, s.Longitude, gs.Latitude, gs.Longitude)
      if d < min {
         server = []string{gs.Server}
         min = d
      } else if d == min {
         server = append(server, gs.Server)
      }
   }

   if len(server) == 1 {
      return server[0]
   }

   return server[s.r.Intn(len(server))]
}
consistentHashSelector

一致性hash

关于一致性hash的定义,简单的说就是:同样的hash值,会映射到同一个节点上;如果节点被移除,那会自动转移到其他节点,为了避免其他节点的突增流量问题,可以可以设置虚拟节点。可以参考:一致性哈希算法——虚拟节点

相同的servicePath, serviceMethod 和 参数会路由到同一个节点上。

func (s consistentHashSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
	ss := s.servers
	if len(ss) == 0 {
		return ""
	}

	//将三者拼接起来,获取hash值
	key := genKey(servicePath, serviceMethod, args)
    //根据一致性hash算法,获取节点地址
	selected, _ := s.h.Get(key).(string)
	return selected
}

func genKey(options ...interface{}) uint64 {
	keyString := ""
	for _, opt := range options {
		keyString = keyString + "/" + toString(opt)
	}

	return HashString(keyString)
}

//根据一致性hash算法,获取节点地址
func (this *Hash) Get(key uint64) interface{} {
	obj := this.loose.get(key)
	switch obj {
	case nil:
		return this.compact.get(key)
	default:
		return obj
	}
}

golang库获取hash值的方式有很多种,有兴趣的同学可以自行了解,作者选用的是fnvfnv的算法思路是:先初始化 hash,然后循环 乘以素数 prime32,再与每位 byte 进行异或运算

  • fnv
  • adler32
  • crc32/64
  • crypto

一致性hash算法使用的库:doublejump。这个库可以了解下,老版本这个库是不支持节点删除的,现在作者做了支持

weightedRoundRobinSelector

权重调度

根据份分配的权重比例来分配服务节点,权重越高,分配的机会就越大。主要是为了解决服务节点的存在的性能差异问题

比如如果三个节点abc的权重是{ 5, 1, 1 }, 这个算法的调用顺序是 { a, a, b, a, c, a, a }, 相比较 { c, b, a, a, a, a, a }, 虽然权重都一样,但是前者更好,不至于在一段时间内将请求都发送给a

func (s *weightedRoundRobinSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
	ss := s.servers
	if len(ss) == 0 {
		return ""
	}
	w := nextWeighted(ss)
	if w == nil {
		return ""
	}
	return w.Server
}
func nextWeighted(servers []*Weighted) (best *Weighted) {
	total := 0

	for i := 0; i < len(servers); i++ {
		w := servers[i]

		if w == nil {
			continue
		}
		//if w is down, continue

		w.CurrentWeight += w.EffectiveWeight
		total += w.EffectiveWeight

		if best == nil || w.CurrentWeight > best.CurrentWeight {
			best = w
		}

	}

	if best == nil {
		return nil
	}

	best.CurrentWeight -= total
	return best
}

代码还是很简单的,不做阐述

weightedICMPSelector

网络质量优先

首先客户端会基于ping(ICMP)探测各个节点的网络质量,越短的ping时间,这个节点的权重也就越高。但是,我们也会保证网络较差的节点也有被调用的机会。

调度算法类似于:weightedRoundRobinSelector

func (s weightedICMPSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
	ss := s.servers
	if len(ss) == 0 {
		return ""
	}
	w := nextWeighted(ss)
	if w == nil {
		return ""
	}
	return w.Server
}

至此,路由算法全部讲解完毕。

可以看到,并没有什么复杂之处,所以不要被名字吓倒,在代码面前,一切都赤裸裸

2、失败策略

有时候服务会存在宕机、网络被挖断、网络变慢等情况,稳定的rpc框架应该要能容忍这些情况,rpcx支持四种失败调度算法。

接下来我们来分拆Call方法

// Call invokes the named function, waits for it to complete, and returns its error status.
// It handles errors base on FailMode.
func (c *xClient) Call(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error {
  	……
	var err error
    //1、路由算法
	k, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args)
	if err != nil {
        //“在路由发生错误的时候”,如果是最快失败策略,则直接返回,不做任何重试
		if c.failMode == Failfast {
			return err
		}
	}

	var e error
    //2、失败策略
	switch c.failMode {
	case Failtry:
		……
        //3、远程调用
        err = c.wrapCall(ctx, client, serviceMethod, args, reply)
        ……
	case Failover:
		……
        //3、远程调用
        err = c.wrapCall(ctx, client, serviceMethod, args, reply)
        ……
	case Failbackup:
		……
	default: //Failfast
		……
        //3、远程调用
        err = c.wrapCall(ctx, client, serviceMethod, args, reply)
        ……
	}
}
Failfast

最快返回

在这种模式下, 一旦调用一个节点失败, rpcx立即会返回错误。

  • 发生路由错误的时候,立即返回,不做重试
k, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args)
if err != nil {
	if c.failMode == Failfast {
		return err
	}
}
  • 如果是业务错误,也立即返回,不做重试
default: //Failfast
	err = c.wrapCall(ctx, client, serviceMethod, args, reply)
	if err != nil {
		if uncoverError(err) {
			c.removeClient(k, client)
		}
	}

	return err
}
Failover

失败重试:选择其他节点

在这种模式下, rpcx如果遇到错误,它会尝试调用另外一个节点, 直到服务节点能正常返回信息,或者达到最大的重试次数。 重试测试Retries在参数Option中设置, 缺省设置为3

case Failover:
	retries := c.option.Retries
	//没有到达最大失败次数
	for retries >= 0 {
		retries--

		if client != nil {
			err = c.wrapCall(ctx, client, serviceMethod, args, reply)
			if err == nil {
				return nil
			}
			if _, ok := err.(ServiceError); ok {
				return err
			}
		}

		if uncoverError(err) {
			c.removeClient(k, client)
		}
		//重新选择节点重试
		k, client, e = c.selectClient(ctx, c.servicePath, serviceMethod, args)
	}

	if err == nil {
		err = e
	}
	return err
Failtry

失败重试:还是这个节点

在这种模式下, rpcx如果调用一个节点的服务出现错误, 它也会尝试,但是还是选择这个节点进行重试, 直到节点正常返回数据或者达到最大重试次数。

case Failtry:
	retries := c.option.Retries
	//没有到达最大失败次数
	for retries >= 0 {
		retries--

		if client != nil {
			err = c.wrapCall(ctx, client, serviceMethod, args, reply)
			if err == nil {
				return nil
			}
			if _, ok := err.(ServiceError); ok {
				return err
			}
		}

		if uncoverError(err) {
			c.removeClient(k, client)
		}
        //从缓存中重新获取这个链接(可能需要重连)
		client, e = c.getCachedClient(k)
	}
	if err == nil {
		err = e
	}
	return err
Failbackup

多节点

在这种模式下, 如果服务节点在一定的时间内不返回结果, rpcx客户端会发送相同的请求到另外一个节点, 只要这两个节点有一个返回, rpcx就算调用成功。

case Failbackup:
	ctx, cancelFn := context.WithCancel(ctx)
	defer cancelFn()
	call1 := make(chan *Call, 10)
	call2 := make(chan *Call, 10)

	var reply1, reply2 interface{}

	//返回值接口反射
	if reply != nil {
		reply1 = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface()
		reply2 = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface()
	}
	//第一个节点发起调用
	_, err1 := c.Go(ctx, serviceMethod, args, reply1, call1)
	
	t := time.NewTimer(c.option.BackupLatency)
	select {
	case <-ctx.Done(): //cancel by context
		err = ctx.Err()
		return err
	case call := <-call1:
		err = call.Error
		if err == nil && reply != nil {
            //通过反射复制值
			reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply1).Elem())
		}
		return err
	case <-t.C://c.option.BackupLatency没有返回,则继续往下走,发起第二个节点调用

	}
	//第二个节点发起调用
	_, err2 := c.Go(ctx, serviceMethod, args, reply2, call2)
	if err2 != nil {
		if uncoverError(err2) {
			c.removeClient(k, client)
		}
		err = err1
		return err
	}

	select {
	case <-ctx.Done(): //cancel by context
		err = ctx.Err()
	case call := <-call1://第一个节点返回值
		err = call.Error
		if err == nil && reply != nil && reply1 != nil {
			reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply1).Elem())
		}
	case call := <-call2://第二个节点返回值
		err = call.Error
		if err == nil && reply != nil && reply2 != nil {
			reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply2).Elem())
		}
	}

	return err

至此,失败策略全部讲解完毕。

需要注意的是:FailMode的设置仅仅对同步调用有效(XClient.Call), 异步调用用,这个参数是无意义的。

3、远程调用

这是是指wrapCall方法

func (c *xClient) wrapCall(ctx context.Context, client RPCClient, serviceMethod string, args interface{}, reply interface{}) error {
	if client == nil {
		return ErrServerUnavailable
	}

	ctx = share.NewContext(ctx)
    //调用前处理,用户自定义plugin
	c.Plugins.DoPreCall(ctx, c.servicePath, serviceMethod, args)
    //发起socket调用
	err := client.Call(ctx, c.servicePath, serviceMethod, args, reply)
    //调用后处理,用户自定义plugin
	c.Plugins.DoPostCall(ctx, c.servicePath, serviceMethod, args, reply, err)

	return err
}

结语

我打算花足够多的时间来和大家读一读rpcx的源码,来一层层的剖解rpcx,有兴趣的朋友,可以关注我。

下一篇我们分析序列化/反序列化

相关阅读

rpc框架之rpcx-简介(1)

rpc框架之rpcx-服务注册与服务发现(2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值