Разработка Node-as-a-Service платформы
Запускать блокчейн-ноду руками — несложно. Запускать их сотнями, с гарантией uptime, версионированием, изоляцией клиентов, биллингом и API-прокси — это полноценный инфраструктурный продукт. Именно такую платформу строят те, кто хочет конкурировать с Infura, Alchemy, QuickNode или предлагать managed infrastructure для enterprise-клиентов в конкретном регионе или для конкретной экосистемы.
Прежде чем идти в разработку, стоит честно ответить на вопрос: вы строите NaaS для публичных блокчейнов (Ethereum, Solana, BNB) или для частных/permissioned сетей (Hyperledger Besu, Quorum)? Архитектурно это разные системы с разными проблемами.
Уровни архитектуры NaaS платформы
Уровень оркестрации нод
Kubernetes — стандарт для управления lifecycle нод. Но стандартный K8s deployment не подходит для блокчейн-нод напрямую: ноды имеют огромные stateful данные, требуют специфичных network policies, и перезапуск pod = повторная синхронизация с нуля (что занимает дни).
StatefulSet с PVC:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ethereum-geth
spec:
serviceName: "geth"
replicas: 1
selector:
matchLabels:
app: ethereum-geth
template:
spec:
containers:
- name: geth
image: ethereum/client-go:v1.13.14
args:
- "--datadir=/data"
- "--http"
- "--http.addr=0.0.0.0"
- "--http.vhosts=*"
- "--http.api=eth,net,web3,txpool"
- "--ws"
- "--ws.addr=0.0.0.0"
- "--maxpeers=50"
- "--cache=4096"
ports:
- containerPort: 8545 # HTTP RPC
- containerPort: 8546 # WebSocket
- containerPort: 30303 # P2P
protocol: TCP
- containerPort: 30303
protocol: UDP
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
memory: "16Gi"
cpu: "4"
limits:
memory: "32Gi"
cpu: "8"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "fast-nvme"
resources:
requests:
storage: 3Ti # Ethereum archive node
Проблема P2P портов в K8s: блокчейн-ноды требуют фиксированных портов для peer discovery (30303/TCP+UDP для Ethereum). NodePort или LoadBalancer с фиксированным портом на каждую ноду — единственный рабочий подход. HostNetwork — альтернатива, но теряется изоляция.
Bootstrapping: проблема "дня первого"
Синхронизация Ethereum mainnet с нуля (snap sync): 12–24 часа. Archive node: 2–5 недель. Это неприемлемо для платформы, где клиент платит с первой минуты.
Решения:
Snapshot distribution — инициализация ноды из актуального snapshot базы данных. Нужно хранить актуальные snapshots (~500GB–3TB в зависимости от типа) и предоставлять HTTP/S3-доступ для bootstrap. Стратегия: snapshot каждые 7 дней, инкрементальные дифы ежедневно.
#!/bin/bash
# Bootstrap нода из snapshot
NODE_ID=$1
CHAIN=$2
SNAPSHOT_BASE="s3://your-snapshots/${CHAIN}/latest"
# Скачиваем snapshot
aws s3 sync ${SNAPSHOT_BASE} /data/${NODE_ID}/chaindata \
--no-sign-request \
--region eu-west-1
# Проверяем целостность
sha256sum -c /data/${NODE_ID}/chaindata/CHECKSUM
# Запускаем ноду
kubectl rollout restart statefulset/${CHAIN}-node-${NODE_ID}
Firehose / Erigon snapshots — для некоторых цепей сообщество поддерживает публичные snapshots (Erigon uploads snapshots to BitTorrent/IPFS).
API Gateway и маршрутизация
Клиент получает один endpoint. За ним — load balancer, health checks, rate limiting, API key management.
Client → API Gateway (Kong/custom) → Node Pool → Blockchain Node
↓
[Auth, RateLimit, Billing, Logging]
Кастомный RPC прокси необходим потому что:
- Нужно фильтровать опасные методы (
debug_traceTransaction— дорогостоящий, только для premium) - Нужна маршрутизация по типу запроса (archive requests → archive node, latest block → sync node)
- Нужен response caching для частых запросов (
eth_chainId,eth_blockNumber)
// Пример RPC прокси с routing logic
package proxy
type RPCRouter struct {
archivePool NodePool
fullNodePool NodePool
cacheClient *redis.Client
}
var archiveMethods = map[string]bool{
"eth_getBalance": true, // с block parameter != "latest"
"eth_call": true,
"eth_getStorageAt": true,
"trace_call": true,
"trace_replayTransaction": true,
}
func (r *RPCRouter) Route(req *RPCRequest) NodePool {
if archiveMethods[req.Method] {
if req.RequiresHistoricalBlock() {
return r.archivePool
}
}
return r.fullNodePool
}
func (r *RPCRouter) Handle(w http.ResponseWriter, req *RPCRequest, apiKey string) {
// Check cache
cacheKey := req.CacheKey()
if cached, err := r.cacheClient.Get(ctx, cacheKey).Bytes(); err == nil {
w.Write(cached)
return
}
pool := r.Route(req)
node := pool.GetHealthyNode()
resp := node.Forward(req)
if req.IsCacheable() {
r.cacheClient.Set(ctx, cacheKey, resp, req.CacheTTL())
}
// Billing: записываем использование
r.billing.RecordRequest(apiKey, req.Method, resp.ComputeUnits())
w.Write(resp)
}
Health checking и failover
Блокчейн-нода может быть технически "живой" (отвечает на ping), но фактически бесполезной (отстала от сети на 100 блоков, или sync mode = syncing).
type NodeHealthChecker struct {
client *ethclient.Client
}
func (h *NodeHealthChecker) IsHealthy(ctx context.Context) (bool, error) {
// Проверяем, не в режиме синхронизации
syncing, err := h.client.SyncProgress(ctx)
if err != nil {
return false, err
}
if syncing != nil {
return false, fmt.Errorf("node is syncing: %d/%d",
syncing.CurrentBlock, syncing.HighestBlock)
}
// Проверяем свежесть блока
header, err := h.client.HeaderByNumber(ctx, nil)
if err != nil {
return false, err
}
blockAge := time.Since(time.Unix(int64(header.Time), 0))
if blockAge > 2*time.Minute {
return false, fmt.Errorf("block too old: %v", blockAge)
}
return true, nil
}
Health checks нужно запускать каждые 10–30 секунд. Нода исключается из пула при двух последовательных неудачах, возвращается после трёх успешных.
Мультитенантность и изоляция
Разделение ресурсов
Три модели:
Shared nodes — несколько клиентов используют одну ноду. Дёшево, но нет гарантий производительности. Подходит для free tier и небольших проектов.
Dedicated nodes — одна нода на клиента. Гарантированные ресурсы, изоляция. Для enterprise.
Node clusters — несколько реплик за load balancer для одного клиента. Для high-availability требований.
-- Схема биллинга
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
customer_id UUID NOT NULL,
key_hash BYTEA NOT NULL, -- никогда не храним ключ в открытом виде
tier VARCHAR(20) NOT NULL, -- free, starter, pro, enterprise
rate_limit_rps INTEGER NOT NULL,
monthly_cu_limit BIGINT, -- compute units
node_type VARCHAR(20) NOT NULL, -- shared, dedicated
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE usage_records (
id BIGSERIAL PRIMARY KEY,
api_key_id UUID NOT NULL REFERENCES api_keys(id),
method VARCHAR(100) NOT NULL,
chain_id INTEGER NOT NULL,
compute_units INTEGER NOT NULL,
response_time_ms INTEGER,
recorded_at TIMESTAMPTZ DEFAULT NOW()
);
-- Индекс для биллинга по периоду
CREATE INDEX idx_usage_billing ON usage_records (api_key_id, recorded_at);
Compute Units (CU) — стандартная единица биллинга в NaaS. Каждый RPC метод имеет вес:
-
eth_blockNumber→ 10 CU -
eth_getTransactionReceipt→ 15 CU -
eth_call→ 26 CU -
trace_replayTransaction→ 75 CU -
eth_getLogs→ 75 CU + 1 CU per log returned
Rate limiting
Redis-based sliding window лучше token bucket для RPC нагрузок:
func (rl *RateLimiter) Allow(ctx context.Context, apiKey string, rps int) (bool, error) {
now := time.Now().UnixMilli()
window := int64(1000) // 1 секунда в миллисекундах
pipe := rl.redis.Pipeline()
pipe.ZRemRangeByScore(ctx, apiKey, "0",
strconv.FormatInt(now-window, 10))
pipe.ZCard(ctx, apiKey)
pipe.ZAdd(ctx, apiKey, redis.Z{Score: float64(now), Member: now})
pipe.Expire(ctx, apiKey, 2*time.Second)
results, err := pipe.Exec(ctx)
count := results[1].(*redis.IntCmd).Val()
return count < int64(rps), nil
}
Мониторинг и алертинг
Метрики которые важны
# Prometheus metrics для NaaS
- node_sync_lag_blocks{chain, node_id} # отставание от head
- node_peer_count{chain, node_id} # число пиров
- rpc_request_duration_seconds{method, status} # p50, p95, p99
- rpc_requests_total{method, chain, tier} # для биллинга
- node_restart_total{chain, node_id, reason} # частота рестартов
- compute_units_consumed{api_key, chain} # биллинговые данные
Alert rules для on-call:
| Метрика | Порог | Severity |
|---|---|---|
| sync_lag_blocks | > 10 блоков | Warning |
| sync_lag_blocks | > 50 блоков | Critical |
| peer_count | < 5 | Warning |
| rpc_error_rate | > 5% за 5 мин | Warning |
| node_restart_total | > 3 за час | Critical |
Поддерживаемые клиенты и их специфика
| Цепь | Клиент | Размер данных | Особенности |
|---|---|---|---|
| Ethereum (full) | Geth / Reth | ~1.2 TB | snap sync доступен |
| Ethereum (archive) | Erigon | ~2.5 TB | trace API отличается от Geth |
| Solana | Agave (Solana Labs) | ~50 TB (full ledger) | geyser plugin для стриминга |
| BNB Chain | BSC Geth fork | ~800 GB | faster block time (3s) |
| Polygon | Bor + Heimdall | ~600 GB | два процесса на одну ноду |
| Arbitrum | Nitro | ~1 TB | sequencer feed, не P2P |
| Base | op-geth | ~800 GB | OP Stack, op-node рядом |
Reth — новый Ethereum-клиент на Rust от Paradigm. Значительно быстрее Geth при синхронизации (~2× меньше времени), лучше resource utilization. Для новых деплоев — первый выбор для Ethereum full nodes.
Этапы разработки
Фаза 1 — Core infrastructure (4–6 нед): K8s setup, StatefulSet templates для 2–3 цепей, snapshot bootstrap pipeline, базовый health checker.
Фаза 2 — API Gateway (3–4 нед): RPC прокси, API key management, rate limiting, compute units counting.
Фаза 3 — Multi-tenancy & billing (3–4 нед): tenant isolation, usage tracking, billing integration (Stripe), usage dashboard.
Фаза 4 — Observability (2–3 нед): Prometheus + Grafana, alerting, log aggregation (Loki), on-call runbooks.
Фаза 5 — Self-service portal (4–6 нед): веб-интерфейс для создания нод, просмотра метрик, управления API ключами.
Итого: 16–23 недели до production-ready платформы. Добавление каждой новой цепи после запуска — 1–2 недели (шаблон + snapshot pipeline + тестирование).







