網站首頁 編程語言 正文
前幾天有個同學想了解下如何在go-micro中做鏈路跟蹤,這幾天正好看到wrapper這塊,wrapper這個東西在某些框架中也稱為中間件,里邊有個opentracing的插件,正好用來做鏈路追蹤。opentracing是個規范,還需要搭配一個具體的實現,比如zipkin、jeager等,這里選擇zipkin。
鏈路跟蹤實戰
安裝zipkin
通過docker快速啟動一個zipkin服務端:
docker run -d -p 9411:9411 openzipkin/zipkin
程序結構
為了方便演示,這里把客戶端和服務端放到了一個項目中,程序的目錄結構是這樣的:
- main.go 服務端程序。client/main.go 客戶端程序。
- config/config.go 程序用到的一些配置,比如服務的名稱和監聽端口、zipkin的訪問地址等。
- zipkin/ot-zipkin.go opentracing和zipkin相關的函數。
安裝依賴包
需要安裝go-micro、opentracing、zipkin相關的包:
go get go-micro.dev/v4@latest
go get github.com/go-micro/plugins/v4/wrapper/trace/opentracing
go get -u github.com/openzipkin-contrib/zipkin-go-opentracing
編寫服務端
首先定義一個服務端業務處理程序:
type Hello struct { } func (h *Hello) Say(ctx context.Context, name *string, resp *string) error { *resp = "Hello " + *name return nil }
這個程序只有一個方法Say,輸入name,返回 "Hello " + name。
然后使用go-micro編寫服務端框架程序:
func main() { tracer := zipkin.GetTracer(config.SERVICE_NAME, config.SERVICE_HOST) defer zipkin.Close() tracerHandler := opentracing.NewHandlerWrapper(tracer) service := micro.NewService( micro.Name(config.SERVICE_NAME), micro.Address(config.SERVICE_HOST), micro.WrapHandler(tracerHandler), ) service.Init() micro.RegisterHandler(service.Server(), &Hello{}) if err := service.Run(); err != nil { log.Println(err) } }
這里NewService的時候除了指定服務的名稱和訪問地址,還通過micro.WrapHandler設置了一個用于鏈路跟蹤的HandlerWrapper。
這個HandlerWrapper是通過go-micro的opentracing插件提供的,這個插件需要傳入一個tracer。這個tracer可以通過前邊安裝的 zipkin-go-opentracing 包來創建,我們把創建邏輯封裝在了config.go中:
func GetTracer(serviceName string, host string) opentracing.Tracer { // set up a span reporter zipkinReporter = zipkinhttp.NewReporter(config.ZIPKIN_SERVER_URL) // create our local service endpoint endpoint, err := zipkin.NewEndpoint(serviceName, host) if err != nil { log.Fatalf("unable to create local endpoint: %+v\n", err) } // initialize our tracer nativeTracer, err := zipkin.NewTracer(zipkinReporter, zipkin.WithLocalEndpoint(endpoint)) if err != nil { log.Fatalf("unable to create tracer: %+v\n", err) } // use zipkin-go-opentracing to wrap our tracer tracer := zipkinot.Wrap(nativeTracer) opentracing.InitGlobalTracer(tracer) return tracer }
service創建完畢之后,還要通過 micro.RegisterHandler 來注冊前邊編寫的業務處理程序。
最后通過 service.Run 讓服務運行起來。
編寫客戶端
再來看一下客戶端的處理邏輯:
func main() { tracer := zipkin.GetTracer(config.CLIENT_NAME, config.CLIENT_HOST) defer zipkin.Close() tracerClient := opentracing.NewClientWrapper(tracer) service := micro.NewService( micro.Name(config.CLIENT_NAME), micro.Address(config.CLIENT_HOST), micro.WrapClient(tracerClient), ) client := service.Client() go func() { for { <-time.After(time.Second) result := new(string) request := client.NewRequest(config.SERVICE_NAME, "Hello.Say", "FireflySoft") err := client.Call(context.TODO(), request, result) if err != nil { log.Println(err) continue } log.Println(*result) } }() service.Run() }
這段代碼開始也是先NewService,設置客戶端程序的名稱和監聽地址,然后通過micro.WrapClient注入鏈路跟蹤,這里注入的是一個ClientWrapper,也是由opentracing插件提供的。這里用的tracer和服務端tracer是一樣的,都是通過config.go中GetTracer函數獲取的。
然后為了方便演示,啟動一個go routine,客戶端每隔一秒發起一次RPC請求,并將返回結果打印出來。運行效果如圖所示:
zipkin中跟蹤到的訪問日志:
Wrap原理分析
Wrap從字面意思上理解就是封裝、嵌套,在很多的框架中也稱為中間件,比如gin中,再比如ASP.NET Core中。這個部分就來分析下go-micro中Wrap的原理。
服務端Wrap
在go-micro中服務端處理請求的邏輯封裝稱為Handler,它的具體形式是一個func,定義為:
func(ctx context.Context, req Request, rsp interface{}) error
這個部分就來看一下服務端Handler是怎么被Wrap的。
HandlerWrapper
要想Wrap一個Handler,必須創建一個HandlerWrapper類型,這其實是一個func,其定義如下:
type HandlerWrapper func(HandlerFunc) HandlerFunc
它的參數和返回值都是HandlerFunc類型,其實就是上面提到的Handler的func定義。
以本文鏈路跟蹤中使用的 tracerHandler 為例,看一下HandlerWrapper是如何實現的:
func(h server.HandlerFunc) server.HandlerFunc { return func(ctx context.Context, req server.Request, rsp interface{}) error { ... if err = h(ctx, req, rsp); err != nil { ... } }
從中可以看出,Wrap一個Hander就是定義一個新Handler,在它的的內部調用傳入的原Handler。
Wrap Handler
創建了一個HandlerWrapper之后,還需要把它加入到服務端的處理過程中。
go-micro在NewService的時候通過調用 micro.WrapHandler 設置這些 HandlerWrapper:
service := micro.NewService( ... micro.WrapHandler(tracerHandler), )
WrapHandler的實現是這樣的:
func WrapHandler(w ...server.HandlerWrapper) Option { return func(o *Options) { var wrappers []server.Option for _, wrap := range w { wrappers = append(wrappers, server.WrapHandler(wrap)) } o.Server.Init(wrappers...) } }
它返回的是一個函數,這個函數會將我們傳入的HandlerWrapper通過server.WrapHandler轉化為一個server.Option,然后交給Server.Init進行初始化處理。
這里的server.Option其實還是一個func,看一下WrapHandler的源碼:
func WrapHandler(w HandlerWrapper) Option { return func(o *Options) { o.HdlrWrappers = append(o.HdlrWrappers, w) } }
這個func將我們傳入的HandlerWrapper添加到了一個切片中。
那么這個函數什么時候執行呢?就在Server.Init中。看一下Server.Init中的源碼:
func (s *rpcServer) Init(opts ...Option) error { ... for _, opt := range opts { opt(&s.opts) } if s.opts.Router == nil { r := newRpcRouter() r.hdlrWrappers = s.opts.HdlrWrappers ... s.router = r } ... }
它會遍歷傳入的所有server.Option,也就是執行每一個func(o *Options)。這樣Options的切片HdlrWrappers中就添加了我們設置的HandlerWrapper,同時還把這個切片傳遞到了rpcServer的router中。
可以看到這里的Options就是rpcServer.opts,HandlerWrapper切片同時設置到了rpcServer.router和rpcServer.opts中。
還有一個問題:WrapHandler返回的func什么時候執行呢?
這個在micro.NewService -> newService -> newOptions中:
func newOptions(opts ...Option) Options { opt := Options{ ... Server: server.DefaultServer, ... } for _, o := range opts { o(&opt) } ... }
遍歷opts就是執行每一個設置func,最終執行到rpcServer.Init。
到NewService執行完畢為止,我們設置的WrapHandler全部添加到了一個名為HdlrWrappers的切片中。
再來看一下服務端Wrapper的執行過程是什么樣的?
執行Handler的這段代碼在rpc_router.go中:
func (s *service) call(ctx context.Context, router *router, sending *sync.Mutex, mtype *methodType, req *request, argv, replyv reflect.Value, cc codec.Writer) error { defer router.freeRequest(req) ... for i := len(router.hdlrWrappers); i > 0; i-- { fn = router.hdlrWrappers[i-1](fn) } ... // execute handler return fn(ctx, r, rawStream) }
根據前面的分析,可以知道router.hdlrWrappers中記錄的就是所有的HandlerWrapper,這里通過遍歷router.hdlrWrappers實現了HandlerWrapper的嵌套,注意這里遍歷時索引采用了從大到小的順序,后添加的先被Wrap,先添加在外層。
實際執行時就是先調用到最先添加的HandlerWrapper,然后一層層向里調用,最終調用到我們注冊的業務Handler,然后再一層層的返回,每個HandlerWrapper都可以在調用下一層前后做些自己的工作,比如鏈路跟蹤這里的檢測執行時間。
客戶端Wrap
在客戶端中遠程調用的定義在Client中,它是一個接口,定義了若干方法:
type Client interface { ... Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error ... }
我們這里為了講解方便,只關注Call方法,其它的先省略。
下面來看一下Client是怎么被Wrap的。
XXXWrapper
要想Wrap一個Client,需要通過struct嵌套這個Client,并實現Client接口的方法。至于這個struct的名字無法強制要求,一般以XXXWrapper命名。
這里以鏈路跟蹤使用的 otWrapper 為例,它的定義如下:
type otWrapper struct { ot opentracing.Tracer client.Client } func (o *otWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { ... if err = o.Client.Call(ctx, req, rsp, opts...); err != nil { ... } ...
注意XXXWrapper實現的接口方法中都去調用了被嵌套Client的對應接口方法,這是能夠嵌套執行的關鍵。
Wrap Client
有了上面的 XXXWrapper,還需要把它注入到程序的執行流程中。
go-micro在NewService的時候通過調用 micro.WrapClient 設置這些 XXXWrapper:
service := micro.NewService( ... micro.WrapClient(tracerClient), )
和WrapHandler差不多,WrapClient的參數不是直接傳入XXXWrapper的實例,而是一個func,定義如下:
type Wrapper func(Client) Client
這個func需要將傳入的的Client包裝到 XXXWrapper 中,并返回 XXXWrapper 的實例。這里傳入的 tracerClient 就是這樣一個func:
return func(c client.Client) client.Client { if ot == nil { ot = opentracing.GlobalTracer() } return &otWrapper{ot, c} }
要實現Client的嵌套,可以給定一個初始的Client實例作為第一個此類func的輸入,然后前一個func的輸出作為后一個func的輸入,依次執行,最終形成業務代碼中要使用的Client實例,這很像俄羅斯套娃,它有很多層Client。
那么這個俄羅斯套娃是什么時候創建的呢?
在 micro.NewService -> newService -> newOptions中:
func newOptions(opts ...Option) Options { opt := Options{ ... Client: client.DefaultClient, ... } for _, o := range opts { o(&opt) } return opt }
可以看到這里給Client設置了一個初始值,然后遍歷這些NewService時傳入的Option(WrapClient返回的也是Option),這些Option其實都是func,所以就是遍歷執行這些func,執行這些func的時候會傳入一些初始默認值,包括Client的初始值。
那么前一個func的輸出怎么作為后一個func的輸入的呢?再來看下WrapClient的源碼:
func WrapClient(w ...client.Wrapper) Option { return func(o *Options) { for i := len(w); i > 0; i-- { o.Client = w[i-1](o.Client) } } }
可以看到Wrap方法從Options中獲取到當前的Client實例,把它傳給Wrap func,然后新生成的實例又被設置到Options的Client字段中。
正是這樣形成了前文所說的俄羅斯套娃。
再來看一下客戶端調用的執行流程是什么樣的?
通過service的Client()方法獲取到Client實例,然后通過這個實例的Call()方法執行RPC調用。
client:=service.Client() client.Call()
這個Client實例就是前文描述的套娃實例:
func (s *service) Client() client.Client { return s.opts.Client }
前文提到過:XXXWrapper實現的接口方法中調用了被嵌套Client的對應接口方法。這就是能夠嵌套執行的關鍵。
這里給一張圖,讓大家方便理解Wrap Client進行RPC調用的執行流程:
客戶端Wrap和服務端Wrap的區別
一個重要的區別是:對于多次WrapClient,后添加的先被調用;對于多次WrapHandler,先添加的先被調用。
有一個比較怪異的地方是,WrapClient時如果傳遞了多個Wrapper實例,WrapClient會把順序調整過來,這多個實例中前邊的先被調用,這個處理和多次WrapClient處理的順序相反,不是很理解。
func WrapClient(w ...client.Wrapper) Option { return func(o *Options) { // apply in reverse for i := len(w); i > 0; i-- { o.Client = w[i-1](o.Client) } } }
客戶端Wrap還提供了更低層級的CallWrapper,它的執行順序和服務端HandlerWrapper的執行順序一致,都是先添加的先被調用。
// wrap the call in reverse for i := len(callOpts.CallWrappers); i > 0; i-- { rcall = callOpts.CallWrappers[i-1](rcall) }
還有一個比較大的區別是,服務端的Wrap是調用某個業務Handler之前臨時加上的,客戶端的Wrap則是在調用Client.Call時就已經創建好。這樣做的原因是什么呢?這個可能是因為在服務端,業務Handler和HandlerWrapper是分別注冊的,注冊業務Handler時HandlerWrapper可能還不存在,只好采用動態Wrap的方式。而在客戶端,通過Client.Call發起調用時,Client是發起調用的主體,用戶有很多獲取Client的方式,無法要求用戶在每次調用前都臨時Wrap。
Http服務的鏈路跟蹤
關于Http或者說是Restful服務的鏈路跟蹤,go-micro的httpClient支持CallWrapper,可以用WrapCall來添加鏈路跟蹤的CallWrapper;但是其httpServer實現的比較簡單,把http內部的Handler處理完全交出去了,不能用WrapHandler,只能自己在http的框架中來做這件事,比如go-micro+gin開發的Restful服務可以使用gin的中間件機制來做鏈路追蹤。
代碼已經上傳到Github,歡迎訪問:https://github.com/bosima/go-demo/tree/main/go-micro-opentracing
原文鏈接:https://www.cnblogs.com/bossma/p/16223243.html
相關推薦
- 2022-04-12 git 提交到遠程倉庫 報錯 error: failed to push some refs to
- 2022-05-02 ASP.NET?Core中間件實現限流的代碼_實用技巧
- 2023-09-17 Android 獲取Wifi列表詳解(包含動態權限申請)
- 2022-11-12 Python?Multinomial?Naive?Bayes多項貝葉斯模型實現原理介紹_python
- 2022-07-12 Linux虛擬機設置雙網卡
- 2022-06-14 Qt拖放操作和打印操作的實現_C 語言
- 2022-07-12 eureka更換為nacos
- 2023-07-04 JUC阻塞隊列BlockingQueue---LinkedBlockingQueue
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支