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

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

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

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

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1308
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1222
  • 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 — особливо добре кешується: код задеплоенного контракту ніколи не міняється. Один запрос на весь срок життя контракту.

Індексація: від 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 показав реальне вузьке місце.