10分钟完成分布式追踪
原始发布地址 OpenTracing blog
随着并发和异步成为现代软件应用的必然特性,分布式追踪系统成为有效监控的一个必须的组成部分。尽管如此,监控并追踪一个系统的调用情况,至今仍是一个耗时而复杂的任务。随着系统的调用分布式程度(超过10个进程)和并发度越来越高,移动端与web端、客户端到服务端的调用关系越来越复杂,追踪调用关系带来的好处是显而易见的。但是选择和部署一个追踪系统的过程十分复杂。OpenTracing标准将改变这一点,OpenTracing尽力让监控一个分布式调用过程简单化。正如我下面视频演示的那样,你能在10分钟内快速配置一个监控系统。
本文描述的示例应用程序使用过程截图
试想一个简单的web网站。当用户访问你的首页时,web服务器发起两个HTTP调用,其中每个调用又访问了数据库。这个过程是否简单直白,我们可以不费什么力气就能发现请求缓慢的原因。如果你考虑到调用延迟,你可以为每个调用分布式唯一的ID,并通过HTTP头进行传递。如果请求耗时过长,你通过使用唯一ID来grep日志文件,发现问题出在哪里。现在,想想一下,你的web网站变得流行起来,你开始使用分布式架构,你的应用需要跨越多个机器,多个服务来工作。随着机器和服务数量的增长,日志文件能明确解决问题的机会越来越少。确定问题发生的原因将越来越困难。这时,你发现投入调用流程追踪能力是非常有价值的。
正如我提到的,OpenTracing因为standardizes instrumentation, 监控标准化,会使得追踪过程变得容易。它意味着,你可以先进行追踪,再决定最终的实现方案。
以AppDash为例,你可以根据如下的步骤,从编译web项目到查看追踪信息。或者,你可以直接使用Appdash来完成追踪并查看追踪信息。
docker run --rm -ti -p 8080:8080 -p 8700:8700 bg451/opentracing-example
这将启动一个测试的本地的Appdash实例。点击查看源码
如果你想看到完成的实例,你可以根据下面的步骤,自己构建webapp,使用OpenTracing设置追踪,绑定到一个追踪系统(如AppDash),并最终查看调用情况。
创建一个web工程
在开始之前,先写几个简单的调用点:
// Acts as our index page
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<a href="/home"> Click here to start a request </a>`))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Request started"))
go func() {
http.Get("http://localhost:8080/async")
}()
http.Get("http://localhost:8080/service")
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
w.Write([]byte("Request done!"))
}
// Mocks a service endpoint that makes a DB call
func serviceHandler(w http.ResponseWriter, r *http.Request) {
// ...
http.Get("http://localhost:8080/db")
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
// ...
}
// Mocks a DB call
func dbHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
// here would be the actual call to a DB.
}
将这些调用点组合成一个server
func main() {
port := 8080
addr := fmt.Sprintf(":%d", port)
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/home", homeHandler)
mux.HandleFunc("/async", serviceHandler)
mux.HandleFunc("/service", serviceHandler)
mux.HandleFunc("/db", dbHandler)
fmt.Printf("Go to http://localhost:%d/home to start a request!\n", port)
log.Fatal(http.ListenAndServe(addr, mux))
}
将这些放到main.go
文件中,运行go run main.go
。
监控应用程序
现在,你有了一个可以工作的web应用服务器,你可以开始监控它了。你可以开始像下面这样,在入口设置一个span:
func homeHandler(w http.ResponseWriter, r *http.Request) {
span := opentracing.StartSpan("/home") // Start a span using the global, in this case noop, tracer
defer span.Finish()
// ... the rest of the function
}
这个span记录homeHandler方法完成所需的时间,这只是可以记录的信息的冰山一角。OpenTracing允许你为每一个span设置tags和logs。例如:你可以通过homeHandler方法是否正确返回,决定是否记录方法调用的错误信息:
// The ext package provides a set of standardized tags available for use.
import "github.com/opentracing/opentracing-go/ext"
func homeHandler(w http.ResponseWriter, r *http.Request) {
// ...
// We record any errors now.
_, err := http.Get("http://localhost:8080/service")
if err != nil {
ext.Error.Set(span, true) // Tag the span as errored
span.LogEventWithPayload("GET service error", err) // Log the error
}
// ...
}
你也可以添加其他事件信息,如:发生的重要事件,用户id,浏览器类型。
然而,这只是其中的一个功能。为了构建真正的端到端追踪,你需要包含调用HTTP请求的客户端的span信息。在我们的示例中,你需要在端到端过程中传递span的上下文信息,使得各端中的span可以合并到一个追踪过程中。这就是API中Inject/Extract的职责。homeHandler方法在第一次被调用时,创建一个根span,后续过程如下:
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Request started"))
span := opentracing.StartSpan("/home")
defer span.Finish()
// Since we have to inject our span into the HTTP headers, we create a request
asyncReq, _ := http.NewRequest("GET", "http://localhost:8080/async", nil)
// Inject the span context into the header
err := span.Tracer().Inject(span.Context(),
opentracing.TextMap,
opentracing.HTTPHeaderTextMapCarrier(asyncReq.Header))
if err != nil {
log.Fatalf("Could not inject span context into header: %v", err)
}
go func() {
if _, err := http.DefaultClient.Do(asyncReq); err != nil {
span.SetTag("error", true)
span.LogEvent(fmt.Sprintf("GET /async error: %v", err))
}
}()
// Repeat for the /service call.
// ....
}
上述代码,在底层实际的执行逻辑是:将关于本地追踪调用的span的元信息,被设置到http的头上,并准备传递出去。下面会展示如何在serviceHandler服务中提取这个元数据信息。
func serviceHandler(w http.ResponseWriter, r *http.Request) {
var sp opentracing.Span
opName := r.URL.Path
// Attempt to join a trace by getting trace context from the headers.
wireContext, err := opentracing.GlobalTracer().Extract(
opentracing.TextMap,
opentracing.HTTPHeaderTextMapCarrier(r.Header))
if err != nil {
// If for whatever reason we can't join, go ahead an start a new root span.
sp = opentracing.StartSpan(opName)
} else {
sp = opentracing.StartSpan(opName, opentracing.ChildOf(wireContext))
}
defer sp.Finish()
// ... rest of the function
如上述程序所示,你可以通过http头获取元数据。你可以重复此步骤,为你需要追踪的调用进行设置,很快,你将可以监控整套系统。如何决定哪些调用需要被追踪呢?你可以考虑你的调用的关键路径。
连接到追踪系统
OpenTracing最重要的作用就是,当你的系统按照标准被监控之后,增加一个追踪系统将变得非常简单!在这个示例,你可以看到,我使用了一个叫做Appdash的开源追踪系统。你需要通过在main函数中增加一小段代码,来启动Appdash实例。但是,你不需要修改任何你关于监控的代码。在你的main函数中,加入如下内容:
import (
"sourcegraph.com/sourcegraph/appdash"
“sourcegraph.com/sourcegraph/appdash/traceapp”
appdashot "sourcegraph.com/sourcegraph/appdash/opentracing"
)
func main() {
// ...
store := appdash.NewMemoryStore()
// Listen on any available TCP port locally.
l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
if err != nil {
log.Fatal(err)
}
collectorPort := l.Addr().(*net.TCPAddr).Port
collectorAdd := fmt.Sprintf(":%d", collectorPort)
// Start an Appdash collection server that will listen for spans and
// annotations and add them to the local collector (stored in-memory).
cs := appdash.NewServer(l, appdash.NewLocalCollector(store))
go cs.Start()
// Print the URL at which the web UI will be running.
appdashPort := 8700
appdashURLStr := fmt.Sprintf("http://localhost:%d", appdashPort)
appdashURL, err := url.Parse(appdashURLStr)
if err != nil {
log.Fatalf("Error parsing %s: %s", appdashURLStr, err)
}
fmt.Printf("To see your traces, go to %s/traces\n", appdashURL)
// Start the web UI in a separate goroutine.
tapp, err := traceapp.New(nil, appdashURL)
if err != nil {
log.Fatal(err)
}
tapp.Store = store
tapp.Queryer = store
go func() {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appdashPort), tapp))
}()
tracer := appdashot.NewTracer(appdash.NewRemoteCollector(collectorPort))
opentracing.InitGlobalTracer(tracer)
// ...
}
这样你会增加一个嵌入式的Appdash实例,并对本地程序进行监控。
如果你想换一个监控系统的实现,如果他们都符合OpenTracing,你只需要进行一步操作。你只需要修改你的main函数,其他所有的监控代码,都可以保持不变。例如,如果你决定使用Zipkin,你只需要在main函数中进行如下修改:
import zipkin "github.com/openzipkin/zipkin-go-opentracing"
func main() {
// ...
// Replace Appdash tracer code with this
collector, err := zipkin.NewKafkaCollector("ZIPKIN_ADDR")
if err != nil {
log.Fatal(err)
return
}
tracer, err = zipkin.NewTracer(
zipkin.NewRecorder(collector, false, "localhost:8000", "example"),
)
if err != nil {
log.Fatal(err)
}
opentracing.InitGlobalTracer(tracer)
// ...
}
到目前为止,你会发现,使用OpenTracing使得监控你的代码更简单。我推荐在启动一个新项目的研发过程中,就加入监控的代码。因为,即使你的应用很小,追踪数据也可以在你的应用演进,引入分布式的时候,提供数据支持。帮助你在这个过程中,构建一个可持续迭代的产品。