Uber 的 zap 庫是如何做到高性能的?

Go 生態(tài)系統(tǒng)有許多流行的日志庫,選擇一個(gè)可以在所有項(xiàng)目中使用的日志庫對于保持最小的一致性至關(guān)重要。易用性和性能通常是我們在日志庫中考慮的兩個(gè)指標(biāo)。接下來我們回顧一下 Uber[1] 開發(fā)的 Zap[2] 日志庫。
核心思想
Zap 基于三個(gè)概念優(yōu)化性能,第一個(gè)是:
避免使用 interface{}有利于強(qiáng)類型的設(shè)計(jì)。
這一點(diǎn)隱藏另外兩個(gè)概念:
無反射。反射是有代價(jià)的,而且可以避免,因?yàn)榘軌驔Q定被調(diào)用的類型。
在 JSON 編碼中沒有額外內(nèi)存分配。如果對標(biāo)準(zhǔn)庫進(jìn)行了優(yōu)化,則可以輕松避免在此處進(jìn)行內(nèi)存分配,因?yàn)?package 包含所有已發(fā)送參數(shù)的類型。
以上幾點(diǎn),對開發(fā)人員來說成本不高,因此他們需要在記錄消息時(shí)聲明每種類型:
logger.Info("failed to fetch URL",
// Structured context as strongly typed Field values.
zap.String("url", `http://foo.com`),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
每個(gè)字段的顯式聲明將允許包在日志記錄過程中高效地工作。讓我們回顧一下包的設(shè)計(jì),以了解這些優(yōu)化將在何處發(fā)生。
設(shè)計(jì)
在高亮顯示包的優(yōu)化部分之前,讓我們繪制日志庫的全局工作流:

第一步優(yōu)化,為了避免進(jìn)行系統(tǒng)分配,我們看到優(yōu)化使用同步池在記錄消息。每個(gè)要記錄的消息都將重用之前創(chuàng)建的結(jié)構(gòu)體(structure),并將其釋放到池中。
第二部優(yōu)化,涉及編碼器和 JSON 的存儲方式。要記錄的每個(gè)字段都是強(qiáng)類型的,如前一節(jié)所示。它允許編碼器通過直接將值轉(zhuǎn)儲到緩沖區(qū)來避免反射和分配:

這個(gè)緩沖區(qū)的管理要感謝 sync.Pool.
最終調(diào)用方的性能/成本的權(quán)衡非常有趣,因?yàn)轱@式聲明每個(gè)字段不需要開發(fā)人員付出太多努力。但是,該庫為 logger 提供了一層封裝,它公開了一個(gè)對開發(fā)人員更友好的接口,您不需要定義要記錄的每個(gè)字段的每種類型。可從 logger.Sugar() 方法中獲取,它將稍微減慢并增加日志庫的分配數(shù)。
與 Go 生態(tài)系統(tǒng)中可用的其他包相比,所有這些優(yōu)化使包的速度相當(dāng)快,并顯著減少了內(nèi)存分配。讓我們?yōu)g覽并比較一下可用的替代方案。
其他選擇
Zap 提供的 基準(zhǔn)[3] 測試清楚地表明 Zerolog[4] 是與 Zap 競爭最激烈的一個(gè)。Zerolog 還提供了結(jié)果非常相似的 基準(zhǔn)[5] :

它清楚地展示 Zerolog 和 Zap 在性能方面比其他軟件包要好得多,速度快 5 到 27 倍。
現(xiàn)在讓我們比較一下用 Zerolog 編寫的同一段代碼:
l := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
l.Info().
Str("url", `http://foo.com`).
Int("attempt", 3).
Dur("backoff", time.Second).
Msg("failed to fetch URL")
寫法上非常接近,并且我們可以看到 Zerolog 也引入強(qiáng)類型參數(shù)以優(yōu)化性能。如 encoder 接口所述,JSON 編碼器還根據(jù)類型轉(zhuǎn)儲數(shù)據(jù):

發(fā)送到日志庫的每個(gè)條目( Zerolog 中稱為 event )也使用 sync 包中的池,以避免在記錄消息時(shí)進(jìn)行系統(tǒng)分配。
正如我們所看到的,這些軟件包非常相似。這解釋了為什么他們的性能很接近。讓我們嘗試另一個(gè)具有不同設(shè)計(jì)的包,以了解在這些包中缺少的優(yōu)化。
現(xiàn)在讓我們將這些 logger 與 Golang 生態(tài)系統(tǒng)中另一個(gè)著名的包 Logrus[6] 進(jìn)行比較。以下是相同功能的代碼:
log.SetOutput(os.Stdout)
log.WithFields(log.Fields{
"url": "http://foo.com",
"attempt": 3,
"backoff": time.Second,
}).Info("failed to fetch URL")
在內(nèi)部,Logrus 還將為 entry 對象使用一個(gè)池,但是在檢查與消息一起發(fā)送的字段時(shí)將添加一個(gè)反射層。此反射允許日志庫檢測傳遞給日志庫的所有參數(shù)是否有效,但會稍微減慢執(zhí)行速度。
另外,與 Zap 或 Zerolog 相反,參數(shù)不是類型化的,這將導(dǎo)致將起始類型轉(zhuǎn)換為空接口,然后返回起始類型以便對其進(jìn)行編碼。
該包還為鉤子添加了一層額外的鎖,如果需要,可以將其移除,但默認(rèn)情況下會激活。
沒有優(yōu)化
閱讀這些庫的編寫方式對于每個(gè) Go 開發(fā)人員來說都是一個(gè)很好的練習(xí),以便了解如何優(yōu)化我們的代碼和潛在的好處。大多數(shù)情況下,對于非關(guān)鍵應(yīng)用程序,您不需要深入研究,但是如果像 Zap 或 Zerolog 這樣的外部包免費(fèi)提供這些優(yōu)化,我們絕對應(yīng)該利用它。如果您想了解使用池的潛在好處,我建議您閱讀我的文章“Understand the design of sync.Pool[7]”.
via: https://medium.com/a-journey-with-go/go-how-zap-package-is-optimized-dbf72ef48f2d
作者:Vincent Blanchon[8]譯者:lts8989[9]校對:polaris1119[10]
本文由 GCTT[11] 原創(chuàng)編譯,Go 中文網(wǎng)[12] 榮譽(yù)推出
參考資料
Uber: https://github.com/uber-go
[2]Zap: https://github.com/uber-go/zap
[3]基準(zhǔn): https://github.com/uber-go/zap/tree/v1.10.0/benchmarks
[4]Zerolog: https://github.com/rs/zerolog
[5]基準(zhǔn): https://github.com/rs/logbench
[6]Logrus: https://github.com/sirupsen/logrus
[7]Understand the design of sync.Pool: https://medium.com/@blanchon.vincent/go-understand-the-design-of-sync-pool-2dde3024e277
[8]Vincent Blanchon: https://medium.com/@blanchon.vincent
[9]lts8989: https://github.com/lts8989
[10]polaris1119: https://github.com/polaris1119
[11]GCTT: https://github.com/studygolang/GCTT
[12]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
