Wasmは新しいCGI
(roborooter.com)- WasmはCGIプロトコルではなく、
cgi-binが生み出したものと同じように、Webアプリの実行単位とデプロイ方式を変える次のアプリケーションモデルと見なせる - CGI、FastCGI、Rack/WSGI、サーバーレスはいずれもリクエスト処理とサーバー管理を再設計してきており、共通の目標は高性能なアプリケーションをより簡単に作り、保守することだった
- Wasmモジュールはホストランタイムで実行され、分離されたメモリとスナップショットによってクリーンな実行状態を保ちながら起動コストを下げられる
- ネイティブスレッドの不在、JIT不可、データコピーのコスト、Interface Typesとmodule linkingの未成熟さは、Wasmベースのサーバー実行モデルの主な制約である
- サーバーレス関数とエッジ関数がWasm実行環境と結び付くと、コールドスタートと分離コストを下げつつ、複数言語のモジュールをより軽量に組み合わせられる
CGIからサーバーレスへと続くWebアプリケーションモデル
- CGIの核心はCommon Gateway Interfaceプロトコル自体よりも、
cgi-binフォルダに置いたスクリプトや実行ファイルをURLで呼び出すアプリケーションモデルにあった - この方式は、Webを文書アーカイブから相互作用可能なアプリケーションのネットワークへと変える初期のWebアプリケーションモデルだった
- リクエストごとに新しいプロセスを起動する構造のため、今日の基準ではプロセス起動とスクリプト解析のコストが大きなボトルネックになる
FastCGIと言語別Webサーバーの変化
- FastCGIはCGIの性能問題を減らすために、長寿命プロセスがCGIリクエストを処理するモデルを導入した
- Webサーバーはリクエストごとに新しいプロセスを立ち上げず、1つ以上の長期実行プロセスと通信する
- 既存のCGIアプリケーションは、リクエスト後にプロセスが終了する前提で作られていたため、長期実行環境ではリソースリークが起きやすかった
- その後、言語ベースのWebサーバーが慣習として定着し、アプリケーションはリクエスト/レスポンスモデルを中心に作られるようになった
- Apacheやnginxのような機能豊富で実績あるWebサーバーの背後に配置し、遅いリクエストやHTTPの細かな仕様からアプリケーションを切り離す方式が一般的だった
- サーバー実装は、リクエストごとのfork、OSまたは言語スレッド、イベントまたはリアクターモデルなど、さまざまなプロセス管理戦略を使っていた
- RubyコミュニティのRackインターフェースは、PythonのFlaskアプリケーションサーバーやWSGI仕様にも影響を与えた
- 単純化すると、リクエストはHTTPメソッド、ヘッダーハッシュマップ、入力バイト文字列またはストリームで構成される
- レスポンスはステータスコード、ヘッダーオブジェクト、レスポンスバイト文字列またはストリームで構成される
クラウドのオートスケーリングとサーバーレスの折衷
- 物理サーバーや仮想サーバーを前提にしたモデルでは、稼働中サーバーの台数を直接管理する必要があった
- トラフィックが高いとアプリケーションサーバーが遅くなることがある
- トラフィックが低いと多くのコンピュータが遊休状態のままになることがある
- クラウドのautoscalingは、CPU、メモリ負荷、時間帯に応じてアプリケーションサーバーの台数を調整できるようにした
- 新しいコンピュータをスケールアウトするには、アプリケーションや設定に応じて2〜20分かかることがある
- スケールアウトはクラウドホスティングリージョンの利用可能なリソースにも依存する
- Amazon LambdaとAPI Gatewayを組み合わせたサーバーレスコンピュートは、サーバーの代わりに関数を管理するモデルを生み出した
- リクエストごとに単一プロセス、分離されたCPU、分離されたメモリを保証する
- プロセスは最大で数時間再利用されることもあるが、使われなければ一時停止または削除される
- AWSはリクエスト量に応じて数秒単位でスケールアップとスケールダウンができる
- サーバーレスには明確なトレードオフがある
- 新しいプロセスのコストのため、同時リクエストに合わせて拡張する際、一部のリクエストはコールドスタートを経験する
- レスポンス後にプロセスが一時停止されることがあるため、リクエスト間で永続TCP接続を管理するのが難しい
- HTTPベースのデータベースAPIは、多数の接続に耐え、関数とともにスケールアップ・ダウンしやすいため、サーバーレスモデルで人気がある
- リクエストごとに専用のCPUとメモリを提供する性質は、ワークロードによって長所にも短所にもなる
- 一部のワークロードでは、リソース管理を減らし、コストやスケーリングの懸念を小さくできる
- 他のワークロードでは、単一プロセスが多数のリクエストを処理したり、共有メモリでバッチ処理やキャッシュを活用したりする方が効率的な場合がある
- CMSベースのWebアプリケーションをサーバーレスへ移してコストを90%削減した例と、イベント分析サービスをサーバーベースモデルへ移してコストを90%削減した例が併せて示される
サーバーにおけるWasmの実行モデル
- Wasmはもともとブラウザで高性能コードを実行するために開発され、asm.jsのような試みともつながっている
- WebAssemblyはスタックベース仮想マシン向けのバイナリ命令形式であり、複数のプログラミング言語の移植可能なコンパイルターゲットとして設計されている
- Webのクライアントおよびサーバーアプリケーションの配布を可能にする
- サイズとロード時間の効率に優れたバイナリ形式でエンコードされるよう設計されている
- さまざまなプラットフォームに共通するハードウェア機能を活用し、ネイティブ速度に近い実行を目指す
- 現在では複数の言語をWasm命令へコンパイルし、ブラウザとサーバーの両方で実行できる
- ブラウザが要求した分離とセキュリティモデルは、サーバーアプリケーションにも有用である
- 信頼できないコードを、VMやDockerコンテナよりはるかに軽量な形で分離できる
- Node.js、Cloudflare Workers、DenoのようなV8ベースのサーバーレス環境は、ブラウザで蓄積されたWasm実行能力を活用している
- Fastly、Shopify、SuborbitalのようなWasmネイティブ環境も存在する
モジュール、ホスト、メモリ、スナップショット
- Wasmモジュールは仮想マシン向け命令であるため、実行にはランタイムが必要になる
- ランタイムは汎用Wasmをローカルアーキテクチャ向けにコンパイルし、実行環境を提供する
- 一部の環境はLinuxシステムのPOSIX APIに似たインターフェースを提供する
- 一部の環境はホストシステムの特定の関数だけを提供し、モジュールのexport関数を実行させる
- WebAssemblyプログラムはmodules単位で構成され、モジュールを実行するVMはhostと呼ばれる
- モジュールはデプロイ、ロード、コンパイルの単位である
- 型、関数、テーブル、メモリ、グローバル値の定義を集める
- importとexportを宣言し、データセグメント、要素セグメント、start関数という形の初期化を提供できる
- Wasmの“memories”は途切れのない連続バイト配列として表現され、ホストがインスタンス化時に割り当てる
- 各ゲストモジュールはメモリ分離を得る
- このメモリは仮想マシンのRAMのように動作する
- 空のまま提供することも、データセグメントで事前に埋めることもできる
- モジュールの分離方式のおかげで、モジュールを一時停止してメモリをデータセグメントとして保存できる
- 仮想マシンのスナップショットに近い概念である
- 一時停止されたモジュールのコピーを複数起動できる
- WizerはWasmモジュールをインスタンス化して初期化関数を実行した後、インスタンス状態を記録してWasmバイナリを書き換える
- グローバル値の状態を直接初期化する
- 0以外のメモリ領域を記録する
- 既存のデータセグメントを削除し、記録されたメモリ領域のデータセグメントに置き換える
- この方式は、新しいプロセスのようなクリーンな実行を提供しつつ、新規プロセスの起動コストは回避する
- CGIの欠点がないCGIに近い
- 現代的な表現では、コールドスタートのないサーバーレスに近い
Wasmの制約と利点
- Wasmはネイティブ構成ではスレッドを持たない
- ブロッキング処理はホストメソッドへ移さなければならない
- ホストはファイルやネットワークインターフェースの読み書きラッパーを提供したり、モジュールを一時停止したり、コールバックハンドラを提供したりできる
- リアクターモデルやブロッキングモデルは作れるが、thread proposalが適用されるまでは、実行環境の設計が大きな比重を占める
- セキュリティ上の理由から動的なWasmコード生成は許可されず、JITコンパイルは不可能である
- ランタイム内でコードそのものをアドレスとして扱えない
- V8、CRubyなどJITに性能を依存する実行環境は、Wasm VM上では動かせないか、JITを諦める必要がある
- スクリプト向け最適化ランタイムをビルド段階で出力する“pre-jit”アプローチも提案されているが、広く使われてはいない
- Wasmモジュールとホストの基本インターフェースはメモリであるため、データ移動にはコピーが必要になる場合がある
- メモリチャンクの共有は可能だが、ランタイムのメモリモデルによって可能性や推奨可否が異なる
- ほとんどのランタイムでは、モジュールとI/O処理の間でゼロコピー通信を行うのは難しいとされる
- ストリーミングデータをWasmモジュールの内外へ移動する処理は、ホスト側で処理するより遅くなることがある
- Wasm VMは使用量制御の面で高い統制を提供する
- 一部のランタイムはWasmtime fuel conceptのようにCPU命令数を数え、CPU上限を強制できる
- VMはメモリとwall clock時間を制限できる
- Interface Typesとmodule linkingは、モジュールサイズを減らし、I/O速度と使いやすさを改善できる可能性がある
- Interface Typesは利用可能だが、まだ批准済み標準ではない
- module linkingには動作するプロトタイプがあるが、標準的アプローチはまだ存在しない
- Wizerは現時点でmodule linkingと衝突する可能性があり、これを解決するカスタムアプローチはあるものの、明確な勝者はまだない
- Interface Typesは、言語中立のオブジェクトがWasmメモリ境界を高コストなエンコード・デコードなしで通過できるようにする方向を目指している
- 現在よくある方法は、JSONをメモリへコピーして入出力することだ
- Wasmモジュールは基本的に与えられたものにしかアクセスできないため、セキュリティモデルが小さく明確である
- 信頼できないWasmコードを実行することは一般に比較的安全である
- VMの攻撃面はDockerや他の分離モデルより小さい
- ホストハードウェア上で変換済み命令を実行するためタイミング攻撃は可能だが、緩和策がある
- Wasmをネイティブバイナリへコンパイルすることもできるが、攻撃面はより大きくなる
エッジ関数とWasmベースの関数実行
- Wasm実行環境と開発ツールが広く使われるようになれば、スクリプト言語にもWasmランタイムとWizerのような**事前起動(preboot)**方式を備える圧力がかかるかもしれない
- 理論上、アプリケーションはリクエストごとにスナップショットされたコピーから再開され、現在の他モデルより高速に動作できる可能性がある
- ローカルCLIもRubyで書いた後、スナップショット済みWasmモジュールとして配布し、Wasm Rubyランタイムに接続すれば、C++ユーティリティに近い起動時間を得つつ、プロジェクトディレクトリ内だけで動作するよう制限できる
- 最初の大きな変化は、関数をユーザーに近いエッジへ移すことである
- コンピュートはデータベースの近くではなく、ユーザーの近くで実行される
- Vercel Edge FunctionsはV8ベースだが、多くの原則を共有する例として挙げられている
- next-authはJWTログイントークンに基づいて事前レンダリング済みページへのアクセスを制御できる
- CDNにキャッシュされたデータで構成された動的なパーソナライズコンテンツを提供できる
- もう1つの変化は、サーバーレスアプリケーションでプロセスベース関数をWasmベース関数へ置き換えることである
- SuborbitalはWasmベースの関数実行環境を自作できるようにし、Wasm関数をワークフローとして連結する方式も提供する
- 多くの実行プラットフォームはリクエストごとに単一モジュールを推奨しており、メモリ共有や複数モジュールの高速呼び出し可能性を十分に活用していないとされる
- Interface Typesが標準化されれば、Rackに似たミドルウェアデータモデルが登場する可能性がある
- WasmはLambda関数の内部でも実行できる
- 既存インフラ上でスナップショット済みアプリケーションのバージョンを使える
- この組み合わせは興味深いが、最終的には消えていくかもしれない技術の混在として扱われている
次のWebアプリケーションモデルの方向
- Wasmは性能を高め、プロセスレベルのセキュリティを容易にし、サーバーレス関数のビルドと実行コストを下げられる技術として提示されている
- ほぼあらゆる言語を実行でき、module linkingとInterface Typesによって関数間レイテンシを大きく下げられる可能性がある
- システムの制約が変わると以前は不可能だったことが可能になる、という点がWasmを新しいCGIと見る核心的な根拠である
1件のコメント
Hacker News のコメント
WASM が Java Applet、ActiveX、Silverlight、Macromedia Flash のような昔の技術と何が違うのか、よく分からない
ブラウザで信頼できないサードパーティのコンパイル済みコードを実行することについては、もう教訓を得たと思っていたし、顧客体験の改善という名目でサーバーの計算コストをクライアントに押し付ける構造のように見える
実装には脆弱性が多く、結局ブラウザでは事実上使えなくなったし、他の技術はそもそもそうした約束すらしていなかったように思う
JavaScript はその約束を実際に守り、今日のブラウザはどのドメインからダウンロードされた JavaScript でも、ユーザーに信頼するかどうかを尋ねずに実行する
WASM は JavaScript エンジン上に載って同様のセキュリティ保証を提供するので、JVM バイトコードと根本的な違いはなく、実質的な違いはWASM は安全性を証明できたが、JVM はできなかった点にある
いまや Google Chrome は、数十億人が悪意ある WASM を実行してもスマートフォンが侵害されないほど安全で、このエンジンをサーバーに持ち込んで、複数ユーザーのスクリプトを強固なサンドボックス内で実行し、リソースを共有させることができる
代替は仮想化で、コードを WASM の塊にコンパイルして大きな WASM サーバーで動かすか、amd64 バイナリと切り詰めた Linux カーネルをまとめて VM で動かすことができる
現時点ではどちらかが明確な勝者とは言いにくく、それぞれのアプローチに長所と短所がある
JVM と違って WASM は線形メモリを提供し、デフォルトではガベージコレクションがないので、C/C++ を Emscripten でコンパイルしたり Rust のターゲットにしたりするなど、より広い言語群のコンパイル先に向いている
WASM はバイトコードであり、ほとんどの実装はホストの JavaScript エンジンやランタイムをかなり共有していると思う
クライアントとサーバーの間で計算コストが移動するのは、業界が昔からリッチクライアントとシンクライアントの間を行き来してきた流れの一部であり、この振り子は今後も動き続けるだろう
Wasm バイトコードが従うべき検証仕様があり、この検証済みサブセットのおかげで、過去の技術で見られたセキュリティ脆弱性の多くが原理的に不可能になる
Heartbleed や Rowhammer のようにハードウェアの誤動作に依存する攻撃は可能かもしれないが、たとえば数値をポインタのように解釈するよう VM をだまして、自分の Wasm メモリの外を参照するようなことはできない
Wasm バイトコードは機械語に変換するのがかなり単純なため、VM を使うより実装が小さく高速になり得る
特定企業の所有物ではなく、誰でも使えるよく書かれた公開仕様があり、Web 標準として採用されているのでブラウザ拡張は不要
クライアント側の計算はすでに JavaScript でも起きていることであり、Wasm コードは JavaScript では不可能な形で効率的になり得るので、むしろその傾向は強い
Applet はある程度、ActiveX はほとんどまったく仲介されておらず、WASM の「外側のプラットフォーム」はおおむね JavaScript ランタイムである一方、Applet の外側のプラットフォームは execve(2) に近い
WASM ではそうなることもあるが常にそうとは限らず、より良いサンドボックス化や分離といった違いについても、すでに他の回答がうまく扱っている
「Amazon が Lambda でサーバーレスコンピューティングの時代を開いた」と言うには、Google App Engine が 2008 年に登場しており、Lambda より 6 年早かった
すでに何年も前からそうした製品があり、名前もあったのに、なぜ「serverless」という名前が付いたのか、どこから来たのかよく分からない
App Engine にもバッチワーカーと Web ワーカーがあり、Heroku も同じだった
どちらも Docker 以前の製品なので人々が違って感じるのかもしれないが、Lambda も最初から Docker でリリースされたわけではなかったと思う
「セキュリティ上の理由で動的な Wasm コード生成が許可されていないため、JIT コンパイルは不可能だ」という話は正しくないように見える
クリーンなコードのホットリロードのような作業を許可するには、本質的な機能に近い
セキュリティの論理は粗いと思う
JS は実行中のホットリロードや、もっと過激なコード生成もセキュリティを破らずにできるし、Wasm ランタイム全体を動的に再ロードしつつメモリを保持すれば、コード生成やホットリロードをまねることもできるが、ユーザー体験は不格好になるだろう
技術的に不可能でなければならない理由が見当たらず、セキュリティ対策だとしたらあまりに簡単に迂回できそうに見える
WASM バイトコードは概念的には .NET IL や Java バイトコードのように、JIT コンパイルを念頭に置いたものと非常によく似ている
WASM は強い方向性と、タイムリーに成功させようという意思が足りないプロジェクトのようで、あまり好きではない
名前は「Web 用アセンブリ」、つまり仮想 CPU 向けの機械語を示唆しているが、実際にはコンパイラバックエンド向けの中間表現であり、ガベージコレクション対応のような高水準機能も計画されていて、概念が曖昧だ
前述のホットリロード、ハックに近くないスレッディング、JavaScript なしでの DOM との直接連携、低オーバーヘッドのグラフィックス/計算 API、低レベルのオーディオアクセスといった基本機能もまだ不足している
大きなマルチメディアアプリを大きな妥協なしに動かすのは難しい
Wasm はメモリを実行可能としてマークできず、実質的にハーバードアーキテクチャのようにコードとメモリが分離されている
さらにコードの任意の位置へジャンプすることもできず、ジャンプ命令自体もない
ここでいう JIT は実行中にネイティブコードをコンパイルして実行することを意味し、ブラウザや Wasm サンドボックスでは大きなセキュリティ上の弱点になる
これは設計と命令セットに組み込まれているため、簡単に迂回できる性質のものではなく、詳しくは https://webassembly.org/docs/security/ で確認できる
Wasm もエンジンが JIT を担うという点では .NET IL や Java バイトコードに似ているが、ユーザーにランタイムの外でネイティブコードを作り、そこへジャンプする権限を与えるのは危険だ
そのため JIT と同じように、WASM コードを初めて実行するときに多少の「ウォームアップ時の引っかかり」が見えることがあるが、ここ数年でかなり改善された
また、ブラウザでは WASM の塊を動的に作成し、インスタンス化して実行できると理解している
他の WASM ランタイムでも可能かは分からないし、ブラウザでも JavaScript を経由する必要はあるが、何らかの「Web API」にアクセスしようとすれば、どうせそれは必要になる
WASM は JavaScript VM のような特定言語専用の VM を、JavaScript VM が使われている場所ならどこでも使える汎用 VMに変えるものだ
もちろん、そこだけに限定されるわけではない
汎用というのは、コンパイラやインタプリタさえあればほぼ何でも実行できるという意味で、JavaScript も含まれる
おおむね JavaScript エンジンの一部として実装されるため、サンドボックス化や当該 API へのアクセスといった特性を多く受け継ぐ
そのアクセスを標準化する作業はまだ進行中だが、最終的には現在 JavaScript でしかできないことが WASM でも可能になり、JavaScript では難しい、あるいは不可能なさらに多くのことも可能になるだろう
より高速かつ滑らかに実行される可能性もある
WASM の核心は、JavaScript が人気のある環境にあった制約をかなり取り除くことにある
JavaScript は好き嫌いの分かれる言語なので、唯一の選択肢から複数の選択肢の一つへ変わるということだ
WASM は JavaScript の代替、Docker の代替、Java の代替、CGI の代替としても説明されてきたし、短く言えばそのすべてであり、それ以上でもある
似たようなことをするツールが多すぎて、境界が不明確だ
結局すべて動くようにしても個人的には脆く感じるし、専門でやっていない人には怖い作業だ
最近 Web 向けに何か作る必要があるときは leptos を使っているが、開発者体験はずっと良く、まだ 1.x にもなっていないものの、JS バンドルをトランスパイルし、難読化し、縮小し、パッキングするためにツールを 5 個つなぎ合わせるより安定しているように感じる
この記事を見て、昔よく引き合いに出していたソフトウェアの法則を思い出した
十分に大きく長く生き残ったアプリケーションは、最終的に自分が動いているソフトウェアスタック全体、OS までも再実装することになり、しかもそれをひどい出来で再実装する、という法則だ
出典はよく分からないが、たいてい当てはまることが多い
「十分に複雑な C や Fortran のプログラムは、場当たり的に、非公式な仕様に従って、バグが多く遅い Common Lisp の半分を実装する」
より一般的なパターンは Inner-Platform Effect と呼ばれる
タイトルの前提を広げて考えると、WASM がその系譜の真の後継者になるには、任意のホスティング業者の LAMP スタックに PHP アプリを置いてデプロイしていたのと同じくらい簡単でなければならない
まだPHP のデプロイ体験ほど簡単ではないように思う
ホスティングで何が変わるべきだと期待しているのか分からない
これは違う見方をしている
未来はローカルファーストだと思う
アプリがサーバーの助けをほとんど受けず、ユーザーのブラウザ内で大部分実行される方式だ
Figma、Linear、Superhuman のようなアプリはこのモデルを非常にうまく使っていて、Stackblitz もある程度そうだ
Figma のようにある程度複雑なアプリがユーザーのブラウザ内でほぼすべて実行できるなら、たいていのアプリも可能だと思う
サーバー側は主に、ユーザーが複数の場所でアプリを使うときに、異なるインスタンス間でデータを同期する役割を担う
Electric-SQL のようなツールが作られているところだが、まだ成熟してはいない。こうしたライブラリが成熟すれば、この領域は大きく成長するだろう
サーバーレスは概して Amazon や Azure のような会社が儲けるためのもので、結局 CGI のようになるだろう
WASM も成功しうるが、主にユーザーのブラウザ内でのことになる可能性が高い
Microsoft は C#/Blazor に WASM を使っているが、ブラウザ内の dotnet が JavaScript と同じくらい速くなるとは思えないので、正しいアプローチではないと思う
秒間 1兆回の広告インプレッションにスケールできないので、誰も話題にしないだけだ
サーバーレス関数は、誰かが 1つ書くたびに Bezos のヨットに10フィートを追加する
オフラインだったり Wi-Fi が不安定な場所にいたりするとデザインを読み込めないし、最近変わっていなければ、編集後に Wi-Fi が切れた状態でブラウザを終了すると保存されない
それに、まったく新しい考えでもない
私たちは Blazor WASM を開発していて、性能面で dotnet は問題ではない
基本的には JVM とそのエコシステムを作り直しているということ?
過去の教訓を反映して X を作り直すことが、実際に素晴らしいアイデアである場合もある
たとえば 2010年ごろ、ブラウザで C/C++ コードを動かすことを調べ始めたとき、C/C++ を JVM にコンパイルするのは事実上不可能だった
当時は Java Applet がまだ意味を持っていたので、それが可能ならよかったのだが、WASM もまだなく、Emscripten はあり、それが asm.js を経て最終的に WASM の誕生につながった
2000年代のプログラミング言語の授業でクラスローダーをいじり、JVM アセンブリを直接書いた立場からすると、JVM がこの分野の頂点なのかもよく分からない
大きなエコシステムを可能にしたのは確かだが、JVM が外の世界と接するインターフェースは本当に不格好な混乱だ
20年以上、JVM 関連の何かに出くわすたびにうめき声が出た
Rust のパッケージングとエコシステムを Python や、さらにひどい C++ と比べてみれば、過去数十年の教訓を反映した再発明が非常に良いことになり得ると分かる
2つの wasm ファイルを 1つに結合し、メモリのマージを正しく合わせることはまだ非常に難しい
コンポーネントモデルで直せるかもしれないが、無駄に肥大化した要素が多すぎるので、Safari が採用するまでは長くかかるかもしれない
ずっと前から、WASM はクラウドの Lambda 関数コードを置き換える世界へ向かうと思ってきた
WASM は伝統的にホストプラットフォーム上で実行されるものと見なされているが、必ずそうでなければならない理由はない
WASM のサンドボックス性のおかげで、技術的には OS の外側や ring0 で実行し、多くの OS オーバーヘッドを回避することもできる
WASM にコンパイルすれば、ユーザー側から見ると多くのデプロイ上の問題がずっと単純になり、ホスティング環境には最適化の余地が大きくなる
WASM をより高速に動かすカスタムハードウェアまで可能かもしれない
任意の信頼できない出所から来た任意の信頼できないコードを自動実行しなければならないのは悪いことで、サーバー側 CGI と同じ役割を埋めるとも言えない