Quantitative Finance · Book 14 · Technology

Networks, Hardware and Trading Infrastructure

Networks, Hardware and Trading Infrastructure · Technology

3Network Cards and Kernel Bypass

Between the wire and a UDP socket’s recv a datagram is copied by the card into the host’s memory, announced by an interrupt, picked up by the kernel’s networking code in a software interrupt, matched to a socket, queued, and copied again into the buffer the program handed to recv; the program, meanwhile, was asleep and must be woken. On the laptop this book is written on, the median datagram spends under a microsecond getting into the kernel over loopback and about fifteen more microseconds getting out of it to a sleeping reader. None of that work decides anything a trading program cares about.

This chapter follows a packet through the network card and the kernel, then removes the kernel from the path. One Quant Book 13 (chapter 13) tuned the host so that busy polling and kernel bypass can do their job; this chapter looks at the same machinery from the card’s side: descriptor rings, interrupts and their coalescing, the steering of flows to queues, the stacks that run in user space, and the timestamps the card itself can take.

3.1 The path of a packet through the kernel

A network card and its driver share memory. The card writes arriving packets into buffers that the driver has given it, and says which ones it has filled; the driver takes them, hands the buffers back, and passes the packets up.

Definition 3.1 (Descriptor ring)

A descriptor ring is a circular array of descriptors, each pointing at a packet buffer in host memory and carrying an ownership flag. On the receive side the card fills the next descriptor it owns with an arriving packet (by direct memory access), marks it as the host’s, and advances its head; the host processes the descriptors it owns in order and hands them back to the card. When the next descriptor still belongs to the host, the card has nowhere to put the packet and drops it.

Two ways out of a descriptor ring. The kernel path (top) takes an interrupt, processes the packet in a software interrupt, queues it on a socket and copies it to a program that must be woken. A kernel-bypass stack (bottom) maps the ring into the program, which polls it and reads the packet where the card wrote it.
Figure 3.1. Two ways out of a descriptor ring. The kernel path (top) takes an interrupt, processes the packet in a software interrupt, queues it on a socket and copies it to a program that must be woken. A kernel-bypass stack (bottom) maps the ring into the program, which polls it and reads the packet where the card wrote it.

On Linux the kernel side of this is NAPI, the networking stack’s event mechanism: the card raises an interrupt, the kernel schedules the ring’s processing in a software interrupt, and the processing runs until the ring is empty or a budget is spent, with further interrupts from that ring disabled meanwhile. Under load the kernel is thus already polling; what it does not do by default is spare the program the socket queue, the copy and the wake-up.

A receive ring of twelve descriptors. Five are the host’s (filled and not yet handed back), seven are the card’s. The card writes at the head, the host reads at the tail; the ring is full when the head meets a descriptor the host still owns, whether the host has read it or merely not yet handed it back.
Figure 3.2. A receive ring of twelve descriptors. Five are the host’s (filled and not yet handed back), seven are the card’s. The card writes at the head, the host reads at the tail; the ring is full when the head meets a descriptor the host still owns, whether the host has read it or merely not yet handed it back.

Proposition 3.2 (When a ring overflows)

Let a ring of NN descriptors receive packets at rate λ\lambda while the host processes none for a time TT, starting from a ring in which the host holds HH descriptors. The ring drops (λT−(N−H))+\bigl(\lambda T - (N - H)\bigr)^{+} packets. A host that hands descriptors back in batches of bb holds up to b−1b - 1 processed descriptors at any time, so it must size the ring for the longest pause plus b−1b - 1.

Proof. The card has N−HN - H descriptors to fill; each arrival consumes one and none come back during the pause. Descriptors processed but not yet handed back remain the host’s until the batch completes. ∎

3.2 Interrupts, coalescing and receive-side scaling

An interrupt costs the host a switch into the kernel, the handler, and, for a sleeping reader, the scheduler’s wake-up: microseconds, paid per interrupt. Cards therefore wait before interrupting.

Definition 3.3 (Interrupt coalescing)

Interrupt coalescing is a card’s practice of delaying the receive interrupt after a packet arrives, until a given time has passed or a given number of packets are waiting, so that one interrupt serves several packets. On Linux the two limits are set with ethtool -C as rx-usecs and rx-frames; zero microseconds and one frame disable coalescing.

Proposition 3.4 (What coalescing buys and costs)

Let packets arrive as a Poisson process of rate λ\lambda, let the card interrupt uu after the first waiting packet (the frame limit never binding), and let each interrupt cost cirqc_{\mathrm{irq}} of CPU and each packet cpktc_{\mathrm{pkt}}. Each interrupt serves on average 1+λu1 + \lambda u packets, the host spends a fraction

λ cirq1+λu+λ cpkt\frac{\lambda\, c_{\mathrm{irq}}}{1 + \lambda u} + \lambda\, c_{\mathrm{pkt}}

of one core, and the first packet of each batch waits uu for the interrupt. Without coalescing (u=0u = 0) the host saturates one core at λ=1/(cirq+cpkt)\lambda = 1/(c_{\mathrm{irq}} + c_{\mathrm{pkt}}).

Proof. A batch opens with a packet and lasts uu; by the memorylessness of the Poisson process, λu\lambda u more packets arrive on average. Interrupts therefore come at rate λ/(1+λu)\lambda/(1+\lambda u), each costing cirqc_{\mathrm{irq}}, and every packet costs cpktc_{\mathrm{pkt}}. ∎

Definition 3.5 (Receive-side scaling, flow steering)

Receive-side scaling (RSS) is a multi-queue card’s distribution of arriving packets over several receive rings, by a hash of their addresses and ports, so that different cores can process different flows. Flow steering is the card’s placing of chosen flows (by exact match on addresses and ports) on a chosen ring, overriding the hash.

A trading host uses both against RSS’s intent. RSS spreads load evenly, which is what a web server wants; a feed handler wants every packet of line A on the one ring its pinned core polls, and nothing else there. Flow steering puts the venue’s groups on the rings of the cores that handle them and the rest of the machine’s traffic elsewhere, and the core that polls a ring should sit on the socket the card is attached to (One Quant Book 13, chapter 4).

3.3 Polling drivers and user-space stacks

Definition 3.6 (Poll-mode driver, user-space network stack)

A poll-mode driver runs in the application’s process: it maps a card’s rings into user space and reads and refills them in a loop on a dedicated core, with no interrupts and no system calls. A user-space network stack implements IP, UDP and TCP in the application’s process on top of such access to the card, usually behind the standard socket calls, so that an unmodified program runs over it.

Remark 3.7 (The families of bypass)

Three kinds are in use, and a firm often runs two. Raw ring access through a vendor’s interface (AMD’s ef_vi for its Solarflare cards) or a portable poll-mode framework (DPDK) gives the program frames and nothing else: it must parse its own UDP and, for orders, bring its own TCP. A user-space stack behind the socket calls (OpenOnload, open source, which intercepts the socket calls of an unmodified binary) keeps the program’s code and removes the kernel from its path. And Linux’s own AF_XDP moves frames from a card’s ring to a region of the program’s memory through an in-kernel filter, in copy or in zero-copy mode, with the kernel still in charge of the device. Method 3.8 says which to use where.

Method 3.8 (Choosing a receive path)

  1. Market data on the hot path: raw ring access, polled by a pinned core on the card’s socket, the handler parsing UDP itself; the feed is multicast UDP, which needs no protocol state.
  2. Order entry: a user-space TCP stack, or the card vendor’s TCP in hardware (chapter 7), because order entry needs TCP’s state and retransmission and writing one’s own TCP is a project of its own.
  3. Everything else (drop copy, reference data, monitoring): the kernel, on cores and rings of its own, so that it never shares a queue with the hot path.
  4. In every case measure: the gain depends on the card, the host’s tuning and the traffic, and a poller that is ever descheduled loses more than it gained.
    bool nic_rx(std::uint64_t seq, std::uint32_t length) {
        const std::uint32_t i = head_;
        if (owner_[i] == 1) { ++drops; return false; }   // still the host's: the ring is full
        seq_[i] = seq, len_[i] = length, owner_[i] = 1;
        head_ = (i + 1) & (size_ - 1);
        ++rx;
        return true;
    }

    std::uint32_t poll(std::uint32_t budget) {
        std::uint32_t n = 0;
        while (n < budget && rx > processed) {
            const std::uint32_t i = tail_;
            mix(seq_[i]), mix(len_[i]);
            tail_ = (i + 1) & (size_ - 1);
            ++processed, ++pending_, ++n;
            if (pending_ >= refill_) give_back();
        }
        return n;
    }
Listing 3.1. The receive ring of firm.nicring: the card fills descriptors it owns; the poll loop processes those it owns and hands them back in batches. code/firm/nicring/cpp/firm_nicring.hpp

3.4 Zero copy and the transmit side

Definition 3.9 (Zero-copy networking)

Zero-copy networking is the delivery of a packet to the program, or from it, in the buffer the card reads or writes by direct memory access, without the processor copying its bytes between a kernel buffer and the program’s.

A copy of a 100-byte datagram is a few nanoseconds of memory traffic; what zero copy saves on the receive side is less the copy than the machinery around it (the socket buffer, its accounting, the cache lines of a second location). On the transmit side the same ring structure runs in reverse: the program writes an order into a transmit buffer, fills a descriptor and writes the card’s doorbell register, and the card fetches the frame by direct memory access. Each of those steps is a crossing of the processor’s interconnect to the card (One Quant Book 13, chapter 4). Some cards let the program push the frame’s bytes into the card’s own memory with processor writes instead: AMD’s ef_vi documents such a programmed-I/O region, which “avoids the latency associated with a DMA read”, can be filled before the critical path and updated on it, and, in a cut-through mode, starts transmitting while the frame is still crossing the bus. The cheapest transmit is the one prepared before it is needed: the order’s frame built in advance, with only price, size and identifier to fill, the same idea chapter 7 takes into hardware.

3.5 Timestamps on the card

The kernel can timestamp a packet when it enters the kernel (SO_TIMESTAMPING, software receive timestamps), and a card with its own clock can timestamp it when it reaches the card’s port, a hardware timestamp in the sense of One Quant Book 13, chapter 5. The difference is the time the packet spent in the card and its ring, which is exactly what a firm tuning its receive path wants to see and what software timestamps cannot show. The card’s clock is only as good as its synchronisation to the firm’s time reference, the subject of chapter 4, and the timestamps of the whole cage are compared in chapter 5.

3.6 Tutorial: the kernel’s receive path, measured and modelled

Goal. Measure what the kernel’s receive path costs on the laptop, model what interrupts and polling cost on a card, and run the descriptor ring in three languages. End state: Table 3.1, Figures 3.3 and 3.4, and the ring’s fixture green in Python, C++ and Rust.

  1. Three ways to read. cpp/nw_rx.hpp sends 64-byte datagrams over loopback, each carrying its send time, and reads them by a blocking recvmsg, by non-blocking reads in a loop, and by recvmmsg in batches of 16, with the kernel’s software receive timestamp in each (Listing 3.2). python bench_rx.py runs 20 000 datagrams per mode, 20 microseconds apart, and writes the quantiles.
  2. Split the path. The kernel’s timestamp splits each datagram’s latency into getting into the kernel and getting out of it to the program.
  3. Model the card. nw_nic.coalesce and poll simulate a card’s interrupts with coalescing and a polling core, with the model’s costs; python fig_nic.py writes Figure 3.4’s data.
  4. The ring. firm.nicring runs the same stream of arrivals and polls through four ring configurations in Python, C++ and Rust and checks counters and hashes against the shared fixture.

What to change next. Pin the busy-polling reader to a core and the sender to another (taskset) and read its tail again; in the ring’s fixture, find the smallest ring that drops nothing when the host hands back in batches of 32.

    while (static_cast<int>(out.size()) < n) {
        int got = 0;
        if (mode == 2) {
            for (int k = 0; k < kBatch; ++k) mm[k].msg_hdr.msg_controllen = sizeof ctrl[k];
            got = recvmmsg(rx, mm, kBatch, 0, nullptr);
        } else {
            mm[0].msg_hdr.msg_controllen = sizeof ctrl[0];
            const ssize_t r = recvmsg(rx, &mm[0].msg_hdr, mode == 1 ? MSG_DONTWAIT : 0);
            got = r > 0 ? 1 : 0;
        }
        const std::int64_t app = now_ns();
        for (int k = 0; k < got; ++k) {
            std::int64_t sent = 0;
            std::memcpy(&sent, data[k], sizeof sent);
            out.push_back({sent, kernel_stamp(mm[k].msg_hdr), app});
        }
    }
Listing 3.2. The three reads: a blocking read, a non-blocking read in a loop, and a batched read; each datagram keeps its send, kernel and application times. code/networks/03-network-cards-and-kernel-bypass/cpp/nw_rx.hpp
Read methodInto the kernel, p50Kernel to program, p50Total, p50Total, p99
Blocking recvmsg0.75 µs0.75\,\text{µ}\mathrm{s}15.4 µs15.4\,\text{µ}\mathrm{s}16.2 µs16.2\,\text{µ}\mathrm{s}215 µs215\,\text{µ}\mathrm{s}
Busy polling0.62 µs0.62\,\text{µ}\mathrm{s}0.92 µs0.92\,\text{µ}\mathrm{s}1.5 µs1.5\,\text{µ}\mathrm{s}3457 µs3457\,\text{µ}\mathrm{s}
Batched recvmmsg0.45 µs0.45\,\text{µ}\mathrm{s}21.4 µs21.4\,\text{µ}\mathrm{s}22.0 µs22.0\,\text{µ}\mathrm{s}75 µs75\,\text{µ}\mathrm{s}
Table 3.1. The kernel’s receive path over loopback, 20 000 datagrams of 64 bytes per method, 20 microseconds apart (the batched reader’s sender sends bursts of 16). Measured on a laptop (Intel Core Ultra 7 155H) under WSL2, no isolated cores; g++ 11, -O2. Data: bench_rx.py, measured_rx.csv.
Quantiles of the time from sendto to the program holding the datagram, on the laptop’s loopback, by read method. Busy polling cuts the median tenfold; without an isolated core the polling thread is sometimes descheduled for milliseconds, and every datagram behind it waits: from the 95th quantile on, it is the worst of the three. Measured on a laptop (Intel Core Ultra 7 155H) under WSL2, no isolated cores. Data: bench_rx.py.
Figure 3.3. Quantiles of the time from sendto to the program holding the datagram, on the laptop’s loopback, by read method. Busy polling cuts the median tenfold; without an isolated core the polling thread is sometimes descheduled for milliseconds, and every datagram behind it waits: from the 95th quantile on, it is the worst of the three. Measured on a laptop (Intel Core Ultra 7 155H) under WSL2, no isolated cores. Data: bench_rx.py.

The measurement shows the two halves of this chapter’s argument (Table 3.1). Getting a datagram into the kernel costs under a microsecond; getting it out to a sleeping program costs fifteen at the median and hundreds of microseconds at the 99th percentile, because the program must be woken and scheduled. A program that never sleeps removes that: busy polling brings the median down to 1.5 µs1.5\,\text{µ}\mathrm{s}. And a program that never sleeps must never be interrupted: on a laptop whose cores are shared with everything else the poller is sometimes descheduled, and the datagrams that arrive meanwhile wait for milliseconds. Batching amortises the system call but not the wake-up, and waits for its batch. Loopback involves no card at all, so these figures are the kernel’s share alone; a real card adds its ring, its interrupt and its coalescing.

def coalesce(lam, u, m, c_irq=C_IRQ, w=WAKE, c_pkt=C_PKT, n=100_000, seed=1):
    rng = np.random.default_rng(seed)
    t = np.cumsum(rng.exponential(1.0 / lam, n))
    lat = np.empty(n)
    i, busy_until, irqs, cpu = 0, 0.0, 0, 0.0
    while i < n:
        first = t[i]
        deadline = first + u
        j = i
        while j < n and j - i < m and t[j] <= deadline:
            j += 1
        fire = t[j - 1] if j - i == m else deadline          # m-th packet or the timer
        start = max(fire + w, busy_until) + c_irq
        done = start + c_pkt * np.arange(1, j - i + 1)
        lat[i:j] = done - t[i:j]
        busy_until = done[-1]
        irqs += 1
        cpu += c_irq + c_pkt * (j - i)
        i = j
    span = t[-1] - t[0]
    return {"mean_us": float(lat.mean()), "p99_us": float(np.quantile(lat, 0.99)), "cpu": float(cpu / span),
            "irq_rate": float(irqs / span)}
Listing 3.3. The coalescing model: an interrupt at the mm-th waiting packet or uu after the first, then wake-up, interrupt cost and per-packet cost. code/networks/03-network-cards-and-kernel-bypass/python/nw_nic.py
Model: interrupt coalescing at two packet rates. Longer timers cost latency and save CPU. At 500 000 packets a second an interrupt per packet saturates the core (no point at zero). A polling core, not drawn, holds latency near 0.4\, µ s at both rates and uses the whole core. Model costs: 2\, µ s per interrupt, 3\, µ s of wake-up, 0.3\, µ s per packet. Data: fig_nic.py.
Figure 3.4. Model: interrupt coalescing at two packet rates. Longer timers cost latency and save CPU. At 500 000 packets a second an interrupt per packet saturates the core (no point at zero). A polling core, not drawn, holds latency near 0.4 µs0.4\,\text{µ}\mathrm{s} at both rates and uses the whole core. Model costs: 2 µs2\,\text{µ}\mathrm{s} per interrupt, 3 µs3\,\text{µ}\mathrm{s} of wake-up, 0.3 µs0.3\,\text{µ}\mathrm{s} per packet. Data: fig_nic.py.

The model (Listing 3.3) takes 2 µs2\,\text{µ}\mathrm{s} of CPU per interrupt, 3 µs3\,\text{µ}\mathrm{s} of wake-up and 0.3 µs0.3\,\text{µ}\mathrm{s} per packet: model values of the order of the laptop’s wake-up, not a particular card’s. At 100 000 packets a second an interrupt per packet gives 5.6 µs5.6\,\text{µ}\mathrm{s} of mean latency for 23% of a core; an 8 µs8\,\text{µ}\mathrm{s} timer gives 11.7 µs11.7\,\text{µ}\mathrm{s} for 14%; polling gives 0.4 µs0.4\,\text{µ}\mathrm{s} for the whole core. At 500 000 an interrupt per packet is beyond one core (the saturation rate of Proposition 3.4 is 435 000 a second), and coalescing is what keeps the host alive. A trading host resolves the trade-off by not choosing it: the hot path polls on cores bought for the purpose, and everything else coalesces.

3.7 Build: the descriptor ring

Purpose. A model of a card’s receive ring precise enough to reason about drops, batching and polling budgets, in the three languages of the firm’s systems.

Interface. Python firm_nicring.Ring(size, refill) with nic_rx(seq, length), poll(budget), run(events) and the counters rx, drops, processed, doorbells, max_owned, hash; make_events. C++20 firm::nicring::Ring (cpp/firm_nicring.hpp) and Rust firm_nicring::Ring with the same methods.

Rules. The ring’s size is a power of two; the card never overwrites a descriptor the host owns; the host processes in order, never more than its budget per poll, and hands back in batches of refill, one doorbell per batch; the hash is FNV-1a over the processed packets’ sequence numbers and lengths in processing order.

Acceptance tests. code/firm/nicring/: the shared fixture (20 000 arrivals, four configurations) reproduced exactly in Python, C++ and Rust; a full ring drops and a processed but unreturned descriptor still counts as full; under random interleavings every accepted packet is processed once and in order.

Stretch. A transmit ring with a completion queue; several rings with a steering function, to reproduce the imbalance of a hash that puts two busy groups on one ring.

Sources and further reading

  • Linux kernel documentation: “NAPI”, “Scaling in the Linux Networking Stack”, “Timestamping”, “AF_XDP”; include/uapi/linux/ethtool.h (struct ethtool_coalesce); man page recvmmsg(2).
  • DPDK Programmer’s Guide, “Poll Mode Driver”; AMD, ef_vi User Guide; OpenOnload README.

3.8 Exercises

Exercise 3.1 ★

A ring of 512 descriptors, of which the host holds 40, receives 1.5 million packets a second while its core is interrupted for 400 µs400\,\text{µ}\mathrm{s}. How many packets are dropped?

Solution

Solution of Exercise 3.1.

1.5×106×400×10−6=6001.5 \times 10^6 \times 400 \times 10^{-6} = 600 arrivals for 512−40=472512 - 40 = 472 free descriptors: 128 packets dropped.

Exercise 3.2 ★

With coalescing at 20 microseconds, and packets at 200 000 a second, how many packets does an interrupt serve on average, and at what rate do interrupts arrive?

Solution

Solution of Exercise 3.2.

1+λu=1+0.2×20=51 + \lambda u = 1 + 0.2 \times 20 = 5 packets an interrupt; interrupts at 200 000/5=40 000200\,000 / 5 = 40\,000 a second.

Exercise 3.3 ★

From Table 3.1, what share of the blocking read’s median is spent after the datagram entered the kernel?

Solution

Solution of Exercise 3.3.

15.4/16.215.4 / 16.2: 95%. Almost all of it is the wake-up and scheduling of the sleeping reader, not the network stack.

Exercise 3.4 ★★

Why does receive-side scaling’s even spreading of flows work against a feed handler, and what does flow steering change?

Solution

Solution of Exercise 3.4.

RSS hashes flows over rings to balance cores; a feed handler wants all of a line’s groups on the one ring its pinned core polls, and no other traffic there. Flow steering pins the venue’s flows to the chosen ring and keeps the rest of the host’s traffic on others.

Exercise 3.5 ★★

With the model’s costs, at what packet rate does an interrupt per packet use a whole core? And if every interrupt serves 16 packets?

Solution

Solution of Exercise 3.5.

1/(2+0.3)=0.4351/(2 + 0.3) = 0.435 packets a microsecond: 435 000 a second. With 16 packets an interrupt, 1/(2/16+0.3)=2.351/(2/16 + 0.3) = 2.35 million a second.

Exercise 3.6 ★★

Busy polling improved the median tenfold and made the 99th percentile sixteen times worse (Table 3.1). Explain both, and say what must change on a production host.

Solution

Solution of Exercise 3.6.

The poller never sleeps, so the wake-up (most of the blocking median) disappears. But on shared cores the scheduler sometimes runs something else on the poller’s core for milliseconds, and every datagram arriving meanwhile waits. On a production host the polling thread gets an isolated core with its interrupts and timer tick moved away (One Quant Book 13, chapter 13), on the card’s socket.

Exercise 3.7 ★★★

Coding. With firm.nicring and the fixture’s events, find the smallest ring size that drops nothing when descriptors are handed back in batches of 32.

Solution

Solution of Exercise 3.7.

128 descriptors: with batches of 32, a ring of 64 still drops 36 packets of the fixture, a ring of 128 drops none.

Exercise 3.8 ★★★

Find the flaw. “We moved order entry to a raw poll-mode interface and wrote our own minimal TCP: no retransmission timer, since colocation links do not lose packets. Latency fell by two microseconds.”

Solution

Solution of Exercise 3.8.

Loss is rare, not impossible (a flapping optic, a switch buffer during a burst, a failover): without retransmission one lost segment leaves the session stuck or silently missing an order, and the venue’s session layer will disconnect or cancel. The two microseconds came from removing the kernel, not from removing reliability; use a user-space or hardware TCP that implements it.

3.9 Problem: Interrupts or Polling

Problem 3.1

Weekend problem — the price of a sleeping reader

A feed arrives at 200 000 packets a second on one receive ring. Use the chapter’s model costs: 2 µs2\,\text{µ}\mathrm{s} of CPU per interrupt, 3 µs3\,\text{µ}\mathrm{s} of wake-up, 0.3 µs0.3\,\text{µ}\mathrm{s} per packet, and a polling loop that checks the ring every 0.2 µs0.2\,\text{µ}\mathrm{s}.

Part I — One interrupt per packet.

  1. What share of a core do interrupts and packets take?
  2. What is the smallest latency a packet can have?
  3. The model’s mean latency is 6.3 µs6.3\,\text{µ}\mathrm{s}. Why is it above the minimum?
  4. At what rate would the core saturate?

Part II — A 10-microsecond timer.

  1. How many packets does an interrupt serve on average?
  2. At what rate do interrupts come?
  3. What share of the core is used?
  4. The model’s mean latency is 12.4 µs12.4\,\text{µ}\mathrm{s}. Where does the increase come from?

Part III — Polling.

  1. What is the mean wait for the next check of the ring?
  2. What is the mean latency, and the model’s (0.41 µs0.41\,\text{µ}\mathrm{s})?
  3. What share of a core is used?
  4. What must be true of that core?

Part IV — The verdict.

  1. State the named result: the mean latency and CPU of the timer and of polling at this rate, and the rate at which interrupts alone would take a whole core.
  2. How many microseconds per packet does polling save over the timer?
  3. At what packet rate does the timer’s CPU reach a quarter of a core?
  4. Why do trading hosts poll the hot path and coalesce the rest?
  5. What would you measure on the real card before believing the model?
  6. What does a descriptor ring of 1 024 entries give the polling core, in time, at this rate?
  7. In one sentence: what does a sleeping reader cost?
  8. Which of the chapter’s three families of bypass would you use for this feed, and why?
Solution

Solution of Problem 3.1.

  1. 0.2×(2+0.3)=0.460.2 \times (2 + 0.3) = 0.46: 46% of a core.
  2. 3+2+0.3=5.3 µs3 + 2 + 0.3 = 5.3\,\text{µ}\mathrm{s}.
  3. Queueing: an interrupt that arrives while the previous one is still being handled waits for it.
  4. 1/2.31/2.3 per microsecond: 435 000 packets a second.
  5. 1+0.2×10=31 + 0.2 \times 10 = 3.
  6. 200 000/3≈66 667200\,000 / 3 \approx 66\,667 a second.
  7. 66 667×2×10−6+0.2×0.3=0.133+0.0666\,667 \times 2 \times 10^{-6} + 0.2 \times 0.3 = 0.133 + 0.06: 19.3%.
  8. From the timer: the first packet of a batch waits 10 µs10\,\text{µ}\mathrm{s} for it, later ones less, before the 5.3 µs5.3\,\text{µ}\mathrm{s} of handling.
  9. Half a loop, 0.1 µs0.1\,\text{µ}\mathrm{s}.
  10. 0.1+0.3=0.4 µs0.1 + 0.3 = 0.4\,\text{µ}\mathrm{s}; the model’s 0.41 adds the rare packet that arrives while the previous one is processed.
  11. The whole core.
  12. Isolated, never descheduled, on the card’s socket, with nothing else to do.
  13. Named result. At 200 000 packets a second: a 10 µs10\,\text{µ}\mathrm{s} timer gives 12.4 µs12.4\,\text{µ}\mathrm{s} of mean latency for 19.3% of a core; polling gives 0.41 µs0.41\,\text{µ}\mathrm{s} for all of it; interrupts alone would take a whole core at 435 000 packets a second.
  14. About 12 µs12\,\text{µ}\mathrm{s} (12.4 against 0.41).
  15. Where λ(2/(1+10λ)+0.3)=0.25\lambda(2/(1 + 10\lambda) + 0.3) = 0.25: about 324 000 packets a second.
  16. The hot path is worth a core; the rest of the host’s traffic is not, and coalescing keeps it from interrupting the machine all day.
  17. Interrupt cost and wake-up with a profiler and hardware timestamps, the card’s coalescing behaviour under the real feed, and the distribution of pauses of the polling core.
  18. 1 024/200 0001\,024 / 200\,000 seconds: 5.1 ms5.1\,\mathrm{m}\mathrm{s} of pause before the ring overflows.
  19. Microseconds of wake-up per batch, and a tail as long as the scheduler’s longest decision.
  20. Raw ring access for the multicast feed, polled on an isolated core: the feed needs no TCP, and raw access removes the most.

3.10 Interview questions

Interview question 3.1 ★ developer

Walk me through what happens between a packet arriving at the card and recv returning.

Solution

Solution of Interview question 3.1.

The card writes the frame by DMA into a buffer named by the next receive descriptor and marks it done; it raises an interrupt (perhaps after coalescing); the kernel schedules NAPI processing in a software interrupt, which reads the ring, builds the socket buffer, passes it up IP and UDP, queues it on the socket and wakes the reader; recv copies it into the program’s buffer.

What the interviewer is looking for: DMA, the ring, the interrupt and softirq, the socket queue, the copy and the wake-up.

Interview question 3.2 ★★ developer

What is interrupt coalescing? How would you set it on a market-data host?

Solution

Solution of Interview question 3.2.

The card delays the receive interrupt by a time or a packet count so that one interrupt serves several packets: less CPU, more latency. On a market-data host the hot rings are polled, so their coalescing does not matter; for the others choose moderate values and turn adaptive coalescing off where predictability matters.

What the interviewer is looking for: the latency-CPU trade-off and that the hot path avoids it by polling.

Interview question 3.3 ★★ developer

What is kernel bypass, what forms does it take, and what do you give up?

Solution

Solution of Interview question 3.3.

Running the data path in user space: raw ring access (vendor interfaces, DPDK), user-space stacks behind the sockets API, or AF_XDP. It removes interrupts, system calls, copies and wake-ups. You give up the kernel’s tools, isolation and TCP (unless the stack brings its own), and you spend a core per polling thread.

What the interviewer is looking for: the families and the costs, not only the speed.

Interview question 3.4 ★★ developer

Your busy-polling feed handler has a great median and a terrible 99.9th percentile. What do you check?

Solution

Solution of Interview question 3.4.

Whether the polling core is really isolated (other tasks, interrupts, timer ticks, kernel threads on it), frequency and idle states, page faults and allocation on the path, the ring size against the longest pause, and whether the tail coincides with bursts. Measure with hardware timestamps against the software ones.

What the interviewer is looking for: scheduling interference first, then the ring.

Interview question 3.5 ★★★ developer

A descriptor ring drops packets although the handler’s average load is 30%. Explain how, and how you would size the ring.

Solution

Solution of Interview question 3.5.

Average load says nothing about pauses: while the host is not processing (a burst, a descheduling, a batched refill), the card consumes free descriptors at the arrival rate, and a pause longer than free descriptors divided by that rate drops packets. Size the ring for the burst rate times the longest pause plus the refill batch.

What the interviewer is looking for: pause times rate, and the descriptors held by batching.

Interview question 3.6 ★★ developer, researcher

Why would you want hardware receive timestamps when you already timestamp in the kernel?

Solution

Solution of Interview question 3.6.

The kernel’s timestamp is taken after the card, its ring, the interrupt and the driver; the card’s is taken at the port. Their difference is the receive path the firm is trying to tune, and only the card’s timestamp compares with the timestamps of switches and taps (chapter 5).

What the interviewer is looking for: where on the path each timestamp is taken.

Terms defined in this chapter

See all 2333 terms in the glossary