作者 | 彭阳
导读
性能中台负责MEG端研发数据的接入、传输、管理、应用等各个环节。为了应对移动应用领域中端技术的快速迭代和线上突增问题的挑战,中台提出了实时拦截与问题的分发机制,旨在在端上线的不同阶段及时发现并拦截异常上线,最大程度减少线上变更对用户体验的不良影响。本文在数据建设的时效性和准确性上进行深入的探讨,包括:变更上线的染色过程、基于染色ID的性能核心数据指标的监控、线上问题实时分发至相关模块组件和人员等。
全文7719字,预计阅读时间20分钟。
01 背景
1.1 业务背景
在快速发展的移动应用领域中,持续的技术迭代是保持APP竞争力的关键因素。然而,对于规模庞大、用户众多的APP应用,每一次的变更上线都存在引入线上问题的风险。APP的各个组件模块相互交织,一旦某处出现异常,往往会像连锁反应一样影响整个系统的稳定性,导致用户体验的下降。在以往的经验中,即便在开发和测试阶段充分验证,也难免会有一些问题在真实用户环境中暴露出来。这些问题可能表现为崩溃、卡顿、功能失效等,严重影响用户的使用体验。在过去,应对这些问题通常是事后进行问题修复,然而这种方式并不能完全避免线上问题对用户体验的不良影响。因此我们希望在业务迭代变更上线的过程中,尽可能早的发现和拦截问题,最大程度的降低问题对用户的影响。因此,性能中台引入了多级拦截和问题分发的机制,这一机制旨在在变更上线的不同灰度放量阶段,对每次上线或者放量操作进行染色形成唯一染色ID,通过对每个染色ID的核心性能数据指标进行监控,一旦发现异常,多级拦截机制将会被触发,拦截本次上线以及后续放量。同时,问题实时分发机制能够直接将问题指向导致该问题发生的模块和组件以及开发和测试人员。从而准确定位问题,并迅速修复,避免问题在更大范围内扩散。
1.2 技术背景
实时UV计算:在处理异常上线的拦截过程中,数据的实时消费以及数据的时效性的要求特别高,必须在分钟级别内完成。同时,业务方不仅需要获得异常的PV数,同时也需要获得在各个维度下异常影响的用户数(UV)。但实时UV计算不能简单地累加,这涉及到同维度间用户交集的处理。例如:A版本和B版本异常影响的用户数分别是100,但整体用户数实际上可能不足200。但是也不能直接存储用户ID,例如通过使用HashSet或者HashMap存储所有的用户ID进行去重,这面对大量用户时,会占用大量的计算节点内存资源。因此,我们采取一种计算的时效性和准确性较高的数据结构Bitmap来计算实时UV。
Bitmap 的底层数据结构用的是 String 类型的 SDS 数据结构来保存位数组,把每个字节数组的 8 个 bit 位利用起来,每个 bit 位 表示一个元素的二值状态。该数据结构能节约大量的存储,同时在用户群做交集和并集运算的时候也有极大的便利。例如,每个用户ID如果存储在HashSet或者HashMap中,需要占4个字节即32bit,而一个用户在Bitmap中只占一个bit。在做交并集运算时,例如,员工1、员工2都是程序员,员工1使用苹果手机,那么如何查找使用苹果手机的程序员用户?
直接使用位运算,使用苹果手机的程序员用户:(0000000110B & 0000000010B = 0000000010B)
异常反混淆:主要用于问题分发阶段。APP厂商在发布应用程序包时,通常会对包进行混淆操作,这是为了提高APP应用的安全性和减少反编译的风险。混淆是将源代码中的符号、名称和结构等转换为难以理解的形式,使得反编译后的代码难以还原为原始的源代码,但是APP上报的异常信息也被混淆了。反混淆操作是将混淆后的异常信息还原为可读的形式,使开发人员能够更准确地分析问题的原因,并迅速采取正确的修复措施。在APP产出应用程序包时,同时也会产生一份用于反混淆异常信息的映射文件(密码本),通过映射文件 + 解析算法对混淆的异常进行解析,即可得到已读的异常堆栈。
△异常信息反混淆过程
1.3 名词解释
性能中台:性能中台是APP性能追踪的一站式解决方案平台,为APP提供全面、实时的性能分析服务与工具链。
移动线上质量平台: 移动线上质量平台是移动端APP发包后,用来查看、分析包质量数据、进行核心指标监控/报警、变更异常拦截。
日志中台:指端日志中台,包括端日志全生命周期的能力建设。包括打点SDK / 打点server/ 日志管理平台等核心组件。
Tekes平台:App端研发平台,提供包组件管理等基础设施。
02 系统设计
2.1 整体流程
在以往的流程中,针对客户端上线变更,我们通常使用大盘性能指标来进行监控,以便进行问题定位和止损。然而,在灰度用户数量较少的情况下,线上问题往往无法在大盘性能指标中产生明显波动。当业务决定全量上线或扩大灰度用户规模时,问题就可能显现出来了。在问题定位和解决的阶段,我们过多地依赖人工干预和手动排查,这导致问题定位和解决的时间较长,并可能升级为事故。因此,在旧流程中,线上问题影响面大小主要取决于灰度用户的规模以及问题排查人员对客户端各个模块和相关人员的了解程度,这是不合理的。因此,系统设计的关键在于解决两个核心问题:首先,如何在灰度阶段拦截问题,避免其进一步扩大;其次,一旦线上问题出现,如何能够迅速进行问题召回与解决。
△线上异常实时拦截与问题分发整体流程设计图
因此,在新流程中,引入了两个关键模块:"变更拦截模块"和"问题分发模块"。对于每次平台的上线变更,必须先在变更拦截模块中进行注册,从而生成一个唯一的上线染色ID。同时,将染色ID下发至本次变更上线的用户客户端。此后,该数据集的用户日志将携带染色ID进行上报。变更拦截模块将基于染色ID的粒度进行监控和拦截,以保证在上线过程中问题的及时发现。同时,问题分发模块建立了问题自动分发机制。当一个上线变更被拦截或者在线上出现问题时,该模块将直接将问题指派给涉及问题的模块、组件,以及相关的研发和测试人员。协助业务方快速准确的定位问题,人工再介入修复。
2.2 异常上线变更拦截
异常上线变更拦截的核心思路是:为每次上线变更生成独特的染色ID,通过对每个染色ID的性能核心数据进行拦截与监控。
△异常上线变更拦截流程设计图
变更拦截流程的具体步骤如下:
① 变更上线注册: 针对厂内各个配置变更平台,需要在每次上线配置生效之前,将上线信息在染色通用服务进行注册。
② 获取染色ID: 染色通用服务通过HTTP接口为每次上线注册生成通用的染色ID,并将其返回给上线变更平台。
③ 下发染色配置: 在圈定的用户群范围内,上线变更平台将新配置和染色信息同时下发到端上的业务SDK(如AB-SDK)中。
④ 染色日志上报: 业务SDK会判定染色是否生效,如果生效,则在涉及性能核心场景的日志中附加染色ID信息,然后通过UBC-SDK上报。这些日志会通过日志中台实时转发,并写入消息队列。
⑤ 实时指标计算: 性能中台会实时订阅消息队列中的核心性能数据,例如崩溃、APP启动次数等,然后针对每个染色ID,根据多个维度(如产品线、APP版本、操作系统、地域等)形成性能聚合指标,并将其写入持久存储。
⑥ 异常拦截服务: 基于存储中的染色数据,异常拦截服务通过配置监控项来检测数据是否出现异常。一旦染色数据异常,系统会触发拦截措施并发出告警。
⑦ 异常止损: 在触发拦截和告警后,系统会通过关联染色ID和变更上线的关系,拦截继续放量以及通知业务方针对本次上线的配置回滚。
针对每次线上配置的变更,都会有一段观察期。这个观察期的长短需要适度,以确保数据的可靠性。过短的观察期会影响数据的置信度,而过长则可能降低研发效率。一般而言,每次变更后需要等待10分钟的观察期,然后再逐步增加线上流量。因此,变更拦截模块对数据时效性的要求非常高,要求数据的端到端传输时效在3分钟以内,以确保有足够的数据累积时间,从而提升监控指标的可信度。同时,染色ID的数据指标项不仅涵盖了基础的PV指标(如崩溃次数和APP启动用户数),还扩展至UV级别的指标(如受崩溃影响的用户数收敛情况和APP用户启动数)。多个指标维度下UV的计算也给数据链路的时效性和准确性带来了挑战。具体的数据流设计如下所示:
△变图更拦截数据流设计
变更拦截的数据流主要分为两个部分:
(1)实时指标计算服务;
(2) ID生成服务。
ID生成服务
ID生成服务主要用于将厂内的CUID生成INT类型的数字,从而存储到Bitmap的数据结构中。整个服务需要满足以下条件:
(1)CUID-ID的的映射全局唯一,不会出现重复的ID,且ID的整体趋势递增。
(2)高并发低延时。核心数据在计算节点内存中进行产出,减少数据库压力。
(3)高可用,服务基于云上分布式架构,即使存储mysql宕机,也能容忍一段时间数据库不可用。
在实时流中,在接收到原始数据时,先根据CUID进行keyby分发,将相同的CUID分发到同一个计算节点。对于每个计算节点:
(1)优先查询自身内存中的缓存的映射关系,若不存在,则查询redis。
(2)若redis不存在映射关系,则访问生成新ID服务。
(3)服务请求hash到号段节点上,每次去DB拿固定长度的ID List进行分发,然后把最大的ID持久化下来,也就是并非每个ID都做持久化,仅仅持久化一批ID中最大的那一个。这个方式有点像游戏里的定期存档功能,只不过存档的是未来某个时间下发给用户的ID,这样极大地减轻了DB持久化的压力。
(4)最终将映射关系写入到Redis存储中。
实时指标计算服务
在数据处理流程中,端上传的日志数据经由日志中台进行转发,进而分发到性能的各个消息队列中。实时计算服务订阅这些消息队列中的数据,以多级聚合方式进行维度指标的计算:
(1)数据分发与映射: 首先,从消息队列中获取原始数据。根据CUID进行keyby分发,将相同的CUID分发到同一个计算节点。防止相同的CUID同时访问ID-Mapping服务,导致CUID-ID的的映射全局不唯一。
(2)本地聚合: 对数据进行解析获得指标和维度,然后在每个计算节点上进行本地窗口聚合操作,将具有相同维度(版本、操作系统、染色ID等)的CUID汇总成Bitmap格式。本地聚合的目的在于减少后续的keyby shuffle阶段的数据量。
(3)全局聚合: 状态服务维护实时流的运行时状态信息和历史数据的Bitmap结果。将实时数据与历史数据进行全局聚合,从而得到最终的结果数据。同时,新的Bitmap结重新写入状态服务。其中,运行时状态信息保证了在实时流断流或重启时,能够恢复上次运行状态。加上可重入的数据源和幂等的数据输出,确保了数据流的不丢不重。
通过上述服务,当新的变更上线导致端上异常数据突增时,变更拦截服务能够在分钟级内对异常上线进行监控告警以及拦截。此外,除了向业务方披露拦截的数据指标,我们也希望中台能够直接协助业务方定位排查出问题的根因。
2.3 问题自动分发
问题自动分发的核心思路是:建立端上各个模块、组件、类方法和研发测试人员的映射关系,当发生线上问题时,通过聚类规则将经过反混淆之后问题直接指向导致问题发生的组件、模块以及负责该模块的研发测试人员。
△线上问题实时自动分发数据流
问题分发的数据流主要分为四个部分:
(1)映射文件写入存储
(2)线上异常反混淆
(3)端组件关系建立
(4)问题分发
其中(1)(2)步骤是为了将线上异常解析为可读的形式。(3)(4)步骤为将可读的堆栈进行聚类以及分发。
映射文件写入存储
在技术背景的实时反混淆介绍中,为了将经过混淆的异常信息恢复成可读的形式,关键在于使用映射文件。映射文件分为两种:一种是针对每个APP发版时生成的映射文件,用于对该版本的APP自身的异常信息进行反混淆;另一种是操作系统发版产生的映射文件,用于系统级别的异常信息的反混淆。当APP发版或操作系统升级时,通过配置流水线将相应的映射文件写入映射文件缓存中。
APP的版本又有线上和线下的区别。针对线上发版的APP版本,其版本的特点表现为每个版本的发版周期较长,线上异常数量较多,同时对数据解析的实时性要求较高。为满足这些特点,将线上发版的映射文件存储于高性能的Redis集群中。对于厂内线下测试发版的APP版本,其版本特点体现在研发测试人员都能发版,导致版本数量相对较多。然而,与线上环境不同的是,线下测试环境中构造的异常较少,而对数据解析的实时性要求较低。鉴于这些特点,更适合将线下测试发版的映射文件存储于性能稍弱但更适合存储大量数据的Hbase集群中。
线上异常反混淆
在端发生线上异常时,端会上报两条信息流。一条是崩溃的指标数据流,一条是定位堆栈的文件流。指标数据流的特点是上传信息快,包含核心信息以及简化版的异常信息,但异常信息量不全。而文件流的特点是,上传速度偏慢,但是异常信息完整(2M)。结合这两种信息流,可以获得完整的线上异常信息,供业务分析使用。但是由于两条信息流是异步上报,经常面临数据流乱序到达的问题。因此,为了解决乱序问题,我们设计了状态服务。
△状态服务数据流设计
状态服务的主要目的是处理双流乱序的数据,以确保它们能够被正确关联。其核心流程如下:
(1)同时间窗口数据关联:从消息队列中订阅数据后,首先会对处于计算节点同一个时间窗口的数据进行关联,若关联成功,则数据直接发往下游进行计算。
(2)同历史未关联的数据关联:若未成功,则查询状态服务中之前窗口中尚未被关联的数据进行关联,若关联成功,发往下游,状态存储中清除关联数据。
(3)未关联数据写入状态存储:若未成功,则将未关联的数据写入到状态存储中,等待被将来的数据进行关联。
此外,状态存储也不能无限的增长,需要有过期淘汰的策略,在该场景下,我们设置的是30min的TTL,能够关联到99.9%以上的数据。
在成功关联双流数据后,紧接着是对堆栈进行反混淆解析。反混淆的目标是将经过混淆后的异常信息还原为可读的形式。在反混淆过程中,各种类型异常的解析算法工具(如腾讯Bugly、谷歌CrashPad)通常能够在秒级别(约10秒)内完成解析操作。然而,在实时数据流处理中,仅仅满足秒级的解析时效性是不够的,需要将解析速度提升至毫秒级。因此,中台实时流中对反混淆解析进行了升级,包含算法升级适配实时流以及多级缓存在构建。
多级缓存由计算节点内存,Redis以及Hbase构成。其中,根据不同的缓存命中情况,反混淆解析的性能会有所不同。如果堆栈对应的映射文件在计算节点内存中命中,解析可以在<10ms内完成。若在Redis或Hbase中命中,则解析时间会略有延长,达到秒级别(约10秒),那么最理想的方式是将所有的映射文件都在计算节点内存中被命中。然而,由于每个版本都有对应的映射文件,且每个线上APP都有数十个版本,每个映射文件大小约为300MB,这使得无法将线上流量所需的所有映射文件都加载到算子内存中。为了解决这一问题,我们对解析算法进行了多级索引的优化。这种优化策略将整个映射文件进行了细粒度拆分,仅将异常堆栈命中的映射文件行信息加载到内存中。因此,解析算法现在不再需要将整个映射文件完全存入内存。相反,它只需存储一级索引和二级索引的关系,以及命中堆栈后获得的结果数据。这一方式在显著提高内存利用效率的同时,也解决了计算节点内存不足的问题。
△多级索引查询
但是,随着线上任务长时间运行时,我们注意到程序性能逐渐下降,导致实时流任务的数据处理经常出现延迟。我们发现问题的根本原因是计算节点缓存中的映射文件被频繁替换,导致缓存命中率低。因此,我们采用了更为适合业务场景的缓存替代算法---W-TinyLFU代替常规的LRU缓存替代策略。
△W-TinyLFU算法
相比LRU算法,W-TinyLFU:
1、热点数据适应性更强: 在高流量的场景中,一些热点数据项可能会在短时间内被多次访问。与LRU只关注最近的访问,W-TinyLFU通过维护频繁访问计数来更好地捕获这种热点数据的特征,从而更好地适应瞬时的流量变化。
2、低频数据保护:LRU在遇到新数据时,会立即淘汰最近最少使用的数据,这可能导致低频数据被频繁淘汰。W-TinyLFU通过维护一个近似的频率计数,可以更好地保护低频数据,防止它们被过早地淘汰。
3、适应性更强:W-TinyLFU在面对访问模式的变化时,能够更快地适应新的访问模式。它在长时间内持续观察访问模式,并逐渐调整数据项的权重,以更好地反映最近的访问模式。
4、写入操作考虑:LRU通常对写入操作的适应性较差,因为写入操作可能导致数据被立即淘汰。W-TinyLFU考虑了写入操作,通过维护写入时的频繁访问计数,可以更好地处理写入操作。
5、内存效率:W-TinyLFU使用了一些压缩技术来存储频繁访问计数,从而在一定程度上减少了内存占用。
经过对线上异常进行反混淆处理后,我们获得了可读的堆栈信息。接下来,我们可以对这些可读的堆栈信息进行问题聚类和分发。
端组件关系建立
组件关系变更的数据来源分为两个部分:
(1)全量数据:在APP进行发版时,通过EasyBox等工具将组件、模块等关系从包中解析出来,将该版本的组件信息上传到组件管理平台,触发组件管理平台的全量同步,将组件信息写入消息队列中。中台通过订阅组件数据,建立类方法<->组件、模块、研发人员、测试人员的关系,写入到存储中。
(2)增量数据:在组件管理平台进行人为的修改,例如修改组件类的研发、测试负责人等,触发增量同步,变更的数据写入消息队列。
通过上述数据同步,建立了类方法<->人的映射。
问题分发
通过数据流的反混淆解析,我们成功地将异常信息从二进制地址转换为可读的信息。接下来,借助聚类规则算法,我们将不同的异常堆栈逐行遍历,并优先将其聚类到厂内维护的组件和模块中所包含的类中。在此过程中,我们建立了线上问题<->类之间的关系。而在端组件关系建立的流程中,我们成功地构建了类方法<->研发测试人员之间的映射关系。将这两者的关系结合起来,我们获得了问题<->人员之间的关联。因此,当线上出现问题时,无需人工干预,系统可以直接将该问题指向负责该问题模块的负责人。负责人随后可以根据反混淆后的异常信息,进行问题的排查和修复工作。
03 总结与展望
本文主要介绍了性能中台在处理异常上线过程中,针对变更拦截和问题分发方面做的一些努力。当然,后面我们还会继续更好的服务业务,如:
1、提升影响力:对接更多的上线变更平台以及落地更多的APP。
2、提升场景覆盖度:覆盖卡顿、启动速度、网络性能、搜索性能等更多的核心业务场景。
3、提升问题聚类以及分发策略的准确度:将线上问题分发的更加合理与准确。
希望,性能中台持续不断优化,为保障APP的质量做出贡献。
——END——
推荐阅读