关于golang:深入浅出-Golang-资源嵌入方案gobindata篇

50次阅读

共计 10162 个字符,预计需要花费 26 分钟才能阅读完成。

上篇文章中,咱们讲到了 Golang 原生的资源嵌入计划,本篇咱们先来聊聊开源实现中排行中靠前的计划:go-bindata

之所以先聊这个计划,是因为尽管它目前的热度和受欢迎水平并不是最高的,然而它的影响范畴和工夫综合来看,是比拟大的,而且在实现和应用上,因为历史起因,它的硬分叉版本也是最多的,状况最为简单。

各个开源我的项目之间的渊源

先来聊聊这类开源我的项目之间的渊源吧。目前我的项目中会用到的 go-bindata 的我的项目次要有四个,别离是:

  • (1500+ stars)https://github.com/go-bindata/go-bindata
  • (840+ stars)https://github.com/elazarl/go-bindata-assetfs
  • (630+ stars)https://github.com/jteeuwen/go-bindata
  • (280+ stars)https://github.com/kevinburke/go-bindata

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

然而在 2018 年 2 月 7 日,作者因为一些起因删除了他创立的所有仓库,随后这个账号也被弃用。这个时候,有一位善意的国外用户在 Twitter 上对其余用户进行了揭示。

随后天然是引发了相似最近 fake.js 作者删库、早些时候的 npm left-pad 仓库软件删除雷同的,极其蹩脚的连锁反应,大量软件无奈失常构建。

在一些遗留的我的项目中,咱们能够分明的看到这个事件的产生工夫点,比方 twitter 对 go-bindata 的 fork 存档。

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

在尔后的岁月里,尽管这个仓库失去了原作者的保护。然而 Golang 和 Golang 社区生态仍旧在蓬勃发展,动态资源嵌入的需要还是比拟旺盛的,于是便有了上文中的其余三个开源软件仓库,以及一些我尚未提到的知名度更低的一些仓库。

各个版本的软件的差别

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

在这几个仓库中,go-bindata/go-bindata 是知名度最高的版本,elazarl/go-bindata-assetfs 提供了原版软件不反对 net/http 应用的 FS 封装。还记得上一篇文章中提到的 FS 接口实现吗,没错,这个我的项目次要就是做了这个性能。除此之外,在过来几年里,前端畛域技术的蓬勃发展,尤其是 SPA 类型的前端利用的蓬勃发展,也让 elazarl/go-bindata-assetfs 这个专一于服务 SPA 利用单文件散发的解决方案有了实战的中央。所以 如果你有相似的需要,仍旧能够应用这个仓库,将你的前端 SPA 我的项目打包成一个可执行文件进行疾速散发

当然,开源社区中的软件倒退常常是交织的,在 elazarl/go-bindata-assetfs 提供了 FS 封装不久,go-bindata/go-bindata 也提供了 -fs 参数,反对了将嵌入资源和 net/http 一起应用的性能。所以如果你谋求程序的依赖最小化,并心愿嵌入的资源和 net/http 一起应用,能够思考只应用这个仓库

此外,还有一些有代码洁癖的程序员,则创立了一个新的 fork 版本,kevinburke/go-bindata。相比拟原版以及go-bindata/go-bindata 代码,它的代码强壮水平更好,并且修改了社区用户对 go-bindata/go-bindata 反馈的一些问题,增加了一些社区用户冀望的新性能。不过这个仓库中的程序和原版一样,并未蕴含配合 net/http 一起应用所须要的 fs 封装。所以如果想应用这个程序处理的动态资源和 net/http 一起应用,须要搭配 elazarl/go-bindata-assetfs,或者本人封装一个简略的 fs

这些软件与官网实现的差别

go-bindata 相比拟官网实现,其实会多一些额定的性能:

  • 容许用户应用两种不同的模式来读取动态资源(比方应用反射和 unsafe.Pointer 的形式间接读取数据,或者应用 Golang 程序变量的形式来进行数据交互)
  • 在某些场景下,绝对更低的资源存储空间占用(基于构建时进行的 GZip 压缩)
  • 对动态资源的援用门路,进行动静调整或预处理的能力
  • 更凋谢的资源引入模式,反对从下级目录引入资源(官网实现仅反对当前目录)

当然,相比拟上一篇文章中官网实现而言,go-bindata 的实现绝对“脏一些”,会将动态资源打包为一个 go 程序文件。并且在程序运行之前,咱们须要先执行资源构建操作,能力让程序跑起来。而不是像官网实现一样,“零增加无污染”,go run 或者 go 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-bindata 的话,因为咱们须要应用一个额定生成的程序文件,程序须要改为相似上面这样,并且须要增加一段 go: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 generate 指令,申明了程序运行前所须要执行的相干命令,它除了反对运行环境中的全局程序之外,还能够运行通过 go get 装置的可执行的命令。如果你应用过 Node.js 生态中的 npx (npm) 命令,你会感觉很亲切,不过和 npx 不同的是,这个指令和程序的上下文更亲密,反对扩散写在不同的程序中,和程序上下文更亲密一些。

先执行 go generate,我的项目当前目录的 pkg/assets/assets.go 地位会呈现一个的程序文件,它蕴含了咱们所须要的资源,因为 bindata 实现应用了 \x00 之类的字符进行编码,所以生成的代码相比拟原始的动态资源会收缩 4~5 倍,然而并不影响咱们编译后失去的二进制文件大小(和官网实现体现统一)

du -hs *
 17M    assets
4.0K    go.mod
4.0K    go.sum
4.0K    main.go
 83M    pkg

不管咱们抉择应用 go run main.go 还是 go build main.go,当程序运行起来之后,拜访 http://localhost:8080/assets/example.txt 就能验证程序是否失常啦。

相干代码实现在 https://github.com/soulteary/awesome-golang-embed/tree/main/go-bindata-related/basic-go-bindata,感兴趣能够自取。

此外,相比拟官网程序不反对应用以后程序目录之外的资源(须要应用 go generate cp -r ../originPath ./destPath 的形式来曲线救国),go-bindata 能够间接在生成资源的应用援用内部资源。并在对外提供服务之前,应用-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

这部分应用的相干代码,我上传到了 https://github.com/soulteary/awesome-golang-embed/tree/main/go-bindata-related/benchmark,有须要能够自取。

嵌入大文件的性能情况

这里咱们仍旧是应用 go tool pprof -http=:8090 cpu-large.out 来展现程序计算调用过程的资源耗费情况(因为调用十分多,这里咱们只看间接关系比拟大的局部)。在浏览器中关上 http://localhost:8090/ui/,能够看到相似上面的调用图:

相比拟官网 go:embed 实现中 embed 函数只耗费了 0.07s,io.copy 只耗费 0.88s。go-bindata 在 embed 解决和 io.copy 上则别离破费了 12.99~13.08s 和 26.06~27.03s。前者性能耗费减少了 180 多倍,后者则靠近 30 倍。

持续应用 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 之后,能够看到一个十分壮观的调用。(在咱们代码足够简略的前提下,这个调用复杂度能够说比拟离谱)

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

不过请留神,这个测试建设在上千次的小文件获取上的,所以均匀每次的工夫耗费,其实也是可能承受的。当然,如果有同类需要,应用原生的实现计划更加高效。

接着来看看内存资源的应用。相比拟官网实现,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 压缩、开启缩小内存占用性能

默认的 go-bindata 会开启 GZip 压缩(采纳 Go 默认压缩比率),如果咱们不开启 GZip 测试性能会有改善吗?此外,如果咱们开启基于反射和 unsafe.Pointer的缩小内存占用的性能,程序的性能是否会有改善?

想要敞开 GZip,开启缩小内存占用的性能,只须要在 go:generate 指令中增加上面的参数开关即可。

-nocompress -nomemcopy

从新执行 go generate 之后,咱们查看生成文件的尺寸,会发现竟然比没开启 GZip 还更小一些(有一些资源的确不适宜 GZip):

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,能够看到执行工夫产生了质的变动,甚至迫近了官网实现(仅相差 0.01s 和 0.07s)。

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

接下来,咱们来看看程序调用又产生了哪些惊人的变动呢?

对于这部分的相干代码,我上传到了 https://github.com/soulteary/awesome-golang-embed/tree/main/go-bindata-related/benchmark-no-compress,感兴趣能够自取,并进行试验。

嵌入大文件的性能情况

还是先应用 go tool pprof -http=:8090 cpu-large.out 来展现程序计算调用过程的资源耗费情况。能够看到这里对于资源解决的调用复杂度和官网比拟差不多了,相比拟官网实现的调用链,开启了缩小内存占用和敞开了 GZip 压缩后的程序,在程序并行计算上来看,甚至是优于前文中官网调用的

也这是即便资源解决调用有着差不多的调用复杂度,即便执行工夫 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

    • 一个在实现上更加简洁的优化版本。

最初

在测试到这里,咱们就能够针对 go-bindata 做出一个简略的判断了,如果你谋求不应用或者少应用反射和unsafe.Pointer,那么在大量文件、不蕴含大体积文件的前提下,应用 go-bindata 是可行的。

一旦数据量大起来,倡议还是应用官网实现。当然,如果你可能承受应用反射和unsafe.Pointer,go-bindata 能够提供给你不逊于官网 go-embed 实现的性能,以及更多的定制化能力。

–EOF


咱们有一个小小的折腾群,外面汇集了几百位喜爱折腾的小伙伴。

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

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

对于折腾群入群的那些事


如果你感觉内容还算实用,欢送点赞分享给你的敌人,在此谢过。

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


本文应用「署名 4.0 国内 (CC BY 4.0)」许可协定,欢送转载、或从新批改应用,但须要注明起源。署名 4.0 国内 (CC BY 4.0)

本文作者: 苏洋

创立工夫: 2022 年 01 月 16 日
统计字数: 12144 字
浏览工夫: 25 分钟浏览
本文链接: https://soulteary.com/2022/01…

正文完
 0