mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-26 01:44:48 +00:00
All three io_uring backends (iceber, giouring, raw) now require explicit build tags — no tag means standard-only. Each backend registers its name via IOUringImpl so startup logs show compiled implementation alongside requested/selected backend mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
//go:build linux && iouring_iceber
|
|
|
|
package batchio
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/iceber/iouring-go"
|
|
)
|
|
|
|
func init() { IOUringImpl = "iceber" }
|
|
|
|
// ioUringBatchIO implements BatchIO using Linux io_uring.
|
|
// Requires kernel 5.6+ (linked fsync: 5.10+).
|
|
type ioUringBatchIO struct {
|
|
ring *iouring.IOURing
|
|
ringSize int // max SQEs per submission
|
|
}
|
|
|
|
// NewIOUring creates a BatchIO backed by io_uring with the given ring size.
|
|
// Returns ErrIOUringUnavailable if io_uring cannot be initialized.
|
|
func NewIOUring(ringSize uint) (BatchIO, error) {
|
|
ring, err := iouring.New(ringSize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrIOUringUnavailable, err)
|
|
}
|
|
return &ioUringBatchIO{ring: ring, ringSize: int(ringSize)}, nil
|
|
}
|
|
|
|
func (u *ioUringBatchIO) PreadBatch(fd *os.File, ops []Op) error {
|
|
if len(ops) == 0 {
|
|
return nil
|
|
}
|
|
// Submit in chunks that fit the ring to avoid "too many requests" rejection.
|
|
for start := 0; start < len(ops); start += u.ringSize {
|
|
end := start + u.ringSize
|
|
if end > len(ops) {
|
|
end = len(ops)
|
|
}
|
|
if err := u.preadChunk(fd, ops[start:end]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *ioUringBatchIO) preadChunk(fd *os.File, ops []Op) error {
|
|
requests := make([]iouring.PrepRequest, len(ops))
|
|
// fd.Fd() is safe: the os.File stays live through this call.
|
|
fdInt := int(fd.Fd())
|
|
for i := range ops {
|
|
requests[i] = iouring.Pread(fdInt, ops[i].Buf, uint64(ops[i].Offset))
|
|
}
|
|
|
|
resultSet, err := u.ring.SubmitRequests(requests, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("iouring PreadBatch submit: %w", err)
|
|
}
|
|
|
|
// Wait for all completions before checking individual results.
|
|
<-resultSet.Done()
|
|
for i, req := range resultSet.Requests() {
|
|
n, err := req.ReturnInt()
|
|
if err != nil {
|
|
return fmt.Errorf("iouring PreadBatch op[%d]: %w", i, err)
|
|
}
|
|
if n < len(ops[i].Buf) {
|
|
return fmt.Errorf("iouring PreadBatch op[%d]: short read %d/%d", i, n, len(ops[i].Buf))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *ioUringBatchIO) PwriteBatch(fd *os.File, ops []Op) error {
|
|
if len(ops) == 0 {
|
|
return nil
|
|
}
|
|
// Submit in chunks that fit the ring to avoid "too many requests" rejection.
|
|
for start := 0; start < len(ops); start += u.ringSize {
|
|
end := start + u.ringSize
|
|
if end > len(ops) {
|
|
end = len(ops)
|
|
}
|
|
if err := u.pwriteChunk(fd, ops[start:end]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *ioUringBatchIO) pwriteChunk(fd *os.File, ops []Op) error {
|
|
requests := make([]iouring.PrepRequest, len(ops))
|
|
fdInt := int(fd.Fd())
|
|
for i := range ops {
|
|
requests[i] = iouring.Pwrite(fdInt, ops[i].Buf, uint64(ops[i].Offset))
|
|
}
|
|
|
|
resultSet, err := u.ring.SubmitRequests(requests, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("iouring PwriteBatch submit: %w", err)
|
|
}
|
|
|
|
// Wait for all completions before checking individual results.
|
|
<-resultSet.Done()
|
|
for i, req := range resultSet.Requests() {
|
|
n, err := req.ReturnInt()
|
|
if err != nil {
|
|
return fmt.Errorf("iouring PwriteBatch op[%d]: %w", i, err)
|
|
}
|
|
if n < len(ops[i].Buf) {
|
|
return fmt.Errorf("iouring PwriteBatch op[%d]: short write %d/%d", i, n, len(ops[i].Buf))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Fsync issues fdatasync via io_uring. fdatasync flushes data to disk without
|
|
// updating file metadata (size, mtime). This is sufficient for pre-allocated
|
|
// extent files where metadata does not change during writes, and matches the
|
|
// standard path behavior (see standard_linux.go).
|
|
func (u *ioUringBatchIO) Fsync(fd *os.File) error {
|
|
req := iouring.Fdatasync(int(fd.Fd()))
|
|
result, err := u.ring.SubmitRequest(req, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("iouring Fsync submit: %w", err)
|
|
}
|
|
<-result.Done()
|
|
_, err = result.ReturnInt()
|
|
if err != nil {
|
|
return fmt.Errorf("iouring Fsync: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LinkedWriteFsync submits a pwrite + fdatasync as a linked SQE chain.
|
|
// The kernel executes them in order in a single io_uring_enter() call.
|
|
// Requires kernel 5.10+ for linked fsync. Falls back to sequential on error.
|
|
func (u *ioUringBatchIO) LinkedWriteFsync(fd *os.File, buf []byte, offset int64) error {
|
|
fdInt := int(fd.Fd())
|
|
writeReq := iouring.Pwrite(fdInt, buf, uint64(offset))
|
|
fsyncReq := iouring.Fdatasync(fdInt)
|
|
|
|
// SubmitLinkRequests sets IOSQE_IO_LINK on all SQEs except the last,
|
|
// ensuring the fdatasync executes only after the pwrite completes.
|
|
resultSet, err := u.ring.SubmitLinkRequests(
|
|
[]iouring.PrepRequest{writeReq, fsyncReq},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
// Fallback to sequential if linked submission fails.
|
|
if _, werr := fd.WriteAt(buf, offset); werr != nil {
|
|
return werr
|
|
}
|
|
return fdatasync(fd)
|
|
}
|
|
|
|
// Wait for all linked ops to complete, then check results.
|
|
<-resultSet.Done()
|
|
for i, req := range resultSet.Requests() {
|
|
_, rerr := req.ReturnInt()
|
|
if rerr != nil {
|
|
return fmt.Errorf("iouring LinkedWriteFsync op[%d]: %w", i, rerr)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *ioUringBatchIO) Close() error {
|
|
if u.ring != nil {
|
|
return u.ring.Close()
|
|
}
|
|
return nil
|
|
}
|