Deep Engineering

ЗАМЕР

bench/hashring/practice.py

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/sre/consistent-hashing
Как запустить
python3 bench/hashring/ring.py     > bench/hashring/runs/ring.txt
python3 bench/hashring/practice.py > bench/hashring/runs/practice.txt

Запись прогона

Замеры для урока «Консистентное хеширование»

Файл Что делает
ring.py пять вычислений: сколько ключей меняет узел при добавлении одного узла к делению по модулю; то же самое на кольце; перекос кольца без виртуальных узлов; чем за виртуальные узлы платят; и чьи ключи переезжают при удалении узла
practice.py ответы к задачам урока: доля переехавших ключей при делении по модулю и на кольце, доля затронутых чужих ключей и перекос кольца без виртуальных узлов

Запуск из корня репозитория:

python3 bench/hashring/ring.py     > bench/hashring/runs/ring.txt
python3 bench/hashring/practice.py > bench/hashring/runs/practice.txt

Это вычисление, а не модель

Раскладка ключей по узлам считается точно: у каждого ключа один владелец, и доля переехавших получается пересчётом, а не оценкой. Случайности здесь нет нигде, кроме самих ключей, — они порождаются фиксированным зерном.

Хеш взят как первые восемь байт SHA-1, а не встроенный hash(): у строк тот рандомизируется при каждом запуске, и прогон не воспроизводился бы.

Что воспроизводимо

Всё, кроме последней цифры после запятой в долях. Восемьдесят восемь процентов переезда при делении по модулю, около одной девятой на кольце, нулевая доля чужих ключей при удалении узла и порядок перекоса без виртуальных узлов получатся такими же на любой машине с тем же зерном.

Перекос зависит от того, какие именно точки легли на кольцо, то есть от имён узлов. С другими именами кратность будет другой — воспроизводится не число 90,5, а то, что при одной точке на узел перекос измеряется десятками раз, а при сотне точек — единицами.

Требования к среде

Только CPython, ничего внешнего. Прогон занимает около минуты.

Числа сняты на CPython 3.11.15, 100 000 ключей, 8 узлов.

Скрипт

66 строк
"""Практика к уроку про консистентное хеширование: три ответа и один перекос.

ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленное число — печататься самой программой.

ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Два места, где интуиция ошибается. Первое: насколько
именно хуже деление по модулю и переезжают ли при удалении узла чужие ключи.
Второе: во сколько раз самый нагруженный узел кольца тяжелее самого лёгкого,
если виртуальных узлов нет.

Кольцо и хеш берутся из `ring.py`, чтобы задача и разбор урока стояли на одном
и том же коде.

ЗАПУСК: python3 bench/hashring/practice.py
Вывод: runs/practice.txt
"""

import os
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from ring import KEYS, NODES, Ring, digest, keys  # noqa: E402


def main() -> None:
    print(f"Python {sys.version.split()[0]} · Linux {os.uname().release}")
    print("exact computation, not a simulation: only the keys are random")
    print()

    hashes = [digest(k) for k in keys()]
    names = [f"node-{i}" for i in range(NODES)]

    # --- Часть 1: три ответа о поведении --------------------------------
    modulo = sum(1 for h in hashes if h % NODES != h % (NODES + 1)) / KEYS
    full, less = Ring(names, 128), Ring(names[:-1], 128)
    before = [full.owner_of_hash(h) for h in hashes]
    after = [less.owner_of_hash(h) for h in hashes]
    ring_moved = sum(1 for a, b in zip(before, after) if a != b) / KEYS
    others = sum(1 for a, b in zip(before, after) if a != names[-1] and a != b) / KEYS
    print(f"{modulo:.1%}")
    print(f"{ring_moved:.1%}")
    print(f"{others:.1%}")

    # --- Часть 2: перекос кольца без виртуальных узлов --------------------
    #
    # ПОЧЕМУ КРАТНОСТЬ ПЕЧАТАЕТ ПРОГРАММА. Деление в уме на стороне редакции —
    # ровно тот шаг, где вкрадывается ошибка, которую потом нечем поймать.
    plain = Ring(names, 1)
    counts = {name: 0 for name in names}
    for h in hashes:
        counts[plain.owner_of_hash(h)] += 1
    shares = sorted(c / KEYS for c in counts.values())
    print()
    print(f"nodes on the ring                    {NODES}")
    print(f"points per node                      1")
    print(f"smallest share of keys               {shares[0]:.1%}")
    print(f"largest share of keys                {shares[-1]:.1%}")
    print(f"largest over smallest                {shares[-1] / shares[0]:.1f}")


if __name__ == "__main__":
    main()