关于后端:分布式链路追踪Jaeger在Golang中的使用

4次阅读

共计 6345 个字符,预计需要花费 16 分钟才能阅读完成。

导读】本文介绍了 Tracing 零碎 Jeager 在 go 我的项目中的集成实际。

一个残缺的微服务体系至多须要包含:

  • CI / CD 也就是自动化部署
  • 服务发现
  • 对立的 PRC 协定
  • 监控
  • 追踪 (Tracing)
    要配置下面这些货色堪称说超级简单, 所以我倡议读者 如果能够间接应用 istio

它弱小到蕴含了微服务开发须要思考的所有货色, 上图中的”Observe”就包含了这篇文章所说的”链路追踪(Tracing)”.

但软件行业没有银弹, 弱小的工具天然须要弱小的人员去治理, 在进阶为大佬之前, 还是得钻研一些传统的计划以便成长, 所以便有了这篇文章.

Tracing 在微服务中的作用

和传统单体服务不同, 微服务通常部署在一个分布式的零碎中, 并且一个申请可能会通过好几个微服务的解决, 这样的环境下谬误和性能问题就会更容易产生, 所以察看 (Observe) 尤为重要,
这就是 Tracing 的用武之地, 它收集调用过程中的信息并可视化, 让你晓得在每一个服务调用过程的耗时等状况, 以便及早发现问题.

为什么是 Jaeger

笔者正在学习 Golang, 选用应用 Golang 并开源的 Tracing 零碎 – Jaeger 当然就不再须要理由了. (`⌒´メ)

Uber 出品也不会太差。

装置

为了疾速上手, 官网提供了”All in One”的 docker 镜像, 启动 Jaeger 服务只须要一行代码:

$ 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 16686:16686 \  -p 14268:14268 \  -p 9411:9411 \  jaegertracing/all-in-one:1.12

具体端口作用就不再赘述, 官网文档都有.

All in One 只应该用于试验环境. 如果是生产环境, 你须要按官网这样部署

本文在前面会讲到部署并应用 Elasticsearch 作为存储后端.

当初用于测试的服务端就实现了, 你能够拜访

http://{host}:16686

来拜访 JaegerUI, 它就像这样:

客户端

当初就能够编写客户端了, 官网提供了 Go/Java/Node.js/Python/C++/C# 语言的客户端库, 读者可自行抉择, 应用形式可在各自的仓库中查看.

我也只试验了 Golang 客户端, 先从最简略的场景动手:

在单体利用中实现 Tracing.
在编写代码之前还得了解下 Jaeger 中最根底的几个概念, 也是 OpenTracing
的数据模型: Trace / Span

Trace: 调用链, 其中蕴含了多个 Span.
Span: 跨度, 计量的最小单位, 每个跨度都有开始工夫与截止工夫. Span 和 Span 之间能够存在 References(关系): ChildOf 与 FollowsFrom
如下图 (来至凋谢分布式追踪(OpenTracing)入门与 Jaeger 实现)

单个 Trace 中,span 间的因果关系

        [Span A]  ←←←(the root span)
            |
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C 是 Span A 的孩子节点, ChildOf)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                       ↑
                                       ↑
                                       ↑
                         (Span G 在 Span F 后被调用, FollowsFrom)

接下来是代码工夫, 参考我的项目的 Readme(https://github.com/jaegertrac…)和搜索引擎不难写出以下代码

package tests
import (
    "context"
    "github.com/opentracing/opentracing-go"
    "github.com/uber/jaeger-client-go"
    "log"
    "testing"
    "time"
    jaegercfg "github.com/uber/jaeger-client-go/config"
)
func TestJaeger(t *testing.T) {
    cfg := jaegercfg.Configuration{
        Sampler: &jaegercfg.SamplerConfig{
            Type:  jaeger.SamplerTypeConst,
            Param: 1,
        },
        Reporter: &jaegercfg.ReporterConfig{
            LogSpans:           true,
            LocalAgentHostPort: "{host}:6831", // 替换 host
        },
    }
    closer, err := cfg.InitGlobalTracer("serviceName",)
    if err != nil {log.Printf("Could not initialize jaeger tracer: %s", err.Error())
        return
    }
    var ctx = context.TODO()
    span1, ctx := opentracing.StartSpanFromContext(ctx, "span_1")
    time.Sleep(time.Second / 2)
    span11, _ := opentracing.StartSpanFromContext(ctx, "span_1-1")
    time.Sleep(time.Second / 2)
    span11.Finish()
    span1.Finish()
    defer closer.Close()}

代码惟一须要留神的中央是 closer, 这个 closer 在程序完结时肯定记得敞开, 因为在客户端中 span 信息的发送不是同步发送的, 而是有一个暂存区, 调用 closer.Close()就会让暂存区的 span 强制发送到 agent.

运行之, 咱们就能够在 UI 看到:

点击进入详情就能看到咱们刚刚收集到的调用信息

通过 Grpc 中间件应用

在单体程序中, 父子 Span 通过 context 关联, 而 context 是在内存中的, 不言而喻这样的办法在垮利用的场景下是行不通的.

垮利用通信应用的形式通常是”序列化”, 在 jaeger-client-go 库中也是通过相似的操作去传递信息, 它们叫:Tracer.Inject() 与 Tracer.Extract().

其中 inject 办法反对将 span 系列化成几种格局:

  • Binary: 二进制
  • TextMap: key=>value
  • HTTPHeaders: Http 头, 其实也是 key=>value
    正好 grpc 反对传递 metadata 也是 string 的 key=>value 模式, 所以咱们就能通过 metadata 实现在不同利用间传递 Span 了.

这段代码在 github 上有人实现了

题外话: 下面的库应用到了 grpc 的 Interceptor, 但 grpc 不反对多个 Interceptor, 所以当你又应用到了其余中间件 (如 grpc_retry) 的话就能导致抵触. 同样也能够应用这个库 grpc_middleware.ChainUnaryClient 解决这个问题.

在 grpc 服务端的中间件代码如下(已省略错误处理)

import (
    "context"
    "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
    "google.golang.org/grpc"
)
jcfg := jaegercfg.Configuration{
        Sampler: &jaegercfg.SamplerConfig{
            Type:  "const",
            Param: 1,
        },
        ServiceName: "serviceName",
    }
report := jaegercfg.ReporterConfig{
        LogSpans:           true,
        LocalAgentHostPort: "locahost:6831",
    }
reporter, _ := report.NewReporter(serviceName, jaeger.NewNullMetrics(), jaeger.NullLogger)
tracer, closer, _ = jcfg.NewTracer(jaegercfg.Reporter(reporter),
)
server := grpc.NewServer(grpc.UnaryInterceptor(grpc_opentracing.UnaryServerInterceptor(grpc_opentracing.WithTracer(tracer))))
在 grpc 客户端的中间件代码如下

conn, err := grpc.Dial(addr, grpc.WithUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor(grpc_opentracing.WithTracer(tracer),
)))

当初服务端和客户端之间的调用状况就能被 jaeger 收集到了.

在业务代码中应用

有时候只监控一个”api”是不够的,还须要监控到程序中的代码片段(如办法),能够这样封装一个办法

package tracer
type SpanOption func(span opentracing.Span)
func SpanWithError(err error) SpanOption {return func(span opentracing.Span) {
        if err != nil {ext.Error.Set(span, true)
            span.LogFields(tlog.String("event", "error"), tlog.String("msg", err.Error()))
        }
    }
}
// example:
// SpanWithLog(
//    "event", "soft error",
//    "type", "cache timeout",
//    "waited.millis", 1500)
func SpanWithLog(arg ...interface{}) SpanOption {return func(span opentracing.Span) {span.LogKV(arg...)
    }
}
func Start(tracer opentracing.Tracer, spanName string, ctx context.Context) (newCtx context.Context, finish func(...SpanOption)) {
    if ctx == nil {ctx = context.TODO()
    }
    span, newCtx := opentracing.StartSpanFromContextWithTracer(ctx, tracer, spanName,
        opentracing.Tag{Key: string(ext.Component), Value: "func"},
    )
    finish = func(ops ...SpanOption) {
        for _, o := range ops {o(span)
        }
        span.Finish()}
    return
}

应用

newCtx, finish := tracer.Start("DoSomeThing", ctx)
err := DoSomeThing(newCtx)
finish(tracer.SpanWithError(err))
if err != nil{...}

最初能失去一个像这样的后果

能够看到在服务的调用过程中各个 span 的工夫,这个 span 能够是一个微服务之间的调用也能够是某个办法的调用。

点开某个 span 也能看到额定的 log 信息。

通过 Gin 中间件中应用
在我的我的项目中应用 http 服务作为网关提供给前端应用,那么这个 http 服务层就是 root span 而不必关怀父 span 了,编写代码就要简略一些。

封装一个 gin 中间件就能实现

import (
    "context"
    "github.com/gin-gonic/gin"
    "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/ext"
)
engine.Use(func(ctx *gin.Context) {
        path := ctx.Request.URL.Path
        span := j.tracer.StartSpan(path,
            ext.SpanKindRPCServer)
        ext.HTTPUrl.Set(span, path)
        ext.HTTPMethod.Set(span, ctx.Request.Method)
        c := opentracing.ContextWithSpan(context.Background(), span)
        ctx.Set("ctx", c)
        ctx.Next()
        ext.HTTPStatusCode.Set(span, uint16(ctx.Writer.Status()))
        span.Finish()})
    
如果须要向上层传递 context 则这样获取 context

func Api(gtx *gin.Context) {ctx = gtx.Get("ctx").(context.Context)
}

结语

应用 trace 会入侵局部代码,特地是追踪一个办法,但这是不可避免的(应用 istio 框架能缓解这个问题,倡议有趣味的敌人钻研一下)。其实并不是整个零碎的服务都须要追踪,可只针对于重要或者有性能问题的中央进行追踪。

部署篇
应用 Elasticsearch 作为存储后端
笔者对于 Elasticsearch 更为相熟, 故抉择它了.

es 的部署就不说了.

这里是 jaeger 的 docker-compose.yaml

version: '2'
services:
  jaeger-agent:
    image: jaegertracing/jaeger-agent:1.12
    stdin_open: true
    tty: true
    links:
    - jaeger-collector:jaeger-collector
    ports:
    - 6831:6831/udp
    command:
    - --reporter.grpc.host-port=jaeger-collector:14250
  jaeger-collector:
    image: jaegertracing/jaeger-collector:1.12
    environment:
      SPAN_STORAGE_TYPE: elasticsearch
      ES_SERVER_URLS: http://elasticsearch:9200
    stdin_open: true
    external_links:
    - elasticsearch/elasticsearch:elasticsearch
    tty: true
  jaeger-query:
    image: jaegertracing/jaeger-query:1.12
    environment:
      SPAN_STORAGE_TYPE: elasticsearch
      ES_SERVER_URLS: http://elasticsearch:9200
    stdin_open: true
    external_links:
    - elasticsearch/elasticsearch:elasticsearch
    tty: true
    ports:
    - 16686:16686/tcp

其中 agent 和 collect 都被设计成无状态的,也就意味着他们能够被放在代理 (如 Nginx) 前面而实现负载平衡。

侥幸的是笔者在部署过程中没有遇见任何问题,所以也就没有”疑难杂症”环节了。一般来说遇到的问题都能够去 issue 搜到。

转自:

cnblogs.com/ExMan/p/12084524.html

本文由 mdnice 多平台公布

正文完
 0