Масштабирование блокчейн-инфраструктуры

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1Все 1306 услуг
Масштабирование блокчейн-инфраструктуры
Сложный
~1-2 недели
Часто задаваемые вопросы

Направления блокчейн-разработки

Этапы блокчейн-разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1308
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1221
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    921
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1149
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    612
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    886

Масштабирование блокчейн-инфраструктуры

Инфраструктура, которая нормально работала при 100 пользователях, начинает сыпаться при 10 000. Специфика блокчейн-стека в том, что узкое место часто не там, где ожидаешь: не база данных, не CPU — а RPC нода, которая не успевает отдавать eth_getLogs, или indexer, который отстаёт на 50 блоков, или WebSocket handler, который дропает соединения под нагрузкой. Масштабирование блокчейн-инфраструктуры — это отдельная дисциплина с непривычными паттернами.

Диагностика: где реальное узкое место

Прежде чем что-то масштабировать — измерьте. Типичные узкие места:

// Инструментация RPC вызовов
class InstrumentedProvider {
  private metrics: Map<string, number[]> = new Map();

  async call(method: string, params: any[]): Promise<any> {
    const start = performance.now();
    try {
      const result = await this.provider.send(method, params);
      this.record(method, performance.now() - start);
      return result;
    } catch (err) {
      this.recordError(method);
      throw err;
    }
  }

  getPercentiles(method: string) {
    const samples = (this.metrics.get(method) || []).sort((a, b) => a - b);
    return {
      p50: samples[Math.floor(samples.length * 0.5)],
      p95: samples[Math.floor(samples.length * 0.95)],
      p99: samples[Math.floor(samples.length * 0.99)],
      count: samples.length,
    };
  }
}

Что измерять: latency по каждому RPC методу, queue depth у indexer'а, lag между head блоком ноды и head в вашей БД, throughput WebSocket соединений.

Масштабирование RPC слоя

Пул нод с балансировкой

Единственная нода — single point of failure и bottleneck. Минимальная production конфигурация:

class NodePool {
  private nodes: RpcNode[];
  private currentIndex = 0;
  private healthStatus: Map<string, boolean> = new Map();

  async sendRequest(method: string, params: any[]): Promise<any> {
    // Round-robin с пропуском нездоровых нод
    for (let i = 0; i < this.nodes.length; i++) {
      const node = this.nodes[this.currentIndex % this.nodes.length];
      this.currentIndex++;

      if (!this.healthStatus.get(node.url)) continue;

      try {
        return await node.send(method, params);
      } catch (err) {
        // Помечаем ноду как нездоровую при ошибке
        this.healthStatus.set(node.url, false);
        setTimeout(() => this.healthStatus.set(node.url, true), 30_000);
      }
    }
    throw new Error('All nodes unhealthy');
  }
}

Для stateful операций (подписки, pending transactions) — sticky routing: один клиент всегда идёт к одной ноде.

Кеширование RPC ответов

Многие запросы идентичны и кешируемы:

const CACHEABLE_METHODS: Record<string, number> = {
  'eth_chainId': 86400,          // 24 часа — не меняется
  'eth_getCode': 3600,           // 1 час — код контракта стабилен
  'eth_getBlockByNumber': 60,    // 1 мин для не-latest
  'eth_getTransactionReceipt': 300,  // 5 мин — после финализации не меняется
};

class CachingRpcProxy {
  async send(method: string, params: any[]): Promise<any> {
    const ttl = CACHEABLE_METHODS[method];
    if (!ttl) return this.upstream.send(method, params);

    // Не кешируем 'latest' блок
    if (params.includes('latest') || params.includes('pending')) {
      return this.upstream.send(method, params);
    }

    const cacheKey = `rpc:${method}:${JSON.stringify(params)}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached);

    const result = await this.upstream.send(method, params);
    await this.redis.setex(cacheKey, ttl, JSON.stringify(result));
    return result;
  }
}

eth_getCode — особенно хорошо кешируется: код задеплоенного контракта никогда не меняется. Один запрос на весь срок жизни контракта.

Indexing: от polling к event-driven

Проблема polling

// Плохо: polling каждые 5 секунд
setInterval(async () => {
  const balance = await provider.getBalance(address);
  if (balance !== lastBalance) notifyUser(balance);
}, 5000);

При 10 000 адресов — 2 000 запросов в секунду только на мониторинг балансов. Нода захлебнётся.

Event-driven через logs

EVM события — правильный инструмент:

class EventIndexer {
  private lastProcessedBlock: number;

  async start() {
    // Подписка на новые блоки
    this.provider.on('block', async (blockNumber) => {
      await this.processRange(this.lastProcessedBlock + 1, blockNumber);
      this.lastProcessedBlock = blockNumber;
    });
  }

  private async processRange(from: number, to: number) {
    // Один запрос для всех интересующих событий во всех блоках диапазона
    const logs = await this.provider.getLogs({
      fromBlock: from,
      toBlock: to,
      topics: [
        // OR-логика: любой из этих event selectors
        [
          ethers.id('Transfer(address,address,uint256)'),
          ethers.id('Approval(address,address,uint256)'),
          ethers.id('Deposit(address,uint256)'),
        ]
      ],
    });

    // Группируем по типу события и обрабатываем батчем
    const grouped = Map.groupBy(logs, log => log.topics[0]);
    await Promise.all([
      this.processTransfers(grouped.get(ethers.id('Transfer(...)'))),
      this.processApprovals(grouped.get(ethers.id('Approval(...)'))),
    ]);
  }
}

The Graph для сложной индексации

Для сложных запросов (агрегация, исторические данные по пользователям) — The Graph субграф:

# schema.graphql
type User @entity {
  id: Bytes!
  totalDeposited: BigInt!
  transactions: [Transaction!]! @derivedFrom(field: "user")
}

type Transaction @entity {
  id: Bytes!
  user: User!
  amount: BigInt!
  blockNumber: BigInt!
  timestamp: BigInt!
}
// AssemblyScript handler в subgraph
export function handleDeposit(event: DepositEvent): void {
  let user = User.load(event.params.user);
  if (!user) {
    user = new User(event.params.user);
    user.totalDeposited = BigInt.zero();
  }
  user.totalDeposited = user.totalDeposited.plus(event.params.amount);
  user.save();

  let tx = new Transaction(event.transaction.hash);
  tx.user = event.params.user;
  tx.amount = event.params.amount;
  tx.blockNumber = event.block.number;
  tx.timestamp = event.block.timestamp;
  tx.save();
}

Self-hosted Graph Node на production: PostgreSQL 14+ с достаточным I/O, индексация крупного протокола занимает часы. Для начала — Graph Studio (managed), потом мигрируете при необходимости.

WebSocket: масштабирование подписок

WebSocket соединения stateful — нельзя просто добавить nginx upstream и балансировать round-robin. Нужен pub/sub слой:

// Redis Pub/Sub как backbone для WebSocket событий
class WebSocketGateway {
  private redisSub: Redis;  // subscriber connection
  private clients: Map<string, Set<WebSocket>> = new Map();

  constructor() {
    this.redisSub = new Redis();
    // Один subscriber на сервер, события раздаются всем клиентам
    this.redisSub.psubscribe('blockchain:*');
    this.redisSub.on('pmessage', this.broadcastToClients.bind(this));
  }

  subscribeClient(ws: WebSocket, topic: string) {
    if (!this.clients.has(topic)) this.clients.set(topic, new Set());
    this.clients.get(topic)!.add(ws);
  }

  private broadcastToClients(pattern: string, channel: string, message: string) {
    const clients = this.clients.get(channel);
    if (!clients) return;

    const dead: WebSocket[] = [];
    for (const ws of clients) {
      if (ws.readyState !== WebSocket.OPEN) { dead.push(ws); continue; }
      ws.send(message);
    }
    dead.forEach(ws => clients.delete(ws));
  }
}

Отдельный сервис публикует события в Redis при новом блоке или транзакции. WS-серверы подписаны на Redis и раздают своим клиентам. Горизонтальное масштабирование: добавляйте WS-серверы, все они читают один Redis канал.

Управление нагрузкой на ноды

Request coalescing

Множество одновременных запросов на один и тот же ресурс:

class RequestCoalescer {
  private pending: Map<string, Promise<any>> = new Map();

  async get(key: string, fetcher: () => Promise<any>): Promise<any> {
    // Если запрос уже летит — ждём его результат
    if (this.pending.has(key)) {
      return this.pending.get(key);
    }

    const promise = fetcher().finally(() => this.pending.delete(key));
    this.pending.set(key, promise);
    return promise;
  }
}

// Использование: 100 одновременных запросов на eth_getBalance одного адреса
// превращаются в 1 RPC запрос
const coalescer = new RequestCoalescer();
const balance = await coalescer.get(
  `balance:${address}:latest`,
  () => provider.getBalance(address)
);

Multicall для батчевых запросов

// Вместо 100 отдельных вызовов balanceOf — один multicall
import { Multicall3 } from '@ethcall/core';

const multicall = new Multicall3({ ethersProvider: provider });

const calls = addresses.map(address => ({
  target: tokenAddress,
  callData: erc20Interface.encodeFunctionData('balanceOf', [address]),
}));

const results = await multicall.aggregate(calls);
const balances = results.map((result, i) =>
  erc20Interface.decodeFunctionResult('balanceOf', result)[0]
);

Один HTTP запрос вместо 100. Для read-heavy приложений с большим числом адресов — обязательный паттерн.

Мониторинг и observability

Prometheus метрики для блокчейн-инфраструктуры:

import { Counter, Histogram, Gauge } from 'prom-client';

const rpcLatency = new Histogram({
  name: 'rpc_request_duration_ms',
  help: 'RPC request latency in milliseconds',
  labelNames: ['method', 'node', 'status'],
  buckets: [10, 50, 100, 250, 500, 1000, 2500, 5000],
});

const indexerLag = new Gauge({
  name: 'indexer_block_lag',
  help: 'Blocks behind chain head',
  labelNames: ['network'],
});

const wsConnections = new Gauge({
  name: 'websocket_connections_total',
  help: 'Active WebSocket connections',
});

Алерты в Grafana:

  • indexer_block_lag > 10 — indexer отстаёт, что-то тормозит
  • rpc_request_duration_ms{p95} > 2000 — нода деградирует
  • rate(rpc_errors_total[5m]) > 10 — нода возвращает ошибки
  • websocket_connections_total > 8000 — приближаемся к лимиту fd

Архитектурные решения: когда что применять

Проблема Решение Сложность
RPC нода — bottleneck Пул нод + балансировка Низкая
Повторяющиеся одинаковые запросы Request coalescing + Redis cache Низкая
100+ адресов — мониторинг балансов Multicall + event indexing Средняя
WS под нагрузкой дропает соединения Redis pub/sub backbone Средняя
Исторические запросы медленные Erigon/Reth archive + query optimization Средняя
Сложная аналитика on-chain данных The Graph subgraph Высокая
Мультисеть, тысячи событий/сек Kafka + event streaming Высокая

Масштабирование всегда начинается с измерений. Добавляйте сложность только там, где profiling показал реальное узкое место.