OpenTracing
从分布式系统架构普及之后,各个公司+社区出了很多监控调用链的项目,因为标准都不相同,最后 OpenTracing 这种规范一统天下。https://github.com/opentracing 安装opentracing的python包,pip install opentracing, 可以看到源码,里面统一定义了概念和流程,并且有一些接口需要被具体实现才能用。
核心原理是定义了 Span 和 Trace。Span容纳单次请求的信息并上报,Trace可以把多个Span连成一个链路。如果要追踪A服务调用B服务的调用链,要把A服务的信息放在请求B服务的header里
Jaeger
Jaeger【读作 耶格儿】是Uber出的实现了OpenTracing规范的框架。同时同样流行的还有Zipkin。我目前主要只在看Jaeger。
Jaeger是一套含有信息上报,信息展示的框架,一般需要配合存储比如ES。如果想本地起一下试一下全套Jaeger功能,可以使用docker pull jaeger的all-in-one镜像 - https://www.jaegertracing.io/docs/1.18/getting-started/
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 14250:14250
-p 9411:9411
jaegertracing/all-in-one:1.18
核心的端口:6831是UDP上报jaeger-agent端口, 16686是Jaeger UI 和 API 端口
Jaeger的架构
https://www.jaegertracing.io/docs/1.18/architecture/
- Jaeger Client 和用户应用集成在一起,主要功能就是收集网络请求并生成span,trace等符合OpenTracing要求的对象
- Jaeger Client 通过 UDP 把span信息上报给 Jaeger Agent 。 这一步传说也可以跳过,传说 Jaeger Client可以直接上报 Jaeger Collector
- Jaeger Agent 要和用户应用部署在一起。 比如Jaeger-agent可以起在应用所在服务器的localhost:6831,并配置好要上报的Collector的信息
docker run jaegertracing/jaeger-agent /go/bin/agent-linux --reporter.grpc.host-port=xxx
- Jaeger Collector 接收agent上报的信息,并存入Kafka,ES等
- Jaeger UI 可视化调用链
Jaeger-Client Python 支持
Jaeger官方提供了各个语言的支持包 https://github.com/jaegertracing, python的代码集成jaeger使用 pip install jaeger-client。查看源码发现是基于OpenTracing又实现了很多具体的类方法。
使用Jaeger的话,在request header里使用的key名默认是 "uber-trace-id"(也可以通过jaeger config里的trace_id_header修改),值为"trace_id: span_id: parent_id: flags",其中所有值16进制编码后传输。 举例 {'uber-trace-id': '6997ed0a6a74f050:bf49be2de63d86e7:e02975aab05fd358:1'}
阿里云 链路追踪 Tracing Analysis
通过Jaeger上报Python应用数据 文档 https://help.aliyun.com/document_detail/90506.html?
,实际这个文档只写了非常简洁的核心,简洁到关于Span的部分也就算个示意吧 = =
阿里云的 Tracing Analysis 代替了 Jaeger Collector 的部分,有UI展示(代替Jaeger UI)并且提供了存储,省去了用户自己配置ES等存储的成本和部署 Jaeger Collector的成本。
本地起 jaeger-agent 服务,连接阿里云上报:
docker run jaegertracing/jaeger-agent /go/bin/agent-linux --reporter.grpc.host-port=xxx:xxxx --jaeger.tags=Authentication={xxx}
接入信息endpoint可以在阿里云里看到
集成 Django 具体代码实现
在调研了一堆开源中间件之后,发现真的就用官方的就够用了,规范,好用 https://github.com/opentracing-contrib/python-django
不过这个中间件最大的问题,就是默认只能上报所有Django View(including function view)的request。而我们微服务间的调用,比如A服务调用B服务,往往使用了 requests 包 (我们封装了一层sparrowcloud,变request_client)。这种使用 requests 包直接发起的请求,是需要明白Jaeger传递信息调用的方式,自己写代码传递的。而接收方,比如B服务,因为已经是Django View了,中间件里提供了解析 parent span,所以B服务的代码不用多修改。
使用中间件的方式
A B 服务都需要配置 settings.py
import django_opentracing
import opentracing
from jaeger_client import Config
MIDDLEWARE = (
'django_opentracing.OpenTracingMiddleware',
'...'
)
# config这里除了 service_name 要配置,其他的不用配置都是用的默认值,具体有什么默认值可以看源码
config = Config(
config={ # usually read from some yaml config
'sampler': {
'type': 'const',
'param': 1,
},
# 'local_agent': {
# 'reporting_host': os.environ.get('JAEGER_REPORTING_HOST', 'localhost'),
# 'reporting_port': os.environ.get('JAEGER_REPORTING_PORT', '6831'),
# },
'logging': True,
# 'enabled': True,
},
service_name=os.environ.get('JAEGER_SERVICE_NAME', 'test-sparrow-promotion'),
validate=True,
)
# generate a Jaeger tracer
tracer = config.initialize_tracer()
# trace all views
OPENTRACING_TRACE_ALL = True
# attributes need to be traced, e.g. ['path', 'method']
OPENTRACING_TRACED_ATTRIBUTES = ['META']
# will use Jaeger tracer
OPENTRACING_TRACING = django_opentracing.DjangoTracing(tracer)
requests包调用的方式
A 服务调用B的地方需要单独写代码,以某个测试接口为例 views.py
from django.conf import settings
from opentracing.propagation import Format
import django_opentracing
import opentracing
import requests
@api_view(('POST', ))
@permission_classes((AllowAny, ))
def test_jaeger_one_price(request, *args, **kwargs):
'''测试服务间调用'''
subject_id = 1
page = 1
api_path = '/api/sparrow_products_i/subject/subject_product/?subject_id={0}&page={1}&page_size=500'
url = 'http://127.0.0.1:8002' + api_path.format(subject_id,page)
# 拿到当前生效的span
tracer = opentracing.global_tracer()
span = tracer.active_span
# inject span 进 requests header
headers = {}
tracer.inject(span, Format.HTTP_HEADERS, headers)
# inject之后,实际 headers = {'uber-trace-id': '6997ed0a6a74f050:bf49be2de63d86e7:e02975aab05fd358:1'}
res = requests.get(url, headers=headers)
return Response(res.json())
代码生效后,A调用B并上报阿里云,实际效果:
NOTE: span的其他信息,比如tags,不需要传递,所以也不需要特别设置
进一步源码研究
到底在传递什么信息
在源码中加断点,查看A服务调用B服务,Jaeger传递了哪些信息。
可以看到,A服务中原始的span_id和trace_id就是随机数字生成的,然后hex之后冒号分隔,放header里。 调用B服务接口后,B看到来的request header了有span_id和trace_id,就对应到自己的parent_id和trace_id,然后生成自己的span_id,继续传。以此AB共用一个trace_id,而A的span_id就是B的parent_id。之后Jaeger根据这些信息串成调用链。
核心函数
- inject 把span等信息inject到header(carrier)里
在 jaeger_client 包里 codecs.py 中找到 inject 方法,可以看到原理就是把 span中的 trace_id, span_id, parent_id, flags, baggage 放入carrier(header)中
2. extract 从carrier(header)里解析span等信息
在 jaeger_client 包里 codecs.py 找到 extract 方法,可以看到就是把carrier(header)里的信息解析出来并生成 SpanContext
client数据怎么上报agent
在第一次new tracer的时候,tracer = config.initialize_tracer(),会调用 config.py中的new_tracer(),一系列代码后创建了一个LocalAgentSender实例和tornado.ioloop
传输给agent的原理是一直buffer在内存里,直到flush()被调用然后发送给agent(NOTE: LocalAgentSender derives from TBufferedTransport. This will buffer up all written data until flush() is called. Flush gets called at the end of the batch span submission call.)
agent怎么上报collector
client怎么直接上报collector
jaeger python 目前看还不支持通过http方式直接上报collector,参考 https://github.com/jaegertracing/jaeger-client-python/issues/98 相关issue显示要在 jaeger-client==5.0.0 以后才有可能上这个功能(尽管 go和java现在已经支持http了。。。)
Kubernetes 部署
Jaeger Agent
jaeger-agent 可以部署成sidecar的形式,也可以部署成daemonset的形式
https://medium.com/@masroor.hasan/tracing-infrastructure-with-jaeger-on-kubernetes-6800132a677(此文章写的非常清晰)
官方文档 https://www.jaegertracing.io/docs/1.16/operator/#installing-the-agent-as-daemonset
项目(已转移至Jaeger Operator)https://github.com/jaegertracing/jaeger-kubernetes中提到的样例:https://raw.githubusercontent.com/jaegertracing/jaeger-kubernetes/master/jaeger-production-template.yml
我们选择用daemonset的部署方式。编排文件如下
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: jaeger-agent
namespace: default
labels:
app: jaeger
jaeger-infra: agent-daemonset
spec:
revisionHistoryLimit: 10
selector:
matchLabels:
app: jaeger
jaeger-infra: agent-instance
template:
metadata:
labels:
app: jaeger
jaeger-infra: agent-instance
spec:
containers:
- name: agent-instance
image: 'jaegertracing/jaeger-agent:1.16.0'
imagePullPolicy: IfNotPresent
args:
- '--reporter.grpc.host-port=xxx:xxxx'
- '--jaeger.tags=Authentication=xxx'
ports:
- containerPort: 5775
hostPort: 5775
protocol: UDP
- containerPort: 6831
hostPort: 6831
protocol: UDP
- containerPort: 6832
hostPort: 6832
protocol: UDP
- containerPort: 5778
hostPort: 5778
protocol: TCP
resources:
limits:
cpu: 200m
memory: 200M
requests:
cpu: 200m
memory: 200M
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirstWithHostNet
hostNetwork: true
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
updateStrategy:
type: OnDelete
可以看到在阿里云K8S中守护进程集启动成功
Deployments
在需要使用jaeger的应用部署中增加环境变量。其中 JAEGER_AGENT_HOST必填,需要配置成变量引用status.hostIP。JAEGER_AGENT_PORT 可以不配置,默认就是6831。JAEGER_SERVICE_NAME看代码可知,jaeger创建tracer传入config的时候必须有service_name,但是如果settings里已经写了,此处可以不覆盖。
- name: JAEGER_SERVICE_NAME
value: test-sparrow-promotion
- name: JAEGER_AGENT_HOST
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
- name: JAEGER_AGENT_PORT
value: '6831'
集群部署完毕。