新着:HeliusがLight Protocolを買収
Solana Geyser Plugins
ブログ/基礎

Solana Geyser Plugins:光速のデータストリーミング

Developer Experience EngineerXの0xIchigoLinkedInの0xIchigoGitHubの0xIchigo
読了時間:13分

この記事で解説する内容

Geyser Pluginsは、アカウント、スロット、ブロック、トランザクションに関するデータを外部データストアへ送信するためのモジュール式コンポーネントです。これにより、開発者はバリデータからRPC(Remote Procedural Call)の負荷を取り除けます。Geyser Pluginsは、データのストリーミングや処理をニーズに合わせてカスタマイズしたい開発者に、柔軟なソリューションを提供します。

この記事では、Solana Geyser Pluginsの詳細を掘り下げます。まず、データレプリケーションと負荷管理の手法として提案されたものの、最終的にGeyser Pluginsを優先して廃止されたAccountsDBレプリカについて説明します。

次に、Geyser Pluginsとは何か、どのように機能するのか、Plugin Interfaceを通じてどのように構成されているのかを詳しく解説します。

続いて、一般的に利用できるGeyser Pluginsを紹介し、独自プラグインを作成する複雑なプロセスをご案内します。最後に、Heliusと、Solana上のデータストリーミングをどのように簡素化しているかを説明します。

AccountsDBレプリカ:廃止されたデータレプリケーションとRPC負荷へのアプローチ

Solanaは、重いRPC負荷とデータレプリケーションという課題に対処するため、複数の方法を検討しました。有望なアプローチの1つが、AccountsDBレプリカの利用でした。このレプリカは、アカウントスキャンリクエストをメインバリデータからAccountsDBレプリカへオフロードするように設計されていました。有望ではあったものの、システムは本質的に複雑で、メインバリデータとレプリカ間の同期を確保するために新たな一連のサービスが必要でした。最終的に、この提案はGeyser Plugin Systemを優先して廃止されました。Geyser Plugin Systemはバリデータクライアントがより簡単にサポートでき、開発者がアプリケーションを実装する際の柔軟性も高いソリューションです。

では、Solana Geyser Pluginsとは具体的に何でしょうか?

Solana Geyser Pluginsとは?

Solana Geyser PluginsはSolanaデータへの低レイテンシアクセスを提供し、バリデータにRPCコールを行う必要をなくすアプリケーションを実現できます。たとえば、バリデータが多数のgetProgramAccountsコールを立て続けに処理しなければならない場合、この高負荷なトラフィックによってネットワークへの追従が遅れる可能性があります。

Geyser Pluginsは、アカウント、ブロック、スロット、トランザクションに関する情報を、リレーショナルデータベース、NoSQLデータベース、Kafkaなどの外部データストアへ転送することで、この問題に対処します。

このデータ転送により、RPCサービスは外部ストアからデータを取得するユーザーに対して、キャッシュやインデックス作成など、より柔軟で目的に特化した最適化を提供できます。

Geyser Pluginsは、Solanaと外部データストレージソリューションをつなぐ橋渡し役を果たします。開発者はデータ管理タスクの大部分をバリデータからオフロードできるため、パフォーマンスが向上し、潜在的なボトルネックのリスクも低減します。

Geyser Pluginsにより、RPCトラフィックの量にかかわらず、バリデータをネットワークと同期した状態に保てます。

Geyser Plugin Interface

開発者は、Solana Geyser Plugin Interfaceを使用してGeyser Pluginsを構築できます。このインターフェースからは、アカウント、トランザクション、スロット、ブロックメタデータ、エントリにアクセスできます。solana-geyser-plugin-interfaceクレートで宣言され、GeyserPluginトレイトによって定義されています。

このトレイトでは、いずれも先頭にupdate_が付くメソッドが定義されており、新しいデータの作成時や既存データの更新時に呼び出されます。また、Geyser Pluginsはロードおよびアンロード処理時の動作も指定する必要があります。このトレイトは、求めるプラグイン動作に基づいて効率的なデータストリーミングを実現するために、Geyser Pluginが実装すべき必須メソッドを定めています。

ソースコード

コード
pub trait GeyserPlugin:Any +Send +Sync +Debug {
    // Required method
    fn name(&self) -> &'static str;

    // Provided methods
    fn on_load(&mut self, _config_file: &str) ->Result<()> { ... }
    fn on_unload(&mut self) { ... }
    fn update_account(
        &self,
        account:ReplicaAccountInfoVersions<'_>,
        slot: Slot,
        is_startup:bool
    ) ->Result<()> { ... }
    fn notify_end_of_startup(&self) ->Result<()> { ... }
    fn update_slot_status(
        &self,
        slot: Slot,
        parent:Option,
        status:SlotStatus
    ) ->Result<()> { ... }
    fn notify_transaction(
        &self,
        transaction:ReplicaTransactionInfoVersions<'_>,
        slot: Slot
    ) ->Result<()> { ... }
    fn notify_entry(&self, entry:ReplicaEntryInfoVersions<'_>) ->Result<()> { ... }
    fn notify_block_metadata(
        &self,
        blockinfo:ReplicaBlockInfoVersions<'_>
    ) ->Result<()> { ... }
    fn account_data_notifications_enabled(&self) ->bool { ... }
    fn transaction_notifications_enabled(&self) ->bool { ... }
    fn entry_notifications_enabled(&self) ->bool { ... }
}

トレイト宣言

GeyserPluginトレイトは、Solana Geyser Pluginエコシステム内のすべてのプラグインに対する基盤インターフェースとして機能します。Rust標準ライブラリのAny、Send、Sync、Debugというトレイト境界を持つ公開トレイトとして宣言されています。各トレイト境界は次のとおりです。

  • Anyは型リフレクションを可能にし、具象型へのダウンキャストを実現します
  • Sendは、このトレイトを実装する型の所有権をスレッド間で移動できることを示します
  • Syncは、このトレイトを実装する型への参照をスレッド間で共有できることを示します
  • Debugは、特にデバッグを目的として、型を出力用にフォーマットできるようにします

AnyとDebugは、ここではそれほど重要ではありません。

本当に重要なのは、プログラムをスレッドセーフにするために、GeyserPluginがSendとSyncを必要とする点です。

必須メソッド

コード
fn name(&self) -> &'static str;

nameメソッドは、GeyserPluginを実装するすべての型に必須です。このメソッドはGeyser Pluginの識別子として機能します。Geyser Pluginの名前を表す静的文字列スライスを返します。

このメソッドと、on_loadおよびon_unloadを除くすべてのメソッドが、&mut selfではなく&selfを使用するようになったのは、Solana 1.16アップデートでの変更点です。Geyser PluginをRead-Write Lockでラップし、その関数を呼び出すたびに書き込みロックを取得する必要がなくなったため、パフォーマンスが大幅に向上します。

提供されるメソッド

このトレイトにはデフォルト実装を含む複数のメソッドが用意されており、GeyserPluginの実装側でオーバーライドできます。

コード
fn on_load(&mut self, _config_file: &str) ->Result<()> { ... }

on_loadメソッドは、システムがプラグインをロードした際に呼び出されるコールバックで、プラグインに必要な初期化に使用されます。設定ファイルへのパスを表すstringへの参照を受け取ります。設定はJSON5形式で、このインターフェースを実装する共有ライブラリのフルパス名を示すlibpathフィールドを含める必要があります。

コード
fn on_unload(&mut self) { ... }

on_unloadメソッドは、システムがプラグインをアンロードする前に必要なクリーンアップを行うために呼び出されるコールバックです。

コード
fn update_account(
        &self,
        account:ReplicaAccountInfoVersions<'_>,
        slot: Slot,
        is_startup:bool
    ) ->Result<()> { ... }

update_accountメソッドは、processed確定レベルでアカウントが更新されたときに呼び出されます。これは1つのスロット内で複数回発生する場合があります。ここでは、正規チェーンにコミットされたアカウント更新を取得できるように、confirmedになったスロットを追跡することが不可欠です。

ReplicaAccountInfoVersions構造体には、ストリーミングされたアカウントのメタデータとデータが含まれます。

slotパラメータは、アカウントが更新されるスロットを指します。

is_startupがtrueの場合、バリデータの起動時にアカウントがスナップショットからロードされたことを示します。is_startupがfalseの場合、アカウントはトランザクション処理中に更新されます。

コード
fn notify_end_of_startup(&self) ->Result<()> { ... }

notify_end_of_startupメソッドは、起動フェーズの終了を通知するために呼び出されます。これは、バリデータがスナップショットからアカウントデータベースを復元し、それに応じてすべてのアカウントが更新された時点で発生します。

コード
fn update_slot_status(
        &self,
        slot: Slot,
        parent:Option,
        status:SlotStatus
    ) ->Result<()> { ... }

update_slot_statusメソッドは、スロットのステータスが更新されたときに呼び出されます。Slot、親スロット用のOption<u64>、SlotStatus enumインスタンスを受け取ります。

SlotStatusは、Solanaにおけるスロットの3つの状態を定義しています。

  • Processed - ノードが処理した最も高いスロットです。スロットはconfirmedでもfinalizedでもありませんが、バリデータが正規チェーンになる可能性が最も高いと見なすチェーンの一部です
  • Confirmed - 安全かつチェーンの一部であると見なすのに十分な票を得たスロットです。このスロットはSolanaのバリデータの圧倒的多数から支持されています
  • Rooted - ブロックチェーンの恒久的な一部となったスロットであり、チェーンのほかのすべてのバージョンやフォークは、このスロットを基盤として構築する必要があります。つまり、ネットワーク上のすべてのブランチがこのブロックから派生しています
コード
fn notify_transaction(
        &self,
        transaction:ReplicaTransactionInfoVersions<'_>,
        slot: Slot
    ) ->Result<()> { ... }

notify_transactionメソッドは、スロット内でトランザクションが処理されたときに呼び出され、トランザクションの詳細をプラグインへ通知します。

ReplicaTransactionInfoVersionsは、ReplicaTransactionInfoを処理するenumラッパーです。RepicaTransactionInfoの構造が変更された場合、新しいバージョン用に新たなenumエントリが追加されます。これにより、プラグイン実装では新しいenumエントリに対応し、変更を処理する必要があります。現在、enumは次の2つのバリアントをラップしています。

  1. V0_0_1(&'a ReplicaTransactionInfo<'a>)
  2. V0_0_2(&'a ReplicaTransactionInfoV2<'a>)
コード
pub struct ReplicaTransactionInfo<'a> {
    pub signature: &'a Signature,
    pub is_vote: bool,
    pub transaction: &'a SanitizedTransaction,
    pub transaction_status_meta: &'a TransactionStatusMeta,
}

pub struct ReplicaTransactionInfoV2<'a> {
    pub signature: &'a Signature,
    pub is_vote: bool,
    pub transaction: &'a SanitizedTransaction,
    pub transaction_status_meta: &'a TransactionStatusMeta,
    pub index: usize,
}

バリアント間の主な違いは、2つ目がブロック内のトランザクションのインデックスを保存する点です。

コード
fn notify_entry(&self, entry:ReplicaEntryInfoVersions<'_>) ->Result<()> { ... }

notify_entryは、新しいエントリをプラグインへ通知します。ReplicaEntryInfoの処理を将来にわたって維持できるようにするラッパーであるReplicaEntryInfoVersionsのインスタンスを受け取ります。現在はV0_0_1(&'a ReplicaEntryInfo<'a>**)**バリアントが含まれています。

このバリアントは、エントリのスロット、ブロック内のインデックス、前のエントリからのハッシュ数、エントリのSHA-256ハッシュ、エントリ内で実行されたトランザクション数に関する情報を含むstructです。

コード
fn notify_block_metadata(
        &self,
        blockinfo:ReplicaBlockInfoVersions<'_>
    ) ->Result<()> { ... }

notify_block_metadataメソッドは、ブロックのメタデータが更新されたときに呼び出されます。ブロック情報としてReplicaBlockInfoVersions enumインスタンスを受け取ります。このenumは、スロット、ハッシュ、報酬、ブロック時間、ブロック高などのブロック情報を含む、さまざまなReplicaBlockInfoバージョンのラッパーです。

コード
fn account_data_notifications_enabled(&self) ->bool { ... }
fn transaction_notifications_enabled(&self) ->bool { ... }
fn entry_notifications_enabled(&self) ->bool { ... }

これらのメソッドは、それぞれアカウントデータ、トランザクション、エントリに関する通知をプラグインが有効にするかどうかを示すブール値を返します。

コミットメントレベルに関する注意事項

Geyserは、アカウントデータとトランザクションが処理されると、ただちに更新を送信します。これはエンドツーエンドのインデックス作成速度に有効ですが、processedスロットがスキップされるリスクがあります。

スキップされたスロットとは、リーダーがオフラインだったか、そのスロットを含むフォークがより良い代替案を優先して破棄されたために、ブロックを生成しなかった過去のスロットを指します。ストリーミング先のデータストレージシステムがこの可能性を認識し、それに応じて更新を管理することが極めて重要です。

一般的なSolana Geyser Plugins

開発者が利用でき、特定のニーズに合わせてフォークすることも可能なSolana Geyser Pluginsは数多くあります。代表的なプラグインは次のとおりです。

  • PostgreSQL Plugin:PostgreSQLを使用したデータの管理とクエリ用
  • gRPC Service Streaming Plugin:Solanaアカウントの更新をgRPCサービスへストリーミングするためのもの
  • RabbitMQ Producer Plugin:RabbitMQによるメッセージキューイング用
  • Kafka Producer Plugin:Kafkaを使用したデータストリーミング用
  • Amazon SQS Plugin:AmazonのSimple Queue Serviceを活用したメッセージキューイング用
  • Google BigTable Plugin:Google BigTableを使用したデータの管理とクエリ用

これらのプラグインは、さまざまなユースケースに対応するよう調整できます。

たとえば、ClockworkはGeyser Pluginを活用してトランザクションをスケジュールし、自動化されたイベント駆動型のSolanaプログラムを構築しました。このプロジェクトは終了していますが、オープンソースコードは現在も有用なリソースとしてGitHubで閲覧できます。

ほかにも、Geyser Pluginsを使用してDeFiプラットフォーム上のアカウント残高を監視する、ネットワークの健全性指標を提供する、サプライチェーンのイベントをリアルタイムで監視するといったユースケースが考えられます。

独自のSolana Geyser Pluginを作成する

独自のプラグインを構築するためのリソースとコンポーネントをいくつかご紹介します。

Solana Geyser Plugin Scaffold

Solana Geyser Plugin Scaffoldは、Solana Geyser Plugin開発を始める際に最も使いやすいリソースです。このスキャフォールドは、Plugin Managerとプラグイン自体のやり取りをログに記録する最小限のテンプレートとして機能します。プラグインのワークフローやデバッグ手法に慣れるための優れた出発点です。

Plugin Manager

Plugin Managerは、すべてのGeyser Pluginsのライフサイクルと連携を管理する中核コンポーネントです。実行時にプラグインを動的にロードおよびアンロードできるため、柔軟性とモジュール性が向上します。

実行時に、Plugin Managerは設定ファイルのパスをプラグインへ渡します。これにより、プラグインのコードを変更することなく修正できるカスタム設定をGeyser Pluginsに追加できます。

プラグインをバリデータへ統合するには、--geyser-plugin-configパラメータを使用して動的ライブラリのパスを指定する必要があります。これにより、プラグインと関連設定の場所がバリデータに伝えられます。

設定ファイルは少なくともJSON形式で、Geyser Pluginの動的ライブラリ(Linuxでは**.so**)へのパスを含む必要があります。最小限の設定ファイルは次のようになります。

コード
{
    "libpath": "/.so"
}

Geyser Pluginをゼロから作成する

スキャフォールドを使用したり既存のプラグインを変更したりせず、未開拓の方法で独自のGeyser Pluginを作成する場合は、Geyser Plugin Interfaceを使用してプラグインをコーディングする必要があります。

プラグインがランタイムで動作するには、GeyserPluginトレイトを必ず実装する必要があります。さらに、動的ライブラリは、プラグインの実装を作成する「C」関数_create_pluginをエクスポートする必要があります。

その一例として、GeyserPluginトレイトを実装するWebhookプラグインを作成できます。

コード
#[no_mangle]
#[allow(improper_ctypes_definitions)]
/// # Safety
///
/// This function returns the WebhookPlugin pointer as trait GeyserPlugin.
pub unsafe extern "C" fn _create_plugin() -> *mut dyn GeyserPlugin {
    let plugin = WebhookPlugin::new();
    let plugin: Box = Box::new(plugin);
    Box::into_raw(plugin)
}

ここでは、C呼び出し規約extern "C"を使用するunsafeな公開関数を作成しており、Cやほかの言語との互換性を確保しています。関数fn _create*_*plugin() -> *mut dyn GeyserPlugin自体は、GeyserPluginトレイトであるdynGeyserPluginへの可変rawポインタを返します。関数本体はWebhookPluginの新しいインスタンスを作成し、このインスタンスをトレイトオブジェクトとしてボックス化した後、ボックス化したトレイトオブジェクトをrawポインタへ変換して、関数から返せるようにします。

したがって、独自のGeyser Pluginを作成する手順は次のとおりです。

  • Solana Geyser Pluginインターフェースを実装するプラグインを構築します
  • target/releaseまたはtarget/debugフォルダから動的ライブラリ(.soファイル)を取得します
  • geyser-config.jsonファイルを作成します。このファイルには、「libpath」フィールドにGeyser Plugin動的ライブラリへのパスを含める必要があります
  • --geyser-plugin-config geyser-config.jsonフラグを指定してバリデータを起動します

これらの手順は比較的簡単に見えますが、Solana Geyser Pluginを実際に運用・保守するプロセスは非常に困難な場合があります。

Helius Geyserストリーミング

Heliusは、Solana上で他に類を見ない開発者体験を提供することで高く評価されています。Solanaだけに注力してきたことで、Heliusは多岐にわたる課題を乗り越え、数多くの大規模な統合を支援してきた豊富な経験を有しています。Heliusは、開発者が直面し得るあらゆる問題に対応できる独自の立場にあります。

Heliusでは、Solanaエコシステム内の複数の高性能チーム向けにGeyser Pluginsを管理しています。冗長性と耐障害性を強化した専用Geyserクラスターを運用しているため、データの欠落やダウンタイムを心配する必要はありません。プログラムからのAPIアクセスにより、信頼性を気にすることなくGeyser Pluginsを動的に変更できます。Geyser Pluginsの管理ではデータの一貫性、信頼性、可用性を確保する責任が伴うため、多くの場合は困難な作業になります。Heliusに任せてみませんか?

Geyserストリーミングにご興味がある場合は、Heliusダッシュボードで専用ノードをご注文いただくか、Discordでお問い合わせいただき、今すぐ始めましょう。

まとめ

おめでとうございます!

この記事では、Solana Geyser Pluginsを詳しく検討しながら、データレプリケーションとRPC負荷管理の複雑さを解説しました。このシステムを理解するのは簡単ではありません。ドキュメントがほとんどない高度なアーキテクチャですが、Solana開発者に豊富なカスタマイズとパフォーマンス最適化の機会を提供します。

この記事で得た知識は、特にSolana上で高性能なアプリケーションを構築または管理しようとしている開発者やチームにとって非常に有用です。Geyser PluginsはSolanaエコシステムにスケーラブルで信頼性の高いソリューションを提供するため、その理解は不可欠です。

匿名の皆さん、ここまでお読みいただきありがとうございます!

その他のリソース

Heliusを購読

Solana開発の最新情報や新しい記事の公開通知を受け取れます