4 · Talking between machines Draft
Designing and checking the feed loop
On this page
How the program behaves
- Poll, never wait. The hot thread’s loop is a non-blocking
recvand the work on what it returns, and on an empty socket it callsrecvagain: no blocking wait, sleep or scheduler yield. A thread that sleeps pays 5–20 µs to wake, and 100–200 µs after a quiet spell, on the packet that ends the quiet. - One
recvper batch of messages, into one buffer the thread owns, allocated at startup. Each complete message is parsed where it lies, withfrom_le_bytesor azerocopyoverlay, and the partial tail is moved to the front for the nextrecvto append to. Therecvis the only copy, and nothing is allocated. Where the venue requires TLS, the records are decrypted in user space in the same thread, just ahead of the framer: measure kTLS against that path if its offloads or copy savings may help. Count allocations for the exact TLS API and version; a library name alone does not establish hot-path allocation behaviour. - Binary where the venue offers it: 10–20 ns for a 48-byte fixed layout, against 1–3 µs in a running program, and an allocation per owned string field, for the same fields as 200 bytes of JSON through
serde_json. A few venues now offer a binary encoding of the same streams. Where only JSON is offered, it is parsed into borrowed&strfields, so that at least the allocations go. - Connections are warm: opened, authenticated and subscribed at startup, kept alive with the venue’s heartbeats, each with a spare pre-connected beside it. An idle spare may still need authentication, subscription and book recovery; a subscribed and sequence-aligned spare can make failover a switch, not by 140–210 ms of handshakes to a venue 35 ms away and a congestion window of 10 segments.
net.ipv4.tcp_slow_start_after_idle=0keeps a quiet order connection’s window from shrinking back towards that. Reconnects run on a housekeeping thread with randomised, growing delays. - One writer per socket. The order gateway alone writes the order socket, fed by single-producer single-consumer rings from the strategies; two writers would contend for its lock and could splice two messages into one corrupt stream.
- Sends are batched into one
writewhere the protocol allows: two orders that leave together go out in one call and one segment, from one buffer or several withwrite_vectored.MSG_MORE, orTCP_CORKset and cleared, is used only around a single message written in several calls; a cork left on holds data for up to 200 ms.
How it is checked
strace -c -p <tid>, orperf trace -s -t <tid>with far less overhead, on each hot thread after warm-up shows onerecvfromorrecvmsgper batch and nothing else, the empty polls counted asEAGAINerrors, and on the gateway onewriteper batch of orders besides. Anepoll_wait,futex,nanosleep,mmaporbrkamong them is a wait or an allocation that has crept in.voluntary_ctxt_switchesin/proc/<pid>/task/<tid>/statusstays flat on every hot thread; each increment is a wake-up paid./proc/net/softnet_statshows the second column, packets dropped, at zero, and the third, time squeezes, not growing on the hot core’s row. The card’s missed-frame counters inethtool -Sstay flat.ss -tirecords sender-side retransmissions and the actualrto; do not derive it by adding RTT to 200 ms. On an incoming feed these sender counters do not diagnose the venue’s retransmissions; inspect receive-side out-of-order counters and captures too./proc/interrupts, read twice ten seconds apart, grows only on the hot queue’s row in the hot core’s column, and in the columns of cores 0–2.- The wire-to-decision histogram, from the card’s receive stamp to the strategy’s decision, has its p99 within a few microseconds of its p50 on a quiet feed. A p99 tens of microseconds higher is a wake-up, a coalescing hold or an interrupt on the wrong core; a p99.9 in tens of milliseconds is a lost segment being recovered, and one near 200 ms a retransmission timeout.
The table
| Need | Mechanism it answers | How | How to check |
|---|---|---|---|
| The hot queue’s work on the hot core, nothing else there | Interrupt steering; a softirq on another core | Hot queue’s smp_affinity_list to the hot core, the rest to 0–2; irqbalance off; SO_PREFER_BUSY_POLL | /proc/interrupts, column by column |
| The hot flow on a queue of its own | RSS hashing; RPS and RFS moving the walk | An ntuple rule with ethtool -N; ethtool -X weights; RPS and RFS off; XPS for sends | ethtool -n lists the rule; /proc/interrupts |
| No hold before the protocol walk | Coalescing; GRO and LRO merging | ethtool -C <iface> adaptive-rx off rx-usecs 0; ethtool -K <iface> gro off lro off | ethtool -c, ethtool -k; the histogram’s p50 |
| No drops in a burst | A full ring; a small receive buffer | ethtool -G <iface> rx 4096; SO_RCVBUF set before connect | ethtool -S missed counters; softnet_stat drops; ss -tm drops |
| No wake-ups | The sleep and wake path; C-state exit | Non-blocking recv in a loop; SO_BUSY_POLL at 50 µs; C-states capped | voluntary_ctxt_switches flat |
| No Nagle stall | Nagle with delayed ACK | TCP_NODELAY; one write per message or batch | No 40 ms gaps in tcpdump -ttt or the order histogram |
No copy past the recv, no allocation | Text parsing; JSON’s allocations; the TLS library’s per-record buffers | One owned buffer; TLS in the reading thread; parse in place; binary encoding | Counting allocator flat; perf stat cycles per message |
| No handshake or slow start on the hot path | TCP and TLS handshakes; slow start, also after idle | Warm connections; pre-connected spares; tcp_slow_start_after_idle=0; reconnect off the hot path | ss -ti cwnd steady |
| One writer per socket | The socket lock; spliced messages | The gateway alone writes; rings into it | perf trace -s: writes from the gateway thread only |
| Measured from the wire | Every mechanism above | SO_TIMESTAMPING hardware stamps; phc2sys | strace -c shows only the recv; retrans zero; p99 within a few µs of p50 |
Three caveats
The kernel stack is right for everything that is not the hot socket, and most of a trading system is not: the control connections, the logs shipped off the box, the monitoring, ssh. They keep the defaults, and every rule above applies to the hot sockets and the hot card alone.
A busy-polling thread on a shared core is preempted like any other thread, and then its packets wait behind it, in the ring and in the socket’s queue, for as long as it is away. Everything above assumes each hot thread has a core of its own.
And 70 ms of fibre makes 15 µs of kernel invisible. Measure where the time goes, from the wire stamp to the decision and from the decision to the venue’s acknowledgement, before removing the stack or tuning it; to a venue across a sea, where the machine sits and how TCP recovers from loss are worth more than every setting in this section.