本文概述 分布式监控 的一些概念,并进行分布式追踪实战。
分布式监控是一个市场庞大的领域,尤其在现在微服务越来越被广泛采用的的现代,监控和追踪系统可以说百花齐放,诞生了很多开源框架和商业公司。
本质上,无论监控还是日志,关注的其实是同一个东西:打点和收集分析。这里的点可以是一段无结构或者有结构的日志,也可以是一个数字,或者是带 id 上下文的结构化数据。既然要 打点,那么就存在以下几个问题:如何打点,如何收集、展示、分析。细分一下可以分成以下几个领域。
这里强烈推荐一个网站 https://openapm.io/landscape 在这个网站上你可以选择一些组件,构建出你自己的监控系统。比如下图就是笔者拖出来的一个可以被真实使用的监控系统。这个监控系统中,节点上使用 collectd + promethues exporter 来收集节点数据,应用端使用 promethues 收集监控数据,监控数据在 promethues server 汇总,并在 influxdb 持久化存储,日志数据使用 elk。 grafana 进行集中展示。
Logging,Metrics 和 Tracing 有各自专注的部分。
Logging - 用于记录离散的事件。例如,应用程序的调试信息或错误信息。它是我们诊断问题的依据。
Metrics - 用于记录可聚合的数据。例如,队列的当前深度可被定义为一个度量值,在元素入队或出队时被更新;HTTP 请求个数可被定义为一个计数器,新请求到来时进行累加。
Tracing - 用于记录请求范围内的信息。例如,一次远程方法调用的执行过程和耗时。它是我们排查系统性能问题的利器。
以上的分类办法 参考自 https://zhuanlan.zhihu.com/p/34318538,下图来自 http://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html
但是这张图除了好看之外,对监控的理解其实很不对, 以 request scoped 或者可否 aggregatable 进行划分并不是一个准确的划分方式,比如说,用日志也能打点绘制监控数据;基于 logid 的日志方式也能追踪调用链。
那么如何正确的理解 监控、追踪和日志 之间的关系呢。考虑 这样的一个原始服务端应用:
从上面的各步可以看出,开发者对于监控的要求是逐渐增加的,日志和监控 trace 直接的要求越来越高,但是从本质上看,三者并无区别,在日志中 写入耗时数据和使用 专用的监控系统,只是在分析和展示步骤有所不同。因此可以看出 其中一切都来自开发者对于应用的监控需求,而工作的原理都是打点。监控和追踪是日志的高级形式,本质并无不同,理解了这一点,你就能不变应万变了。换个说法,监控和追踪是将日志格式化和专用化的一种方式。当日志专注于 metric类数据,使用监控系统更为方便,当日志系统带有 context 属性(在 opentrace 里面就是 span,你也可以理解成 logid),那么使用专用的 trace 系统更为方便。但是本质都是日志,所以当 es 说他同时能支持 日志、监控、追踪的时候,你就不会觉得奇怪了吧。
由于笔者在监控方面已经写过一些文章(未来可能会重新整理),不再赘述,本文重点会介绍一下追踪(trace)以及 opentrace 规范。
经过上文的分类,大家应该理解,trace 其实也是一种特殊的日志,opentrace 则是用来定义这种特殊的日志规范,而这种日志规范,最特殊的地方在于 span 的定义,即单个日志不是孤岛,通过 span 的串联,他是能组成一组调用链的。
一个tracer过程中,各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) tracer与span的时间轴关系 ––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time [Span A···················································] [Span B··············································] [Span D··········································] [Span C········································] [Span E·······] [Span F··] [Span G··] [Span H··]
当然,除了 span 之外,opentrace 还有其他关注这种特殊日志的定义,比如:
opentrace 定义的是一个规范,具体的实现了这个规范的又 Zipkin,Jaeger 等,opentrace 的规范保证,只要使用 opentrace client 的代码,底层实现的切换,内部的代码无需修改。
opentrace client 具体定义了哪些东西呢。和上面讲的概念对应,其实 opentrace 的定义很简洁,主要就 三个 interface,Tracer
, Span
, SpanContext
type Tracer interface { // 用于新建,启动,返回一个新的 Span // 比如: // // var tracer opentracing.Tracer = ... // // // The root-span case: // sp := tracer.StartSpan("GetFeed") // // // The vanilla child span case: // sp := tracer.StartSpan( // "GetFeed", // opentracing.ChildOf(parentSpan.Context())) // // // All the bells and whistles: // sp := tracer.StartSpan( // "GetFeed", // opentracing.ChildOf(parentSpan.Context()), // opentracing.Tag{"user_agent", loggedReq.UserAgent}, // opentracing.StartTime(loggedReq.Timestamp), // ) // StartSpan(operationName string, opts ...StartSpanOption) Span // 注入 Inject 和 Extract 是 Span 传递的关键,Inject 将 Span 信息以某种格式注入载体, // Extract 则是做提取,以最常见的 Http 为例,Inject 将 Span 信息以 Http Header 的方式 // 注入,提取的时候则 从 Http Header 提取,这里只要 Inject 和 Extract 对应就可以,你也可以 // 定义自己的 Http 注入方式 // // 比如: // // carrier := opentracing.HTTPHeadersCarrier(httpReq.Header) // err := tracer.Inject( // span.Context(), // opentracing.HTTPHeaders, // carrier) // Inject(sm SpanContext, format interface{}, carrier interface{}) error // 提取,这里给了一个最常见的例子,理解这个例子:StartSpan 有两种情况:一是过来的请求里面有 Span // 信息了,那么要 Start with clientContext;二是 过来的请求没有 Span,那么 新建一个新的无关的 Span // // 例子: // // // carrier := opentracing.HTTPHeadersCarrier(httpReq.Header) // clientContext, err := tracer.Extract(opentracing.HTTPHeaders, carrier) // // // ... assuming the ultimate goal here is to resume the trace with a // // server-side Span: // var serverSpan opentracing.Span // if err == nil { // span = tracer.StartSpan( // rpcMethodName, ext.RPCServerOption(clientContext)) // } else { // span = tracer.StartSpan(rpcMethodName) // } Extract(format interface{}, carrier interface{}) (SpanContext, error) } // SpanContext represents Span state that must propagate to descendant Spans and across process // boundaries (e.g., a <trace_id, span_id, sampled> tuple). type SpanContext interface { // ForeachBaggageItem grants access to all baggage items stored in the // SpanContext. ForeachBaggageItem(handler func(k, v string) bool) } // Span represents an active, un-finished span in the OpenTracing system. // // Spans are created by the Tracer interface. // 这里注释进行了删减,比较重要的是 SetOperationName/SetTag/LogFields => Finish type Span interface { // Sets the end timestamp and finalizes Span state. Finish() // FinishWithOptions is like Finish() but with explicit control over timestamps and log data. FinishWithOptions(opts FinishOptions) // Context() yields the SpanContext for this Span. Note that the return // value of Context() is still valid after a call to Span.Finish(), as is // a call to Span.Context() after a call to Span.Finish(). Context() SpanContext // Sets or changes the operation name. SetOperationName(operationName string) Span // Adds a tag to the span. SetTag(key string, value interface{}) Span // LogFields is an efficient and type-checked way to record key:value // logging data about a Span, though the programming interface is a little // more verbose than LogKV(). Here's an example: // // span.LogFields( // log.String("event", "soft error"), // log.String("type", "cache timeout"), // log.Int("waited.millis", 1500)) // // Also see Span.FinishWithOptions() and FinishOptions.BulkLogData. LogFields(fields ...log.Field) // LogKV is a concise, readable way to record key:value logging data about // a Span, though unfortunately this also makes it less efficient and less // type-safe than LogFields(). Here's an example: // // span.LogKV( // "event", "soft error", // "type", "cache timeout", // "waited.millis", 1500) // LogKV(alternatingKeyValues ...interface{}) // SetBaggageItem sets a key:value pair on this Span and its SpanContext // that also propagates to descendants of this Span. SetBaggageItem(restrictedKey, value string) Span // Gets the value for a baggage item given its key. Returns the empty string // if the value isn't found in this Span. BaggageItem(restrictedKey string) string // Provides access to the Tracer that created this Span. Tracer() Tracer }
span 如何传递是 opentrace client 中比较重要的内容,毕竟如何 span 不能被跨进程传递,那么和本地日志的区别不是很大了(当然也有这样的使用场景,比如对本进程内的一组函数进行耗时分析)。opentrace client 内置了两种传递、存储方式,分别是 TextMap 和 HTTPHeaders:
理解了 Span 的原理,自己实现类似的传递方式并不困难,比如在 Grpc-go 里面使用 Meta 字段(类似 HttpHeader)传递Span 信息.
一个 OpenTrace 的实现系统通常出来实现了 Opentrace 协议的 客户端之外,还包括
Jagger Client 的 Inject 使用的 Http Header 如下
func (p Propagator) Inject( sc jaeger.SpanContext, abstractCarrier interface{}, ) error { textMapWriter, ok := abstractCarrier.(opentracing.TextMapWriter) if !ok { return opentracing.ErrInvalidCarrier } textMapWriter.Set("x-b3-traceid", sc.TraceID().String()) if sc.ParentID() != 0 { textMapWriter.Set("x-b3-parentspanid", strconv.FormatUint(uint64(sc.ParentID()), 16)) } textMapWriter.Set("x-b3-spanid", strconv.FormatUint(uint64(sc.SpanID()), 16)) if sc.IsSampled() { textMapWriter.Set("x-b3-sampled", "1") } else { textMapWriter.Set("x-b3-sampled", "0") } sc.ForeachBaggageItem(func(k, v string) bool { textMapWriter.Set(p.baggagePrefix+k, v) return true }) return nil }
实战例子改编自 https://github.com/yurishkuro/opentracing-tutorial
这个例子中我们使用 一个 client, 两个 server(publish,formatstring),其中 publish server 收到请求后,同时会异步的发一个 回调消息到 mq,而 client 端则等待这个异步消息并推出。
这个例子中除了最基础的 trace 使用,意在解释当常见的 carrier 不能满足要求,如何通过封装消息的方式来包装 span 信息
type Message struct { Body string Extra map[string]string }
我们的做法为封装 mq 消息为 Message, 其中 Body 是实际 mq消息内容,而 Extra 为 Span 消息。通过定制 mq sdk,我们可以做到 Message 格式对用户不暴露。
本地启动 mq 和 jaegertracing
// jaegertracing docker run --rm -p 6831:6831/udp -p 6832:6832/udp -p 16686:16686 jaegertracing/all-in-one:1.7 --log-level=debug // rabbitmq, 启动后进入容器新建 root 用户,新建一个 test queue docker run --name rabbitmq -p 15672:15672 -p 5672:5672 ccr.ccs.tencentyun.com/wajika/rabbitmq-management:3.7.8
启动 server publish,formatstring后,运行一次 client,打开 http://localhost:16686/ 查看 trace 效果
实战代码在 https://github.com/u2takey/trace-example
原创声明,本文系作者授权云+社区发表,未经许可,不得转载。
如有侵权,请联系 yunjia_community@tencent.com 删除。
我来说两句