Merge pull request #10934 from filecoin-project/sbansal/backport-v1.23.1-rc3
feat: chainstore: sharded mutex for filling chain height index
This commit is contained in:
commit
05d3dc811a
@ -2,18 +2,21 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"hash/maphash"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
|
|
||||||
|
"github.com/puzpuzpuz/xsync/v2"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/filecoin-project/go-state-types/abi"
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
|
|
||||||
"github.com/filecoin-project/lotus/chain/types"
|
"github.com/filecoin-project/lotus/chain/types"
|
||||||
|
"github.com/filecoin-project/lotus/lib/shardedmutex"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultChainIndexCacheSize = 32 << 15
|
// DefaultChainIndexCacheSize no longer sets the maximum size, just the initial size of the map.
|
||||||
|
var DefaultChainIndexCacheSize = 1 << 15
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if s := os.Getenv("LOTUS_CHAIN_INDEX_CACHE"); s != "" {
|
if s := os.Getenv("LOTUS_CHAIN_INDEX_CACHE"); s != "" {
|
||||||
@ -27,8 +30,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChainIndex struct {
|
type ChainIndex struct {
|
||||||
indexCacheLk sync.Mutex
|
indexCache *xsync.MapOf[types.TipSetKey, *lbEntry]
|
||||||
indexCache map[types.TipSetKey]*lbEntry
|
|
||||||
|
fillCacheLock shardedmutex.ShardedMutexFor[types.TipSetKey]
|
||||||
|
|
||||||
loadTipSet loadTipSetFunc
|
loadTipSet loadTipSetFunc
|
||||||
|
|
||||||
@ -36,9 +40,14 @@ type ChainIndex struct {
|
|||||||
}
|
}
|
||||||
type loadTipSetFunc func(context.Context, types.TipSetKey) (*types.TipSet, error)
|
type loadTipSetFunc func(context.Context, types.TipSetKey) (*types.TipSet, error)
|
||||||
|
|
||||||
|
func maphashTSK(s maphash.Seed, tsk types.TipSetKey) uint64 {
|
||||||
|
return maphash.Bytes(s, tsk.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
func NewChainIndex(lts loadTipSetFunc) *ChainIndex {
|
func NewChainIndex(lts loadTipSetFunc) *ChainIndex {
|
||||||
return &ChainIndex{
|
return &ChainIndex{
|
||||||
indexCache: make(map[types.TipSetKey]*lbEntry, DefaultChainIndexCacheSize),
|
indexCache: xsync.NewTypedMapOfPresized[types.TipSetKey, *lbEntry](maphashTSK, DefaultChainIndexCacheSize),
|
||||||
|
fillCacheLock: shardedmutex.NewFor(maphashTSK, 32),
|
||||||
loadTipSet: lts,
|
loadTipSet: lts,
|
||||||
skipLength: 20,
|
skipLength: 20,
|
||||||
}
|
}
|
||||||
@ -59,17 +68,23 @@ func (ci *ChainIndex) GetTipsetByHeight(ctx context.Context, from *types.TipSet,
|
|||||||
return nil, xerrors.Errorf("failed to round down: %w", err)
|
return nil, xerrors.Errorf("failed to round down: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ci.indexCacheLk.Lock()
|
|
||||||
defer ci.indexCacheLk.Unlock()
|
|
||||||
cur := rounded.Key()
|
cur := rounded.Key()
|
||||||
for {
|
for {
|
||||||
lbe, ok := ci.indexCache[cur]
|
lbe, ok := ci.indexCache.Load(cur) // check the cache
|
||||||
|
if !ok {
|
||||||
|
lk := ci.fillCacheLock.GetLock(cur)
|
||||||
|
lk.Lock() // if entry is missing, take the lock
|
||||||
|
lbe, ok = ci.indexCache.Load(cur) // check if someone else added it while we waited for lock
|
||||||
if !ok {
|
if !ok {
|
||||||
fc, err := ci.fillCache(ctx, cur)
|
fc, err := ci.fillCache(ctx, cur)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
lk.Unlock()
|
||||||
return nil, xerrors.Errorf("failed to fill cache: %w", err)
|
return nil, xerrors.Errorf("failed to fill cache: %w", err)
|
||||||
}
|
}
|
||||||
lbe = fc
|
lbe = fc
|
||||||
|
ci.indexCache.Store(cur, lbe)
|
||||||
|
}
|
||||||
|
lk.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if to == lbe.targetHeight {
|
if to == lbe.targetHeight {
|
||||||
@ -137,7 +152,6 @@ func (ci *ChainIndex) fillCache(ctx context.Context, tsk types.TipSetKey) (*lbEn
|
|||||||
targetHeight: skipTarget.Height(),
|
targetHeight: skipTarget.Height(),
|
||||||
target: skipTarget.Key(),
|
target: skipTarget.Key(),
|
||||||
}
|
}
|
||||||
ci.indexCache[tsk] = lbe
|
|
||||||
|
|
||||||
return lbe, nil
|
return lbe, nil
|
||||||
}
|
}
|
||||||
|
1
go.mod
1
go.mod
@ -139,6 +139,7 @@ require (
|
|||||||
github.com/open-rpc/meta-schema v0.0.0-20201029221707-1b72ef2ea333
|
github.com/open-rpc/meta-schema v0.0.0-20201029221707-1b72ef2ea333
|
||||||
github.com/polydawn/refmt v0.89.0
|
github.com/polydawn/refmt v0.89.0
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.14.0
|
||||||
|
github.com/puzpuzpuz/xsync/v2 v2.4.0
|
||||||
github.com/raulk/clock v1.1.0
|
github.com/raulk/clock v1.1.0
|
||||||
github.com/raulk/go-watchdog v1.3.0
|
github.com/raulk/go-watchdog v1.3.0
|
||||||
github.com/stretchr/testify v1.8.2
|
github.com/stretchr/testify v1.8.2
|
||||||
|
2
go.sum
2
go.sum
@ -1484,6 +1484,8 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5
|
|||||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||||
github.com/prometheus/statsd_exporter v0.21.0 h1:hA05Q5RFeIjgwKIYEdFd59xu5Wwaznf33yKI+pyX6T8=
|
github.com/prometheus/statsd_exporter v0.21.0 h1:hA05Q5RFeIjgwKIYEdFd59xu5Wwaznf33yKI+pyX6T8=
|
||||||
github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ=
|
github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ=
|
||||||
|
github.com/puzpuzpuz/xsync/v2 v2.4.0 h1:5sXAMHrtx1bg9nbRZTOn8T4MkWe5V+o8yKRH02Eznag=
|
||||||
|
github.com/puzpuzpuz/xsync/v2 v2.4.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
|
||||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
|
github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
|
||||||
|
75
lib/shardedmutex/shardedmutex.go
Normal file
75
lib/shardedmutex/shardedmutex.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package shardedmutex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hash/maphash"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cacheline = 64
|
||||||
|
|
||||||
|
// padding a mutex to a cacheline improves performance as the cachelines are not contested
|
||||||
|
// name old time/op new time/op delta
|
||||||
|
// Locks-8 74.6ns ± 7% 12.3ns ± 2% -83.54% (p=0.000 n=20+18)
|
||||||
|
type paddedMutex struct {
|
||||||
|
mt sync.Mutex
|
||||||
|
pad [cacheline - 8]uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShardedMutex struct {
|
||||||
|
shards []paddedMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new ShardedMutex with N shards
|
||||||
|
func New(nShards int) ShardedMutex {
|
||||||
|
if nShards < 1 {
|
||||||
|
panic("n_shards cannot be less than 1")
|
||||||
|
}
|
||||||
|
return ShardedMutex{
|
||||||
|
shards: make([]paddedMutex, nShards),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm ShardedMutex) Shards() int {
|
||||||
|
return len(sm.shards)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm ShardedMutex) Lock(shard int) {
|
||||||
|
sm.shards[shard].mt.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm ShardedMutex) Unlock(shard int) {
|
||||||
|
sm.shards[shard].mt.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm ShardedMutex) GetLock(shard int) sync.Locker {
|
||||||
|
return &sm.shards[shard].mt
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShardedMutexFor[K any] struct {
|
||||||
|
inner ShardedMutex
|
||||||
|
|
||||||
|
hasher func(maphash.Seed, K) uint64
|
||||||
|
seed maphash.Seed
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFor[K any](hasher func(maphash.Seed, K) uint64, nShards int) ShardedMutexFor[K] {
|
||||||
|
return ShardedMutexFor[K]{
|
||||||
|
inner: New(nShards),
|
||||||
|
hasher: hasher,
|
||||||
|
seed: maphash.MakeSeed(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm ShardedMutexFor[K]) shardFor(key K) int {
|
||||||
|
return int(sm.hasher(sm.seed, key) % uint64(len(sm.inner.shards)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm ShardedMutexFor[K]) Lock(key K) {
|
||||||
|
sm.inner.Lock(sm.shardFor(key))
|
||||||
|
}
|
||||||
|
func (sm ShardedMutexFor[K]) Unlock(key K) {
|
||||||
|
sm.inner.Unlock(sm.shardFor(key))
|
||||||
|
}
|
||||||
|
func (sm ShardedMutexFor[K]) GetLock(key K) sync.Locker {
|
||||||
|
return sm.inner.GetLock(sm.shardFor(key))
|
||||||
|
}
|
159
lib/shardedmutex/shardedmutex_test.go
Normal file
159
lib/shardedmutex/shardedmutex_test.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package shardedmutex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/maphash"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLockingDifferentShardsDoesNotBlock(t *testing.T) {
|
||||||
|
shards := 16
|
||||||
|
sm := New(shards)
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
panic("test locked up")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for i := 0; i < shards; i++ {
|
||||||
|
sm.Lock(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(done)
|
||||||
|
}
|
||||||
|
func TestLockingSameShardsBlocks(t *testing.T) {
|
||||||
|
shards := 16
|
||||||
|
sm := New(shards)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(shards)
|
||||||
|
ch := make(chan int, shards)
|
||||||
|
|
||||||
|
for i := 0; i < shards; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
if i != 15 {
|
||||||
|
sm.Lock(i)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
wg.Wait()
|
||||||
|
sm.Lock((15 + i) % shards)
|
||||||
|
ch <- i
|
||||||
|
sm.Unlock(i)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
for i := 0; i < 2*shards; i++ {
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
for i := 0; i < shards; i++ {
|
||||||
|
if a := <-ch; a != i {
|
||||||
|
t.Errorf("got %d instead of %d", a, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShardedByString(t *testing.T) {
|
||||||
|
shards := 16
|
||||||
|
sm := NewFor(maphash.String, shards)
|
||||||
|
|
||||||
|
wg1 := sync.WaitGroup{}
|
||||||
|
wg1.Add(shards * 20)
|
||||||
|
wg2 := sync.WaitGroup{}
|
||||||
|
wg2.Add(shards * 20)
|
||||||
|
|
||||||
|
active := atomic.Int32{}
|
||||||
|
max := atomic.Int32{}
|
||||||
|
|
||||||
|
for i := 0; i < shards*20; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
wg1.Done()
|
||||||
|
wg1.Wait()
|
||||||
|
sm.Lock(fmt.Sprintf("goroutine %d", i))
|
||||||
|
activeNew := active.Add(1)
|
||||||
|
for {
|
||||||
|
curMax := max.Load()
|
||||||
|
if curMax >= activeNew {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if max.CompareAndSwap(curMax, activeNew) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
active.Add(-1)
|
||||||
|
sm.Unlock(fmt.Sprintf("goroutine %d", i))
|
||||||
|
wg2.Done()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg2.Wait()
|
||||||
|
|
||||||
|
if max.Load() != 16 {
|
||||||
|
t.Fatal("max load not achieved", max.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkShardedMutex(b *testing.B) {
|
||||||
|
shards := 16
|
||||||
|
sm := New(shards)
|
||||||
|
|
||||||
|
done := atomic.Int32{}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
sm.Lock(0)
|
||||||
|
sm.Unlock(0)
|
||||||
|
if done.Load() != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
sm.Lock(1)
|
||||||
|
sm.Unlock(1)
|
||||||
|
}
|
||||||
|
done.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkShardedMutexOf(b *testing.B) {
|
||||||
|
shards := 16
|
||||||
|
sm := NewFor(maphash.String, shards)
|
||||||
|
|
||||||
|
str1 := "string1"
|
||||||
|
str2 := "string2"
|
||||||
|
|
||||||
|
done := atomic.Int32{}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
sm.Lock(str1)
|
||||||
|
sm.Unlock(str1)
|
||||||
|
if done.Load() != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
sm.Lock(str2)
|
||||||
|
sm.Unlock(str2)
|
||||||
|
}
|
||||||
|
done.Add(1)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user