Come lavorare con Graph Node
Reading time: 18 min
Graph Node è il componente che indica i subgraph e rende i dati risultanti disponibili per l'interrogazione tramite API GraphQL. È quindi centrale per lo stack degli indexer, ed inoltre il corretto funzionamento di Graph Node è cruciale per il buon funzionamento di un indexer di successo.
Questo fornisce una panoramica contestuale di Graph Node e alcune delle opzioni più avanzate disponibili per gli Indexer. La documentazione e le istruzioni dettagliate si trovano nel .
è l'implementazione di riferimento per l'indicizzazione dei subgraph su Graph Network, la connessione ai client blockchain, l'indicizzazione dei subgraph e la disponibilità dei dati indicizzati per le query.
Graph Node (e l'intero stack di indicizzatori) può essere eseguito su metallo nudo o in un ambiente cloud. Questa flessibilità della componente centrale di indicizzazione è fondamentale per la robustezza del protocollo The Graph. Allo stesso modo, Graph Node può essere , oppure gli indexer possono usare una delle .
È l'archivio principale del Graph Node, in cui vengono memorizzati i dati dei subgraph, i metadati sui subgraph e i dati di rete che non dipendono dal subgraph, come la cache dei blocchi e la cache eth_call.
Per indicizzare una rete, Graph Node deve accedere a un cliente di rete tramite un'API JSON-RPC compatibile con EVM. Questo RPC può connettersi a un singolo cliente o può essere una configurazione più complessa che bilancia il carico su più clienti.
Mentre alcuni subgraph possono richiedere solo un nodo completo, alcuni possono avere caratteristiche di indicizzazione che richiedono funzionalità RPC aggiuntive. In particolare, i subgraph che effettuano eth_call
come parte dell'indicizzazione richiedono un nodo archivio che supporti e i subgraph con callHandlers
o blockHandlers
con filtro call
richiedono il supporto trace_filter
().
Firehose di rete - un Firehose è un servizio gRPC che fornisce un flusso ordinato, ma consapevole dei blocchi, sviluppato dagli sviluppatori di The Graph per supportare meglio l'indicizzazione performante su scala. Al momento non è un requisito per gli Indexer, ma questi ultimi sono incoraggiati a familiarizzare con la tecnologia, prima del supporto completo della rete. Per saperne di più sul Firehose .
I metadati di distribuzione del subgraph sono memorizzati sulla rete IPFS. The Graph Node accede principalmente al nodo IPFS durante la distribuzione del subgraph per recuperare il manifest del subgraph e tutti i file collegati. Gli indexer di rete non devono ospitare un proprio nodo IPFS. Un nodo IPFS per la rete è ospitato su .
Per consentire il monitoraggio e la creazione di report, Graph Node può opzionalmente registrare le metriche su un server di metriche Prometheus.
-
Rust
-
PostgreSQL
-
IPFS
-
Requisiti aggiuntivi per gli utenti di Ubuntu - Per eseguire un Graph Node su Ubuntu potrebbero essere necessari alcuni pacchetti aggiuntivi.
sudo apt-get install -y clang libpq-dev libssl-dev pkg-config
- Avviare un server di database PostgreSQL
initdb -D .postgrespg_ctl -D .postgres -l logfile startcreatedb graph-node
Clonare la repository di e costruire il sorgente eseguendo
cargo build
Ora che tutte le dipendenze sono state configurate, avviare il Graph Node:
cargo run -p graph-node --release -- \--postgres-url postgresql://[USERNAME]:[PASSWORD]@localhost:5432/graph-node \--ethereum-rpc [NETWORK_NAME]:[URL] \--ipfs https://ipfs.network.thegraph.com
Un esempio completo di configurazione Kubernetes si trova nel .
Quando è in funzione, Graph Node espone le seguenti porte:
Porta | Obiettivo | Routes | Argomento CLI | Variabile d'ambiente |
---|---|---|---|---|
8000 | GraphQL HTTP server (per le query di subgraph) | /subgraphs/id/... /subgraphs/name/.../... | --http-port | - |
8001 | GraphQL WS (per le sottoscrizioni ai subgraph) | /subgraphs/id/... /subgraphs/name/.../... | --ws-port | - |
8020 | JSON-RPC (per la gestione dei deployment) | / | --admin-port | - |
8030 | Subgraph indexing status API | /graphql | --index-node-port | - |
8040 | Metriche di Prometheus | /metrics | --metrics-port | - |
Importante: fare attenzione a esporre le porte pubblicamente - le porte di amministrazione devono essere tenute sotto chiave. Questo include l'endpoint JSON-RPC del Graph Node.
Nella sua forma più semplice, Graph Node può essere utilizzato con una singola istanza di Graph Node, un singolo database PostgreSQL, un nodo IPFS e i client di rete richiesti dai subgraph da indicizzare.
Questa configurazione può essere scalata orizzontalmente, aggiungendo più Graph Node e più database per supportare tali Graph Node. Gli utenti avanzati potrebbero voler sfruttare alcune delle capacità di scalatura orizzontale di Graph Node, nonché alcune delle opzioni di configurazione più avanzate, tramite il file config.toml
e le variabili d'ambiente di Graph Node.
Un file di configurazione può essere usato per impostare configurazioni più complesse di quelle esposte nella CLI. Il percorso del file viene passato con l'opzione --config della riga di comando.
Quando si usa un file di configurazione, non è possibile usare le opzioni --postgres-url, --postgres-secondary-hosts e --postgres-host-weights.
È possibile fornire un file config.toml
minimo; il file seguente è equivalente all'uso dell'opzione della riga di comando --postgres-url:
[store][store.primary]connection="<.. postgres-url argument ..>"[deployment][[deployment.rule]]indexers = [ "<.. list of all indexing nodes ..>" ]
La documentazione completa di config.toml
si trova nei documenti di .
Graph Node indexing can scale horizontally, running multiple instances of Graph Node to split indexing and querying across different nodes. This can be done simply by running Graph Nodes configured with a different node_id
on startup (e.g. in the Docker Compose file), which can then be used in the config.toml
file to specify , , and splitting subgraphs across nodes with .
Si noti che più Graph Node possono essere configurati per utilizzare lo stesso database, che può essere scalato orizzontalmente tramite sharding.
Dati più Graph Node, è necessario gestire la distribuzione di nuovi subgraph in modo che lo stesso subgraph non venga indicizzato da due nodi diversi, il che porterebbe a collisioni. Questo può essere fatto usando le regole di distribuzione, che possono anche specificare in quale shard
devono essere memorizzati i dati di un subgraph, se si usa lo sharding del database. Deployment rules can match on the subgraph name and the network that the deployment is indexing in order to make a decision.
Esempio di configurazione della regola di distribuzione:
[deployment][[deployment.rule]]match = { name = "(vip|importante)/.*" }shard = "vip"indexers = [ "index_node_vip_0", "index_node_vip_1" ][[deployment.rule]]match = { network = "kovan" }# Nessun shard, quindi usiamo lo shard predefinito chiamato "primario".indicizzatori = [ "index_node_kovan_0" ][[deployment.rule]]match = { network = [ "xdai", "poa-core" ] }indexers = [ "index_node_other_0" ][[deployment.rule]]# Non c'è nessun "match", quindi qualsiasi sottografo corrispondeshard = [ "sharda", "shardb" ]indicizzatori = ["index_node_community_0","index_node_community_1","index_node_community_2","index_node_community_3","index_node_community_4","indice_nodo_comunità_5"]
Per saperne di più sulle regole di distribuzione .
I nodi possono essere configurati per essere esplicitamente nodi di query includendo quanto segue nel file di configurazione:
[general]query = "<regular expression>"
Ogni nodo il cui --node-id corrisponde all'espressione regolare sarà impostato per rispondere solo alle query.
Per la maggior parte dei casi d'uso, un singolo database Postgres è sufficiente per supportare un'istanza del graph-node. Quando un'istanza del graph-node supera un singolo database Postgres, è possibile suddividere l'archiviazione dei dati del graph-node su più database Postgres. Tutti i database insieme formano lo store dell'istanza del graph-node. Ogni singolo database è chiamato shard.
Gli shard possono essere utilizzati per suddividere le distribuzioni di subgraph su più database e per utilizzare le repliche per distribuire il carico delle query tra i database. Questo include la configurazione del numero di connessioni al database disponibili che ogni graph-node
deve mantenere nel suo pool di connessioni per ogni database, cosa che diventa sempre più importante quando si indicizzano più subgraph.
Lo sharding diventa utile quando il database esistente non riesce a reggere il carico che Graph Node gli impone e quando non è più possibile aumentare le dimensioni del database.
In genere è meglio creare un singolo database il più grande possibile, prima di iniziare con gli shard. Un'eccezione è rappresentata dai casi in cui il traffico di query è suddiviso in modo molto disomogeneo tra i subgraph; in queste situazioni può essere di grande aiuto tenere i subgraph ad alto volume in uno shard e tutto il resto in un altro, perché questa configurazione rende più probabile che i dati per i subgraph ad alto volume rimangano nella cache interna del database e non vengano sostituiti da dati non necessari per i subgraph a basso volume.
Per quanto riguarda la configurazione delle connessioni, iniziare con max_connections in postgresql.conf impostato a 400 (o forse anche a 200) e osservare le metriche di Prometheus store_connection_wait_time_ms e store_connection_checkout_count. Tempi di attesa notevoli (qualsiasi cosa superiore a 5 ms) indicano che le connessioni disponibili sono troppo poche; tempi di attesa elevati possono anche essere causati da un database molto occupato (come un elevato carico della CPU). Tuttavia, se il database sembra altrimenti stabile, tempi di attesa elevati indicano la necessità di aumentare il numero di connessioni. Nella configurazione, il numero di connessioni che ogni istanza del graph-node può utilizzare è un limite massimo e Graph Node non manterrà aperte le connessioni se non ne ha bisogno.
Per saperne di più sulla configurazione dell'archivio .
Se sono stati configurati più nodi, sarà necessario specificare un nodo responsabile dell'ingestione dei nuovi blocchi, in modo che tutti i nodi indice configurati non eseguano il polling della testa della chain. Questo viene fatto come parte dello spazio dei nomi chains
, specificando il node_id
da usare per l'ingestione dei blocchi:
[chains]ingestor = "block_ingestor_node"
Il Graph Protocol sta aumentando il numero di reti supportate per l'indicizzazione delle ricompense ed esistono molti subgraph che indicizzano reti non supportate che un indexer vorrebbe elaborare. Il file config.toml
consente una configurazione espressiva e flessibile di:
- Reti multiple
- Fornitori multipli per rete (questo può consentire di suddividere il carico tra i fornitori e di configurare nodi completi e nodi di archivio, con Graph Node che preferisce i fornitori più economici se un determinato carico di lavoro lo consente).
- Ulteriori dettagli sul provider, come le caratteristiche, l'autenticazione e il tipo di provider (per il supporto sperimentale di Firehose)
La sezione [chains]
controlla i fornitori di ethereum a cui graph-node si connette e dove vengono memorizzati i blocchi e altri metadati per ogni chain. L'esempio seguente configura due chain, mainnet e kovan, dove i blocchi per mainnet sono memorizzati nello shard vip e quelli per kovan nello shard primario. La chain mainnet può utilizzare due diversi provider, mentre kovan ha un solo provider.
[catene]ingestor = "block_ingestor_node"[chains.mainnet]shard = "vip"provider = [{ label = "mainnet1", url = "http://..", features = [], headers = { Authorization = "Bearer foo" } },{ label = "mainnet2", url = "http://..", features = [ "archivio", "tracce" ] } }][catene.kovan]shard = "primary"provider = [ { label = "kovan", url = "http://..", features = [] } ]
Per saperne di più sulla configurazione dei provider .
Graph Node supporta una serie di variabili d'ambiente che possono abilitare funzioni o modificare il comportamento di Graph Node. Queste sono documentate .
Gli utenti che gestiscono una configurazione di indicizzazione scalare con una configurazione avanzata possono trarre vantaggio dalla gestione dei Graph Node con Kubernetes.
- Il repository dell'indexer ha un
- è un kit di strumenti per l'esecuzione di un Graph Protocol Indexer su Kubernetes, gestito da GraphOps. Fornisce una serie di grafici Helm e una CLI per gestire una distribuzione di Graph Node.
Dato un Graph Node (o più Graph Nodes!) in funzione, la sfida consiste nel gestire i subgraph distribuiti tra i nodi. Graph Node offre una serie di strumenti che aiutano a gestire i subgraph.
I registri di Graph Node possono fornire informazioni utili per il debug e l'ottimizzazione di Graph Node e di specifici subgraph. Graph Node supporta diversi livelli di log tramite la variabile d'ambiente GRAPH_LOG
, con i seguenti livelli: error, warn, info, debug o trace.
Inoltre, impostando GRAPH_LOG_QUERY_TIMING
su gql
si ottengono maggiori dettagli sull'esecuzione delle query GraphQL (anche se questo genera un grande volume di log).
Graph Node fornisce le metriche tramite l'endpoint Prometheus sulla porta 8040. È possibile utilizzare Grafana per visualizzare queste metriche.
Il repository dell'indexer fornisce un .
graphman
è uno strumento di manutenzione per Graph Node, che aiuta nella diagnosi e nella risoluzione di diversi compiti quotidiani ed eccezionali.
Il comando graphman è incluso nei contenitori ufficiali e si può eseguire con docker exec nel contenitore graph-node. Richiede un file config.toml
.
La documentazione completa dei comandi di graphman
è disponibile nel repository di Graph Node. Vedere [/docs/graphman.md] () nel Graph Node /docs
Disponibile sulla porta 8030/graphql per impostazione predefinita, l'API dello stato di indicizzazione espone una serie di metodi per verificare lo stato di indicizzazione di diversi subgraph, controllare le prove di indicizzazione, ispezionare le caratteristiche dei subgraph e altro ancora.
Lo schema completo è disponibile .
Il processo di indicizzazione si articola in tre parti distinte:
- Recuperare gli eventi di interesse dal provider
- Elaborare gli eventi in ordine con i gestori appropriati (questo può comportare la chiamata alla chain per lo stato e il recupero dei dati dall'archivio)
- Scrivere i dati risultanti nell'archivio
Questi stadi sono collegati tra loro (cioè possono essere eseguiti in parallelo), ma dipendono l'uno dall'altro. Se i subgraph sono lenti da indicizzare, la causa dipende dal subgraph specifico.
Cause comuni di lentezza dell'indicizzazione:
- Tempo impiegato per trovare eventi rilevanti dalla chain (i gestori di chiamate in particolare possono essere lenti, dato che si affidano a
trace_filter
) - Effettuare un gran numero di
eth_calls
come parte dei gestori - Una grande quantità di interazioni con l'archivio durante l'esecuzione
- Una grande quantità di dati da salvare nell'archivio
- Un numero elevato di eventi da elaborare
- Tempo di connessione al database lento, per i nodi affollati
- Il fornitore stesso è in ritardo rispetto alla testa della chain
- Lentezza nell'acquisizione di nuove ricevute dal fornitore alla testa della chain
Le metriche di indicizzazione dei subgraph possono aiutare a diagnosticare la causa principale della lentezza dell'indicizzazione. In alcuni casi, il problema risiede nel subgraph stesso, ma in altri, il miglioramento dei provider di rete, la riduzione della contesa del database e altri miglioramenti della configurazione possono migliorare notevolmente le prestazioni dell'indicizzazione.
Durante l'indicizzazione, i subgraph possono fallire se incontrano dati inaspettati, se qualche componente non funziona come previsto o se c'è un bug nei gestori di eventi o nella configurazione. Esistono due tipi generali di errore:
- Guasti deterministici: si tratta di guasti che non possono essere risolti con tentativi di risposta
- Fallimenti non deterministici: potrebbero essere dovuti a problemi con il provider o a qualche errore imprevisto di Graph Node. Quando si verifica un errore non deterministico, Graph Node riprova i gestori che non hanno funzionato, riducendo il tempo a disposizione.
In alcuni casi, un errore può essere risolto dall'indexer (ad esempio, se l'errore è dovuto alla mancanza del tipo di provider giusto, l'aggiunta del provider richiesto consentirà di continuare l'indicizzazione). In altri casi, invece, è necessario modificare il codice del subgraph.
I fallimenti deterministici sono considerati "definitivi", con la generazione di una Prova di Indicizzazione per il blocco fallito, mentre i fallimenti non deterministici non lo sono, in quanto il subgraph può riuscire a "non fallire" e continuare l'indicizzazione. In alcuni casi, l'etichetta non deterministica non è corretta e il subgraph non supererà mai l'errore; tali fallimenti devono essere segnalati come problemi sul repository di Graph Node.
Graph Node memorizza nella cache alcuni dati nell'archiivio, per risparmiare il refetching dal provider. I blocchi sono memorizzati nella cache, così come i risultati delle chiamate eth_call
(queste ultime sono memorizzate nella cache a partire da un blocco specifico). Questa cache può aumentare notevolmente la velocità di indicizzazione durante la "risincronizzazione" di un subgraph leggermente modificato.
Tuttavia, in alcuni casi, se un nodo Ethereum ha fornito dati non corretti per un certo periodo, questi possono entrare nella cache, causando dati errati o subgraph falliti. In questo caso gli indexer possono usare graphman
per cancellare la cache avvelenata e quindi riavvolgere i subgraph interessati, che recupereranno quindi dati freschi dal provider (auspicabilmente) sano.
Se si sospetta un'incongruenza nella cache a blocchi, come ad esempio un evento di ricezione tx mancante:
elenco chain di graphman
per trovare il nome della chain.chain graphman check-blocks <CHAIN> by-number <NUMBER>
controlla se il blocco in cache corrisponde al fornitore e, in caso contrario, lo cancella dalla cache.- Se c'è una differenza, può essere più sicuro troncare l'intera cache con
graphman chain truncate <CHAIN>
. - Se il blocco corrisponde al provider, è possibile eseguire il debug del problema direttamente sul provider.
Una volta che un subgraph è stato indicizzato, gli indexer possono aspettarsi di servire le query attraverso l'endpoint di query dedicato al subgraph. Se l'indexer spera di servire un volume significativo di query, è consigliabile un nodo di query dedicato; in caso di volumi di query molto elevati, gli indexer potrebbero voler configurare shard di replica in modo che le query non abbiano un impatto sul processo di indicizzazione.
Tuttavia, anche con un nodo di query dedicato e le repliche, alcune query possono richiedere molto tempo per essere eseguite e, in alcuni casi, aumentare l'utilizzo della memoria e avere un impatto negativo sul tempo di query per gli altri utenti.
Non esiste una "pallottola d'argento", ma una serie di strumenti per prevenire, diagnosticare e gestire le query lente.
Graph Node memorizza nella cache le query GraphQL per impostazione predefinita, riducendo in modo significativo il carico del database. Questo può essere ulteriormente configurato con impostazioni GRAPH_QUERY_CACHE_BLOCKS
e GRAPH_QUERY_CACHE_MAX_MEM
- per saperne di più .
Le query problematiche emergono spesso in due modi. In alcuni casi, sono gli stessi utenti a segnalare la lentezza di una determinata query. In questo caso, la sfida consiste nel diagnosticare la ragione della lentezza, sia che si tratti di un problema generale, sia che si tratti di un problema specifico di quel subgraph o di quella query. E poi, naturalmente, risolverlo, se possibile.
In altri casi, il fattore scatenante potrebbe essere l'elevato utilizzo della memoria su un nodo di query, nel qual caso la sfida consiste nell'identificare la query che causa il problema.
Gli indexer possono usare per elaborare e riassumere i log delle query di Graph Node. Si può anche attivare GRAPH_LOG_QUERY_TIMING
per aiutare a identificare e debuggare le query lente.
Con una query lenta, gli indexer hanno alcune opzioni. Naturalmente possono modificare il loro modello di costo, aumentando in modo significativo il costo di invio della query problematica. Questo può portare a una riduzione della frequenza della query. Tuttavia, questo spesso non risolve la causa principale del problema.
Le tabelle di database che memorizzano le entità sembrano essere generalmente di due tipi: "tipo transazioni", in cui le entità, una volta create, non vengono mai aggiornate, cioè memorizzano qualcosa di simile a un elenco di transazioni finanziarie, e "tipo account", in cui le entità vengono aggiornate molto spesso, cioè memorizzano qualcosa di simile a conti finanziari che vengono modificati ogni volta che viene registrata una transazione. Le tabelle di tipo account sono caratterizzate dal fatto di contenere un gran numero di versioni di entità, ma relativamente poche entità distinte. Spesso, in queste tabelle il numero di entità distinte è pari all'1% del numero totale di righe (versioni di entità)
Per le tabelle di tipo account, graph-node
può generare query che sfruttano i dettagli del modo in cui Postgres finisce per memorizzare i dati con un tasso di modifica così elevato, ovvero che tutte le versioni per i blocchi recenti si trovano in una piccola sottosezione dello spazio di archiviazione complessivo di una tabella.
Il comando graphman stats show <sgdNNN
> mostra, per ogni tipo di entità/tabella in una distribuzione, quante entità distinte e quante versioni di entità contiene ogni tabella. Questi dati si basano su stime interne a Postgres e sono quindi necessariamente imprecisi e possono essere sbagliati di un ordine di grandezza. Un -1
nella colonna entità
significa che Postgres ritiene che tutte le righe contengano un'entità distinta.
In generale, le tabelle in cui il numero di entità distinte è inferiore all'1% del numero totale di righe/versioni di entità sono buone candidate per l'ottimizzazione di tipo account. Quando l'output di graphman stats show
indica che una tabella potrebbe beneficiare di questa ottimizzazione, l'esecuzione di graphman stats show <sgdNNN> <table>
eseguirà un conteggio completo della tabella - che può essere lento, ma fornisce una misura precisa del rapporto tra entità distinte e versioni complessive delle entità.
Una volta che una tabella è stata determinata come tipo account, l'esecuzione di graphman stats tipo account <sgdNN>.<table>
attiverà l'ottimizzazione tipo account per le query contro quella tabella. L'ottimizzazione può essere nuovamente disattivata con graphman stats tipo account --clear <sgdNNN>.<table>
. Ci vogliono fino a 5 minuti prima che i nodi delle query notino che l'ottimizzazione è stata attivata o disattivata. Dopo aver attivato l'ottimizzazione, è necessario verificare che la modifica non renda effettivamente più lente le query per quella tabella. Se si è configurato Grafana per monitorare Postgres, le query lente verrebbero visualizzate in pg_stat_activity
in gran numero, impiegando diversi secondi. In questo caso, l'ottimizzazione deve essere nuovamente disattivata.
Per i subgraph simili a Uniswap, le tabelle pair
e token
sono le prime candidate per questa ottimizzazione e possono avere un effetto drammatico sul carico del database.
Si tratta di una nuova funzionalità, che sarà disponibile in Graph Node 0.29.x
A un certo punto un indexer potrebbe voler rimuovere un determinato subgraph. Questo può essere fatto facilmente tramite graphman drop
, che cancella una distribuzione e tutti i suoi dati indicizzati. La distribuzione può essere specificata come un nome di subgraph, un hash IPFS Qm..
, o lo spazio dei nomi del database sgdNNN
. È disponibile ulteriore documentazione .