Olivine:以 Go 建構與 Redis 相容的服務 · 章節目錄

第 16 章

Day 16:分析 Mutex Lock

未指定 -r 時,redis-benchmark 使用相同 key。並行 SET、GET 就會遇到 mutex 等待。

我以 redis-benchmark -p 16379 -t SET -P 16 取得以下 flame graph:

Image

約 40% CPU 時間花在爭取 mutex;即使改成 redis-benchmark -p 16379 -t SET -P 16 -r 1000000,仍有相同問題。

注意:指定 -r <keyspacelen> 時,key 會使用 key:{12 位數字} 的隨機格式,數值範圍為 [0,<keyspacelen>][0, <keyspacelen>]。

Benchmark 後,可以從 http://localhost:6060/debug/pprof/block 取得阻塞資訊:

Image

該次報告記錄 mutex 等待累計 1464.62 秒。

Lock Striping

Lock striping 是並行程式設計的模式:將資源分組,只鎖定要操作的部分,而不是鎖住全部資源。

先定義多個 slots,再以 key 的 hash 決定資料所在的 slot。

重構 mapStorage

func NewStorage() Storage {
	s := mapStorage{}
	for i := range lockStripeCount {
		s.storage[i] = make(map[string]object.Object)
	}

	return &s
}

const lockStripeCount = 16

type mapStorage struct {
	storage [lockStripeCount]map[string]object.Object
	stripes [lockStripeCount]sync.Mutex
}

這裡使用 const lockStripeCount = 16,在 slot 數量與成本間取得平衡。

func (s *mapStorage) stripe(k string) uint64 {
	return xxh3.HashString(k) & (lockStripeCount - 1)
}

xxh3.HashString(k) 回傳 uint64,以 hashed % lockStripeCount 選擇 16 個 slots 之一。

注意:lockStripeCount 為 2n2^n 時,hashed % lockStripeCount 等於 hashed & (lockStripeCount-1)。

重構 (*mapStorage).Set() 與 Get()

讀寫前先找出對應 slot:

func (s *mapStorage) setString(param SetStringParam) error {
	obj := param.Obj()

	slot := s.stripe(obj.Key())
	s.stripes[slot].Lock()
	defer s.stripes[slot].Unlock()

	cur, exists := s.storage[slot][obj.Key()]
func (s *mapStorage) Get(_ context.Context, k string) (v object.Object, err error) {
	slot := s.stripe(k)
	s.stripes[slot].Lock()
	defer s.stripes[slot].Unlock()

	var ok bool
	if v, ok = s.storage[slot][k]; !ok {
		return nil, fmt.Errorf("%w: miss", ErrNotFound)
	}
	if v.ExpiresAt() != nil && time.Now().After(*v.ExpiresAt()) {
		delete(s.storage[slot], k) // remove key from storage when expired
		return nil, fmt.Errorf("%w: expired", ErrNotFound)
	}

	return
}

結果

重構後的 profiling flame graph:

Image

該次 mutex 相關 CPU 時間從 47.33 秒(37.7%)降到 8.58 秒(4.7%)。

Image

阻塞累計時間則從 1464.62 秒降到 22.53 秒。