本文已参与「新人创作礼」活动,一起开启掘金创作之路

分布式链路追踪,也称为分布式请求追踪,是一种用于分析和监视应用程序的方法,特别是那些使用微服务体系结构构建的应用程序; 分布式追踪有助于查明故障发生的位置以及导致性能低下的原因,开发人员可以使用分布式跟踪来帮助调试和优化他们的代码。

链路追踪系统核心步骤一般有三个:代码埋点,数据存储、查询展示。

在数据采集过程,需要侵入用户代码做埋点,不同系统的API不兼容会导致切换追踪系统需要做很大的改动。为了解决这个问题,诞生了opentracing 规范。

   +-------------+  +---------+  +----------+  +------------+
   | Application |  | Library |  |   OSS    |  |  RPC/IPC   |
   |    Code     |  |  Code   |  | Services |  | Frameworks |
   +-------------+  +---------+  +----------+  +------------+
          |              |             |             |
          |              |             |             |
          v              v             v             v
     +-----------------------------------------------------+
     | · · · · · · · · · · OpenTracing · · · · · · · · · · |
     +-----------------------------------------------------+
       |               |                |               |
       |               |                |               |
       v               v                v               v
 +-----------+  +-------------+  +-------------+  +-----------+
 |  Tracing  |  |   Logging   |  |   Metrics   |  |  Tracing  |
 | System A  |  | Framework B |  | Framework C |  | System D  |
 +-----------+  +-------------+  +-------------+  +-----------+

OpenTracing

opentracing 是一套分布式追踪协议,与平台,语言无关,统一接口,方便开发接入不同的分布式追踪系统。

语义规范 : 描述定义的数据模型 Tracer,Sapn 和 Span Context 等;

语义惯例 : 罗列出 tag 和 logging 操作时,标准的key值;

Trace 和 span

opentracing 中的 Trace(调用链)通过归属此链的 Span 来隐性定义。

Trace 事物在分布式系统中移动时的描述, 一条 Trace 可以认为一个有多个 Span 组成的有向无环图(DAG图)。

Span 是一种命名的、定时的操作,是一个逻辑执行单元,Spans接受key:value标签以及附加到特定Span实例的细粒度、带时间戳的结构化日志。

Span Contenxt 携带分布式事务的跟踪信息,包括当它通过网络或消息总线将服务传递给服务时。

Trace通过传递 Span Context 来提供足够的信息建立 span 间的关系。

下图是由8个Spans构成的一个Trace

 [Span A]  ←←←(the root span)
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                         (Span G `FollowsFrom` Span F)

使用时间轴来可视化展示Trace

––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]

Inject 和 Extract 操作

在单体程序中, 父子Span通过Span Context关联, 而Span Context是在内存中的, 显而易见这样的方法在跨应用的场景下是行不通的。

跨应用跨进程通讯使用的方式通常是"序列化",在jaeger-client-python库中也是通过类似的操作去传递信息, 它们叫:Tracer.inject() 与 Tracer.extract()。

当客户端发起http通信时候,当前进程调用Tracer.inject(...)注入当前活动的Span Context以及其他相关参数,通常客户端可以将该Span Context以 http 的 headers 参数(trace_id)的方式标识传递, 服务进程调用Tracer.extract(...),从传入的请求的headers中抽取从上面注入的Span Context和参数还原上下文。

Jaeger

Jaeger (ˈyā-gər) 是Uber开发的一套分布式追踪系统,实现了opentracing的规范,对Tracer与Span等接口写了自己的实现,是CNCF的开源项目。

  • Client - 应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 jaeger-agent。
  • Agent - 是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。 它被设计成一个基础组件,推荐部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节。
  • Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。 Collector 被设计成无状态的组件,因此您可以同时运行任意数量的 jaeger-collector。
  • Data Store - 后端存储被设计成一个可插拔的组件,支持将数据写入 cassandra、elastic search。
  • Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示。 Query 是无状态的,您可以启动多个实例,把它们部署在 nginx 这样的负载均衡器后面。
  • 官方释放部署的镜像到 dockerhub,所以部署 jaeger 非常方便,如果是本地测试,可以直接用 jaeger 提供的 all-in-one 镜像部署(上报的链路数据保存在本地内存,所以只能用于测试)。

    追踪实践 - flask/django

    github demo(jaeger in flask/django)

    jaeger api链路追踪使用流程:

  • 1.应用启动创建全局的Tracer
  • 2.请求进入时提取(或生成)链路信息,提取Parent Span(或生成Root Span)和上下文,打上相关tag
  • 3.请求结束后添加相关tag和日志,并提交Span信息
  • 构建全局Tracer(单例模式)

    根据opentracing的规范,每个服务建议只有一个Tracer。

    jaeger默认使用header的uber_trace_id传递链路标识,此处默认使用trace_id标识

    # coding: utf-8
    import six
    from jaeger_client import Config
    # Name of the HTTP header used to encode trace ID
    DEFAULT_TRACE_ID_HEADER = 'trace_id' if six.PY3 else b'trace_id'
    def init_tracer(service_name: str, config: dict):
        initialize the global tracer
        :param service_name:
        :param config:
        :return:
        assert isinstance(config, dict)
        # default use `trace_id` replace jaeger `uber-trace-id`
        config['trace_id_header'] = config.get('trace_id_header',
                                               DEFAULT_TRACE_ID_HEADER)
        config = Config(config=config, service_name=service_name, validate=True)
        # this call also sets opentracing.tracer
        return config.initialize_tracer()
    

    请求进入时提取(或生成)链路信息,提取Parent Span(或生成Root Span)和上下文,打上相关tag

    def before_request_trace(tracer, request, view_func):
        Helper function to avoid rewriting for middleware and decorator.
        Returns a new span from the request with logged attributes and
        correct operation name from the view_func.
        # strip headers for trace info
        headers = format_request_headers(request.META)
        # start new span from trace info
        operation_name = view_func.__name__
            span_ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, headers)
            scope = tracer.start_active_span(operation_name, child_of=span_ctx)
        except (opentracing.InvalidCarrierException,
                opentracing.SpanContextCorruptedException):
            scope = tracer.start_active_span(operation_name)
        span = scope.span
        span.set_tag(tags.COMPONENT, 'Django')
        span.set_tag(tags.TRACE_ID, format_hex_trace_id(span.trace_id))
        span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER)
        span.set_tag(tags.HTTP_METHOD, request.method)
        span.set_tag(tags.HTTP_URL, request.get_full_path())
        request_id = headers.get(tags.REQUEST_ID)
        if request_id:
            span.set_tag(tags.REQUEST_ID, request_id)
        request.scope = scope
        return scope
    

    请求结束后添加相关tag和日志,并提交Span信息

    def after_request_trace(request, response=None, error=None):
        scope = getattr(request, 'scope', None)
        if scope is None:
            return
        if response is not None:
            scope.span.set_tag(tags.HTTP_STATUS_CODE, response.status_code)
        if error is not None:
            scope.span.set_tag(tags.ERROR, True)
            scope.span.log_kv({
                'event': tags.ERROR,
                'error.kind': type(error),
                'error.object': error,
                'error.stack': error.__traceback__,
                'request.headers': format_request_headers(request.META),
                'request.args': request.GET,
                'request.data': request.POST
        scope.close()
    

    链路追踪装饰器

    def trace(tracer):
        Function decorator that traces functions such as Views
        def decorator(view_func):
            def wrapper(request, *args, **kwargs):
                before_request_trace(tracer, request, view_func)
                try:
                    response = view_func(request, *args, **kwargs)
                except Exception as e:
                    after_request_trace(request, error=e)
                    raise e
                else:
                    after_request_trace(request, response)
                return response
            wrapper.__name__ = view_func.__name__
            return wrapper
        return decorator
    

    django链路追踪中间件

    class OpenTracingMiddleware(MiddlewareMixin):
        def __init__(self, get_response=None):
             __init__() is only called once, no arguments, when the Web server
            responds to the first request
            self._init_tracer()
            self.get_response = get_response
        def _init_tracer(self):
            get global tracer callable function from Django settings.
            :return:
            assert settings.SERVICE_NAME
            assert settings.OPENTRACING_TRACER_CONFIG
            self.tracer = init_tracer(settings.SERVICE_NAME,
                                      settings.OPENTRACING_TRACER_CONFIG)
        def process_view(self, request, view_func, view_args, view_kwargs):
            before_request_trace(self.tracer, request, view_func)
        def process_exception(self, request, exception):
            after_request_trace(request, error=exception)
        def process_response(self, request, response):
            after_request_trace(request, response=response)
            return response
    

    追踪ERROR级别日志

    # coding: utf-8
    import datetime
    import logging
    from logging import Handler
    import opentracing
    from tracing import tags
    class ErrorTraceHandler(Handler):
        Custom ErrorTraceHandlerimplementation to forward python logger records to Jaeger / OpenTracing
        def __init__(self, level=logging.ERROR):
            Initialize the handler.
            super().__init__(level)
        def emit(self, record):
            try:
                msg = self.format(record)
                operation_name = 'logger[{}]'.format(record.name)
                parent_span = opentracing.tracer.active_span
                if not parent_span:
                    return
                with opentracing.tracer.start_span(operation_name, child_of=parent_span) as logger_span:
                    logger_span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_LOG)
                    logger_span.set_tag(tags.LOGGER, record.name)
                    logger_span.log_kv({
                        'event': tags.LOG_ERROR,
                        'message': msg,
                        'log.stack_info': record.stack_info,
                        'log.asctime': getattr(record, 'asctime', datetime.datetime.now()),
                        'log.created': record.created,
                        'log.filename': record.filename,
                        'log.funcName': record.funcName,
                        'log.levelname': record.levelname,
                        'log.lineno': record.lineno,
                        'log.module': record.module,
                        'log.msecs': record.msecs,
                        'log.name': record.name,
                        'log.pathname': record.pathname,
                        'log.process': record.process,
                        'log.thread': record.thread
            except Exception as e:
                self.handleError(record)
    

    通过 http://localhost:16686 可以在浏览器查看 Jaeger UI

    <trace-without-root-span>

    这个错误原因是: B span 是归属于 A span的, 但Jaeger服务器只收集到了B span, 但没有收集到父级A span, 这时候B span就是一个 without-root-span.

    可能原因有下:

  • A 忘记调用scope.close()
  • A 在程序退出时没有调用close(), 这会导致缓冲区的spans没有被push到服务器
  • 等待一段时间, 缓冲区Span会经过一段时间才会被Push到服务器
  • 发送的Spans个数大于了QueueSize, 多余QueueSize的Spans可能会被丢弃,
  • 有时候Jaeger上有数据, 有时候没有