Jaeger in Django

栏目: Python · 发布时间: 5年前

内容简介:微服务的流行程度不需要我们多说,随着业务的扩张和规模的扩大,单体架构的支撑能力越来越有限。所以服务拆分成了必须的选择,而随着服务的增多,以前单体应用里的函数调用都变成了服务之间的请求与调用。随之而来的就是运维和问题定位难度也会变的很大。所以,我们就需要一个工具来帮助我们排查系统性能瓶颈和定位问题, 称他为Tracing 在90年代就已经出现了,真正的老大是Google的 Dapper.随后出现了不少比较不错的 tracing 软件。比如StackDriver Trace (Google),Zipkin(t

微服务的流行程度不需要我们多说,随着业务的扩张和规模的扩大,单体架构的支撑能力越来越有限。所以服务拆分成了必须的选择,而随着服务的增多,以前单体应用里的函数调用都变成了服务之间的请求与调用。随之而来的就是运维和问题定位难度也会变的很大。所以,我们就需要一个 工具 来帮助我们排查系统性能瓶颈和定位问题, 称他为 Tracing 吧,tracing能记录每次调用的过程和耗时。

Tracing简介

Tracing 在90年代就已经出现了,真正的老大是Google的 Dapper.随后出现了不少比较不错的 tracing 软件。比如StackDriver Trace (Google),Zipkin(twitter),鹰眼(taobao) 等等。

一般 tracing 系统核心组成都有:代码打点;数据发送;数据存储;数据查询展示。

在数据采集过程中,需要在代码中打点,并且不同系统的 API 并不兼容,这就导致了如果希望切换追踪系统,往往会带来较大改动成本。

Opentracing

为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范。OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。

Jaeger in Django
  • opentracing 优点
    • opentracing 进入了CNCF,为分布式追踪,提供统一的概念和数据标准。
    • opentracing 通过提供平台无关、厂商无关的 API,使得开发人员能够方便的添加(或更换)追踪系统的实现。
  • opentracing 数据定义

    两个核心组成 tracespan

    trace 是一次调用的统称(一条调用链),经过的各个服务生成一个 span, 多个 span 组成一个 trace。 span 与 span形成链式关系。

    下图展示了两者的关系

    Jaeger in Django
  • span组成部分

    • An operation name,操作名称
    • A start timestamp,起始时间
    • A finish timestamp,结束时间
    • Span Tag,一组键值对构成的Span标签集合。键值对中,键必须为string,值可以是字符串,布尔,或者数字类型。
    • Span Log,一组span的日志集合。 每次log操作包含一个键值对,以及一个时间戳。 键值对中,键必须为string,值可以是任意类型。 但是需要注意,不是所有的支持OpenTracing的Tracer,都需要支持所有的值类型。
    • SpanContext,Span上下文对象 (下面会详细说明)
    • References(Span间关系),相关的零个或者多个Span(Span间通过SpanContext建立这种关系)
      每一个SpanContext包含以下状态:
  • 任何一个OpenTracing的实现,都需要将当前调用链的状态(例如:trace和span的id),依赖一个独特的Span去跨进程边界传输
  • Baggage Items,Trace的随行数据,是一个键值对集合,它存在于trace中,也需要跨进程边界传输
    关于 OpenTracing 更多语义,请参考 OpenTracing语义标准

Jaeger架构

在 OpenTracing的实现中, ZipkinJaeger 是比较留下的方案。

在 Jaeger 和 Zipkin的对比中,我认为Jaeger的优势在:

  • 更加cloud native(docker环境搭建更加方便,对kubernetes支持的更好)
  • 支持的客户端更多,并且我觉得代码(python客户端)易读和清晰
  • 组成架构更加科学(我喜欢)

Jaeger 主要由以下几部分组成。

  • Jaeger Client - 为不同语言实现了符合 OpenTracing 标准的 SDK。应用程序通过 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 这样的负载均衡器后面。
    下图是 Jaeger官方文档的架构图
    Jaeger in Django

Jaeger搭建

  • 本地测试

    我们使用官方的 all-in-one image就可以运行一个完整的链路追踪系统。这种方式数据存在内存中,仅供我们用来本地开发和测试。

    运行方式

    docker run -d --name jaeger \
      -e COLLECTOR_ZIPKIN_HTTP_PORT=9411\
      -p5775:5775/udp \
      -p6831:6831/udp \
      -p6832:6832/udp \
      -p5778:5778\
      -p16686:16686\
      -p14268:14268\
      -p9411:9411\
      jaegertracing/all-in-one:latest
    

访问 http://localhost:16686就能看到 jaeger的数据查询页

  • 正式环境搭建

    Jaeger目前支持的后代存储有 Cassandra 和 Elasticsearch, 因为我们已经有搭建好的 ES, 所以自然存储选择使用 ES.

    • agent

      运行方式

      version: "3"
      	
      services:
      jaeger-agent:
          image: jaegertracing/jaeger-agent
          hostname: jaeger-agent
          command: ["--collector.host-port=collector-host:14267"]
          ports:
          - "5775:5775/udp"
          - "6831:6831/udp"
          - "6832:6832/udp"
          - "5778:5778"
          networks:
          -default
          restart: on-failure
          environment:
          - SPAN_STORAGE_TYPE=elasticsearch
      
  • collector 和 query

    可以搭建在同一个实例上,运行方式

    version: "3"
    
    services:
      jaeger-collector:
          image: jaegertracing/jaeger-collector
          ports:
          - "14269:14269"
          - "14268:14268"
          - "14267:14267"
          - "9411:9411"
          networks:
          -default
          restart: on-failure
          environment:
          - SPAN_STORAGE_TYPE=elasticsearch
          command: [
          "--es.server-urls=http:es-host:9200",
          "--log-level=debug"
          ]
          #depends_on:
          #  - elasticsearch
    
      jaeger-query:
          image: jaegertracing/jaeger-query
          environment:
          - SPAN_STORAGE_TYPE=elasticsearch
          - no_proxy=localhost
          ports:
          - "16686:16686"
          - "16687:16687"
          networks:
          -default
          restart: on-failure
          command: [
          "--es.server-urls=http://es-host:9200",
          "--span-storage.type=elasticsearch",
          "--log-level=debug",
          #"--query.static-files=/go/jaeger-ui/"
          ]
          depends_on:
          - jaeger-collector
    
    
      networks:
      elastic-jaeger:
          driver: bridge
    

    数据简单展示图例

    Jaeger in Django

Django接入

我们开发了自己的 jaeger-python 包(huipy),可以非常简单的在 Django 项目中使用。

接入方式

  • 在中间件中引入

    MIDDLEWARE = [
        'huipy.tracer.middleware.TraceMiddleware',
        # 其他中间件
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'corsheaders.middleware.CorsMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]
    settings.SERVICE_NAME = 'atlas'
    # 其他配置
    ...
    
  • 在发送请求时 引入

    from huipy.tracer.httpclient import HttpClient
    HttpClient(url='http://httpbin.org/get').get()
    

线上部署问题

我们使用 uWSGI 作为 Django app 的容器, 默认的启动模式是 preforking

在 uWSGI启动的时候,首先主进程会初始化并且load app, 然后会 fork 出指定数目的子进程。

Jaeger in Django

使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。

这里提一下linux fork 使用的机制是 copy-on-write (inux系统为了提高系统性能和资源利用率,for出一个新进程时,系统并没有真正复制一个副本。如果多个进程要读取它们自己的那部分资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。如果一个进程要修改自己的那份资源的“副本”,那么就会复制那份资源)

在绝大多数场景下这种方式不会有问题, 但是当主进程本身是多线程的时候可能就会造成问题。

我们的 tracer初始化后会启动一个后台线程向agent 发送udp数据包,而这个过程在主进程load app的时候就完成了。fork子进程的时候这个后台线程当然是不会被fork的, 所以当子进程真正处理请求时,没有后台线程来发送数据。早造成的后果就是我们始终看不到我们请求的trace.

我们可以通过查看特定进程的系统调用来查看到信息:

首先是 preforking 模式,我们查看某一个 uWSGI进程的调用情况

# mac上使用 dtruss, linux使用 strace
sudo dtruss -p 1310
# 输出
SYSCALL(args) 		 = return

lazy-apps模式

sudo dtruss -p 1509
# 输出
SYSCALL(args) 		 = return
gettimeofday(0x7000050F58E8, 0x0, 0x0)		 = 0 0
psynch_cvwait(0x105FABF80, 0xC2901000C2A00, 0x5B700)		 = -1 Err#316
gettimeofday(0x7000050F58E8, 0x0, 0x0)		 = 0 0
psynch_cvwait(0x105FABF80, 0xC2A01000C2B00, 0x5B700)		 = -1 Err#316
gettimeofday(0x7000050F58E8, 0x0, 0x0)		 = 0 0
psynch_cvwait(0x105FABF80, 0xC2B01000C2C00, 0x5B700)		 = -1 Err#316
gettimeofday(0x7000050F58E8, 0x0, 0x0)		 = 0 0
psynch_cvwait(0x105FABF80, 0xC2C01000C2D00, 0x5B700)		 = -1 Err#316
gettimeofday(0x7000050F58E8, 0x0, 0x0)		 = 0 0

很明显在 lazy-apps 模式下一直有线程在监听事件,而前者没有这样的线程

  • 解决方案
    • lazy-apps

      uWSGI可以使用 lazy-apps模式启动,在主进程fork子进程后,每个子进程再初始化和load app。 这样可以保证每个进程独立启动,保证了更好的的隔离性。在我们的场景中这样每个子进程会启动自己的后台线程。

      Jaeger in Django

      这个方案的缺点是:

      • 启动时间会稍微变长,但是有copy-on-write其实影响不大
      • 占用的内存的会变多
  • 延迟初始化 tracer

    重构我们的实现,在 middleware执行到 process_request 的时候再进行全局的初始化

    def process_request(self, request):
         from huipy.tracer.initial_tracer import initialize_global_tracer
         self._tracer = initialize_global_tracer()
         ...
    

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

测试驱动开发的艺术

测试驱动开发的艺术

Lasse Koskela / 李贝 / 人民邮电出版社 / 20101023 / 59.00元

在传统的软件开发中,开发人员对于代码是否正确心中无底,一切依赖于后期的测试环节。极限编程反其道而行之,主张采用测试驱动开发(TDD)的方法,即通过测试定义所要开发的功能的接口,然后实现功能的开发过程。TDD通过不断地测试推动代码的开发,既简化了代码,又保证了软件质量。 本书采用“手把手”的教学方式,通过大量实例来解释TDD,还专门用几章的篇幅来讲解如何为难于测试的技术编写单元测试。全书内容循......一起来看看 《测试驱动开发的艺术》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具