go基於grpc構建微服務框架-服務註冊與發現

g4zhuj發表於2018-04-20

概述

圖
grpc 是谷歌開源的rpc框架,基於http2實現,並支援跨語言,目前基本涵蓋了主流語言.跨語言的實現主要得益於protobuf,通過編寫proto檔案,通過protobuf工具生成對應語言的類庫進行使用.

對於go這樣一門新生語言來說,生態鏈還處於發展階段,微服務框架也是如此,下面將基於grpc-go版本搭建一個微服務通訊框架.

1.服務註冊與釋出的機制

1.1 解決的問題

服務註冊與釋出主要解決的服務依賴問題,通常意義上,如果A服務呼叫B服務時,最直接的做法是配置IP地址和埠.但隨著服務依賴變多時,配置將會十分龐雜,且當服務發生遷移時,那麼所有相關服務的配置均需要修改,這將十分難以維護以及容易出現問題. 因此為了解決這種服務依賴關係,服務註冊與釋出應運而生.

1.2 機制

圖
服務註冊與發現主要分為以下幾點.

  • 服務資訊釋出
    這裡主要是服務的服務名,IP資訊,以及一些附件後設資料.通過註冊介面註冊到服務註冊釋出中心.
  • 存活檢測
    當服務意外停止時,客戶端需要感知到服務停止,並將服務的IP地址踢出可用的IP地址列表,這裡可以使用定時心跳去實現.
  • 客戶端負載均衡
    通過服務註冊與釋出,可以實現一個服務部署多臺例項,客戶端實現在例項直接的負載均衡,從而實現服務的橫向擴充套件.

因此,服務註冊與釋出可以概況為,服務將資訊上報,客戶端拉取服務資訊,通過服務名進行呼叫,當服務當機時客戶端踢掉故障服務,服務新上線時客戶端自動新增到呼叫列表.

2.實現

grpc-go的整個實現大量使用go的介面特性,因此通過擴充套件介面,可以很容易的實現服務的註冊與發現,這裡服務註冊中心考慮到可用性以及一致性,一般採用etcd或zookeeper來實現,這裡實現etcd的版本.
完整程式碼以及使用示例見:grpc-wrapper

2.1 客戶端

具體需要實現幾個介面,針對客戶端,最簡單的實現方式只需要實現兩個介面方法Resolve(),以及Next(),然後使用輪詢的負載均衡方式.
主要通過etcd的Get介面以及Watch介面實現.

  • Resolve()介面
//用於生成Watcher,監聽註冊中心中的服務資訊變化
func (er *etcdRegistry) Resolve(target string) (naming.Watcher, error) {
	ctx, cancel := context.WithTimeout(context.TODO(), resolverTimeOut)
	w := &etcdWatcher{
		cli:    er.cli,
		target: target + "/",
		ctx:    ctx,
		cancel: cancel,
	}
	return w, nil
}

複製程式碼
  • Next() 介面
//Next介面主要用於獲取註冊的服務資訊,通過channel以及watch,當服務資訊發生
//變化時,Next介面會將變化返回給grpc框架從而實現服務資訊變更.
func (ew *etcdWatcher) Next() ([]*naming.Update, error) {
	var updates []*naming.Update
    //初次獲取時,建立監聽channel,並返回獲取到的服務資訊
	if ew.watchChan == nil {
		//create new chan
		resp, err := ew.cli.Get(ew.ctx, ew.target, etcd.WithPrefix(), etcd.WithSerializable())
		if err != nil {
			return nil, err
		}
		for _, kv := range resp.Kvs {
			var upt naming.Update
			if err := json.Unmarshal(kv.Value, &upt); err != nil {
				continue
			}
			updates = append(updates, &upt)
		}
        //建立etcd的watcher監聽target(服務名)的資訊.
		opts := []etcd.OpOption{etcd.WithRev(resp.Header.Revision + 1), etcd.WithPrefix(), etcd.WithPrevKV()}
		ew.watchChan = ew.cli.Watch(context.TODO(), ew.target, opts...)
		return updates, nil
	}

    //阻塞監聽,服務發生變化時才返回給上層
	wrsp, ok := <-ew.watchChan
	if !ok {
		err := status.Error(codes.Unavailable, "etcd watch closed")
		return nil, err
	}
	if wrsp.Err() != nil {
		return nil, wrsp.Err()
	}
	for _, e := range wrsp.Events {
		var upt naming.Update
		var err error
		switch e.Type {
		case etcd.EventTypePut:
			err = json.Unmarshal(e.Kv.Value, &upt)
			upt.Op = naming.Add
		case etcd.EventTypeDelete:
			err = json.Unmarshal(e.PrevKv.Value, &upt)
			upt.Op = naming.Delete
		}

		if err != nil {
			continue
		}
		updates = append(updates, &upt)
	}
	return updates, nil
}

複製程式碼

2.2 服務端

服務端只需要上報服務資訊,並定時保持心跳,這裡通過etcd的Put介面以及KeepAlive介面實現. 具體如下:

func (er *etcdRegistry) Register(ctx context.Context, target string, update naming.Update, opts ...wrapper.RegistryOptions) (err error) {
	//將服務資訊序列化成json格式
    var upBytes []byte
	if upBytes, err = json.Marshal(update); err != nil {
		return status.Error(codes.InvalidArgument, err.Error())
	}

	ctx, cancel := context.WithTimeout(context.TODO(), resolverTimeOut)
	er.cancal = cancel
	rgOpt := wrapper.RegistryOption{TTL: wrapper.DefaultRegInfTTL}
	for _, opt := range opts {
		opt(&rgOpt)
	}

	switch update.Op {
	case naming.Add:
		lsRsp, err := er.lsCli.Grant(ctx, int64(rgOpt.TTL/time.Second))
		if err != nil {
			return err
		}

        //Put服務資訊到etcd,並設定key的值TTL,通過後面的KeepAlive介面
        //對TTL進行續期,超過TTL的時間未收到續期請求,則說明服務可能掛了,從而清除服務資訊
		etcdOpts := []etcd.OpOption{etcd.WithLease(lsRsp.ID)}
		key := target + "/" + update.Addr
		_, err = er.cli.KV.Put(ctx, key, string(upBytes), etcdOpts...)
		if err != nil {
			return err
		}

        //保持心跳
		lsRspChan, err := er.lsCli.KeepAlive(context.TODO(), lsRsp.ID)
		if err != nil {
			return err
		}
		go func() {
			for {
				_, ok := <-lsRspChan
				if !ok {
					grpclog.Fatalf("%v keepalive channel is closing", key)
					break
				}
			}
		}()
	case naming.Delete:
		_, err = er.cli.Delete(ctx, target+"/"+update.Addr)
	default:
		return status.Error(codes.InvalidArgument, "unsupported op")
	}
	return nil
}
複製程式碼

3. 參考

grpc
etcd
grpc-wrapper

相關文章