一个有意思的点子

671 阅读7分钟

前言

前端基于运行时检测问题、定位原因、预警提示的可行性思考🤔

背景

部门要治理线上故障过多的问题,故障主要分后端异常以及前端的性能问题和业务问题。我们讨论的范围是前端业务故障,经过分析可以把它们大致分为UI、业务、工程技术、日志、三方依赖等。

首先要确定降低故障的指标,MTTI和MTTD是关键,因为只有及时发现和定位问题才能快速消灭它们。我们暂时没有统计线上MTTI、MTTD的数据,因为缺乏相关的预警所以问题的发现和定位耗时通常很久,下面的一些复盘统计显示发现问题的时间可以持续甚至好几个月。

举个真实例子。Z同学在开发优惠券视图需求时,无意间影响到了相邻元素的布局,导致其中的关键业务信息没有全部显示直到上线后,有用户反馈才被发现。🫢

线上的问题中,UI和业务异常占比超过80%,这些问题发现的不及时,一方面是问题本身不会阻碍核心链路,另一方面说明团队对业务的稳定缺少监控手段。

等等☝️

我们的开发流程已经包含了Design QA来验收交付的UI是否符合预期; 开发工程师会和QA工程师一起执行Test Case验证业务的稳定; 在CI环节还有UT的保障; 最后线上还有大盘监控。既然如此,那为什么线上还是会不可避免的出现发现不及时的故障呢?

问题归因

在Dev和Stage阶段的验收能发现和处理绝大多数显而易见的异常,但是这些验收的场景是有限的

  1. 开发环境数据集的局限
  2. 考虑到AB因素的影响,很难做到全场景覆盖
  3. 开发过程可能会无意间影响到其他业务,但是我们的注意力集中在现有的业务,也就导致问题难以被发现
  4. 线上大盘监控的是大粒度的趋势,流程运转无影响的异常无法被探测

现有质量保障流程已经很全面足以覆盖绝大多数场景,但是仍然存在UI、业务的Edge case,归根到底,验收环节中数据和场景的局限以及人治导致一些Edge case被遗漏。

这就好比质检的主干道已经很完善,但是仍有乡间小路有待开发。

解决方案

我们该如何解决数据和场景的局限呢?这个其实通过Monkey和流量回放就能解决。

那如何解决问题发现和定位的效率呢?运行时阶段包含了所有业务和代码的上下文,所以在这个阶段发现问题、分析原因并预警效率是最高的,人治的问题可以得到改善。下面是这种机制执行的思路

  1. 自动化测试时,通过流量回放模拟线上的数据环境并覆盖尽可能多的场景。
  2. 运行时阶段,前端通过UT代码验收业务(🤔UT只能在Unit Test阶段执行,运行时执行的原理后面会讲)
  3. 运行时阶段,分析UI元素间的关系并探测异常问题
  4. 运行时阶段,分析… …(log, perf等)

方案实现

方案实现仅讨论前端的部分。 UI和业务检测比较通用,因为它们的判别条件是客观的。比如我们完全可以复用UT代码检测业务。

自动检测、定位原因、预警

这个机制实现没有困难。要思考普适性,考虑到自动检测的范围在不同项目中不尽相同,所以实现的思路可以是插件的形式,模块间通过协议解耦。 主要功能模块有:

  1. 告警模块
  2. 日志生成模块
  3. 业务注册模块(业务层和框架层的检测)

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代码从逻辑上可以被分为三个部分:

  1. Give
  2. When
  3. Then

Given表示输入的数据,可以是真实接口也可以是Mock数据。

When表示调用业务函数,同时这里会产生一个输出结果。

Then表示检验输入的数据是否和输出的结果匹配,如果不匹配会在UT环节报错。

业务代码从逻辑上可以被分为两个部分

  1. Give
  2. When

Given可以是上下文的变量也可以是接口调用

When表示执行业务的代码块

Blank diagram (23).png 如果把UT的Then引入到业务的函数里,就可以实现运行时执行UT的效果了😁。

将UT代码引入到业务函数中,思路是首先把UT按照Given、When、Then三部分拆分,把Given和Then部分独立出去,独立的部分通过代码插桩的方式在UT和业务代码中复用。

Blank diagram (24).png

代码插桩

我们项目基于Swift,且插桩需要有判别逻辑,基于此选用AST的方式在编译期修改FunctionDeclaration,把代码通过字符串的形式插到function body的合适位置。正巧2023 WWDC苹果发布了Swift Macro,其中的@Attached(Peer)正好可以满足对FunctionDecliation可编程修改,通过实现自动以@Attached(Peer)以及增加DEBUG宏(Swift Macro不能读取工程配置信息)可以改变UT的流程。真想说Swift太香了。

Macro有两大类,要用到的仅是Peer,效果如下图绿色、蓝色部分。 swift macro 不过到这里遗留了几个问题,暂时还没有太好的思路🧐

  1. 异步回调 - 业务代码或者UT检测逻辑只能执行一个
  2. 业务方法名的修改 - 使用Swift Macro插桩方式等同新增加一个全新的方法,所以运行时执行UT需要更改业务逻辑()
  3. macro 接受参数 - 要接受 input 和 output 也就是要macro和代码有交互
  4. UT代码被执行多次 - 上面的图可以看出来,新的UT代码被同时插到旧的UT和业务代码中,所以执行UT时会UT逻辑被执行了两次

最终

总结一下,这个思路的主要目的是服务业务,从运行时环节提供一种检测问题、定位原因并预警的机制。已经实现的检查有UI检查和待完善的UT检测,这些都只是刚刚开始。

至于在工程里落地的可行性呢,UI检测因为判别条件是客观普适的,所以不存在集成和使用的困难。但是业务检测不太一样,业务正常流转和单元测试覆盖率是落地的前提。

完整的项目的整体架构大致如下,主要分了三部分

  1. Hubble - 主要职责是提供基础协议、日志、预警和一些协助分析问题的工具集
  2. HubbleCoordinator - 主要职责是作为业务和组件交互的中间层,业务相关的逻辑都放在这里,隔离业务和Hubble
  3. 业务 - 仅处理Hubble初始化工作,对业务代码没有任何侵入 AR