最近的工作中需要大量使用 Litmus 来生成故障来进行注入,对系统进行破坏,总算是把 Litmus 配置完毕后开始一步步进行 workflow 的开发了。
但是在进行 pod-io-stress 的使用中就发现很奇怪,注入根本没成功,日志也显示没什么错误,只是提示进程被杀死了,helper pod 失败。
这就感觉很诡异了,想了想和以前用的时候唯一的不同是这个路径是挂载的 PVC,会是这个问题吗?于是继续排查。
检查了下 litmus-go 这个 repo 中的源代码,我使用的是 2.14,所以我直接git checkout 2.14.1
这个 tag 了。然后找到了这里,这就是那个 helper 的调用了。可以很清晰的看到他们是用了nsutil
这个命令,然后再运行了 stress-ng 来进行磁盘的压力读写操作。
所以我首先排除了 stress-ng 命令行的错误的问题,在目标 pod 的容器中直接运行了 stress-ng 的命令和参数,确认了是可以正常运行的。
那么问题就在 nsutil 了,估摸着应该是一个他们自己写的用来进入容器 namespace 的工具,于是我就看到了他们的nsutil repo。那么事情就简单了,直接看源代码。
其实 main 就一个简单的调用,主要内容全在root.go中。不过一看吓一跳,他们没有 mnt 的参数,那怎么能对目标进程的文件系统进行操作呢?答案当然是不可能的。那我想问题应该也很简单吧,修改一下这
var ns = []string{"net", "pid", "cgroup", "uts", "ipc"}
改成
var ns = []string{"net", "pid", "cgroup", "uts", "ipc", "mnt"}
然后加上新的 cli 参数
func init() {
rootCmd.PersistentFlags().BoolVarP(&nsSelected[0], "net", "n", false, "network namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[1], "pid", "p", false, "pid namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[2], "cgroup", "c", false, "cgroup namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[3], "uts", "u", false, "uts namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[4], "ipc", "i", false, "ipc namespace to enter")
rootCmd.PersistentFlags().IntVarP(&t, "target", "t", 0, "target process id (required)")
err := rootCmd.MarkPersistentFlagRequired("target")
if err != nil {
log.WithError(err).Fatal("Failed to mark required flag")
}
}
改成
func init() {
rootCmd.PersistentFlags().BoolVarP(&nsSelected[0], "net", "n", false, "network namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[1], "pid", "p", false, "pid namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[2], "cgroup", "c", false, "cgroup namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[3], "uts", "u", false, "uts namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[4], "ipc", "i", false, "ipc namespace to enter")
rootCmd.PersistentFlags().BoolVarP(&nsSelected[5], "mnt", "m", false, "mnt namespace to enter")
rootCmd.PersistentFlags().IntVarP(&t, "target", "t", 0, "target process id (required)")
err := rootCmd.MarkPersistentFlagRequired("target")
if err != nil {
log.WithError(err).Fatal("Failed to mark required flag")
}
}
最后,我对这个项目进行了编译GOOS=linux GOARCH=amd64 go build
。之所以要这么写因为 Mac 肯定是没法直接调用 setns 函数的。
一切就绪,我将编译后的 nsutil 拷贝进一个测试用的 pod 里,参数什么的全和之前的 helper pod 保持一致,然后敲入命令行,成功了吗?并没有!提示我
invalid argument, ns-type mnt, Failed to setns
怎么回事?感觉检查路径,没错啊?那是哪里出了问题?经过我一番检索后,终于找到了问题,原来是 Go 的问题。
Go 的 runtime 在启动后就会启动多个线程,mnt 对于进程中有多线程的程序禁止 enter,所以才会有这个问题,所以 Go 确实是没法 enter mnt 的,即使调用了runtime.LockOSThread()
。
A process can't join a new mount namespace if it is sharing filesystem-related attributes (the attributes whose sharing is controlled by the clone(2) CLONE_FS flag) with another process.
至此我也总算是搞明白了为什么他们的nsutil
没有 mnt 的选项,因为使用纯 Go 根本做不到。
那么接下去就是该解决这个问题了,如果硬要用 Go,那该怎么办?我想到了 runc 的 nsenter,但是很不幸,这东西基本就是个套着 Go 的壳子的 C 程序,和直接写 C 基本没什么太大区别了。而且 nsenter 还有个问题,就是无法在目标 namespace 中执行没有的命令。
然后我想到了chaos-mesh项目中用到的 nsexec,这个我以前也用过,是用 Rust 写的,应该没什么问题也能满足我的需求,而且我也不需要再造轮子了,那么现在的问题就变成了直接改造 Litmus 的 go-runner。
那么我们再一次回到这里。只是这次我把它更新成了
stressCommand := fmt.Sprintf("pause nsexec -m /proc/%d/ns/mnt -c /proc/%d/ns/cgroup -i /proc/%d/ns/ipc -n /proc/%d/ns/net -p /proc/%d/ns/pid -l", targetPID, targetPID, targetPID, targetPID, targetPID) + " -- " + stressors
然后我们将nsexec
和libnsenter.so
与 go-runner 代码一起编译打包成 docker image。你可能需要从ns-exec的 release 中找到已经发布的版本。
再次启动那个 helper pod,只是这次我将LIB_IMAGE
这个环境变量改成了我已经打包好的 image。然后再次运行 pod-io-stress,这次就没什么问题了。能从监控指标中看到 IO Throughput 在不停的上升。而且目标的 pod 中的容器并没有 stress-ng 命令行工具,也可以正常的使用。
源代码依然是最可靠的东西。Litmus 的 pod-stress-io 看来一直就没可用过,就因为这个问题。但是在知道了原因后解决起来也不是什么很困难的事。了解操作系统的一些知识目前来看,对于测试开发人员来说还是必不可少的。
相关的代码都可以在我的repo中找到,欢迎直接 fork。