Ellyn-Golang调用级覆盖率&方法调用链插桩采集方案

news/2025/1/13 17:20:00/文章来源:https://www.cnblogs.com/lvyahui/p/18668964

词语解释

Ellyn要解决什么问题?

在应用程序并行执行的情况下,精确获取单个用例、流量、单元测试走过的方法链(有向图)、出入参数、行覆盖等运行时数据,经过一定的加工之后,应用在覆盖率、影响面评估、流量观测、精准测试、流量回放、风险分析等研发效能相关场景。

常见的覆盖率工具实现

常见的覆盖率工具,原理都是通过在代码关键节点,插入全局探针数组来实现覆盖状态的采集。伪代码如下

var flags []boolfunc method() {flags[0] = truen := 100k := 10 sum := 0 for i := 0 ; i < n ; i ++ {if i % k == 0  {flags[1] = truesum += i} else {flags[2] = truesum += 1}}flags[3] = trueprintln(sum)
}

这里为了可读性调整了格式,实际生成的代码,一般追加在文件或者已有行的尾部,不会影响原代码的行号。

以上面的伪代码为例,我们看看各开源实现是怎么对代码插桩的。

Go test cover实现方案

Go test cover插桩逻辑可以在go源码中看到

插桩完成后的代码如下

//line api.go:1
package cover_example                              import "fmt"                                       func method() {GoCover.Count[0]++;                 n := 100                                   k := 10                                    sum := 0                                   for i := 0; i < n; i++ {GoCover.Count[2]++;if i%k == 0 {GoCover.Count[3]++;   sum += i                   } else{ GoCover.Count[4]++;{       sum += 1                   }}                                 }                                          GoCover.Count[1]++;fmt.Println(sum)        
}                                                  var GoCover = struct {                             Count     [5]uint32                        Pos       [3 * 5]uint32                    NumStmt   [5]uint16                        
} {                                                Pos: [3 * 5]uint32{                        5, 9, 0x19000f, // [0]16, 16, 0x120002, // [1]9, 10, 0xf0019, // [2]10, 12, 0x4000f, // [3]12, 14, 0x40009, // [4]},NumStmt: [5]uint16{4, // 01, // 11, // 21, // 31, // 4},
}

七牛云GOC

https://github.com/qiniu/goc

核心逻辑跟Go test cover类似的,部分代码也是复用的go test cover源码:pkg/cover/internal/tool/cover.go。

七牛云cover需要有main package才能插桩,实现原理与go test cover一致。不过七牛云将探针数组生成到了一个单独的文件,而不是像go test coverI 具追加到源码文件尾部

➜  goc-build-4fa554c51f8f ls
api.go  api_test.go  go.mod  http_cover_apis_auto_generated.go  src
➜  goc-build-4fa554c51f8f cat api.go                           
//line /tmp/goc-build-4fa554c51f8f/api.go:1
package main; import . "cover_example/src/gocbuild4fa554c51f8f"import "fmt"func method() {GoCover_0_396638376133663931613965.Count[0]++;n := 100k := 10sum := 0for i := 0; i < n; i++ {GoCover_0_396638376133663931613965.Count[2]++;if i%k == 0 {GoCover_0_396638376133663931613965.Count[3]++;sum += i} else{ GoCover_0_396638376133663931613965.Count[4]++;{sum += 1}}}GoCover_0_396638376133663931613965.Count[1]++;fmt.Println(sum)
}func main() {GoCover_0_396638376133663931613965.Count[5]++;method()
}➜  goc-build-4fa554c51f8f cat src/gocbuild4fa554c51f8f/cover.go
package gocbuild4fa554c51f8fvar GoCover_0_396638376133663931613965 = struct {Count     [6]uint32Pos       [3 * 6]uint32NumStmt   [6]uint16
} {Pos: [3 * 6]uint32{5, 9, 0x19000f, // [0]16, 16, 0x120002, // [1]9, 10, 0xf0019, // [2]10, 12, 0x4000f, // [3]12, 14, 0x40009, // [4]19, 21, 0x2000d, // [5]},NumStmt: [6]uint16{4, // 01, // 11, // 21, // 31, // 41, // 5},
}

Jacoco方案

$jacocoInit方法将按照class+method维度获取相应的全局探针数组,原理其实与go的类似。得益于java语言的动态能力,jacoco不仅支持编译期插桩,也支持运行时插桩。另外,Go是源码插桩,所见即所得,而Jacoco是字节码插桩,插入的是字节码指令,下面的代码是插桩完的字节码反编译之后的源码。

package testapp;public class Application {public Application() {boolean[] var1 = $jacocoInit();super();var1[0] = true;}public static void method() {boolean[] var0 = $jacocoInit();int n = 100;int k = 10;int sum = 0;int i = 0;for(var0[1] = true; i < n; var0[4] = true) {if (i % k == 0) {sum += i;var0[2] = true;} else {++sum;var0[3] = true;}++i;}System.out.println(sum);var0[5] = true;}
}

全局探针方案的优劣

这类方案的优势是,实现简单,并且性能影响极小(特别在客户端大规模代码插桩时)。但最明显的缺点是只能收集全局粒度的数据,无法细分单个调用的覆盖和链路数据。

要细分单个调用数据,折中的方案是通过求快照差来近似获取覆盖数据,比如在做流量回放时,要收集单个回放流量的覆盖数据:在回放之前,先清空全部的覆盖数据;在回放完成后,记录一次最新的覆盖率数据,以这份数据作为流量的覆盖数据。这么做会有两个很明显的问题

  • 回放流量\用例执行等只能串行执行,否则因为并发影响,无法通过快照差来求覆盖率数据,回放效率低下。

  • 单个流量\用例的覆盖数据依然可能存在噪音,比如一些旁路的异步逻辑(定时器、MQ消费等)造成的覆盖数据,也会统计到当前流量\用例上。

    另外基于全局探针采集的方案,还有两个明显的缺点,即使通过快照差也无法解决:

  • 虽然采集了覆盖率或者已覆盖的方法,但无法还原调用链/控制流图。

  • 数据无法全链路串联起来,流量很可能经过了多个后端服务,每个服务收集的覆盖数据是孤立且不绑定请求信息的,因此无法串联起来

Ellyn实现方案

Ellyn命令行工具,在编译期修改目标业务代码,在函数、代码块入口等关键位置,植入SDK调用,并将SDK源码拷贝到目标项目,跟随目标项目一起编译。

遍历代码并在关键位置插入代码,则是基于GO AST API,读取每一个源码文件,解析并遍历AST(抽象语法树),在函数和代码块的开始位置植入代码,函数植入非常容易,函数有很直接的分隔符,AST可以直接遍历单个函数,因此很容易在函数开始位置植入代码。比较麻烦的是代码块,这里代码块可以理解为一段在不发生异常的情况下,可以连续执行的一段代码,一直到控制语句或者代码块结束符(go为}')为止,跟静态分析中的Basic Block 很相似,程序并没有直接的、固定的块分隔符,AST也没有抽象的Block节点,因此需要自行遍历所有Statement,寻找控制语句和结束符,自行记录开始结束位置,手动划分Block。遍历完所有文件后,将方法、Block等元数据通过go embed压缩集成到目标程序中。

插桩完的目标代码编译运行后,SDK将按照协程粒度收集数据,模拟函数弹栈入栈的操作,当函数弹空时,说明当前协程调用结束了,可以将当前协程数据放入本地的RingBuffer队列,等待后续的加工、上报等处理。如果是同一个调用(流量)触发的多个异步覆盖,则将多个协程的数据通过链路ID关联起来,这个关联合并的动作可以放在上报后端实现,进一步降低对本地的性能影响。

程序架构

原理图

工具能力

支持调用级链路数据采集,链路数据包括方法链路(堆栈)、方法出入参、方法耗时、异常、error、行覆盖等。并且工具内部默认集成了一个简单的web页面,可以在本地可视化查看链路数据

难点和挑战

Ellyn插桩是对代码有侵入的,因此需要充分考虑稳定性和性能方面的影响,并且由于采集的是调用级的数据,数据量巨大,数据存储本身也是一大挑战。

稳定性

  • 避免插桩之后目标项目无法运行,插桩工具可以支持回滚。同时插桩工具的准出应该配套丰富的自动化和灰度流程。

  • 避免自身的异常抛出给业务,导致业务代码异常。对Go语言即应该recover自身所有可能的panic(当然fatal error是无法recover的,此类问题可以依赖自动化、灰度等在准出阶段发现)。

  • 可以进一步支持动态关闭、自动降级等能力

  • 可以监控CPU、内存使用情况,自动暂停恢复采集能力

  • 可以增加监控埋点,结合监控系统进行告警

  • 插桩采集逻辑可以增加限流,降低在大流量场景下对目标项目的影响。

性能影响

插桩代码要避免对业务代码造成明显的性能影响。

技术上

  • 避免加重量级锁,确保每个协程只操作当前协程的数据

  • 需要高频创建使用的对象,考虑池化,减少GC的压力。

  • 插桩到业务代码的方法要确保每一个方法都是O(1)时间复杂度的操作。

  • 高频访问的字段进行缓存行填充,避免伪共享。

  • 整数索引的场景,尽量考虑用bitmap(bitset)或者数组,而非map。

能力上

  • 支持多种采样策略

  • 参数采集涉及序列化,对性能影响较大,支持不同的参数采集策略,必要时可以关闭参数采集

当然,即使经过各种优化,由于插桩语句做了更多的操作,即使是0(1)级别的无锁操作,依然比传统方案仅一次数组访问的指令要更多。在一些CPU 敏感型场景,Ellyn插桩性能损耗依然比传统方案要高。

基准测试

以下对比了排序、搜索、压缩、加密、文件读写、网络请求等场景下的性能影响,在涉及有IO操作的情况下,Elyn影响可以忽略不计,在纯CPU密集型场景,有一定性能损失。实际上,互联网业务大部分场景都是有IO操作的,比如读写DB、RPC调用,甚至于仅打印日志(非异步写),因此性能影响基本可以忽略。

无插桩(基准)

goos: linux
goarch: arm64
pkg: benchmark
BenchmarkQuickSort-4                        137570             42951 ns/op            4088 B/op               9 allocs/op
BenchmarkBinarySearch-4                   228617994                26.24 ns/op               0 B/op               0 allocs/op
BenchmarkBubbleSort-4                        44918            133785 ns/op            4088 B/op               9 allocs/op
BenchmarkShuffle-4                          330484             18054 ns/op               0 B/op               0 allocs/op
BenchmarkStringCompress-4                     3034           1903760 ns/op          876214 B/op              33 allocs/op
BenchmarkEncryptAndDecrypt-4                590178              9990 ns/op            1312 B/op              10 allocs/op
BenchmarkWrite2DevNull-4                   1428777              4202 ns/op             304 B/op               5 allocs/op
BenchmarkWrite2TmpFile-4                    535009             10967 ns/op             128 B/op               1 allocs/op
BenchmarkLocalPipeReadWrite-4               265272             21792 ns/op            2176 B/op              18 allocs/op
BenchmarkSerialNetRequest-4                    387          15407760 ns/op           40489 B/op             480 allocs/op
BenchmarkConcurrentNetRequest-4               1713           3576828 ns/op          136009 B/op             990 allocs/op
PASS
ok          benchmark        76.928s

0.0001采样(万分之一)

goos: linux
goarch: arm64
pkg: benchmark
BenchmarkQuickSort-4                        107018             55802 ns/op            4089 B/op               9 allocs/op
BenchmarkBinarySearch-4                   81365398                72.32 ns/op               0 B/op               0 allocs/op
BenchmarkBubbleSort-4                        33294            182848 ns/op            4093 B/op               9 allocs/op
BenchmarkShuffle-4                          320906             18466 ns/op               0 B/op               0 allocs/op
BenchmarkStringCompress-4                     2468           3261636 ns/op          876280 B/op              35 allocs/op
BenchmarkEncryptAndDecrypt-4                563416             10802 ns/op            1344 B/op              12 allocs/op
BenchmarkWrite2DevNull-4                   1368524              4353 ns/op             304 B/op               5 allocs/op
BenchmarkWrite2TmpFile-4                    521224             11328 ns/op             128 B/op               1 allocs/op
BenchmarkLocalPipeReadWrite-4               272166             20679 ns/op            2193 B/op              18 allocs/op
BenchmarkSerialNetRequest-4                    435          13852948 ns/op           40875 B/op             494 allocs/op
BenchmarkConcurrentNetRequest-4               1730           3471552 ns/op          136226 B/op             992 allocs/op
PASS
ok          benchmark        77.277s

数据存储

如果采集每个调用的全量链路的所有数据,存储开销必然是非常巨大的,可以考虑采样,并且只采集关键的出入参数据,或者仅采集方法链路和代码块覆盖数据。同时可以考虑冷热分离,全量数据采用廉价的离线存储,而实时数据则限定有效时间,定期清理和压缩归档。也可以考虑只存储聚合计算之后的数据,而不存储每一个调用的明细数据,比如汇总和去重存储调用链。具体策略可以根据应用场景调整。

主要应用场景

覆盖率采集

除了能支持基本的增量、全量、分支覆盖率之外,还可以实现更为精细化的覆盖数据采集。比如

  • 链路覆盖率

    • 与传统的分支覆盖率不同的是,分支覆盖率分母是所有条件的笛卡尔积,实际其中很多分支链路是不可达的,因此无法准确给分支覆盖率一个合理的目标。而链路覆盖率,分母可以是线上环境和测试环境累积的所有可达链路,分子是本次回归覆盖的所有链路,因此可以以100%为近似的覆盖目标(有可能无法达到100%是因为分母中的部分链路可能已经失效,这里就需要考虑数据的保鲜策略了)。
  • 分场景覆盖率

    • 比如区分自动化还是手工测试的覆盖,甚至可以二次开发,将覆盖数据与测试账号绑定,明确具体是哪个场景、哪个用户造成的覆盖。

影响面评估

影响面评估的核心基础是callgraph或控制流图。主流的方案是基于静态分析,但静态分析除了算法本身准确性之外,一些运行时决策的调用,比如反射,比如将一组方法放在slice、map中,运行时计算key进行的调用,静态分析是完全无法分析出来的,此时基于Ellyn动态收集的链路数据可以作为有效补充。基于动静结合的方式可以有效提升影响面评估的准确率(查准率)和召回率(查全率)。

链路观测

支持采集单个单元测试\自动化测试\流量的调用链明细数据,包括函数调用链、方法出入参、耗时、error/panic、行覆盖等信息,并将其绑定到一个链路ID上(可以是logid/traceid等)。进一步可以基于链路ID将全链路的数据串联起来。

最直接的应用场景就是基于可视化页面,帮助研发和QA同学在测试环境定位联调测试问题。相对于基于日志定位问题更加直观。

单测生成

由于可以全量采集所有方法的出入参和方法调用链,因此可以基于累积的数据,辅助生成单元测试。比如按照单测试AAA模式

  • Arrange(准备)

    • 基于采集的入参,构造请求参数
    • 基于方法调用链以及下游函数的出入参,生成下游函数调用的mock
  • Act(执行):执行单测

  • Assert(断言)

    • 基于采集的出参,对返回结果生成断言语句

精准测试

Ellyn可以将单个用例的覆盖数据绑定到一个链路ID上(logid/traceid等),因此,只需要进一步建立链路ID和用例的关系,就可以间接建立用例与代码方法或代码块的映射关系(知识库)。在用例推荐时,只需要对变更版本和线上版本进行Function Diff或者Block Diff,再基于Diff结果反查知识库,即可实现函数级精准(成本更低)或者代码块级(裁剪率更高)精准。

而建立用例和链路ID的关系往往很容易做到,如自动化用例、单元测试等,在执行前后我们都可以很容易从上下文拿到链路ID,而对于手工用例,则可以通过录制工具来绑定这个关系。

与基于传统覆盖率方案实现的精准方案不同的是,Ellyn实现精准可以更精确,并且很容易可以做到代码块级别,可以获得远高于方法级精准的裁剪率。

Mock平台

Ellyn插桩的本质是在所有方法内插入语句,因此可以拦截方法的执行。插桩过程会遍历项目,获取项目中的所有方法标识、参数类型列表等元数据,可以进一步实现一个基于方法标识+实际参数匹配的规则引擎,在任意方法维度配置mock规则,插桩代码检查是否命中mock规则,命中则直接返回。可以实现方法级mock,比服务粒度的mock灵活度更高。

风险分析

可以基于插桩采集的链路数据,分析程序中潜在的风险,包括但不限于稳定性、资损防控、隐私合规等。

比如稳定性方面

  • Ellyn采集的链路数据包含链路是否有异步调用,以及各异步链路的出入参,通过分析异步链路的出参结果是否影响主链路的出参结果,可以识别该异步链路是否为弱依赖。

  • 可以基于动态采集的链路数据结合静态分析数据得到一份非常完整的流量(链路)大图,可以应用在容量治理、红蓝攻防的爆炸半径分析等。

再比如,可以收集运行时的panic信息,包括panic发生时的堆栈信息,调用链信息、出入参数等等,帮助研发定位panic根因,降低线上panic风险。

项目地址

https://github.com/lvyahui8/ellyn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/868696.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[megatron代码阅读] 1. 初始化和组网

以pretrain_gpt.py为例, 看megatron的整体逻辑. 本章主要包括megatron初始化相关逻辑, 核心函数为initialize_megatron, setup_model_and_optimizer两个 initialize_megatron parse_args 从argparse中直接读取超参数配置. 如学习率, 正则化等. 从环境变量中获取rank等 load_arg…

巧用VTable打造炫酷金字塔图表

在数据分析和可视化领域,表格是展示数据直观、有效的方式之一。今天,就让我们来探索如何利用VTable这个强大的表格组件,制作出既美观又富有信息量的金字塔图表,以及深入了解VTable中各种单元格类型的使用方法,让你的表格也能“绘”出精彩图表!在数据分析和可视化领域,表…

基于 Performace 分析事件循环

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。本文作者:千寻什么是事件循环? 我们为什么需要事件循环?对于 JavaScript 是一门单线程语言我们是肯定的,JavaScript 单线程的特性保证了渲…

万字图文:SaaS业务架构、价值流、业务能力、业务流程、业务对象、组织架构

大家好,我是汤师爷~ 本文为读者提供一个SaaS业务架构的系统性框架,探讨业务架构分析的核心要素,帮助SaaS企业深入剖析目标客户的业务模式,全面理解他们的业务架构。 无论你是SaaS创业者、产品经理还是架构师,本文内容都将为你的系统设计和决策提供帮助。 1 目标与步骤 Saa…

老奶奶看了都会的WSL2连接USB设备教程!

老奶奶看了都会的WSL2-Ubuntu连接USB设备教程!作者:SkyXZ CSDN:SkyXZ~-CSDN博客 博客园:SkyXZ - 博客园参考资料:微软官方文档连接 USB 设备 | Microsoft Learn在Win11上用WSL2安装Ubuntu来开发简直不要太爽!!!但是很多小伙伴会发现,欸~为什么我在宿主机上插入的USB设…

HighReport报表工具V4.0带来十大核心优势变化

1.概述经过一年时间产品升级研发,HighReport报表工具正式推出V4.0版本,报表算法和报表功能获得全面提升。HighReportV4.0带来全面质的飞跃,具有明显的产品优势。 2.亮点一:双父格扩展模型报表引擎核心算法是父子格扩展模型,下面是常见模型一般报表厂商下面的扩展模型是不支…

一个超经典 WinForm,WPF 卡死问题的终极反思

一:背景 1. 讲故事 写这篇文章起源于训练营里一位朋友最近在微信聊到他对这个问题使用了一种非常切实可行,简单粗暴的方式,并且也成功解决了公司里几个这样的卡死dump,如今在公司已是灵魂级人物,让我也尝到了什么叫反哺!对,这个东西叫 Harmony, github网址: https://gi…

nginx 简单实践:静态资源部署、URL 重写【nginx 实践系列之一】

本文为 nginx 简单实践系列文章之一,主要简单实践了两个内容:静态资源部署、重写,仅供参考。〇、前言 本文为 nginx 简单实践系列文章之一,主要简单实践了两个内容:静态资源部署、重写,仅供参考。 关于 Nginx 基础,以及安装和配置详解,可以参考博主过往文章: https://…

题解:AT_abc353_f [ABC353F] Tile Distance

[ABC353F] Tile Distance 题解 cnblogs 题目传送门:洛谷,Atcoder Solution 很恶心人的分类讨论题。 很显然走大格子大概率比走小格子快。 对终点和起点向上下左右枚举大格子,我们就把问题转化为给两个大格子 \((a,b)\)、\((c,d)\),求怎样走最快。 对角的大格子可以通过 \(2…

数字化转型中的项目管理优化:协作工具的优势与应用

一、企业数字化转型的背景与挑战 1.1 数字化转型的驱动力数字化转型是指企业通过采用数字技术、创新流程和业务模式,提升运营效率、创造新价值并优化客户体验。随着云计算、大数据、人工智能和物联网等技术的不断发展,数字化转型已成为企业实现长期竞争力和持续增长的重要战略…

rk3568屏幕抖动问题

问题描述:有时候操作屏幕界面,发现屏幕有抖动的情况。经跟RK原厂沟通,此问题跟给ddr供电的vdd_logic有关系。vdd_logic默认定义:vdd_logic: DCDC_REG1 {regulator-always-on;regulator-boot-on;regulator-min-microvolt = <500000>;regulator-max-microvolt = <13…

B@se-还原错误字母表转码的base64编码

题目: 密文:MyLkTaP3FaA7KOWjTmKkVjWjVzKjdeNvTnAjoH9iZOIvTeHbvD== JASGBWcQPRXEFLbCDIlmnHUVKTYZdMovwipatNOefghq56rs****kxyz012789+/oh holy shit, something is missing... 第一行是密文,有明显的Base64编码特征(等号结尾) 第二行是大小写字母、数字、+、/,有明显的…