這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
概述
grpc 是Google開源的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的版本.
完整代碼以及使用樣本見:https://github.com/g4zhuj/grp...
2.1 用戶端
具體需要實現幾個介面,針對用戶端,最簡單的實現方式只需要實現兩個介面方法Resolve(),以及Next(),然後使用輪詢的負載平衡方式.
主要通過etcd的Get介面以及Watch介面實現.
//用於產生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介面主要用於擷取註冊的服務資訊,通過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. 參考
https://grpc.io/
https://coreos.com/etcd/
https://github.com/g4zhuj/grp...