Skip to content

Feature request: Scatter-gather I/O support for large message batches #254

@Millnert

Description

@Millnert

Hi Matt,

I'd like to propose adding scatter-gather I/O support for sending multiple netlink messages, which would allow sending large message batches without requiring a single contiguous memory allocation.

Background

The current SendMessages implementation marshals all messages into a single []byte buffer before calling sendmsg():

// Current approach (simplified from conn_linux.go)
func (c *conn) SendMessages(messages []Message) error {
    var buf []byte
    for _, m := range messages {
        b, err := m.MarshalBinary()
        buf = append(buf, b...)  // Single growing buffer
    }
    _, err := c.s.Sendmsg(ctx, buf, nil, sa, 0)
    return err
}

This works well for typical use cases, but creates a hard ceiling when sending very large batches. For context, the nft CLI (userspace tool for nftables) uses scatter-gather I/O via sendmsg() with an iovec array to send arbitrarily large atomic ruleset updates. From the kernel mailing list (Pablo Neira Ayuso, nftables maintainer, 2013):

"While discussing atomic rule-set for nftables with Patrick McHardy, we decided to put all rule-set updates that need to be applied atomically in one single batch to simplify the existing approach. However, as explained above, the existing netlink code limits us to a maximum of ~20000 rules that fit in one single batch without hitting ENOBUFS."

The kernel still receives everything as a single datagram—scatter-gather just avoids the userspace contiguous allocation requirement.

Proposal

Add a new method that uses Go's unix.SendmsgBuffers() (available since Go 1.20, unix.SendmsgBuffers ) to send pre-marshaled message buffers via scatter-gather:

1. In mdlayher/socket — new method on *Conn:

// SendmsgBuffers wraps sendmsg(2) with scatter-gather I/O support.
// Each buffer in the slice becomes an iovec entry, sent as a single datagram.
func (c *Conn) SendmsgBuffers(ctx context.Context, buffers [][]byte, oob []byte, to unix.Sockaddr, flags int) (int, error) {
    // Implementation using unix.SendmsgBuffers
}

2. In mdlayher/netlink — new method on *Conn:

// SendMessagesScatter sends multiple messages using scatter-gather I/O.
// Each message is marshaled to its own buffer, avoiding a single large
// contiguous allocation. All messages are sent as one datagram.
func (c *Conn) SendMessagesScatter(messages []Message) error {
    buffers := make([][]byte, len(messages))
    for i, m := range messages {
        b, err := m.MarshalBinary()
        if err != nil {
            return err
        }
        buffers[i] = b
    }

    _, err := c.sock.SendmsgBuffers(ctx, buffers, nil, sa, 0)
    return err
}

Why a new method rather than modifying SendMessages?

I'd suggest keeping this as a separate opt-in method initially:

  1. Zero risk to existing users — current behavior is unchanged
  2. Different memory characteristics — scatter-gather trades one large allocation for many small ones; existing users may prefer current behavior
  3. Easier to review and merge — purely additive change
  4. Can consolidate later — if proven stable, could hypothetically-theoretically become the default implementation in a future major version

Implementation Notes

  • Requires Go 1.20+ for unix.SendmsgBuffers() (aligns with your two-recent-versions policy)
  • The socket-level change would need to land in mdlayher/socket first
  • No changes to the netlink protocol or message format—just the syscall mechanics

Offer to Help

I'm happy to prepare the PRs for both repositories if you're open to this feature. I can also write tests and benchmarks comparing the two approaches across various batch sizes.

Let me know your thoughts, and thanks for maintaining this excellent library!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions