Deep Engineering

ЗАМЕР

bench/async-vs-sync/env.py

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

Цитируется в статье
/ru/python/concurrency/async-vs-sync

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

Асинхронность против синхронности: замеры для статьи

Проверяется утверждение: перевод проекта с синхронного кода на асинхронный может не ускорить работу, а замедлить — в первую очередь там, где приложение работает с базой.

Все замеры ходят в один Postgres и в одну таблицу; параметры собраны в env.py, чтобы «прочие равные» были действительно равными.

Как запустить

# 1. Поднять Postgres
/usr/lib/postgresql/16/bin/postgres -D <каталог данных> -p 5433 -k /tmp

# 2. Завести базу и таблицу
psql -h /tmp -p 5433 -U postgres -c "create database bench_async"
psql -h /tmp -p 5433 -U postgres -d bench_async -c "
  create table items (id int primary key, payload text not null);
  insert into items select g, repeat('x', 64) from generate_series(1, 100000) g;
  analyze items;"

# 3. Драйверы
python3.13 -m pip install 'psycopg[binary]' psycopg_pool asyncpg

# 4. Замеры
python3.13 pool.py       # потолок пула
python3.13 latency.py    # цена одной операции
python3.13 executor.py   # синхронный драйвер внутри async
python3.13 waiting.py    # цена одновременного ожидания
python3.13 blocking.py   # счёт рядом с запросами и хвост задержек

Адрес базы переопределяется переменной BENCH_DSN.

Скрипт Что меряет
env.py общие параметры и печать версий — не замер, а гарантия сравнимости
pool.py одна и та же работа при пуле 1…32 в трёх моделях: потоки + psycopg, async psycopg, asyncpg
latency.py время одного короткого запроса по одному соединению, без одновременности
executor.py синхронный драйвер через run_in_executor против трёх остальных моделей
waiting.py цена держать N одновременных ожиданий: потоки против корутин, время и память
blocking.py распределение задержки, когда рядом с запросами выполняется счётный код

Что показали замеры (машина замеров: 2 vCPU, Postgres 16.13 локально)

  1. Потолок ставит пул соединений, а не модель исполнения. При одном и том же размере пула три модели дают одинаковое время в пределах шума — на пуле 8 это 0,43 / 0,45 / 0,43 с при «идеале» 0,38 с.
  2. На коротком запросе без одновременности async медленнее. Запрос по первичному ключу: 59 мкс синхронно против 70 мкс на async psycopg и 68 мкс на asyncpg — то есть на 16–19 % дороже. Ускорять здесь нечего: ждать параллельно нечего, остаётся только накладной расход.
  3. run_in_executor не хуже и не лучше остальных, потому что и он упирается в тот же пул: 0,434 с против 0,428 с у чистых потоков.
  4. Где async выигрывает по-настоящему — держать много ожиданий сразу. Четыре тысячи одновременных ожиданий: 0,31 с и +0 МБ RSS у корутин против 2,03 с и +61 МБ у потоков.
  5. Счётный код рядом с запросами портит обе модели. Ожидаемого «цикл событий встал, а потоки живут» не видно: GIL делает счёт последовательным в любой модели. Один кусок на 191 мс даёт максимум 184 мс у async и 212 мс у потоков.

Числа привязаны к этой машине и к локальной базе. Воспроизводить на своей — скрипты печатают все версии и параметры, без которых числа не значат ничего.

Скрипт

41 строк
"""Общие параметры замеров: адрес базы, размеры выборок, печать окружения.

Отдельным модулем, потому что все скрипты этой темы обязаны ходить в ОДНУ базу
с ОДНИМИ параметрами: сравнение моделей исполнения имеет смысл только при
прочих равных, а «прочие равные» здесь — это адрес, таблица и версия сервера.
"""

import os
import platform
import sys

DSN = os.environ.get(
    "BENCH_DSN",
    "postgresql://postgres@127.0.0.1:5433/bench_async",
)

# Тот же адрес через сокет — им пользуется отдельный замер про то, сколько
# стоит сам TCP. Через сокет ходит только он.
DSN_SOCKET = os.environ.get(
    "BENCH_DSN_SOCKET",
    "postgresql:///bench_async?host=/tmp&port=5433&user=postgres",
)


def describe() -> None:
    """Печатает всё, без чего числа не воспроизводимы."""
    import asyncpg
    import psycopg

    print(f"python      {sys.version.split()[0]} ({platform.machine()})")
    print(f"psycopg     {psycopg.__version__}")
    print(f"asyncpg     {asyncpg.__version__}")
    print(f"cpu         {os.cpu_count()}")
    with psycopg.connect(DSN) as conn:
        with conn.cursor() as cur:
            cur.execute("select version()")
            print(f"server      {cur.fetchone()[0].split(',')[0]}")
            cur.execute("show max_connections")
            print(f"max_conn    {cur.fetchone()[0]}")
    print()