词语解释

Ellyn要解决什么问题?

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

常见的覆盖率工具实现

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

var flags []bool

func method() {
flags[0] = true
n := 100
k := 10
sum := 0
for i := 0 ; i < n ; i ++ {
if i % k == 0 {
flags[1] = true
sum += i
} else {
flags[2] = true
sum += 1
}
}
flags[3] = true
println(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, // 0
1, // 1
1, // 2
1, // 3
1, // 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 := 100
k := 10
sum := 0
for 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 gocbuild4fa554c51f8f var GoCover_0_396638376133663931613965 = struct {
Count [6]uint32
Pos [3 * 6]uint32
NumStmt [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, // 0
1, // 1
1, // 2
1, // 3
1, // 4
1, // 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

Ellyn-Golang调用级覆盖率&方法调用链插桩采集方案的更多相关文章

  1. 微信的API都是通过https调用实现的,分为post方法调用和get方法调用。不需要上传数据的采用get方法(使用IntraWeb开发)

    首先需要明确的是,微信的API都是通过https调用实现的,分为post方法调用和get方法调用.不需要上传数据的采用get方法(例如获取AccessToken),而需要向微信服务器提交数据的采用po ...

  2. CLR 虚方法调用和接口方法调用

    不知接口方法和虚方法分发有什么区别?似乎在CIL中都是callvirt指令. 对,MSIL里都是callvirt,但JIT的时候得到了不同的处理:对虚方法的分发是编译成这样: mov  ecx, es ...

  3. Javsssist用InsertAt()方法对语句插桩

    基于上一篇的方法插桩,这一篇则是进一步的对每行的语句进行插桩. 对于存在分支的方法(例如if(){}else{}),对方法插桩的方法是不能够全部涉及到的.所以要对程序的每条语句进行插桩. 插入什么语句 ...

  4. Struts2之action 之 感叹号 ! 动态方法调用

    struts2的动态方法调用的方式: 1.第一种方式:设置method属性 在Action类中定义一个签名与execute方法相同.只是名字不同的方法,如定义为: public String logi ...

  5. 2015/9/21 Python基础(17):绑定和方法调用

    绑定和方法调用现在我们需要再次阐述Python中绑定(binding)的概念,它主要与方法调用相关联.方法是类内部定义的函数,这意味着方法是类属性而不是实例属性.其次,方法只有在其所属的类拥有实例时, ...

  6. Action方法调用

    一.Action访问路径 Action的访问路径是由struts.xml文件中配置的Action所在包的命名空间,Action的名字和常struts.action.extension共同决定的 例如: ...

  7. JVM(十二):方法调用

    JVM(十二):方法调用 在 JVM(七):JVM内存结构 中,我们说到了方法执行在何种内存结构上执行:Java 方法活动在虚拟机栈中的栈帧上,栈帧的具体结构在内存结构中已经详细讲解过了,下面就让我们 ...

  8. 【Java基础】方法调用机制——MethodHandle

    MethodHandle是Java7引入的一种机制,主要是为了JVM支持动态语言. 一个MethodHandle调用示例 共有方法调用 首先,演示一下最基本的MethodHandle使用. 第一步:创 ...

  9. 第47篇-解释执行的Java方法调用native方法小实例

    举个小实例,如下: public class TestJNI { static { // 程序在加载时,自动加载libdiaoyong.so库 System.loadLibrary("dia ...

  10. 《Spring技术内幕》学习笔记17——Spring HTTP调用器实现远程调用

    1.Spring中,HTTPInvoker(HTTP调用器)是通过基于HTTP协议的分布式远程调用解决方案,和java RMI一样,HTTP调用器也需要使用java的对象序列化机制完成客户端和服务器端 ...

随机推荐

  1. cnblogs的GitHub同步markdown文件的blog如何识别文章的唯一性(身份ID如何判定)

    本篇blog是写在GitHub的对应的仓库中的. cnblogs会给终身用户提供一个把GitHub仓库中的markdown文件同步到cnblogs上的一个服务,本文就是使用这个服务同步到个人blog地 ...

  2. 使用wxpython开发跨平台桌面应用,动态工具的创建处理

    在我们开发系统的时候,往往需要一个很容易理解功能的工具栏,工具栏是一个系统的快速入口,美观易用的工具栏是可以给系统程序增色不少的,本篇随笔介绍在使用wxpython开发跨平台桌面应用,工具栏的动态展现 ...

  3. VL4AD:让语义分割认识未知类别,无需额外数据和训练的OOD语义分割 | ECCV'24

    来源:晓飞的算法工程笔记 公众号,转载请注明出处 论文: VL4AD: Vision-Language Models Improve Pixel-wise Anomaly Detection 论文地址 ...

  4. git clone 远程代码执行漏洞(CVE-2024-32002) 升级

    接到提醒说git有个漏洞,Git clone 远程代码执行漏洞(CVE-2024-32002) 看了看,说是git的Symlinks模块存在高危漏洞,攻击者可以利用该漏洞执行任意代码,导致服务器失陷. ...

  5. 【Spring】IOC核心源码学习(二):容器初始化过程

    接上文 啃啃老菜: Spring IOC核心源码学习(一) ,本文将以 ClassPathXmlApplicationContext 这个容器的实现作为基础,学习容器的初始化过程. ClassPath ...

  6. hashcode和equals为何要同时重写

    浅谈为何要重写 hashcode()与equals() 首先,这两个方法都来自于Object对象,根据API文档查看下原意.(1)public boolean equals(Objectobj),对于 ...

  7. 深入理解ReferenceQueue GC finalize Reference

    关于对象如何销毁以及finalize更详细的信息 目录 概述 1 先看一个对象finalize的顺序问题. 2 对象再生及finalize只能执行一次 3 SoftReference WeakRefe ...

  8. 推荐UML插件Green UML、AmaterasUML

    项目上要求release时需要同时给出详细的类关系图,可惜本人之前只是使用XMind手工画过很简单的类关系图(只是类的继承关系),可苦了我呀. 这两天一直在网上查找能够在Eclipse 已有的代码基础 ...

  9. 用VuePress在GitHub Pages上搭建博客

    请先点击链接RobinDevNotes,体验用VuePress搭建博客的效果(logo还没有合适的替换),目前部署在GitHub Pages上,国内访问速度还可以,再阅读本文感受来龙去脉和搭建过程. ...

  10. NET 6 中新增的LINQ 方法

    .NET 6 中添加了许多 LINQ 方法. 下表中列出的大多数新方法在 System.Linq.Queryable 类型中具有等效方法. 欢迎关注 如果你刻意练习某件事情请超过10000小时,那么你 ...