Medium の DFINITY 公式の記事 Optimizing the Internet Computer Memory System’s Performance(2021/10/22) の日本語訳です。
Internet Computer におけるキャニスタースマートコントラクトの成長により、クエリーコールとアップデートコールの呼び出し性能が大幅に向上しました。
By Ulan Degenbaev, Principal Software Engineer | DFINITY
Internet Computer ブロックチェーン は、完全にオンチェーンで動作する高度なキャニスタースマートコントラクトを用いて、世界のシステムとサービスを生み出すことを可能にします。キャニスターは、驚異的なネットワーク効果をもたらす自律的なサービスコンポーザビリティ(複数のサービスを組み合わせることができる)を可能にし、実質的にあらゆる企業で根本から見直すことを可能にします。5月のネットワーク Genesis 以降、DFINITY Canister SDK を利用して、Internet Computer 上で数千のキャニスタースマートコントラクトが作成され、その多くが Web3 dapps として完結しています。
Internet Comuter ブロックチェーンにおけるキャニスターとユーザーの急増により、興味深い工学的課題が提示されます。最近、メモリを多用するキャニスターが増加したことで、高負荷時にメモリシステムが性能ボトルネックになっていることが実証されました。このブログ記事では、NNS proposal 20461 を基に性能の最適化について説明し、スケーリングとWebAssembly(Wasm)メモリについて詳しく説明します。
結果
この最適化は、9月14日に NNS proposal 20461 を採択した後、すべての Internet Computer のサブネットに段階的に展開されました。図1~3は、アップグレード時に高負荷がかかった1つのサブネットにおける最適化の影響を示しています。主に以下の2つの改善を確認できます。
- ファイナライズの増加と安定: 不安定だったファイナライズ速度が 0.5ブロック/秒 から期待通りの 1ブロック/秒 に回復しました。
- メッセージの実行時間の改善: メッセージの平均実行時間は ~3倍、最大実行時間は ~10倍、改善されました。
図1. 最適化の展開前と展開後のブロックファイナライゼーション速度。赤線は、バージョンアップしてレプリカを再起動した時刻です。
図2. 最適化の展開前と展開後の平均メッセージ実行時間。
図3. 最適化の展開前と展開後の最大メッセージ実行時間。
背景:直行永続性
キャニスターが受信して実行できるメッセージには、クエリーとアップデートの2種類があります。クエリーは読み取り専用で、クエリーがWasmメモリで実行したすべての変更は、実行後に破棄されます。一方、アップデートメッセージの実行に成功すると、すべてのメモリ変更は自動的に永続化され、以降のアップデートメッセージとクエリーで利用できるようになります。この概念は、直交永続性として知られています。
直交永続性の実装は、以下の2つの問題を解決しなければなりません。
- 永続性メモリをWasmメモリへマッピングする方法。
- Wasmメモリのすべての変更を追跡し続けて、後で永続化できるようにする方法。
現在の実装では、この両方の問題を解決するためにページプロテクションを利用しています。メッセージの実行が始まると、Wasmメモリのアドレス範囲全体をページと呼ばれる4KiBのチャンクに分割します。最初は、オペレーティングシステムのページプロテクションフラグを使用して、すべてのページがアクセス不能としてマークされます。つまり、最初のメモリアクセスでページフォルトが発生し、実行が一時停止され、シグナルハンドラが呼び出されます。シグナルハンドラは、対応するページを永続化メモリからフェッチし、そのページを読み取り専用としてマークします。そのページへのその後の読み取りアクセスは、シグナルハンドラの助けを借りずに成功します。最初の書き込みアクセスは、別のページフォルトを引き起こしますが、シグナルハンドラがページを変更されたものとして記憶することを可能にし、ページを読み取り可能および書き込み可能としてマークすることを可能にします。それ以降のそのページへの全アクセス(読み取りと書き込みの両方)は、シグナルハンドラを呼び出すことなく成功します。
シグナルハンドラの起動 と ページプロテクションフラグの変更は高価な操作です。大きなメモリのチャンクを読み書きするメッセージは、そのような操作の嵐を引き起こし、システム全体のパフォーマンスを低下させます。これが高負荷時に見られる性能のボトルネックです。なお、このシグナルハンドラは、Internet Computer のローンチ前に書かれたもので、性能ではなく正確さを最優先したものでした。
背景:クエリの同時実行
キャニスターは、更新メッセージを1つずつ順次実行します。これに対し、クエリーは、互いに、そして更新メッセージに対して同時に実行できます。同時実行をサポートすることで、メモリ実装はよりチャレンジングなものになります。あるキャニスターがブロック高 H で更新メッセージを実行しているとします。同時に、ブロック高 H-K で先に開始した長時間実行のクエリがまだ残っている可能性があります。つまり、同じキャニスターで、複数のバージョンのメモリが同時にアクティブになる可能性があります。
この問題に対する素朴な解決策は、各更新メッセージの後にメモリ全体をコピーすることでしょう。これでは遅く、多くのストレージを使うことになります。そこで、現在の実装では別の方法をとっています。Fast Mergeable Integer Maps をベースにした PageDelta と呼ばれる永続的な木構造データ で、変更されたメモリページを保持します。一定の間隔で(つまり、Nラウンド毎で)、ファイルのクローンを作成した後、変更されたページをチェックポイントファイルにコミットして、以前のバージョンを保持するチェックポイントイベントがあります。図4は、Wasm メモリが PageDelta とチェックポイントファイルからどのように構築されるかを示しています。
図4. a) チェックポイントファイルには、前回のチェックポイント時のWasmメモリが保存されています。 b) 前回のチェックポイント以降に変更されたページは、PageDelta という永続データ構造に格納されます。 c) Wasmメモリは、チェックポイントファイルのページと変更されたページのコピーにより、シグナルハンドラーにより、遅延して構築されます。
最適化1:チェックポイントファイルのメモリマッピング
最初の最適化は、チェックポイントファイルのページをメモリマッピングすることです。これは、同時に実行される複数のメッセージ間でページを共有することで、メモリ使用量を削減します。また、この最適化により、読み取りアクセス時のページコピーが回避されることで、パフォーマンスが向上します。シグナルハンドラーの呼び出し回数は以前と同じなので、この最適化後もシグナルの嵐の問題は未解決です。
最適化2:クエリーでのページ追跡
クエリが変更する全メモリページは、実行後に破棄されます。つまり、シグナルハンドラはクエリで変更されたページを追跡する必要がないのです。しかし、シグナルハンドラの古い実装では、更新メッセージとクエリーを区別していませんでした。私たちは、最初のアクセスでページを読み取り可能、書き込み可能とマークすることでクエリーが高速になる方法を導入しました。このように簡単に実現できる最適化により、クエリーは平均で 1.5 倍から 2 倍高速化されました。
最適化3:ページの償却型プリフェッチ
最もインパクトのある最適化のアイデアはシンプルです。ページフォルトの数を減らしたいのであれば、シグナルハンドラの呼び出しごとに多くの処理を行う必要があります。新しいシグナルハンドラでは、一度に 1 ページをフェッチする代わりに、推論してより多くのページをプリフェッチしようとします。あまりに多くのページをプリフェッチすると、数ページしかアクセスしない小さなメッセージのパフォーマンスを低下させる可能性があるため、ここでは適切なバランスが求められます。この最適化では、現在のページの直前にアクセスされたページの最大連続範囲を計算します。この範囲の大きさを、より多くのページをプリフェッチするためのヒントとして使用します。この方法により、プリフェッチのコストは、以前にアクセスされたページによって償却(訳注: おそらくアルゴリズムでいうところの償却。amortized)されます。その結果、この最適化により、メモリ負荷の高いメッセージにおけるページフォルトの数が1桁減ります。
結論
このシグナルハンドラーのオリジナルは、Internet Computer のローンチ前に、性能よりも正確さに重点を置いて書かれたものでした。この部分は性能の最適化が必要となるのは当然でした。しかし、Internet Computer の急成長に伴い、予想以上に早く最適化が必要になったのです。これらの最適化により1つのボトルネックは解消されましたが、Internet Computer が成長し続けることで、さらなるボトルネックが発見されるかもしれません。もしあなたがパフォーマンスに関する仕事に興味があるならば — DFINITYは採用しています。
謝辞
Akhi Singhania, Alin Sinpalean, Dimitris Sarlis, Dominic Williams, Johan Granström, Kiran Joshi, Roman Kashitsyn, Saša Tomić, Stefan Dietiker, Stefan Kaestle にアイデアを議論してもらい、コードの変更をレビューしてもらったことに感謝します。また、このブログ記事をレビューしてくれた Diego Prats に感謝します。