Dapper and Tracing
Dapper
Dapper 感觉最早是 Google 的追踪系统。实际系统的链路会非常长,从上游发起一个搜索,到下游运行,下游可能又是个分布式请求。如果最终结果是上游出现满请求/问题,需要能够找到对应的选项。本身请求的监控是有用的,通过监控,开发者能够知道每个阶段大概的耗时。但对于偶现的查询,可能无法找到对应的耗时来源,需要具体的请求-阶段耗时。
这里还有个问题,就是定位到的结果也要分锅什么的,慢只是一个结果。可能某个系统多租户没做好,然后来个傻逼用户打一堆 AP 请求过来,把别的地方的请求搞慢了。这些地方也要根据 tracing 之类的来分锅。
竟然要分锅,这里暗含了几件事:
- 这个系统对性能的影响,应该要在预期之内,不能太大
- 可能可以全打,或者精细的按照一些配置来输出
- 可能可以 hack 进一些基本的工具,包括 rpc、runtime 等(作者说不需要手工编码的部分,强调了对应用的透明,但我感觉为了一些细粒度的排查,还是需要的吧)
数据模型
一个调用的链路如下文所示:
基本的思路是,这里需要有一些 context (大概可以用类似 snowflake 之类的方式生成):
- 跟踪标识符(message identifiers)
- 时间戳(timestamped events)。
这里是单个的跟踪关系,dapper 这里非常有意思,考虑了层次的/并发的跟踪关系:
如上,这里其实是有一个(多个) Span
对象的,Root 本身构建了一个 Span 对象,然后调用下层的时候也会起 Span。这里可以有一个很显著的事实:Span 是可以跨机器的。
还有个很奇怪的问题是时间戳的偏移,这里本身靠请求发出的时间 > 请求收到的时间保证。(TODO:不过感觉这个是个特别工程的问题了,可以看看这些系统是具体怎么实现的了)。
Dapper 在使用的时候,可能会被丢到 Thread Local Storage 系统里,或者和某个 Context 对象绑定,具体而言:
- 这个 context 开销尽量小,然后可能可以丢到 TLS 或者某个
context
参数里 - 在异步调用、起别的线程的时候,能够正确处理它们的所有权
这个时候还要注意到一点,这些大公司可能用的是一套 RPC,RPC 可能又会定义一些 runtime 之类的,trace 得很好的和他们做到一起。这里还可以加入一些 annotation,注入一些标记:
这个我感觉就依赖一些奇怪的内存申请了。论文里还提到可以存 key-value, counter,不过我觉得就是一个多模/语义的问题了。
数据收集
这里分为三个阶段:
- log 到本地文件
- Dapper 的 deamon 把它弄到某个 collector 里面
- 最后写到 BigTable 里面,对 模型为 trace_id/span/span_cnt
同时,这里 latency 从 15s - 2min,不算很延迟敏感。这里还有一些重要数据要走 OOB,我个人感觉这里暗示了有一些 tracing 可以是「重要的」。
上图也表明,tracing 大概包括:
- 客户端和对应的 Dapper daemon
- Collectors
- 一个时序的库,用来存储这样的数据
Tracing 的开销和应对
毕竟要在线上系统跑,肯定要考虑性能问题,考虑 Root Span 需要分配 unique id ,论文描述:
根span的创建和销毁需要损耗平均204纳秒的时间,而同样的操作在其他span上需要消耗176纳秒
如果一个span没有被采样的话,那么这个额外的span下创建annotation的成本几乎可以忽略不计,他由在Dapper运行期对ThreadLocal查找操作构成,这平均只消耗9纳秒。如果这个span被计入采样的话,会用一个用字符串进行标注—在图4中有展现—平均需要消耗40纳秒。这些数据都是在2.2GHz的x86服务器上采集的。
在Dapper运行期写入到本地磁盘是最昂贵的操作,但是他们的可见损耗大大减少,因为写入日志文件和操作相对于被跟踪的应用系统来说都是异步的。
Dapper 将自己的进程设置为 Linux 调度最低的优先级,然后在实践中,占用资源一般非常非常低。
同时,采样率对性能会产生影响,内容如下:
Dapper 能够可变的、自适应的、aggressive 的采样。上层还支持了一些二级采样,继续 discard 一些数据。
百度大搜的一些方案
如果你真的查过一些问题,你就会知道,Dapper 很好,但是很多东西你会希望更多的日志、更高的自由度、必要的时候有一些定制的信息,这里可以参考百度大搜的方案:
这个方案相对来说可能和开放方案不是 100% 对齐。里面对数据的可追踪性做了考量,我认为,有意思的地方在于:
- 希望开启根据某些特征采样的时候,一定能开启
- 支持全量的采样
我觉得这两点还是比较重要的
采样的一些讨论
上述 baidu 的系统通过一些资源开销完成了完全的采样. 关于 Dapper 本身的采样,可以看 <Uncertainty in Aggregate Estimates from Sampled Distributed Traces>
,Google 在这篇论文对采样和概率做了量化描述。
Trace: 工业界实践
工业界有着不少的实践,或许最值得瞅一眼的入门是 Opentracing。有的体系过于庞大,我们会一步一步介绍这个系统是怎么构建的。
brpc tracing: 客户端实现的一个简单例子
介绍这玩意一方面是它是 Dapper 描述的客户端的一个很好的实现,另一方面我上班用的是 brpc… 出于私心瞅一眼。
入口可以看到 Collector
和 Span
:
1 | // described in http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/36356.pdf |
我们首先关注一下这个类型的成员.
Channel::CallMethod
里面,描述了作为 client 发送请求的对应 Span 和行为,调用了 Span::CreateClientSpan
。
这里接收一个请求的时候,会初始化它(以 HTTP Server 为例):
1 | Span* span = NULL; |
可以看到,这个地方,如果没有给 trace_id
, log_id
的话,这两个会是 0
,那我们细看一下对应的逻辑:
1 | Span* Span::CreateServerSpan( |
这里有生成 Span 的逻辑,同时注意到,这个逻辑绑定在了 Runtime 的 bthread 上:
1 | void EndAsParent() { |
这里,它把 trace 上下文挂在了 thread local 的地方。那怎么创建子 Span 呢?内容如下:
1 | if (parent) { |
这里挂了一个 Span 的单链表。
那 Annotation 呢?其实很简单:
1 | void Span::Annotate(const char* fmt, va_list args) { |
这里会更新格式,然后丢到 _info
对象里。
我们再看看它是怎么异步析构的:
1 | void Span::destroy() { |
ok,此外,这里可能会造一个 LevelDB 对象,然后有一个 Collector
定期把这些数据收集到 LevelDB.
那采样的逻辑呢?看看这里:
1 | // Check this function first before creating a span. |
这里通过 CollectorSpeedLimit
这个限流器,完成了「低访问不采样,高访问采样」这样的逻辑。
tokio tracing
Tokio Tracing 的文档见:https://tracing.rs/tracing/ . 其实它处理的也是一个 runtime 的问题。因为
The span in which a thread is currently executing is referred to as that thread’s current span.
同时它也处理了一些 field
tag
什么的,原论文里面说自己能处理 kv pair 写到 BigTable 什么的,我觉得这里差不多。
tracing-core
实现了一些基础的语义,包括 Span
元信息 MetaData
,和 event
(记录消息,开 Span 退出 Span 在 tokio-tracing 里面都叫做 event
),大致内容如下:
1 |
|
上面是需要的元数据,包括 Parent
,Current
. 还有一部分重要内容是 Dispatch
,文档可见: https://tracing.rs/tracing_core/struct.dispatch
Every thread in a program using
tracing
has a default collector. When events occur, or spans are created, they are dispatched to the thread’s current collector.
dispatch 内容如下:
1 | /// `Dispatch` trace data to a [`Collect`]. |
这部分有个 thread_local
的 State
内容,表示本线程对应的 tracing 上下文。
下面我们看看 Span 具体是怎么实现的,这部分在 tracing
中:
1 | /// A handle representing a span, with the capability to enter the span if it |
这里用 DefaultGuard
等内容,很 RAII 的做了一下它们生命周期的变化
让我们来看看 Span 的 Enter 和 Exit
1 |
|
看,这里调用了 tracing-core 的内容。OK,那最后一个问题,这里都是 thread-local,那么 tokio 呢?这里可以看 span 的 enter
注释,instrument
和 Span::in_scope
minitrace
https://github.com/tikv/minitrace-rust
这个项目不力图完善,而是力图快,用 tsc
寄存器等方式大幅度优化了 trace 的性能。比较值得再提的是 ministant: https://github.com/tikv/minstant
Jaeger & OpenTelemetry(OpenTracing)
OpenTracing 通过提供了 API 标准,基本上算是做了很不错的事情:https://github.com/opentracing/specification/blob/master/specification.md
Jaeger 提供了下面 的架构(使用 Kakfa or Direct-to-storage):
这里的 DB 可能还需要一些时序/多模之类的需求: 本质上这个和存储日志差不多,半结构化的数据、最近的数据需要高效查询 etc…
参考
[1] https://bigbully.github.io/Dapper-translation/
[2] https://mp.weixin.qq.com/s/BMbdk5RviLG1Ftlo-qRsDQ
[3] https://mp.weixin.qq.com/s/IHVUnyhJr4fhiopMLOJqjA
[4] https://zhuanlan.zhihu.com/p/34318538
[5] https://www.jaegertracing.io/docs/1.31/architecture/
[6] https://tracing.rs/tracing/
[7] Dapper, a Large-Scale Distributed Systems Tracing Infrastructure