本文做者:陳明傑(sandyskies)
Tars是騰訊從2008年到今天一直在使用的後臺邏輯層的統一應用框架,目前支持C++,Java,PHP,Nodejs,Golang語言。該框架爲用戶提供了涉及到開發、運維、以及測試的一整套解決方案,幫助一個產品或者服務快速開發、部署、測試、上線。 它集可擴展協議編解碼、高性能RPC通訊框架、名字路由與發現、發佈監控、日誌統計、配置管理等於一體,經過它能夠快速用微服務的方式構建本身的穩定可靠的分佈式應用,並實現完整有效的服務治理。目前該框架在騰訊內部,各大核心業務都在使用,頗受歡迎,基於該框架部署運行的服務節點規模達到上萬個。
Tars 於2017年4月開源,並於2018年6月加入Linux 基金會。
TarsGo 是Tars 的Go語言實現版本, 於2018年9月開源, 項目地址 https://github.com/TarsCloud/... ,歡迎star 。git
在上次開源以後,有些用戶反饋了一些需求,基於用戶反饋的需求,咱們進行了實現,併發布了1.1.0版本。 本次發佈新增了:支持pb、支持zipkin分佈式追蹤、支持filter(自定義插件編寫)、支持context 等,除此以外還作了一系列優化和bugfix。github
Protocol Buffers (簡稱 PB )是 Google 的一種數據交換的格式,它獨立於語言,獨立於平臺,最先公佈於 2008年7月。隨着微服務架構的發展及自身的優異表現,ProtoBuf 可用於諸如網絡傳輸、配置文件、數據存儲等諸多領域,目前在互聯網上有着大量應用。
若是對於現有已使用grpc,使用proto文件,想轉換成tars協議的用戶而言,須要將上面的proto文件翻譯成Tars文件。這種翻譯會比較繁瑣,並且容易出錯。 爲此咱們決定編寫插件支持proto文件直接生成tars的rpc邏輯。protoc-gen-go的代碼邏輯裏面是預留了插件編寫的規範的,參照grpc,主要有 grpc/grpc.go 和一個導入插件的link_grpc.go 。 這裏咱們編寫 tarsrpc/tarsrpc.go 和 link_tarsrpc.go
使用方面:golang
protoc --go_out=plugins=tarsrpc:. helloworld.proto
爲了支持用戶編寫插件,咱們支持了filter機制,分爲服務端的過濾器和客戶端過濾器,用戶能夠基於這個機制,實現本身的TarsGo插件。shell
//服務端過濾器, 傳入dispatch,和f, 用於調用用戶代碼, req, 和resp爲傳入的用戶請求和服務端相應包體 type ServerFilter func(ctx context.Context, d Dispatch, f interface{}, req *requestf.RequestPacket, resp *requestf.ResponsePacket, withContext bool) (err error) //客戶端過濾器, 傳入msg(包含obj信息,adapter信息,req和resp包體), 還有用戶設定的調用超時 type ClientFilter func(ctx context.Context, msg *Message, invoke Invoke, timeout time.Duration) (err error) //註冊服務端過濾器 //func RegisterServerFilter(f ServerFilter) //註冊客戶端過濾器 //func RegisterClientFilter(f ClientFilter)
有了過濾器,咱們就能對服務端和客戶端的請求作一些過濾,好比使用 hook用於分佈式追蹤的opentracing 的span。
咱們來看下客戶端filter的例子:網絡
//生成客戶端tars filter,經過註冊這個filter來實現span的注入 func ZipkinClientFilter() tars.ClientFilter { return func(ctx context.Context, msg *tars.Message, invoke tars.Invoke, timeout time.Duration) (err error) { var pCtx opentracing.SpanContext req := msg.Req //先從客戶端調用的context 裏面看下有沒有傳遞來調用鏈的信息, //若是有,則以這個作爲父span,若是沒有,則起一個新的span,span名字是RPC請求的函數名 if parent := opentracing.SpanFromContext(ctx); parent != nil { pCtx = parent.Context() } cSpan := opentracing.GlobalTracer().StartSpan( req.SFuncName, opentracing.ChildOf(pCtx), ext.SpanKindRPCClient, ) defer cSpan.Finish() cfg := tars.GetServerConfig() //設置span的信息,好比咱們調用的客戶端的ip地址,請求的接口,方法,協議,客戶端版本等信息 cSpan.SetTag("client.ipv4", cfg.LocalIP) cSpan.SetTag("tars.interface", req.SServantName) cSpan.SetTag("tars.method", req.SFuncName) cSpan.SetTag("tars.protocol", "tars") cSpan.SetTag("tars.client.version", tars.TarsVersion) //將span注入到 請求包體的 Status裏面,status 是一個map[strint]string 的結構體 if req.Status != nil { err = opentracing.GlobalTracer().Inject(cSpan.Context(), opentracing.TextMap, opentracing.TextMapCarrier(req.Status)) if err != nil { logger.Error("inject span to status error:", err) } } else { s := make(map[string]string) err = opentracing.GlobalTracer().Inject(cSpan.Context(), opentracing.TextMap, opentracing.TextMapCarrier(s)) if err != nil { logger.Error("inject span to status error:", err) } else { req.Status = s } } //沒什麼其餘須要修改的,就進行客戶端調用 err = invoke(ctx, msg, timeout) if err != nil { //調用錯誤,則記錄span的錯誤信息 ext.Error.Set(cSpan, true) cSpan.LogFields(oplog.String("event", "error"), oplog.String("message", err.Error())) } return err }
服務端也會註冊一個filter,主要功能就是從request包體的status 提取調用鏈的上下文,以這個做爲父span,進行調用信息的記錄。
總體的一個效果:架構
詳細代碼參見 TarsGo/tars/plugin/zipkintracing
完整的zipkin tracing的客戶端和服務端例子,詳見 TarsGo/examples下面的ZipkinTraceClient和ZipkinTraceServer併發
TarsGo 以前在生成的客戶端代碼,或者用戶傳入的實現代碼裏面,都沒有使用context。 這使得咱們想傳遞一些框架的信息,好比客戶端ip,端口等,或者用戶傳遞一些調用鏈的信息給框架,都很難於實現。 經過接口的一次重構,支持了context,這些上下文的信息,將都經過context來實現。 此次重構爲了兼容老的用戶行爲,採用了徹底兼容的設計。app
服務端使用context框架
type ContextTestImp struct { } //只需在接口上添加 ctx context.Context參數 func (imp *ContextTestImp) Add(ctx context.Context, a int32, b int32, c *int32) (int32, error) { //咱們能夠經過context 獲取框架傳遞的信息,好比下面的獲取ip, 甚至返回一些信息給框架,詳見tars/util/current下面的接口 ip, ok := current.GetClientIPFromContext(ctx) if !ok { logger.Error("Error getting ip from context") } return 0, nil } //之前使用AddServant ,如今只需改爲AddServantWithContext app.AddServantWithContext(imp, cfg.App+"."+cfg.Server+".ContextTestObj")
客戶端使用context運維
ctx := context.Background() c := make(map[string]string) c["a"] = "b" //之前使用app.Add 進行客戶端調用,這裏只要變成app.AddWithContext ,就能夠傳遞context給框架,若是要設置給tars請求的context //能夠多傳入參數,好比c,參數c是可選的,格式是 ...[string]string ret, err := app.AddWithContext(ctx, i, i*2, &out, c)
服務端和客戶端的完整例子,詳見 TarGo/examples