mirror of
https://github.com/minio/minio.git
synced 2025-11-07 12:52:58 -05:00
fix: Speed up multi-object delete by taking bulk locks (#8974)
Change distributed locking to allow taking bulk locks
across objects, reduces usually 1000 calls to 1.
Also allows for situations where multiple clients sends
delete requests to objects with following names
```
{1,2,3,4,5}
```
```
{5,4,3,2,1}
```
will block and ensure that we do not fail the request
on each other.
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"errors"
|
||||
pathutil "path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -51,16 +52,10 @@ func newNSLock(isDistXL bool) *nsLockMap {
|
||||
if isDistXL {
|
||||
return &nsMutex
|
||||
}
|
||||
nsMutex.lockMap = make(map[nsParam]*nsLock)
|
||||
nsMutex.lockMap = make(map[string]*nsLock)
|
||||
return &nsMutex
|
||||
}
|
||||
|
||||
// nsParam - carries name space resource.
|
||||
type nsParam struct {
|
||||
volume string
|
||||
path string
|
||||
}
|
||||
|
||||
// nsLock - provides primitives for locking critical namespace regions.
|
||||
type nsLock struct {
|
||||
*lsync.LRWMutex
|
||||
@@ -72,23 +67,24 @@ type nsLock struct {
|
||||
type nsLockMap struct {
|
||||
// Indicates if namespace is part of a distributed setup.
|
||||
isDistXL bool
|
||||
lockMap map[nsParam]*nsLock
|
||||
lockMap map[string]*nsLock
|
||||
lockMapMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Lock the namespace resource.
|
||||
func (n *nsLockMap) lock(ctx context.Context, volume, path string, lockSource, opsID string, readLock bool, timeout time.Duration) (locked bool) {
|
||||
func (n *nsLockMap) lock(ctx context.Context, volume string, path string, lockSource, opsID string, readLock bool, timeout time.Duration) (locked bool) {
|
||||
var nsLk *nsLock
|
||||
|
||||
resource := pathJoin(volume, path)
|
||||
|
||||
n.lockMapMutex.Lock()
|
||||
param := nsParam{volume, path}
|
||||
nsLk, found := n.lockMap[param]
|
||||
nsLk, found := n.lockMap[resource]
|
||||
if !found {
|
||||
n.lockMap[param] = &nsLock{
|
||||
nsLk = &nsLock{
|
||||
LRWMutex: lsync.NewLRWMutex(ctx),
|
||||
ref: 1,
|
||||
}
|
||||
nsLk = n.lockMap[param]
|
||||
n.lockMap[resource] = nsLk
|
||||
} else {
|
||||
// Update ref count here to avoid multiple races.
|
||||
nsLk.ref++
|
||||
@@ -109,7 +105,7 @@ func (n *nsLockMap) lock(ctx context.Context, volume, path string, lockSource, o
|
||||
nsLk.ref--
|
||||
if nsLk.ref == 0 {
|
||||
// Remove from the map if there are no more references.
|
||||
delete(n.lockMap, param)
|
||||
delete(n.lockMap, resource)
|
||||
}
|
||||
n.lockMapMutex.Unlock()
|
||||
}
|
||||
@@ -117,10 +113,10 @@ func (n *nsLockMap) lock(ctx context.Context, volume, path string, lockSource, o
|
||||
}
|
||||
|
||||
// Unlock the namespace resource.
|
||||
func (n *nsLockMap) unlock(volume, path string, readLock bool) {
|
||||
param := nsParam{volume, path}
|
||||
func (n *nsLockMap) unlock(volume string, path string, readLock bool) {
|
||||
resource := pathJoin(volume, path)
|
||||
n.lockMapMutex.RLock()
|
||||
nsLk, found := n.lockMap[param]
|
||||
nsLk, found := n.lockMap[resource]
|
||||
n.lockMapMutex.RUnlock()
|
||||
if !found {
|
||||
return
|
||||
@@ -137,45 +133,16 @@ func (n *nsLockMap) unlock(volume, path string, readLock bool) {
|
||||
nsLk.ref--
|
||||
if nsLk.ref == 0 {
|
||||
// Remove from the map if there are no more references.
|
||||
delete(n.lockMap, param)
|
||||
delete(n.lockMap, resource)
|
||||
}
|
||||
}
|
||||
n.lockMapMutex.Unlock()
|
||||
}
|
||||
|
||||
// Lock - locks the given resource for writes, using a previously
|
||||
// allocated name space lock or initializing a new one.
|
||||
func (n *nsLockMap) Lock(volume, path, opsID string, timeout time.Duration) (locked bool) {
|
||||
readLock := false // This is a write lock.
|
||||
|
||||
lockSource := getSource() // Useful for debugging
|
||||
return n.lock(context.Background(), volume, path, lockSource, opsID, readLock, timeout)
|
||||
}
|
||||
|
||||
// Unlock - unlocks any previously acquired write locks.
|
||||
func (n *nsLockMap) Unlock(volume, path, opsID string) {
|
||||
readLock := false
|
||||
n.unlock(volume, path, readLock)
|
||||
}
|
||||
|
||||
// RLock - locks any previously acquired read locks.
|
||||
func (n *nsLockMap) RLock(volume, path, opsID string, timeout time.Duration) (locked bool) {
|
||||
readLock := true
|
||||
|
||||
lockSource := getSource() // Useful for debugging
|
||||
return n.lock(context.Background(), volume, path, lockSource, opsID, readLock, timeout)
|
||||
}
|
||||
|
||||
// RUnlock - unlocks any previously acquired read locks.
|
||||
func (n *nsLockMap) RUnlock(volume, path, opsID string) {
|
||||
readLock := true
|
||||
n.unlock(volume, path, readLock)
|
||||
}
|
||||
|
||||
// dsync's distributed lock instance.
|
||||
type distLockInstance struct {
|
||||
rwMutex *dsync.DRWMutex
|
||||
volume, path, opsID string
|
||||
rwMutex *dsync.DRWMutex
|
||||
opsID string
|
||||
}
|
||||
|
||||
// Lock - block until write lock is taken or timeout has occurred.
|
||||
@@ -185,7 +152,7 @@ func (di *distLockInstance) GetLock(timeout *dynamicTimeout) (timedOutErr error)
|
||||
|
||||
if !di.rwMutex.GetLock(di.opsID, lockSource, timeout.Timeout()) {
|
||||
timeout.LogFailure()
|
||||
return OperationTimedOut{Path: di.path}
|
||||
return OperationTimedOut{}
|
||||
}
|
||||
timeout.LogSuccess(UTCNow().Sub(start))
|
||||
return nil
|
||||
@@ -202,7 +169,7 @@ func (di *distLockInstance) GetRLock(timeout *dynamicTimeout) (timedOutErr error
|
||||
start := UTCNow()
|
||||
if !di.rwMutex.GetRLock(di.opsID, lockSource, timeout.Timeout()) {
|
||||
timeout.LogFailure()
|
||||
return OperationTimedOut{Path: di.path}
|
||||
return OperationTimedOut{}
|
||||
}
|
||||
timeout.LogSuccess(UTCNow().Sub(start))
|
||||
return nil
|
||||
@@ -215,22 +182,26 @@ func (di *distLockInstance) RUnlock() {
|
||||
|
||||
// localLockInstance - frontend/top-level interface for namespace locks.
|
||||
type localLockInstance struct {
|
||||
ctx context.Context
|
||||
ns *nsLockMap
|
||||
volume, path, opsID string
|
||||
ctx context.Context
|
||||
ns *nsLockMap
|
||||
volume string
|
||||
paths []string
|
||||
opsID string
|
||||
}
|
||||
|
||||
// NewNSLock - returns a lock instance for a given volume and
|
||||
// path. The returned lockInstance object encapsulates the nsLockMap,
|
||||
// volume, path and operation ID.
|
||||
func (n *nsLockMap) NewNSLock(ctx context.Context, lockersFn func() []dsync.NetLocker, volume, path string) RWLocker {
|
||||
func (n *nsLockMap) NewNSLock(ctx context.Context, lockersFn func() []dsync.NetLocker, volume string, paths ...string) RWLocker {
|
||||
opsID := mustGetUUID()
|
||||
if n.isDistXL {
|
||||
return &distLockInstance{dsync.NewDRWMutex(ctx, pathJoin(volume, path), &dsync.Dsync{
|
||||
drwmutex := dsync.NewDRWMutex(ctx, &dsync.Dsync{
|
||||
GetLockersFn: lockersFn,
|
||||
}), volume, path, opsID}
|
||||
}, pathsJoinPrefix(volume, paths...)...)
|
||||
return &distLockInstance{drwmutex, opsID}
|
||||
}
|
||||
return &localLockInstance{ctx, n, volume, path, opsID}
|
||||
sort.Strings(paths)
|
||||
return &localLockInstance{ctx, n, volume, paths, opsID}
|
||||
}
|
||||
|
||||
// Lock - block until write lock is taken or timeout has occurred.
|
||||
@@ -238,9 +209,16 @@ func (li *localLockInstance) GetLock(timeout *dynamicTimeout) (timedOutErr error
|
||||
lockSource := getSource()
|
||||
start := UTCNow()
|
||||
readLock := false
|
||||
if !li.ns.lock(li.ctx, li.volume, li.path, lockSource, li.opsID, readLock, timeout.Timeout()) {
|
||||
timeout.LogFailure()
|
||||
return OperationTimedOut{Path: li.path}
|
||||
var success []int
|
||||
for i, path := range li.paths {
|
||||
if !li.ns.lock(li.ctx, li.volume, path, lockSource, li.opsID, readLock, timeout.Timeout()) {
|
||||
timeout.LogFailure()
|
||||
for _, sint := range success {
|
||||
li.ns.unlock(li.volume, li.paths[sint], readLock)
|
||||
}
|
||||
return OperationTimedOut{}
|
||||
}
|
||||
success = append(success, i)
|
||||
}
|
||||
timeout.LogSuccess(UTCNow().Sub(start))
|
||||
return
|
||||
@@ -249,7 +227,9 @@ func (li *localLockInstance) GetLock(timeout *dynamicTimeout) (timedOutErr error
|
||||
// Unlock - block until write lock is released.
|
||||
func (li *localLockInstance) Unlock() {
|
||||
readLock := false
|
||||
li.ns.unlock(li.volume, li.path, readLock)
|
||||
for _, path := range li.paths {
|
||||
li.ns.unlock(li.volume, path, readLock)
|
||||
}
|
||||
}
|
||||
|
||||
// RLock - block until read lock is taken or timeout has occurred.
|
||||
@@ -257,9 +237,16 @@ func (li *localLockInstance) GetRLock(timeout *dynamicTimeout) (timedOutErr erro
|
||||
lockSource := getSource()
|
||||
start := UTCNow()
|
||||
readLock := true
|
||||
if !li.ns.lock(li.ctx, li.volume, li.path, lockSource, li.opsID, readLock, timeout.Timeout()) {
|
||||
timeout.LogFailure()
|
||||
return OperationTimedOut{Path: li.path}
|
||||
var success []int
|
||||
for i, path := range li.paths {
|
||||
if !li.ns.lock(li.ctx, li.volume, path, lockSource, li.opsID, readLock, timeout.Timeout()) {
|
||||
timeout.LogFailure()
|
||||
for _, sint := range success {
|
||||
li.ns.unlock(li.volume, li.paths[sint], readLock)
|
||||
}
|
||||
return OperationTimedOut{}
|
||||
}
|
||||
success = append(success, i)
|
||||
}
|
||||
timeout.LogSuccess(UTCNow().Sub(start))
|
||||
return
|
||||
@@ -268,7 +255,9 @@ func (li *localLockInstance) GetRLock(timeout *dynamicTimeout) (timedOutErr erro
|
||||
// RUnlock - block until read lock is released.
|
||||
func (li *localLockInstance) RUnlock() {
|
||||
readLock := true
|
||||
li.ns.unlock(li.volume, li.path, readLock)
|
||||
for _, path := range li.paths {
|
||||
li.ns.unlock(li.volume, path, readLock)
|
||||
}
|
||||
}
|
||||
|
||||
func getSource() string {
|
||||
|
||||
Reference in New Issue
Block a user