Go 微服務實踐
簡介
近一兩年來,微服務架構已經成為熱門話題( microservices.io ),與傳統的一體化應用架構相比,微服務架構在開發、測試、部署方面都有眾多吸引人之處,越來越多沒有歷史包袱的新項目都啟用微服務架構的模式來開發。
我們這個團隊經過深入思考之后,決定在一起美這個APP的后端開發中,選擇G o 作為開發語言,采用微服務模式來實現,經過近半年的實踐,形成了一些心得,簡單總結后分享出來,希望能夠給大家一些幫助。
框架選擇
不同的團隊在選擇基礎框架(庫)時考慮的要素不同,我們團隊更喜歡小而美的框架,盡可能不要讓框架侵入業務,易于升級、維護和替換,所以我們更愿意選擇Library而不是Framework。
在web方面,我們選擇了 negroni 作為middleware庫,采用性能不錯的 httprouter 替換go標準庫的mux,而沒有用任何web相關的框架。
在微服務之間的rpc調用方面,為了將來的擴展性、跨語言調用等因素,我們沒有直接用go標準庫的rpc模塊,而是采納了google最新推出的grpc。但grpc本身屬于比較重型的rpc框架,對業務代碼有一定的侵入性,我們做了一系列的庫(包括 worpc 、 worc 、 wonaming 等https://github.com/wothing)來屏蔽這些不必要的業務代碼侵入,保持了業務代碼本身的整潔。
微服務劃分
在微服務體系中,如何切分微服務也是一個重要的話題,在我們的實踐中,我們遵循了如下一些原則:
-
邏輯獨立、邊界清晰的模塊作為一個獨立的微服務
-
每個table只由一個微服務操作(包括插入、讀取、更改、刪除等)
-
table之間不引入外鍵約束,id字段全部采用uuid
-
將需要保持數據一致性的操作放在一個微服務中,避免跨服務帶來的數據一致性難題
-
微服務之間的通信,盡可能采用消息隊列實現松耦合,當需要同步調用時再借助于rpc
-
微服務獨立部署,通過etcd實現服務的注冊與發現
總體架構
Gateway
Gateway是微服務對外提供服務的一個屏障,它的核心點在于:
-
屏蔽微服務之間通過消息隊列、rpc等通信方式,為Web頁面和移動APP提供基于HTTP協議的RESTful API接口
-
對每一個http業務請求進行必要的鑒權和數據完整性、合法性檢查,以減少微服務的負擔,讓微服務的代碼更純粹
-
微服務部署體系中,每個微服務可能會部署多個實例,Gateway還承擔著在這些實例中進行負載均衡的功能
-
進行必要的日志輸出、監控打點等功能,對每一個來自于APP和頁面的http請求,生成一個唯一的trace id,并將trace id傳導到每一個后續的微服務中,以便后續的查錯和性能調優
-
Gateway的每一個http請求都是無狀態的,采用JWT(Json Web Token)機制實現一個客戶端的請求狀態信息的傳遞
服務的注冊與發現 wonaming
微服務體系中,服務的注冊和發現對整體架構非常重要,尤其對于同步的rpc調用,每個服務有多少實例,每個實例的地址等,都需要有一個統一的管理。我們采用etcd保存服務信息,同時封裝了wonaming作為微服務注冊和發現的中間件,它的主要功能包括:
-
服務在啟動時,調用wonaming向etcd注冊包含TTL的服務“索引”、
-
注冊后,服務與etcd保持定時心跳,當微服務主動退出或超時,服務解注冊并“下線”
-
在Gateway中,通過resolver進行服務發現,配合grpc提供的balancer實現負載均衡,resolver啟動后會對etcd中的 /wonaming 目錄進行監控,當有服務注冊或者解注冊時,動態維護可用服務清單。
r := wonaming.NewResolver(name) b := grpc.RoundRobin(r) conn, err := grpc.Dial(etcd, grpc.WithInsecure(), grpc.WithBalancer(b))
服務的rpc調用 worc
grpc是一個比較重的rpc框架,當客戶端通過grpc調用服務端時,需要大量的重復性代碼來建立連接、調用、處理錯誤返回等,影響業務代碼的整潔性,并且對業務代碼具有很強的侵入性,為了規避這個問題,我們封裝了worc,以實現便捷的grpc調用:
resp, err := worc.CallRPC(ctx, "hello", "Hello", req)
grpc的中間件鏈 worpc
grpc提供了interceptor機制,但并沒有提供chain來實現不同的中間件的順序執行,為了將不同的中間件功能(如鑒權、日志、recover)封裝在不同的函數里,worpc提供了組合gprc interceptor為一個chain的能力,可以根據自身業務的需要,撰寫不同的grpc中間件進行組合,比如實現 grpc 的 recovery 與 log 中間件:
func Recovery(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
// log stack
stack := make([]byte, MAXSTACKSIZE)
stack = stack[:runtime.Stack(stack, false)]
log.CtxErrorf(ctx, "panic grpc invoke: %s, err=%v, stack:\n%s", info.FullMethod, r, string(stack))
// if panic, set custom error to 'err', in order that client and sense it.
err = grpc.Errorf(codes.Internal, "panic error: %v", r)
}
}()
return handler(ctx, req)
}
func Logging(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
start := time.Now()
log.CtxInfof(ctx, "calling %s, req=%s", info.FullMethod, marshal(req))
resp, err = handler(ctx, req)
log.CtxInfof(ctx, "finished %s, took=%v, resp=%v, err=%v", info.FullMethod, time.Since(start), marshal(resp), err)
return resp, err
}
s := grpc.NewServer(grpc.UnaryInterceptor(worpc.UnaryInterceptorChain(worpc.Recovery, worpc.Logging)))</code></pre>
通過以上組合,可為微服務提供panic恢復能力,保障服務穩定可用;同時還將上文中提到的注入context中的trace id取出,這樣Gateway與微服務的日志通過trace id就銜接了起來,方便查錯、調優等。
其它經驗
-
使用grpc.Errorf封裝業務中的邏輯錯誤,隨grpc服務調用一起返回,將業務response與error 分離。
-
數據可在Gateway中完成組裝工作,但無需刻意避免微服務互調,理清依賴關系,尤其當protobuf升級時,根據具體業務來判斷引用微服務是否需要同步重部署。
-
微服務雖好,但一定程度上會加大實施難度,要根據業務體量合理入坑。
總結
以上是微服務架構在我們團隊的實踐方案,麻雀雖小,五臟俱全。通過各中間件的靈活組合,保障業務有序與服務的高可用,還不抓緊實踐起來?在后續的文章中,我們還會介紹目前微服務測試、運維及部署方案。
來自:http://mp.weixin.qq.com/s?__biz=MjM5OTcxMzE0MQ==&mid=2653369755&idx=1&sn=73e69c0e4b0d01f0b3f6530d6f07507f&scene=0