前言
前端基于运行时检测问题、定位原因、预警提示的可行性思考🤔
背景
部门要治理线上故障过多的问题,故障主要分后端异常以及前端的性能问题和业务问题。我们讨论的范围是前端业务故障,经过分析可以把它们大致分为UI、业务、工程技术、日志、三方依赖等。
首先要确定降低故障的指标,MTTI和MTTD是关键,因为只有及时发现和定位问题才能快速消灭它们。我们暂时没有统计线上MTTI、MTTD的数据,因为缺乏相关的预警所以问题的发现和定位耗时通常很久,下面的一些复盘统计显示发现问题的时间可以持续甚至好几个月。
举个真实例子。Z同学在开发优惠券视图需求时,无意间影响到了相邻元素的布局,导致其中的关键业务信息没有全部显示直到上线后,有用户反馈才被发现。🫢
线上的问题中,UI和业务异常占比超过80%,这些问题发现的不及时,一方面是问题本身不会阻碍核心链路,另一方面说明团队对业务的稳定缺少监控手段。
等等☝️
我们的开发流程已经包含了Design QA来验收交付的UI是否符合预期; 开发工程师会和QA工程师一起执行Test Case验证业务的稳定; 在CI环节还有UT的保障; 最后线上还有大盘监控。既然如此,那为什么线上还是会不可避免的出现发现不及时的故障呢?
问题归因
在Dev和Stage阶段的验收能发现和处理绝大多数显而易见的异常,但是这些验收的场景是有限的
- 开发环境数据集的局限
- 考虑到AB因素的影响,很难做到全场景覆盖
- 开发过程可能会无意间影响到其他业务,但是我们的注意力集中在现有的业务,也就导致问题难以被发现
- 线上大盘监控的是大粒度的趋势,流程运转无影响的异常无法被探测
现有质量保障流程已经很全面足以覆盖绝大多数场景,但是仍然存在UI、业务的Edge case,归根到底,验收环节中数据和场景的局限以及人治导致一些Edge case被遗漏。
这就好比质检的主干道已经很完善,但是仍有乡间小路有待开发。
解决方案
我们该如何解决数据和场景的局限呢?这个其实通过Monkey和流量回放就能解决。
那如何解决问题发现和定位的效率呢?运行时阶段包含了所有业务和代码的上下文,所以在这个阶段发现问题、分析原因并预警效率是最高的,人治的问题可以得到改善。下面是这种机制执行的思路
- 自动化测试时,通过流量回放模拟线上的数据环境并覆盖尽可能多的场景。
- 运行时阶段,前端通过UT代码验收业务(🤔UT只能在Unit Test阶段执行,运行时执行的原理后面会讲)
- 运行时阶段,分析UI元素间的关系并探测异常问题
- 运行时阶段,分析… …(log, perf等)
方案实现
方案实现仅讨论前端的部分。 UI和业务检测比较通用,因为它们的判别条件是客观的。比如我们完全可以复用UT代码检测业务。
自动检测、定位原因、预警
这个机制实现没有困难。要思考普适性,考虑到自动检测的范围在不同项目中不尽相同,所以实现的思路可以是插件的形式,模块间通过协议解耦。 主要功能模块有:
- 告警模块
- 日志生成模块
- 业务注册模块(业务层和框架层的检测)
UI检测
业务不同,遇到的UI问题会有差异,这部分需要具体问题具体分析,所以不做过多讨论。针对我们业务的现状Overlap、Truncate、Clip在UI issue中占比较高。我的做法是对显示的视图按多叉树遍历到叶子节点,然后分析子节点和兄弟节点间的关系,并找到Overlap、Truncate、Clip问题。具体的实现可以参考代码LensWindowGuard.swift:31。
需要注意的是UI检测很难一蹴而就,需要在工程中细调,误报需要用排除策略以及产品规则排除。
/// Invisible Situations
/// SuperView
/// 1. No superView
/// 2. SuperView.isHidden = true
/// 3. SuperView.alpha < 0.01
/// self
/// 4. view.size.width == 0 || view.size.height == 0
/// 5. view.alpha < 0.01
/// 6. view.isHidden = true
/// 7. view.size.width == 0 || view.size.height == 0
/// 8. superView maskView != view
/// UIImageView
/// 9. UIImageView.image == nil && No Fill Color
/// Content view (UIImageView / UILable)
/// 10. do not have content or its content is invisible
业务检测
UT代码从逻辑上可以被分为三个部分:
- Give
- When
- Then
Given表示输入的数据,可以是真实接口也可以是Mock数据。
When表示调用业务函数,同时这里会产生一个输出结果。
Then表示检验输入的数据是否和输出的结果匹配,如果不匹配会在UT环节报错。
业务代码从逻辑上可以被分为两个部分
- Give
- When
Given可以是上下文的变量也可以是接口调用
When表示执行业务的代码块
如果把UT的Then引入到业务的函数里,就可以实现运行时执行UT的效果了😁。
将UT代码引入到业务函数中,思路是首先把UT按照Given、When、Then三部分拆分,把Given和Then部分独立出去,独立的部分通过代码插桩的方式在UT和业务代码中复用。
代码插桩
我们项目基于Swift,且插桩需要有判别逻辑,基于此选用AST的方式在编译期修改FunctionDeclaration,把代码通过字符串的形式插到function body的合适位置。正巧2023 WWDC苹果发布了Swift Macro,其中的@Attached(Peer)正好可以满足对FunctionDecliation可编程修改,通过实现自动以@Attached(Peer)以及增加DEBUG宏(Swift Macro不能读取工程配置信息)可以改变UT的流程。真想说Swift太香了。
Macro有两大类,要用到的仅是Peer,效果如下图绿色、蓝色部分。
不过到这里遗留了几个问题,暂时还没有太好的思路🧐
- 异步回调 - 业务代码或者UT检测逻辑只能执行一个
- 业务方法名的修改 - 使用Swift Macro插桩方式等同新增加一个全新的方法,所以运行时执行UT需要更改业务逻辑()
- macro 接受参数 - 要接受 input 和 output 也就是要macro和代码有交互
- UT代码被执行多次 - 上面的图可以看出来,新的UT代码被同时插到旧的UT和业务代码中,所以执行UT时会UT逻辑被执行了两次
最终
总结一下,这个思路的主要目的是服务业务,从运行时环节提供一种检测问题、定位原因并预警的机制。已经实现的检查有UI检查和待完善的UT检测,这些都只是刚刚开始。
至于在工程里落地的可行性呢,UI检测因为判别条件是客观普适的,所以不存在集成和使用的困难。但是业务检测不太一样,业务正常流转和单元测试覆盖率是落地的前提。
完整的项目的整体架构大致如下,主要分了三部分
- Hubble - 主要职责是提供基础协议、日志、预警和一些协助分析问题的工具集
- HubbleCoordinator - 主要职责是作为业务和组件交互的中间层,业务相关的逻辑都放在这里,隔离业务和Hubble
- 业务 - 仅处理Hubble初始化工作,对业务代码没有任何侵入