代码炼金工坊

链路追踪建设分享
December 22, 2020

背景

  • 开发跨部门权限问题,无法在线上(测试、预发布、正式环境)查看容器内错误日志
  • 微服务化,即使有权限查看容器日志,业务出现bug时排查定位问题所在的微服务很慢很麻烦
  • 被动发现问题。比如说某些api的请求出现异常,被动等待下游反馈

链路追踪如何解决问题

  • 上报每一个请求和响应的详细数据,包括header头,路由,请求参数,请求体,通信状态,以及支持自定义更多参数上报。
  • 阿里采集,提供查询接口,开发可以另外搭建查询服务独立查询,无需权限
  • 采用OpenTracing标准,可以跨语言跨服务形成链路调用日志,一个请求经过了哪些服务,在哪里发生了故障被截断一目了然
  • 通过请求和响应状态可以为链路日志打上状态标签,然后上报到Prometheus统计指标。可以另外扩展出指标异常统计告警机制,及时主动获知错误发生,在下游反馈前解决问题

原理简述

Jaeger参考文档

  • header头注入标识,支持多种标识(b3,uber-trace-id等)。我们采用jaeger默认的uber-trace-id标记链路id。
  • id值格式为:{trace-id}:{span-id}:{parent-span-id}:{flags}
    • trace-id:链路追踪id,一次请求从头到尾的唯一标识
    • span-id:span的标识。span是链路追踪的基本单位,表示一个独立的工作单元,可以是一次函数调用,对一个服务的请求等。记录了服务名称,开始结束时间,以及服务打上的tag(上文说的状态异常标签,error=true),logs(记录请求和响应的详细数据)
    • parent-span-id:父span标识
    • flags:采样标志
  • 作为服务端,接收请求的时候检测是否有uber-trace-id标识,如果没有就创建一个,如果有则获取值并按照格式拆分,然后把请求中的span-id改成自身的parent-span-id,生成一个新的span-id
  • 作为客户端,对其他服务发起调用,在请求头注入uber-trace-id标识,传递给被调用服务
  • 服务在一次请求和响应完成后独自上报span
  • 一个trace由多个span组成,表示了一次完整的链路追踪,按照span的父子关系向下排列,以时间轴的方式展示。

落地实践

  • 原子服务集成上报中间件
  • 部署Jaeger服务端,包括Jaeger Agent和Jaegger Collector
  • Agent采用udp协议与客户端(原子服务)通信,无状态,消耗低,适用每个请求都上报的场景。客户端发送就完事,不关心Agent是否收到
  • Agent收集一定量链路日志后批量上报给Collector,用的是http协议,然后Collector再写入到数据库里
  • 阿里提供了日志数据库存储服务。链路日志存储到第三方
  • 通过另外部署的Jaeger Query客户端读取阿里存储的日志,完全和生产服务解耦。可以用默认的客户端查询,或者将客户端暴露的api集成到Grafana查阅。
  • go服务端采用的第三方上报工具内置了指标暴露接口,可以被promehteus采集统计请求状态

go服务端gin中间件使用方法

代码库地址:yuchanns/bullets

go get -u github.com/yuchanns/bullets
  • 使用中间件
package main

import (
	"context"
	"github.com/gin-gonic/gin"
	"github.com/yuchanns/bullets/common"
	"github.com/yuchanns/bullets/common/middlewares"
	"os"
)

func main() {
	g := gin.Default()
	// 服务名
	serviceName := "openapi-service"
	// 上报agent地址
	agentAddr := os.Getenv("OPENTRACING_AGENT")
	// 操作前缀
	operationPrefix := []byte("api-request-")
	opentracerCloseFunc, opentracerMiddleware, err := middlewares.BuildOpenTracerInterceptor(serviceName, agentAddr, operationPrefix)
	if err != nil {
		common.Logger.Error(context.Background(), err)
	} else {
		defer opentracerCloseFunc()
		g.Use(opentracerMiddleware)
	}
}
  • 自定义打tag
import (
	"github.com/gin-gonic/gin"
	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/log"
	"github.com/pkg/errors"
)

func CustomTag(ctx *gin.Context) {
	if cspan, ok := ctx.Get("tracing-context"); ok {
		if span, ok := cspan.(opentracing.Span); ok {
			span.SetTag("error", true)
			span.LogFields(log.Error(errors.New("err")))
			span.LogFields(log.String("exampleKey", "stringValue"))
		}
	}
}