cmd/go: test cache hash inputs include ModTime, often causing cache misses on CI

yhuiod9q  于 5个月前  发布在  Go
关注(0)|答案(7)|浏览(55)

你使用的Go版本是什么( go version )?

$ go version
go version devel go1.21-518889b35c Thu Feb 16 02:02:00 2023 +0000 linux/amd64

这个问题在最新版本的发布中是否重现?

是的,已通过Go 1.20验证。

你正在使用什么操作系统和处理器架构( go env )?

go env 输出

$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/mvdan/.cache/go-build"
GOENV="/home/mvdan/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/mvdan/go/pkg/mod"
GONOPROXY="github.com/cue-unity"
GONOSUMDB="github.com/cue-unity"
GOOS="linux"
GOPATH="/home/mvdan/go"
GOPRIVATE="github.com/cue-unity"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/home/mvdan/tip"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/home/mvdan/tip/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="devel go1.21-518889b35c Thu Feb 16 02:02:00 2023 +0000"
GCCGO="gccgo"
GOAMD64="v3"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/mvdan/src/nowt/go.mod"
GOWORK=""
CGO_CFLAGS="-O2 -g"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-O2 -g"
CGO_FFLAGS="-O2 -g"
CGO_LDFLAGS="-O2 -g"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3213309622=/tmp/go-build -gno-record-gcc-switches"

你做了什么?

我参与的许多项目都在GitHub Actions上使用go test ./...。其中大多数还使用GitHub的https://github.com/actions/cache来持久化Go的GOCACHEGOMODCACHE,以便连续的CI运行如果几乎没有代码更改,就可以避免很多工作。
这在很大程度上起作用,但我注意到那些完全没有更改任何代码(例如,只修改README)的提交只会显示一些包为(cached),而其他包总是从头开始运行。例如,参见cue-lang/cue#2253
经过相当多的调查与GODEBUG=gocachehash=1及其testInputs哈希值,并阅读了src/cmd/go/internal/test/test.go后,我认为原因是修改时间。每当测试打开一个文件时,Go的自动测试缓存会跟踪该文件的路径、文件信息和内容,以了解是否应该在未来重新运行测试。文件信息包括修改时间:

func hashWriteStat(h io.Writer, info fs.FileInfo) {
	fmt.Fprintf(h, "stat %d %x %v %v\n", info.Size(), uint64(info.Mode()), info.ModTime(), info.IsDir())
}

包括修改时间可能对测试缓存本身有用,而且通常在不断发展的机器上连续开发时不是一个问题。然而,CI有点特殊:它很常见地在临时环境中运行并从获取代码开始。特别是,GitHub Actions作为CI的使用方式往往从https://github.com/actions/checkout开始,它使用git clone将仓库下载到类似于/home/runner/work/foobar/foobar的路径。请注意,克隆目录的位置在每次运行之间不会改变;我最初怀疑这是问题所在,但事实并非如此。
问题在于git没有任何方式存储修改时间,而git clone在检出仓库时创建新文件,因此克隆的文件最终具有当前时间作为它们的修改时间。
作为一个复现者和演示示例,我已经编写了https://github.com/mvdan/nowt/tree/master/test-cache-open-modtime。它包含一个打开测试数据的Go测试和一个重现这种情况的脚本:

$ ./repro.bash 
Clone and test twice in a row, using the same directory and -trimpath. The test caching does not work.
-rw-r--r-- 1 mvdan mvdan 8 Feb 16 22:14 test-cache-open-modtime/testdata/foo.txt
ok  	mvdan.cc/nowt/test-cache-open-modtime	0.001s

-rw-r--r-- 1 mvdan mvdan 8 Feb 16 22:14 test-cache-open-modtime/testdata/foo.txt
ok  	mvdan.cc/nowt/test-cache-open-modtime	0.001s

The same again, but this time resetting the testdata file's mtime, since git clone otherwise sets it to the time it ran.
-rw-r--r-- 1 mvdan mvdan 8 Jan  1  2000 test-cache-open-modtime/testdata/foo.txt
ok  	mvdan.cc/nowt/test-cache-open-modtime	0.001s

-rw-r--r-- 1 mvdan mvdan 8 Jan  1  2000 test-cache-open-modtime/testdata/foo.txt
ok  	mvdan.cc/nowt/test-cache-open-modtime	(cached)

你期望看到什么?

我希望修改时间不影响Go的测试缓存命中与未命中决策。特别是因为Go很大程度上允许其开发者不知道修改时间以便增量构建能够良好工作,与其他构建系统如make不同。

你看到了什么?

大多数人在像GitHub Actions这样的流行平台上使用CI会导致不必要的Go测试缓存未命中,这是由于修改时间造成的。
我可以为我的项目通过跟随每个actions/checkout步骤的脚本来修复这个问题,该脚本可以遍历所有克隆的文件并将它们的修改时间重置为静态时间戳。然而,那感觉非常不幸,我想象许多Go用户甚至可能没有意识到他们的CI比一开始就应该慢得多。
我们还可以尝试说服第三方如git cloneactions/checkout在文件上留下可重复的修改时间,但我不认为这是可能的。

yk9xbfzb

yk9xbfzb1#

如果go不跟踪外部文件的内容,这是不是必要的?
例如,如果你修改了testdata,没有modtime,它将不知道重新运行测试。

oxosxuxt

oxosxuxt2#

@mvdan 是否有可能先运行一些脚本,将相关文件的修改时间设置为该文件的最后一次提交时间?

ntjbwcob

ntjbwcob4#

你是对的,目前打开的文件不会在其内容被测试缓存跟踪。

然而,我认为这种逻辑存在一些问题,可以进行改进。例如,我们已经获取了文件信息,因此如果一个文件很小(比如小于256KiB),我们可以读取并对其内容进行哈希,而不是使用其修改时间作为缓存输入。

这将自动解决我在使用CI时遇到的几乎所有问题;我使用的大多数测试数据文件都是纯文本且非常小,如https://github.com/cue-lang/cue/blob/7d89acd5c44bca0c756f76089436ee9913818b03/cmd/cue/cmd/testdata/script/cmd_dir.txtar。Go的测试缓存不太容易出现意外的丢失,而且我不认为它会明显变慢。

你是否有可能先运行一些脚本,将相关文件的mtime设置为该文件的最后提交时间?

我可以做到这一点,但对每个项目都要这样做相当繁琐。此外,我在多年的时间里甚至没有注意到我的多个项目由于这个bug而在CI上运行缓慢。我认为Go的测试缓存默认情况下在CI系统上表现不佳是不幸的。

总的来说,编写CI脚本也存在问题。这些项目中大多数都在Linux、Mac和Windows上进行测试。即使编写一个适用于所有这些平台的POSIX Shell或Bash脚本也是相当棘手的。

0sgqnhkj

0sgqnhkj5#

我们还可以尝试说服第三方,如git cloneactions/checkout,在文件上保留可复现的修改时间,但我认为这不太可能发生。
看起来有一个第三方工具git-restore-mtime(用Python编写,我认为它应该是足够便携的),它做了显而易见的事情——将mtime设置为与每个文件的最后作者时间戳或提交时间戳相匹配。但我同意,最好不要依赖它,因为有各种各样的方法可以改变mtime

qrjkbowd

qrjkbowd6#

我们考虑使用git-restore-mtime作为我们的CI,但该脚本相当慢,抵消了许多缓存的好处。我们目前正在使用this script的修改版本,该版本改进了目录处理。

mum43rcc

mum43rcc7#

我们考虑使用 git-restore-mtime 作为我们的 CI,但该脚本相当慢,抵消了许多缓存的好处。我们目前正在使用 this script 的修改版本,以改进目录处理。
@oschwald,你是否有可能更新评论中的链接?我们目前正在探索在 CI/CD 中进行测试缓存的选项。

相关问题