這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。鎖可用於同步操作。但如果使用不當的話,也會引發顯著的效能問題。一個比較常見出問題的地方是 HTTP handlers 處。尤其很容易在不經意間就會鎖住網路 I/O。要理解這種問題,我們最好還是來看一個例子。這篇文章中,我會使用 Go。為此,我們需要編寫一個簡單的 HTTP 伺服器用以報告它接收到的請求數量。所有的代碼可以從 [這裡](https://github.com/gobuildit/gobuildit/tree/master/lock) 獲得。報告請求數量的服務看起來是這樣的:```gopackage main// import statements// ...const (payloadBytes = 1024 * 1024)var (mu sync.Mutexcount int)// register handler and start server in main// ...// BAD: Don't do this.func root(w http.ResponseWriter, r *http.Request) {mu.Lock()defer mu.Unlock()count++msg := []byte(strings.Repeat(fmt.Sprintf("%d", count), payloadBytes))w.Write(msg)}````root` handler 在最頂部用了常規的上鎖和 `defer` 解鎖。接著,在持有鎖期間,增長了 `count` 的值,並將 `count` 的值通過重複 `payloadBytes` 次產生的資料寫入 `http.ResponseWriter` 之中。對於經驗不足的人,這個 handler 看起來貌似完美無缺。實際上,它會引發一個顯著的效能問題。在網路 I/O 期間上鎖,導致了這個 handler 執行起來的速度取決於最慢的那個用戶端。為了能夠直接地看清楚問題,我們需要類比一個緩慢的讀取用戶端(以下簡稱為慢用戶端)。實際上,因為有些用戶端實在是太慢了,所以對於暴露在開放網路中的 Go HTTP 用戶端來說設定一個逾時時間很有必要。因為核心擁有緩衝寫入和從 TCP sockets 讀取的機制,所以我們的類比需要一些技巧。假設我們建立的用戶端發送了一個 `GET` 請求,卻沒有從 socket 讀取到任何資料(代碼在 [此處](https://github.com/gobuildit/gobuildit/blob/master/lock/client/main.go))。這會使服務在 `w.Write` 處阻塞嗎?因為核心緩衝了讀寫資料,所以至少在緩衝填充滿之前,我們不會看到服務速度有任何下滑。為了觀察到這種速度下滑,我們要保證每次的寫入資料都能填充滿緩衝。有兩個辦法。1) 調校一下核心。2) 每次都寫入大批量的位元組。調校核心本身就是件迷人的事情。可以通過 [proc 目錄](https://twitter.com/b0rk/status/981159808832286720),有所有網路相關參數的 [文檔](https://www.kernel.org/doc/Documentation/sysctl/net.txt),也有 [各類](https://www.cyberciti.biz/faq/linux-tcp-tuning/) [主機調校](http://fasterdata.es.net/host-tuning/) 的 [教程](https://www.tecmint.com/change-modify-linux-kernel-runtime-parameters/)。但是對於我們而言,只需要往 socket 中寫入大批量的資料,就可以填滿普通的 Darwin (v17.4) 核心的 TCP 緩衝了。注意,運行這個樣本,你可能需要調整寫入資料的量以保證填充滿你的緩衝。現在我們啟動服務,使用慢用戶端來觀察其他的用戶端等待慢用戶端的速度。慢用戶端的代碼在 [這裡](https://github.com/gobuildit/gobuildit/blob/master/lock/client/main.go)。首先,確認一個請求可以被快速地處理:```curl localhost:8080/# Output:# numerous 1's without any meaningful delay```現在,我們先運行慢用戶端:```# Assuming $GOPATH/github.com/gobuildit/gobuildit/lock directorygo run client/main.go# Output:dialingsending GET requestblocking and never reading```當慢用戶端串連上伺服器之後,再嘗試運行“快”用戶端:```curl localhost:8080/# Hangs```我們可以直接地看到我們的鎖策略如何不經意間阻塞了快用戶端。如果回到我們的 handler 想一下我們是怎麼使用鎖的,就會明白其中的問題。```gofunc root(w http.ResponseWriter, r *http.Request) {mu.Lock()defer mu.Unlock()// ...}```通過在方法頂部的加鎖和使用 `defer` 解鎖,我們在整個 handler 期間都持有鎖對象。這個過程包含了共用狀態的操作,共用狀態的讀取和網路資料寫入。也就是這些操作導致了問題。網路 I/O 是 [天生不可預知](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing) 的。誠然,我們可以通過配置逾時來保護我們的服務避免過長時間的調用,但我們無法保證所有的網路 I/O 都能在固定的時間內完成。解決問題的關鍵在於不要在 I/O 周圍加鎖。這個例子中,在 I/O 周圍加鎖沒有任何意義。在 I/O 周圍加鎖會使我們的程式被不良網路情況和慢用戶端影響。實際上,我們也部分放棄了對於我們程式同步化的控制。讓我們重寫 handler 來只在關鍵區段加鎖。```go// GOOD: Keep the critical section as small as possible and don't lock around// I/O.func root(w http.ResponseWriter, r *http.Request) {mu.Lock()count++current := countmu.Unlock()msg := []byte(strings.Repeat(fmt.Sprintf("%d", current), payloadBytes))w.Write(msg)}```為了看出區別,嘗試使用一個慢用戶端和一個普通的用戶端。同樣,先啟動慢用戶端:```# Assuming $GOPATH/github.com/gobuildit/gobuildit/lock directorygo run client/main.go```現在,使用 `curl` 來發送一個請求:```curl localhost:8080/```觀察 `curl` 是否立即返回並帶回了期望的 count。誠然,這個例子過於不自然,也比典型的生產環境代碼要簡單得多。而且對於同步計數而言,使用 [atomics](https://golang.org/pkg/sync/atomic/) 包可能更加明智。雖然如此,我也希望這個例子闡述了對於謹慎加鎖的重要性。雖然也會有例外,但通常大部分情況下不要在 I/O 周圍加鎖。
via: https://commandercoriander.net/blog/2018/04/10/dont-lock-around-io/
作者:Eno 譯者:alfred-zhong 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
628 次點擊