转载本文需注明出处:微信公众号EAWorld,违者必究。
现在越来越多的应用迁移到基于微服务的云原生的架构之上,微服务架构很强大,但是同时也带来了很多的挑战,尤其是如何对应用进行调试,如何监控多个服务间的调用关系和状态。如何有效的对微服务架构进行有效的监控成为微服务架构运维成功的关键。用软件架构的语言来说就是要增强微服务架构的可观测性(Observability)。
微服务的监控主要包含一下三个方面:
- 通过收集日志,对系统和各个服务的运行状态进行监控
- 通过收集量度(Metrics),对系统和各个服务的性能进行监控
- 通过分布式追踪,追踪服务请求是如何在各个分布的组件中进行处理的细节
对于是日志和量度的收集和监控,大家会比较熟悉。常见的日志收集架构包含利用Fluentd对系统日志进行收集,然后利用ELK或者Splunk进行日志分析。而对于性能监控,Prometheus是常见的流行的选择。
分布式追踪正在被越来越多的应用所采用。分布式追踪可以通过对微服务调用链的跟踪,构建一个从服务请求开始到各个微服务交互的全部调用过程的视图。用户可以从中了解到诸如应用调用的时延,网络调用(HTTP,RPC)的生命周期,系统的性能瓶颈等等信息。那么分布式追踪是如何实现的呢?
1.分布式追踪的概念
谷歌在2010年4月发表了一篇论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》(http://1t.click/6EB),介绍了分布式追踪的概念。
对于分布式追踪,主要有以下的几个概念:
- 追踪 Trace:就是由分布的微服务协作所支撑的一个事务。一个追踪,包含为该事务提供服务的各个服务请求。
- 跨度 Span:Span是事务中的一个工作流,一个Span包含了时间戳,日志和标签信息。Span之间包含父子关系,或者主从(Followup)关系。
- 跨度上下文 Span Context:跨度上下文是支撑分布式追踪的关键,它可以在调用的服务之间传递,上下文的内容包括诸如:从一个服务传递到另一个服务的时间,追踪的ID,Span的ID还有其它需要从上游服务传递到下游服务的信息。
2.OpenTracing 标准概念
基于谷歌提出的概念OpenTracing(http://1t.click/6tC)定义了一个开放的分布式追踪的标准。
Span是分布式追踪的基本组成单元,表示一个分布式系统中的单独的工作单元。每一个Span可以包含其它Span的引用。多个Span在一起构成了Trace。
OpenTracing的规范定义每一个Span都包含了以下内容:
- 操作名(Operation Name),标志该操作是什么
- 标签 (Tag),标签是一个名值对,用户可以加入任何对追踪有意义的信息
- 日志(Logs),日志也定义为名值对。用于捕获调试信息,或者相关Span的相关信息
- 跨度上下文呢 (SpanContext),SpanContext负责子微服务系统边界传递数据。它主要包含两部分:
- 和实现无关的状态信息,例如Trace ID,Span ID
- 行李项 (Baggage Item)。如果把微服务调用比做从一个城市到另一个城市的飞行, 那么SpanContext就可以看成是飞机运载的内容。Trace ID和Span ID就像是航班号,而行李项就像是运送的行李。每次服务调用,用户都可以决定发送不同的行李。
这里是一个Span的例子:
t=0 operation name: db_query t=x +-----------------------------------------------------+ | · · · · · · · · · · Span · · · · · · · · · · | +-----------------------------------------------------+Tags:- db.instance:"jdbc:mysql://127.0.0.1:3306/customers- db.statement: "SELECT * FROM mytable WHERE foo='bar';"Logs:- message:"Can't connect to mysql server on '127.0.0.1'(10061)"SpanContext:- trace_id:"abc123"- span_id:"xyz789"- Baggage Items: - special_id:"vsid1738"
要实现分布式追踪,如何传递SpanContext是关键。OpenTracing定义了两个方法Inject和Extract用于SpanContext的注入和提取。
Inject 伪代码
span_context = ...outbound_request = ...# We'll use the (builtin) HTTP_HEADERS carrier format. We# start by using an empty map as the carrier prior to the# call to `tracer.inject`.carrier = {}tracer.inject(span_context, opentracing.Format.HTTP_HEADERS, carrier)# `carrier` now contains (opaque) key:value pairs which we pass# along over whatever wire protocol we already use.for key, value in carrier: outbound_request.headers[key] = escape(value)
这里的注入的过程就是把context的所有信息写入到一个叫Carrier的字典中,然后把字典中的所有名值对写入 HTTP Header。
Extract 伪代码
inbound_request = ...# We'll again use the (builtin) HTTP_HEADERS carrier format. Per the# HTTP_HEADERS documentation, we can use a map that has extraneous data# in it and let the OpenTracing implementation look for the subset# of key:value pairs it needs.## As such, we directly use the key:value `inbound_request.headers`# map as the carrier.carrier = inbound_request.headersspan_context = tracer.extract(opentracing.Format.HTTP_HEADERS, carrier)# Continue the trace given span_context. E.g.,span = tracer.start_span("...