目錄

pprof: Sync.atomic vs Sync.mutex

Sync.atomic vs Sync.mutex

sync.atomic: https://pkg.go.dev/sync/atomic sync.mutex: https://pkg.go.dev/sync#Mutex

寫多讀少 -> 互斥鎖 寫少讀多 -> 原子操作, 讀寫鎖

Sync.atomic vs Sync.mutex

Mutex vs Atomic 的情況裡,Mutex 相對更重。 因為涉及到更多的 goroutine 之間的上下文切換 pack blocking goroutine,以及喚醒 goroutine。

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main

import (
	"fmt"
	"log"
	"net/http"
	_ "net/http/pprof"
	"sync"
	"sync/atomic"
)

type Config struct {
	a []int
}

func (c *Config) T() {}

func Atomic(numbers int) {
	var v atomic.Value
	v.Store(&Config{})

	go func() {
		i := 0
		for {
			i++
			cfg := &Config{a: []int{i, i + 1, i + 2, i + 3, i + 4, i + 5}}
			v.Store(cfg)
		}
	}()

	var wg sync.WaitGroup
	for n := 0; n < 4; n++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for n := 0; n < numbers; n++ {
				cfg := v.Load().(*Config)
				cfg.T()
				// fmt.Printf("%v\n", cfg)
			}
		}()
	}
	wg.Wait()
}

func RWMutex(numbers int) {
	var rwmu sync.RWMutex
	var cfg *Config

	go func() {
		i := 0
		for {
			i++
			rwmu.Lock()
			cfg = &Config{a: []int{i, i + 1, i + 2, i + 3, i + 4, i + 5}}
			rwmu.Unlock()
		}
	}()

	var wg sync.WaitGroup
	for n := 0; n < 4; n++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for n := 0; n < numbers; n++ {
				rwmu.RLock()
				cfg.T()
				rwmu.RUnlock()
				// fmt.Printf("%v\n", cfg)
			}
		}()
	}
	wg.Wait()
}

func main() {
	Atomic(100)
	RWMutex(100)
	log.Fatalln(http.ListenAndServe("localhost:9999", nil))
}

main_test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "testing"

func BenchmarkAtomic(b *testing.B) {
	b.ResetTimer()
	Atomic(b.N)
}

func BenchmarkRWMutex(b *testing.B) {
	b.ResetTimer()
	RWMutex(b.N)
}

從結果來看 atomic 的效能快 mutex很多~

1
2
3
4
5
6
7
8
goos: darwin
goarch: amd64
pkg: MyGoNote/Golang/Package_Sync/examples/atomic_mutex
cpu: Intel(R) Core(TM) i5-8259U CPU @ 2.30GHz
BenchmarkAtomic-8       552392600                2.861 ns/op           1 B/op          0 allocs/op
BenchmarkRWMutex-8        753264              1415 ns/op            1967 B/op         54 allocs/op
PASS
ok      MyGoNote/Golang/Package_Sync/examples/atomic_mutex      3.655s

sync.atoml

Copy-On-Write(COW) 思路在微服務降級或者 local cache 場景中經常使用。 寫時複製(Copy-On-Write)指的是,寫操作時候複製全量老數據到一個新的對象中, 攜帶上本次新寫的數據,之後利用原子替換(atomic.Value), 更新調用者的變量。來完成無鎖訪問共享數據。

wiki: cow

寫入時複製(英語:Copy-on-write,簡稱COW)是一種電腦程式設計領域的最佳化策略。其核心思想是, 如果有多個呼叫者(callers)同時請求相同資源(如記憶體或磁碟上的資料儲存),他們會共同取得相同的指標指向相同的資源, 直到某個呼叫者試圖修改資源的內容時,系統才會真正複製一份專用副本(private copy)給該呼叫者, 而其他呼叫者所見到的最初的資源仍然保持不變。這過程對其他的呼叫者都是透明的。 此作法主要的優點是如果呼叫者沒有修改該資源,就不會有副本(private copy)被建立,因此多個呼叫者只是讀取操作時可以共享同一份資源。

pprof

如果使用 benchmark的方式來評估效能, 最主要遇到的問題會是,在一大段程式碼及邏輯中,要找出慢的主因. 這時可以使用到 pprof 來找出程式碼所有執行的時間,怎麼輸出 CPU 所花的時間,可以透過底下指令:

1
go test -bench=. -benchtime=3s -cpuprofile cpu.out .

產生.out後可以用 go tool pprof cpu.out 來看

1
2
3
4
5
6
$ go tool pprof cpu.out
Type: cpu
Time: Aug 24, 2022 at 10:41am (CST)
Duration: 11.74s, Total samples = 32.51s (276.81%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

使用 top 來看數據

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(pprof) top 10
Showing nodes accounting for 20040ms, 61.64% of 32510ms total
Dropped 153 nodes (cum <= 162.55ms)
Showing top 10 nodes out of 98
      flat  flat%   sum%        cum   cum%
    6370ms 19.59% 19.59%     6380ms 19.62%  runtime.madvise
    2590ms  7.97% 27.56%     9010ms 27.71%  runtime.mallocgc
    2080ms  6.40% 33.96%     2520ms  7.75%  sync/atomic.(*Value).Load (inline)
    1960ms  6.03% 39.99%     4950ms 15.23%  MyGoNote/Golang/Package_Sync/examples/atomic_mutex.Atomic.func2
    1820ms  5.60% 45.59%     1840ms  5.66%  runtime.usleep
    1660ms  5.11% 50.69%     1670ms  5.14%  runtime.pageIndexOf (inline)
    1140ms  3.51% 54.20%     1140ms  3.51%  runtime.asyncPreempt
     880ms  2.71% 56.91%      880ms  2.71%  runtime.procyield
     810ms  2.49% 59.40%     1550ms  4.77%  sync/atomic.StorePointer
     730ms  2.25% 61.64%      730ms  2.25%  runtime.memclrNoHeapPointers
(pprof) %      

看atom function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
(pprof) list atom
Total: 32.51s
ROUTINE ======================== MyGoNote/Golang/Package_Sync/examples/atomic_mutex.Atomic.func1 in /Users/kimi/go/src/MyGoNote/Golang/Package_Sync/examples/atomic_mutex/main.go
     450ms      9.43s (flat, cum) 29.01% of Total
         .          .     43:   v.Store(&Config{})
         .          .     44:
         .          .     45:   go func() {
         .          .     46:           i := 0
         .          .     47:           for {
      40ms       50ms     48:                   i++
     370ms      7.40s     49:                   cfg := &Config{a: []int{i, i + 1, i + 2, i + 3, i + 4, i + 5}}
      40ms      1.98s     50:                   v.Store(cfg)
         .          .     51:           }
         .          .     52:   }()
         .          .     53:
         .          .     54:   var wg sync.WaitGroup
         .          .     55:   for n := 0; n < 4; n++ {
ROUTINE ======================== MyGoNote/Golang/Package_Sync/examples/atomic_mutex.Atomic.func2 in /Users/kimi/go/src/MyGoNote/Golang/Package_Sync/examples/atomic_mutex/main.go
     1.96s      4.95s (flat, cum) 15.23% of Total
         .          .     54:   var wg sync.WaitGroup
         .          .     55:   for n := 0; n < 4; n++ {
         .          .     56:           wg.Add(1)
         .          .     57:           go func() {
         .          .     58:                   defer wg.Done()
     1.10s      1.31s     59:                   for n := 0; n < numbers; n++ {
     860ms      3.64s     60:                           cfg := v.Load().(*Config)
         .          .     61:                           cfg.T()
         .          .     62:                           // fmt.Printf("%v\n", cfg)
         .          .     63:                   }
         .          .     64:           }()
         .          .     65:   }
...
名稱含義
flat本函數的執行耗時
flat%flat 佔 CPU 總時間的比例。程序總耗時 16.22s, Eat 的 16.19s 佔了 99.82%
sum%前面每一行的 flat 佔比總和
cum累計量。指該函數加上該函數調用的函數總耗時
cum%cum 佔 CPU 總時間的比例

pprof web 方式來進行 UI 操作

首先要先安裝 graphviz https://graphviz.gitlab.io/download/

MAC

1
brew install graphviz

接下來輸入 go tool pprof -http=:8080 cpu.out 此時你就可以在你的瀏覽器上面 http://localhost:8080/ui/ 看到

/zh-tw/sync.atomic_vs_sync.mutex/pprof_%EF%BC%92.png

net/http/pprof

如果線上遇到 CPU 或內存佔用過高,該怎麼辦呢?總不能將上面的 Profile 代碼編譯到生產環境吧,這無疑會極大地影響性能。 net/http/pprof提供了一個方法,不使用時不會造成任何影響,遇到問題時可以開啟 profiling 幫助我們排查問題。 我們只需要使用import這個包,然後在一個新的 goroutine 中調用http.ListenAndServe()在某個端口啟動一個默認的 HTTP 服務器即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
  ...
	_ "net/http/pprof"
)


func main() {
	...
	log.Fatalln(http.ListenAndServe("localhost:9999", nil))
}

打開瀏覽器 http://localhost:9999/debug/pprof/

/zh-tw/sync.atomic_vs_sync.mutex/pprof_3.png

支援遠程調用

1
go tool pprof -http :8080 http://localhost:9999/debug/pprof/allocs\?debug\=1                                              

打開瀏覽器 http://localhost:8080/ui /zh-tw/sync.atomic_vs_sync.mutex/pprof_4.png

Reference