golang 下对时间漂移的故障注入

很多时候我们在做测试时候,需要对程序的时间或者定时器进行一些操纵,各个语言对于获取时间和定时器的实现都不尽相同,在网上找了一下,发现有一个chaos-mesh的项目里面提供了一个对golang和rust程序注入故障的方案。

要想能针对某个进程进行时间漂移的注入,首先要知道这个进程里面对于获取时间的调用是怎么进行的。对于golang程序而言,当我们调用time.Now()时,实际上是利用了vDSO (virtual dynamic shared object)机制,该机制可以让一些诸如gettimeofdayclock_gettime的系统调用更快,而golang的time.Now()或者golang的定时器实现中维护堆时使用的就是clock_gettime

知道了调用的函数,接下来就是需要想办法修改掉它,这里chaos-mesh使用的方案是通过ptrace去修改对应进程的内存空间,将clock_gettime的跳转改成一个自己的实现。

chaos-mesh还提供了一个测试程序的封装叫watchmaker,这个程序可以方便我们直接运行,只要直接输入pid和偏移量就可以完成注入。

主要功能的代码是func ModifyTime(pid int, deltaSec int64, deltaNsec int64, clockIdsMask uint64) error

大体流程如下

  1. 利用runtime.LockOSThread(),将当期goroutine和linux的thread绑定,这个我猜应该是防止该goroutine被切走,ptrace失效。
  2. ptrace.Trace(pid)attach到pid上。通过/proc/pid/task获取该进程的所有threads,并通过PtraceAttach syscall attach到每个tid上,同时通过/proc/pid/maps拿到该进程的虚拟内存映射信息。
  3. 通过关键字[vdso]找到vDSO entry的起始地址。
  4. 准备好一个fakeImage,里面直接写好了我们的跳转的程序,最后24 bytes是我们的三个参数。这块涉及汇编,没仔细看fakeImage里面的实现。
  5. 在当前进程查询到是否曾经注入过我们的fakeImage,如果没有那么通过mmap新map进去我们的fakeImage,否则找到我们之前的entry。把最后24 bytes改写为当前设置的参数。
  6. 找到vDSO entry中的clock_gettime的原始地址,这里需要利用golang的debug/elf包,来找到对应的symbol address。
  7. 把vDSO entry中的clock_gettime跳转修改掉。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // JumpToFakeFunc writes jmp instruction to jump to fake function
    func (p *TracedProgram) JumpToFakeFunc(originAddr uint64, targetAddr uint64) error {
    instructions := make([]byte, 16)

    // mov rax, targetAddr;
    // jmp rax ;
    instructions[0] = 0x48
    instructions[1] = 0xb8
    binary.LittleEndian.PutUint64(instructions[2:10], targetAddr)
    instructions[10] = 0xff
    instructions[11] = 0xe0

    return p.PtraceWriteSlice(originAddr, instructions)
    }

我们可以写个程序测试下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// go build -o test test.go
func main() {
pid := os.Getpid()
if len(os.Args) > 1 {
// normal process
fmt.Println(pid, "start count", time.Now())
t := time.NewTicker(time.Second)
go func() {
for _ = range t.C {
fmt.Println(pid, "tick", time.Now())
}
}()
} else {
// time shift
fmt.Println(pid, "30s ticker", time.Now())
j := 0
t := time.NewTicker(time.Second*30)
go func() {
for tick := range t.C {
j++
fmt.Println(pid, "trigger tick", tick, "now", time.Now(), j)
}
}()
}
select {}
}

为了观察方便,同时起两个test进程,一个带参数,一个不带。
./test & ./test x &

如果正常运行,那么输出结果会是这样的:

1
2
3
4
5
6
7
8
99335 start count 2021-04-26 15:14:40.290945165 +0800 CST m=+0.000102053
99334 30s ticker 2021-04-26 15:14:40.290987246 +0800 CST m=+0.000068288
99335 tick 2021-04-26 15:14:41.291190225 +0800 CST m=+1.000347008
99335 tick 2021-04-26 15:14:42.291188665 +0800 CST m=+2.000345428
// 忽略当中的一些打印
99335 tick 2021-04-26 15:15:10.291344814 +0800 CST m=+30.000501519
99334 trigger tick 2021-04-26 15:15:10.291371963 +0800 CST m=+30.000452968 now 2021-04-26 15:15:10.291389026 +0800 CST m=+30.000470067 1 // 这是第一次30s定时器超时
99335 tick 2021-04-26 15:15:11.291317403 +0800 CST m=+31.000474367

我们先等到99334进程触发了一次定时器超时之后,我们立刻通过watchmaker工具注入一个30秒的漂移。
sudo ./bin/watchmaker -pid 99334 -sec_delta 30 -nsec_delta 0 -clk_ids "CLOCK_REALTIME,CLOCK_MONOTONIC"

注意如果我们只是想影响程序中time.Now(),只修改CLOCK_REALTIME就行了。这里我们需要影响定时器的超时,根据golang的定时器实现,定时器数据结构中取时间使用的是runtime·nanotime1,而对应的汇编实现里面取的是CLOCK_MONOTONIC,所以我们修改的clockid除了CLOCK_REALTIME还需要包含CLOCK_MONOTONIC

程序会输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
watchmaker Version: version.Info{GitVersion:"master-ge885846a41fd88", GitCommit:"e885846a41fd88aa3a46f8b318321b8e889312b9", BuildDate:"2021-04-13T08:09:23Z", GoVersion:"go1.14.14", Compiler:"gc", Platform:"linux/amd64"}
2021-04-26T15:15:18.764+0800 INFO zapr@v0.1.0/zapr.go:69 get clock ids mask {"mask": 3}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99334}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99337}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99339}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99341}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99343}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99334}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99337}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99339}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99341}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99343}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 Successfully detach and rerun process {"pid": 99334}

这里也可以看到,一个普通的go程序也会启动很多线程。。。

然后回去看我们测试进程的输出变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
99335 tick 2021-04-26 15:15:10.291344814 +0800 CST m=+30.000501519
99334 trigger tick 2021-04-26 15:15:10.291371963 +0800 CST m=+30.000452968 now 2021-04-26 15:15:10.291389026 +0800 CST m=+30.000470067 1 // 这是上面第一次30s定时器超时
99335 tick 2021-04-26 15:15:11.291317403 +0800 CST m=+31.000474367
99335 tick 2021-04-26 15:15:12.291291676 +0800 CST m=+32.000448352
99335 tick 2021-04-26 15:15:13.291336311 +0800 CST m=+33.000493035
99335 tick 2021-04-26 15:15:14.291333735 +0800 CST m=+34.000490470
99335 tick 2021-04-26 15:15:15.29133939 +0800 CST m=+35.000496121
99335 tick 2021-04-26 15:15:16.291349531 +0800 CST m=+36.000506260
99335 tick 2021-04-26 15:15:17.291232264 +0800 CST m=+37.000389035
99335 tick 2021-04-26 15:15:18.291307312 +0800 CST m=+38.000464034
99334 trigger tick 2021-04-26 15:15:48.765121761 +0800 CST m=+68.474202738 now 2021-04-26 15:15:48.765129654 +0800 CST m=+68.474210641 2 // 由于我们修改了时间,里面触发了第二次的30s定时器超时
99335 tick 2021-04-26 15:15:19.291362269 +0800 CST m=+39.000519076 // 这里可以看见99335对比进程中的时间还是正常的

注意golang的默认的time这个数据结构的打印,m=+后面的数字就是进程启动后的monotinic时间,这里99334进程的monotinic时间一下子从38变成了68,所以立马触发了第二次超时。

至此,我们就完成了对一个golang进程的时间漂移的注入,赶快去测试下生产代码中是否有问题吧,据chaos-mesh团队的分享中所说,很多开源项目均有或大或小的问题哦。

golang 下反射 plugin 中的类型实例实现动态注入

设想一个场景,我们需要和其他团队配合一起开发,并且不想使用源码构建,也就是说最终希望通过集成对方发布的二进制的方式来完成部署。

那么我们有两种方式可以解决这个问题:

  • 定义进程间api,各自构建进程,通过进程间调用
  • 定义接口,实现者编译成动态库,运行时通过某些机制加载动态库然后再想办法获取到对应的实现的实例

对于c语言来说其实编译成动态库的方式是很自然的,但是golang的话就需要利用plugin机制。

首先我们需要使用go build -buildmode=plugin来将一个包含main包的代码编译成动态链接库。

1
2
p, _ := plugin.Open("plugin_name.so") // open so
f, _ := p.Lookup("routerFactory") // 查找符号

注意这里的plugin.Open并不会执行main包,而是仅仅会去调用所有包init函数。

p.Lookup("routerFactory")可以查找到名字为routerFactory的全局变量,我们可以要求实现者以一个固定的全局变量来实现我们的接口,接下来我们只需要断言到自己的interface,就可以调用了。注意,只有导出的变量或者函数符号才能被查找到。

如果我们想更加灵活一些,我们希望能查找未导出的符号,或者通过包名+类型名反射出实例,比如我们需要使用方法而不是全局的函数,那咋办。

这时候我们就要掏出抄代码大法了,go的reflect包内里面其实有这段代码typesByString

我们可以利用go:linkname来链接到reflect包内的实现,我们只用申明签名即可。注意使用这个需要import unsafe

1
2
//go:linkname typesByString reflect.typesByString
func typesByString(s string) []*_type

为了完整的实现,我们还需要从源码里面拷贝一些结构体和方法出来,比如type的定义,pkgpath()方法等,不用完整拷贝,调用的到的片段弄出来就行。

最后把这些骚代码封装到我们的internal包内,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//go:linkname typesByString reflect.typesByString
func typesByString(s string) []*_type

func searchForType(pkgPath, typeName string) (*_type, error) {
tbs := typesByString("*" + typeName)
for _, tb := range tbs {
p := (*ptrtype)(unsafe.Pointer(tb))
path := p.elem.pkgpath()
if path == pkgPath {
return &p.typ, nil
}
}
return nil, fmt.Errorf("%s, %s not found in executable image", pkgPath, typeName)
}

func Instantiate(pkgPath, typeString string, isPtr bool) (reflect.Value, error) {
res, err := searchForType(pkgPath, typeString)
if err != nil {
return reflect.Value{}, err
}
var emptyFace interface{}
ptr := (*eface)(unsafe.Pointer(&emptyFace))
ptr._type = res
ty := reflect.ValueOf(emptyFace).Type().Elem()
if isPtr {
e := reflect.New(ty)
return e, nil
}
e := reflect.New(ty).Elem()
return e, nil
}

对外就暴露个api即可。

1
2
// Instantiate returns an instance of a given full-package-path type string.
func Instantiate(typ string) (interface{}, reflect.Value, error)

测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
"testing"

"github.com/titanxxh/di/example"
_ "github.com/titanxxh/di/example/en"
)
func TestInstantiate(t *testing.T) {
{
a, _, err := Instantiate("github.com/titanxxh/di/example/en.english")
if err != nil {
panic(err)
}
fmt.Println(a.(example.Greeter).Hello())
}
}

然而并不能正确反射出来,报github.com/titanxxh/di/example/en, en.english not found in executable image,这就很奇怪了,我明明匿名引入了啊。

最后经过一番调查才发现,go会优化掉没使用的类型,我们需要搞个全局变量引用到自己的类型一下才行。。。比如这样。

1
2
3
4
5
6
var avoidOpt interface{}

func init() {
// use this symbol to avoid compiler optimization
avoidOpt = english{}
}

于是我们终于正确获得输出hello

再结合之前的plugin,就是我们的example啦。

项目地址在此,求个star呗。

压缩算法笔记

Know how to solve every problem that has been solved.

What I cannot create I do not understand.

–Richard P. Feynman

上个月因为公司内部的比赛,被迫短时间内了解了一些压缩算法,还动手实现了一些,比如lz77,deflate,bwt,bcm等,不实践不知道,一写代码就发现有些东西你以为你懂了实际上你没懂,加上最近看了已故物理学大师理查德费曼的一系列视频,其中一个细节让我印象深刻,他去世后大家在他办公室的黑板的左上角(这样就可以防止不小心被擦掉)发现他一直保留着上面的两句话。

理查德费曼很小的时候就受到他父亲的教育,明白了知道和理解是两个概念,所以他一直能保持好奇的心态去思考每个问题。
这两句话的本质是一样的,也就是——只有自己能做出来才算真正理解了,要做到这一点就需要知道每个问题背后是如何真正被解决的,而不是只知道个结论,所以理解一个概念的最高境界就是你能教会别人这个概念。

有感于此,特撰此文记录一下这段时间研究压缩算法的过程。

阅读更多...

GopherChina2020个人总结

如果你是新入坑的gopher那么建议一定要看一下Go Programming Patterns这个演讲。本次大会讲框架的比较多,听下来感觉go-zero做的比较完善,也更适合小公司或者个人上手,而且作者比较有激情。然后推荐那个Go编译器的、TiDB遇到的问题的、还有探探的。最水的是一个老外的Go in the Cloud - Why People Choose Go for Cloud Computing。。。

由于大会第二天分了两个会场,所以一个人只能听到部分的演讲,所以我在会场1和2反复横跳,挑着听了一些。比较遗憾的是阿里的那个EDAS的没听到,据说讲的比较好。

大会的PPT可以在这个git仓库找到

现场偶遇了前同事666,一开始带着口罩还没太确认,后来坐下来发微信才确认。

  1. 探探 ttdb
    探探是本次大会的联合主办方,是一个年轻人社交的app,呃,其实他们的现场的宣传片里面好像还有60多岁的使用者。如果让你设计一个系统,扫描两个人经过的地理位置,从而进行匹配出擦肩而过的人,你会怎么设计?
    他们介绍了自己的数据库的一些实践经验:

    • sql解析上的优化,比如做谓词下推等
    • 列存储,selection阶段利用cache coherence提升性能
    • 故障检测没有选择去中心化的gossip,而是使用的中心化的超时检测,故障恢复时候避免所有请求打到一个新的从节点导致峰值,而是偶尔将请求给从节点,保持热数据。
    • 利用这个库来进行goroutine的泄漏检测。
  2. Go Programming Patterns
    这个演讲里面的内容其实是我们平时代码里面都常会用到的一些小技巧还有一些控制反转等理念,尤其建议新接触Golang的同学都可以听一下,重点体会一下看开源代码学习好的编程模式的这种理念。另外,作者应该是比较熟悉各种语言的特性,所以对Golang也有很多吐槽。

    • functional option,这个应该应用是最为广泛的pattern了,几乎各大开源项目里面都用,很好地解决了可选参数必选参数区分,内部结构封装不破坏,使用时自注释等一系列问题。
    • go generation,因为不支持泛型(至少在2021年之前),所以很多时候利用好go generation是很必要的。
    • 错误处理,这里他提到rob pike的一个上古文章建议大家看看,但是我不确定他指的是哪一篇,不过go1.13新增加了errors.IsAs 两个用来帮助处理函数,同时fmt.Errorf里面支持通过%w来wrap一下error了。
    • kubernetes visitor、装饰器、pipeline等就看ppt吧,讲的比较清楚。
  3. Grab Food
    Grab也是GopherChina的老赞助商了,Grab是东南亚的超级巨无霸,可以理解为美团+支付宝+滴滴+高德地图。
    他们先是介绍了他们的Grab-Kit服务框架,和其他框架一样,支持各种middleware,validate、trace、throttling等,还支持chaos来模拟失败(捣乱猴?)
    然后介绍了Grab Food内部的ML Pipeline:

    1. 候选集合
    2. 过滤
    3. 重排序
    4. 后排序
      先引用Google的文章列了下当前机器学习的痛点,介绍了他们的chimera系统是如何做到持续集成、持续训练、持续交付的自动化的,做到了ML-ops。并提到了他们用了一种multi arm bandit AB test来解决模型的适应性问题。
  4. go-chassis
    这个是我司的人讲的,略过。

  5. Functional options and config for APIs
    这个演讲内容和前面第二个撞车了,看前面那个就行。

  6. 百度BFE
    这个老兄的因为主要不是什么技术型的演讲,所以我没怎么听就跑了,里面介绍了一些做技术管理的东西。

  7. go-zero
    又是一个搞框架的,不过这个老兄挺有意思,他说自己70后还在笔耕不辍的写代码。他这个go-zero是他们公司内部使用过程中迭代出来的东西,不是其他一些框架可能是脱离具体业务的产物。听下来感觉go-zero的功能还是很完善的,的确一站式解决了一些开发上的boilplate代码,而且他上来就是说我希望自己团队里面风格尽量统一,所以才有的这个东西。除了这个框架本身,我印象比较深的是他其中加入的一些自己产品中的解决问题方案。

    • 面向故障编程。
    • 不要join!
    • 如何正确的设计缓存,防止缓存穿透、击穿、雪崩。
    • 负载均衡上他提到了Power of Two Choices算法,这样既可以保证尽量多的就近选择,又动态控制不会把附近的服务处理时长搞得太长。基于这两篇文章12可以仔细研究下。
    • 自适应熔断,放弃了Netflix的Hystrix,介绍了他们用的Google SRE算法,支持自定义触发条件等。
    • 自适应降载。
  8. Go+
    由于布道师许式伟本人没来,找了个人临时讲,效果就不好,没有讲清楚为Go+替代Python搞数据科学的必要性,虽然堆了一堆PPT。。。Go+的运行可以使用转换成Go语言编译运行,也可以转换成字节码直接运行,性能差一点,感觉这个很鸡肋直接python不好么。而且当前不支持interface,没有runtime优化。

  9. PingCAP-Go runtime related problems in TiDB production environment
    这个演讲中提到的问题其实在我们工作中也遇到了,演讲者是golang internals的作者。
    第一个问题是golang的goroutine调度导致时延变大,其实这个我理解并不算是问题,或者说golang的goroutine调度实现就是如此,golang不像别的语言是原生支持了协程,同时也就绑定了他的实现,很多其他语言是通过库的方式来做的协程,调度器其实是可以根据需求来换实现的。所以如果要在go里面解决调度问题,要么就只能把高优先级的给绑核了。
    第三个问题是GC的bug
    第四个是NUMA的使用的建议,GC不是NUMA aware的,在allocation里面的帮助扫描触发之后,在众核环境下表现很差。这个现在版本已经优化了。

  10. Go语言编译器简介 史斌
    这个老兄就很硬核了,是给golang提交代码最多的50人之一,拥有Go的git库的提交权限,也年年收到go官方的邀请去美国开会。
    他提到了Go语言其实用的是自举,而不是走LLVM这种通用的方案,所以很多在LLVM那边做的优化,golang的编译器都没有,需要添加(大佬你的提交就是这样来的吧)。golang从源码到汇编有48道工序,他讲的话估计三天都不够。最后他选了前端、中端、后端的优化讲了一下自己的patch,还是挺有意思。

  11. GORM 剖析与最佳实践
    这个应该是国内做的最好的golang开源项目了,作者介绍了GORM2.0的重构和设计和一些最佳实践,感兴趣的可以仔细看下,当前暂时没用之后用了再回头看看。

  • Copyrights © 2011-2021 仙雾

请我喝杯咖啡吧~

支付宝
微信