第 16 章
Day 16:分析 Mutex Lock
未指定 -r 時,redis-benchmark 使用相同 key。並行 SET、GET 就會遇到 mutex 等待。
我以 redis-benchmark -p 16379 -t SET -P 16 取得以下 flame graph:
約 40% CPU 時間花在爭取 mutex;即使改成 redis-benchmark -p 16379 -t SET -P 16 -r 1000000,仍有相同問題。
注意:指定
-r <keyspacelen>時,key 會使用key:{12 位數字}的隨機格式,數值範圍為 。
Benchmark 後,可以從 http://localhost:6060/debug/pprof/block 取得阻塞資訊:
該次報告記錄 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為 時,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:
該次 mutex 相關 CPU 時間從 47.33 秒(37.7%)降到 8.58 秒(4.7%)。
阻塞累計時間則從 1464.62 秒降到 22.53 秒。