Skip to content

Releases: thatsme/maglev_ex

v0.2.1

Choose a tag to compare

@thatsme thatsme released this 06 Aug 13:21

No behaviour change. Slot assignment is identical to 0.2.0 and 0.1.0, which test/fixtures/golden_0_1_0.txt asserts on every run against digests taken from the 0.1.0 package as published.

This is the first release to reach Hex since 0.1.0. v0.2.0 was tagged but never published; everything in it is included here.

Fixed

  • The golden fixture is declared as an @external_resource, so regenerating it recompiles the tests that read it. Without that, a regenerated fixture left the previous digests compiled into the test module and the suite passed while checking nothing — invisible on CI, which always compiles from clean, and visible only on the local run immediately after regenerating.

Documentation

  • Tables are compared with Maglev.slots/1 rather than with ==. A table records how it was built as well as what it decided, so two tables that route every key identically can compare unequal across a release that changes which fill strategy a given weight distribution selects. Deciding whether to publish a rebuild by comparing structs would occasionally trigger a :persistent_term write, and its global collection scan, for a table that routes exactly as the one it replaces.

Included from 0.2.0

  • Lopsided weights build faster. A second fill strategy holds the turn order in a priority queue keyed on each backend's next eligible iteration, so backends not yet eligible to claim a slot are never visited. At 1000 backends and a 65537-slot table, one backend weighted 10000 against the rest at 1 builds in 59 ms rather than 1395 ms, while evenly weighted sets keep their previous cost. The strategy is selected from the weights, and both produce identical tables.
  • A golden fixture pinning slot assignment to the 0.1.0 package. Forcing every pinned configuration through the priority queue reproduces 0.1.0 exactly, including 1000 backends at 655373 slots with a weight ratio of 10000 — well beyond the range the properties generate.
  • Which hash functions are used, and that tables built here match no other Maglev implementation.
  • lookup_index/2 applies no mixing, so a narrow or skewed hash leaves slots unreachable or unevenly loaded.
  • Consistent worker assignment, which is the more common use on the BEAM than packet routing.
  • :persistent_term rebuild frequency in minutes or hours rather than seconds.

See the CHANGELOG for the full list.

v0.2.0

Choose a tag to compare

@thatsme thatsme released this 06 Aug 12:48

Lookup tables are unchanged by this release. The same backends and weights produce the same slot assignment as 0.1.0, so upgrading moves no traffic.

That is checked rather than argued. test/fixtures/golden_0_1_0.txt records slot tables produced by the 0.1.0 package as published, and the suite rebuilds them on every run. Forcing every pinned configuration through the new strategy reproduces 0.1.0 exactly, including 1000 backends at 655373 slots with a weight ratio of 10000.

Lopsided weights build faster

The fill walks every backend on every iteration and skips those not yet eligible to claim a slot. That is free when weights are equal, because every backend claims every time, but the iteration count grows with the ratio between the largest and smallest weight while the number of claims stays at the table size — so nearly every visit does nothing.

A second strategy holds the turn order in a priority queue keyed on each backend's next eligible iteration, so ineligible backends are never visited. Neither strategy dominates, so the choice is made from the weights and both ship.

At 1000 backends and a 65537-slot table, each measurement in a fresh process:

Weights Scanning Queue Selected
all equal 22.9 ms 163.7 ms scanning
spread over 1..10 22.1 ms 98.0 ms scanning
one at 100, rest at 1 155.5 ms 132.5 ms queue
one at 1000, rest at 1 664.4 ms 103.0 ms queue
one at 10000, rest at 1 1394.5 ms 58.8 ms queue

Selection reads the weights once and costs single-digit microseconds against builds of tens of milliseconds, so the selected column is the chosen strategy's own cost.

Two limits are worth stating. The boundary is approximate: the curves cross near a weight ratio of 40 at 100 backends and 85 at 1000, and no single threshold fits both, because the queue's cost per claim grows faster than the log2(count) term it is weighed against. The threshold errs late, so near-equal weights are never moved onto the queue; the largest penalty measured from a wrong choice is about 1.7x, against gains of 5x to 20x where the queue wins.

And scanning allocates nothing while the queue allocates on the process heap. Measured in a fresh process the queue looks its best; called from a long-lived process holding a large heap it will do worse. The figures above are a best case for the queue, not a typical one.

Documentation

  • Which hash functions are used. The construction referred to h1 and h2 without defining them, while Maglev.slots/1 was described as the form to hand to an external datapath — together implying an interoperability that does not exist. Tables built here match no other Maglev implementation.
  • lookup_index/2 applies no mixing, so a narrow or skewed hash leaves slots unreachable or unevenly loaded.
  • Consistent worker assignment, which is the more common use on the BEAM than packet routing.
  • :persistent_term rebuild frequency in minutes or hours rather than seconds, since the write triggers a global garbage collection scan that can cost more than the build.

See the CHANGELOG for the full list.

v0.1.0

Choose a tag to compare

@thatsme thatsme released this 06 Aug 11:14

First release.

Maglev consistent hashing, following section 3.4 of Eisenbud et al., Maglev: A Fast and Reliable Software Network Load Balancer (NSDI '16).

A Maglev table gives every backend an almost equal share of a fixed-size slot table, and keeps most keys pointing at the same backend when the backend set changes. Lookups are a single tuple index, independent of the number of backends.

Highlights

  • Distribution within one slot. Every backend claims either div(size, count) or div(size, count) + 1 slots, whatever the table size.
  • Weighted backends. Integer weights, so the arithmetic assigning slots is exact and independently configured nodes cannot diverge on rounding.
  • Independent of input order. Backends are sorted by encoded key before construction, so a given set yields one table however it arrives. The published algorithm fills slots in index order, which would otherwise let two nodes reading the same set from service discovery disagree about every key.
  • Callable from Erlang. Plain functions over a struct; options are proplists.

Performance

Measured at 1000 backends and a 65537-slot table:

Operation Cost
Build 19.0 ms, 1.00 MB
lookup/2 ~43 ns
lookup_index/2 ~21 ns

The fill runs over :atomics, which measures 3.9x faster and uses 44x less memory than a map-based equivalent. A map implementation is retained as the behavioural definition, with an equivalence property checking the two agree slot for slot.

Verification

The guarantees stated in the paper are encoded as property tests rather than fixed examples: slot counts differing by at most one, completeness, order independence, and agreement with the worked example in table 1. Table movement under backend removal replicates figure 12.

Full coverage, dialyzer clean, tested on Elixir 1.14/OTP 25, 1.16/OTP 26 and 1.19/OTP 27.

See the CHANGELOG for the full list.