Documentation
¶
Overview ¶
Package afxdp is the packetio backend for Linux AF_XDP.
It is a thin adapter over github.com/atoonk/go-afxdp: the two libraries were designed around the same model -- a region of fixed-size frames, descriptors that name one frame each, and the four verbs that move a frame between the pool, the application and the NIC -- so almost every method here forwards.
Where it differs from the mlx5 backend, and why it matters:
Each queue has its own frame region, because each AF_XDP socket maps its own. Capabilities reports SharedRegion false, and a frame received on one queue cannot be transmitted on another without a copy.
The kernel is in the path. It owns the descriptors after Transmit and does the work in a soft interrupt, which is real CPU this library cannot see. Measure it with the whole machine, not with this process.
Opening is backend-specific: the filter that decides what reaches the sockets is an AF_XDP concept with no equivalent elsewhere, so Open takes options of its own. WithXDP carries a go-afxdp option through for anything this package does not name.
**A socket is bound per queue, and an unbound queue goes to the kernel.** The XDP program delivers a packet to the socket on the queue the card hashed it to; a queue with no socket is passed up the stack, silently and by design. Opening fewer queues than the card has therefore means the card's hash decides whether anything arrives: one flow lands on exactly one queue, and if that queue is outside the range opened, the receive loop returns nothing and nothing is wrong. Measured on a 48-queue ConnectX: four queues received none of 200 kpps, forty-eight received all of it. Open every queue, or narrow what the card spreads with `ethtool -X`. mlx5 has no equivalent problem -- a steering rule delivers to the queue group whatever the hash decided.
**On a tagged interface, the kernel's own VLAN filter comes first.** A driver with `rx-vlan-filter on` -- the default on mlx5e -- drops a tag no VLAN sub-interface has registered, in the card, before the XDP hook runs. The program never sees the packet, the filter matches nothing, and a receive loop reports zero with no error anywhere. Register the id (`ip link add link eth0 name eth0.100 type vlan id 100`) or turn the filter off (`ethtool -K eth0 rx-vlan-filter off`). Direct Verbs does not have this problem: its steering rule carries the id and lives in this process's own flow table, below the netdev filter.
Opening attaches an XDP program to the interface, and Close detaches it. A process killed between the two leaves it attached, and the next Open fails with "already attached ... likely owned by another running process". Nothing in userspace can prevent that -- the kernel keeps the program because the link holds a reference, not the process -- so a program that may be killed should say how to clear it:
sudo ip link set dev eth0 xdp off
This is worth knowing before it happens: the interface keeps working for the kernel's own traffic, so the only symptom is that this library will not open.
Index ¶
- func SteeringOption(f packetio.SteeringFilter) (xdp.Option, error)
- type Device
- func (d *Device) Capabilities() packetio.Capabilities
- func (d *Device) Close() error
- func (d *Device) Detach()
- func (d *Device) Fleet() *xdp.Fleet
- func (d *Device) NumRxQueues() int
- func (d *Device) NumTxQueues() int
- func (d *Device) Rx(i int) *RxQueue
- func (d *Device) RxQueue(i int) packetio.RxQueue
- func (d *Device) Tx(i int) *TxQueue
- func (d *Device) TxQueue(i int) packetio.TxQueue
- type Option
- type RxQueue
- func (q *RxQueue) Close() error
- func (q *RxQueue) Err() error
- func (q *RxQueue) Fill(n int) int
- func (q *RxQueue) NumFreeFillSlots() int
- func (q *RxQueue) NumFreeFrames() int
- func (q *RxQueue) NumReceived() int
- func (q *RxQueue) Pin() (int, error)
- func (q *RxQueue) Poll(timeout time.Duration) (int, error)
- func (q *RxQueue) Receive(max int) []packetio.Desc
- func (q *RxQueue) ReceivePackets(maxFrames int) []xdp.Packet
- func (q *RxQueue) Recycle(descs []packetio.Desc)
- func (q *RxQueue) RecyclePackets(pkts []xdp.Packet)
- func (q *RxQueue) Region() packetio.Region
- func (q *RxQueue) Socket() *xdp.Socket
- func (q *RxQueue) Stats() (packetio.RxStats, error)
- type TxQueue
- func (q *TxQueue) Alloc(n int) []packetio.Desc
- func (q *TxQueue) Close() error
- func (q *TxQueue) Complete(max int) int
- func (q *TxQueue) Err() error
- func (q *TxQueue) Free(descs []packetio.Desc)
- func (q *TxQueue) NumCompleted() int
- func (q *TxQueue) NumFreeFrames() int
- func (q *TxQueue) NumFreeSlots() int
- func (q *TxQueue) NumInFlight() int
- func (q *TxQueue) Pin() (int, error)
- func (q *TxQueue) Reclaim(max int, out []packetio.Desc) []packetio.Desc
- func (q *TxQueue) Region() packetio.Region
- func (q *TxQueue) SendBatch(payloads [][]byte) (int, error)
- func (q *TxQueue) SendFunc(count int, build func(i int, frame []byte) int) (int, error)
- func (q *TxQueue) Socket() *xdp.Socket
- func (q *TxQueue) Stats() (packetio.TxStats, error)
- func (q *TxQueue) Transmit(descs []packetio.Desc) int
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SteeringOption ¶
func SteeringOption(f packetio.SteeringFilter) (xdp.Option, error)
SteeringOption compiles a backend-neutral filter into a go-afxdp option.
go-afxdp's matches are ORed, and its only AND is a source-and-destination prefix pair, so not every SteeringFilter has an XDP form. What is expressible: promiscuous; any number of ports of one protocol; any number of source or destination prefixes; exactly one source and one destination prefix together; one EtherType; one IP protocol. A filter that needs an AND go-afxdp cannot express is refused with packetio.ErrUnsupported rather than installed as something wider.
Two different things about VLANs, and it is worth keeping them apart.
A VLAN cannot be matched at all: go-afxdp skips an 802.1Q tag transparently so the same program works whether or not the NIC strips it, and there is no match on the identifier. A filter naming one is refused; on a tagged interface the traffic is usually distinguished by port or address anyway. For anything beyond this vocabulary, pass go-afxdp's own matches to Open directly.
Separately, and more likely to waste an afternoon: on a tagged interface the packets may never reach the program at all. See the note on the package doc about rx-vlan-filter.
Types ¶
type Device ¶
type Device struct {
// contains filtered or unexported fields
}
Device is a NIC opened for AF_XDP, one socket per queue.
func NewDevice ¶
NewDevice wraps a fleet the caller already has, for an application that manages the fleet's lifetime itself -- keeping one attached across several runs, say. Close then closes that fleet, so do not also close it directly.
func (*Device) Capabilities ¶
func (d *Device) Capabilities() packetio.Capabilities
Capabilities reports what this backend and this NIC can do.
func (*Device) Close ¶
Close detaches the program and closes every socket, unless Detach was called first, in which case the fleet belongs to somebody else and is left alone.
It is safe to call twice, but not from two goroutines at once.
func (*Device) Detach ¶
func (d *Device) Detach()
Detach drops the reference to the fleet without closing it, for an application that owns the fleet and is only done with this view of it.
func (*Device) Fleet ¶
Fleet is the underlying go-afxdp fleet, for the things this API does not describe: link state, the attached program, kernel ring counters.
func (*Device) NumRxQueues ¶
NumRxQueues is how many receive queues were opened.
func (*Device) NumTxQueues ¶
NumTxQueues and NumRxQueues are how many queues were opened. AF_XDP binds one socket per queue and it both sends and receives, so these are equal.
type Option ¶
type Option func(*config)
An Option configures a device at Open.
It is this package's own type rather than go-afxdp's so that go-afxdp's API is not inside packetio's compatibility promise: an option changing shape there would otherwise be a breaking change here, for callers who never named that package. WithXDP is the way through for anything not named here.
func WithAffinity ¶
WithAffinity places the workers on the given processors, one per queue in order, instead of letting the backend choose.
Placement is automatic without this: go-afxdp puts each worker beside the processor its queue's interrupt lands on, which is most of the difference between a good AF_XDP number and a poor one. Reach for this only when the machine is partitioned and the automatic choice would take cores that belong to something else. Passing no processors is WithoutAffinity.
func WithFrameSize ¶
WithFrameSize sets the size of one frame in bytes. The default is 2048; some drivers need 4096 to run zero-copy, and go-afxdp already picks 4096 where it knows that to be true.
func WithFrames ¶
WithFrames sets how many frames the region holds, across both directions. The default is go-afxdp's, currently 8192.
func WithMultiBuffer ¶ added in v0.1.4
func WithMultiBuffer() Option
WithMultiBuffer binds the sockets for packets that span several frames -- a jumbo frame over a 2048-byte frame size -- marked with OptContinued on every frame but the last, as packetio.Capabilities.MultiBuffer describes. It is go-afxdp's WithMultiBuffer under the name every backend here uses.
Receive counts its batch in frames and may end it part-way through a chain, the rest arriving next call; RxQueue.ReceivePackets returns whole packets.
func WithQueues ¶
WithQueues limits how many queues to bind, starting from queue 0. The default binds every queue on the interface, which is what keeps RSS-distributed traffic from landing on a queue nobody is reading.
AF_XDP binds one socket per queue and that socket both sends and receives, so unlike the other backends there is no separate transmit and receive count: this sets both.
func WithSteering ¶
func WithSteering(f packetio.SteeringFilter) Option
WithSteering asks for only the packets a filter matches, leaving the rest to the kernel. It is the backend-neutral spelling of a receive filter; see SteeringOption for what AF_XDP can and cannot express.
func WithXDP ¶
WithXDP passes go-afxdp options straight through, for what this package does not name: the XDP program, the ring geometry, the wakeup flags.
afxdp.Open("eth0", afxdp.WithXDP(xdp.WithBusyPoll(50, 64)))
Options are applied in the order given, here and above, so a WithXDP that repeats one of the named options above wins if it comes after it.
func WithoutAffinity ¶
func WithoutAffinity() Option
WithoutAffinity leaves the workers wherever the scheduler puts them.
For a goroutine that drives a queue and nothing else this costs throughput. It is the right choice when something else on the machine owns processor placement, or when the worker does enough other work that tying it to one core is wrong.
type RxQueue ¶
type RxQueue struct {
// contains filtered or unexported fields
}
RxQueue is one AF_XDP socket's receive side.
func (*RxQueue) Err ¶
Err reports that the queue is out of service, or nil.
AF_XDP has no failure that outlives one call: the kernel drops what it has nowhere to put and counts it. So this reports only that the socket is closed.
func (*RxQueue) Fill ¶
Fill posts up to n frames for the kernel to receive into and returns how many it posted. A receiver that stops filling stops receiving.
Fill also wakes the driver when it has parked. go-afxdp's Fill only writes the ring -- its Poll is what restarts a parked driver -- but the packetio contract is that Fill/Receive with no Poll must work, because that is how every other backend is driven. Without this kick, a queue whose NAPI loop completed while the ring was quiet never posts another descriptor: measured on the ConnectX-6 Dx, seven of eight queues took exactly their initial 1,024 frames and then nothing, for hours, while the port dropped 2.5 billion packets and the fill rings sat provably full. The check is one atomic load when the driver is running; the recvfrom is paid only when it parked.
func (*RxQueue) NumFreeFillSlots ¶
NumFreeFillSlots is how many more frames the receive ring can hold.
func (*RxQueue) NumFreeFrames ¶
NumFreeFrames is how many frames are in the free pool.
func (*RxQueue) NumReceived ¶
NumReceived is how many packets are ready right now. AF_XDP has no way to ask without taking them, so this polls without waiting.
func (*RxQueue) Poll ¶
Poll waits until at least one packet has arrived or timeout elapses. Unlike the mlx5 backend this really sleeps rather than spinning.
func (*RxQueue) Receive ¶
Receive takes up to max received packets. The returned slice is owned by the queue and reused by the next call.
func (*RxQueue) ReceivePackets ¶
ReceivePackets takes up to maxFrames frames' worth of packets, each returned as the run of frames it occupies. Do not mix it with Receive on one queue: they consume the same ring and count differently.
func (*RxQueue) RecyclePackets ¶
RecyclePackets returns whole packets, however many frames each took.
type TxQueue ¶
type TxQueue struct {
// contains filtered or unexported fields
}
TxQueue is one AF_XDP socket's transmit side.
It is owned by one goroutine, as everywhere in this API. The first call from that goroutine also places it on the queue's processor; see go-afxdp's Pin.
func (*TxQueue) Close ¶
Close releases the queue. The socket is shared with the receive side and is closed with the device.
func (*TxQueue) Complete ¶
Complete reclaims up to max frames the kernel has finished with, returning them to the pool, and reports how many.
func (*TxQueue) Err ¶
Err reports that the queue is out of service, or nil.
AF_XDP has no failure that outlives one call: the kernel refuses a bad descriptor and carries on, and its counters say how often. So this reports only that the socket is closed, which is the honest answer here.
func (*TxQueue) Free ¶
Free returns frames to the pool without transmitting them.
A descriptor this queue's region does not contain is refused and counted, not appended: it would come back from a later Alloc as an address the kernel rejects, or worse, one belonging to another socket. Each AF_XDP queue maps its own region, so "another queue's frame" is a real and easy mistake here.
func (*TxQueue) NumCompleted ¶
NumCompleted is how many frames Complete would reclaim right now.
func (*TxQueue) NumFreeFrames ¶
NumFreeFrames is how many frames are in the free pool.
func (*TxQueue) NumFreeSlots ¶
NumFreeSlots is how many more frames the transmit ring can accept.
func (*TxQueue) NumInFlight ¶
NumInFlight is how many frames the kernel currently owns.
func (*TxQueue) Pin ¶
Pin places the calling goroutine on this queue's processor and reports which one, or -1 if placement is off. The packet path does it on first use; a caller wanting to know before the first batch asks here.
func (*TxQueue) Reclaim ¶
Reclaim is Complete for frames that belong somewhere else.
AF_XDP cannot name the frames it reclaimed: Complete drains the completion ring straight into a pool, and there is no way to see which addresses came back. So this completes and returns nothing, and a forwarder on this backend must be opened with go-afxdp's WithTxReuseRxFrames, which makes Complete return each frame to the pool its address belongs to rather than to the transmit pool. Without it a receive frame transmitted here leaks into the transmit pool and the receive side starves -- go-afxdp keeps a separate pool per direction, so this is a real trap and not a theoretical one.
Capabilities.HandsBackFrames is false here, which is how a caller written against the interface finds this out rather than discovering it as a stall.
func (*TxQueue) SendBatch ¶
SendBatch transmits whole payloads, splitting any that do not fit a frame across several. It reports how many payloads went out.
func (*TxQueue) Socket ¶
Socket is the underlying go-afxdp socket, for what this API does not describe: the wakeup flags, the kernel's own ring counters, placement.
func (*TxQueue) Transmit ¶
Transmit hands descriptors to the kernel and returns how many it took, always a prefix. It publishes the batch before returning; there is no separate kick.
A descriptor that does not name at least one byte inside one frame ends the batch: the prefix before it is transmitted and the rest is left with the caller. That is checked here rather than left to the kernel, which counts a bad descriptor in tx_invalid_descs and carries on -- so the caller would get a full-length return and no way to know which descriptor was dropped.