微服务架构 作为云原生核心技术之一,提倡将单体应用程序(巨石架构)划分成一组小的服务(微服务),服务之间互相协调、互相配合,为用户提供最终价值。

微服务架构设计中,通常由多个微服务组件组成,有 1) API 网关 ( apisix, kong, traefik ) 负责认证鉴权、负载均衡、限流和静态响应处理; 2) 服务注册发现中心( ZooKeeper、Consul 、ETCD ) ,负责服务的注册和发现。3)可观测性 负责日志收集查看的ELK、Loki,负责服务性能指标告警的指标 Metrics 监控 Prometheus, 负责追踪请求的 Tracing 链路追踪。在多个组件的组成下,才能顺利组成一个好的微服务架构。

今天我就来简单的讲一讲微服务组成中可观测性的分布式链路追踪。

OpenTracing 介绍

OpenTracing是一个新的、开放的分布式追踪标准,用于应用程序和OSS包。有过大规模构建微服务经验的开发者都知道分布式追踪的作用和重要性:每个进程的日志和指标监控都有它们的用武之地,但它们都无法重建事务在分布式系统中传播时的复杂旅程。分布式跟踪就是这些旅程。

OpenTracing 项目定义了一套分布式追踪的标准,以统一各种分布式追踪系统的实现。OpenTracing 中包含了一套分布式追踪的标准规范,各种语言的 API,以及实现了该标准的编程框架和函数库。

OpenTracing 提供了平台无关、厂商无关的 API,因此开发者只需要对接 OpenTracing API,无需关心后端采用的到底是什么分布式追踪系统,Jager、Skywalking、LightStep 等都可以无缝切换。

数据模型

OpenTracing 定义了以下数据模型:

Trace (调用链):一个 Trace 代表一个事务或者流程在(分布式)系统中的执行过程。例如来自客户端的一个请求从接收到处理完成的过程就是一个 Trace。

Span(跨度):Span 是分布式追踪的最小跟踪单位,一个 Trace 由多段 Span 组成。可以被理解为一次方法调用, 一个程序块的调用, 或者一次 RPC/数据库访问。只要是一个具有完整时间周期的程序访问,都可以被认为是一个 Span。

SpanContext(跨度上下文):分布式追踪的上下文信息,包括 Trace id,Span id 以及其它需要传递到下游服务的内容。一个 OpenTracing 的实现需要将 SpanContext 通过某种序列化协议 (Wire Protocol) 在进程边界上进行传递,以将不同进程中的 Span 关联到同一个 Trace 上。对于 HTTP 请求来说,SpanContext 一般是采用 HTTP header 进行传递的。

OpenTracing for go

接下来 创建 main.go ,实现一个 Web 服务,并在请求流程中使用 OpenTracing API 进行埋点处理。

code:

package main

import (
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/opentracing/opentracing-go"
)

var (
	port = "8080"
	addr = ":8080"
)

func main() {
	engine := gin.New()
	engine.GET("/", indexHandler)
	engine.GET("/home", homeHandler)
	engine.GET("/async", serviceHandler)
	engine.GET("/service", serviceHandler)
	engine.GET("/db", dbHandler)
	fmt.Printf("http://localhost:%s\n", port)
	engine.Run(addr)
}

func dbHandler(c *gin.Context) {
	var sp opentracing.Span
	opName := c.Request.URL.Path
	wireContext, err := opentracing.GlobalTracer().Extract(
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(r.Header))
	if err != nil {
		// 获取失败,则直接新建一个根节点 span
		sp = opentracing.StartSpan(opName)
	} else {
		sp = opentracing.StartSpan(opName, opentracing.ChildOf(wireContext))
	}
	defer sp.Finish()

	time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}

func serviceHandler(c *gin.Context) {
	// 通过http header,提取span元数据信息
	var sp opentracing.Span
	opName := c.Request.URL.Path
	wireContext, err := opentracing.GlobalTracer().Extract(
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(c.Request.Header))
	if err != nil {
		// 获取失败,则直接新建一个根节点 span
		sp = opentracing.StartSpan(opName)
	} else {
		sp = opentracing.StartSpan(opName, opentracing.ChildOf(wireContext))
	}
	defer sp.Finish()

	dbReq, _ := http.NewRequest("GET", "http://localhost:8080/db", nil)
	err = sp.Tracer().Inject(sp.Context(),
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(dbReq.Header))
	if err != nil {
		log.Fatalf("[dbReq]无法添加span context到http header: %v", err)
	}
	if _, err = http.DefaultClient.Do(dbReq); err != nil {
		sp.SetTag("error", true)
		sp.LogKV("请求 /db error", err)
	}

	time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}

func homeHandler(c *gin.Context) {
	c.String(200, "开始请求...\n")

	// 设置一个根节点 span
	span := opentracing.StartSpan("请求 /home")
	defer span.Finish()

	asyncReq, _ := http.NewRequest("GET", "http://localhost:8080/async", nil)
	err := span.Tracer().Inject(span.Context(),
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(asyncReq.Header))
	if err != nil {
		log.Fatalf("[asyncReq]无法添加span context到http header: %v", err)
	}
	go func() {
		if _, err := http.DefaultClient.Do(asyncReq); err != nil {
			span.SetTag("error", true)
			span.LogKV(fmt.Sprintf("请求 /async error: %v", err))
		}
	}()

	time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)

	syncReq, _ := http.NewRequest("GET", "http://localhost:8080/service", nil)
	err = span.Tracer().Inject(span.Context(),
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(syncReq.Header))
	if err != nil {
		log.Fatalf("[syncReq]无法添加span context到http header: %v", err)
	}
	if _, err = http.DefaultClient.Do(syncReq); err != nil {
		span.SetTag("error", true)
		span.LogKV(fmt.Sprintf("请求 /service error: %v", err))
	}
	c.String(200, "请求结束!")
}

func indexHandler(c *gin.Context) {
	c.String(200, string(`<a href="/home"> 点击开始发起请求 </a>`))
}

就这样一个使用 OpenTracing API 进行链路追踪的 web 服务就这样完成了,接下来只需要在应用程序启动时连接到任意实现了 OpenTracing 标准的链路追踪系统即可。

Jaeger

接下来介绍实现了 OpenTracing API 标准的链路追踪系统 Jaeger。

Jaeger 受 Dapper 和 OpenZipkin 的启发,是 Uber Technologies 开源的分布式跟踪系统,遵循 OpenTracing 标准,功能包括:

  • 分布式上下文传播
  • 监控分布式事务
  • 执行根原因分析
  • 服务依赖分析
  • 优化性能和延迟时间

Jaeger 是云原生计算基金会( CNCF )毕业项目。

架构

Jaeger可以被部署为一体式二进制,即所有Jaeger后端组件在一个单一进程中运行,也可以被部署为可扩展的分布式系统。

Jaeger 部署

因为环境所限,就采用最简单的部署方式,下载 jaeger-1.26.0-windows-amd64.tar ,解压。

进入解压后的文件夹,执行命令行 jaeger-all-in-one –processor.zipkin-compact.server-host-port=9411。

然后在浏览器输入 http://localhost:16686/ ,就可以看到 Jaeger UI 。如下图: img.png

Jaeger 使用

上面已经成功安装运行 Jaeger 了, 接下来给我们的应用程序接入 Jaeger。

安装 Jaeger go Client

go get -u github.com/uber/jaeger-client-go

修改后的代码:

package main

import (
	"fmt"

	"log"
	"math/rand"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	jaegercfg "github.com/uber/jaeger-client-go/config"
	jaegerlog "github.com/uber/jaeger-client-go/log"
	"github.com/uber/jaeger-lib/metrics"
)

var (
	port = "8080"
	addr = ":8080"
)

func init() {
	cfg := jaegercfg.Configuration{
		Sampler: &jaegercfg.SamplerConfig{
			Type:  jaeger.SamplerTypeConst,
			Param: 1,
		},
		Reporter: &jaegercfg.ReporterConfig{
			LogSpans: true,
		},
	}
	_, err := cfg.InitGlobalTracer(
		"jaeger-example", // 服务名
		jaegercfg.Logger(jaegerlog.StdLogger),
		jaegercfg.Metrics(metrics.NullFactory),
	)
	if err != nil {
		panic(err)
	}
}

func main() {
	engine := gin.New()
	engine.GET("/", indexHandler)
	engine.GET("/home", homeHandler)
	engine.GET("/async", serviceHandler)
	engine.GET("/service", serviceHandler)
	engine.GET("/db", dbHandler)
	engine.Run(addr)
}

func dbHandler(c *gin.Context) {
	var sp opentracing.Span
	opName := c.Request.URL.Path
	wireContext, err := opentracing.GlobalTracer().Extract(
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(c.Request.Header))
	if err != nil {
		// 获取失败,则直接新建一个根节点 span
		sp = opentracing.StartSpan(opName)
	} else {
		sp = opentracing.StartSpan(opName, opentracing.ChildOf(wireContext))
	}
	defer sp.Finish()

	time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}

func serviceHandler(c *gin.Context) {

	// 通过http header,提取span元数据信息
	var sp opentracing.Span
	opName := c.Request.URL.Path
	wireContext, err := opentracing.GlobalTracer().Extract(
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(c.Request.Header))
	if err != nil {
		// 获取失败,则直接新建一个根节点 span
		sp = opentracing.StartSpan(opName)
	} else {
		sp = opentracing.StartSpan(opName, opentracing.ChildOf(wireContext))
	}
	defer sp.Finish()

	dbReq, _ := http.NewRequest("GET", "http://localhost:8080/db", nil)
	err = sp.Tracer().Inject(sp.Context(),
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(dbReq.Header))
	if err != nil {
		log.Fatalf("[dbReq]无法添加span context到http header: %v", err)
	}
	if _, err = http.DefaultClient.Do(dbReq); err != nil {
		sp.SetTag("error", true)
		sp.LogKV("请求 /db error", err)
	}

	time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}

func homeHandler(c *gin.Context) {
	c.Header("Content-Type", "text/html; charset=utf-8")

	c.String(200, "开始请求...\n")

	// 设置一个根节点 span
	span := opentracing.StartSpan("请求 /home")
	defer span.Finish()

	asyncReq, _ := http.NewRequest("GET", "http://localhost:8080/async", nil)
	err := span.Tracer().Inject(span.Context(),
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(asyncReq.Header))
	if err != nil {
		log.Fatalf("[asyncReq]无法添加span context到http header: %v", err)
	}
	go func() {
		if _, err := http.DefaultClient.Do(asyncReq); err != nil {
			span.SetTag("error", true)
			span.LogKV(fmt.Sprintf("请求 /async error: %v", err))
		}
	}()

	time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)

	syncReq, _ := http.NewRequest("GET", "http://localhost:8080/service", nil)
	err = span.Tracer().Inject(span.Context(),
		opentracing.TextMap,
		opentracing.HTTPHeadersCarrier(syncReq.Header))
	if err != nil {
		log.Fatalf("[syncReq]无法添加span context到http header: %v", err)
	}
	if _, err = http.DefaultClient.Do(syncReq); err != nil {
		span.SetTag("error", true)
		span.LogKV(fmt.Sprintf("请求 /service error: %v", err))
	}
	c.String(200, "请求结束!")
}

func indexHandler(c *gin.Context) {
	c.Header("Content-Type", "text/html; charset=utf-8")
	c.String(200, string(`<a href="/home"> 点击开始发起请求 </a>`))
}

启动程序,浏览器访问 http://localhost:8080/

img_1.png 点击链接,访问 Jaeger UI,可以看到刚才的请求链。

img_2.png

小结

今天就简单介绍了 opentracing 和 使用 Jaeger 进行链路追踪。

参考链接: