fusefronted: optimize NFS streaming writes by saving one Stat()
Stat() calls are expensive on NFS as they need a full network round-trip. We detect when a write immediately follows the last one and skip the Stat in this case because the write cannot create a file hole. On my (slow) NAS, this takes the write speed from 24MB/s to 41MB/s.
This commit is contained in:
parent
9b7135224b
commit
a08d55f42d
@ -9,6 +9,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -43,6 +44,11 @@ type file struct {
|
|||||||
header *contentenc.FileHeader
|
header *contentenc.FileHeader
|
||||||
// go-fuse nodefs.loopbackFile
|
// go-fuse nodefs.loopbackFile
|
||||||
loopbackFile nodefs.File
|
loopbackFile nodefs.File
|
||||||
|
// Store what the last byte was written
|
||||||
|
lastWrittenOffset int64
|
||||||
|
// The opCount is used to judge whether "lastWrittenOffset" is still
|
||||||
|
// guaranteed to be correct.
|
||||||
|
lastOpCount uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFile returns a new go-fuse File instance.
|
// NewFile returns a new go-fuse File instance.
|
||||||
@ -282,6 +288,16 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {
|
|||||||
return written, status
|
return written, status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isConsecutiveWrite returns true if the current write
|
||||||
|
// directly (in time and space) follows the last write.
|
||||||
|
// This is an optimisation for streaming writes on NFS where a
|
||||||
|
// Stat() call is very expensive.
|
||||||
|
// The caller must "wlock.lock(f.ino)" otherwise this check would be racy.
|
||||||
|
func (f *file) isConsecutiveWrite(off int64) bool {
|
||||||
|
opCount := atomic.LoadUint64(&wlock.opCount)
|
||||||
|
return opCount == f.lastOpCount+1 && off == f.lastWrittenOffset+1
|
||||||
|
}
|
||||||
|
|
||||||
// Write - FUSE call
|
// Write - FUSE call
|
||||||
//
|
//
|
||||||
// If the write creates a hole, pads the file to the next block boundary.
|
// If the write creates a hole, pads the file to the next block boundary.
|
||||||
@ -299,11 +315,20 @@ func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) {
|
|||||||
defer wlock.unlock(f.ino)
|
defer wlock.unlock(f.ino)
|
||||||
tlog.Debug.Printf("ino%d: FUSE Write: offset=%d length=%d", f.ino, off, len(data))
|
tlog.Debug.Printf("ino%d: FUSE Write: offset=%d length=%d", f.ino, off, len(data))
|
||||||
// If the write creates a file hole, we have to zero-pad the last block.
|
// If the write creates a file hole, we have to zero-pad the last block.
|
||||||
status := f.writePadHole(off)
|
// But if the write directly follows an earlier write, it cannot create a
|
||||||
if !status.Ok() {
|
// hole, and we can save one Stat() call.
|
||||||
return 0, status
|
if !f.isConsecutiveWrite(off) {
|
||||||
|
status := f.writePadHole(off)
|
||||||
|
if !status.Ok() {
|
||||||
|
return 0, status
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return f.doWrite(data, off)
|
n, status := f.doWrite(data, off)
|
||||||
|
if status.Ok() {
|
||||||
|
f.lastOpCount = atomic.LoadUint64(&wlock.opCount)
|
||||||
|
f.lastWrittenOffset = off + int64(len(data)) - 1
|
||||||
|
}
|
||||||
|
return n, status
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release - FUSE call, close file
|
// Release - FUSE call, close file
|
||||||
|
@ -2,6 +2,7 @@ package fusefrontend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -20,6 +21,12 @@ var wlock wlockMap
|
|||||||
// 2) lock ... unlock ...
|
// 2) lock ... unlock ...
|
||||||
// 3) unregister
|
// 3) unregister
|
||||||
type wlockMap struct {
|
type wlockMap struct {
|
||||||
|
// Counts lock() calls. As every operation that modifies a file should
|
||||||
|
// call it, this effectively serves as a write-operation counter.
|
||||||
|
// The variable is accessed without holding any locks so atomic operations
|
||||||
|
// must be used. It must be the first element of the struct to guarantee
|
||||||
|
// 64-bit alignment.
|
||||||
|
opCount uint64
|
||||||
// Protects map access
|
// Protects map access
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
inodeLocks map[uint64]*refCntMutex
|
inodeLocks map[uint64]*refCntMutex
|
||||||
@ -62,6 +69,7 @@ func (w *wlockMap) unregister(ino uint64) {
|
|||||||
|
|
||||||
// lock retrieves the entry for "ino" and locks it.
|
// lock retrieves the entry for "ino" and locks it.
|
||||||
func (w *wlockMap) lock(ino uint64) {
|
func (w *wlockMap) lock(ino uint64) {
|
||||||
|
atomic.AddUint64(&w.opCount, 1)
|
||||||
w.Lock()
|
w.Lock()
|
||||||
r := w.inodeLocks[ino]
|
r := w.inodeLocks[ino]
|
||||||
w.Unlock()
|
w.Unlock()
|
||||||
|
Loading…
Reference in New Issue
Block a user