硬核!如何在容器中做时间的漫游者
题目稍有些标题党,最近公司想用 chaos-mesh
对 k8s
做混沌测试,开始做前期的调研,发现 pingcap 对时间的注入非常硬核,而且最终方案居然是实习生构思出来的 ^^
TL;DR: 通过劫持 vdso
, 将时间函数跳转到 hack 过的汇编指令来实现 time skew
. 原理不难懂,但细节超多,参考官方文档
为什么需要 time skew
可以参考 Chaos Mesh - 让时间在容器中自由摇摆, 简单来说就是:
分布式数据库要实现全局一致性快照,很多方案使用时间做逻辑时钟,所以需要解决不同节点之间时钟一致的问题。但往往物理节点上的物理时间总是会出现偏差,不管是使用 NPT 服务同步也好,或者其他方法总是没办法完全避免出现误差,这时候如果我们的应用不能够很好的处理这样的情况的话,就可能造成无法预知的错误。
其实这很符合工程设计哲学:design for failure
, 任何一个硬件或是软件都会有错误(fault),系统如何在不影响对外提供服务的前提下,如何处理这些故障,就是我们常说的 fault tolerance
但是对于非金融业务来说,时间偏移一点影响并不大,相比其它 chaos, time的场景还是受限一些
如何注入
从实体机的经验来看,所谓的混沌测试都比较直观的,比如用 tc
做网络的丢包,限速来模拟网络故障,使用 stress
模拟 cpu 压力。但是在容器中做如何模拟 time skew
呢?
如果直接使用 linux date
命令修改,会影响到宿主机上其它所有容器。有没有方法能只影响某个容器?
之前发过一篇文章 时钟源为什么会影响性能, 从中可以看到,go 调用系统时间函数时,会先调用 vdso
的代码,如果时钟源符合条件,直接在用户空间完成,并不会进入内核空间,所以针对这一现象,syscall
劫持的方法就不能使用了
那么是否可以直接修改 vdso
段代码呢?
查看 vdso
1 | cat /proc/1970/maps |
可以看到 vdso
代码段的起始逻辑地址,同时注意权限位是 r-xp
, 这就意味着用户态的进程是无法直接修改该内容。
真的就没办法了嘛?有的,ptrace 法力无边
The ptrace() system call provides a means by which one process
(the “tracer”) may observe and control the execution of another
process (the “tracee”), and examine and change the tracee’s
memory and registers. It is primarily used to implement
breakpoint debugging and system call tracing.
ptrace
提供了一种修改和观察其它进程的手段,包括修改内存值和寄存器,巧了这些 chaos-mesh 都用到了。如何实现 go 调试器 这篇文章也讲了 ptrace 的用途,很棒的文章。
整体实现
chaos-flow.png
这就是简单的流程图,主要代码都是 time_linux_amd64.go, 当前仅支持 linux amd64 平台,不支持 Windows/MacOS
1 | // ModifyTime modifies time of target process |
代码写上了注释,分别对应上面的流程图。下面分解来看。
1. Ptrace
1 | type TracedProgram struct { |
TracedProgram
结构体比较简单,pid 是待注入 chaos 的进程 id, 同时 tids 保存所有的线程 id, Entries 是进程逻辑地址空间,
Trace
函数在代码 ptrace_linux_amd64.go 中
通过读取 /proc/{pid}/task
获取进程的所有线程,然后分别对所有线程执行 linux ptrace
调用。然后生成 Entries, 什么是 Entry 呢?就是上文提到的 /proc/{pid}/maps
内容
1 | ...... |
2. Mmap FakeImage
查找 vdso, 如何失败,直接退出。一般 vdso 都在最后,所以从尾开始遍历
同时还要查找 fakeEntry
, 如果存在,直接复用。否则会造成内存泄漏,当然了,一直创建新的 fakeEntry
…..
program.MmapSlice 用于创建 fakeEntry, Mmap 分配内存,然后将 fakeImage (新的汇编代码) 写到这块内存中。
1 | // MmapSlice mmaps a slice and return it's addr |
注意,这不是简单的调用 Mmap Syscall !!! ptrace.Syscall 是利用 ptrace 控制进程,让目标进程单步执行 syscall
1 | // Syscall runs a syscall at main thread of process |
参考代码的注释,搞过嵌入式的肯定熟悉:保存寄存器现场,设置新的寄存器值为 syscall number 以及参数,最后设置指令寄存器 rip 单步执行,就完成了让目标进程执行 mmap 的操作,最后也要恢复寄存器,还原现场。
这里为什么 rip 寄存器要设置成 0x050f
呢???其实这是 syscall 的操作码
另外 p.WriteSlice
是使用 syscall process_vm_writev
将数据写入目标进程的内存逻辑地址空间。
3. FindSymbolInEntry
1 | file /tmp/vdso.so |
FindSymbolInEntry
函数很简单,就是要找到 clock_gettime
在 vdso 中的地址,参考我之前的文章,上面是 dump 出来的符号表
4. JumpToFakeFunc
1 | // JumpToFakeFunc writes jmp instruction to jump to fake function |
JumpToFakeFunc, 修改 vdso 符号表中的汇编代码,使所有调用 clock_gettime
的都跳转到我们 fakeEntry 的地址,劫持 vdso
FakeImage
1 | var fakeImage = []byte{ |
fakeImage
最后三个参数是偏移量,以及传递的 CLOCK_IDS_MASK, 这些汇编是什么意思呢???
查看汇编操作码,0xe4 是系统调用 clock_gettime
的操作码,后续都是对结果进行注入,要么增要么减,制造偏移量 time skew
测试案例
1 | git clone https://github.com/chaos-mesh/chaos-mesh |
首先下载 chaos-mesh, 然后编译 watchmaker
, 这是一个方便注入的小工具。
1 | package main |
上面是测试的代码,每隔 20 打印当前时间,编译执行,同时用 watchmaker 注入 time skew
1 | ./watchmaker -pid 1970 -sec_delta -300 |
隔一段时间间后,再次执行停止执行注入
1 | ./watchmaker -pid 1970 -sec_delta 0 |
1 | ./test |
上面是代码执行的输出,可以看到 03:32:26
之后时间变成了 03:27:46
, 停止注入后恢复
Limits
当前的实现,停止注入,并不会还原 vdso 代码,也就是说 fakeEntry 会一直存在,每次 clock_gettime
都会跳转,只不过偏移量为 0 而己
由于以上原因的存在,注入及注入之后的 clock_gettime
都是走的 syscall 系统调用,性能很慢,敏感业务需要重启,细节可以参考我之前的文章《时钟源为什么会影响性能》
当前注入,只能针对容器里的主进程,那些 fork 出来,派生出来的无做做到注入
小结
这次分享就这些,以后面还会分享更多的内容,如果感兴趣,可以关注并点击左下角的分享
转发哦(: