Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+45
-8
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package batching implements a socket optimized for increased throughput.
|
||||
@@ -19,27 +19,64 @@ var (
|
||||
_ ipv6.Message = ipv4.Message{}
|
||||
)
|
||||
|
||||
// Conn is a nettype.PacketConn that provides batched i/o using
|
||||
// ReceivedPacket describes a packet read by [Conn.ReadBatch].
|
||||
type ReceivedPacket struct {
|
||||
// Offset is the starting byte offset into the slab supplied to [Conn.ReadBatch].
|
||||
Offset int
|
||||
// Size is the size of the packet.
|
||||
Size int
|
||||
// Source is the source address that sent the packet.
|
||||
Source netip.AddrPort
|
||||
}
|
||||
|
||||
const (
|
||||
// ReadSlabMultiple is the minimum slab length accepted by [Conn.ReadBatch],
|
||||
// and is the suggested multiple when passing a larger value.
|
||||
ReadSlabMultiple = 1<<16 - 1
|
||||
// MinimumReadBatchSize is the minimum number of packets descriptors accepted
|
||||
// by [Conn.ReadBatch].
|
||||
MinimumReadBatchSize = udpGROCountMax
|
||||
// MaximumWriteBatchSize is the maximum number of buffs accepted by
|
||||
// [Conn.WriteBatchTo].
|
||||
MaximumWriteBatchSize = 128
|
||||
// udpGROCountMax is the maximum number of datagrams the kernel will coalesce
|
||||
// together to present in a single recvmmsg() slot.
|
||||
udpGROCountMax = 64
|
||||
)
|
||||
|
||||
// Conn is a [nettype.PacketConn] that provides batched i/o using
|
||||
// platform-specific optimizations, e.g. {recv,send}mmsg & UDP GSO/GRO.
|
||||
//
|
||||
// Conn does not support single packet reads (see ReadFromUDPAddrPort docs). It
|
||||
// is the caller's responsibility to use the appropriate read API where a
|
||||
// [nettype.PacketConn] has been upgraded to support batched i/o.
|
||||
//
|
||||
// Conn originated from (and is still used by) magicsock where its API was
|
||||
// strongly influenced by [wireguard-go/conn.Bind] constraints, namely
|
||||
// wireguard-go's ownership of packet memory.
|
||||
type Conn interface {
|
||||
nettype.PacketConn
|
||||
// ReadBatch reads messages from [Conn] into msgs. It returns the number of
|
||||
// messages the caller should evaluate for nonzero len, as a zero len
|
||||
// message may fall on either side of a nonzero.
|
||||
// ReadFromUDPAddrPort always returns an error, as UDP GRO is incompatible
|
||||
// with single packet reads. A single datagram may be multiple, coalesced
|
||||
// datagrams, and this API lacks the ability to pass that context.
|
||||
//
|
||||
// Each [ipv6.Message.OOB] must be sized to at least MinControlMessageSize().
|
||||
ReadBatch(msgs []ipv6.Message, flags int) (n int, err error)
|
||||
// TODO: consider detaching Conn from [nettype.PacketConn]
|
||||
ReadFromUDPAddrPort([]byte) (int, netip.AddrPort, error)
|
||||
// ReadBatch reads datagrams from [Conn] into slab, and describes them in
|
||||
// packets. It returns the number of populated packet descriptors. A single
|
||||
// GRO-coalesced datagram may produce multiple descriptors.
|
||||
//
|
||||
// packets must have a length >= [MinimumReadBatchSize]. slab must have a
|
||||
// length >= [ReadSlabMultiple]. ReadBatch reads only as many datagrams as
|
||||
// both arguments can accommodate.
|
||||
ReadBatch(slab []byte, packets []ReceivedPacket) (n int, err error)
|
||||
// WriteBatchTo writes buffs to addr.
|
||||
//
|
||||
// If geneve.VNI.IsSet(), then geneve is encoded into the space preceding
|
||||
// offset, and offset must equal [packet.GeneveFixedHeaderLength]. If
|
||||
// !geneve.VNI.IsSet() then the space preceding offset is ignored.
|
||||
//
|
||||
// len(buffs) must be <= batchSize supplied in TryUpgradeToConn().
|
||||
// len(buffs) must be <= [MaximumWriteBatchSize].
|
||||
//
|
||||
// WriteBatchTo may return a [neterror.ErrUDPGSODisabled] error if UDP GSO
|
||||
// was disabled as a result of a send error.
|
||||
|
||||
+3
-10
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
@@ -6,18 +6,11 @@
|
||||
package batching
|
||||
|
||||
import (
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/types/nettype"
|
||||
)
|
||||
|
||||
// TryUpgradeToConn is no-op on all platforms except linux.
|
||||
func TryUpgradeToConn(pconn nettype.PacketConn, _ string, _ int) nettype.PacketConn {
|
||||
func TryUpgradeToConn(pconn nettype.PacketConn, _ string, _ string, _ *controlknobs.Knobs) nettype.PacketConn {
|
||||
return pconn
|
||||
}
|
||||
|
||||
var controlMessageSize = 0
|
||||
|
||||
func MinControlMessageSize() int {
|
||||
return controlMessageSize
|
||||
}
|
||||
|
||||
const IdealBatchSize = 1
|
||||
+416
-156
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package batching
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
@@ -20,10 +21,13 @@ import (
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/neterror"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
)
|
||||
|
||||
// xnetBatchReaderWriter defines the batching i/o methods of
|
||||
@@ -51,26 +55,29 @@ var (
|
||||
// linuxBatchingConn is a UDP socket that provides batched i/o. It implements
|
||||
// [Conn].
|
||||
type linuxBatchingConn struct {
|
||||
pc *net.UDPConn
|
||||
xpc xnetBatchReaderWriter
|
||||
rxOffload bool // supports UDP GRO or similar
|
||||
txOffload atomic.Bool // supports UDP GSO or similar
|
||||
setGSOSizeInControl func(control *[]byte, gsoSize uint16) // typically setGSOSizeInControl(); swappable for testing
|
||||
getGSOSizeFromControl func(control []byte) (int, error) // typically getGSOSizeFromControl(); swappable for testing
|
||||
sendBatchPool sync.Pool
|
||||
pc *net.UDPConn
|
||||
xpc xnetBatchReaderWriter
|
||||
rxOffload bool // supports UDP GRO or similar
|
||||
txOffload atomic.Bool // supports UDP GSO or similar
|
||||
msgsPool sync.Pool
|
||||
rxqOverflowsMetric *clientmetric.Metric
|
||||
// neverGSOEqualTail, when non-nil and true, enables a sentinel-tail
|
||||
// workaround in the UDP GSO TX path. It points at a
|
||||
// [controlknobs.Knobs.NeverGSOEqualTail] field so the value can be
|
||||
// toggled live via the control plane without requiring a socket rebind.
|
||||
// It is read once per write at the top of [linuxBatchingConn.WriteBatchTo].
|
||||
neverGSOEqualTail *atomic.Bool
|
||||
|
||||
// readOpMu guards read operations that must perform accounting against
|
||||
// rxqOverflows in single-threaded fashion. There are no concurrent usages
|
||||
// of read operations at the time of writing (2026-03-09), but it would be
|
||||
// unidiomatic to push this responsibility onto callers.
|
||||
readOpMu sync.Mutex
|
||||
rxqOverflows uint32 // kernel pumps a cumulative counter, which we track to push a clientmetric delta value
|
||||
}
|
||||
|
||||
func (c *linuxBatchingConn) ReadFromUDPAddrPort(p []byte) (n int, addr netip.AddrPort, err error) {
|
||||
if c.rxOffload {
|
||||
// UDP_GRO is opt-in on Linux via setsockopt(). Once enabled you may
|
||||
// receive a "monster datagram" from any read call. The ReadFrom() API
|
||||
// does not support passing the GSO size and is unsafe to use in such a
|
||||
// case. Other platforms may vary in behavior, but we go with the most
|
||||
// conservative approach to prevent this from becoming a footgun in the
|
||||
// future.
|
||||
return 0, netip.AddrPort{}, errors.New("rx UDP offload is enabled on this socket, single packet reads are unavailable")
|
||||
}
|
||||
return c.pc.ReadFromUDPAddrPort(p)
|
||||
return 0, netip.AddrPort{}, errors.New("single packet reads are unsupported")
|
||||
}
|
||||
|
||||
func (c *linuxBatchingConn) SetDeadline(t time.Time) error {
|
||||
@@ -89,6 +96,15 @@ const (
|
||||
// This was initially established for Linux, but may split out to
|
||||
// GOOS-specific values later. It originates as UDP_MAX_SEGMENTS in the
|
||||
// kernel's TX path, and UDP_GRO_CNT_MAX for RX.
|
||||
//
|
||||
// As long as we use one fragment per datagram, this also serves as a
|
||||
// limit for the number of fragments we can coalesce during scatter-gather writes.
|
||||
//
|
||||
// 64 is below the 1024 of IOV_MAX (Linux) or UIO_MAXIOV (BSD),
|
||||
// and the 256 of WSABUF_MAX_COUNT (Windows).
|
||||
//
|
||||
// (2026-04) If we begin shipping datagrams in more than one fragment,
|
||||
// an independent fragment count limit needs to be implemented.
|
||||
udpSegmentMaxDatagrams = 64
|
||||
)
|
||||
|
||||
@@ -98,24 +114,63 @@ const (
|
||||
maxIPv6PayloadLen = 1<<16 - 1 - 8
|
||||
)
|
||||
|
||||
// neverGSOEqualTailSentinelPayload is appended to UDP GSO packet batches under
|
||||
// certain conditions in order to workaround Linux kernel UDP GSO bugs. In the
|
||||
// case of magicsock, 0x07 is handled as WireGuard, and wireguard-go silently
|
||||
// drops the packet as it's less than [device.MinMessageSize].
|
||||
var neverGSOEqualTailSentinelPayload = []byte{0x07}
|
||||
|
||||
// coalesceMessages iterates 'buffs', setting and coalescing them in 'msgs'
|
||||
// where possible while maintaining datagram order.
|
||||
//
|
||||
// It aggregates message components as a list of buffers without copying,
|
||||
// and expects to be used only on Linux with scatter-gather writes via sendmmsg(2).
|
||||
//
|
||||
// All msgs[i].Buffers len must be one. Will panic if there is not enough msgs
|
||||
// to coalesce all buffs.
|
||||
//
|
||||
// All msgs have their Addr field set to addr.
|
||||
//
|
||||
// All msgs[i].Buffers[0] are preceded by a Geneve header (geneve) if geneve.VNI.IsSet().
|
||||
func (c *linuxBatchingConn) coalesceMessages(addr *net.UDPAddr, geneve packet.GeneveHeader, buffs [][]byte, msgs []ipv6.Message, offset int) int {
|
||||
//
|
||||
// neverGSOEqualTail, when true, enables the sentinel-tail workaround. It is
|
||||
// loaded by the caller and passed in so a single coalesceMessages call sees a
|
||||
// consistent value even if the underlying control knob flips concurrently.
|
||||
//
|
||||
// TODO(illotum) explore MSG_ZEROCOPY for large writes (>10KB).
|
||||
func (c *linuxBatchingConn) coalesceMessages(addr *net.UDPAddr, geneve packet.GeneveHeader, buffs [][]byte, msgs []ipv6.Message, offset int, neverGSOEqualTail bool) int {
|
||||
var (
|
||||
base = -1 // index of msg we are currently coalescing into
|
||||
gsoSize int // segmentation size of msgs[base]
|
||||
dgramCnt int // number of dgrams coalesced into msgs[base]
|
||||
endBatch bool // tracking flag to start a new batch on next iteration of buffs
|
||||
base = -1 // index of msg we are currently coalescing into
|
||||
gsoSize int // segmentation size of msgs[base]
|
||||
dgramCnt int // number of dgrams coalesced into msgs[base]
|
||||
endBatchDueToSmallerTail bool // tracking flag to start a new batch on next iteration of buffs
|
||||
coalescedLen int // bytes coalesced into msgs[base]
|
||||
)
|
||||
maxPayloadLen := maxIPv4PayloadLen
|
||||
if addr.IP.To4() == nil {
|
||||
maxPayloadLen = maxIPv6PayloadLen
|
||||
}
|
||||
maxDatagramsPerGSOBatch := udpSegmentMaxDatagrams
|
||||
if neverGSOEqualTail {
|
||||
// If neverGSOEqualTail is set we might end up appending a sentinel 1-byte
|
||||
// payload, so we must leave space in our accounting.
|
||||
maxDatagramsPerGSOBatch -= 1
|
||||
maxPayloadLen -= len(neverGSOEqualTailSentinelPayload)
|
||||
}
|
||||
vniIsSet := geneve.VNI.IsSet()
|
||||
|
||||
maybeAppendSentinelTail := func() {
|
||||
if !neverGSOEqualTail || endBatchDueToSmallerTail {
|
||||
// If neverGSOEqualTail is unset we should never append a sentinel
|
||||
// payload as we are running on an unaffected kernel. Or, if we
|
||||
// already have a smaller-than-GSO sized tail, there is no need, since
|
||||
// the kernel bug we are avoiding only triggers when all fragments
|
||||
// are equal in length.
|
||||
return
|
||||
}
|
||||
msgs[base].Buffers = append(msgs[base].Buffers, neverGSOEqualTailSentinelPayload)
|
||||
}
|
||||
|
||||
for i, buff := range buffs {
|
||||
if vniIsSet {
|
||||
geneve.Encode(buff)
|
||||
@@ -124,91 +179,148 @@ func (c *linuxBatchingConn) coalesceMessages(addr *net.UDPAddr, geneve packet.Ge
|
||||
}
|
||||
if i > 0 {
|
||||
msgLen := len(buff)
|
||||
baseLenBefore := len(msgs[base].Buffers[0])
|
||||
freeBaseCap := cap(msgs[base].Buffers[0]) - baseLenBefore
|
||||
if msgLen+baseLenBefore <= maxPayloadLen &&
|
||||
// okToCoalesceWithSentinel ensures we never coalesce if a sentinel
|
||||
// 1-byte payload might be required, but gsoSize (or more specifically
|
||||
// UDP payload length) is also 1. The whole point of appending a sentinel
|
||||
// 1-byte payload is to append a smaller-than-GSO tail.
|
||||
//
|
||||
// This is defensive as a 1-byte payload, at the time of writing
|
||||
// (2026-05-28), is unlikely to occur. The smallest WireGuard
|
||||
// message size is 32 bytes ([device.MinMessageSize]), and the
|
||||
// [disco.Message] header is 62 bytes.
|
||||
//
|
||||
// It's also overly conservative as it checks for msgLen == 1, but a
|
||||
// msgLen of 1 on the tail where gsoSize is greater would also be fine.
|
||||
okToCoalesceWithSentinel := !neverGSOEqualTail || msgLen > len(neverGSOEqualTailSentinelPayload)
|
||||
if msgLen+coalescedLen <= maxPayloadLen &&
|
||||
msgLen <= gsoSize &&
|
||||
msgLen <= freeBaseCap &&
|
||||
dgramCnt < udpSegmentMaxDatagrams &&
|
||||
!endBatch {
|
||||
msgs[base].Buffers[0] = append(msgs[base].Buffers[0], make([]byte, msgLen)...)
|
||||
copy(msgs[base].Buffers[0][baseLenBefore:], buff)
|
||||
if i == len(buffs)-1 {
|
||||
c.setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
|
||||
}
|
||||
dgramCnt < maxDatagramsPerGSOBatch &&
|
||||
!endBatchDueToSmallerTail &&
|
||||
okToCoalesceWithSentinel {
|
||||
// msgs[base].Buffers[0] is set to buff[i] when a new base is set.
|
||||
// This appends a struct iovec element in the underlying struct msghdr (scatter-gather).
|
||||
msgs[base].Buffers = append(msgs[base].Buffers, buff)
|
||||
dgramCnt++
|
||||
coalescedLen += msgLen
|
||||
if msgLen < gsoSize {
|
||||
// A smaller than gsoSize packet on the tail is legal, but
|
||||
// it must end the batch.
|
||||
endBatch = true
|
||||
endBatchDueToSmallerTail = true
|
||||
}
|
||||
if i == len(buffs)-1 {
|
||||
maybeAppendSentinelTail()
|
||||
setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if dgramCnt > 1 {
|
||||
c.setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
|
||||
maybeAppendSentinelTail()
|
||||
setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
|
||||
}
|
||||
// Reset prior to incrementing base since we are preparing to start a
|
||||
// new potential batch.
|
||||
endBatch = false
|
||||
endBatchDueToSmallerTail = false
|
||||
base++
|
||||
gsoSize = len(buff)
|
||||
msgs[base].OOB = msgs[base].OOB[:0]
|
||||
msgs[base].Buffers[0] = buff
|
||||
msgs[base].Addr = addr
|
||||
dgramCnt = 1
|
||||
coalescedLen = len(buff)
|
||||
}
|
||||
return base + 1
|
||||
}
|
||||
|
||||
type sendBatch struct {
|
||||
msgs []ipv6.Message
|
||||
ua *net.UDPAddr
|
||||
// msgsBatch contains recyclable (via [linuxBatchingConn.msgsPool] elements
|
||||
// used across [linuxBatchingConn.ReadBatch] & [linuxBatchingConn.WriteBatchTo].
|
||||
type msgsBatch struct {
|
||||
msgs []ipv6.Message
|
||||
writeBatchToUDPAddr *net.UDPAddr
|
||||
}
|
||||
|
||||
func (c *linuxBatchingConn) getSendBatch() *sendBatch {
|
||||
batch := c.sendBatchPool.Get().(*sendBatch)
|
||||
func (c *linuxBatchingConn) getMsgsBatch() *msgsBatch {
|
||||
batch := c.msgsPool.Get().(*msgsBatch)
|
||||
return batch
|
||||
}
|
||||
|
||||
func (c *linuxBatchingConn) putSendBatch(batch *sendBatch) {
|
||||
for i := range batch.msgs {
|
||||
batch.msgs[i] = ipv6.Message{Buffers: batch.msgs[i].Buffers, OOB: batch.msgs[i].OOB}
|
||||
// putMsgsBatch resets each message in batch.msgs[:usedMsgs] for reuse, then
|
||||
// returns batch to its [sync.Pool].
|
||||
func (c *linuxBatchingConn) putMsgsBatch(batch *msgsBatch, usedMsgs int) {
|
||||
// Clear references to previously used packet buffers, otherwise they
|
||||
// may never be GC'd.
|
||||
for i := range batch.msgs[:usedMsgs] {
|
||||
clear((batch.msgs)[i].Buffers)
|
||||
// Non-coalesced write paths access only batch.msgs[i].Buffers[0],
|
||||
// but we append more during [linuxBatchingConn.coalesceMessages].
|
||||
// Leave index zero accessible:
|
||||
batch.msgs[i] = ipv6.Message{Buffers: batch.msgs[i].Buffers[:1], OOB: batch.msgs[i].OOB[:0]}
|
||||
}
|
||||
c.sendBatchPool.Put(batch)
|
||||
c.msgsPool.Put(batch)
|
||||
}
|
||||
|
||||
// appendSentinelTailBatchSizeThreshold represents the minimum batch size
|
||||
// required to enter [linuxBatchingConn.coalesceMessages] when
|
||||
// [linuxBatchingConn.neverGSOEqualTail] is set. If the batch of packets is less
|
||||
// than this value, and neverGSOEqualTail is set, we avoid UDP GSO altogether.
|
||||
// Appending a sentinel packet, regardless of size, is still overhead on sender,
|
||||
// middle network, and receiver.
|
||||
//
|
||||
// Coalescing (UDP GSO) greatly improves performance for sender (and receiver if
|
||||
// they support UDP GRO), but there are diminishing returns if batches are small.
|
||||
// We attempt to balance these diminishing returns against the introduction of
|
||||
// dead-weight sentinel packets.
|
||||
//
|
||||
// The initial value of 8 is a power of 2, and in the worst case leads to 6%
|
||||
// payload overhead if the batch is made up of minimum-sized WireGuard transport
|
||||
// messages (empty payload keepalives). Worst case is unlikely.
|
||||
//
|
||||
// 8 * (20 bytes IPv4 header + 8 byte UDP header + 32 byte WG message) = 480 bytes
|
||||
// sentinel tail is 20 byte IPv4 header + 8 byte UDP header + 1 byte payload = 29 bytes
|
||||
// 29/480 = 0.060...
|
||||
const appendSentinelTailBatchSizeThreshold = 8
|
||||
|
||||
func (c *linuxBatchingConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort, geneve packet.GeneveHeader, offset int) error {
|
||||
batch := c.getSendBatch()
|
||||
defer c.putSendBatch(batch)
|
||||
batch := c.getMsgsBatch()
|
||||
usedMsgs := len(buffs)
|
||||
defer c.putMsgsBatch(batch, usedMsgs)
|
||||
if addr.Addr().Is6() {
|
||||
as16 := addr.Addr().As16()
|
||||
copy(batch.ua.IP, as16[:])
|
||||
batch.ua.IP = batch.ua.IP[:16]
|
||||
copy(batch.writeBatchToUDPAddr.IP, as16[:])
|
||||
batch.writeBatchToUDPAddr.IP = batch.writeBatchToUDPAddr.IP[:16]
|
||||
} else {
|
||||
as4 := addr.Addr().As4()
|
||||
copy(batch.ua.IP, as4[:])
|
||||
batch.ua.IP = batch.ua.IP[:4]
|
||||
copy(batch.writeBatchToUDPAddr.IP, as4[:])
|
||||
batch.writeBatchToUDPAddr.IP = batch.writeBatchToUDPAddr.IP[:4]
|
||||
}
|
||||
batch.ua.Port = int(addr.Port())
|
||||
batch.writeBatchToUDPAddr.Port = int(addr.Port())
|
||||
batch.writeBatchToUDPAddr.Zone = addr.Addr().Zone()
|
||||
// Load the control knob once per write so a single call sees a consistent
|
||||
// value even if the knob flips concurrently.
|
||||
neverGSOEqualTail := c.neverGSOEqualTail != nil && c.neverGSOEqualTail.Load()
|
||||
var (
|
||||
n int
|
||||
retried bool
|
||||
)
|
||||
retry:
|
||||
if c.txOffload.Load() {
|
||||
n = c.coalesceMessages(batch.ua, geneve, buffs, batch.msgs, offset)
|
||||
if c.txOffload.Load() && (!neverGSOEqualTail || len(buffs) >= appendSentinelTailBatchSizeThreshold) {
|
||||
n = c.coalesceMessages(batch.writeBatchToUDPAddr, geneve, buffs, batch.msgs, offset, neverGSOEqualTail)
|
||||
} else {
|
||||
mutableOffset := offset // don't mutate offset across retries
|
||||
vniIsSet := geneve.VNI.IsSet()
|
||||
if vniIsSet {
|
||||
offset -= packet.GeneveFixedHeaderLength
|
||||
mutableOffset -= packet.GeneveFixedHeaderLength
|
||||
}
|
||||
for i := range buffs {
|
||||
if vniIsSet {
|
||||
geneve.Encode(buffs[i])
|
||||
}
|
||||
batch.msgs[i].Buffers[0] = buffs[i][offset:]
|
||||
batch.msgs[i].Addr = batch.ua
|
||||
batch.msgs[i].Buffers[0] = buffs[i][mutableOffset:]
|
||||
// Buffers length may be > 1 (scatter-gather) if we passed through
|
||||
// coalesceMessages during a first pass, and landed here as part of
|
||||
// goto retry.
|
||||
batch.msgs[i].Buffers = batch.msgs[i].Buffers[:1]
|
||||
batch.msgs[i].Addr = batch.writeBatchToUDPAddr
|
||||
batch.msgs[i].OOB = batch.msgs[i].OOB[:0]
|
||||
}
|
||||
n = len(buffs)
|
||||
@@ -245,68 +357,166 @@ func (c *linuxBatchingConn) writeBatch(msgs []ipv6.Message) error {
|
||||
}
|
||||
}
|
||||
|
||||
// splitCoalescedMessages splits coalesced messages from the tail of dst
|
||||
// beginning at index 'firstMsgAt' into the head of the same slice. It reports
|
||||
// the number of elements to evaluate in msgs for nonzero len (msgs[i].N). An
|
||||
// error is returned if a socket control message cannot be parsed or a split
|
||||
// operation would overflow msgs.
|
||||
func (c *linuxBatchingConn) splitCoalescedMessages(msgs []ipv6.Message, firstMsgAt int) (n int, err error) {
|
||||
for i := firstMsgAt; i < len(msgs); i++ {
|
||||
msg := &msgs[i]
|
||||
if msg.N == 0 {
|
||||
return n, err
|
||||
// getDataFromControl returns the data portion of the first control msg with
|
||||
// matching cmsgLevel, matching cmsgType, and min data len of minDataLen, in
|
||||
// control. If no matching cmsg is found or the len(control) < unix.SizeofCmsghdr,
|
||||
// this function returns nil data. A non-nil error will be returned if
|
||||
// len(control) > unix.SizeofCmsghdr but its contents cannot be parsed as a
|
||||
// socket control message.
|
||||
func getDataFromControl(control []byte, cmsgLevel, cmsgType int32, minDataLen int) ([]byte, error) {
|
||||
var (
|
||||
hdr unix.Cmsghdr
|
||||
data []byte
|
||||
rem = control
|
||||
err error
|
||||
)
|
||||
|
||||
for len(rem) > unix.SizeofCmsghdr {
|
||||
hdr, data, rem, err = unix.ParseOneSocketControlMessage(rem)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing socket control message: %w", err)
|
||||
}
|
||||
if hdr.Level == cmsgLevel && hdr.Type == cmsgType && len(data) >= minDataLen {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getRXQOverflowsFromControl returns the rxq overflows cumulative counter found
|
||||
// in control. If no rxq counter is found or the len(control) < unix.SizeofCmsghdr,
|
||||
// this function returns 0. A non-nil error will be returned if control is
|
||||
// malformed.
|
||||
func getRXQOverflowsFromControl(control []byte) (uint32, error) {
|
||||
data, err := getDataFromControl(control, unix.SOL_SOCKET, unix.SO_RXQ_OVFL, 4)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(data) >= 4 {
|
||||
return binary.NativeEndian.Uint32(data), nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// handleRXQOverflowCounter handles any rx queue overflow counter contained in
|
||||
// the tail of msgs.
|
||||
func (c *linuxBatchingConn) handleRXQOverflowCounter(msgs []ipv6.Message, rxErr error) {
|
||||
if len(msgs) == 0 || rxErr != nil || c.rxqOverflowsMetric == nil {
|
||||
return
|
||||
}
|
||||
tailMsg := msgs[len(msgs)-1] // we only care about the latest value as it's a cumulative counter
|
||||
if tailMsg.NN == 0 {
|
||||
return
|
||||
}
|
||||
rxqOverflows, err := getRXQOverflowsFromControl(tailMsg.OOB[:tailMsg.NN])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// The counter is always present once nonzero on the kernel side. Compare it
|
||||
// with our previous view, push the delta to the clientmetric, and update
|
||||
// our view.
|
||||
if rxqOverflows == c.rxqOverflows {
|
||||
return
|
||||
}
|
||||
delta := int64(rxqOverflows - c.rxqOverflows)
|
||||
c.rxqOverflowsMetric.Add(delta)
|
||||
c.rxqOverflows = rxqOverflows
|
||||
}
|
||||
|
||||
const (
|
||||
// groEnabledSlotSize is the size to supply per recvmmsg() slot when GRO is
|
||||
// enabled. If we supply less than this, coalesced datagrams may be silently
|
||||
// truncated by the kernel.
|
||||
groEnabledSlotSize = 1<<16 - 1
|
||||
// groDisabledSlotSize is the size to supply per recvmmsg() slot when GRO is
|
||||
// disabled. It's ~twice the size of max Ethernet MTU, but smaller than
|
||||
// [ReadSlabMultiple]. Without GRO we might as well read more datagrams per
|
||||
// syscall, and it's reasonable to expect datagrams to be no larger than this
|
||||
// size.
|
||||
//
|
||||
// TODO: Reconsider divisor. Older kernels without UDP GRO support may
|
||||
// benefit from more slots per recvmmsg syscall. Run benchmarks to inform.
|
||||
groDisabledSlotSize = groEnabledSlotSize / 4
|
||||
)
|
||||
|
||||
func (c *linuxBatchingConn) ReadBatch(slab []byte, packets []ReceivedPacket) (n int, err error) {
|
||||
if len(slab) < ReadSlabMultiple {
|
||||
return 0, fmt.Errorf("len(slab): %d < ReadSlabMultiple(%d)", len(slab), ReadSlabMultiple)
|
||||
}
|
||||
if len(packets) < MinimumReadBatchSize {
|
||||
return 0, fmt.Errorf("len(packets): %d < MinimumReadBatchSize(%d)", len(packets), MinimumReadBatchSize)
|
||||
}
|
||||
c.readOpMu.Lock()
|
||||
defer c.readOpMu.Unlock()
|
||||
batch := c.getMsgsBatch()
|
||||
recvmmsgSlotSize := groEnabledSlotSize
|
||||
maxMsgsPerRecvmmsgSlot := udpGROCountMax
|
||||
if !c.rxOffload {
|
||||
recvmmsgSlotSize = groDisabledSlotSize
|
||||
maxMsgsPerRecvmmsgSlot = 1
|
||||
}
|
||||
maxMsgCount := min(
|
||||
len(slab)/recvmmsgSlotSize, // how many slots fit in slab
|
||||
len(packets)/maxMsgsPerRecvmmsgSlot, // how many datagrams fit in packets
|
||||
len(batch.msgs), // how many [ipv6.Message] we have
|
||||
)
|
||||
defer c.putMsgsBatch(batch, maxMsgCount)
|
||||
rem := slab
|
||||
for i := range maxMsgCount {
|
||||
end := recvmmsgSlotSize
|
||||
if len(rem) < recvmmsgSlotSize {
|
||||
end = len(rem)
|
||||
}
|
||||
batch.msgs[i].Buffers[0] = rem[:end]
|
||||
batch.msgs[i].OOB = batch.msgs[i].OOB[:cap(batch.msgs[i].OOB)]
|
||||
rem = rem[end:]
|
||||
}
|
||||
n, err = c.xpc.ReadBatch(batch.msgs[:maxMsgCount], 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
c.handleRXQOverflowCounter(batch.msgs[:n], err)
|
||||
return fillReceivedPackets(batch.msgs[:n], recvmmsgSlotSize, packets, c.rxOffload)
|
||||
}
|
||||
|
||||
func fillReceivedPackets(msgs []ipv6.Message, slabOffset int, packets []ReceivedPacket, rxOffload bool) (n int, err error) {
|
||||
for i, msg := range msgs {
|
||||
var (
|
||||
gsoSize int
|
||||
start int
|
||||
end = msg.N
|
||||
numToSplit = 1
|
||||
)
|
||||
gsoSize, err = c.getGSOSizeFromControl(msg.OOB[:msg.NN])
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if gsoSize > 0 {
|
||||
numToSplit = (msg.N + gsoSize - 1) / gsoSize
|
||||
end = gsoSize
|
||||
}
|
||||
for j := 0; j < numToSplit; j++ {
|
||||
if n > i {
|
||||
return n, errors.New("splitting coalesced packet resulted in overflow")
|
||||
if rxOffload {
|
||||
gsoSize, err = getGSOSizeFromControl(msg.OOB[:msg.NN])
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
copied := copy(msgs[n].Buffers[0], msg.Buffers[0][start:end])
|
||||
msgs[n].N = copied
|
||||
msgs[n].Addr = msg.Addr
|
||||
start = end
|
||||
end += gsoSize
|
||||
if end > msg.N {
|
||||
end = msg.N
|
||||
if gsoSize > 0 {
|
||||
numToSplit = (msg.N + gsoSize - 1) / gsoSize
|
||||
}
|
||||
}
|
||||
addrPort := msg.Addr.(*net.UDPAddr).AddrPort()
|
||||
regionOffset := i * slabOffset // region may contain multiple coalesced packets
|
||||
for j := 0; j < numToSplit; j++ {
|
||||
if n >= len(packets) {
|
||||
return n, fmt.Errorf("%w: filling received packet metadata resulted in overflow", io.ErrShortBuffer)
|
||||
}
|
||||
end := msg.N
|
||||
if gsoSize > 0 && start+gsoSize < end {
|
||||
end = start + gsoSize
|
||||
}
|
||||
packets[n] = ReceivedPacket{
|
||||
Offset: regionOffset + start,
|
||||
Size: end - start,
|
||||
Source: addrPort,
|
||||
}
|
||||
n++
|
||||
}
|
||||
if i != n-1 {
|
||||
// It is legal for bytes to move within msg.Buffers[0] as a result
|
||||
// of splitting, so we only zero the source msg len when it is not
|
||||
// the destination of the last split operation above.
|
||||
msg.N = 0
|
||||
start = end
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *linuxBatchingConn) ReadBatch(msgs []ipv6.Message, flags int) (n int, err error) {
|
||||
if !c.rxOffload || len(msgs) < 2 {
|
||||
return c.xpc.ReadBatch(msgs, flags)
|
||||
}
|
||||
// Read into the tail of msgs, split into the head.
|
||||
readAt := len(msgs) - 2
|
||||
numRead, err := c.xpc.ReadBatch(msgs[readAt:], 0)
|
||||
if err != nil || numRead == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return c.splitCoalescedMessages(msgs, readAt)
|
||||
}
|
||||
|
||||
func (c *linuxBatchingConn) LocalAddr() net.Addr {
|
||||
return c.pc.LocalAddr().(*net.UDPAddr)
|
||||
}
|
||||
@@ -319,19 +529,45 @@ func (c *linuxBatchingConn) Close() error {
|
||||
return c.pc.Close()
|
||||
}
|
||||
|
||||
// tryEnableRXQOverflowsCounter attempts to enable the SO_RXQ_OVFL socket option
|
||||
// on pconn, and returns the result. SO_RXQ_OVFL was added in Linux v2.6.33.
|
||||
func tryEnableRXQOverflowsCounter(pconn nettype.PacketConn) (enabled bool) {
|
||||
if c, ok := pconn.(*net.UDPConn); ok {
|
||||
rc, err := c.SyscallConn()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rc.Control(func(fd uintptr) {
|
||||
enabled = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RXQ_OVFL, 1) == nil
|
||||
})
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
// tryEnableUDPOffload attempts to enable the UDP_GRO socket option on pconn,
|
||||
// and returns two booleans indicating TX and RX UDP offload support.
|
||||
func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
|
||||
// and returns two booleans indicating TX and RX UDP offload support. If knobs
|
||||
// is non-nil, UDP GSO and/or UDP GRO may be disabled via control-plane node
|
||||
// attributes.
|
||||
func tryEnableUDPOffload(pconn nettype.PacketConn, knobs *controlknobs.Knobs) (hasTX bool, hasRX bool) {
|
||||
disableGSO := envknob.Bool("TS_DEBUG_DISABLE_UDP_GSO") ||
|
||||
(knobs != nil && knobs.DisableUDPGSO.Load())
|
||||
disableGRO := envknob.Bool("TS_DEBUG_DISABLE_UDP_GRO") ||
|
||||
(knobs != nil && knobs.DisableUDPGRO.Load())
|
||||
if c, ok := pconn.(*net.UDPConn); ok {
|
||||
rc, err := c.SyscallConn()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = rc.Control(func(fd uintptr) {
|
||||
_, errSyscall := syscall.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT)
|
||||
hasTX = errSyscall == nil
|
||||
errSyscall = syscall.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
|
||||
hasRX = errSyscall == nil
|
||||
var errSyscall error
|
||||
if !disableGSO {
|
||||
_, errSyscall = syscall.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT)
|
||||
hasTX = errSyscall == nil
|
||||
}
|
||||
if !disableGRO {
|
||||
errSyscall = syscall.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
|
||||
hasRX = errSyscall == nil
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return false, false
|
||||
@@ -340,32 +576,25 @@ func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
|
||||
return hasTX, hasRX
|
||||
}
|
||||
|
||||
// getGSOSizeFromControl returns the GSO size found in control. If no GSO size
|
||||
// is found or the len(control) < unix.SizeofCmsghdr, this function returns 0.
|
||||
// A non-nil error will be returned if len(control) > unix.SizeofCmsghdr but
|
||||
// its contents cannot be parsed as a socket control message.
|
||||
// getGSOSizeFromControl returns the GSO size found in control associated with a
|
||||
// cmsg type of UDP_GRO, which the kernel populates in the read direction. If no
|
||||
// GSO size is found or the len(control) < unix.SizeofCmsghdr, this function
|
||||
// returns 0. A non-nil error will be returned if control is malformed.
|
||||
func getGSOSizeFromControl(control []byte) (int, error) {
|
||||
var (
|
||||
hdr unix.Cmsghdr
|
||||
data []byte
|
||||
rem = control
|
||||
err error
|
||||
)
|
||||
|
||||
for len(rem) > unix.SizeofCmsghdr {
|
||||
hdr, data, rem, err = unix.ParseOneSocketControlMessage(rem)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error parsing socket control message: %w", err)
|
||||
}
|
||||
if hdr.Level == unix.SOL_UDP && hdr.Type == unix.UDP_GRO && len(data) >= 2 {
|
||||
return int(binary.NativeEndian.Uint16(data[:2])), nil
|
||||
}
|
||||
data, err := getDataFromControl(control, unix.SOL_UDP, unix.UDP_GRO, 2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(data) >= 2 {
|
||||
return int(binary.NativeEndian.Uint16(data)), nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// setGSOSizeInControl sets a socket control message in control containing
|
||||
// gsoSize. If len(control) < controlMessageSize control's len will be set to 0.
|
||||
// gsoSize with an associated cmsg type of UDP_SEGMENT, which we are responsible
|
||||
// for populating prior to writing towards the kernel. If len(control) < controlMessageSize
|
||||
// control's len will be set to 0.
|
||||
func setGSOSizeInControl(control *[]byte, gsoSize uint16) {
|
||||
*control = (*control)[:0]
|
||||
if cap(*control) < int(unsafe.Sizeof(unix.Cmsghdr{})) {
|
||||
@@ -383,10 +612,39 @@ func setGSOSizeInControl(control *[]byte, gsoSize uint16) {
|
||||
*control = (*control)[:unix.CmsgSpace(2)]
|
||||
}
|
||||
|
||||
var (
|
||||
rxqOverflowsMetricsMu sync.Mutex
|
||||
rxqOverflowsMetricsByName map[string]*clientmetric.Metric
|
||||
)
|
||||
|
||||
// getRXQOverflowsMetric returns a counter-based [*clientmetric.Metric] for the
|
||||
// provided name in a thread-safe manner. Callers may pass the same metric name
|
||||
// multiple times, which is common across rebinds of the underlying, associated
|
||||
// [Conn].
|
||||
func getRXQOverflowsMetric(name string) *clientmetric.Metric {
|
||||
if len(name) == 0 {
|
||||
return nil
|
||||
}
|
||||
rxqOverflowsMetricsMu.Lock()
|
||||
defer rxqOverflowsMetricsMu.Unlock()
|
||||
m, ok := rxqOverflowsMetricsByName[name]
|
||||
if ok {
|
||||
return m
|
||||
}
|
||||
if rxqOverflowsMetricsByName == nil {
|
||||
rxqOverflowsMetricsByName = make(map[string]*clientmetric.Metric)
|
||||
}
|
||||
m = clientmetric.NewCounter(name)
|
||||
rxqOverflowsMetricsByName[name] = m
|
||||
return m
|
||||
}
|
||||
|
||||
// TryUpgradeToConn probes the capabilities of the OS and pconn, and upgrades
|
||||
// pconn to a [Conn] if appropriate. A batch size of [IdealBatchSize] is
|
||||
// suggested for the best performance.
|
||||
func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int) nettype.PacketConn {
|
||||
// pconn to a [Conn] if appropriate. If len(rxqOverflowsMetricName) is
|
||||
// nonzero, then read ops will propagate the SO_RXQ_OVFL control message counter
|
||||
// to a clientmetric with the supplied name. If knobs is non-nil, UDP GSO
|
||||
// and/or UDP GRO may be disabled via control-plane node attributes.
|
||||
func TryUpgradeToConn(pconn nettype.PacketConn, network string, rxqOverflowsMetricName string, knobs *controlknobs.Knobs) nettype.PacketConn {
|
||||
if runtime.GOOS != "linux" {
|
||||
// Exclude Android.
|
||||
return pconn
|
||||
@@ -394,7 +652,8 @@ func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int) n
|
||||
if network != "udp4" && network != "udp6" {
|
||||
return pconn
|
||||
}
|
||||
if strings.HasPrefix(hostinfo.GetOSVersion(), "2.") {
|
||||
osVer := hostinfo.GetOSVersion()
|
||||
if strings.HasPrefix(osVer, "2.") {
|
||||
// recvmmsg/sendmmsg were added in 2.6.33, but we support down to
|
||||
// 2.6.32 for old NAS devices. See https://github.com/tailscale/tailscale/issues/6807.
|
||||
// As a cheap heuristic: if the Linux kernel starts with "2", just
|
||||
@@ -408,23 +667,21 @@ func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int) n
|
||||
return pconn
|
||||
}
|
||||
b := &linuxBatchingConn{
|
||||
pc: uc,
|
||||
getGSOSizeFromControl: getGSOSizeFromControl,
|
||||
setGSOSizeInControl: setGSOSizeInControl,
|
||||
sendBatchPool: sync.Pool{
|
||||
pc: uc,
|
||||
msgsPool: sync.Pool{
|
||||
New: func() any {
|
||||
ua := &net.UDPAddr{
|
||||
IP: make([]byte, 16),
|
||||
}
|
||||
msgs := make([]ipv6.Message, batchSize)
|
||||
msgs := make([]ipv6.Message, MaximumWriteBatchSize)
|
||||
for i := range msgs {
|
||||
msgs[i].Buffers = make([][]byte, 1)
|
||||
msgs[i].Addr = ua
|
||||
msgs[i].OOB = make([]byte, controlMessageSize)
|
||||
}
|
||||
return &sendBatch{
|
||||
ua: ua,
|
||||
msgs: msgs,
|
||||
return &msgsBatch{
|
||||
writeBatchToUDPAddr: ua,
|
||||
msgs: msgs,
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -438,23 +695,26 @@ func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int) n
|
||||
panic("bogus network")
|
||||
}
|
||||
var txOffload bool
|
||||
txOffload, b.rxOffload = tryEnableUDPOffload(uc)
|
||||
txOffload, b.rxOffload = tryEnableUDPOffload(uc, knobs)
|
||||
b.txOffload.Store(txOffload)
|
||||
if knobs != nil {
|
||||
b.neverGSOEqualTail = &knobs.NeverGSOEqualTail
|
||||
}
|
||||
if len(rxqOverflowsMetricName) > 0 && tryEnableRXQOverflowsCounter(uc) {
|
||||
// Don't register the metric unless the socket option has been
|
||||
// successfully set, otherwise we will report a misleading zero value
|
||||
// counter on the wire. This is one reason why we prefer to handle
|
||||
// clientmetric instantiation internally, vs letting callers pass them
|
||||
// to TryUpgradeToConn.
|
||||
b.rxqOverflowsMetric = getRXQOverflowsMetric(rxqOverflowsMetricName)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var controlMessageSize = -1 // bomb if used for allocation before init
|
||||
|
||||
func init() {
|
||||
// controlMessageSize is set to hold a UDP_GRO or UDP_SEGMENT control
|
||||
// message. These contain a single uint16 of data.
|
||||
controlMessageSize = unix.CmsgSpace(2)
|
||||
controlMessageSize =
|
||||
unix.CmsgSpace(2) + // UDP_GRO or UDP_SEGMENT gsoSize (uint16)
|
||||
unix.CmsgSpace(4) // SO_RXQ_OVFL counter (uint32)
|
||||
}
|
||||
|
||||
// MinControlMessageSize returns the minimum control message size required to
|
||||
// support read batching via [Conn.ReadBatch].
|
||||
func MinControlMessageSize() int {
|
||||
return controlMessageSize
|
||||
}
|
||||
|
||||
const IdealBatchSize = 128
|
||||
Reference in new issue
Block a user