Observability之深度采样Sampling场景和落地案例 下 (observe)

Observability之深度采样Sampling场景和落地案例 下 (observe)

案例分享和深度思考

整理过去自己项目和查阅了圈内一些朋友分享案例。

一方面,通过案例给大家一些技术实现和选型的参考。

另一方面,做可观测系统采样设计,从产品、业务、技术不同的维度去做一些思考。

希望对大家有帮助。

采样规则策略:要不要做全量采样?

业务系统接入全链路监控

伴鱼案例

货拉拉案例

从伴鱼和货拉拉的落地实践看出:有一定数据量的链路系统,全量采样对于业务系统并非有价值,甚至说完全没必要。从业务角度,少量采样是一个很合理节省资源成本方案,这在生产环境是经过验证的。

阿里鹰眼

鹰眼是基于日志的分布式调用跟踪系统,其理念来自于 Google Dapper 论文,其关键核心在于调用链,为每个请求生成全局唯一的 ID(Traceld)目前支撑阿里集团泛电商、高德、优酷等业务,技术层面覆盖前端网关接入层、远端服务调用框架(RPC)、消息队列、数据库、分布式缓存、自定义组件(如支付、搜索 SDK、本地方法埋点等)

鹰眼在面对海量数据,采样比较简单: 百分比取样

90%链路数据采集意义不大

思考「没意思」的 trace 不采,「有意思」的 trace 全采

生产环境绝大部分链路数据平时需要采集么?往往我们的生产环境就正如伴鱼情况类似:

1、监控系统采集了成千上万的链路数据,花了大量服务器资源存储数据,同时还不得不面对高并发带来的性能问题,投入不菲的研发人力做性能调优。

2、监控系统真正研发看的时间很低频,可能在请求量大的活动,或者重大发布时,研发团队会

只有在真正系统异常时候会关注链路数据,而且对每一个链路节点的信息都如视珍宝,很有可能从一些细节定位到故障和原因。

所以,生产环境是 稳定性远远大于故障时间 ,我们很多公司甚至要求 SLO 达到 99% 可用性。从另外一个维度反映出:平时,系统稳定态,95%以上链路数据是正常的,所以它们并不需要那么关注。当然,并不是说你可以完全无视这些链路数据,它的价值体现在你怎么用它,比如下面一些场景:

只不过,95%链路数据价值已经很小了。如果你的场景不涉及 1、2,其实完全可以考虑节约成本。甚至在异常预测场景,也可以很小部分的全采样,只要达到你想要的数据样品范围足够。

哪些是应该全量采集的链路

关注的调用链全采样:研发在分析、排障过程中想查询的任何调用链都是重要调用链。比如伴鱼提到日常排障经验,总结以下三种优先级高场景:

A、在调用链上打印过 ERROR 级别日志

B、在调用链上出现过大于 200ms 的数据库查询

C、整个调用链请求耗时超过 1s

关心的调用链,从日志级别、响应时间、核心组件的性能指标(这里举例数据库)几个维度入手

尾部采样的好处

调用链追踪系统支持了稳态分析,而业务研发亟需的是异常检测。要同时支持这两种场景,采用尾部连贯采样 (tail-based coherent sampling)。相对于头部连贯采样在第一个 span 处就做出是否采样的决定,尾部连贯采样可以让我们在获取完整的 trace 信息后再做出判断

我们再看看货拉拉对「有意思」的 trace 策略 :错、慢、核心服务采样

可以看出,货拉拉采样维度和伴鱼场景类似,相同的响应时间,核心组件的性能指标,还加了高级扩展: 精细化采样 ,通过 APPID 来获取想要服务的全链路采样。

常用采样策略

根据 TraceId 中的顺序数进行采样,提供了多种采样策略搭配:

链路完整采样

链路完整性:

这里举个例子,如图现在有一条远程调用,经过 ABC 和分支 AD。这里有个前提就是 ABCD 它是 4 个不同的服务,独立异步上报 Trace 数据没有严格的时间顺序。在 B 调用 C 出现异常时,我们能轻松识别到并将 B 和 C 的 Trace 数据段采样到,只保留 B 和 C 的这种情况,称为部分采样。但是在实际的一个排障过程中,我们还需要 A 和 D 这条链路数据作为辅助信息来支持排障,所以最好的方式是把 ABCD 都采样到,作为一个完整的异常链路保存起来,这称为完整采样。

采样是如何保证链路的完整性?

我们的目标是完整采样,如何实现完整采样也是业界的一个难点,当前行业有一些解决方案,比如阿里鹰眼和字节的方案

阿里鹰眼方案

阿里鹰眼采用一直纯内存的解决方案:

例如现在有个链路经过 ABCD 和分支 AE,B 调用 C 出现异常时,他会做一个染色标记,那么 C 到 D 自然也携带了染色标记,理论上 BCD 会被采样的保存起来,但是 A 和 E 是前置的节点也没有异常也没被染色,该怎么办呢?他引入一个采样决策点的角色,假设 B 出了异常,采样决策点感知到,最后在内存里查是否存在像 A 和 E 这种异常链路的前置节点,然后将它保存起来。

这里需要注意这种场景对查询的 QPS 要求非常的高,那如果不是存在内存而是类似 HBase 这种服务里的话是很难满足这种高 QPS 的需求。所以他选择将一小时或者半小时内的 Trace 数据放内存中,来满足采样决策点快速查询的要求。

但基于内存存储也存在弊端,因为内存资源是比较昂贵的。我们做个简单的计算,如果想保存 1 小时以内的 Trace 数据,单条 Trace 2K 大小,要支撑百万 TPS,大约需要 6T 内存,成本比较高

字节跳动方案

提出 PostTrace 后置采样方案。

当一个 Trace 一开始未命中采样,但在执行过程中发生了一些令人感兴趣的事(例如出错或时延毛刺)时,在 Trace 中间状态发起采样。PostTrace 的缺点只能采集到 PostTrace 时刻尚未结束的 Span,因此数据完整性相较前置采样有一定损失。

发生错误的服务将采样决定强制进行翻转,如果这条链路没有进行采样的话。但这样的话会丢失采样决策改变之前的所有链路以及其他分支链路的数据。

我们结合一个示例来更好的理解什么是 PostTrace。左图是一个请求,按照阿拉伯数字标识的顺序在微服务间发生了调用,本来这条 trace 没有采样,但是在阶段 5 时发生了异常,触发了 posttrace,这个 posttrace 信息可以从 5 回传到 4,并传播给后续发生的 6 和 7,最后再回传到 1,最终可以采集到 1,4,5,6,7 这几个环节的数据,但是之前已经结束了的 2、3 环节则采集不到。右图是我们线上的一个实际的 posttrace 展示效果,错误层层向上传播最终采集到的链路的样子。PostTrace 对于错误链传播分析、强弱依赖分析等场景有很好的应用

染色采样:对特定的请求添加染色标记,SDK 检测到染色标对该请求进行强制采样

这些采样策略可以同时组合使用。采样不影响 Metrics 和 Log。Metrics 是全量数据的聚合计算结果,不受采样影响。业务日志也是全量采集,不受采样影响

货拉拉方案

基于 Kafka 延迟消费+布隆过滤器实现:

实时消费队列:根据采样规则写入 Bloom 过滤器,热数据全量写入热存储;

延迟消费队列:根据 Bloom 过滤器实现条件过滤逻辑,冷数据写入冷存储。

基于 Kafka 延迟消费+Bloom Filter 来实现完整采样。

比如说我们 Kafka 有两个消费组,一个是实时消费,一个是延迟消费,实时消费每条 Trace 数据时会判断下是否满足我们的采用规则,如果满足就将 TraceID 放在 Bloom Filter 里,另外一方面延时消费组在半小时(可配置)开始消费,从第一条 Trace 数据开始消费,针对每条 Trace 数据判断 TraceID 是否在 Bloom Filter 中,如果命中了,就认为这条 Trace 应该被保留的,从而能做到整个 Trace 链路的完整采样保存

除此之外,里面其实还有一些细节,比如说 Bloom 不可能无限大,所以我们对其按分钟进行划分出多个小的 Bloom,又比如我们其实采用的是一个 Redis 的 Bloom,但 Redis Bloom 如果想达到百万 QPS 预计需要 10~20 个 2C4G 的节点,但是我们实际只用了 5 个 2C4G 的节点就能满足百万的一个吞吐量。这里涉及到专利保护规定,就不展开说了,大家如果感兴趣,有机会可以私底下聊。整体上我们就是具有这套采样方案实现整体成本的降低了 60%

OpenTelemetry 生产环境采样的案例

OT 社区的 tailsampling 方案利用以下几个 processor 和 exporter 实现高伸缩性:

负载均衡网关 loadbalancingexporter:把属于同一个 TraceID 的所有 Trace 和 Log 分发给一组固定的下游 Collector

tailsamplingprocessor:通过预定义的组合策略进行采样

tailsamplingprocessor 支持 4 种策略:

always_sample:全采样

numeric_attribute:某数值属性位于 [min_value, max_value] 之间

string_attribute:某字符串属性位于集合 [value1, value2, …] 之中

rate_limiting:按照 spans 数量限流,由参数 spans_per_second 控制

以「在调用链上如果打印了 ERROR 级别日志」为例,按照规范我们会记录 span.SetTag("error" , true),但 tailsamplingprocessor 并未支持 bool_attribute;此外,未来我们可能会有更复杂的组合条件,这时仅靠 numeric_attribute 和 string_attribute 也无法实现。经过再三分析,我们最终决定利用 Processors 的链式结构,组合多个 Processor 完成采样,流水线如下图所示:

其中 probattr 负责在 trace 级别按概率抽样,anomaly 负责分析每个 trace 是否符合「有意思」的规则,如果命中二者之一,trace 就会被打上标记,即 sampling.priority。最后在 tailsamplingprocessor 上配置一条规则即可,如下所示:

tail_sampling:name: sample_with_high_priority,type: numeric_attribute,numeric_attribute: { key: "sampling.priority", min_value: 1, max_value: 1 }
复制代码

这里 sampling.priority 是整数类型,当前取值只有 0 和 1。按上面的配置,所以 sampling.priority = 1 的 trace 都会被采集。后期可以增加更多的采集优先级,在必要的时候可以多采样 (upsampling) 或降采样 (downsampling)。

OpenTelemetry Agent

sampling OpenTelemetry 客服端的实现

作者介绍

蒋志伟,爱好技术的架构师,先后就职于阿里、Qunar、美团,前 pmcaff.com CTO,目前 OpenTelemetry 中国社区发起人,核心维护者

欢迎大家关注 OpenTelemetry 公众号,这是中国区唯一官方技术公众号

声明:本文来自用户分享和网络收集,仅供学习与参考,测试请备份。