Jaeger in Django
注意:Jaeger是德语单词,猎人的意思,读做“耶格”
链路追踪背景
微服务的流行程度不需要我们多说,随着业务的扩张和规模的扩大,单体架构的支撑能力越来越有限。所以服务拆分成了必须的选择,而随着服务的增多,以前单体应用里的函数调用都变成了服务之间的请求与调用。随之而来的就是运维和问题定位难度也会变的很大。所以,我们就需要一个工具来帮助我们排查系统性能瓶颈和定位问题, 称他为Tracing吧,tracing能记录每次调用的过程和耗时。
Tracing简介
Tracing 在90年代就已经出现了,真正的老大是Google的 Dapper.随后出现了不少比较不错的 tracing 软件。比如StackDriver Trace (Google),Zipkin(twitter),鹰眼(taobao) 等等。
一般 tracing 系统核心组成都有:代码打点;数据发送;数据存储;数据查询展示。
在数据采集过程中,需要在代码中打点,并且不同系统的 API 并不兼容,这就导致了如果希望切换追踪系统,往往会带来较大改动成本。
Opentracing
为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范。OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。
-
opentracing 优点
- opentracing 进入了CNCF,为分布式追踪,提供统一的概念和数据标准。
- opentracing 通过提供平台无关、厂商无关的 API,使得开发人员能够方便的添加(或更换)追踪系统的实现。
-
opentracing 数据定义
两个核心组成 trace 和 span trace 是一次调用的统称(一条调用链),经过的各个服务生成一个 span, 多个 span 组成一个 trace。 span 与 span形成链式关系。 下图展示了两者的关系 -
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的实现中,Zipkin和 Jaeger是比较留下的方案。 在 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搭建
-
本地测试 我们使用官方的 all-in-one image就可以运行一个完整的链路追踪系统。这种方式数据存在内存中,仅供我们用来本地开发和测试。
运行方式1 2 3 4 5 6 7 8 9 10
docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 9411: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
可以搭建在同一个实例上,运行方式1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
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
数据简单展示图例
链路依赖关系图
Django接入
我们开发了自己的 jaeger-python 包(huipy),可以非常简单的在 Django 项目中使用。 接入方式
-
在中间件中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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' # 其他配置 ...
-
在发送请求时引入
1 2
from huipy.tracer.httpclient import HttpClient HttpClient(url='http://httpbin.org/get').get()
线上部署问题
我们使用 uWSGI 作为 Django app 的容器, 默认的启动模式是 preforking 在 uWSGI启动的时候,首先主进程会初始化并且load app, 然后会fork出指定数目的子进程。
使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
这里提一下linux fork 使用的机制是 copy-on-write(inux系统为了提高系统性能和资源利用率,for出一个新进程时,系统并没有真正复制一个副本。如果多个进程要读取它们自己的那部分资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。如果一个进程要修改自己的那份资源的“副本”,那么就会复制那份资源)
在绝大多数场景下这种方式不会有问题, 但是当主进程本身是多线程的时候可能就会造成问题。
我们的 tracer初始化后会启动一个后台线程向agent 发送udp数据包,而这个过程在主进程load app的时候就完成了。fork子进程的时候这个后台线程当然是不会被fork的, 所以当子进程真正处理请求时,没有后台线程来发送数据。早造成的后果就是我们始终看不到我们请求的trace.
我们可以通过查看特定进程的系统调用来查看到信息:
首先是 preforking模式,我们查看某一个 uWSGI进程的调用情况
|
|
lazy-apps模式
|
|
很明显在 lazy-apps模式下一直有线程在监听事件,而前者没有这样的线程
-
解决方案
-
lazy-apps uWSGI可以使用 lazy-apps模式启动,在主进程fork子进程后,每个子进程再初始化和load app。 这样可以保证每个进程独立启动,保证了更好的的隔离性。在我们的场景中这样每个子进程会启动自己的后台线程。 这个方案的缺点是:
- 启动时间会稍微变长,但是有copy-on-write其实影响不大
- 占用的内存的会变多
-
延迟初始化tracer 重构我们的实现,在 middleware执行到 process_request的时候再进行全局的初始化
1 2 3 4
def process_request(self, request): from huipy.tracer.initial_tracer import initialize_global_tracer self._tracer = initialize_global_tracer() ...