從零開始實現一個RPC框架(二)

熊紀元發表於2019-03-17

前言

上一篇文章裡我們實現了基本的RPC客戶端和服務端,這次我們開始著手實現更上層的功能。篇幅所限,具體的程式碼實現參見:程式碼地址

基礎支撐部分

升級版的Client和Server

client實現

server實現

首先讓我們來重新定義Client和Server:SGClient和SGServer。SGClient封裝了上一節定義的RPCClient的操作,提供服務治理的相關特性;SGServer則由上一節定義的RPCServer升級而來,支援服務治理的相關特性。這裡的SG(service governance)表示服務治理。 這裡直接貼上相關的定義:

type SGClient interface {
	Go(ctx context.Context, ServiceMethod string, arg interface{}, reply interface{}, done chan *Call) (*Call, error)
	Call(ctx context.Context, ServiceMethod string, arg interface{}, reply interface{}) error
}
type sgClient struct {
	shutdown  bool
	option    SGOption
	clients   sync.Map //map[string]RPCClient
	serversMu sync.RWMutex
	servers   []registry.Provider
}
type RPCServer interface {
	Register(rcvr interface{}, metaData map[string]string) error
	Serve(network string, addr string) error
	Services() []ServiceInfo
	Close() error
}
type SGServer struct { //原來的RPCServer
	codec      codec.Codec
	serviceMap sync.Map
	tr         transport.ServerTransport
	mutex      sync.Mutex
	shutdown   bool
	Option Option
}
複製程式碼

攔截器

在之前的文章提到過,我們需要提供過濾器一樣的使用方式,來達到對擴充套件開放對修改關閉的目標。我們這裡採用高階函式的方式來定義方切面和法攔截器,首先定義幾個切面:

//客戶端切面
type CallFunc func(ctx context.Context, ServiceMethod string, arg interface{}, reply interface{}) error
type GoFunc func(ctx context.Context, ServiceMethod string, arg interface{}, reply interface{}, done chan *Call) *Call
//服務端切面
type ServeFunc func(network string, addr string) error
type ServeTransportFunc func(tr transport.Transport)
type HandleRequestFunc func(ctx context.Context, request *protocol.Message, response *protocol.Message, tr transport.Transport)
複製程式碼

以上幾個是RPC呼叫在客戶端和服務端會經過的幾個函式,我們將其定義為切面,然後再定義對應的攔截器:

//客戶端攔截器
packege client
type Wrapper interface {
	WrapCall(option *SGOption, callFunc CallFunc) CallFunc
	WrapGo(option *SGOption, goFunc GoFunc) GoFunc
}
//f服務端攔截器
package server
type Wrapper interface {
	WrapServe(s *SGServer, serveFunc ServeFunc) ServeFunc
	WrapServeTransport(s *SGServer, transportFunc ServeTransportFunc) ServeTransportFunc
	WrapHandleRequest(s *SGServer, requestFunc HandleRequestFunc) HandleRequestFunc
}
複製程式碼

這樣一來,使用者可以通過實現Wapper介面來對客戶端或者服務端的行為進行增強,比如將請求引數和結果記錄到日誌裡,動態的修改引數或者響應等等。我們的框架自身 的相關功能也可以通過Wrapper實現。目前客戶端實現了用於封裝後設資料的MetaDataWrapper和記錄請求和響應的LogWrapper;服務端目前在DefaultWrapper實現了用於服務註冊、監聽退出訊號以及請求計數的邏輯。

因為go並不提供抽象類的方式,所以對於某些實現類可能並不需要攔截所有切面(比如只攔截Call不想攔截Go),這種情況直接返回引數裡的函式物件就可以了。

客戶端攔截器實現

服務端攔截器實現

服務治理部分

服務註冊與發現

在這之前,我們的RPC服務呼叫都是通過在客戶端指定服務端的ip和埠來呼叫的,這種方式十分簡單但也場景十分有限,估計只能在測試或者demo中使用。所以我們需要提供服務註冊和發現相關的功能,讓客戶端的配置不再與實際的IP繫結,而是通過獨立的註冊中心獲取服務端的列表,並且能夠在服務端節點變更時獲得實時更新。

首先定義相關的介面(程式碼地址):

//Registry包含兩部分功能:服務註冊(用於服務端)和服務發現(用於客戶端)
type Registry interface {
	Register(option RegisterOption, provider ...Provider) //註冊
	Unregister(option RegisterOption, provider ...Provider) //登出
	GetServiceList() []Provider //獲取服務列表
	Watch() Watcher //監聽服務列表的變化
	Unwatch(watcher Watcher) //取消監聽
}
type RegisterOption struct {
	AppKey string //AppKey用於唯一標識某個應用
}
type Watcher interface {
	Next() (*Event, error) //獲取下一次服務列表的更新
	Close()
}
type EventAction byte
const (
	Create EventAction = iota
	Update
	Delete
)
type Event struct { //Event表示一次更新
	Action    EventAction
	AppKey    string
	Providers []Provider //具體變化的服務提供者(增量而不是全量)
}
type Provider struct { //某個具體的服務提供者
	ProviderKey string // Network+"@"+Addr
	Network     string
	Addr        string
	Meta        map[string]string
}
複製程式碼

AppKey

我們使用AppKey這樣一個概念來標識某個服務,比如com.meituan.demo.rpc.server。服務端在啟動時將自身的相關資訊(包括AppKey、ip、port、方法列表等)註冊到註冊中心;客戶端在需要呼叫時只需要根據服務端的AppKey到註冊中心查詢即可。

目前暫時只實現了直連(peer2peer)和基於記憶體(InMemory)的服務註冊,後續再接入其他獨立的元件如etcd或者zookeeper等等。

InMemory程式碼實現地址

負載均衡

有了服務註冊與發現之後,一個客戶端所面對的可能就不只有一個服務端了,客戶端在發起呼叫前需要從多個服務端中選擇一個出來進行實際的通訊,具體的選擇策略有很多,比如隨機選擇、輪詢、基於權重選擇、基於服務端負載或者自定義規則等等。

這裡先給出介面定義:

//Filter用於自定義規則過濾某個節點
type Filter func(provider registry.Provider, ctx context.Context, ServiceMethod string, arg interface{}) bool
type SelectOption struct {
	Filters []Filter
}
type Selector interface {
	Next(providers []registry.Provider, ctx context.Context, ServiceMethod string, arg interface{}, opt SelectOption) (registry.Provider, error)
}
複製程式碼

目前暫時只實現了隨機負載均衡,後續再實現其他策略比如輪詢或者一致性雜湊等等,使用者也可以選擇實現自己的負載均衡策略。

容錯處理

長連線以及網路重連

為了減少頻繁建立和斷開網路連線的開銷,我們維持了客戶端到服務端的長連線,並把建立好的連線(RPCClient物件)用map快取起來,key就是對應的服務端的標識。客戶端在呼叫前根據負載均衡的結果檢索到快取好的RPCClient然後發起呼叫。當我們檢索不到對應的客戶端或者發現快取的客戶端已經失效時,需要重新建立連線(重新建立RPCClient物件)。

func (c *sgClient) selectClient(ctx context.Context, ServiceMethod string, arg interface{}) (provider registry.Provider, client RPCClient, err error) {
        //根據負載均衡決定要呼叫的服務端
	provider, err = c.option.Selector.Next(c.providers(), ctx, ServiceMethod, arg, c.option.SelectOption)
	if err != nil {
		return
	}
	client, err = c.getClient(provider)
	return
}

func (c *sgClient) getClient(provider registry.Provider) (client RPCClient, err error) {
	key := provider.ProviderKey
	rc, ok := c.clients.Load(key)
	if ok {
		client := rc.(RPCClient)
		if client.IsShutDown() {
		    //如果已經失效則清除掉
			c.clients.Delete(key)
		}
	}
        //再次檢索
	rc, ok = c.clients.Load(key)
	if ok {
	        //已經有快取了,返回快取的RPCClient
		client = rc.(RPCClient)
	} else {
	        //沒有快取,新建一個然後更新到快取後返回
		client, err = NewRPCClient(provider.Network, provider.Addr, c.option.Option)
		if err != nil {
			return
		}
		c.clients.Store(key, client)
	}
	return
}
複製程式碼

目前的實現當中,每個服務提供者只有一個對應的RPCClient,後續可以考慮類似連線池的實現,即每個服務提供者對應多個RPCClient,每次呼叫前從連線池中取出一個RPCClient。

叢集容錯

在分散式系統中,異常是不可避免的,當發生呼叫失敗時,我們可以選擇要採取的處理方式,這裡列舉了常見的幾種:

type FailMode byte
const (
	FailFast FailMode = iota //快速失敗
	FailOver //重試其他伺服器
	FailRetry //重試同一個伺服器
	FailSafe //忽略失敗,直接返回
)
複製程式碼

具體實現比較簡單,就是根據配置的容錯選項和重試次數決定是否重試;其他包括FailBack(延時一段時間後重發)、Fork以及Broadcast等等暫時沒有實現。

優雅退出

在收到程式退出訊號時,server端會嘗試優先處理完當前還未結束的請求,等請求處理完畢之後再退出,當超出了指定的時間(預設12s)仍未處理完畢時,server端會直接退出。

func (s *SGServer) Close() error {
	s.mutex.Lock()
	defer s.mutex.Unlock()
	s.shutdown = true
	//等待當前請求處理完或者直到指定的時間
	ticker := time.NewTicker(s.Option.ShutDownWait)
	defer ticker.Stop()
	for {
		if s.requestInProcess <= 0 { //requestInProcess表示當前正在處理的請求數,在wrapper裡計數
			break
		}
		select {
		case <-ticker.C:
			break
		}
	}
	return s.tr.Close()
}
複製程式碼

結語

到這裡就是這次的全部內容了,總的來說是在之前的基礎上做了封裝,預留了後續的擴充套件點,然後實現了簡單的服務治理相關的功能。總結一下,這次我們在上一篇文章的基礎上做了以下改動:

  1. 重新定義了Client和Server的介面
  2. 提供了攔截器(Wrapper介面)
  3. 提供了服務註冊與發現以及負載均衡的介面和簡單實現
  4. 實現了簡單的容錯處理
  5. 實現了簡單的優雅退出
  6. 增加了gob序列化方式支援(比較簡單,文章裡並沒有提到)

歷史連結

從零開始實現一個RPC框架(零)

從零開始實現一個RPC框架(一)

相關文章