go rpc 原始碼分析

g4zhuj發表於2019-02-27

1.概述

go 原始碼中帶了rpc框架,以相對精簡的當時方式實現了rpc功能,目前原始碼中的rpc官方已經宣佈不再新增新功能,並推薦使用grpc.
作為go標準庫中rpc框架,還是有很多地方值得借鑑及學習,這裡將從原始碼角度分析go原生rpc框架。

2.server端

server端主要分為兩個步驟,首先進行方法註冊,通過反射處理將方法取出,並存到map中.然後是網路呼叫,主要是監聽埠,讀取資料包,解碼請求
呼叫反射處理後的方法,將返回值編碼,返回給客戶端.

2.1 方法註冊

圖
2.1.1 Register
// Register publishes the receiver`s methods in the DefaultServer.
func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) }

// RegisterName is like Register but uses the provided name for the type
// instead of the receiver`s concrete type.
func RegisterName(name string, rcvr interface{}) error {
	return DefaultServer.RegisterName(name, rcvr)
}
複製程式碼

如上,方法註冊的入口函式有兩個,分別為Register以及RegisterName,這裡interface{}通常是帶方法的物件.如果想要自定義方法的接收物件,則可以使用RegisterName.

2.1.2 反射處理過程
type methodType struct {
	sync.Mutex // protects counters
	method     reflect.Method    //反射後的函式
	ArgType    reflect.Type      //請求引數的反射值
	ReplyType  reflect.Type      //返回引數的反射值
	numCalls   uint              //呼叫次數
}


type service struct {
	name   string                 // 服務名,這裡通常為register時的物件名或自定義物件名
	rcvr   reflect.Value          // 服務的接收者的反射值
	typ    reflect.Type           // 接收者的型別
	method map[string]*methodType // 物件的所有方法的反射結果.
}
複製程式碼

反射處理過程,其實就是將物件以及物件的方法,通過反射生成上面的結構,如註冊Arith.Multiply(xx,xx) error 這樣的物件時,生成的結構為 map[“Arith”]*service, service 中ethod為 map[“Multiply”]*methodType.

幾個關鍵程式碼如下:

生成service物件

func (server *Server) register(rcvr interface{}, name string, useName bool) error {
	//生成service
    s := new(service)
	s.typ = reflect.TypeOf(rcvr)
	s.rcvr = reflect.ValueOf(rcvr)
	sname := reflect.Indirect(s.rcvr).Type().Name()
 
    ....
	s.name = sname

	// 通過suitableMethods將物件的方法轉換成map[string]*methodType結構
	s.method = suitableMethods(s.typ, true)
    
    ....

    //service儲存為鍵值對
	if _, dup := server.serviceMap.LoadOrStore(sname, s); dup {
		return errors.New("rpc: service already defined: " + sname)
	}
	return nil
}
複製程式碼

生成 map[string] *methodType


func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType {
	methods := make(map[string]*methodType)

    //通過反射,遍歷所有的方法
	for m := 0; m < typ.NumMethod(); m++ {
		method := typ.Method(m)
		mtype := method.Type
		mname := method.Name
		// Method must be exported.
		if method.PkgPath != "" {
			continue
		}
		// Method needs three ins: receiver, *args, *reply.
		if mtype.NumIn() != 3 {
			if reportErr {
				log.Println("method", mname, "has wrong number of ins:", mtype.NumIn())
			}
			continue
		}
        //取出請求引數型別
		argType := mtype.In(1)
        ...

		// 取出響應引數型別,響應引數必須為指標
		replyType := mtype.In(2)
		if replyType.Kind() != reflect.Ptr {
			if reportErr {
				log.Println("method", mname, "reply type not a pointer:", replyType)
			}
			continue
		}
		...


		// 去除函式的返回值,函式的返回值必須為error.
		if returnType := mtype.Out(0); returnType != typeOfError {
			if reportErr {
				log.Println("method", mname, "returns", returnType.String(), "not error")
			}
			continue
		}
        
        //將方法儲存成key-value
		methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}
	}
	return methods
}
複製程式碼

2.2 網路呼叫

// Request 每次rpc呼叫的請求的頭部分
type Request struct {
	ServiceMethod string   // 格式為: "Service.Method"
	Seq           uint64   // 客戶端生成的序列號
	next          *Request // server端保持的連結串列
}

// Response 每次rpc呼叫的響應的頭部分
type Response struct {
	ServiceMethod string    // 對應請求部分的 ServiceMethod
	Seq           uint64    // 對應請求部分的 Seq
	Error         string    // 錯誤
	next          *Response // server端保持的連結串列
}

複製程式碼

如上,網路呼叫主要用到上面的兩個結構體,分別是請求引數以及返回引數,通過編解碼器(gob/json)實現二進位制到結構體的相互轉換.主要涉及到下面幾個步驟:

圖

關鍵程式碼如下:
取出請求,並得到相應函式的呼叫引數

func (server *Server) readRequestHeader(codec ServerCodec) (svc *service, mtype *methodType, req *Request, keepReading bool, err error) {
	// Grab the request header.
	req = server.getRequest()
    //編碼器讀取生成請求
	err = codec.ReadRequestHeader(req)
	if err != nil {
        //錯誤處理
        ...
		return
	}

	keepReading = true

    //取出服務名以及方法名
	dot := strings.LastIndex(req.ServiceMethod, ".")
	if dot < 0 {
		err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod)
		return
	}
	serviceName := req.ServiceMethod[:dot]
	methodName := req.ServiceMethod[dot+1:]

	//從註冊時生成的map中查詢出相應的方法的結構
	svci, ok := server.serviceMap.Load(serviceName)
	if !ok {
		err = errors.New("rpc: can`t find service " + req.ServiceMethod)
		return
	}
	svc = svci.(*service)

    //獲取出方法的型別
	mtype = svc.method[methodName]
	if mtype == nil {
		err = errors.New("rpc: can`t find method " + req.ServiceMethod)
	}

複製程式碼

//迴圈處理,不斷讀取連結上的位元組流,解密出請求,呼叫方法,編碼響應,回寫到客戶端.

func (server *Server) ServeCodec(codec ServerCodec) {
	sending := new(sync.Mutex)
	for {
		//讀取請求
        service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)
		if err != nil {
            ...
		}

        //呼叫
		go service.call(server, sending, mtype, req, argv, replyv, codec)
	}
	codec.Close()
}
複製程式碼

通過引數進行函式呼叫

func (s *service) call(server *Server, sending *sync.Mutex, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {
	mtype.Lock()
	mtype.numCalls++
	mtype.Unlock()
	function := mtype.method.Func
	// 通過反射進行函式呼叫
	returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})
	// 返回值是不為空時,則取出錯誤的string
	errInter := returnValues[0].Interface()
	errmsg := ""
	if errInter != nil {
		errmsg = errInter.(error).Error()
	}
    
    //傳送相應,並釋放請求結構
    server.sendResponse(sending, req, replyv.Interface(), codec, errmsg)
	server.freeRequest(req)
}


複製程式碼

3.client端

// 非同步呼叫
func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
}

// 同步呼叫
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
}

複製程式碼
// Call represents an active RPC.
type Call struct {
	ServiceMethod string      // 服務名及方法名 格式:服務.方法
	Args          interface{} // 函式的請求引數 (*struct).
	Reply         interface{} // 函式的響應引數 (*struct).
	Error         error       // 方法完成後 error的狀態.
	Done          chan *Call  // 方法呼叫結束後的channel.
}

複製程式碼

client端部分則相對要簡單很多,主要提供Call以及Go兩個方法,分別表示同步呼叫以及非同步呼叫,但其實同步呼叫底層實現其實也是非同步呼叫,呼叫時主要用到了Call結構,相關解釋如上.

3.1 主要流程

圖

3.2 關鍵程式碼

傳送請求部分程式碼,每次send一次請求,均生成一個call物件,並使用seq作為key儲存在map中,服務端返回時從map取出call,進行相應處理.

func (client *Client) send(call *Call) {
    //請求級別的鎖
	client.reqMutex.Lock()
	defer client.reqMutex.Unlock()

	// Register this call.
	client.mutex.Lock()
	if client.shutdown || client.closing {
		call.Error = ErrShutdown
		client.mutex.Unlock()
		call.done()
		return
	}

    //生成seq,每次呼叫均生成唯一的seq,在服務端相應後會通過該值進行匹配
	seq := client.seq
	client.seq++
	client.pending[seq] = call
	client.mutex.Unlock()

	// 請求併傳送請求
	client.request.Seq = seq
	client.request.ServiceMethod = call.ServiceMethod
	err := client.codec.WriteRequest(&client.request, call.Args)
	if err != nil {
        //傳送請求錯誤時,將map中call物件刪除.
		client.mutex.Lock()
		call = client.pending[seq]
		delete(client.pending, seq)
		client.mutex.Unlock()
		if call != nil {
			call.Error = err
			call.done()
		}
	}
}

複製程式碼

接收響應部分的程式碼,這裡是一個for迴圈,不斷讀取tcp上的流,並解碼成Response物件以及方法的Reply物件.

func (client *Client) input() {
	var err error
	var response Response
	for err == nil {
		response = Response{}
		err = client.codec.ReadResponseHeader(&response)
		if err != nil {
			break
		}

        //通過response中的 Seq獲取call物件
		seq := response.Seq
		client.mutex.Lock()
		call := client.pending[seq]
		delete(client.pending, seq)
		client.mutex.Unlock()

		switch {
		case call == nil:
			err = client.codec.ReadResponseBody(nil)
			if err != nil {
				err = errors.New("reading error body: " + err.Error())
			}
		case response.Error != "":
            //服務端返回錯誤,直接將錯誤返回
			call.Error = ServerError(response.Error)
			err = client.codec.ReadResponseBody(nil)
			if err != nil {
				err = errors.New("reading error body: " + err.Error())
			}
			call.done()
		default:
            //通過編碼器,將Resonse的body部分解碼成reply物件.
			err = client.codec.ReadResponseBody(call.Reply)
			if err != nil {
				call.Error = errors.New("reading body " + err.Error())
			}
			call.done()
		}
	}

	// 客戶端退出處理
	client.reqMutex.Lock()
	client.mutex.Lock()
	client.shutdown = true
	closing := client.closing
	if err == io.EOF {
		if closing {
			err = ErrShutdown
		} else {
			err = io.ErrUnexpectedEOF
		}
	}
	for _, call := range client.pending {
		call.Error = err
		call.done()
	}
	client.mutex.Unlock()
	client.reqMutex.Unlock()
	if debugLog && err != io.EOF && !closing {
		log.Println("rpc: client protocol error:", err)
	}
}

複製程式碼

4.一些缺點

  • 同步呼叫無法超時
    由於原生rpc只提供兩個方法,同步的Call以及非同步的Go,同步的Call服務端不返回則會一直阻塞,這裡如果存在大量的不返回,會導致協程一直無法釋放.

  • 非同步呼叫超時後會記憶體洩漏
    基於非同步呼叫加channel實現超時功能也會存在洩漏問題,原因是client的請求會存在map結構中,Go函式退出並不會清理map的內容,因此如果server端不返回的話,map中的請求會一直儲存,從而導致記憶體洩漏.

  • 底層連結狀態無法維持
    由於沒有keepalive機制,當對底層連結進行復用時會出現連結實際已經不可用,但上層無法感知到的情況,從而導致發出請求,一直無法收到回應.

5. 總結

總的來說,go原生rpc算是個基礎版本的rpc,程式碼精簡,可擴充套件性高,但是隻是實現了rpc最基本的網路通訊,像超時熔斷,連結管理(保活與重連),服務註冊發現,還是欠缺的,因此還是達不到生產環境開箱即用,相對來說grpc則要成熟很多,最近準備基於grpc整合一套微服務通訊框架,大部分元件都是開源的,專案見grpc-wrapper.

6. 參考

rpc

相關文章