Project

General

Profile

Actions

Optimization #8939

open
JL

datasets/cidr: shadow-swap on bulk update to avoid blocking readers

Optimization #8939: datasets/cidr: shadow-swap on bulk update to avoid blocking readers

Added by Jeff Lucovsky about 11 hours ago. Updated about 11 hours ago.

Status:
New
Priority:
Normal
Assignee:
-
Target version:
Effort:
Difficulty:
Label:

Description

The CIDR dataset type (to be added in #8124) uses a per-address-family
SCRWLock: one for the IPv4 radix tree, one for the IPv6 radix tree.
All reader operations (dataset:isset / dataset:isnotset on the packet
path) take the read lock; writers (dataset:set from packets, unix-socket
dataset-add / dataset-remove / dataset-clear / dataset-add-batch) take
the write lock. The write lock blocks all concurrent readers on that
family until it releases.

For steady-state per-entry admin updates the blocking window is a few
microseconds and is invisible on the packet path. For bulk operations
it is not:

  • dataset-clear on a large CIDR set: holds both family write locks
    while SCRadix{4,6}TreeRelease frees the entire tree, then
    SCRadix{4,6}TreeInitialize sets up empty ones. Duration scales with
    entry count.
  • dataset-add-batch: holds each family's write lock for the whole
    batch. A 10k-entry admin import is on the order of tens of
    milliseconds of lock hold on a representative box; every reader
    (packet worker) waits for the whole window.

Even the packet-path dataset:set with attacker-controlled unique
sources shows the same pattern: the microbench under
tests/datasets-cidr/ documents rdlock aggregate throughput dropping
by roughly 3x when a single sustained writer is added.

Proposal

For "replace the whole set" style operations, avoid the write lock
entirely on the reader side by building a new CIDRType off-thread and
publishing it with a single atomic pointer swap. Old readers finish
their walk on the old tree; new readers pick up the new one. The old
tree is freed after a grace period.

Concretely:

  1. Change Dataset.cidr_data from a direct-owned CIDRType* to an
    atomic pointer. Reader path: atomic-load the pointer, take the
    tree's rdlock, walk, release rdlock. This adds one atomic load
    per lookup (negligible).
  2. Add a bulk API DatasetReplaceCIDR(set, entries[], n):
    • allocate a fresh CIDRType and populate it (no locks; nobody
      can see it yet).
    • CAS Dataset.cidr_data to point at the new one, keeping the
      old pointer.
    • defer-free the old tree once no worker can still be inside a
      walk that observed the old pointer.
  3. Grace period detection: the cheapest options are:
    • a per-worker "in-CIDR-lookup" counter that the swap thread
      waits to see quiesce, or
    • defer-free at the next flow-manager tick or packet-pool
      reload sync point, whichever exists first.
      The former is more responsive; the latter is simpler and reuses
      existing synchronization.
  4. Wire the new API into UnixSocketDatasetClear (for CIDR sets) and
    optionally UnixSocketDatasetAddBatch when the caller asks for
    "replace" semantics (add a boolean argument, or a separate
    dataset-replace-batch command).

Non-goals

  • Per-entry dataset:set from packets keeps using the current
    rdlock-first / wrlock-on-miss pattern. Shadow-swap on a per-entry
    basis is not the right shape for that path. The write-lock-under-
    spoofed-flood risk documented in datasets.rst is unaffected by
    this issue.
  • dataset:unset from packets stays on the write lock; unset is used
    much less than set and the per-entry cost is comparable.

Semantics to preserve

  • Concurrent packet-path dataset:set entries that arrive during a
    shadow-swap end up in whichever version the worker's atomic-load
    pointed at. Existing docs already note that "clear + batch is not
    atomic against concurrent packet-path writes"; that stays true and
    the doc note doesn't need to change.
  • unix-socket admin ops themselves remain serialized by the unix-command
    thread, so admin cannot race admin.

Rough scope

  • src/datasets.h: atomic pointer type change for cidr_data.
  • src/datasets-cidr.[ch]: new DatasetReplaceCIDR + per-worker
    quiescence hook or defer-free integration.
  • src/runmode-unix-socket.c: wire dataset-clear (and optionally
    dataset-add-batch with replace=true) to the new API for CIDR type.
  • doc/userguide/rules/datasets.rst: update the "schedule bulk imports
    during quiet windows" guidance to note that shadow-swap has
    displaced it for replace-style admin ops. The write-lock-under-
    attack note for packet-path dataset:set stays.
  • tests/datasets-cidr: add coverage that concurrent readers see either
    the old or new state through a swap, and never a torn state.

Related

  • #8124 (base CIDR dataset feature)
  • Only the bulk-import ("clear + reload") guidance in datasets.rst
    goes away when this lands. The per-packet dataset:set flood note
    stays -- that path uses per-entry writes and is out of scope here.

JL Updated by Jeff Lucovsky about 11 hours ago Actions #1

  • Description updated (diff)
Actions

Also available in: PDF Atom