微服務(wù)-如何解決鏈路追蹤問題
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
? 作者?|??王澤賓
來源 |? urlify.cn/6jaMbq
66套java從入門到精通實(shí)戰(zhàn)課程分享
一、鏈路追蹤
微服務(wù)架構(gòu)是將單個(gè)應(yīng)用程序被劃分成各種小而連接的服務(wù),每一個(gè)服務(wù)完成一個(gè)單一的業(yè)務(wù)功能,相互之間保持獨(dú)立和解耦,每個(gè)服務(wù)都可以獨(dú)立演進(jìn)。相對(duì)于傳統(tǒng)的單體服務(wù),微服務(wù)具有隔離性、技術(shù)異構(gòu)性、可擴(kuò)展性以及簡(jiǎn)化部署等優(yōu)點(diǎn)。
同樣的,微服務(wù)架構(gòu)在帶來諸多益處的同時(shí),也為系統(tǒng)增加了不少?gòu)?fù)雜性。它作為一種分布式服務(wù),通常部署于由不同的數(shù)據(jù)中心、不同的服務(wù)器組成的集群上。而且,同一個(gè)微服務(wù)系統(tǒng)可能是由不同的團(tuán)隊(duì)、不同的語言開發(fā)而成。通常一個(gè)應(yīng)用由多個(gè)微服務(wù)組成,微服務(wù)之間的數(shù)據(jù)交互需要通過遠(yuǎn)過程調(diào)用的方式完成,所以在一個(gè)由眾多微服務(wù)構(gòu)成的系統(tǒng)中,請(qǐng)求需要在各服務(wù)之間流轉(zhuǎn),調(diào)用鏈路錯(cuò)綜復(fù)雜,一旦出現(xiàn)問題,是很難進(jìn)行問題定位和追查異常的。
鏈路追蹤系統(tǒng)就是為解決上述問題而產(chǎn)生的,它用來追蹤每一個(gè)請(qǐng)求的完整調(diào)用鏈路,記錄從請(qǐng)求開始到請(qǐng)求結(jié)束期間調(diào)用的任務(wù)名稱、耗時(shí)、標(biāo)簽數(shù)據(jù)以及日志信息,并通過可視化的界面進(jìn)行分析和展示,來幫助技術(shù)人員準(zhǔn)確地定位異常服務(wù)、發(fā)現(xiàn)性能瓶頸、梳理調(diào)用鏈路以及預(yù)估系統(tǒng)容量。
鏈路追蹤系統(tǒng)的理論模型幾乎都借鑒了 Google 的一篇論文”Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”,典型產(chǎn)品有Uber jaeger、Twitter zipkin、淘寶鷹眼等。這些產(chǎn)品的實(shí)現(xiàn)方式雖然不盡相同,但核心步驟一般都有三個(gè):數(shù)據(jù)采集、數(shù)據(jù)存儲(chǔ)和查詢展示。
鏈路追蹤系統(tǒng)第一步,也是最基本的工作就是數(shù)據(jù)采集。在這個(gè)過程中,鏈路追蹤系統(tǒng)需要侵入用戶代碼進(jìn)行埋點(diǎn),用于收集追蹤數(shù)據(jù)。但是由于不同的鏈路追蹤系統(tǒng)的API互不兼容,所以埋點(diǎn)代碼寫法各異,導(dǎo)致用戶在切換不同鏈路追蹤產(chǎn)品時(shí)需要做很大的改動(dòng)。為了解決這類問題,于是誕生了OpenTracing規(guī)范,旨在統(tǒng)一鏈路追蹤系統(tǒng)的API。
二、OpenTracing規(guī)范
OpenTracing 是一套分布式追蹤協(xié)議,與平臺(tái)和語言無關(guān),具有統(tǒng)一的接口規(guī)范,方便接入不同的分布式追蹤系統(tǒng)。
OpenTracing語義規(guī)范詳見:https://github.com/opentracing/specification/blob/master/specification.md
2.1 數(shù)據(jù)模型(Data Model)
OpenTracing語義規(guī)范中定義的數(shù)據(jù)模型有 Trace、Sapn以及Reference。
2.1.1 Trace
Trace表示一條完整的追蹤鏈路,例如:一個(gè)事務(wù)或者一個(gè)流程的執(zhí)行過程。一個(gè) Trace 是由一個(gè)或者多個(gè) Span 組成的有向無環(huán)圖(DAG)。
下圖表示一個(gè)由8個(gè)Span組成的Trace:
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C is a `ChildOf` Span A)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G `FollowsFrom` Span F)
按照時(shí)間軸方式更為直觀地展現(xiàn)該Trace:
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
2.1.2 Span
Span表示一個(gè)獨(dú)立的工作單元,它是一條追蹤鏈路的基本組成要素。例如:一次RPC調(diào)用、一次函數(shù)調(diào)用或者一次Http請(qǐng)求。
每個(gè)Span封裝了如下狀態(tài):
操作名稱
用于表示該Span的任務(wù)名稱。例如:一個(gè) RPC方法名, 一個(gè)函數(shù)名,或者大型任務(wù)中的子任務(wù)名稱。
開始時(shí)間戳
任務(wù)開始時(shí)間。
結(jié)束時(shí)間戳。
任務(wù)結(jié)束時(shí)間。通過Span的結(jié)束時(shí)間戳和開始時(shí)間戳,就能夠計(jì)算出該Span的整體耗時(shí)。
一組Span標(biāo)簽
每一個(gè)Span標(biāo)簽是一個(gè)鍵值對(duì)。鍵必須是字符串,值可以是字符串、布爾或數(shù)值類型。常見標(biāo)簽鍵可參考:https://github.com/opentracing/specification/blob/master/semantic_conventions.md
一組Span日志
每一條Span日志由一個(gè)鍵值對(duì)和一個(gè)相應(yīng)的時(shí)間戳組成。鍵必須是字符串,值可以是任何類型。常見日志鍵參考:https://github.com/opentracing/specification/blob/master/semantic_conventions.md
2.1.3 Reference
一個(gè)Span可以與一個(gè)或者多個(gè)Span存在因果關(guān)系,這種關(guān)系稱為Reference。OpenTracing目前定義了兩種關(guān)系:ChildOf(父子)關(guān)系 和 FollowsFrom(跟隨)關(guān)系。
ChildOf關(guān)系
父Span的執(zhí)行依賴子Span的執(zhí)行結(jié)果,此時(shí)子Span對(duì)父Span的Reference關(guān)系是ChildOf。比如對(duì)于一次RPC調(diào)用,服務(wù)端的Span(子Span)與客戶端調(diào)用的Span(父Span)就是ChildOf關(guān)系。
FollowsFrom關(guān)系
父Span的執(zhí)行不依賴子Span的執(zhí)行結(jié)果,此時(shí)子Span對(duì)父Span的Reference關(guān)系是FollowFrom。FollowFrom常用于表示異步調(diào)用,例如消息隊(duì)列中Consumer Span與Producer Span之間的關(guān)系。
2.2 應(yīng)用接口(API)
2.2.1 Tracer
Tracer接口用于創(chuàng)建Span、跨進(jìn)程注入數(shù)據(jù)和提取數(shù)據(jù)。通常具有以下功能:
Start a new span
創(chuàng)建并啟動(dòng)一個(gè)新的Span。Inject
將SpanContext注入載體(Carrier)。Extract
從載體(Carrier)中提取SpanContext。
2.2.2 Span
Retrieve a SpanContext
返回Span對(duì)應(yīng)的SpanContext。Overwrite the operation name
更新操作名稱。Set a span tag
設(shè)置Span標(biāo)簽數(shù)據(jù)。Log structured data
記錄結(jié)構(gòu)化數(shù)據(jù)。Set a baggage item
baggage item是字符串型的鍵值對(duì),它對(duì)應(yīng)于某個(gè) Span,隨Trace一起傳播。由于每個(gè)鍵值都會(huì)被拷貝到每一個(gè)本地及遠(yuǎn)程的子Span,這可能導(dǎo)致巨大的網(wǎng)絡(luò)和CPU開銷。Get a baggage item
獲取baggage item的值。Finish
結(jié)束一個(gè)Span。
2.2.3 Span Context
用于攜帶跨越服務(wù)邊界的數(shù)據(jù),包括trace ID、Span ID以及需要傳播到下游Span的baggage數(shù)據(jù)。在OpenTracing中,強(qiáng)制要求SpanContext實(shí)例不可變,以避免在Span完成和引用時(shí)出現(xiàn)復(fù)雜的生命周期問題。
2.2.4 NoopTracer
所有對(duì)OpenTracing API的實(shí)現(xiàn),必須提供某種形式的NoopTracer,用于標(biāo)記控制OpenTracing或注入對(duì)測(cè)試無害的東西。
三、Jaeger
Jaeger是Uber開源的分布式追蹤系統(tǒng),它的應(yīng)用接口完全遵循OpenTracing規(guī)范。jaeger本身采用go語言編寫,具有跨平臺(tái)跨語言的特性,提供了各種語言的客戶端調(diào)用接口,例如c++、java、go、python、ruby、php、nodejs等。項(xiàng)目地址:https://github.com/jaegertracing
3.1 Jaeger組件
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-miLIEWHv-1604561903414)(https://i.loli.net/2020/04/13/bvTxdUkBRuawY1F.png)]
jaeger-client
jaeger的客戶端代碼庫(kù),它實(shí)現(xiàn)了OpenTracing協(xié)議。當(dāng)我們的應(yīng)用程序?qū)⑵溲b配后,負(fù)責(zé)收集數(shù)據(jù),并發(fā)送到j(luò)aeger-agent。這是我們唯一需要編寫代碼的地方。
jaeger-agent
負(fù)責(zé)接收從jaeger-client發(fā)來的Trace/Span信息,并批量上傳到j(luò)aeger-collector。
jaeger-collector
負(fù)責(zé)接收從jaeger-agent發(fā)來的Trace/Span信息,并經(jīng)過校驗(yàn)、索引等處理,然后寫入到后端存儲(chǔ)。
data store
負(fù)責(zé)數(shù)據(jù)存儲(chǔ)。Jaeger的數(shù)據(jù)存儲(chǔ)是一個(gè)可插拔的組件,目前支持Cassandra、ElasticSearch和Kafka。
jaeger-query & ui
負(fù)責(zé)數(shù)據(jù)查詢,并通過前端界面展示查詢結(jié)果。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-ogkrm3Hb-1604561903417)(https://i.loli.net/2020/04/13/UMoHYtlX1ydsx5Q.jpg)]
3.2 快速入門
Jaeger官方提供了all-in-one鏡像,方便快速進(jìn)行測(cè)試:
#?拉取鏡像
$docker?pull?jaegertracing/all-in-one:latest
#?運(yùn)行鏡像
$docker?run?-d?--name?jaeger?\
??-e?COLLECTOR_ZIPKIN_HTTP_PORT=9411?\
??-p?5775:5775/udp?\
??-p?6831:6831/udp?\
??-p?6832:6832/udp?\
??-p?5778:5778?\
??-p?14268:14268?\
??-p?9411:9411?\
??-p?16686:16686?\
??jaegertracing/all-in-one:latest
通過all-in-one鏡像啟動(dòng),我們發(fā)現(xiàn)Jaeger占據(jù)了很多端口。以下是端口使用說明:
| 端口 | 協(xié)議 | 所屬模塊 | 功能 |
|---|---|---|---|
| 5775 | UDP | agent | 接收壓縮格式的Zipkin thrift數(shù)據(jù) |
| 6831 | UDP | agent | 接收壓縮格式的Jaeger thrift數(shù)據(jù) |
| 6832 | UDP | agent | 接收二進(jìn)制格式的Jaeger thrift數(shù)據(jù) |
| 5778 | HTTP | agent | 服務(wù)配置、采樣策略端口 |
| 14268 | HTTP | collector | 接收由客戶端直接發(fā)送的Jaeger thrift數(shù)據(jù) |
| 9411 | HTTP | collector | 接收Zipkin發(fā)送的json或者thrift數(shù)據(jù) |
| 16686 | HTTP | query | 瀏覽器展示端口 |
啟動(dòng)后,我們可以訪問 http://localhost:16686 ,在瀏覽器中查看和查詢收集的數(shù)據(jù)。
由于通過all-in-one鏡像方式收集的數(shù)據(jù)都存儲(chǔ)在docker中,無法持久保存,所以只能用于開發(fā)或者測(cè)試環(huán)境,無法用于生產(chǎn)環(huán)境。生產(chǎn)環(huán)境中需要依據(jù)實(shí)際情況,分別部署各個(gè)組件。
四、Jaeger在業(yè)務(wù)代碼中的應(yīng)用
系統(tǒng)中使用Jaeger非常簡(jiǎn)單,只需要在原有程序中插入少量代碼。以下代碼模擬了一個(gè)查詢用戶賬戶余額,執(zhí)行扣款的業(yè)務(wù)場(chǎng)景:
4.1 初始化jaeger函數(shù)
主要是按照實(shí)際需要配置有關(guān)參數(shù),例如服務(wù)名稱、采樣模式、采樣比例等等。
func?initJaeger()?(tracer?opentracing.Tracer,?closer?io.Closer,?err?error)?{
?//?構(gòu)造配置信息
?cfg?:=?&config.Configuration{
??//?設(shè)置服務(wù)名稱
??ServiceName:?"ServiceAmount",
??//?設(shè)置采樣參數(shù)
??Sampler:?&config.SamplerConfig{
???Type:??"const",?//?全采樣模式
???Param:?1,???????//?開啟狀態(tài)
??},
?}
?
?//?生成一條新tracer
?tracer,?closer,?err?=?cfg.NewTracer()
?if?err?==?nil?{
??//?設(shè)置tracer為全局單例對(duì)象
??opentracing.SetGlobalTracer(tracer)
?}
?return
}
4.2 檢測(cè)用戶余額函數(shù)
用于檢測(cè)用戶余額,模擬一個(gè)子任務(wù)Span。
func?CheckBalance(request?string,?ctx?context.Context)?{
?//?創(chuàng)建子span
?span,?_?:=?opentracing.StartSpanFromContext(ctx,?"CheckBalance")
?//?模擬系統(tǒng)進(jìn)行一系列的操作,耗時(shí)1/3秒
?time.Sleep(time.Second?/?3)
?//?示例:將需要追蹤的信息放入tag
?span.SetTag("request",?request)
?span.SetTag("reply",?"CheckBalance?reply")
?//?結(jié)束當(dāng)前span
?span.Finish()
?log.Println("CheckBalance?is?done")
}
4.3 從用戶賬戶扣款函數(shù)
從用戶賬戶扣款,模擬一個(gè)子任務(wù)span。
func?Reduction(request?string,?ctx?context.Context)?{
?//?創(chuàng)建子span
?span,?_?:=?opentracing.StartSpanFromContext(ctx,?"Reduction")
?//?模擬系統(tǒng)進(jìn)行一系列的操作,耗時(shí)1/2秒
?time.Sleep(time.Second?/?2)
?//?示例:將需要追蹤的信息放入tag
?span.SetTag("request",?request)
?span.SetTag("reply",?"Reduction?reply")
?//?結(jié)束當(dāng)前span
?span.Finish()
?log.Println("Reduction?is?done")
}
4.4 主函數(shù)
初始化jaeger環(huán)境,生成tracer,創(chuàng)建父span,以及調(diào)用查詢余額和扣款兩個(gè)子任務(wù)span。
package?main
import?(
?"context"
?"fmt"
?"github.com/opentracing/opentracing-go"
?"github.com/uber/jaeger-client-go/config"
?"io"
?"log"
?"time"
)
func?main()?{
?//?初始化jaeger,創(chuàng)建一條新tracer
?tracer,?closer,?err?:=?initJaeger()
?if?err?!=?nil?{
??panic(fmt.Sprintf("ERROR:?cannot?init?Jaeger:?%v\n",?err))
?}
?defer?closer.Close()
?//?創(chuàng)建一個(gè)新span,作為父span,開始計(jì)費(fèi)過程
?span :=?tracer.StartSpan("CalculateFee")
?
?//?生成父span的context
?ctx?:=?opentracing.ContextWithSpan(context.Background(),?span)
?//?示例:設(shè)置一個(gè)span標(biāo)簽信息
?span.SetTag("db.instance",?"customers")
?//?示例:輸出一條span日志信息
?span.LogKV("event",?"timed?out")
?//?將父span的context作為參數(shù),調(diào)用檢測(cè)用戶余額函數(shù)
?CheckBalance("CheckBalance?request",?ctx)
?//?將父span的context作為參數(shù),調(diào)用扣款函數(shù)
?Reduction("Reduction?request",?ctx)
?//?結(jié)束父span
?span.Finish()
}
五、Jaeger在gRPC微服務(wù)中的應(yīng)用
我們依然模擬了一個(gè)查詢用戶賬戶余額,執(zhí)行扣款的業(yè)務(wù)場(chǎng)景,并把查詢用戶賬戶余額和執(zhí)行扣款功能改造為gRPC微服務(wù):
5.1 gRPC Server端代碼
main.go:
代碼使用了第三方依賴庫(kù)github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing,該依賴庫(kù)將OpenTracing封裝為通用的gRPC中間件,并通過gRPC攔截器無縫嵌入gRPC服務(wù)中。
package?main
import?(
?"fmt"
?"github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
?"github.com/opentracing/opentracing-go"
?"github.com/uber/jaeger-client-go/config"
?"google.golang.org/grpc"
?"google.golang.org/grpc/reflection"
?"grpc-jaeger-server/account"
?"io"
?"log"
?"net"
)
//?初始化jaeger
func?initJaeger()?(tracer?opentracing.Tracer,?closer?io.Closer,?err?error)?{
?//?構(gòu)造配置信息
?cfg?:=?&config.Configuration{
??//?設(shè)置服務(wù)名稱
??ServiceName:?"ServiceAmount",
??//?設(shè)置采樣參數(shù)
??Sampler:?&config.SamplerConfig{
???Type:??"const",?//?全采樣模式
???Param:?1,???????//?開啟全采樣模式
??},
?}
?//?生成一條新tracer
?tracer,?closer,?err?=?cfg.NewTracer()
?if?err?==?nil?{
??//?設(shè)置tracer為全局單例對(duì)象
??opentracing.SetGlobalTracer(tracer)
?}
?return
}
func?main()?{
?//?初始化jaeger,創(chuàng)建一條新tracer
?tracer,?closer,?err?:=?initJaeger()
?if?err?!=?nil?{
??panic(fmt.Sprintf("ERROR:?cannot?init?Jaeger:?%v\n",?err))
?}
?defer?closer.Close()
?log.Println("succeed?to?init?jaeger")
?//?注冊(cè)gRPC?account服務(wù)
?server?:=?grpc.NewServer(grpc.UnaryInterceptor(grpc_opentracing.UnaryServerInterceptor(grpc_opentracing.WithTracer(tracer))))
?account.RegisterAccountServer(server,?&AccountServer{})
?reflection.Register(server)
?log.Println("succeed?to?register?account?service")
?//?監(jiān)聽gRPC?account服務(wù)端口
?listener,?err?:=?net.Listen("tcp",?":8080")
?if?err?!=?nil?{
??log.Println(err)
??return
?}
?log.Println("starting?register?account?service")
?//?開啟gRpc?account服務(wù)
?if?err?:=?server.Serve(listener);?err?!=?nil?{
??log.Println(err)
??return
?}
}
計(jì)費(fèi)微服務(wù) accountsever.go:
package?main
import?(
?"github.com/opentracing/opentracing-go"
?"golang.org/x/net/context"
?"grpc-jaeger-server/account"
?"time"
)
//?計(jì)費(fèi)服務(wù)
type?AccountServer?struct{}
//?檢測(cè)用戶余額微服務(wù),模擬子span任務(wù)
func?(s?*AccountServer)?CheckBalance(ctx?context.Context,?request?*account.CheckBalanceRequest)?(response?*account.CheckBalanceResponse,?err?error)?{
?response?=?&account.CheckBalanceResponse{
??Reply:?"CheckBalance?Reply",?//?處理結(jié)果
?}
?//?創(chuàng)建子span
?span,?_?:=?opentracing.StartSpanFromContext(ctx,?"CheckBalance")
?//?模擬系統(tǒng)進(jìn)行一系列的操作,耗時(shí)1/3秒
?time.Sleep(time.Second?/?3)
?//?將需要追蹤的信息放入tag
?span.SetTag("request",?request)
?span.SetTag("reply",?response)
?//?結(jié)束當(dāng)前span
?span.Finish()
?return?response,?err
}
//?從用戶賬戶扣款微服務(wù),模擬子span任務(wù)
func?(s?*AccountServer)?Reduction(ctx?context.Context,?request?*account.ReductionRequest)?(response?*account.ReductionResponse,?err?error)?{
?response?=?&account.ReductionResponse{
??Reply:?"Reduction?Reply",?//?處理結(jié)果
?}
?//?創(chuàng)建子span
?span,?_?:=?opentracing.StartSpanFromContext(ctx,?"Reduction")
?//?模擬系統(tǒng)進(jìn)行一系列的操作,耗時(shí)1/3秒
?time.Sleep(time.Second?/?3)
?//?將需要追蹤的信息放入tag
?span.SetTag("request",?request)
?span.SetTag("reply",?response)
?//?結(jié)束當(dāng)前span
?span.Finish()
?return?response,?err
}
5.2 gRPC Client端代碼main.go:
package?main
import?(
?"context"
?"fmt"
?"github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
?"github.com/opentracing/opentracing-go"
?"github.com/uber/jaeger-client-go/config"
?"google.golang.org/grpc"
?"grpc-jaeger-client/account"
?"io"
?"log"
)
//?初始化jaeger
func?initJaeger()?(tracer?opentracing.Tracer,?closer?io.Closer,?err?error)?{
?//?構(gòu)造配置信息
?cfg?:=?&config.Configuration{
??//?設(shè)置服務(wù)名稱
??ServiceName:?"ServiceAmount",
??//?設(shè)置采樣參數(shù)
??Sampler:?&config.SamplerConfig{
???Type:??"const",?//?全采樣模式
???Param:?1,???????//?開啟全采樣模式
??},
?}
?//?生成一條新tracer
?tracer,?closer,?err?=?cfg.NewTracer()
?if?err?==?nil?{
??//?設(shè)置tracer為全局單例對(duì)象
??opentracing.SetGlobalTracer(tracer)
?}
?return
}
func?main()?{
?//?初始化jaeger,創(chuàng)建一條新tracer
?tracer,?closer,?err?:=?initJaeger()
?if?err?!=?nil?{
??panic(fmt.Sprintf("ERROR:?cannot?init?Jaeger:?%v\n",?err))
?}
?defer?closer.Close()
?log.Println("succeed?to?init?jaeger")
?//?創(chuàng)建一個(gè)新span,作為父span
?span :=?tracer.StartSpan("CalculateFee")
?//?函數(shù)返回時(shí)關(guān)閉span
?defer?span.Finish()
?//?生成span的context
?ctx?:=?opentracing.ContextWithSpan(context.Background(),?span)
?//?連接gRPC?server
?conn,?err?:=?grpc.Dial("localhost:8080",
??grpc.WithInsecure(),
??grpc.WithUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor(grpc_opentracing.WithTracer(tracer),
??)))
?if?err?!=?nil?{
??log.Println(err)
??return
?}
?//?創(chuàng)建gRPC計(jì)費(fèi)服務(wù)客戶端
?client?:=?account.NewAccountClient(conn)
?//?將父span的context作為參數(shù),調(diào)用檢測(cè)用戶余額的gRPC微服務(wù)
?checkBalanceResponse,?err?:=?client.CheckBalance(ctx,
??&account.CheckBalanceRequest{
???Account:?"user?account",
??})
?if?err?!=?nil?{
??log.Println(err)
??return
?}
?log.Println(checkBalanceResponse)
?//?將父span的context作為參數(shù),調(diào)用扣款的gRPC微服務(wù)
?reductionResponse,?err?:=?client.Reduction(ctx,
??&account.ReductionRequest{
???Account:?"user?account",
???Amount:?1,
??})
?if?err?!=?nil?{
??log.Println(err)
??return
?}
?log.Println(reductionResponse)
}
注:
本文全部源代碼位于:https://github.com/wangshizebin/micro-service
粉絲福利:實(shí)戰(zhàn)springboot+CAS單點(diǎn)登錄系統(tǒng)視頻教程免費(fèi)領(lǐng)取
???
?長(zhǎng)按上方微信二維碼?2 秒 即可獲取資料
感謝點(diǎn)贊支持下哈?
