<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
過年在家正好閒得沒有太多事情,想起年前一個研發專案負責人反饋的問題:“老李啊,我們組一直在使用你這邊的 gin 封裝的 webservice 框架開發,我們需要一套標準的非同步紀錄檔輸出模組。現在組內和其他使用 gin 的小夥伴實現的‘各有千秋’不統一,沒有一個組或者部門對這部分的程式碼負責和長期維護。你能不能想想辦法。”
這一看就是掉頭髮的事情,雖然 gin 封裝的 webservice 框架是我開發底層服務包,已經推廣到公司所有 golang 開發組使用,現在需要一個統一非同步紀錄檔輸出的模組是否真的有意義,要認真的考慮和研究下,畢竟有核心業務團隊有這樣的需求。
索性開啟了 uber-go/zap 紀錄檔框架的原始碼,看看到底是什麼原因推動大家都要手寫非同步紀錄檔模組。不看不知道,一看嚇一跳,專案中 issue#998 就有討論,我看了下 issue 留言,覺得大家的說法都挺正確,而專案作者一直無動無衷,而且堅信 bufio + 定時 flush 的方式 才是正道,怪不得大家都要自己手寫一個非同步紀錄檔輸出模組。
在要寫 uber-go/zap 非同步紀錄檔模組之前,首先要明白非同步紀錄檔模組的優點、缺點以及適用的場景,這樣程式碼才寫的有意義,是真正的解決問題和能幫助到小夥伴的。
關於同步和非同步模型的差異,這邊就不展開了,估計再寫幾千字也不一定能說清楚,有需要深入瞭解的小夥伴,可以自行 baidu,那裡有很多相關的文章,而且講解得非常清晰。這裡我就不需要過多解析,而我需要講的是同步和非同步紀錄檔模組。
那麼我就用一句話說明白這兩種紀錄檔模型的差別。
既然這裡說到是心智負擔,但是真正負擔在哪裡? 實際上面已經提到了心智負擔的核心內容:就是如何正確的選擇一個紀錄檔模型。
而我們這邊需求是明確知道有部分紀錄檔可以丟失,追求介面響應速度,希望有統一的實現,有人維護程式碼和與整個 gin 封裝的 webservice 框架融合的品質。
明確了開發的需求,開發的目標。確認了開發有意義,確實能解決問題。那麼:就是幹!!!
在動之前還是要準備些知識,還要做好結構設計,這樣才能解答:一套合理的非同步輸出模型應該是什麼樣的?
分享下我理解的一個非同步紀錄檔模型是什麼樣的(歡迎大家來“錘”,但是錘我的時候,麻煩輕點哈)
有的小夥伴看到這個圖覺得有點眼熟?Kafka?不對,不對,不對,還少了一個 Broker。因為這裡不需要對 Producer 實現一個獨立的緩衝器和分類器,那麼 Broker 這樣的角色就不存在了。
簡單的介紹下成員角色:
為什麼選擇上面的模型:
為了實現這個模型,還需要思考如下幾個問題:
如果看到這裡,估計已經勸退了很多的小夥伴,我想這就是為什麼那個研發專案負責人帶著團隊問題來找我,希望能夠得到解決的原因吧。確實不容易。
在認真看看完了 uber-go/zap 的程式碼以後,發現 uber 就是 uber,程式碼質量還是非常不錯的,很多模組抽象的非常不錯。通過一段時間的思考後,確認我們要實現一個獨立的 WriteSyncer, 跟 uber-go/zap 中的 BufferedWriteSyncer 扮演相同的角色。
既然要實現,我們先看看 uber-go/zap 中的原始碼怎麼定義 WriteSyncer 的。
go.uber.org/zap@v1.24.0/zapcore/write_syncer.go
// A WriteSyncer is an io.Writer that can also flush any buffered data. Note // that *os.File (and thus, os.Stderr and os.Stdout) implement WriteSyncer. type WriteSyncer interface { io.Writer Sync() error }
WriteSyncer 是一個 interface,也就是我們只要參照 io.Writer 和實現 Sync() error 這樣的一個方法就可以對接 uber-go/zap 系統中。那麼 Sync() 這個函數到底是幹嘛的? 顧名思義就是讓 zap 觸發資料同步動作時需要執行的一個方法。但是我們是非同步紀錄檔,明顯 uber-go/zap 處理完紀錄檔相關的資料,丟給我實現的 WriteSyncer 以後,就不應該在干預非同步紀錄檔模組的後期動作了,所以 Sync() 給他一個空殼函數就行了。
當然 uber-go/zap 早考慮到這樣的情況,就給一個非常棒的包裝函數 AddSync()。
go.uber.org/zap@v1.24.0/zapcore/write_syncer.go
// AddSync converts an io.Writer to a WriteSyncer. It attempts to be // intelligent: if the concrete type of the io.Writer implements WriteSyncer, // we'll use the existing Sync method. If it doesn't, we'll add a no-op Sync. func AddSync(w io.Writer) WriteSyncer { switch w := w.(type) { case WriteSyncer: return w default: return writerWrapper{w} } } type writerWrapper struct { io.Writer } func (w writerWrapper) Sync() error { return nil }
uber-go/zap 已經把我們希望要做的事情都給做好了,我們只要實現一個標準的 io.Writer 就行了,那繼續看 io.Writer 的定義方式。
go/src/io/io.go
// Writer is the interface that wraps the basic Write method. // // Write writes len(p) bytes from p to the underlying data stream. // It returns the number of bytes written from p (0 <= n <= len(p)) // and any error encountered that caused the write to stop early. // Write must return a non-nil error if it returns n < len(p). // Write must not modify the slice data, even temporarily. // // Implementations must not retain p. type Writer interface { Write(p []byte) (n int, err error) }
哇,好簡單。要實現 io.Writer 僅僅只要實現一個 Write(p []byte) (n int, err error) 方法就行了,So Easy !!!!
還是回到上一章中的 5 個核心問題,我想到這裡應該有答案了:
TIPS: 這裡說說為什麼我要選擇 golang 自身的 channel 作為 CriticalSurface 和 RingBuffer 的實現體:
有了上面的思路,我的程式碼架構也基本出來了,結構圖如下:
這裡我貼出一個實現程式碼(DEMO 測試用,生產要謹慎重新實現):
const defaultQueueCap = math.MaxUint16 * 8 var QueueIsFullError = errors.New("queue is full") var DropWriteMessageError = errors.New("message writing failure and drop it") type Writer struct { name string bufferPool *extraBufferPool writer io.Writer wg sync.WaitGroup lock sync.RWMutex channel chan *extraBuffer } func NewBufferWriter(name string, w io.Writer, queueCap uint32) *Writer { if len(name) <= 0 { name = "bw_" + utils.GetRandIdString() } if queueCap <= 0 { queueCap = defaultQueueCap } if w == nil { return nil } wr := Writer{ name: name, bufferPool: newExtraBufferPool(defaultBufferSize), writer: w, channel: make(chan *extraBuffer, queueCap), } wr.wg.Add(1) go wr.poller(utils.GetRandIdString()) return &wr } func (w *Writer) Write(p []byte) (int, error) { if w.lock.TryRLock() { defer w.lock.RUnlock() b := w.bufferPool.Get() count, err := b.buff.Write(p) if err != nil { w.bufferPool.Put(b) return count, err } select { case w.channel <- b: // channel 內部傳遞的是 buffer 的指標,速度比傳遞物件快。 break default: w.bufferPool.Put(b) return count, QueueIsFullError } return len(p), nil } else { return -1, DropWriteMessageError } } func (w *Writer) Close() { w.lock.Lock() close(w.channel) w.wg.Wait() w.lock.Unlock() } func (w *Writer) poller(id string) { var ( eb *extraBuffer err error ) defer w.wg.Done() for eb = range w.channel { _, err = w.writer.Write(eb.buff.Bytes()) if err != nil { log.Printf("writer: %s, id: %s, error: %s, message: %s", w.name, id, err.Error(), utils.BytesToString(eb.buff.Bytes())) } w.bufferPool.Put(eb) } }
然後在 uber-go/zap 中如何使用呢?
import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" "time" ) func main() { wr := NewBufferWriter("lee", os.Stdout, 0) defer wr.Close() c := zapcore.NewCore( zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(wr), zap.NewAtomicLevelAt(zap.DebugLevel), ) log := zap.New(c) log.Info("demo log") time.Sleep(3 * time.Second) // 這裡要稍微等待下,因為是非同步的輸出,log.Info() 執行完畢,紀錄檔並沒有完全輸出到 console }
Console 輸出:
$ go run asynclog.go {"level":"info","ts":1674808100.0148869,"msg":"demo log"}
輸出結果符合逾期
為了驗證架構和程式碼質量,這裡做了非同步輸出紀錄檔、同步輸出紀錄檔和不輸出紀錄檔 3 種情況下,對 gin 封裝的 webservice 框架吞吐力的影響。
# | 測試內容 | Requests/sec |
---|---|---|
1 | 同步輸出紀錄檔 | 20074.24 |
2 | 非同步輸出紀錄檔 | 64197.08 |
3 | 不輸出紀錄檔 | 65551.84 |
$ wrk -t 10 -c 1000 http://127.0.0.1:8080/xx/ Running 10s test @ http://127.0.0.1:8080/xx/ 10 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 12.03ms 14.23ms 202.46ms 89.23% Req/Sec 2.03k 1.36k 9.49k 59.28% 202813 requests in 10.10s, 100.58MB read Socket errors: connect 757, read 73, write 0, timeout 0 Requests/sec: 20074.24 Transfer/sec: 9.96MB
$ wrk -t 10 -c 1000 http://127.0.0.1:8080/xx/ Running 10s test @ http://127.0.0.1:8080/xx/ 10 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 3.75ms 2.43ms 39.94ms 92.68% Req/Sec 6.48k 3.86k 14.78k 57.11% 648554 requests in 10.10s, 321.62MB read Socket errors: connect 757, read 79, write 0, timeout 0 Requests/sec: 64197.08 Transfer/sec: 31.84MB
$ wrk -t 10 -c 1000 http://127.0.0.1:8080/xx/ Running 10s test @ http://127.0.0.1:8080/xx/ 10 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 3.69ms 505.13us 9.29ms 77.36% Req/Sec 6.60k 4.25k 15.31k 56.45% 662381 requests in 10.10s, 328.48MB read Socket errors: connect 757, read 64, write 0, timeout 0 Requests/sec: 65551.84 Transfer/sec: 32.51MB
通過對上面的工程程式碼測試,基本實現了 gin + zap 的非同步紀錄檔輸出功能的實現。當然上面的程式碼僅供小夥伴學習研究用,並不能作為生產程式碼使用。
從結果來看,golang 的 channel 整體效能還是非常不錯。基於 channel 實現的非同步紀錄檔輸出基本於不輸出紀錄檔的吞吐力和效能相當。
在實際工作中,我們能用 golang 原生庫的時候就儘量用,因為 golang 團隊在寫庫的時候,大多數的情況和場景都考慮過,所以沒有必自己做一個輪子。安全!安全!安全!
至於 uber-go/zap 團隊為什麼不願意實現這樣的非同步紀錄檔輸出模型,可能有他們的想法吧。但是我想,不論那種非同步紀錄檔模型,都存在著程式異常會丟紀錄檔的情況。這裡再次提醒小夥伴,要慎重選擇紀錄檔系統模型,切不可以一味追求速度而忽略紀錄檔,因為服務紀錄檔也是重要的業務資料。
以上就是uber go zap 紀錄檔框架支援非同步紀錄檔輸出的詳細內容,更多關於uber go zap紀錄檔非同步輸出的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45