Пакетні запити (batch requests) у JSON-RPC 2.0 дозволяють відправити кілька викликів до вузла Solana в одному HTTP-запиті. Це зменшує кількість TCP-з'єднань та round-trip до сервера, що критично для data-пайплайнів, індексаторів та сервісів, яким потрібно агрегувати стан кількох акаунтів або транзакцій за один такт.

Нижче — практичний протокол роботи з пакетними запитами: від формату та оптимізації до обробки помилок і міграції існуючого коду.

Формат пакетних запитів у RPC 2.0

JSON-RPC 2.0 визначає пакетний запит як масив об'єктів, де кожен об'єкт — це окремий виклик з власним ідентифікатором. Сервер зобов'язаний повернути масив відповідей у тому ж порядку, зберігаючи відповідність за полем id.

Базова структура запиту:

[
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getAccountInfo",
    "params": [
      "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      {"encoding": "base64"}
    ]
  },
  {
    "jsonrpc": "2.0",
    "id": 2,
    "method": "getBalance",
    "params": ["7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"]
  }
]

Відповідь — масив об'єктів, де кожен містить або поле result, або поле error:

[
  {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "context": {"slot": 123456789},
      "value": {"data": [...], "lamports": 0, ...}
    }
  },
  {
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
      "context": {"slot": 123456789},
      "value": 1500000
    }
  }
]

Ключові вимоги до формату

  • Поле jsonrpc — обов'язкове для кожного елемента, значення завжди "2.0".
  • Поле id — рядок, число або null. У пакетних запитах використовуйте унікальні ідентифікатори для кожного виклику, інакше неможливо надійно зіставити запити з відповідями. Не використовуйте null у пакетах.
  • Порожній масив — відповідно до специфікації, порожній масив запитів є некоректним. Сервер має повернути помилку на рівні всього запиту.
  • Одинарний елемент — якщо масив містить один елемент, сервер може повернути як масив, так і одиночний об'єкт. Ваш клієнт має обробляти обидва варіанти.

Обмеження провайдерів

Кожен RPC-провайдер встановлює власні ліміти на пакетні запити. Типові обмеження, які варто перевірити в документації вашого провайдера:

  • Максимальна кількість елементів у пакеті (у деяких публічних вузлах — від 10 до 100).
  • Максимальний розмір тіла HTTP-запиту в байтах.
  • Чи рахується пакет як один виклик для rate limiting, чи кожен елемент окремо.
  • Чи підтримуються пакетні запити взагалі (не всі проксі та балансувальники їх коректно прокидують).

Перед розгортанням перевірте ці параметри безпосередньо у вашого провайдера. Не покладайтеся на значення з інших джерел — ліміти різняться.

Оптимізація кількості round-trip

Головна мета пакетних запитів — зменшити кількість мережевих циклів між вашим сервісом та RPC-вузлом. Це особливо помітно коли клієнт і вузол знаходяться в різних дата-центрах.

Коли пакетування дає реальний виграш

  • Масове зчитування стану: отримання getAccountInfo для десятків PDA-адрес одного програмного модуля за один запит.
  • Агрегація балансів: перевірка SOL-балансів списку акаунтів перед розподілом токенів.
  • Комбіновані запити: одночасне отримання getSlot, getBlockHeight, getLatestBlockhash та даних акаунтів для побудови транзакції.

Коли пакетування не допомагає

  • Послідовні залежності: якщо результат запиту A потрібен як параметр для запиту B, їх неможливо об'єднати в один пакет.
  • Підписки (websocket): пакетування стосується виключно HTTP-викликів. Для підписок використовуйте окремий механізм (детальніше — у матеріалі про confirmed та finalized підписки).
  • Одиничні запити: накладні витрати на формування масиву перевищують виграш від одного round-trip.

Стратегія групування

Замість того щоб відправляти всі запити одним великим пакетом, використовуйте логічне групування:

  1. За контекстом: об'єднуйте запити, які логічно належать до однієї операції (наприклад, всі читання для побудови однієї транзакції).
  2. За типом методу: групуйте читання (getAccountInfo, getBalance) окремо від запитів, що можуть викликати важчу роботу вузла.
  3. За пріоритетом: критичні запити відправляйте першими в пакеті — якщо провайдер обрізає пакет при перевищенні ліміту, ви хочете отримати найважливіші дані.

Паралелізм замість одного великого пакету

Якщо вам потрібно виконати 500 запитів, а ліміт провайдера — 50 елементів на пакет, не відправляйте 10 пакетів послідовно. Використовуйте паралельні HTTP-з'єднання (з контролем concurrency), щоб відправити кілька пакетів одночасно. Це дає комбінований ефект: менше round-trip у межах кожного пакету та паралелізм між пакетами.

Обробка часткових помилок у пакеті

На відміну від одиночного запиту, де помилка означає відмову всього виклику, у пакеті частина елементів може успішно виконатися, а частина — повернути помилку. Це ключова відмінність, яку ігнорують у більшості реалізацій.

Типи помилок у пакеті

Рівень Ознака Причина
Помилка всього пакету Сервер повертає не масив, а одиночний об'єкт з error Некоректний JSON, порожній масив, перевищення загального ліміту розміру тіла
Помилка окремого елемента Елемент у масиві відповідей містить error замість result Невідомий метод, невалідні параметри, відсутній акаунт (залежить від методу)
Таймаут елемента Відповідь не містить елемента з відповідним id Вузло обірвав з'єднання, внутрішній таймаут обробки

Алгоритм обробки

  1. Перевірте тип відповіді. Якщо сервер повернув одиночний об'єкт з помилкою — весь пакет відхилений. Не намагайтеся шукати часткові результати.
  2. Звіріть кількість елементів. Якщо у запиті було N елементів, а у відповіді M (M < N) — деякі елементи не були оброблені. Ідентифікуйте їх за відсутніми id.
  3. Ітеруйте по відповідях. Для кожного елемента перевірте наявність поля error. Обробляйте успішні та неуспішні елементи незалежно.
  4. Логікуйте невідповідності. Записуйте повний масив відповідей у логи при будь-якій частковій відмові — це критично для діагностики.

Приклад обробки на псевдокоді

response = sendBatch(requests)

// Крок 1: перевірка на відмову всього пакету
if not isArray(response):
    handleBatchLevelError(response.error)
    return

// Крок 2: перевірка повноти відповіді
requestIds = set(requests.map(r => r.id))
responseIds = set(response.map(r => r.id))
missingIds = requestIds - responseIds

if missingIds is not empty:
    logWarning("Не отримано відповіді для id: " + missingIds)

// Крок 3: покрокова обробка
results = {}
errors = {}

for item in response:
    if item.error:
        errors[item.id] = item.error
    else:
        results[item.id] = item.result

// Крок 4: прийняття рішення
if errors is not empty:
    handlePartialFailure(results, errors, missingIds)

Часткова відмова та бізнес-логіка

Рішення про те, чи можна використовувати часткові результати, залежить від контексту:

  • Індексація: якщо з 50 акаунтів 48 прочитано успішно, а 2 повернули помилку — можна індексувати 48 і повторити 2 окремо.
  • Побудова транзакції: якщо хоча б один getAccountInfo у пакеті повернув помилку, транзакцію, ймовірно, не можна побудувати коректно. Потрібен повний відкат і повторний запит.
  • Моніторинг балансів: часткові результати прийнятні, але потрібно маркувати відсутні значення окремо, щоб не плутати їх із реальним нульовим балансом.

Міграція з окремих викликів на пакетні

Перехід від послідовних одиночних викликів до пакетних вимагає зміни архітектури клієнтського коду. Нижче — перевірена послідовність кроків, яка мінімізує ризики в production.

Крок 1. Аудит існуючих викликів

Зберіть повний перелік RPC-методів, які ваш сервіс викликає регулярно. Для кожного зафіксуйте:

  • Чи є залежності між викликами (результат одного — параметр іншого).
  • Чи є виклики, що відбуваються у межах одного логічного контексту (одна транзакція, один цикл індексації).
  • Частоту та обсяг кожного виклику.

Тільки незалежні виклики всередині одного контексту — кандидати на пакетування.

Крок 2. Ізоляція транспортного шару

Виділіть роботу з RPC у окремий модуль (або клас), який приховає різницю між одиночним і пакетним викликом. Це дозволить:

  • Перемикатися між режимами через конфігурацію без зміни бізнес-логіки.
  • Додати логування, метрики та retry-логіку в одному місці.
  • Швидко відкотитися до одиночних викликів, якщо пакетні виявляться проблемними.

Крок 3. Реалізація з fallback

Почніть з реалізації, яка за замовчуванням використовує пакетні запити, але при помилці рівня пакету автоматично перемикається на послідовні одиночні виклики для того ж набору даних:

  1. Спроба відправити пакет.
  2. Якщо отримано помилку рівня пакету — логування помилки та виконання тих самих запитів по одному.
  3. Метрика: лічильник fallback-подій. Якщо він росте — пакетний режим у поточних умовах не працює.

Цей підхід гарантує, що міграція не погіршить доступність сервісу.

Крок 4. Перевірка на staging

Перед виведенням у production перевірте на staging-середовищі:

  • Коректність даних: порівняйте результати пакетних викликів з одиночними для тих самих вхідних даних. Різниця у значеннях slot у контексті допустима, різниця у самих даних — ні.
  • Поведінка при лімітах: відправте пакет, що перевищує ліміт провайдера. Переконайтеся, що fallback спрацьовує коректно.
  • Поведінка при частковій відмові: імітуйте сценарій, коли частина методів у пакеті повертає помилку (наприклад, запит неіснуючого акаунта).
  • Навантаження: виміряйте час виконання та споживання пам'яті при типовому обсязі пакетів для вашого сервісу.

Крок 5. Поетапне виведення в production

  1. Канарійний реліз: увімкніть пакетний режим для невеликого відсотка трафіку (наприклад, 5%).
  2. Моніторинг метрик: час відповіді RPC-шару, кількість fallback-подій, кількість часткових помилок, споживання пам'яті.
  3. Розгортання: якщо метрики стабільні протягом достатнього періоду — збільшуйте відсоток трафіку.
  4. Повний відкат: якщо метрики погіршуються — вимкніть пакетний режим через конфігурацію. Завдяки ізоляції транспортного шару це не вимагає розгортання.

Типові помилки міграції

  • Ігнорування порядку відповідей. Хоча специфікація вимагає зберігати порядок, деякі проміжні проксі можуть його порушити. Завжди спираєтеся на поле id, а не на індекс у масиві.
  • Пакетування записувальних методів. sendTransaction та інші методи, що змінюють стан, не варто пакетувати з читаннями — це ускладнює retry-логіку та може призвести до дублювання транзакцій при частковій відмові.
  • Безконтрольне зростання розміру пакету. Динамічні списки акаунтів можуть неочікувано зрости. Завжди обмежуйте розмір пакету на рівні клієнта, незалежно від лімітів провайдера.
  • Відсутність таймауту на рівні пакету. Великий пакет обробляється довше, ніж одиночний запит. Налаштуйте таймаут HTTP-клієнта з урахуванням максимального розміру пакету.

Після успішної міграції наступний логічний крок — інтеграція пакетних запитів у архітектуру data pipeline, де вони стають частиною більшої системи збору та обробки даних з Solana.

Джерела