10分钟完成分布式追踪

    随着并发和异步成为现代软件应用的必然特性,分布式追踪系统成为有效监控的一个必须的组成部分。尽管如此,监控并追踪一个系统的调用情况,至今仍是一个耗时而复杂的任务。随着系统的调用分布式程度(超过10个进程)和并发度越来越高,移动端与web端、客户端到服务端的调用关系越来越复杂,追踪调用关系带来的好处是显而易见的。但是选择和部署一个追踪系统的过程十分复杂。标准将改变这一点,OpenTracing尽力让监控一个分布式调用过程简单化。正如我下面视频演示的那样,你能在10分钟内快速配置一个监控系统。

    本文描述的示例应用程序使用过程截图

    试想一个简单的web网站。当用户访问你的首页时,web服务器发起两个HTTP调用,其中每个调用又访问了数据库。这个过程是否简单直白,我们可以不费什么力气就能发现请求缓慢的原因。如果你考虑到调用延迟,你可以为每个调用分布式唯一的ID,并通过HTTP头进行传递。如果请求耗时过长,你通过使用唯一ID来grep日志文件,发现问题出在哪里。现在,想想一下,你的web网站变得流行起来,你开始使用分布式架构,你的应用需要跨越多个机器,多个服务来工作。随着机器和服务数量的增长,日志文件能明确解决问题的机会越来越少。确定问题发生的原因将越来越困难。这时,你发现投入调用流程追踪能力是非常有价值的。

    正如我提到的,OpenTracing因为standardizes instrumentation, 监控标准化,会使得追踪过程变得容易。它意味着,你可以先进行追踪,再决定最终的实现方案。

    以为例,你可以根据如下的步骤,从编译web项目到查看追踪信息。或者,你可以直接使用Appdash来完成追踪并查看追踪信息。

    如果你想看到完成的实例,你可以根据下面的步骤,自己构建webapp,使用OpenTracing设置追踪,绑定到一个追踪系统(如AppDash),并最终查看调用情况。

    在开始之前,先写几个简单的调用点:

    1. func indexHandler(w http.ResponseWriter, r *http.Request) {
    2. w.Write([]byte(`<a href="/home"> Click here to start a request </a>`))
    3. }
    4. func homeHandler(w http.ResponseWriter, r *http.Request) {
    5. w.Write([]byte("Request started"))
    6. go func() {
    7. http.Get("http://localhost:8080/async")
    8. }()
    9. http.Get("http://localhost:8080/service")
    10. time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
    11. w.Write([]byte("Request done!"))
    12. }
    13. // Mocks a service endpoint that makes a DB call
    14. func serviceHandler(w http.ResponseWriter, r *http.Request) {
    15. // ...
    16. http.Get("http://localhost:8080/db")
    17. time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
    18. // ...
    19. }
    20. // Mocks a DB call
    21. func dbHandler(w http.ResponseWriter, r *http.Request) {
    22. time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
    23. // here would be the actual call to a DB.
    24. }

    将这些调用点组合成一个server

    1. func main() {
    2. port := 8080
    3. addr := fmt.Sprintf(":%d", port)
    4. mux := http.NewServeMux()
    5. mux.HandleFunc("/", indexHandler)
    6. mux.HandleFunc("/home", homeHandler)
    7. mux.HandleFunc("/async", serviceHandler)
    8. mux.HandleFunc("/service", serviceHandler)
    9. mux.HandleFunc("/db", dbHandler)
    10. fmt.Printf("Go to http://localhost:%d/home to start a request!\n", port)
    11. log.Fatal(http.ListenAndServe(addr, mux))
    12. }

    将这些放到main.go文件中,运行go run main.go

    监控应用程序

    现在,你有了一个可以工作的web应用服务器,你可以开始监控它了。你可以开始像下面这样,在入口设置一个span:

    这个span记录homeHandler方法完成所需的时间,这只是可以记录的信息的冰山一角。OpenTracing允许你为每一个span设置和logs。例如:你可以通过homeHandler方法是否正确返回,决定是否记录方法调用的错误信息:

    1. // The ext package provides a set of standardized tags available for use.
    2. import "github.com/opentracing/opentracing-go/ext"
    3. func homeHandler(w http.ResponseWriter, r *http.Request) {
    4. // ...
    5. // We record any errors now.
    6. _, err := http.Get("http://localhost:8080/service")
    7. span.LogEventWithPayload("GET service error", err) // Log the error
    8. }
    9. // ...
    10. }

    然而,这只是其中的一个功能。为了构建真正的端到端追踪,你需要包含调用HTTP请求的客户端的span信息。在我们的示例中,你需要在端到端过程中传递span的上下文信息,使得各端中的span可以合并到一个追踪过程中。这就是API中Inject/Extract的职责。homeHandler方法在第一次被调用时,创建一个根span,后续过程如下:

    1. func homeHandler(w http.ResponseWriter, r *http.Request) {
    2. w.Write([]byte("Request started"))
    3. span := opentracing.StartSpan("/home")
    4. defer span.Finish()
    5. // Since we have to inject our span into the HTTP headers, we create a request
    6. asyncReq, _ := http.NewRequest("GET", "http://localhost:8080/async", nil)
    7. // Inject the span context into the header
    8. err := span.Tracer().Inject(span.Context(),
    9. opentracing.TextMap,
    10. opentracing.HTTPHeaderTextMapCarrier(asyncReq.Header))
    11. if err != nil {
    12. log.Fatalf("Could not inject span context into header: %v", err)
    13. }
    14. go func() {
    15. if _, err := http.DefaultClient.Do(asyncReq); err != nil {
    16. span.SetTag("error", true)
    17. span.LogEvent(fmt.Sprintf("GET /async error: %v", err))
    18. }
    19. }()
    20. // Repeat for the /service call.
    21. // ....
    22. }

    上述代码,在底层实际的执行逻辑是:将关于本地追踪调用的span的元信息,被设置到http的头上,并准备传递出去。下面会展示如何在serviceHandler服务中提取这个元数据信息。

    如上述程序所示,你可以通过http头获取元数据。你可以重复此步骤,为你需要追踪的调用进行设置,很快,你将可以监控整套系统。如何决定哪些调用需要被追踪呢?你可以考虑你的调用的关键路径。

    连接到追踪系统

    OpenTracing最重要的作用就是,当你的系统按照标准被监控之后,增加一个追踪系统将变得非常简单!在这个示例,你可以看到,我使用了一个叫做Appdash的开源追踪系统。你需要通过在main函数中增加一小段代码,来启动Appdash实例。但是,你不需要修改任何你关于监控的代码。在你的main函数中,加入如下内容:

    1. import (
    2. "sourcegraph.com/sourcegraph/appdash"
    3. sourcegraph.com/sourcegraph/appdash/traceapp
    4. appdashot "sourcegraph.com/sourcegraph/appdash/opentracing"
    5. )
    6. func main() {
    7. // ...
    8. store := appdash.NewMemoryStore()
    9. // Listen on any available TCP port locally.
    10. l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
    11. if err != nil {
    12. log.Fatal(err)
    13. }
    14. collectorPort := l.Addr().(*net.TCPAddr).Port
    15. collectorAdd := fmt.Sprintf(":%d", collectorPort)
    16. // Start an Appdash collection server that will listen for spans and
    17. cs := appdash.NewServer(l, appdash.NewLocalCollector(store))
    18. go cs.Start()
    19. // Print the URL at which the web UI will be running.
    20. appdashPort := 8700
    21. appdashURLStr := fmt.Sprintf("http://localhost:%d", appdashPort)
    22. appdashURL, err := url.Parse(appdashURLStr)
    23. if err != nil {
    24. log.Fatalf("Error parsing %s: %s", appdashURLStr, err)
    25. }
    26. fmt.Printf("To see your traces, go to %s/traces\n", appdashURL)
    27. // Start the web UI in a separate goroutine.
    28. tapp, err := traceapp.New(nil, appdashURL)
    29. if err != nil {
    30. log.Fatal(err)
    31. }
    32. tapp.Store = store
    33. tapp.Queryer = store
    34. go func() {
    35. log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appdashPort), tapp))
    36. }()
    37. tracer := appdashot.NewTracer(appdash.NewRemoteCollector(collectorPort))
    38. opentracing.InitGlobalTracer(tracer)
    39. // ...
    40. }

    这样你会增加一个嵌入式的Appdash实例,并对本地程序进行监控。

    image alt text

    1. import zipkin "github.com/openzipkin/zipkin-go-opentracing"
    2. func main() {
    3. // ...
    4. // Replace Appdash tracer code with this
    5. collector, err := zipkin.NewKafkaCollector("ZIPKIN_ADDR")
    6. if err != nil {
    7. log.Fatal(err)
    8. return
    9. }
    10. tracer, err = zipkin.NewTracer(
    11. zipkin.NewRecorder(collector, false, "localhost:8000", "example"),
    12. )
    13. if err != nil {
    14. log.Fatal(err)
    15. }
    16. opentracing.InitGlobalTracer(tracer)
    17. // ...

    到目前为止,你会发现,使用OpenTracing使得监控你的代码更简单。我推荐在启动一个新项目的研发过程中,就加入监控的代码。因为,即使你的应用很小,追踪数据也可以在你的应用演进,引入分布式的时候,提供数据支持。帮助你在这个过程中,构建一个可持续迭代的产品。