Команда Picodata выпустила Picopyn 2.0.0 — Python-драйвер для Picodata с пулом соединений, автоматическим обнаружением узлов кластера и маршрутизацией запросов, учитывающей шардирование данных. Новая версия требует Picodata 26.1.1 или новее. Как и ранее, пакет можно установить из PyPI или исходного кода. Подробный changelog можно изучить в документации драйвера, в текущей же статье рассмотрим основные изменения с примерами.
Асинхронный API
Отслеживание топологии кластера
Главное изменение релиза: сбор топологии в асинхронном Picopyn. Драйвер теперь в фоновом режиме собирает топологию кластера (тиры, репликасеты, инстансы) и карту бакетов. Эти данные используются для маршрутизации запросов, а в режиме discovery — ещё и для подстраивания состава пула соединений.
from picopyn.asynchronous import Pool
# в режиме discovery пул по умолчанию собирает топологию с дефолтным интервалом
pool_with_topology = Pool(
dsn="postgresql://admin:pass@host1:5432,host2:5432",
enable_discovery=True,
max_size=10,
)
# чтобы отключить сбор топологии, нужно явно установить `topology_update_interval=None`
discovered_pool_without_topology = Pool(
dsn="postgresql://admin:pass@host1:5432,host2:5432",
enable_discovery=True,
max_size=10,
topology_update_interval=None,
)
# интервал сбора топологии задаётся в секундах
pool_with_slow_topology_updates = Pool(
dsn="postgresql://admin:pass@host1:5432,host2:5432",
enable_discovery=True,
max_size=10,
topology_update_interval=600,
)
# в режиме bootstrap пул собирает топологию по тем же правилам, но
# не использует её для изменения состава пула
bootstrapped_pool_with_topology = Pool(
dsn="postgresql://admin:pass@host1:5432,host2:5432",
enable_discovery=False,
max_size=10,
)
Маршрутизация запросов
Пул теперь умеет отправлять запрос сразу на мастер того репликасета, которому принадлежит бакет с данными, минуя лишний сетевой запрос между узлами кластера Picodata.
Сейчас маршрутизация работает только для запросов, которые соответствуют каждому из этих условий:
- запрос параметризован;
- запрос выполняет операцию
INSERT; - вставляется одна строка данных.
Автоматическая маршрутизация
При включённом сборе топологии пул поддерживает автоматическую маршрутизацию внутри execute. Это работает за счёт получения драйвером метаданных запроса от Picodata, их кеширования и расчёта бакета, по которому затем выбирается мастер-инстанс нужного репликасета из топологии.
Пример:
import asyncio
from picopyn.asynchronous import Pool
async def main():
pool = Pool(
dsn="postgresql://admin:pass@host1:5432,host2:5432",
enable_discovery=True,
max_size=10,
)
await pool.connect()
# ключ шардирования таблицы — по нему и считается бакет
await pool.execute("""
CREATE TABLE "warehouse" (
id INTEGER NOT NULL,
item TEXT NOT NULL,
PRIMARY KEY (id)
) USING memtx DISTRIBUTED BY (id) OPTION (TIMEOUT = 3.0);
""")
query = 'INSERT INTO "warehouse" VALUES ($1::int, $2::text)'
# первый вызов уходит на соединение, выбранное балансировкой,
# в фоновом режиме драйвер запрашивает метаданные у Picodata, тем самым
# не блокируя и не задерживая первое выполнение запроса
await pool.execute(query, 1, "first")
# повторные вызовы того же запроса драйвер отправляет сразу
# на мастер репликасета, которому принадлежит этот бакет,
# если в пуле есть соединение с этим мастером
await pool.execute(query, 2, "second")
await pool.close()
asyncio.run(main())
Ручная маршрутизация
Для использования маршрутизации напрямую:
from picopyn.bucket import calculate_bucket_id
query = 'INSERT INTO "warehouse" VALUES ($1::int, $2::text)'
params = (1, "test")
# метаданные распределения запроса: тир и параметры ключа шардирования
meta = await pool.get_query_metadata(query)
bucket_count = pool.topology.tiers[meta.tier]
bucket_id = calculate_bucket_id(meta.dk_meta, params, bucket_count)
async with pool.acquire_by_tier_and_bucket_id(meta.tier, bucket_id) as conn:
await conn.execute(query, *params)
Приведение состава пула в соответствие с топологией
В режиме discovery при включённом сборе топологии состав пула теперь согласуется с топологией: при изменениях пул сразу перестраивает состав подключений. При выключенном discovery (bootstrap-режиме) топология используется только для наблюдения и не влияет на состав пула.
Пример перестройки состава пула после изменения топологии:

Корректное завершение (graceful shutdown) работы пула
Закрытие асинхронного пула принимает таймаут мягкого завершения: пул перестаёт выдавать соединения и ждёт завершения текущих операций, а по истечении таймаута оставшиеся соединения принудительно закрываются. Только первый вызов запускает процедуру завершения и задаёт таймаут. Конкурентные вызовы ждут завершения той же операции, таким образом не создавая коллизий.
await pool.close(timeout=10)
Синхронный API
Закрытие соединений через менеджер контекста
Синхронное соединение теперь можно использовать как менеджер контекста: на выходе из блока with соединение закрывается само, даже если внутри блока возникло исключение.
from picopyn.synchronous import connect
with connect("postgresql://admin:pass@localhost:5432") as conn:
cur = conn.cursor()
cur.execute('SELECT * FROM "warehouse"')
print(cur.fetchall())
# здесь соединение уже закрыто
У пула для этого есть Pool.connection: на выходе из блока соединение не закрывается, а возвращается в пул. Синхронный пул намеренно не предоставляет методы acquire/release: забрать соединение можно только через менеджер контекста, поэтому его нельзя случайно забыть вернуть в пул.
from picopyn.synchronous import Pool
pool = Pool(
dsn="postgresql://admin:pass@host1:5432,host2:5432",
enable_discovery=True,
max_size=10,
)
pool.open()
with pool.connection() as conn:
cur = conn.cursor()
cur.execute('INSERT INTO "warehouse" VALUES (%s, %s)', (1, "test"))
# соединение вернулось в пул
pool.close()
Свойство autocommit
Picodata пока не поддерживает управление транзакциями (COMMIT/ROLLBACK), поэтому соединение всегда работает в режиме autocommit. Согласно спецификации DB-API 2.0, свойство autocommit теперь доступно. Его значение всегда равно True, а попытка его изменить возвращает ошибку.
from picopyn.synchronous import connect
with connect("postgresql://admin:pass@localhost:5432") as conn:
assert conn.autocommit is True
conn.autocommit = False # ValueError: autocommit=False is not supported
Планы на ближайшие релизы
- Упрощение API для прямого выполнения запросов к шардированным таблицам
- Поддержка приведения состава пула в соответствие с топологией при bootstrap-режиме (
enable_discovery=False) - Оптимизация балансировки соединений между инстансами Picodata
- Внедрение работы с топологией в синхронную часть драйвера
