为什么项目内需要链路追踪?当一个请求中,请求了多个服务单元,如果请求出现了错误或异常,很难去定位是哪个服务出了问题,这时就需要链路追踪。

从图中可以清晰的看出他们之间的调用关系,通过一个例子说明下链路的重要性,比如对方调我们一个接口,反馈在某个时间段这接口太慢了,在排查代码发现逻辑比较复杂,不光调用了多个三方接口、操作了数据库,还操作了缓存,怎么快速定位是哪块执行时间很长?

不卖关子,先说下本篇文章最终实现了什么,如果感兴趣再继续往下看。

实现了通过记录如下参数,来进行问题定位,关于每个参数的结构在下面都有介绍。

// Trace 记录的参数
type Trace struct {
mux sync.Mutex
Identifier string `json:"trace_id"` // 链路 ID
Request *Request `json:"request"` // 请求信息
Response *Response `json:"response"` // 响应信息
ThirdPartyRequests []*Dialog `json:"third_party_requests"` // 调用第三方接口的信息
Debugs []*Debug `json:"debugs"` // 调试信息
SQLs []*SQL `json:"sqls"` // 执行的 SQL 信息
Redis []*Redis `json:"redis"` // 执行的 Redis 信息
Success bool `json:"success"` // 请求结果 true or false
CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒)
}

参数结构

链路 ID

String 例如:4b4f81f015a4f2a01b00。如果请求 Header 中存在 TRACE-ID,就使用它,反之,重新创建一个。将 TRACE_ID 放到接口返回值中,这样就可以通过这个标示查到这一串的信息。

请求信息

Object,结构如下:

type Request struct {
TTL string `json:"ttl"` // 请求超时时间
Method string `json:"method"` // 请求方式
DecodedURL string `json:"decoded_url"` // 请求地址
Header interface{} `json:"header"` // 请求 Header 信息
Body interface{} `json:"body"` // 请求 Body 信息
}

响应信息

Object,结构如下:

type Response struct {
Header interface{} `json:"header"` // Header 信息
Body interface{} `json:"body"` // Body 信息
BusinessCode int `json:"business_code,omitempty"` // 业务码
BusinessCodeMsg string `json:"business_code_msg,omitempty"` // 提示信息
HttpCode int `json:"http_code"` // HTTP 状态码
HttpCodeMsg string `json:"http_code_msg"` // HTTP 状态码信息
CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒)
}

调用三方接口信息

Object,结构如下:

type Dialog struct {
mux sync.Mutex
Request *Request `json:"request"` // 请求信息
Responses []*Response `json:"responses"` // 返回信息
Success bool `json:"success"` // 是否成功,true 或 false
CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒)
}

这里面的 RequestResponse 结构与上面保持一致。

细节来了,为什么 Responses 结构是 []*Response

是因为 HTTP 可以进行重试请求,比如当请求对方接口的时候,HTTP 状态码为 503 http.StatusServiceUnavailable,这时需要重试,我们也需要把重试的响应信息记录下来。

调试信息

Object 结构如下:

type Debug struct {
Key string `json:"key"` // 标示
Value interface{} `json:"value"` // 值
CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒)
}

SQL 信息

Object,结构如下:

type SQL struct {
Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05
Stack string `json:"stack"` // 文件地址和行号
SQL string `json:"sql"` // SQL 语句
Rows int64 `json:"rows_affected"` // 影响行数
CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒)
}

Redis 信息

Object,结构如下:

type Redis struct {
Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05
Handle string `json:"handle"` // 操作,SET/GET 等
Key string `json:"key"` // Key
Value string `json:"value,omitempty"` // Value
TTL float64 `json:"ttl,omitempty"` // 超时时长(单位分)
CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒)
}

请求结果

Bool,这个和统一定义返回值有点关系,看下代码:

// 错误返回
c.AbortWithError(code.ErrParamBind.WithErr(err)) // 正确返回
c.Payload(code.OK.WithData(data))

当错误返回时 且 ctx.Writer.Status() != http.StatusOK 时,为 false,反之为 true

执行时长

Float64,例如:0.041746869,记录的是从请求开始到请求结束所花费的时间。

如何收集参数?

这时有老铁会说了:“规划的稍微还行,使用的时候会不会很麻烦?”

“No,No,使用起来一丢丢都不麻烦”,接着往下看。

无需关心的参数

链路 ID、请求信息、响应信息、请求结果、执行时长,这 5 个参数,开发者无需关心,这些都在中间件封装好了。

调用第三方接口的信息

只需多传递一个参数即可。

在这里厚脸皮自荐下 httpclient 包

  • 支持设置失败时重试,可以自定义重试次数、重试前延迟等待时间、重试的满足条件;
  • 支持设置失败时告警,可以自定义告警渠道(邮件/微信)、告警的满足条件;
  • 支持设置调用链路;

调用示例代码:

// httpclient 是项目中封装的包
api := "http://127.0.0.1:9999/demo/post"
params := url.Values{}
params.Set("name", name)
body, err := httpclient.PostForm(api, params,
httpclient.WithTrace(ctx.Trace()), // 传递上下文
)

调试信息

只需多传递一个参数即可。

调用示例代码:

// p 是项目中封装的包
p.Println("key", "value",
p.WithTrace(ctx.Trace()), // 传递上下文
)

SQL 信息

稍微复杂一丢丢,需要多传递一个参数,然后再写一个 GORM 插件。

使用的 GORM V2 自带的 CallbacksContext 知识点,细节不多说,可以看下这篇文章:基于 GORM 获取当前请求所执行的 SQL 信息

调用示例代码:

// 原来查询这样写
err := u.db.GetDbR().
First(data, id).
Where("is_deleted = ?", -1).
Error // 现在只需这样写
err := u.db.GetDbR().
WithContext(ctx.RequestContext()).
First(data, id).
Where("is_deleted = ?", -1).
Error // .WithContext 是 GORM V2 自带的。
// 插件的代码就不贴了,去上面的文章查看即可。

Redis 信息

只需多传递一个参数即可。

调用示例代码:

// cache 是基于 go-redis 封装的包
d.cache.Get("name",
cache.WithTrace(c.Trace()),
)

核心原理是啥?

在这没关子可卖,看到这相信老铁们都知道了,就两个:一个是 拦截器,另一个是 Context

如何记录参数?

将以上数据转为 JSON 结构记录到日志中。

JSON 示例

{
"level":"info",
"time":"2021-01-30 22:32:48",
"caller":"core/core.go:444",
"msg":"core-interceptor",
"domain":"go-gin-api[fat]",
"method":"GET",
"path":"/demo/trace",
"http_code":200,
"business_code":1,
"success":true,
"cost_seconds":0.054025302,
"trace_id":"2cdb2f96934f573af391",
"trace_info":{
"trace_id":"2cdb2f96934f573af391",
"request":{
"ttl":"un-limit",
"method":"GET",
"decoded_url":"/demo/trace",
"header":{
"Accept":[
"application/json"
],
"Accept-Encoding":[
"gzip, deflate, br"
],
"Accept-Language":[
"zh-CN,zh;q=0.9,en;q=0.8"
],
"Authorization":[
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
],
"Connection":[
"keep-alive"
],
"Referer":[
"http://127.0.0.1:9999/swagger/index.html"
],
"Sec-Fetch-Dest":[
"empty"
],
"Sec-Fetch-Mode":[
"cors"
],
"Sec-Fetch-Site":[
"same-origin"
],
"User-Agent":[
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36"
]
},
"body":""
},
"response":{
"header":{
"Content-Type":[
"application/json; charset=utf-8"
],
"Trace-Id":[
"2cdb2f96934f573af391"
],
"Vary":[
"Origin"
]
},
"body":{
"code":1,
"msg":"OK",
"data":[
{
"name":"Tom",
"job":"Student"
},
{
"name":"Jack",
"job":"Teacher"
}
],
"id":"2cdb2f96934f573af391"
},
"business_code":1,
"business_code_msg":"OK",
"http_code":200,
"http_code_msg":"OK",
"cost_seconds":0.054024874
},
"third_party_requests":[
{
"request":{
"ttl":"5s",
"method":"GET",
"decoded_url":"http://127.0.0.1:9999/demo/get/Tom",
"header":{
"Authorization":[
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
],
"Content-Type":[
"application/x-www-form-urlencoded; charset=utf-8"
],
"TRACE-ID":[
"2cdb2f96934f573af391"
]
},
"body":null
},
"responses":[
{
"header":{
"Content-Length":[
"87"
],
"Content-Type":[
"application/json; charset=utf-8"
],
"Date":[
"Sat, 30 Jan 2021 14:32:48 GMT"
],
"Trace-Id":[
"2cdb2f96934f573af391"
],
"Vary":[
"Origin"
]
},
"body":"{"code":1,"msg":"OK","data":{"name":"Tom","job":"Student"},"id":"2cdb2f96934f573af391"}",
"http_code":200,
"http_code_msg":"200 OK",
"cost_seconds":0.000555089
}
],
"success":true,
"cost_seconds":0.000580202
},
{
"request":{
"ttl":"5s",
"method":"POST",
"decoded_url":"http://127.0.0.1:9999/demo/post",
"header":{
"Authorization":[
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
],
"Content-Type":[
"application/x-www-form-urlencoded; charset=utf-8"
],
"TRACE-ID":[
"2cdb2f96934f573af391"
]
},
"body":"name=Jack"
},
"responses":[
{
"header":{
"Content-Length":[
"88"
],
"Content-Type":[
"application/json; charset=utf-8"
],
"Date":[
"Sat, 30 Jan 2021 14:32:48 GMT"
],
"Trace-Id":[
"2cdb2f96934f573af391"
],
"Vary":[
"Origin"
]
},
"body":"{"code":1,"msg":"OK","data":{"name":"Jack","job":"Teacher"},"id":"2cdb2f96934f573af391"}",
"http_code":200,
"http_code_msg":"200 OK",
"cost_seconds":0.000450153
}
],
"success":true,
"cost_seconds":0.000468387
}
],
"debugs":[
{
"key":"res1.Data.Name",
"value":"Tom",
"cost_seconds":0.000005193
},
{
"key":"res2.Data.Name",
"value":"Jack",
"cost_seconds":0.000003907
},
{
"key":"redis-name",
"value":"tom",
"cost_seconds":0.000009816
}
],
"sqls":[
{
"timestamp":"2021-01-30 22:32:48",
"stack":"/Users/xinliang/github/go-gin-api/internal/api/repository/db_repo/user_demo_repo/user_demo.go:76",
"sql":"SELECT `id`,`user_name`,`nick_name`,`mobile` FROM `user_demo` WHERE user_name = 'test_user' and is_deleted = -1 ORDER BY `user_demo`.`id` LIMIT 1",
"rows_affected":1,
"cost_seconds":0.031969072
}
],
"redis":[
{
"timestamp":"2021-01-30 22:32:48",
"handle":"set",
"key":"name",
"value":"tom",
"ttl":10,
"cost_seconds":0.009982091
},
{
"timestamp":"2021-01-30 22:32:48",
"handle":"get",
"key":"name",
"cost_seconds":0.010681579
}
],
"success":true,
"cost_seconds":0.054025302
}
}

zap 日志组件

有对日志收集感兴趣的老铁们可以往下看,trace_info 只是日志的一个参数,具体日志参数包括:

参数 数据类型 说明
level String 日志级别,例如:info,warn,error,debug
time String 时间,例如:2021-01-30 16:05:44
caller String 调用位置,文件+行号,例如:core/core.go:443
msg String 日志信息,例如:xx 错误
domain String 域名或服务名,例如:go-gin-api[fat]
method String 请求方式,例如:POST
path String 请求路径,例如:/user/create
http_code Int HTTP 状态码,例如:200
business_code Int 业务状态码,例如:10101
success Bool 状态,true or false
cost_seconds Float64 花费时间,单位:秒,例如:0.01
trace_id String 链路ID,例如:ec3c868c8dcccfe515ab
trace_info Object 链路信息,结构化数据。
error String 错误信息,当出现错误时才有这字段。
errorVerbose String 详细的错误堆栈信息,当出现错误时才有这字段。

日志记录可以使用 zaplogrus ,这次我使用的 zap,简单封装一下即可,比如:

  • 支持设置日志级别;
  • 支持设置日志输出到控制台;
  • 支持设置日志输出到文件;
  • 支持设置日志输出到文件(可自动分割);

总结

这个功能比较常用,使用起来也很爽,比如调用方发现接口出问题时,只需要提供 TRACE-ID 即可,我们就可以查到关于它整个链路的所有信息。

以上代码的实现都在 go-gin-api 项目中,地址:https://github.com/xinliangnote/go-gin-api

Go - 实现项目内链路追踪的更多相关文章

  1. Go - 实现项目内链路追踪(二)

    上篇文章 Go - 实现项目内链路追踪 分享了,通过 链路 ID 可以将 请求信息.响应信息.调用第三方接口的信息.调试信息.执行的 SQL 信息.执行的 Redis 信息 串起来,记录的具体参数在文 ...

  2. 探索链路追踪在.NET6工业物联网项目的应用

    ExploringIoTDistributedTracingNet6 如果觉得有用,请留言学到了. 已经会了的老哥,请留言就这? 可能遇到的问题 工业物联网项目自上而下一般分为ERP.Mes.SCAD ...

  3. 10.源码分析---SOFARPC内置链路追踪SOFATRACER是怎么做的?

    SOFARPC源码解析系列: 1. 源码分析---SOFARPC可扩展的机制SPI 2. 源码分析---SOFARPC客户端服务引用 3. 源码分析---SOFARPC客户端服务调用 4. 源码分析- ...

  4. 服务链路追踪(Spring Cloud Sleuth)

    sleuth:英 [slu:θ] 美 [sluθ] n.足迹,警犬,侦探vi.做侦探 微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元.由于服务单元数量众多,业务的 ...

  5. 阿里P7架构师详解微服务链路追踪原理

    背景介绍 在微服务横行的时代,服务化思维逐渐成为了程序员的基本思维模式,但是,由于绝大部分项目只是一味地增加服务,并没有对其妥善管理,当接口出现问题时,很难从错综复杂的服务调用网络中找到问题根源,从而 ...

  6. 三、链路追踪系统 zipkin

    一.构建项目 用到的依赖直接看pom.xml的注释吧 <?xml version="1.0" encoding="UTF-8"?> <proj ...

  7. 在spring boot中三分钟上手apache顶级分布式链路追踪系统skywalking

    原文:https://juejin.im/post/5cd10e81e51d453b560f2d53 skywalking在apache里全票通过成为了apache顶级链路追踪系统 项目地址:gith ...

  8. Spring Cloud 系列之 Sleuth 链路追踪(一)

    随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务.互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发.可能使用不同的编程语言来实现.有可能布在了 ...

  9. skywalking与pinpoint全链路追踪方案对比

    由于公司目前有200多微服务,微服务之间的调用关系错综复杂,调用关系人工维护基本不可能实现,需要调研一套全链路追踪方案,初步调研之后选取了skywalking和pinpoint进行对比; 选取skyw ...

随机推荐

  1. 如何理解java枚举,看例子

    先来看一下不用枚举怎么表示常量: //常量类 class Num { public static String ONE = "ONE"; public static String ...

  2. 关于STM32的CAN的过滤器

    关于STM32的CAN的过滤器STM32普通型芯片的CAN有14组过滤器组,互联型有28组过滤器组.一般我们用的都是普通型的,所以在本文中可以说STM32有14组过滤器组.根据配置,每1组过滤器组可以 ...

  3. webservcie学习之webservice是什么

    之前写代码,只是用到的时候才去看相关技术,用过后也没有再回头特别 去看,现在突然发现对一些技术的了解不够深刻,故现在准备再从头对用到的技术深入的学习下.就从webservice开始.首先对我不解的地方 ...

  4. 整合.NET WebAPI和 Vuejs——在.NET单体应用中使用 Vuejs 和 ElementUI

    .NET简介 .NET 是一种用于构建多种应用的免费开源开发平台,例如: Web 应用.Web API 和微服务 云中的无服务器函数 云原生应用 移动应用 桌面应用 1). Windows WPF 2 ...

  5. Beta冲刺——汇总随笔

    一.代码规范与计划随笔 Beta冲刺--代码规范与计划 二.凡事预则立随笔 Beta冲刺--凡事预则立 三.10篇冲刺随笔 Beta冲刺--第一天 Beta冲刺--第二天 Beta冲刺--第三天 Be ...

  6. Cocos Creator 新资源管理系统剖析

    目录 1.资源与构建 1.1 creator资源文件基础 1.2 资源构建 1.2.1 图片.图集.自动图集 1.2.2 Prefab与场景 1.2.3 资源文件合并规则 2. 理解与使用 Asset ...

  7. redhat_快捷键和shell命令操作.md

    tab键 命令自动补全 history !n 执行历史记录第n条 !! 执行上一条命令 !l 最后一条以l开头的命令 alias 创建命令的别名 alias 命令别名 = "命令行" ...

  8. go判断字符串是否是IP地址

    前言 现在有这样的需求 正文 使用net包 net包的方法可以判断是否是 ip,需要注意的是 ip 分为 ipv4 和 ipv6 此方法将 v4 和 v6 一起判断出来 address := net. ...

  9. 【SpringBoot1.x】SpringBoot1.x 任务

    SpringBoot1.x 任务 文章源码 异步任务 在 Java 应用中,绝大多数情况下都是通过同步的方式来实现交互处理的.但是在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使 ...

  10. 【SpringMVC】SpringMVC 入门

    SpringMVC 入门 文章源码 SpringMVC 基本概念 在 JavaEE 开发中,几乎全都是基于 B/S 架构的开发.在 B/S 架构中,系统标准的三层架构包括:表现层.业务层.持久层. 表 ...