作者:高玉龙(元泊)
首先,咱们理解一下挪动端全链路 Trace 的背景:
从挪动端的视角来看,一个 App 产品从概念产生,到最终的成熟稳固,产品研发过程中波及到的研发人员、工程中的代码行数、工程架构规模、产品公布频率、线上业务问题修复工夫等等都会产生比拟大的变动。这些变动,给咱们在排查问题方面带来不小的艰难和挑战,业务问题会往往难以复现和排查定位。比方,在产品初期的时候,工程规模往往比拟小,业务流程也比较简单,线上问题往往能很快定位。而等到工程规模比拟大的时候,业务流程往往波及到的模块会比拟多,这个时候有些线上问题就会比拟难以复现和定位排查。
本文是笔者在 2022 D2 终端技术大会上的分享,心愿能给大家带来一些思考和启发。
端侧问题为什么很难复现和定位?
线上业务问题为什么很难复现和排查定位?通过咱们的剖析,次要是由 4 个起因导致:
- 挪动端 & 服务端日志采集不对立,没有对立的标准规范来束缚数据的采集和解决。
- 端侧往往波及的模块十分多,研发框架也各不相同,代码互相隔离,设施碎片化,网络环境简单,会导致端侧数据采集比拟难。
- 从端视角登程,不同框架、零碎之间的数据在剖析问题时往往获取比拟难,而且数据之间短少上下文关联信息,数据关联剖析不容易。
- 业务链路波及到的业务域往往也会比拟多,从端的视角去复现和排查问题,往往须要对应域的同学参加排查,人肉运维老本比拟高。
这些问题如何来解决? 咱们的思路是四步走:
- 建设统一标准,应用 标准协议 来束缚数据的采集和解决。
- 针对不同的平台和框架,对立数据采集能力。
- 对多零碎、多模块产生的数据进行主动上下文关联剖析和解决。
- 咱们也基于机器学习,在自动化教训剖析方面做了一些摸索。
对立数据采集规范
如何统一标准? 目前行业内也有各种各样的解决方案,但存在的问题也很显著:
- 不同计划之间,协定 / 数据类型不对立;
- 不同计划之间,也比拟难以兼容 / 互通。
规范这里,咱们抉择了 OTel,OTel 是 OpenTelemetry 的简称,次要起因有两点:
- OTel 是由云原生计算基金会(CNCF)主导,它是由 OpenTracing 和 OpenCensus 合并而来,是目前可观测性畛域的准标准协议;
- OTel 对不同语言和数据模型进行了对立,能够同时兼容 OpenTracing 和 OpenCensus,它还提供了一个厂商无关的 Collectors,用于接管、解决和导出可观测数据。
在咱们的解决方案中,所有端的数据采集标准都基于 OTel,数据存储、解决、剖析是基于 SLS 提供的 LogHub 能力进行构建。
端侧数据采集的难点
只对立数据协定还不够,还要解决端侧在数据采集方面存在的一些问题。总的来说,端侧采集以后面临 3 个次要的难点:
- 数据串联难
- 性能保障难
- 不丢数据难
端侧研发过程中波及到的框架、模块往往比拟多,业务也有肯定的复杂性,存在线程、协程多种异步调用 API,在数据采集过程中,如何解决数据之间的主动串联问题?挪动端设施碎片化重大,零碎版本散布比拟散,机型泛滥,如何保障多端统一的采集性能?App 应用场景的不确定性也比拟大,如何确保采集到的数据不会失落?
端侧数据串联的难点
咱们先来剖析一下 端侧数据主动串联所面临的次要问题。
- 在端侧数据采集过程中,不仅会采集业务链路数据,还会采集各种性能 & 稳定性监控数据,可观测数据源比拟多;
- 如果用到其余的研发框架,如 OkHttp、Fresco 等,可能还会采集三方框架的要害数据用于网络申请,图片加载等问题的剖析和定位。对于业务研发同学来说,咱们往往不会过多的关注这类三方框架技术能力,波及到这类框架问题的排查时,过程往往比拟艰难;
- 除此之外,端侧简直齐全异步调用,而且异步调用 API 比拟多,如线程、协程等,链路买通也存在肯定的挑战。
这里会有几个共性问题:
- 三方框架的数据如何采集?如何串联?
- 不同可观测数据源之间如何串联?
- 散布在不同线程、协程之间的数据如何主动串联?
端侧数据主动串联计划
咱们先看下端侧数据主动串联的计划。
在 OTel 协定规范中,是通过 trace 协定来束缚不同数据之间的串联关系。OTel 定义了 trace 数据链路中每条数据必须要蕴含的必要字段,咱们须要确保同一条链路中数据的一致性。比方,同一条 trace 链路中,trace_id 须要雷同;其次,如果数据之间有父子关系,子数据的 parent_id 也须要与父数据的 span_id 雷同。
咱们晓得,不论是 Android 平台,还是 iOS 平台,线程都是操作系统可能调度的最小单元。也就是说,咱们所有的代码,最终都会在线程中被执行。在代码被执行过程中,如果咱们能把上下文信息和以后线程进行关联,在代码执行时,就能主动获取以后上下文信息,这样就能够解决同一个线程内的 trace 数据主动关联问题。
在 Android 中,能够基于线程变量 ThreadLocal 来存储以后线程栈的上下文信息,这样能够确保在同一线程中采集到的业务数据进行主动关联。如果是在协程中应用,基于线程变量的计划就会存在问题。因为在协程中,协程实在运行的线程是不确定的,可能会在协程执行的生命周期内进行线程切换,咱们须要利用协程调度器和协程 Context 来放弃以后上下文的正确性。在协程复原时,让关联的上下文信息在以后线程失效,在协程挂起时,再让上下文信息在以后线程生效。
在 iOS 中,次要基于 activity tracing 机制来放弃上下文信息的有效性。通过 activity tracing 机制,在一个业务链路开始时,会主动创立一个 activity,咱们把上下文信息与 activity 进行关联。在以后 activity 作用域范畴内,所有产生的数据都会与以后上下文主动关联。
基于这两种计划,在产生 Trace 数据时,SDK 会依照 OTel 协定的规范,主动把上下文信息关联到以后数据中。最终产生的数据,会以一棵树的模式进行逻辑关联,树的根节点就是 Trace 链路的终点。这种形式,不仅反对协程 / 线程内的数据主动关联,还反对多层级嵌套。
三方框架的数据采集和串联
针对三方框架的数据采集,咱们先看看业内通行的做法,目前次要有两类:
- 如果三方库反对拦截器或代理的配置,个别会通过在对应拦截器减少埋点代码的形式来实现;
- 如果三方库对外裸露的接口比拟少,个别会通过 Hook 或其余形式减少埋点代码,或者不反对对应框架的埋点。
这种做法会存在两个次要的问题:
- 埋点不齐全,拿 OkHttp 来举例说明,三方 SDK 外部也可能存在对 OkHttp 的依赖,通过拦截器的形式,可能只反对以后业务代码的埋点采集,三方 SDK 的网络申请信息无奈被采集到,会导致埋点信息不齐全;
- 可能须要侵入业务代码,为了实现对应框架的埋点,须要有一个切入机会,这个切入机会往往须要在对应框架初始化时减少代码配置项来实现。
如何解这两个问题?
咱们应用的计划是实现一个 Gradle Plugin,在 Plugin 中对字节码进行插桩解决。
咱们晓得,Android App 在打包的过程中,有个流程会把 .class 文件转为 .dex 文件,在这个过程中,能够通过 transform api 对 class 文件进行解决。咱们是借助 ASM 的形式来实现 class 文件的插桩解决。在对字节码解决的过程中,须要先找到适合的插桩点,而后注入适合的指令。
这里拿 OkHttp 的字节插桩进行举例:插桩的指标是在 OkHttpClient 调用 newCall 办法时,把以后线程的上下文信息关联到 OkHttp 的 Request 中。在 Transform 过程中,咱们先依据 OkHttpClient 的类名过滤出指标 class 文件,而后再依据 newCall 这个办法名过滤要插桩的办法。接下来,须要在 newCall 办法开始的中央把上下文信息插入到 request 的 tags 对象中。通过咱们的剖析,须要在 newCall 办法调用开始的时候,插入指标代码。为了不便实现和调试,咱们在扩大库中实现了一个 OkHttp 的辅助工具,在指标地位插入调用这个工具的字节码,传入 request 对象就能够了。
插入后的字节码会和扩大库进行关联。这样就能解决三方框架数据采集和上下文主动关联的问题。
绝对于传统做法,应用字节码插桩的计划,业务代码侵入性会更低,埋点对业务代码和三方框架都能失效,同时联合扩大库也能实现上下文的主动关联。
如何确保性能
在可观测数据采集过程中,会有大量的数据产生,对内存、CPU 占用、I/O 负载都有肯定的性能要求。
咱们基于 C 对外围局部进行实现,确保多平台的性能一致性,并从三个方面对性能做了优化:
首先,是对协定化处理过程进行优化。数据协定方面抉择应用 Protocal Buffer 协定,Protocal Buffer 绝对 JSON 来说,不仅速度更快,而且更省内存空间。在协定的序列化上,咱们采纳了手动封装协定的实现,在序列化的过程中,防止了很多长期内存空间的开拓、复制以及无关函数的调用。
其次,在内存治理方面,咱们间接对 SDK 的最大应用内存做了可配置的大小限度。内存的应用,能够依据业务状况按需配置,防止 SDK 内存占用过大对 App 的稳定性造成影响;其次,还引入了动态内存管理机制,内存空间的应用按需减少,不会始终占用 App 的内存空间,防止内存空间的节约。同时还晋升了字符串的解决性能。在字符的解决上,引入了动静字符串机制,它能够记录字符串本身的长度,获取字符长度时,操作复杂度低,而且能够防止缓冲区溢出,同时也能够缩小批改字符串时带来的内存重调配次数。
最初,在文件缓存治理方面,咱们也限度了文件大小的下限,防止对端设施存储空间的节约。在缓存文件的落盘解决上,咱们引入了 Ring File 机制,把缓存数据存储在多个文件下面,以日志文件组的模式对多个文件进行组装。整个日志文件组以环形数组的模式,从头开始写,写到开端再回到头从新循环写。通过这种形式写数据,能够缩小写文件时的随机 Seek,而且 Ring File 的机制,能够确保单个日志文件不会过大,从而尽可能的升高零碎 I/O 的负载。除了 Ring File 的机制外,还把断点保留、缓存清理的逻辑放到了一起聚合执行,缩小随机 Seek。checkpoint 的文件大小也做了限度,在超出指定大小后会对 checkpoint 文件进行清理,防止 checkpoint 文件过大影响文件读写效率。
通过下面的这些优化措施之后,最终 SDK 采集数据的吞吐量晋升了 2 倍,内存和 CPU 占用都有显著的升高。每秒钟最高可反对 400+ 条数据的采集。
如何确保日志不失落?
性能满足要求还不够,还须要确保采集到的数据不能失落。在 App 的应用过程中,app 常常可能会出现异常解体,手机设施异样重启,以及网络品质差,网络延时、抖动大的状况。在这类异样场景下,如何确保采集到数据不会失落?
在采集数据时,咱们应用了预写日志(WAL)机制,并联合自建网络减速通道来优化这个问题。
- 引入预写日志机制的目标是确保写入到 SDK 的数据,在发送到服务器之前,不会因为异样起因而失落。这个过程的外围是,在数据胜利发送到服务器之前,先把数据缓存在挪动设施的磁盘上,数据发送胜利之后,再移除磁盘上的缓存数据。如果因为 App 异样起因,或者设施重启导致数据发送失败,因为缓存的数据还在,SDK 会依据记录的断点信息对数据发送进度进行复原。同时预写日志机制能够确保数据的写入和发送并发执行,不会相互阻塞;
- 在数据发送之前,还会对多条数据做聚合解决,并通过 lz4 算法进行压缩解决,这种做法能够升高数据发送时的申请次数和网络传输流量的耗费。如果数据发送失败,还会有重试策略,确保数据至多能胜利发送一次;
- 在数据发送时,SDK 反对就近接入减速边缘节点,并通过边缘节点与 SLS 之间的外部网络减速通道传输数据。
通过这三种次要的形式优化之后,数据包的均匀大小升高了 2.1 倍,整体的 QPS 均匀晋升 13 倍,数据整体的发送成功率达到了 99.3%,网络延时均匀降落了 50%。
多零碎数据关联解决
解决了端侧数据的串联和采集性能问题之后,还须要解决多零碎之间的数据存储和关联剖析问题。
数据存储方面,咱们间接基于 SLS LogHub 能力,把相干的数据对立存储,基于 SLS,日均能够承载 PB 级别的流量,这个吞吐量能够反对挪动端可观测数据的全量采集。
解决了数据的对立存储问题之后,还须要解决两个次要的问题。
第一个问题,不同零碎可观测数据之间的上下文关联如何解决?
依据 OTel 协定的束缚,咱们能够基于 parent_id 和 span_id 来解决根节点、父节点、子节点之间的映射关系。首先,在查问 Trace 数据链路时,会先从 SLS 拉取肯定时间段内的所有 Trace 数据。而后依照 OTel 协定的束缚,对每条数据进行节点类型的断定。因为多零碎的数据可能存在延时,在查问 Trace 数据链路时,有些数据可能还没有达到。咱们还须要对临时不存在的父节点进行虚拟化解决,确保 Trace 链路的准确性。接下来,还须要对节点进行规整解决,把属于同一个 parent_id 的节点进行聚合,而后再依照每个节点的开始工夫进行排序,最终就能够失去一条 trace 链路信息,基于这个链路信息,咱们能够还原出零碎的调用链路。
第二个问题, 在进行 Trace 剖析时,咱们往往还须要从零碎视角登程,对不同维度的数据进一步剖析。比方,如果想从设施 ID、App 版本、服务调用等不同维度,对 Trace 数据进一步剖析,该怎么做?咱们来看一下怎么解决这个问题。
多零碎数据拓扑生成
当咱们从零碎整体视角对问题进行剖析时,所须要的 Trace 数据规模往往会比拟大,每分钟可能无数千万条数据,而且对数据的时效性要求也比拟高。传统的流解决形式在这种场景下很容易遇到性能瓶颈问题。咱们采纳的计划是,把流解决问题转换为批处理问题,把传统的链路解决视角转换为零碎解决视角。通过视角转化之后,从零碎视角来看,解决这个问题最次要的外围,就是如何确定两个节点之间的关系。
咱们看一下具体的处理过程。在批处理上,咱们应用了 MapReduce 框架。首先,在数据源解决阶段,咱们基于 SLS 的定时剖析(ScheduledSQL)能力,对数据进行聚合解决,依照分钟级从 Trace 数据源中捞取数据。在 Map 阶段,先依照 traceID 进行分组,对分组之后的数据再依照 spanID、parentID 维度对数据进行聚合。而后计算出相干的统计数据,如成功率、失败率、延时指标等根底统计数据。在理论的业务应用中,往往还会采集一些和具体业务属性相干的数据,这部分数据往往会依据业务的不同,有比拟大的差别。针对这部分类型的数据,在聚合解决的过程中,反对依照其余维度对后果进行分组。此时会失去两种两头产物:
- 蕴含两个节点关系的聚合数据,咱们把这种类型的数据,叫做边信息
- 以及未匹配到的原始数据
这两种两头产物,在 Combine 阶段还会再进行聚合解决,最终会失去蕴含根底统计指标,以及其余维度的后果数据。
最终产物会蕴含几个次要的信息:
- 边信息,能够体现调用关系。
- 依赖信息,能够体现服务依赖关系。
- 还有指标信息,以及其余资源信息等。其中,业务属性相干的数据会体现在资源信息中。
基于这些产物,咱们能够通过对资源、服务等信息的多个维度筛选,来统计出对应维度的问题散布和影响链路。
自动化问题根因定位摸索
接下来向大家介绍下,咱们在自动化问题根因定位方向的一些摸索。
咱们晓得,随着 App 版本的迭代,每次 App 的发版可能会波及到多个业务的代码变更。这些变更,有的通过充沛测试,也有的未通过充沛测试,或者惯例测试方法没有笼罩到,对线上业务可能会产生肯定的潜在影响,导致局部业务不可用。App 规模越大,业务模式越多,对应的业务数据量,申请链路,不确定性就越大。出了问题之后,往往须要多人跨域参加排查,人肉运维老本比拟高。
如何在端侧问题排查定位方向,通过技术手段进行研发效力的提速? 咱们基于机器学习技术做了一些摸索。
咱们目前的办法是,先对 Trace 源数据进行特色解决;而后再对特色进行聚类分析,去找到异样 Trace;最初再基于图算法等,对异样 Trace 进行剖析,找到异样的起始点。
首先,实时特色解决阶段会读取 Trace 源数据,对每个 Trace 链路依照由底向上找 5 个节点的形式生成一个特色,并对特色进行编码。而后对编码之后的特色通过 HDBSCAN 算法进行档次聚类分析,此时类似的异样会分到同一个组外面,接下来再从每组异样 Trace 中找出一条典型的异样 Trace。最初,通过图算法找到这条异样 Trace 的终点,从而确定以后异样 Trace 可能存在的问题根因。通过这种形式,只有是遵循 OTel 标准协议的数据源都可能进行解决。
案例:多端链路追踪
通过对数据处理之后,咱们来看下最终的成果。
这里有一个模仿 Android、iOS、服务端,端到端链路追踪的场景。
咱们应用 iOS App 来作为指令的发送端,Android App 来作为指令的响应端,用来模仿近程关上汽车空调的操作。咱们从图上能够看到,iOS 端“关上车机空调”这个操作触发后,顺次通过了“用户权限校验”、“发送指令”、“调用网络申请”等环节。Android 端收到指令后,顺次执行“近程启动空调”、“状态查看”等环节。从这个调用图能够看失去,Android、iOS、服务端,多端链路被串联到了一起。咱们能够从 Android、iOS、服务端的任何一个视角,对调用链路进行剖析。每个操作的耗时,对应服务的申请数,错误率,以及服务依赖都能体现进去。
整体架构
接下来,咱们来看下整套解决方案的架构:
- 最底层是数据源,遵循 OTel 协定,各个端对应的 SDK 依照协定标准对立实现;
- 数据存储层,是间接依靠于 SLS LogHub,所有零碎采集到的数据对立存储;
- 再往上是数据处理层,对要害指标、Trace 链路、依赖关系、拓扑构造、还有特色等进行了预处理。
最初是下层利用,提供链路剖析、拓扑查问、指标查问、原始日志查问,以及根因定位等能力
后续布局
最初总结下咱们后续的布局:
- 在采集层,会持续欠缺插件、注解等形式的反对,升高业务代码的侵入性,晋升接入效率
- 在数据侧,会丰盛可观测数据源,后续会反对网络品质、性能等相干数据的采集
- 在利用侧,会提供用户拜访监测、性能剖析等能力
最初,咱们会把核心技术能力开源,共享社区。