
Оптимизация производительности RPC с помощью пакетных запросов и кэширования слоев
18 июня 2025 г.DAPPS с тяжелым трафиком, которые много раз запрашивают блокчейн Ethereum в течение короткого промежутка, увидят затоки и рабочую нагрузку узлов. Каждое пользовательское обновление набор на панели, запрос на баланс токенов или проблемы с чтением контракта делает вызовы JSON-RPC в узел Ethereum-и чем больше пользовательская база, тем больше вызовов сделаны. Это может привести к медленным ответам, более высоким сборам и плохому пользовательскому опыту. Здесь мы изучим два мощных метода избежания этих проблем: пакетный несколько вызовов RPC в один запрос и введем кэширующие слои для повторного использования частых данных. С помощью запросов на партии Web3.JS, а также кэширование (в памяти и с помощью таких инструментов, как Redis или Memcached), разработчики с полным стеком могут эффективно уменьшить задержку и осветить нагрузку на узлы Ethereum.
Почему партия и кэширование важны для DAPP с высоким трафиком
Перекрытие и кэширование решают неэффективность избыточных вызовов RPC. Вкратце, партии уменьшает накладные расходы на запрос, пакетные вызовы, а кэширование устраняет повторные запросы. Когда оба используются вместе, они сохраняют пропускную способность и отзывчивость. Результатом является улучшенный пользовательский опыт (более быстрое обновление пользовательского интерфейса) и оптимизированный бэкэнд, который может обрабатывать больший трафик с меньшим давлением.
Обращение к вызовам JSON-RPC
API Ethereum JSON-RPC допускает пакетные запросы, что означает, что несколько запросов могут быть отправлены в одну поездку в обратном пути HTTP. Это сводит к минимуму количество HTTP-запросов и сетевые накладные расходы в оба конца, необходимые для получения данных. Меньше поездок в круглых поездках означают меньшую задержку совокупности и меньше времени процессора для обработки запросов. Фактически, объединение нескольких вызовов в одном запросе может значительно снизить накладные расходы и время отклика.Исследователи обнаружилиЭти пакетные запросы сэкономили около 20% от общего времени запроса по сравнению с отправкой запросов индивидуально. Партийные запросы также гарантируют атомность - все вызовы в партии выполняются в качестве единицы. Окраска в целом делает производительность лучше, выполняя больше работы по сетевому вызову.
Кэширование частых данных
Кэширование - это метод размещения часто доступных данных в быстрое хранилище (память), чтобы последующие запросы могли сделать их быстрее без необходимости неоднократно пинговать удаленный узел. Повторяя кэшированные запросы обратно вам, вы уменьшаете вызовы RPC, сделанные в ваш узел Ethereum и реагируете с результатами быстрее из памяти. Это особенно применимо к DAPPS с тяжелым движением, где многие пользователи получают одни и те же данные (например, цены на токен, данные о последних блоках) или где Frontends проводят опрос для новых данных. Вместо каждого пользователя создает новый запрос блокчейна, кэширование может быть дверью, через которую большинство запросов RPC происходит только один раз в окне кэширования, и последующие вызовы обслуживаются кэшированным результатом. Например, без кеша 10 000 пользователей, запрашивающих цену в цепочке, будут приравниться к 10 000 вызовов узлов. С помощью кэша, если данные кэшируются в течение 60 секунд, на 10 000 пользователей можно ответить с одним вызовом RPC (первый запрос, заполняющий кэш) и 9,999 Fast Cache Hits. Это требует огромного количества рабочей нагрузки и задержки с узлов.
Обработка вызовов JSON-RPC с помощью web3.js
Web3.js имеет встроенныйBatchRequest
Функция, которая позволяет простым обращать несколько вызовов JSON-RPC в один HTTP-запрос. Это достигается за кулисами с поддержкой узла Ethereum для отправки множества запросов и получения множества ответов за один раз. Вместо того, чтобы делать, например, пять отдельных HTTP -запросов, чтобы получить пять различных битов информации, вы можете сделать один запрос, содержащий все пять запросов. Узел будет запускать их (обычно параллельно) и вернет все ответы в одной передаче, щадя много сетевых накладных расходов.
С использованиемweb3.eth.BatchRequest
Чтобы создать пакетный запрос в Web3.js (v1.x и выше), вы начинаете с инициализации новогоweb3.BatchRequest()
экземпляр, а затем добавьте к нему отдельные вызовы. Вы добавляете каждый вызов, используя метод.request()
функционируйте вместе с его аргументами и обратным вызовом для обработки ответа. Наконец, вы выполняете партию сbatch.execute()
Полем Все запросы в партии отправляются сразу.
Например, предположим, что нам нужно получить эфирный баланс некоторых адресов. Мы обычно называемweb3.eth.getBalance(address)
Один за другим в цикле, который выполняет отдельные вызовы RPC. Вместо этого мы можем их выровнять:
const Web3 = require('web3');
const web3 = new Web3('https://your.ethereum.node'); // Your Ethereum node RPC URL
const addresses = [
"0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326",
"0x2bB42C655DdDe64EE508a0Bf29f9D1c6150Bee5F",
// ... more addresses
];
async function getBalancesBatch() {
const batch = new web3.BatchRequest();
addresses.forEach((addr) => {
batch.add(web3.eth.getBalance.request(addr, 'latest', (err, balanceWei) => {
if (err) {
console.error(`Error fetching balance for ${addr}:`, err);
} else {
const balanceEth = web3.utils.fromWei(balanceWei, 'ether');
console.log(`${addr} balance: ${balanceEth} ETH`);
}
}));
});
// Send all requests in one HTTP round trip
batch.execute();
}
getBalancesBatch();
В приведенном выше примере мы добавляем несколькоeth_getBalance
Запросы на партию, затем вызовите их всех вместе. Каждый баланс адреса будет напечатан, когда возвращается пакетный ответ. В этом случае это партии намного лучше, чем создание цикла отдельных запросов, поскольку он предотвращает избыточную стоимость многочисленных HTTP -запросов. Все запросы на баланс выполняются в одном сетевом вызове.
Как это работает?Когдаbatch.execute()
называется, web3.js отправляет одну полезную нагрузку JSON, содержащую массив объектов запроса RPC (каждый со своим собственным методом, параметрами и идентификатором). Узел Ethereum обрабатывает каждый и отвечает множеством результатов. Web3.JS затем вызывает соответствующие обратные вызовы с этими результатами. Задержка сети партии примерно эквивалентна одному запросу, поэтому партия (например,) 10 вызовов означает, что вы по существу уменьшили то, что было бы 10 круглыми поездками до 1. Все эти вызовы, обрабатываемые в одной поездке, значительно минимизируют накладные расходы клиента и улучшают общее время отклика.
Пример вариантов использования для пакетных запросов
Перекрытие пригодится, когда вашему DAPP необходимо принести несколько фрагментов данных одновременно, если это возможно. Несколько общих сценариев включают:
Получение нескольких балансов токенов
Рассмотрим кошелек, отображающий остатки пользователя различных токенов ERC-20. Вместо того, чтобы называть каждый токен контрактbalanceOf
отдельно (что было бы многоeth_call
RPC Calls), вы можете выровнять их все. Например, вы можете партияbalanceOf(user, TokenA)
ВbalanceOf(user, TokenB)
ВbalanceOf(user, TokenC)
и так далее, в одном запросе. Узел отправит все балансы токенов за один ответ. Это дает более быстрый, консолидированный ответ и сводит к минимуму нагрузку.
Многокачественные контракты на цепочке также могут достичь аналогичной цели, но отключение json-rpc с нецелесообразной партией проще и было обнаружено, что в некоторых случаях было обнаружено, что в некоторых случаях было немного превосходит многотоколы.
Привлечение нескольких транзакций или информации о блоке
Если ваше приложение нуждается в данных из нескольких блоков или транзакций (например, получения списка блоков в прошлом или данных о многочисленных хэшах транзакций), вы используете партию. Например, чтобы получить данные для блока N, N+1 и N+2, вы можете положить триeth_getBlockByNumber
Звонки в BatchRequest вместо того, чтобы делать их индивидуально. Вы можете взять несколько звонковeth_getTransactionReceipt
Если вы звоните в список хэшей транзакции. Узел будет вызывать их последовательно и ответить массивом объектов блока или транзакции за один вызов.
Обращение с несколькими контрактными звонками сразу
Любой сценарий, в котором вам нужно позвонить в сотни функций смарт-контракта только для чтения, может выиграть от партии. Чтобы привести пример, на DEFI Dashboard может быть несколько данных запроса контрактов (цены, общее предложение, позиции пользователя) при нагрузке. Используя web3.js, вы можете иметь все этиcontract.methods.myMethod()
.call запросы подготовлены и поместите их в партию. Бэкэнд DAPP затем делает один запрос на узел и получает все необходимые данные, требуемые сразу, улучшая время нагрузки.
Лучшая практикаПолем Держите размеры партий разумными и помните о пределах поставщиков. Некоторые поставщики имеют ограничение на количество вызовов на партию (например, алхимия имеет 1000 вызовов на партию по сравнению с HTTP). Кроме того, чрезвычайно большие партии медленнее или могут быть больше, чем ограничения размера отклика. Как правило, он эффективен для получения небольшой группы вызовов, которые имеют смысл только как партия (например, данные для одной страницы или единого пользовательского действия). Также помните, что если один вызов в пакете не удастся, он не помешает другим добиться успеха, но вам необходимо обработать ответы на ошибки на основе вызова в рамках пакетного ответа. Как правило, партия запросов JSON-RPC является простым решением для сокращения задержки на страницу нагрузку или событие на каждого пользователя с помощью минимальных круглых поездок.
Кэширование ответов RPC Ethereum в node.js
В то время как партия снижает затраты на запрос, кэширование решает проблему избыточных запросов. Есть много DAPP, которые запрашивают некоторые данные часто или многих пользователей, спрашивающих те же данные (например, текущая цена ETH или общая подача токена тренда или баланс пользователя, который просят об изменениях). Сохранение этих ответов в оптимизированном магазине может в значительной степени устранить избыточные вызовы и улучшить ответ.
Как это помогает? Как только мы извлекаем элемент данных из блокчейна, мы кэшируем его (в памяти или в каком -либо другом хранилище кеша). Последующие запросы на ту же информацию могут быть обслуживались из кэша в микросекундах, не выполняя сетевую поездку об обратном пути к узлу. Кэширование копий часто доступ к данным к быстрому хранению вблизи приложения, улучшая производительность и масштабируемость за счет сокращения того, как часто вы попадаете в бэкэндскую систему. Кэширование «часто используемые данные API в памяти» означает, что доступна локальная копия, к которой можно получить доступ гораздо быстрее, чем извлечение из удаленной конечной точки RPC каждый раз.
Есть некоторые методы кэширования, которые вы можете реализовать в node.js:
Кэширование в памяти (кэш LRU)
Для одного процесса Node.js или небольшого применения кэширование в памяти является самым простым. Данные хранятся в памяти (ОЗУ) процесса узла для быстрого доступа. Он работает для данных, которые часто смотрят, но не часто модифицируются. Вы можете ограничить использование памяти и удалить наименее недавно используемые элементы, когда кэш заполнен с использованием кэша LRU (наименьший недавно).
Выполнение.Вы можете использовать кэш в памяти с широко известными библиотеками, такими какnode-cache
Вmemory-cache
, илиlru-cache
Полем Например, используяlru-cache
упаковка:
// Install with: npm install lru-cache
const LRU = require('lru-cache');
// Create an LRU cache that holds up to 500 items and expires items after 60 seconds
const cache = new LRU({
max: 500, // maximum number of items
ttl: 1000 * 60 // time-to-live in ms (here, 60s)
});
// Function to get token balance with caching
async function getTokenBalanceCached(tokenContract, userAddress) {
const cacheKey = `${tokenContract.options.address}:${userAddress}:balance`;
const cachedValue = cache.get(cacheKey);
if (cachedValue) {
console.log('Cache hit for token balance');
return cachedValue; // return the cached balance (assumed to be still fresh)
}
console.log('Cache miss, fetching from blockchain...');
// Call the blockchain (e.g., ERC20 balanceOf)
const balance = await tokenContract.methods.balanceOf(userAddress).call();
cache.set(cacheKey, balance);
return balance;
}
Здесь, в этом фрагменте, мы проверяем, есть ли у нас кэшированный результат для баланса токена пользователя, прежде чем сделать вызов RPC. Если мы это сделаем, мы немедленно вернем это («из кеша»). В противном случае мы читаем из блокчейна, а затем кэшируйте результат в следующий раз. Мы также устанавливаем TTL (время к жизни), чтобы запись была удалена после, скажем, 60 секунд, чтобы мы не возвращаем устаревшие данные навсегда. Этот подход аналогичен примеру ответов на кэширование API в течение 1 минуты с использованием кэша в памяти, который подчеркивал, что часто доступные данные, которые часто не меняются, являются хорошим кандидатом на краткосрочное кэширование.
Когда использовать? Кэши в памяти быстрые (сетевые вызовы не требуются для извлечения данных) и просты. Тем не менее, кэшированные данные присутствуют только в этом процессе node.js - если у вас есть несколько экземпляров вашего сервера, каждый из них будет иметь свой отдельный кэш. Кэши памяти также переходные: когда процесс перезагружается, кэш отбрасывается. Несмотря на эти ограничения, кэширование в памяти идеально подходит для мелкого использования или для данных кэширования, которые часто получают доступ в течение короткого периода в результате того же процесса.
Распределенное кэширование с использованием Redis или Memcached
Для более крупных приложений или нескольких экземпляров серверов рекомендуется иметь распределенный кэш, чтобы кэш был обмен и сохраняется в окружающей среде. Такие инструменты, как Redis и Memcached, являются хранилищами данных в памяти, которые работают как отдельные услуги. Ваше приложение Node.js может использовать этот хранилище кэша по сети. Redis очень популярен из-за своей производительности и богатства функций, а Memcached-это легкий кэш, оптимизированный для простого кэширования ключей. Оба могут значительно разгрузить базу данных или чтения узла при использовании в качестве кэширующего слоя.
Реализация с Redis. Чтобы использовать Redis в node.js, вы можете использовать официальныйredis
Клиенты или клиенты сообщества любятioredis
Полем Во -первых, убедитесь, что у вас есть работает сервер Redis и подключитесь к нему:
// Install with: npm install redis
const redis = require('redis');
const client = redis.createClient({ url: 'redis://localhost:6379' });
await client.connect(); // connect to Redis server
async function getLatestBlockNumberCached() {
const cacheKey = 'latestBlockNumber';
// Try to get the value from Redis
const cachedVal = await client.get(cacheKey);
if (cachedVal !== null) {
return Number(cachedVal); // return cached block number
}
// If not in cache, fetch from Ethereum node
const latestBlock = await web3.eth.getBlockNumber();
// Store in cache with an expiration (e.g., 5 seconds, since blocks update frequently)
await client.set(cacheKey, latestBlock, { EX: 5 });
return latestBlock;
}
Здесь мы пытаемся получить кэширование"latestBlockNumber"
от Redis. Если присутствует, мы немедленно вернемся. Если нет присутствующего, или ноль, мы звоняемweb3.eth.getBlockNumber()
Чтобы спросить узел, а затем кэшировать это число в Redis с истечением 5 секунд (Ex = 5 секунд). Второй вызов в течение 5 секунд будет против кеша. Это уменьшает количество раз, когда узел должен быть достигнут за новую высоту блока - то, что часто можно спрашивать большим количеством клиентов. Redis - это общая память для всех ваших процессов узел, поэтому независимо от количества экземпляров вашего приложения, которые все они проверяют один и тот же кэш. Преимущество производительности огромна: загрузка данных из Redis (которая находится в памяти), находится в порядке микро- или миллисекундов, что намного быстрее, чем RPC, который может занять десятки или сотни миллисекунд и потреблять ресурсы узлов.
Реализация с Memcached.Он работает так же, используя MemCached: вы используете клиент с узел Memcached (например,memcached
пакет NPM), подключитесь к серверу Memcached и вызовитеclient.get(key)
/ client.set(key, value)
с истечением срока действия. Сначала применяется та же самая логика проверки кэша, а затем по умолчанию на вызов RPC. Memcached - немного проще (только струны, без устойчивости, выселение LRU по умолчанию) и может быть очень, очень быстро для чтения кеша. Ваш выбор между Redis и Memcached затем сводит к вашему стеку и требованиям - оба являются отличными уровнями кэширования для данных RPC.
Кэш недействительный и управление устаревшим данных
Одной из самых больших проблем кэширования является недействительность кеша - то есть, когда обновлять или истекать кэшированной информации. Для информации о блокчейне мы должны быть осторожны, чтобы сбалансировать свежесть и производительность.
TTL (время для жизни)
Всегда предоставляйте соответствующий TTL для кэшированного RPC -результатов в зависимости от того, насколько быстро могут измениться основные данные. Например, можно кэшировать текущий номер блока в течение 5-10 секунд, потому что новые блоки прибывают примерно каждые ~ 12 секунд на Ethereum. Кэширование баланса токена может основываться на использовании-если пользователь не собирается замечать новые транзакции в течение следующей минуты или около того, кэш 30-60 секунд должен быть в порядке. Но для очень нестабильных данных (например, быстро обновляя цену в цепочке), вы можете кэшировать в течение нескольких секунд или вообще не кэш. Дело в том, чтобы избежать обслуживания устаревших данных, которые могли бы запутать или ввести в заблуждение пользователей. Вы используете короткие TTL, поэтому кэш в любом случае будет промыть, даже если вы не тратите время, чтобы аннулировать его.
Явное признание событий
В дополнение к тому времени, вы можете явно недействительно или обновлять значения кэша всякий раз, когда вы узнаете о соответствующем событии в цепочке. Например, если ваш бэкэнд только что увидел (или опубликовал) транзакцию, которая меняет баланс токена пользователя, вам необходимо немедленно закончить кеш для этого ключа баланса. Это приведет к следующему чтению, чтобы получить новое значение из узла (или даже кэшировать само новое ожидаемое значение).
Другой пример: если вы кэшируете все о блоке, когда вы получаете новый блок, вы можете аннулировать данные кэшированного «последнего блока», чтобы следующий запрос получил новую информацию. Привязывая кэш -недействительность с реальными событиями (новые блоки, пользовательские транзакции, изменения состояния контракта), вы минимизируете устаревшие данные.
Избегание устаревших или неправильных данных
Для критических данных в режиме реального времени вообще не используйте такие стратегии, как Stail-While-revalidate. Уверяющий, что-то-ревалидат-это подход, в котором вы сразу же предоставляете кэшированный контент (чтобы не задержать), но также инициируете асинхронное обновление параллельно. Таким образом, пользователь получает что -то мгновенно, и, если ранее устаревшая, ваша система скоро освежит кэш для будущего. В тех случаях, когда требуется абсолютный в режиме реального времени (например, показ статус вновь проводной транзакции), кэш будет полностью пропущен для простого вызова RPC или подписки WebSocket.
Помнить.Кэширование-это компромисс между производительностью и свежестью-во многих случаях использования, которые в течение нескольких минут или секунды-это справедливая торговля даже в течение нескольких минут или секунд, которые значительно повышают производительность, но вы должны выяснить, какие аспекты вашего приложения могут переносить небольшую задержку в данных, которые требуют точность в секунде.
Пользовательские данные
Держась от кэширования данных, уникальных для отдельного пользователя (например, баланс их учетной записи или их личные данные портфеля). Если поделиться с кешем, ключи пространства имен от пользователя, так что есть шанс для утечек (например, добавить адрес или идентификатор пользователя в начало ключа кеша, как мы сделали с балансом токена вcacheKey
) Для поистине конфиденциальной информации выберите его только в браузере пользователя (через кеш для фронта или LocalStorage) или вообще нет, чтобы устранить даже возможность получить доступ к нему пользователю. Всегда избегайте кэширования личных данных пользователя таким образом, чтобы они могли отправлять обратно другим пользователям-использовать кэши для для сеанса или для каждого пользователя (или отключить кэширование для этих маршрутов).
Потепление кеша и закупорители
В системах с высоким трафиком это часто предварительно заполняет кеши (погрешение кэша) для известных дорогих запросов, чтобы пользователи никогда не испытывали задержки холодного кэша. Кроме того, рассмотрим стратегию отступления: если узел Ethereum находится под тяжелой нагрузкой или временно недоступной, ваша система может служить последним известным кэшированным данным (с флагом, указывающим, что он может быть устарел), а не полностью сбой. Это форма изящной деградации. В некоторых реализациях используется шаблон автоматического выключателя, где, если вызовы RPC не сняты или выведены, служба возвращает кэшированный ответ, чтобы поддерживать работу приложения.
Например, если вы не можете получить последний канал цен из -за проблемы с сетью, вы возвращаете цену несколько минут назад из Cache с предупреждением. Это обеспечивает непрерывность обслуживания - пользователь видит несколько устаревшую информацию вместо сообщения об ошибке.
Улучшение UX и эффективности с помощью партии и кэширования
Приняв партирование и кэширование, вы можете кардинально улучшить пользовательский опыт на переднем крае, а также на средних показателях вашего DAPP.
Снижение задержки для пользователей
Партия избегает ненужного ожидания, получая несколько бит информации в одном запросе. Пользователи могут быстрее загружать свои панели мониторинга и данные, потому что накладные расходы, связанные с каждым инкрементным вызовом RPC, устраняются. Вместо того, чтобы загружать последовательность (которая может водопада в медленный опыт), пакетные вызовы предоставляют данные почти параллельно. Кэширование добавляет к иллюзии скорости - повторные операции или посещения могут быть практически мгновенными, когда данные доступны из памяти. Такая отзывчивость сохраняет целенаправленные и довольные пользователей.
Меньше рабочей нагрузки узлов
С точки зрения узла, пакетирование и кэширование в значительной степени уменьшают поступающие запросы. Вместо того, чтобы, например, 100 отдельных запросов JSON-RPC на узел getBlock, вы получаете 1 пакетный запрос или несколько запросов о пропуске кеша. Это приводит к меньшему использованию процессора, меньшему сетевому вводу/выводу и большему количеству пользователей на одном и том же оборудовании узла. Частое считывание потоковой передачи в кэшах позволяет не делать повторения узла. На практике DAPPS, в котором реализовали кэширование, наблюдались резкие падения трафика RPC - тысячи запросов пользователей могут сводить к одному кэшированному запросу в минуту. Это не только повышает производительность, но также может сократить расходы, если вы используете платный поставщик RPC (поскольку вы делаете меньше звонков в целом).
Масштабируемость и стабильность
Занятые периоды (например, когда многие пользователи находятся в Интернете или когда происходит очень популярное падение NFT), затоплят узел Ethereum с запросами. Перекрытие гарантирует, что ваше приложение по -прежнему использует каждый запрос во время загрузки, а кэширование защиты вашего приложения из шипов путем облегчения спроса на чтение. Результатом является более масштабируемая система, которая с меньшей вероятностью достигнет ограничений или тайм -аутов поставщиков. Ваш DAPP остается стабильным и отзывчивым в результате трафика, потому что основные вызовы RPC оптимизированы и контролируются.
Заключение
Оптимизация производительности RPC с использованием пакетных запросов и слоев кэширования является наилучшей практикой для любого серьезного DAPP. Эти подходы являются дополнительными: партия поддерживает низкую задержку, выполняя больше сетевого вызова, а кэширование предотвращает вызов столько же, сколько путем утилизации результатов. Осторожно, используя оба, вы можете предоставить более четкое, более эффективное приложение. У пользователя есть легкий, плавный опыт, и ваш узел Ethereum (или RPC, такой как GetBlock), становится значительно меньшей нагрузкой-беспроигрышная как для пользовательского опыта, так и для здоровья бэкэнд.
Оригинал