go-bindata

之所以先聊这个方案,是因为虽然它目前的热度和受欢迎程度并不是最高的,但是它的影响范围和时间综合来看,是比较大的,而且在实现和使用上,因为历史原因,它的硬分叉版本也是最多的,情况最为复杂。

各个开源项目之间的渊源

go-bindata

这些项目的共同起源是 jteeuwen/go-bindata 这个项目,它的第一行代码提交于 十年前的2011年6月。

但是在 2018 年2月7日,作者因为一些原因删除了他创建的所有仓库,随后这个账号也被弃用。这个时候,有一位好心的国外用户在 Twitter 上对其他用户进行了提醒。

来自好心人的提醒

fake.jsnpm left-pad

在一些遗留的项目中,我们可以清楚的看到这个事情的发生时间点,比如 twitter 对 go-bindata 的 fork 存档。

从 Twitter fork 修改上游仓库地址也记录了这个事情的发生

在2月8日,开源社区的其他同学想办法申诉得到了这个账号,将“删库”之前的代码恢复到了这个账号中,为了表明这个仓库是仅做恢复之用途,好心人将软件仓库设置为只读(归档)后,做了一个雷锋式的声明。

来自社区其他好心人的补救

在此后的岁月里,虽然这个仓库失去了原作者的维护。但是 Golang 和 Golang 社区生态依旧在蓬勃发展,静态资源嵌入的需求还是比较旺盛的,于是便有了上文中的其他三个开源软件仓库,以及一些我尚未提到的知名度更低的一些仓库。

各个版本的软件的差异

上面将各个开源项目之间的渊源讲完了,我们来看看这几个仓库之间都有哪些不同。

go-bindata/go-bindataelazarl/go-bindata-assetfsnet/httpelazarl/go-bindata-assetfs
elazarl/go-bindata-assetfsgo-bindata/go-bindata-fsnet/httpnet/http
kevinburke/go-bindatago-bindata/go-bindatago-bindata/go-bindatanet/httpfsnet/httpelazarl/go-bindata-assetfsfs

这些软件与官方实现的差异

go-bindata 相比较官方实现,其实会多一些额外的功能:

unsafe.Pointer
gogo rungo build

接下来,我们就先聊聊 go-bindata 的基础使用和性能表现吧。

基础使用:go-bindata 默认配置

和上一篇文章一样,在了解性能差异之前,我们先来完成基础功能的编写。

mkdir basic-go-bindata && cd basic-go-bindata
go mod init solution-embed

这里有一个小细节,因为 go-bindata/go-bindata 最新的 3.1.3 版本并没有正式发布,所以如果我们想安装包含最新功能修复的内容,需要使用下面的方式来进行安装:

# go get -u -v github.com/go-bindata/go-bindata@latest

go get: added github.com/go-bindata/go-bindata v3.1.2+incompatible

在上篇文章中,想要使用官方 go-embed 功能进行资源嵌入,我们的程序实现会类似下面这样:

package main

import (
	"embed"
	"log"
	"net/http"
)

//go:embed assets
var assets embed.FS

func main() {
	mutex := http.NewServeMux()
	mutex.Handle("/", http.FileServer(http.FS(assets)))
	err := http.ListenAndServe(":8080", mutex)
	if err != nil {
		log.Fatal(err)
	}
}
go-bindatago:generate
package main

import (
	"log"
	"net/http"

	"solution-embed/pkg/assets"
)

//go:generate go-bindata -fs -o=pkg/assets/assets.go -pkg=assets ./assets

func main() {
	mutex := http.NewServeMux()
	mutex.Handle("/", http.FileServer(assets.AssetFile()))
	err := http.ListenAndServe(":8080", mutex)
	if err != nil {
		log.Fatal(err)
	}
}
go generatego getnpx
go generatepkg/assets/assets.go\x00
du -hs *
 17M	assets
4.0K	go.mod
4.0K	go.sum
4.0K	main.go
 83M	pkg
go run main.gogo build main.gohttp://localhost:8080/assets/example.txt
go generate cp -r ../originPath ./destPath-prefix

测试准备:go-bindata 默认配置

测试代码和“前文”中的差别不大,稍作调整即可使用:

package main

import (
	"log"
	"net/http"
	"net/http/pprof"
	"runtime"

	"solution-embed/pkg/assets"
)

//go:generate go-bindata -fs -o=pkg/assets/assets.go -pkg=assets ./assets

func registerRoute() *http.ServeMux {

	mutex := http.NewServeMux()
	mutex.Handle("/", http.FileServer(assets.AssetFile()))
	return mutex
}

func enableProf(mutex *http.ServeMux) {
	runtime.GOMAXPROCS(2)
	runtime.SetMutexProfileFraction(1)
	runtime.SetBlockProfileRate(1)

	mutex.HandleFunc("/debug/pprof/", pprof.Index)
	mutex.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
	mutex.HandleFunc("/debug/pprof/profile", pprof.Profile)
	mutex.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
	mutex.HandleFunc("/debug/pprof/trace", pprof.Trace)
}

func main() {
	mutex := registerRoute()
	enableProf(mutex)

	err := http.ListenAndServe(":8080", mutex)
	if err != nil {
		log.Fatal(err)
	}
}

性能测试:go-bindata 默认配置

benchmark.sh

回顾上篇文章中,我们的测试采样的执行结果耗时都不长:

=== RUN   TestSmallFileRepeatRequest
--- PASS: TestSmallFileRepeatRequest (0.04s)
PASS
ok  	solution-embed	0.813s
=== RUN   TestLargeFileRepeatRequest
--- PASS: TestLargeFileRepeatRequest (1.14s)
PASS
ok  	solution-embed	1.331s
=== RUN   TestStaticRoute
--- PASS: TestStaticRoute (0.00s)
=== RUN   TestSmallFileRepeatRequest
--- PASS: TestSmallFileRepeatRequest (0.04s)
=== RUN   TestLargeFileRepeatRequest
--- PASS: TestLargeFileRepeatRequest (1.12s)
PASS
ok  	solution-embed	1.509s

而执行本文中 go-bindata 的采样脚本后,能看到测试时间整体变长了非常多:

=== RUN   TestSmallFileRepeatRequest
--- PASS: TestSmallFileRepeatRequest (1.47s)
PASS
ok  	solution-embed	2.260s
=== RUN   TestLargeFileRepeatRequest
--- PASS: TestLargeFileRepeatRequest (29.43s)
PASS
ok  	solution-embed	29.808s

嵌入大文件的性能状况

go tool pprof -http=:8090 cpu-large.outhttp://localhost:8090/ui/

读取嵌入资源以及相对耗时的调用状况

go:embed
go tool pprof -http=:8090 mem-large.out

读取嵌入资源内存消耗状况

可以看到不论是程序的调用链的复杂度,还是资源的使用量,go-bindata 的消耗看起来都十分夸张。在同样一百次快速调用之后,内存中总计使用过 19180 MB,是官方实现的 3 倍,相当于原始资源的 1000 多倍的消耗,平均到每次请求,我们大概需要付出原文件 10 倍的资源来提供服务,非常不划算

所以,这里不难得出一个简单的结论:请勿在 go-bindata 中嵌入过分大的资源,会造成严重的资源浪费,如果有此类需求,可以使用上篇文章中提到的官方方案来解决问题。

嵌入小文件的资源使用

go tool pprof -http=:8090 cpu-small.out

读取嵌入资源(小文件)CPU调用状况

官方实现中排行比较靠前的调用中,并未出现 embed 相关的函数调用。go-bindata 则出现了大量时间消耗在 0.88~0.95s 的数据读取、内存拷贝操作,另外针对资源的 GZip 解压缩也占用了累计 0.85s 的时间。

读取嵌入资源(小文件)CPU调用详情

不过请注意,这个测试建立在上千次的小文件获取上的,所以平均每次的时间消耗,其实也是能够接受的。当然,如果有同类需求,使用原生的实现方案更加高效。

读取嵌入资源(小文件)内存调用详情

接着来看看内存资源的使用。相比较官方实现,go-bindata大概资源消耗是其的4倍,对比原始文件,我们则需要额外使用6倍的资源。如果小文件特别多或者请求量特别大,使用go-bindata应该不是一个最优解。但如果是临时或者少量文件的需求,偶尔使用也问题不大

使用 Wrk 进行吞吐测试

go build main.go./main
# wrk -t16 -c 100 -d 30s http://localhost:8080/assets/vue.min.js
Running 30s test @ http://localhost:8080/assets/vue.min.js
  16 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    89.61ms   73.12ms 701.06ms   74.80%
    Req/Sec    74.17     25.40   210.00     68.65%
  35550 requests in 30.05s, 3.12GB read
Requests/sec:   1182.98
Transfer/sec:    106.43MB

可以看到相比较前篇文章中官方实现,吞吐能力缩水接近 20 倍。不过依旧能保持每秒 1000 多次的吞吐,对于一般的小项目来说,问题不大。

再来看看针对大文件的吞吐:

# wrk -t16 -c 100 -d 30s http://localhost:8080/assets/chip.jpg 

Running 30s test @ http://localhost:8080/assets/chip.jpg
  16 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     0.00us    0.00us   0.00us     nan%
    Req/Sec     1.66      2.68    10.00     91.26%
  106 requests in 30.10s, 1.81GB read
  Socket errors: connect 0, read 0, write 0, timeout 106
Requests/sec:      3.52
Transfer/sec:     61.46MB

相比较官方实现能够每秒吞吐接近 300 次,使用 go-bindata 后,每秒只能处理 3.5 次的请求,进一步验证了前文中不建议使用 go-bindata 处理大文件的判断。

性能测试:go-bindata 关闭 GZip压缩、开启减少内存占用功能

unsafe.Pointer
go:generate
-nocompress -nomemcopy
go generate
du -hs *   
 17M	assets
4.0K	benchmark.sh
4.0K	go.mod
4.0K	go.sum
 24M	main
4.0K	main.go
 68M	pkg
benchmark.sh
bash benchmark.sh 
=== RUN   TestSmallFileRepeatRequest
--- PASS: TestSmallFileRepeatRequest (0.05s)
PASS
ok  	solution-embed	1.246s
=== RUN   TestLargeFileRepeatRequest
--- PASS: TestLargeFileRepeatRequest (1.19s)
PASS
ok  	solution-embed	1.336s

接下来,我们来看看程序调用又发生了哪些惊人的变化呢?

嵌入大文件的性能状况

go tool pprof -http=:8090 cpu-large.out

读取嵌入资源以及相对耗时的调用状况

也这是即使资源处理调用有着差不多的调用复杂度,即使执行时间 0.91s 是官方 0.42s 一倍有余,整体服务响应时间基本没有差别的原因。

go tool pprof -http=:8090 mem-large.out

读取嵌入资源内存消耗状况

如果你对照前文来看,你会发现在开启“减少内存消耗”功能之后,go-bindata 的内存占用甚至比官方实现还要小3MB。当然,即使是和官方实现一样的资源消耗,平均到每次请求,我们还是需要大概付出原文件 3.6 倍的资源。

嵌入小文件的资源使用

小文件的测试结果粗看起来和官方实现差别不大,这里就不浪费篇幅过多赘述了。我们直接进行压力测试,来看看程序的吞吐能力吧。

使用 Wrk 进行吞吐测试

go build main.go./main
# wrk -t16 -c 100 -d 30s http://localhost:8080/assets/vue.min.js

Running 30s test @ http://localhost:8080/assets/vue.min.js
  16 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.22ms    2.55ms  47.38ms   70.90%
    Req/Sec     1.46k   128.35     1.84k    77.00%
  699226 requests in 30.02s, 61.43GB read
Requests/sec:  23292.03
Transfer/sec:      2.05GB

测试结果非常令人惊讶,每秒的响应能力甚至比官方实现还多几百。接着来看看针对大文件的吞吐能力:

# wrk -t16 -c 100 -d 30s http://localhost:8080/assets/chip.jpg 

Running 30s test @ http://localhost:8080/assets/chip.jpg
  16 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   340.98ms  138.47ms   1.60s    81.04%
    Req/Sec    18.24      9.33    60.00     73.75%
  8478 requests in 30.10s, 141.00GB read
Requests/sec:    281.63
Transfer/sec:      4.68GB

大文件的测试结果和官方实现几乎没有差别,数值差异在每秒几个。

其他

受限于篇幅,关于 “homebrew” 版的 go-bindata 的使用就暂且不提啦,感兴趣的同学可以参考本文做一个测试。

除了上面提到的实现之外,其实还有一些有趣的实现,虽然它们并不出名:

  • https://github.com/kataras/bindata
    • 基于 iris 的web 定制优化,存储数据和输出都使用 GZip 处理,相比较原版有数倍性能提升。
  • https://github.com/conku/bindatafs
    • 基于 go-bindata 的专注处理内嵌页面模版的开源仓库。
  • https://github.com/wrfly/bindata
    • 一个在实现上更加简洁的优化版本。

最后

unsafe.Pointer
unsafe.Pointer

–EOF

我们有一个小小的折腾群,里面聚集了几百位喜欢折腾的小伙伴。

在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。

喜欢折腾的小伙伴欢迎扫码添加好友。(添加好友,请备注实名,注明来源和目的,否则不会通过审核)

如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。

如果你想更快的看到后续内容的更新,请不吝“点赞”或“转发分享”,这些免费的鼓励将会影响后续有关内容的更新速度。

本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)