推荐去官网系统的了解一下 https://www.jaegertracing.io

随着应用容器化和微服务的兴起,借由Docker和 Kubernetes 等工具, 服务的快速开发和部署成为可能,构建微服务应用变得越来越简单。但是随着大型单体应用拆分为微服务,服务之间的依赖和调用变得极为复杂,这些服务可能是不同团队开发的,可能基于不同的语言,微服务之间可能是利用RPC, RESTful API, 也可能是通过消息队列实现调用或通讯。如何理清服务依赖调用关系,如何在这样的环境下快速debug, 追踪服务处理耗时,查找服务性能瓶颈, 合理对服务的容量评估都变成一个棘手的事情。

Tracing在微服务中的作用

 

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

在上图可以看到api层一共花了4.03s, 然后其中调用其他服务: 'service-1'花了2.12s, 而service-1又调用了'service-2'花费了2.12s, 用这样的图示很容易就能排查到系统存在的问题. 在这里我只展示了时间, 如果需要追踪其他信息(如错误信息)也是可以实现的.

为什么是Jaeger

 

支持 OpenTracing 的 server 端有很多,我们总要选一个 。在这里,选用 jaeger 。 jaeger 的开发较为活跃,支持的客户端实现也较多。由于采用了 golang 开发,发行包也比较简洁。
jaeger的官网是 www.jaegertracing.io/

特点

 

  1. jaeger的开发语言是golang
  2. jaeger支持OpenTracing协议,同属于CNCF基金会
  3. jaeger支持各种各样的客户端,包括Go、Java、Node、Python、C++等
  4. jaeger支持udp协议传输,当然也支持http

jaeger能够解决以下问题

 

  1. 分布式事务监控
  2. 性能分析与性能优化
  3. 调用链,找到根源问题
  4. 服务依赖分析(需大数据分析)

安装需了解的技术栈:

 

  1. OpenTracing
  2. Golang
  3. ElasticSearch
  4. Kafka (可选)
     

OpenTracing 标准

 

云原生基金会(CNCF) 推出了 OpenTracing 标准,推进Tracing协议和工具的标准化, 统一 Trace 数据结构和格式。 OpenTracing 通过提供平台无关、厂商无关的 API,使得开发人员能够方便的添加(或更换)追踪系统的实现。比如从Zipkin替换成Jaeger/Skywalking等后端。
在OpenTracing中,有两个主要概念:
1、Trace(调用链): OpenTracing中的Trace(调用链)通过归属于此调用链的Span来隐性的定义。一条Trace(调用链)可以被认为是一个由多个Span组成的有向无环图(DAG图), Span与Span的关系被命名为References。

2、Span(跨度):可以被理解为一次方法调用, 一个程序块的调用, 或者一次RPC/数据库访问. 只要是一个具有完整时间周期的程序访问,都可以被认为是一个Span。

单个Trace中,Span间的因果关系如下图:

 

这里使用目前比较流行的Tracing开源方案Jaeger进行实践,使用jaeger-client-go这个库作为client

github地址:GitHub - jaegertracing/jaeger-client-go: Jaeger Bindings for Go OpenTracing API.

开放分布式追踪(OpenTracing)入门与 Jaeger 实现

jaeger架构

 

jaeger组件介绍:
jaeger-client:jaeger 的客户端,实现了opentracing协议;
jaeger-agent:jaeger client的一个代理程序,client将收集到的调用链数据发给agent,然后由agent发给collector;
jaeger-collector:负责接收jaeger client或者jaeger agent上报上来的调用链数据,然后做一些校验,比如时间范围是否合法等,最终会经过内部的处理存储到后端存储;
jaeger-query:专门负责调用链查询的一个服务,有自己独立的UI;
jaeger-ingester:中文名称“摄食者”,可用从kafka读取数据然后写到jaeger的后端存储,比如Cassandra和Elasticsearch;
spark-job:基于spark的运算任务,可以计算服务的依赖关系,调用次数等;

 

其中jaeger-collector和jaeger-query是必须的,其余的都是可选的,我们没有采用agent上报的方式,而是让客户端直接通过endpoint上报到collector。

官方文档的demo:example
首先,本地起一个jaeger服务作为测试用的服务端,官方提供了”All in One”的docker镜像, 启动Jaeger服务只需要一行代码:

  1. docker run -d --name jaeger \
  2. -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  3. -p 5775:5775/udp \
  4. -p 6831:6831/udp \
  5. -p 6832:6832/udp \
  6. -p 5778:5778 \
  7. -p 16686:16686 \
  8. -p 14268:14268 \
  9. -p 9411:9411 \
  10. jaegertracing/all-in-one:1.12

本人使用下载好的golang二进制文件启动的,jaeger官网地址:https://www.jaegertracing.io/downlo

jaeger的二进制发行包包含五个二进制文件:

  1. jaeger-agent
  2. jaeger-collector
  3. jaeger-query
  4. jaeger-standalone
  5. jaeger-ingester

如果没有执行权限,可以使用

  1. chmod a+x jaeger-*

选择存储

 

trace数据总要存在一个地方。jaeger支持 ES 和 Canssandra 两种后端DB。国内用ES的多一点,我们就以ES为例,来介绍其安装方式。
ES请先自行安装。
由于上面四个命令都有很多参数,所以我们可以创建几个脚本,来支持jaeger的启动。
start-collector.sh

 

  1. export SPAN_STORAGE_TYPE=elasticsearch
  1. nohup ./jaeger-collector --es.server-urls http://10.66.177.152:9200/ --log-level=debug > collector.log 2>&1 &

 

start-agent.sh

  1. export SPAN_STORAGE_TYPE=elasticsearch
  2. nohup .\jaeger-agent.exe --reporter.grpc.host-port=192.168.1.234:14250  --log-level=debug > agent.log 2>&1 &

start-query.sh

  1. export SPAN_STORAGE_TYPE=elasticsearch
  2. nohup ./jaeger-query --span-storage.type=elasticsearch --es.server-urls=http://10.66.177.152:9200/ > query.log 2>&1 &

 

部署方式

 

jaeger有两种部署方式。下面一一介绍。如果你的数据量特别多,使用kafka缓冲一下也是可以的(所以就引入了另外一个组件jaeger-ingester),不多做介绍。

简易环境

 

 

这种方式一般用在dev环境或者其他测试环境。只需要部署一个单一节点即可。我们的app,需要手动填写agent的地址,这个地址一般都是固定的。
这些环境的流量很小,一个agent是足够的。

 

生产环境

 

 

上面这种部署方式,适合生产环境。agent安装在每一台业务机器上。Client端的目标agent只需要填写localhost即可。
这种方式的好处是生产环境的配置非常的简单。即使你的机器是混合部署的,也能正常收集trace信息。

调用关系图

 

jaeger的调用关系图是使用spark任务进行计算的。项目地址为:
https://github.com/jaegertracing/spark-dependencies

 

端口整理

 

Agent

  1. 5775 UDP协议,接收兼容zipkin的协议数据
  2. 6831 UDP协议,接收兼容jaeger的兼容协议
  3. 6832 UDP协议,接收jaeger的二进制协议
  4. 5778 HTTP协议,数据量大不建议使用

 

它们之间的传输协议都是基于thrift封装的。我们默认使用5775作为传输端口

Collector

  1. 14267 tcp agent发送jaeger.thrift格式数据
  2. 14250 tcp agent发送proto格式数据(背后gRPC)
  3. 14268 http 直接接受客户端数据
  4. 14269 http 健康检查

Query

  1. 16686 http jaeger的前端,放给用户的接口
  2. 16687 http 健康检查

至此,我们的jaeger就安装完毕。
以上,就是我们的环境准备。有了一个server接收数据,调用链的主要工作就在于客户端开发

接下来,代码时间, 参考项目的Readme和搜索引擎不难写出以下代码

 1,最简单的使用模式

2,多个函数之间调用

3,http请求

clinet中同步请求:8081/format,:8088/publish

package main

import (
    "fmt"
    "context"
    "github.com/opentracing/opentracing-go/ext"
    "io/ioutil"
    "net/http"
    opentracing "github.com/opentracing/opentracing-go"
    jaeger "github.com/uber/jaeger-client-go"
    config "github.com/uber/jaeger-client-go/config"
    "github.com/opentracing/opentracing-go/log"
    "io"
    "net/url"
)

// Init returns an instance of Jaeger Tracer that samples 100% of traces and logs all spans to stdout.
func initJaeger(service string) (opentracing.Tracer, io.Closer) {
    cfg := &config.Configuration{
        Sampler:&config.SamplerConfig{
            Type:     "const",
            Param:1,
        },
        Reporter: &config.ReporterConfig{
            LogSpans:            true,
            //LocalAgentHostPort:  "192.168.1.234:6831",
            LocalAgentHostPort:  "192.168.2.246:6831",
        },
    }
    tracer, closer, err := cfg.New(service, config.Logger(jaeger.StdLogger))
    if err != nil {
        panic(fmt.Sprintf("Error: connot init Jaeger: %v\n", err))
    }
    return tracer, closer
}



func main() {
    tracer, closer := initJaeger("http-demo-client")
    defer closer.Close()
    opentracing.SetGlobalTracer(tracer)

    span := tracer.StartSpan("say-hello")
    span.SetTag("hello-to", "helloTo")
    defer span.Finish()

    ctx := opentracing.ContextWithSpan(context.Background(), span)
    helloStr := formatString(ctx, "helloTo")
    printHello(ctx, helloStr)
    fmt.Println("exit")

}

func formatString(ctx context.Context, helloTo string) string {
    span, _ := opentracing.StartSpanFromContext(ctx, "formatString")
    defer span.Finish()

    v := url.Values{}
    v.Set("helloTo", helloTo)
    url := "http://localhost:8081/format?" + v.Encode()
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        panic(err.Error())
    }

    ext.SpanKindRPCClient.Set(span)
    ext.HTTPUrl.Set(span, url)
    ext.HTTPMethod.Set(span, "GET")

    span.Tracer().Inject(
        span.Context(),
        opentracing.HTTPHeaders,
        opentracing.HTTPHeadersCarrier(req.Header),
    )

    resp, err := httpDo(req)
    if err != nil {
        panic(err.Error())
    }

    helloStr := string(resp)

    span.LogFields(
        log.String("event", "string-format"),
        log.String("value", helloStr),
    )

    return helloStr
}

func printHello(ctx context.Context, helloStr string) {
    span, _ := opentracing.StartSpanFromContext(ctx, "printHello")
    defer span.Finish()

    v := url.Values{}
    v.Set("helloStr", helloStr)
    url := "http://localhost:8088/publish?" + v.Encode()
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        panic(err.Error())
    }

    ext.SpanKindRPCClient.Set(span)
    ext.HTTPUrl.Set(span, url)
    ext.HTTPMethod.Set(span, "GET")
    span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))

    if _, err := httpDo(req); err != nil {
        panic(err.Error())
    }
}

func httpDo(req *http.Request) ([]byte, error) {
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("StatusCode: %d, Body: %s", resp.StatusCode, body)
    }

    return body, nil
}
package main

import (
    "fmt"
    "io"
    "log"
    "net/http"

    opentracing "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/ext"
    otlog "github.com/opentracing/opentracing-go/log"
    jaeger "github.com/uber/jaeger-client-go"
    config "github.com/uber/jaeger-client-go/config"
)

// Init returns an instance of Jaeger Tracer that samples 100% of traces and logs all spans to stdout.
func initJaeger(service string) (opentracing.Tracer, io.Closer) {
    cfg := &config.Configuration{
        Sampler:&config.SamplerConfig{
            Type:     "const",
            Param:1,
        },
        Reporter: &config.ReporterConfig{
            LogSpans:            true,
            //LocalAgentHostPort:  "192.168.1.234:6831",
            LocalAgentHostPort:  "192.168.2.246:6831",
        },
    }
    tracer, closer, err := cfg.New(service, config.Logger(jaeger.StdLogger))
    if err != nil {
        panic(fmt.Sprintf("Error: connot init Jaeger: %v\n", err))
    }
    return tracer, closer
}

func main() {
    tracer, closer := initJaeger("http-formatter")
    defer closer.Close()

    http.HandleFunc("/format", func(w http.ResponseWriter, r *http.Request) {
        spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
        span := tracer.StartSpan("format", ext.RPCServerOption(spanCtx))
        defer span.Finish()

        helloTo := r.FormValue("helloTo")
        helloStr := fmt.Sprintf("Hello, I am format %s!", helloTo)
        span.LogFields(
            otlog.String("event", "string-format"),
            otlog.String("value", helloStr),
        )
        w.Write([]byte(helloStr))
    })

    log.Fatal(http.ListenAndServe(":8081", nil))
}

formater/main.go
package main

import (
    "fmt"
    "io"
    "log"
    "net/http"

    opentracing "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/ext"
    otlog "github.com/opentracing/opentracing-go/log"
    jaeger "github.com/uber/jaeger-client-go"
    config "github.com/uber/jaeger-client-go/config"
)

// Init returns an instance of Jaeger Tracer that samples 100% of traces and logs all spans to stdout.
func initJaeger(service string) (opentracing.Tracer, io.Closer) {
    cfg := &config.Configuration{
        Sampler:&config.SamplerConfig{
            Type:     "const",
            Param:1,
        },
        Reporter: &config.ReporterConfig{
            LogSpans:            true,
            //LocalAgentHostPort:  "192.168.1.234:6831",
            LocalAgentHostPort:  "192.168.2.246:6831",
        },
    }
    tracer, closer, err := cfg.New(service, config.Logger(jaeger.StdLogger))
    if err != nil {
        panic(fmt.Sprintf("Error: connot init Jaeger: %v\n", err))
    }
    return tracer, closer
}

func main() {
    tracer, closer := initJaeger("http-publish")
    defer closer.Close()

    http.HandleFunc("/publish", func(w http.ResponseWriter, r *http.Request) {
        spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
        span := tracer.StartSpan("format", ext.RPCServerOption(spanCtx))
        defer span.Finish()

        helloTo := r.FormValue("helloTo")
        helloStr := fmt.Sprintf("Hello, I am publish %s!", helloTo)
        span.LogFields(
            otlog.String("event", "string-publish"),
            otlog.String("value", helloStr),
        )
        w.Write([]byte(helloStr))
    })

    log.Fatal(http.ListenAndServe(":8088", nil))
}

publish/main.go

 

Logo

开源、云原生的融合云平台

更多推荐