Deep Engineering

MEASUREMENT

bench/load-balancing/sim3.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/system-design/traffic/load-balancing
How to run it
python3 bins.py
python3 sim2.py
python3 sim3.py
python3 herd.py
python3 herd2.py
pip install dnspython && python3 dns2.py && python3 ecs.py

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Замеры для статьи «Балансировка нагрузки»

Два разных вида доказательств, и путать их нельзя.

Живые замеры DNS

dns2.py — ротация ответов: двадцать запросов подряд к трём доменам, печатает число адресов в ответе, диапазон TTL и распределение того, какой адрес оказался первым.

ecs.py — geoDNS через EDNS Client Subnet (RFC 7871): один и тот же вопрос задаётся с подстановкой четырёх клиентских подсетей, печатает адреса и SCOPE PREFIX-LENGTH из ответа.

Требует dnspython и сетевого доступа. Адреса у вас будут другие — они зависят от того, откуда вы спрашиваете и через какой резолвер. Воспроизводится не адрес, а scope: ноль означает «ответ годится всем», ненулевой — «ответ зависит от подсети». Именно на этом различии построен раздел про geoDNS.

Модель очередей

bins.py — чистая задача о шарах и корзинах: n шаров в n корзин, случайный выбор против выбора менее полной из двух. Пятнадцать прогонов на каждое n, рядом печатаются асимптотические оценки ln n / ln ln n и ln ln n / ln 2.

sim2.py — основная модель: шестнадцать бэкендов, каждый — одна очередь FIFO с одним обслуживающим прибором (G/G/1). Пуассоновский поток, логнормальное время обработки. Сравниваются random, round-robin, least-conn, least-time, p2c на одинаковых серверах, на кластере с медленным сервером и при загрузке 95%.

sim3.py — взвешенный round-robin с угаданным, отсутствующим и заниженным весом, и отдельно цена закреплённых сессий при 5000, 500 и 50 живых сессиях.

herd.py — несколько независимых балансировщиков, у каждого свои счётчики.

herd2.py — те же балансировщики, но с общим снимком состояния, устаревающим на refresh. Эти два скрипта дают главный результат статьи и отвечают на разные вопросы: первый показывает, что неполнота данных не ломает least connections, второй — что общая устаревшая ошибка ломает.

Модель отвечает на один вопрос — как распределение запросов влияет на ожидание в очередях. В ней нет сети и её задержек, разрывов соединений, проверок живости, кеша на бэкенде и ограничений по памяти. Числа воспроизводятся точно: зёрна фиксированы, первые 20 000 запросов из 300 000 отбрасываются, результат усредняется по пяти прогонам.

python3 bins.py
python3 sim2.py
python3 sim3.py
python3 herd.py
python3 herd2.py
pip install dnspython && python3 dns2.py && python3 ecs.py

Script

68 lines
import heapq, random, statistics
"""
Взвешенный round-robin и цена закреплённых сессий.

Модель та же, что в sim2.py: шестнадцать очередей FIFO, пуассоновский поток,
логнормальное время обработки. Файл самостоятельный — из sim2 ничего не
импортируется, чтобы его можно было запускать из любого каталога.
"""

def simulate2(algo, n=16, n_req=300_000, rho=0.80, seed=1, speed=None,
              weights=None, warmup=20_000, sessions=None):
    rng = random.Random(seed)
    speed = speed or [1.0] * n
    weights = weights or [1] * n
    lam = n * rho
    free_at = [0.0] * n; inflight = [0] * n
    done = []; t = 0.0; lat = []
    # развёрнутый список для взвешенного round-robin
    wheel = [k for k in range(n) for _ in range(weights[k])]
    wi = 0
    sticky = {}
    for i in range(n_req):
        t += rng.expovariate(lam)
        while done and done[0][0] <= t:
            _, s = heapq.heappop(done); inflight[s] -= 1
        if algo == "weighted-rr":
            s = wheel[wi % len(wheel)]; wi += 1
        elif algo == "sticky":
            # 5000 живых сессий, запрос принадлежит случайной; сессия закреплена
            sid = rng.randrange(sessions or 5000)
            if sid not in sticky:
                m = min(inflight)
                sticky[sid] = rng.choice([k for k in range(n) if inflight[k] == m])
            s = sticky[sid]
        elif algo == "least-conn":
            m = min(inflight)
            s = rng.choice([k for k in range(n) if inflight[k] == m])
        else:
            raise ValueError(algo)
        service = rng.lognormvariate(-0.36, 0.85) * speed[s]
        finish = max(t, free_at[s]) + service
        free_at[s] = finish; inflight[s] += 1
        heapq.heappush(done, (finish, s))
        if i >= warmup: lat.append(finish - t)
    lat.sort(); q = lambda p: lat[int(len(lat)*p)]
    return {"p50": q(.50), "p95": q(.95), "p99": q(.99)}

def avg(algo, **kw):
    rs = [simulate2(algo, seed=s, **kw) for s in range(5)]
    return {k: statistics.mean(r[k] for r in rs) for k in ("p50","p95","p99")}

slow = [4.0] + [1.0]*15
print("Один сервер вчетверо медленнее — что делает взвешенный round-robin")
for label, kw in [
    ("weighted-rr, вес угадан (1 против 4)", dict(algo="weighted-rr", speed=slow, weights=[1]+[4]*15)),
    ("weighted-rr, вес не менялся",          dict(algo="weighted-rr", speed=slow, weights=[1]*16)),
    ("weighted-rr, вес ошибочно занижен",    dict(algo="weighted-rr", speed=slow, weights=[1]+[2]*15)),
    ("least-conn (веса не нужны)",           dict(algo="least-conn", speed=slow)),
]:
    r = avg(**kw); print(f"  {label:38s} p50 {r['p50']:6.2f}  p95 {r['p95']:8.2f}  p99 {r['p99']:8.2f}")

print("\nЗакреплённые сессии на одинаковых серверах, загрузка 80%")
for ns in (5000, 500, 50):
    r = avg(algo="sticky", sessions=ns)
    print(f"  сессий {ns:>5}: p50 {r['p50']:6.2f}  p95 {r['p95']:7.2f}  p99 {r['p99']:7.2f}")
r = avg(algo="least-conn")
print(f"  без закрепления: p50 {r['p50']:6.2f}  p95 {r['p95']:7.2f}  p99 {r['p99']:7.2f}")