Перейти к содержанию

Шардирование и возобновление

Три Бости бегут друг за другом

Discord распределяет серверы бота по соединениям со шлюзом, которые называются шардами: сервер g принадлежит шарду (g >> 22) % shard_count. Бот, который состоит больше чем в 2500 серверах, обязан шардироваться; Discord закрывает соединение без шардов с кодом 4011, а start() вызывает ShardingRequired.

Шарды в одном процессе

Bot(shard_count="auto"), значение по умолчанию, спрашивает у Discord, сколько шардов использовать, и запускает их все. Больше ничего не нужно: bostie запускает шарды в пределах лимитов Discord на identify, и каждый шард отправляет свои события тем же слушателям, кэшу и обработчикам. event.shard_id говорит, через какой шард пришло событие.

Шарды в нескольких процессах

import asyncio
import logging
import os

import bostie
from bostie.events import ShardDisconnect, ShardReady

log = logging.getLogger("my-bot")
cluster = int(os.environ.get("CLUSTER", "0"))
identify_lock = asyncio.Lock()


async def wait_for_turn(shard_id: int) -> None:
    async with identify_lock:
        await asyncio.sleep(5)


bot = bostie.Bot(
    shard_count=16,
    shard_ids=range(cluster * 4, cluster * 4 + 4),
    before_identify=wait_for_turn,
    session_file=f"sessions-{cluster}.json",
)


@bot.listen
async def shard_ready(event: ShardReady) -> None:
    log.info("shard %d is ready", event.shard_id)


@bot.listen
async def shard_lost(event: ShardDisconnect) -> None:
    action = "resuming" if event.resuming else "starting a new session"
    log.info("shard %d disconnected (%s), %s", event.shard_id, event.code, action)


bot.run()

shard_count=16 фиксирует общее число, а shard_ids выбирает шарды, которые запускает этот процесс; shard_ids требует явного shard_count, а его ID должны быть различными и лежать в range(shard_count).

Discord разрешает боту ограниченное число одновременных identify. В пределах одного процесса bostie соблюдает этот лимит сам; между процессами их согласует before_identify. bostie дожидается before_identify(shard_id) перед каждым identify, так что хук может ждать слота в блокировке, общей для ваших процессов. В примере identify_lock заменяет такую общую блокировку и удерживается те 5 секунд, которые Discord просит выдерживать между identify в одном бакете. Если хук выбрасывает исключение, bostie пишет ошибку в лог и всё равно выполняет identify.

События шардов

Событие Поля Когда отправляется
ShardReady shard_id новая сессия шарда готова и его серверы получены
ShardDisconnect shard_id, code, reason, resuming соединение оборвалось; при resuming шард возобновляет сессию и повторно получает пропущенные события, иначе начинает новую сессию
ShardStop shard_id, code, reason шард остановился окончательно, потому что был вызван close() или Discord его завершил
ShardBackpressure shard_id, queued_bytes шард приостановил чтение, потому что цикл событий отстаёт

Переподключения и возобновления не требуют от вас никаких действий. bot.latency — среднее время кругового пути heartbeat по шардам, а bot.gateway.shard(shard_id).latency — для одного шарда.

Возобновление после перезапуска

Обычно перезапуск начинает новую сессию на каждом шарде, и события за минуты простоя теряются. С Bot(session_file="sessions.json") метод close() оставляет сессии открытыми и записывает их в файл. Следующий start() читает файл, удаляет его и возобновляет каждую сохранённую сессию, которая соответствует текущим shard_count и ID шардов, так что Discord повторно присылает события, произошедшие, пока бот не работал. Файл, который не удалось прочитать, пишется в лог и игнорируется, а шарды без сохранённой сессии выполняют identify.

bot.run() закрывается по Ctrl+C и SIGTERM, поэтому перезапуск менеджером процессов или при деплое сохраняет сессии. Падение — нет, и следующий запуск выполняет identify. Взаимодействие, повторно полученное позже чем через 5 секунд после создания, пропускается с предупреждением, потому что на его токен уже не успеть ответить.