マイクロサービスからHTTPが消える?WASI 0.3で始まった「同一プロセス内サービス」の未来を実装から理解する
マイクロサービスは、本当にネットワーク越しである必要があるのだろうか。
ユーザーサービス。
在庫サービス。
認証サービス。
価格計算サービス。
これらを別々のプログラムとして作り、HTTPやgRPCで接続する。
現在では当たり前の設計だ。
しかし、その「当たり前」にWebAssemblyが別の選択肢を持ち込み始めている。
2026年6月11日、WASI 0.3.0が正式リリースされた。
今回の変更を一言で「WebAssemblyでasync/awaitが使えるようになった」と説明すると、本質をかなり見失う。
重要なのは、WebAssembly Component Modelの境界そのものが非同期処理を理解するようになったことだ。
新しくCanonical ABIへ入ったのは、主に次の3つ。
async func
future<T>
stream<T>
これによって、異なる言語で書かれた複数のWebAssembly Componentを、同一ランタイム上で非同期に直接つなげるための基盤が整った。
極端に言えば、これまで
Service A
↓ HTTP
Service B
↓ HTTP
Service C
だったものを、
Wasmtime Process
│
├── Component A
│ ↓
├── Component B
│ ↓
└── Component C
のように構成できる。
コンポーネント間ではHTTPを飛ばさない。
JSONへシリアライズしない。
TCP接続も不要。
それでも、各サービスは独立したコンポーネントとして型付きのインターフェースを持つ。
これが本記事で扱うWASI 0.3の世界である。
最初に結論:WASI 0.3は「Dockerの代わり」ではない
WASIの記事では、よく「WebAssemblyはDockerより軽い」という説明が出てくる。
間違いではないが、それだけではWASI 0.3の面白さをほとんど説明できていない。
WASI 0.3で注目すべきなのは、コンテナの代替ではなく、プログラムを分割する単位そのものが変わる可能性である。
現在のマイクロサービスでは、プロセス境界とサービス境界がほぼ一致する。
サービス境界
=
プロセス境界
=
ネットワーク境界
その結果、サービスを分割するたびに、
- TCP
- HTTP
- HTTP/2
- gRPC
- TLS
- JSON / Protobuf
- Load Balancer
- Service Discovery
- Retry
- Timeout
- Circuit Breaker
- Distributed Tracing
といった仕組みが必要になる。
Wasm Component Modelでは、サービス境界とプロセス境界を分離できる。
サービス境界
≠
プロセス境界
つまり、独立したサービスのように設計しながら、必要であれば同一プロセス内へ配置できる。
これはかなり大きな変化である。
そもそもWASIとは何なのか
WebAssemblyは元々ブラウザで高速なコードを実行するために広まった。
しかし、WebAssemblyそのものには「ファイルを開く」「TCP接続する」「時計を読む」といったOS機能が定義されていない。
そこで登場するのがWASI、WebAssembly System Interfaceである。
WebAssembly Program
↓
WASI
↓
Runtime / Host
↓
Linux / macOS / Windows
POSIXに少し似ているが、思想はかなり違う。
WASIでは「プログラムだからOSを自由に使える」のではなく、ホストから与えられた能力だけを利用するCapability Orientedな設計が基本になる。
例えばファイルシステムへのアクセスを与えなければ、そのComponentからはホストのファイルを勝手に読むことができない。
ネットワーク、環境変数、時計などについても同様に、ホストが能力を提供する。
WASI Preview 1、Preview 2、Preview 3
WASIの歴史を知らないと0.3が分かりにくい。
WASI Preview 1
↓
WASI 0.1
↓
Core Wasm Module中心
WASI Preview 2
↓
WASI 0.2
↓
Component Model + WIT
WASI Preview 3
↓
WASI 0.3
↓
Component Model + Native Async
Preview 2、つまりWASI 0.2で重要だったのがComponent Modelである。
そしてPreview 3、WASI 0.3では、そのComponent Modelへ非同期処理が本格的に組み込まれた。
Component Modelが重要な理由
通常のWebAssembly Moduleは、低水準な値をやり取りする。
例えばCore Wasmの関数境界で直接扱いやすいのは、
i32
i64
f32
f64
といったプリミティブ値である。
しかし実際のアプリケーションでは、こんな関数を書きたい。
get-user(
user-id: u64
) -> result<user, error>
userには名前やメールアドレスが入っている。
record user {
id: u64,
name: string,
email: string,
}
RustならRustのString。
JavaScriptならJavaScriptのstring。
Pythonならstr。
C#ならSystem.String。
内部表現は全部違う。
この言語間の橋渡しをするのがComponent ModelとCanonical ABIである。
WITという「言語に依存しない型定義」
Component Modelでは、インターフェースをWIT、WebAssembly Interface TypeというIDLで定義する。
例えば商品価格サービスなら、次のように書ける。
package yuus:pricing;
interface pricing {
record request {
product-id: string,
quantity: u32,
}
record response {
unit-price: u64,
total-price: u64,
}
calculate: func(req: request) -> response;
}
world pricing-service {
export pricing;
}
ここにはRustもC#もPythonもJavaScriptも書かれていない。
インターフェースだけである。
Rustで実装してもいい。
Goでもよい。
将来別の言語へ差し替えても、WITインターフェースが維持される限り利用側を大きく変える必要がない。
ここでCanonical ABIが動く
例えばWITのstringを考える。
RustとJavaScriptでは文字列のメモリ表現が違う。
そこでComponent Modelは、Canonical ABIを介して言語固有表現とComponent Model上の値を相互変換する。
Rust String
↓
Canonical Lift / Lower
↓
Component Value
↓
Canonical Lift / Lower
↓
JavaScript String
この仕組みがあるため、WITで定義したrecord、variant、list、string、resultなどを言語を越えて扱える。
言い換えるとComponent Modelは、WebAssemblyを単なる「移植可能なCPU命令」から、移植可能なソフトウェア部品へ引き上げようとしている。
ところがWASI 0.2には非同期処理の問題があった
ネットワークやディスクI/Oでは、処理完了までCPUを占有して待つのは効率が悪い。
そのため現代のサーバーアプリケーションでは非同期I/Oが重要になる。
しかしWASI 0.2では、Component Model自体にfutureやstreamという共通概念が存在しなかった。
そのためWASI側でpollableやinput-streamなどのResourceを組み合わせて非同期処理を表現していた。
WASI 0.2
start-connect()
↓
subscribe()
↓
poll()
↓
finish-connect()
接続という一つの操作を、開始、待機、終了へ分割していたわけだ。
さらにComponentごとに独自のevent loopやasync runtimeを抱えることになり、複数Component間の非同期処理を自然に合成するのが難しかった。
WASI 0.3ではランタイムが非同期処理を知っている
WASI 0.3では、この問題をComponent Model側で解決する。
async func
future<T>
stream<T>
がCanonical ABIの第一級要素になった。
接続処理なら、WASI 0.2のようなstart/finish方式ではなく、概念的には次のようになる。
connect: async func(
remote-address: ip-socket-address
) -> result<_, error-code>;
非常に普通のasync関数に見える。
しかし重要なのは、このasyncがRust固有でもJavaScript固有でもないことだ。
Component ModelのABIそのものがasyncを理解している。
future<T>とは何か
future<T>は、将来得られる一つの値を表す。
future<u32>
future<result<response, error-code>>
JavaScriptのPromiseやRustのFutureと似た考え方だ。
ただし特定言語のFutureではない。
Component境界を越えられるCanonical ABIの値である。
Rust Component
│
future<T>
↓
Python Component
│
↓
JavaScript Component
途中のComponentがFutureの結果を自分で消費せず、そのまま次のComponentへ渡すこともできる。
stream<T>はもっと面白い
stream<T>は複数の値を時間方向に流す。
stream<u8>
ならバイトストリームとして利用できる。
HTTP body、TCP通信、標準入力、ファイル読み込みなどと相性がいい。
例えばWASI 0.3の読み込み処理では、データ本体と完了状態が分離される。
read-via-stream: func()
-> tuple<
stream<u8>,
future<result<_, error-code>>
>;
streamからデータを受け取りつつ、futureから処理全体の終了状態を受け取る。
これにより途中でstreamの読み込みをやめた場合でも、処理の最終状態を独立して扱える。
WASI 0.3はreadiness型ではなくcompletion型
ここはOS内部が好きなエンジニアにはかなり面白い。
LinuxのepollやBSD系のkqueueは、基本的に「読み書き可能になった」というreadinessを通知する。
epoll
ready?
↓
YES
↓
read()
一方、WASI 0.3のComponent Model asyncはcompletionベースで設計されている。
operation submit
↓
runtime
↓
operation completed
↓
future resolved
考え方としてはLinuxのio_uringやWindows IOCPに近い。
これは偶然ではない。
高性能サーバーランタイムを考えた場合、Componentの外側でI/Oスケジューリングを統合できる方が効率的だからだ。
最大の変化は「1個のevent loopを共有できる」こと
WASI 0.2では、複数Componentがそれぞれ非同期ランタイムを持つ場合、互いのスケジューラを自然に協調させることが難しかった。
Component A
└─ Event Loop A
Component B
└─ Event Loop B
Component C
└─ Event Loop C
WASI 0.3ではホストランタイム側がスケジューリングを担当する。
Wasmtime
│
Shared Scheduler
/ | \
/ | \
Component A Component B Component C
AがFutureをBへ渡す。
BがCへ渡す。
値が準備できればランタイムが待機中のタスクを再開する。
Component自身が複雑なpoll loopを中継する必要がない。
そして「Service Chaining」が可能になる
ここからが今回一番面白い部分である。
WASI 0.3のwasi:httpには、serviceとmiddlewareというworldがある。
world service {
import client;
export handler;
}
world middleware {
include service;
import handler;
}
middlewareはhandlerをimportし、自分自身もhandlerをexportできる。
つまり、次のような連鎖を作れる。
Incoming Request
↓
Auth Component
↓
Logging Component
↓
Business Component
↓
Compression Component
↓
Response
見た目はマイクロサービスチェーンに近い。
しかし必ずしも各Component間をHTTP通信する必要はない。
ランタイムが同一プロセス内へComponentをcomposeすれば、Component境界を直接越えられる。
HTTPマイクロサービスでは何が起きているか
同じマシンのlocalhostへHTTPを投げるだけでも、内部ではかなりの処理が発生する。
Application A
↓
JSON Serialize
↓
HTTP Encode
↓
Socket
↓
TCP/IP Stack
↓
Kernel
↓
TCP/IP Stack
↓
Socket
↓
HTTP Parse
↓
JSON Deserialize
↓
Application B
HTTP/2やgRPCを使っても、ネットワークプロトコルとしての処理がなくなるわけではない。
別ホスト間通信なら当然必要だ。
しかし同一ホスト内で極めて頻繁に呼び合うサービスでも、本当に全部必要だろうか。
Wasm Componentなら経路が変わる
Component A
↓
Canonical ABI
↓
Wasmtime
↓
Canonical ABI
↓
Component B
ネットワークスタックを通さない。
それでも、単なる関数ライブラリとは違う。
WITによって明示的なComponent境界が存在する。
ここが重要である。
モノリスの速度と、マイクロサービスの分離性の中間地点を作れる可能性がある。
では本当に動かしてみる
2026年8月時点ではWASI 0.3の仕様自体は正式化しているが、各言語ツールチェーンは移行途中にある。
特にRustのwasm32-wasip3ターゲットは現時点でTier 3であり、通常のrustupから完成済みstandard libraryを取得する形にはなっていない。
そのためBytecode Alliance公式チュートリアルでも、現在はwasm32-wasip2を土台に、wit-bindgenのasync機能を使ってWASI 0.3 Componentを作る方法が紹介されている。
これは「WASI 0.3が未完成」という意味ではない。
仕様とランタイムが先行し、各言語のコンパイラ・標準ライブラリ・binding generatorが追随している時期なのである。
必要なツール
Rust nightly
Wasmtime
wit-bindgen
wkg 0.15+
まずRustを準備する。
rustup toolchain install nightly
rustup target add wasm32-wasip2 --toolchain nightly
Wasmtimeのバージョンを確認する。
wasmtime --version
WASI 0.3を明示的に有効にして実行できるWasmtimeでは、次のオプションを利用できる。
-Sp3
-W component-model-async=y
最初のWASI 0.3 Componentを作る
cargo new --lib wasi03-demo
cd wasi03-demo
cargo add wit-bindgen --features async
Cargo.tomlへcdylibを追加する。
[lib]
crate-type = ["cdylib"]
次にWITを作る。
mkdir -p wit
wit/component.wit
package yuus:wasi03-demo;
interface greet {
greet: func(name: string) -> string;
}
world greeter {
export greet;
// 現在のRust公式チュートリアルでは
// toolchain互換性のためRC snapshotを
// 指定する場合がある。
export wasi:cli/run@0.3.0-rc-2026-03-15;
}
WASIの依存WITを取得する。
wkg wit fetch
すると概ね次の構造になる。
wasi03-demo/
├── Cargo.toml
├── src/
│ └── lib.rs
└── wit/
├── component.wit
└── deps/
└── ...
Rust側を実装する
src/lib.rs
mod bindings {
use super::Component;
wit_bindgen::generate!();
export!(Component);
}
struct Component;
impl bindings::exports::yuus::wasi03_demo::greet::Guest
for Component
{
fn greet(name: String) -> String {
format!("Hello {name} from WASI 0.3")
}
}
impl bindings::exports::wasi::cli::run::Guest
for Component
{
async fn run() -> Result<(), ()> {
eprintln!("Hello from async WASI 0.3");
Ok(())
}
}
ここで注目したいのは、
async fn run()
である。
これは単にRust側でasyncにしているだけではない。
元のWITが、
run: async func() -> result;
だから、wit-bindgenがRustの自然なasync fnへ変換している。
JavaScript用bindingならPromise。
Pythonならcoroutine。
Goならgoroutineとランタイムを組み合わせた形にできる。
WITは言語固有async構文の上位にいるわけだ。
ビルドする
cargo +nightly build \
--target=wasm32-wasip2 \
--release
生成物は概ね次の場所にできる。
target/wasm32-wasip2/release/wasi03_demo.wasm
Wasmtimeで実行する
wasmtime run \
-Sp3 \
-W component-model-async=y \
target/wasm32-wasip2/release/wasi03_demo.wasm
正常に動けば、
Hello from async WASI 0.3
と表示される。
新しいWasmtimeではWASI 0.3とComponent Model Asyncがデフォルト有効になっているため、バージョンによっては明示オプションなしでも実行できる。
重要:これはCore Wasm Moduleではない
従来のWebAssemblyだけを知っていると、生成されたwasmを全部同じものだと思いやすい。
しかしComponent Modelでは意味が違う。
Core Wasm Module
vs
WebAssembly Component
ComponentにはWITに基づいた型付きinterfaceがある。
wasm-toolsを利用すると構造を確認できる。
wasm-tools component wit \
target/wasm32-wasip2/release/wasi03_demo.wasm
低水準のi32/i64しかない世界から、stringやrecord、resultなどを持ったinterfaceが見える。
次に自前のasync interfaceを書く
ここからWASI 0.3らしくしていく。
例えば在庫照会を非同期関数として定義する。
package yuus:inventory;
interface inventory {
record request {
product-code: string,
}
record response {
product-code: string,
quantity: s64,
}
lookup: async func(
request: request
) -> result<response, string>;
}
world inventory-service {
export inventory;
}
このinterfaceだけを見れば、HTTP APIのOpenAPI定義に少し似ている。
しかしネットワークAPIとは限らない。
同一Wasmtimeプロセス内に存在するComponentを直接呼び出すこともできる。
WITは「ネットワークに依存しないAPI定義」と考えると面白い
現在のAPI設計は、インターフェースと通信方式が密接に結びついていることが多い。
REST
↓
HTTP + JSON
gRPC
↓
HTTP/2 + Protobuf
WITの場合、インターフェースと配置方法をある程度分離できる。
WIT Interface
│
├── Same Process
│
├── Different Component
│
└── Host-provided Capability
そのため開発者は「どのHTTP endpointを呼ぶか」より、「どのCapabilityをimportするか」を考える設計へ移れる可能性がある。
マイクロサービスの最大のコストはコードではない
マイクロサービスの問題は、関数呼び出しの速度だけではない。
ネットワークを境界にすると、分散システム特有の問題が一気に増える。
Service A → Service B
Bは生きているか?
DNSは引けるか?
接続できるか?
Timeoutはいくつか?
Retryしてよい処理か?
二重実行されないか?
Circuit Breakerは必要か?
Trace IDは伝播したか?
TLS証明書は正常か?
JSON schemaは一致するか?
同一プロセスComponentでは、このうち相当数が不要になる。
もちろんプロセス障害の分離など、ネットワーク分離を失うことで生じるトレードオフもある。
したがってWASI 0.3は「マイクロサービスを全部やめよう」という技術ではない。
もっと面白い。
今までネットワーク境界にするしかなかったサービス境界を、ネットワークなしでも維持できる選択肢を増やした。
本当に速いのか?ここは自分で測る
Bytecode AllianceはWASI 0.3の発表で、頻繁に相互通信するマイクロサービスを同一プロセス内でcomposeした場合、ネットワーク経由のミリ秒級からナノ秒級へ短縮できる可能性を説明している。
ただし、これは全システムで必ず100万倍になるという意味ではない。
処理内容、Component境界で変換するデータ量、runtime、CPU、キャッシュ、メモリアロケーションなどで結果は変わる。
そこで実験では少なくとも次の4種類を比較したい。
1. Native function call
2. WebAssembly Component call
3. Unix Domain Socket
4. localhost HTTP
重要なのは平均値だけではない。
mean
median
p50
p95
p99
p99.9
requests/sec
CPU cycles
context switches
syscalls
RSS
まで見る。
簡単なベンチマーク関数
例えばComponent側には、あえてほとんど何もしない関数を用意する。
package yuus:bench;
interface ping {
ping: func(value: u64) -> u64;
}
world benchmark {
export ping;
}
Rust実装。
impl bindings::exports::yuus::bench::ping::Guest
for Component
{
fn ping(value: u64) -> u64 {
value + 1
}
}
100万回程度呼び出し、総時間から1回あたりの境界コストを見る。
const ITERATIONS: u64 = 1_000_000;
let start = Instant::now();
for i in 0..ITERATIONS {
black_box(call_component(i));
}
let elapsed = start.elapsed();
println!(
"{} ns/call",
elapsed.as_nanos() / ITERATIONS as u128
);
ここで注意したい。
この単純なexport呼び出しベンチマークは、WASI 0.3のHTTP service chainingそのものを完全再現するものではない。
Component Model境界の基礎コストを切り出して確認するための実験である。
この区別をしないと「WASIはHTTPより1000倍速かった」という雑な記事になってしまう。
Linuxならperfとstraceまで見る
速度の理由を理解するならwall clockだけでは足りない。
perf stat ./benchmark
確認したい項目は、例えば次になる。
cycles
instructions
branches
branch-misses
cache-references
cache-misses
context-switches
cpu-migrations
page-faults
さらにHTTP版では、
strace -c ./http-benchmark
を使ってsyscall回数を見る。
socket、send、recv、epoll、read、writeなどが見えてくる。
一方、Component呼び出しではネットワーク系syscallを大きく削減できる可能性がある。
「速い」だけではない。デプロイ構成が面白い
ここまでなら「高速RPC」の話に見える。
しかしComponent Modelの面白さは配置を変えられる点にある。
例えば最初は一つのWasmtimeプロセス内に全部配置する。
Process A
Inventory
Pricing
Order
Accounting
負荷が増えたらOrderだけ別の実行単位へ分離する。
Process A
Inventory
Pricing
Accounting
↓ network
Process B
Order
インターフェース自体はWITとして維持する。
この思想が成熟すると、開発者が最初から「これはマイクロサービスかモノリスか」を決め切らなくてもよくなる可能性がある。
「モジュラーモノリス」がさらに進化する可能性
近年、何でもマイクロサービスに分割する設計への反省から、Modular Monolithが再評価されている。
同一プロセスで動かしながら内部を明確なモジュール境界で分離する設計だ。
Wasm Component Modelは、この考え方と非常に相性がいい。
Modular Monolith
Module A
Module B
Module C
↓
Component Monolith
Wasm Component A
Wasm Component B
Wasm Component C
さらにWITによる言語非依存interfaceがある。
つまりComponent AをRust、BをGo、CをJavaScriptにすることさえ理論上可能だ。
これは「Polyglot Programming」の形も変える
現在、複数言語を一つのシステムで使う場合、多くはサービスをネットワークで分離する。
PHP
↓ HTTP
Python
↓ HTTP
Rust
理由の一つはFFIが難しいからだ。
C ABIを共通境界として利用すると、メモリ管理、string表現、ownership、exceptionなど様々な問題が出てくる。
Component ModelはそこへCanonical ABIを置く。
Rust
↓
Canonical ABI
↓
WIT Interface
↓
Canonical ABI
↓
Go
異なる言語のライブラリを、HTTPサーバーへ変換せずに組み合わせられる世界が見えてくる。
例えば製造業システムならどうなるか
少し現実的な例を考える。
販売管理・生産管理システムには様々な計算ロジックがある。
受注
↓
在庫引当
↓
原価計算
↓
生産計画
↓
納期計算
↓
請求
現在なら一つのPHPアプリケーションへ全部入れるか、別APIサービスへ分割することが多い。
Component Modelが成熟すれば、例えば重い生産計画計算だけRust Componentとして切り出す構成が考えられる。
PHP Application
↓
WIT
↓
Rust Scheduling Component
Pythonで需要予測モデルを書いた場合も、
Rust Component
↓
Python Forecast Component
↓
PHP Business Logic
のような構成が将来的には考えられる。
全部をHTTP API化する必要がなくなる可能性がある。
Capability Modelも大きな利点になる
Componentへ何でもOS権限を与える必要はない。
例えば価格計算Componentなら、必要なのは「価格を計算する」ための入力だけかもしれない。
Pricing Component
Filesystem ×
Network ×
Environment ×
Database ×
pricing interface ○
データベースへ直接接続させず、Repository Componentから必要なデータだけ渡す設計もできる。
Database
↓
Repository Component
↓ typed data
Pricing Component
単なる高速化ではなく、ソフトウェア設計上の境界そのものを明確にできる。
Componentを差し替えられる世界
WIT interfaceが同じなら、実装Componentを変更できる。
pricing.wit
calculate(...)
↑
Rust v1
Rust v2
Go implementation
Test Mock
Experimental implementation
これはDependency Injectionを言語やプロセスの外まで拡張したような考え方でもある。
A/Bテストにも使える
例えば新しい在庫最適化アルゴリズムを試す。
inventory-optimizer.wit
┌── Rust Algorithm A
Request ─┤
└── Rust Algorithm B
同じinterfaceなので呼び出し側は変えない。
RuntimeやHost側でどのComponentをcomposeするか変更する。
これが一般化すると、Component単位のアップグレードや実験が非常にやりやすくなる。
ではDockerやKubernetesは不要になるのか
ならない。
少なくとも現時点で、そのように考えるのは早い。
Kubernetesが解決しているのはComponent間通信だけではない。
Scheduling
Replication
Health Check
Autoscaling
Rollout
Networking
Persistent Volume
Node Management
Failure Isolation
などを扱っている。
WASI Componentが直接置き換える対象ではない。
むしろ将来的には、
Kubernetes Pod
│
└── Wasmtime
├── Component A
├── Component B
├── Component C
└── Component D
のように、Podの中身をさらにComponentで細かく構成する方が現実的かもしれない。
これは「Nano Service」ではない
ネットワークコストが小さくなるからといって、何でも細かくComponentへ分割すればよいわけではない。
境界が増えれば型変換や管理コストも増える。
重要なのは「分割コストが下がる」ことであって、「無限に分割すべき」ということではない。
WASI 0.3で一番重要なのはasyncではない
ここまで調べると面白いことが分かる。
WASI 0.3最大の変更点は確かにNative Asyncである。
しかし、その目的はasync/awaitという構文を使いたかったからではない。
複数Componentを効率よくcomposeするために必要だった。
Native Async
↓
Cross-component Future
↓
Cross-component Stream
↓
Shared Scheduling
↓
Efficient Composition
↓
Service Chaining
この流れで理解すると、WASI 0.3の意味がかなり違って見える。
そして次はWASI 1.0へ向かう
WASI 0.3は最終地点ではない。
Component ModelとWASIの標準化は、最終的により安定した1.0へ向かって進んでいる。
現在は非常に面白い時期だ。
仕様だけが存在する研究段階ではない。
Wasmtimeで実際に動かせる。
しかし、Rust、Go、Python、JavaScript、C#など各言語の対応はまだ進化している最中だ。
完成後の記事を読むより、今触った方が仕組みがよく見える。
数年後のバックエンドはこうなるかもしれない
Internet
↓
HTTP Gateway
↓
Wasm Runtime
│
├── Auth Component
│
├── Routing Component
│
├── Inventory Component
│
├── Pricing Component
│
├── Order Component
│
└── Logging Component
↓
Database
外との通信にはHTTPを使う。
しかし内部処理では、本当に必要なところまでHTTPを使わない。
サービスは分離する。
言語も分離できる。
Capabilityも分離する。
それでも同一プロセスで高速に動かせる。
マイクロサービスかモノリスか、という議論そのものが古くなるかもしれない
これまでソフトウェアアーキテクチャでは、モノリスとマイクロサービスが対立するように語られることが多かった。
Monolith
高速
単純
しかし密結合
Microservices
疎結合
独立デプロイ
しかし分散システム化
Component Modelは第三の選択肢を作ろうとしているように見える。
Component Architecture
型付き境界
言語非依存
Capability分離
同一プロセス配置可能
必要なら別配置可能
もちろん、すぐに現在のWebバックエンドが全部WebAssemblyになるわけではない。
デバッグ、observability、ライブラリエコシステム、言語対応、運用ツールなど、成熟すべき領域はまだ多い。
それでも、WASI 0.3によって重要なピースが一つ埋まった。
非同期処理である。
ネットワーク、ファイル、HTTP、ストリーム。
現代のサーバーアプリケーションが必要とする処理を、Component境界を越えて効率よく扱うための共通言語ができた。
まとめ
WASI 0.3を「WebAssemblyにasyncが追加された」とだけ覚えるのはもったいない。
本当に面白いのは、その先だ。
async func
↓
future / stream
↓
runtime-controlled scheduling
↓
cross-component async
↓
component composition
↓
service chaining
↓
network-less service boundary
これまで「サービスを分離する」と「ネットワーク越しにする」は、ほとんどセットだった。
Wasm Component Modelは、その二つを切り離そうとしている。
そしてこれは単なる高速化ではない。
Rustで作った高速処理。
Pythonで作った分析処理。
Goで作ったネットワーク処理。
JavaScriptで作ったロジック。
それらが共通のWIT interfaceを通して、一つのアプリケーションを構成する。
ネットワークが必要ならネットワークを使う。
必要なければ使わない。
配置方法とソフトウェア境界を別々に考えられる。
WASI 0.3が変えようとしているのはWebAssemblyの書き方ではない。
ソフトウェアを「どこで分割するか」という設計そのものかもしれない。
2026年現在、WASI 0.3は完成された未来ではない。
仕様が正式化し、ランタイムが対応し、各言語ツールチェーンが追いつき始めた段階だ。
だからこそ今が面白い。
数年後に当たり前になってから使い方を覚えるのではなく、今なら「なぜこの仕組みが必要になったのか」という設計思想から追いかけられる。
次回の実験
次は実際に複数Componentを作り、次の4経路を同じ処理で比較する。
Native Rust function
vs
Wasm Component boundary
vs
Unix Domain Socket
vs
localhost HTTP
単なる平均応答時間だけではなく、p50、p95、p99、CPU cycles、context switch、syscall数、cache missまで測定する。
「HTTPを使わなければ速い」という当たり前の結論では終わらせない。
Component Modelの境界では実際にどれだけコストが発生するのか。
Canonical ABIの型変換はどこまで重いのか。
payloadサイズを増やすとどこで逆転するのか。
そしてWASI 0.3のService Chainingは、現実のバックエンド設計でどこまで使えるのか。
そこまで実測してみたい。
参考資料
本記事の仕様確認には、WebAssembly/WASI公式WASI 0.3.0 Release Notes、Bytecode Alliance「WASI 0.3 Launched」、WebAssembly Component Model公式ドキュメント「Native Async with WASI 0.3」「WIT Reference」「Migrating from WASI 0.2 to WASI 0.3」「Creating Runnable Components (Rust)」、Wasmtime公式Component Modelドキュメントを参照した。