Go 程序太大了,能要個延遲初始化不?
大家好,我是煎魚。
在公司的不斷發(fā)展中,一開始大多是大單體,改造慢了,一個倉庫會有使用十幾年的情況,倉庫的規(guī)模基本是不斷增大的過程。
影響之一就是會應(yīng)用程序打包后的體積越來越大,不知道被用哪里去了...今天要探討的提案《proposal: language: lazy init imports to possibly import without side effects[1]》,就與此有關(guān)。
提案
背景
我們來觀察一段很簡單的 Go 代碼,研究研究。如下代碼:
package main
import _ "crypto/x509"
func main() {}
這個 Go 程序只有 3 行代碼,看起來就沒有任何東西。實際上是這樣嗎?
我們可以執(zhí)行以下命令看看初始化過程:
$ go build --ldflags=--dumpdep main.go 2>&1 | grep inittask
輸出結(jié)果:
runtime.main -> runtime..inittask
runtime.main -> main..inittask
main..inittask -> crypto/x509..inittask
crypto/x509..inittask -> bytes..inittask
crypto/x509..inittask -> crypto/sha256..inittask
crypto/x509..inittask -> encoding/pem..inittask
crypto/x509..inittask -> errors..inittask
crypto/x509..inittask -> sync..inittask
crypto/x509..inittask -> crypto/aes..inittask
crypto/x509..inittask -> crypto/cipher..inittask
crypto/x509..inittask -> crypto/des..inittask
...
context..inittask -> context.init.0
vendor/golang.org/x/net/dns/dnsmessage..inittask -> vendor/golang.org/x/net/dns/dnsmessage.init
vendor/golang.org/x/net/route..inittask -> vendor/golang.org/x/net/route.init
vendor/golang.org/x/net/route..inittask -> vendor/golang.org/x/net/route.init.0
...
這段程序其實初始化了超級多的軟件包(標(biāo)準(zhǔn)庫、第三方包等)。使得包的的大小從標(biāo)準(zhǔn)的 1.3 MB 變成了 2.3 MB。
在一定規(guī)模下,大家認(rèn)為該影響是非常昂貴的。因為你可以看到只有 3 行的 Go 程序并沒有做任何實質(zhì)性的事情。
對啟動性能敏感的程序會比較難受,普通程序也會隨著日積月累進(jìn)入惡性循環(huán),啟動會比常規(guī)的更慢。
方案
在解決方案上我們結(jié)合另外一個提案《proposal: spec: Go 2: allow manual control over imported package initialization[2]》一起來看。
核心思想是:引入惰性初始化(lazy init),業(yè)內(nèi)也常稱為延遲加載。也就是必要的時候再真正的導(dǎo)入,不在引入包時就完成初始化。
優(yōu)化方向上:主要是在導(dǎo)入包路徑后增加懶惰初始化的聲明,例如在下方即將會提到的:go:lazyinit 或 go:deferred 注解。再等待程序真正使用到時再正式初始化。
1.go:lazyinit 的例子:
package main
import (
"crypto/x509" // go:lazyinit
"fmt"
)
func main() {...}
2.go:deferred 的例子:
package main
import (
_ "github.com/eddycjy/core" // go:deferred
_ "github.com/eddycjy/util" // go:deferred
)
func main() {
if os.Args[1] != "util" {
// 現(xiàn)在要使用這個包,開始初始化
core, err := runtime.InitDeferredImport("github.com/some/module/core")
...
}
...
}
以此來實現(xiàn),可以大大提高啟動性能。
討論
實際上在大多數(shù)的社區(qū)討論中,對這個提案是又愛又恨。因為它似乎又有合理的訴求,但細(xì)思似乎又會發(fā)現(xiàn)完全不對勁。
這個提案的背景和解決方案,是治標(biāo)不治本的。因為根本原因是:許多庫濫用了 init 函數(shù),讓許多不必要的東西都初始化了。
Go 核心開發(fā)團(tuán)隊認(rèn)為讓庫作者去修復(fù)這些庫,而不是讓 Go 來 “解決” 這些問題。如果支持惰性初始化,也會為這些低質(zhì)量庫的作者提供繼續(xù)這樣做的借口。
似曾相識的感覺
在寫這篇文章時,我想起了 Go 的依賴管理(Go modules),其有一個設(shè)計是基于語義化版本的規(guī)范。
如下圖:
版本格式為 “主版本號.次版本號.修訂號”,版本號的遞增規(guī)則如下:
- 主版本號:當(dāng)你做了不兼容的 API 修改。
- 次版本號:當(dāng)你做了向下兼容的功能性新增。
- 修訂號:當(dāng)你做了向下兼容的問題修正。
Go modules 的原意是軟件庫都遵守這個規(guī)范,因此內(nèi)部會有最小版本選擇的邏輯。
也就是一個模塊往往依賴著許多其它許許多多的模塊,并且不同的模塊在依賴時很有可能會出現(xiàn)依賴同一個模塊的不同版本,Go 會把版本清單都整理出來,最終得到一個構(gòu)建清單。
如下圖:
你會發(fā)現(xiàn)最終構(gòu)建出來的依賴版本很有可能是與預(yù)期的不一致,從而導(dǎo)致許多業(yè)務(wù)問題。最經(jīng)典的就是 grpc-go、protoc-go、etcd 多版本兼容問題,讓許多人痛苦不已。
Go 團(tuán)隊在這一塊的設(shè)計是比較理想化的,曹大也將其歸類在 Go modules 的七宗罪之一了。而軟件包的 init 函數(shù)亂初始化一堆的問題,也是有些似曾相識了。
總結(jié)
這個問題的解決方案(提案)仍然在討論中,顯然 Go 團(tuán)隊更希望軟件庫的作者能夠約束好自己的代碼,不要亂初始化。
參考資料
[1]proposal: language: lazy init imports to possibly import without side effects: https://github.com/golang/go/issues/38450
[2]proposal: spec: Go 2: allow manual control over imported package initialization: https://github.com/golang/go/issues/48174