Пакетні запити (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.
Стратегія групування
Замість того щоб відправляти всі запити одним великим пакетом, використовуйте логічне групування:
- За контекстом: об'єднуйте запити, які логічно належать до однієї операції (наприклад, всі читання для побудови однієї транзакції).
- За типом методу: групуйте читання (
getAccountInfo,getBalance) окремо від запитів, що можуть викликати важчу роботу вузла. - За пріоритетом: критичні запити відправляйте першими в пакеті — якщо провайдер обрізає пакет при перевищенні ліміту, ви хочете отримати найважливіші дані.
Паралелізм замість одного великого пакету
Якщо вам потрібно виконати 500 запитів, а ліміт провайдера — 50 елементів на пакет, не відправляйте 10 пакетів послідовно. Використовуйте паралельні HTTP-з'єднання (з контролем concurrency), щоб відправити кілька пакетів одночасно. Це дає комбінований ефект: менше round-trip у межах кожного пакету та паралелізм між пакетами.
Обробка часткових помилок у пакеті
На відміну від одиночного запиту, де помилка означає відмову всього виклику, у пакеті частина елементів може успішно виконатися, а частина — повернути помилку. Це ключова відмінність, яку ігнорують у більшості реалізацій.
Типи помилок у пакеті
| Рівень | Ознака | Причина |
|---|---|---|
| Помилка всього пакету | Сервер повертає не масив, а одиночний об'єкт з error |
Некоректний JSON, порожній масив, перевищення загального ліміту розміру тіла |
| Помилка окремого елемента | Елемент у масиві відповідей містить error замість result |
Невідомий метод, невалідні параметри, відсутній акаунт (залежить від методу) |
| Таймаут елемента | Відповідь не містить елемента з відповідним id |
Вузло обірвав з'єднання, внутрішній таймаут обробки |
Алгоритм обробки
- Перевірте тип відповіді. Якщо сервер повернув одиночний об'єкт з помилкою — весь пакет відхилений. Не намагайтеся шукати часткові результати.
- Звіріть кількість елементів. Якщо у запиті було N елементів, а у відповіді M (M < N) — деякі елементи не були оброблені. Ідентифікуйте їх за відсутніми
id. - Ітеруйте по відповідях. Для кожного елемента перевірте наявність поля
error. Обробляйте успішні та неуспішні елементи незалежно. - Логікуйте невідповідності. Записуйте повний масив відповідей у логи при будь-якій частковій відмові — це критично для діагностики.
Приклад обробки на псевдокоді
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
Почніть з реалізації, яка за замовчуванням використовує пакетні запити, але при помилці рівня пакету автоматично перемикається на послідовні одиночні виклики для того ж набору даних:
- Спроба відправити пакет.
- Якщо отримано помилку рівня пакету — логування помилки та виконання тих самих запитів по одному.
- Метрика: лічильник fallback-подій. Якщо він росте — пакетний режим у поточних умовах не працює.
Цей підхід гарантує, що міграція не погіршить доступність сервісу.
Крок 4. Перевірка на staging
Перед виведенням у production перевірте на staging-середовищі:
- Коректність даних: порівняйте результати пакетних викликів з одиночними для тих самих вхідних даних. Різниця у значеннях
slotу контексті допустима, різниця у самих даних — ні. - Поведінка при лімітах: відправте пакет, що перевищує ліміт провайдера. Переконайтеся, що fallback спрацьовує коректно.
- Поведінка при частковій відмові: імітуйте сценарій, коли частина методів у пакеті повертає помилку (наприклад, запит неіснуючого акаунта).
- Навантаження: виміряйте час виконання та споживання пам'яті при типовому обсязі пакетів для вашого сервісу.
Крок 5. Поетапне виведення в production
- Канарійний реліз: увімкніть пакетний режим для невеликого відсотка трафіку (наприклад, 5%).
- Моніторинг метрик: час відповіді RPC-шару, кількість fallback-подій, кількість часткових помилок, споживання пам'яті.
- Розгортання: якщо метрики стабільні протягом достатнього періоду — збільшуйте відсоток трафіку.
- Повний відкат: якщо метрики погіршуються — вимкніть пакетний режим через конфігурацію. Завдяки ізоляції транспортного шару це не вимагає розгортання.
Типові помилки міграції
- Ігнорування порядку відповідей. Хоча специфікація вимагає зберігати порядок, деякі проміжні проксі можуть його порушити. Завжди спираєтеся на поле
id, а не на індекс у масиві. - Пакетування записувальних методів.
sendTransactionта інші методи, що змінюють стан, не варто пакетувати з читаннями — це ускладнює retry-логіку та може призвести до дублювання транзакцій при частковій відмові. - Безконтрольне зростання розміру пакету. Динамічні списки акаунтів можуть неочікувано зрости. Завжди обмежуйте розмір пакету на рівні клієнта, незалежно від лімітів провайдера.
- Відсутність таймауту на рівні пакету. Великий пакет обробляється довше, ніж одиночний запит. Налаштуйте таймаут HTTP-клієнта з урахуванням максимального розміру пакету.
Після успішної міграції наступний логічний крок — інтеграція пакетних запитів у архітектуру data pipeline, де вони стають частиною більшої системи збору та обробки даних з Solana.