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

Go 生態(tài)系統(tǒng)有許多流行的日志庫,選擇一個可以在所有項目中使用的日志庫對于保持最小的一致性至關重要。易用性和性能通常是我們在日志庫中考慮的兩個指標。接下來我們回顧一下 Uber[1] 開發(fā)的 Zap[2] 日志庫。
核心思想
Zap 基于三個概念優(yōu)化性能,第一個是:
避免使用 interface{}有利于強類型的設計。
這一點隱藏另外兩個概念:
無反射。反射是有代價的,而且可以避免,因為包能夠決定被調(diào)用的類型。
在 JSON 編碼中沒有額外內(nèi)存分配。如果對標準庫進行了優(yōu)化,則可以輕松避免在此處進行內(nèi)存分配,因為 package 包含所有已發(fā)送參數(shù)的類型。
以上幾點,對開發(fā)人員來說成本不高,因此他們需要在記錄消息時聲明每種類型:
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),
)
每個字段的顯式聲明將允許包在日志記錄過程中高效地工作。讓我們回顧一下包的設計,以了解這些優(yōu)化將在何處發(fā)生。
設計
在高亮顯示包的優(yōu)化部分之前,讓我們繪制日志庫的全局工作流:

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

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

發(fā)送到日志庫的每個條目( Zerolog 中稱為 event )也使用 sync 包中的池,以避免在記錄消息時進行系統(tǒng)分配。
正如我們所看到的,這些軟件包非常相似。這解釋了為什么他們的性能很接近。讓我們嘗試另一個具有不同設計的包,以了解在這些包中缺少的優(yōu)化。
現(xiàn)在讓我們將這些 logger 與 Golang 生態(tài)系統(tǒng)中另一個著名的包 Logrus[6] 進行比較。以下是相同功能的代碼:
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 對象使用一個池,但是在檢查與消息一起發(fā)送的字段時將添加一個反射層。此反射允許日志庫檢測傳遞給日志庫的所有參數(shù)是否有效,但會稍微減慢執(zhí)行速度。
另外,與 Zap 或 Zerolog 相反,參數(shù)不是類型化的,這將導致將起始類型轉換為空接口,然后返回起始類型以便對其進行編碼。
該包還為鉤子添加了一層額外的鎖,如果需要,可以將其移除,但默認情況下會激活。
沒有優(yōu)化
閱讀這些庫的編寫方式對于每個 Go 開發(fā)人員來說都是一個很好的練習,以便了解如何優(yōu)化我們的代碼和潛在的好處。大多數(shù)情況下,對于非關鍵應用程序,您不需要深入研究,但是如果像 Zap 或 Zerolog 這樣的外部包免費提供這些優(yōu)化,我們絕對應該利用它。如果您想了解使用池的潛在好處,我建議您閱讀我的文章“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] 榮譽推出
參考資料
Uber: https://github.com/uber-go
[2]Zap: https://github.com/uber-go/zap
[3]基準: https://github.com/uber-go/zap/tree/v1.10.0/benchmarks
[4]Zerolog: https://github.com/rs/zerolog
[5]基準: 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/
推薦閱讀
