案例分享和深度思考
整理过去自己项目和查阅了圈内一些朋友分享案例。
一方面,通过案例给大家一些技术实现和选型的参考。
另一方面,做可观测系统采样设计,从产品、业务、技术不同的维度去做一些思考。
希望对大家有帮助。
采样规则策略:要不要做全量采样?
业务系统接入全链路监控
伴鱼案例
2020 年,我们不断收到业务研发的反馈:能不能全量采集 trace?这促使我们开始重新思考如何改进调用链追踪系统。我们做了一个简单的容量预估:目前 Jaeger 每天写入 ES 的数据量接近 100GB/天,如果要全量采集 trace 数据,保守假设平均每个 HTTP API 服务的总 QPS 为 100,那么完整存下全量数据需要 10TB/天;乐观假设 100 名服务器研发每人每天查看 1 条 trace,每条 trace 的平均大小为 1KB,则整体信噪比千万分之一。可以看出,这件事情本身的 ROI 很低,考虑到未来业务会持续增长,存储这些数据的价值也会继续降低,因此全量采集的方案被放弃。退一步想:全量采集真的是本质需求吗?实际上并非如此,我们想要的其实是「有意思」的 trace 全采,「没意思」的 trace 不采。
货拉拉案例
2.0 架构虽然能满足高吞吐量,但是也存在存储成本浪费的问题。其实从实践经验看,我们会发现 80~90%的 Trace 数据都是无价值的、无意义的数据,或者说是用户不关心的。那么用户关心哪些数据呢?关心链路中错、慢的请求以及部分核心服务的请求。那么我们是不是可以通过某些方式,把这些有价值的数据给过滤采样出来从而降低整体存储成本?
在这个背景下,我们进行 3.0 的改造,实现了差异化的完成链路采样,保证 1H 以内的数据全量保存,我定义它为热数据,而一小时以外的数据,只保留错、慢、核心服务请求 Trace,定义为冷数据,这样就将整体的存储成本降低了 60%
从伴鱼和货拉拉的落地实践看出:有一定数据量的链路系统,全量采样对于业务系统并非有价值,甚至说完全没必要。从业务角度,少量采样是一个很合理节省资源成本方案,这在生产环境是经过验证的。
阿里鹰眼
鹰眼是基于日志的分布式调用跟踪系统,其理念来自于 Google Dapper 论文,其关键核心在于调用链,为每个请求生成全局唯一的 ID(Traceld)目前支撑阿里集团泛电商、高德、优酷等业务,技术层面覆盖前端网关接入层、远端服务调用框架(RPC)、消息队列、数据库、分布式缓存、自定义组件(如支付、搜索 SDK、本地方法埋点等)
鹰眼在面对海量数据,采样比较简单:百分比取样
90%链路数据采集意义不大
思考「没意思」的 trace 不采,「有意思」的 trace 全采
生产环境绝大部分链路数据平时需要采集么?往往我们的生产环境就正如伴鱼情况类似:
1、监控系统采集了成千上万的链路数据,花了大量服务器资源存储数据,同时还不得不面对高并发带来的性能问题,投入不菲的研发人力做性能调优。
2、监控系统真正研发看的时间很低频,可能在请求量大的活动,或者重大发布时,研发团队会
只有在真正系统异常时候会关注链路数据,而且对每一个链路节点的信息都如视珍宝,很有可能从一些细节定位到故障和原因。
所以,生产环境是稳定性远远大于故障时间,我们很多公司甚至要求 SLO 达到 99% 可用性。从另外一个维度反映出:平时,系统稳定态,95%以上链路数据是正常的,所以它们并不需要那么关注。当然,并不是说你可以完全无视这些链路数据,它的价值体现在你怎么用它,比如下面一些场景:
1、链路数据用做用户行为分析的大数据来源。它往往不需要实时性计算,离线处理。
2、历史链路数据可以作为周期性异常预测的大数据来源。
只不过,95%链路数据价值已经很小了。如果你的场景不涉及 1、2,其实完全可以考虑节约成本。甚至在异常预测场景,也可以很小部分的全采样,只要达到你想要的数据样品范围足够。
哪些是应该全量采集的链路
关注的调用链全采样:研发在分析、排障过程中想查询的任何调用链都是重要调用链。比如伴鱼提到日常排障经验,总结以下三种优先级高场景:
A、在调用链上打印过 ERROR 级别日志
B、在调用链上出现过大于 200ms 的数据库查询
C、整个调用链请求耗时超过 1s
关心的调用链,从日志级别、响应时间、核心组件的性能指标(这里举例数据库)几个维度入手
只要服务打印了 ERROR 级别的日志就会触发报警,研发人员就会收到 im 消息或电话报警,如果能保证触发报警的调用链数据必采,研发人员的排障体验就会有很大的提升;
我们的 DBA 团队认为超过 200ms 的查询请求都被判定为慢查询,如果能保证这些请求的调用链必采,就能大大方便研发排查导致慢查询的请求;
对于在线服务来说,时延过高会令用户体验下降,但具体高到什么程度会引发明显的体验下降我们暂时没有数据支撑,因此先配置为 1s,支持随时修改阈值。当然,以上条件并不绝对,我们完全可以在之后的实践中根据反馈调整、新增规则,如单个请求引起的数据库、缓存查询次数超过某阈值等
尾部采样的好处
调用链追踪系统支持了稳态分析,而业务研发亟需的是异常检测。要同时支持这两种场景,采用尾部连贯采样 (tail-based coherent sampling)。相对于头部连贯采样在第一个 span 处就做出是否采样的决定,尾部连贯采样可以让我们在获取完整的 trace 信息后再做出判断
我们再看看货拉拉对「有意思」的 trace 策略 :错、慢、核心服务采样
提出另外一种方案,就是更深入一点,从 Trace 详情数据入手。我们可以看到这个 Trace 的结构是由多个 Span 组成,Trace 维度包含 APPID、Latency 信息 Span 维度又包含耗时、类型等更细粒度的信息,根据不同的 Span 类型设置不同的阈值,比如远程调用 SOA 耗时大于 500ms 可以认为是慢请求,而如果是 Redis 请求,阈值就需要设置小一些,比如 20ms,通过这种方式可以将慢请求规则更精细化,同样可以通过判断 APPID 是否为核心服务来过滤保留核心服务 Trace 数据
可以看出,货拉拉采样维度和伴鱼场景类似,相同的响应时间,核心组件的性能指标,还加了高级扩展:精细化采样,通过 APPID 来获取想要服务的全链路采样。
常用采样策略
根据 TraceId 中的顺序数进行采样,提供了多种采样策略搭配:
百分比采样:主要用在链路最开始节点
固定阈值采样:全局或租户内统一控制
限速采样:在入口处按固定频率采样若干条调用链;
异常优先采样:调用出错时优先采样;
个性化采样:按用户 ID、入口 IP、应用、调用链入口、业务标识等配置开启采样
链路完整采样
链路完整性:
这里举个例子,如图现在有一条远程调用,经过 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 生产环境采样的案例
https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/4958
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 上配置一条规则即可,如下所示:
这里 sampling.priority 是整数类型,当前取值只有 0 和 1。按上面的配置,所以 sampling.priority = 1 的 trace 都会被采集。后期可以增加更多的采集优先级,在必要的时候可以多采样 (upsampling) 或降采样 (downsampling)。
OpenTelemetry Agent
sampling OpenTelemetry 客服端的实现
Sampling using Javaagent
https://github.com/open-telemetry/opentelemetry-java-instrumentation/discussions/5803
作者介绍
蒋志伟,爱好技术的架构师,先后就职于阿里、Qunar、美团,前 pmcaff.com CTO,目前 OpenTelemetry 中国社区发起人,https://github.com/open-telemetry/docs-cn 核心维护者
欢迎大家关注 OpenTelemetry 公众号,这是中国区唯一官方技术公众号
评论