kevy4.0

IoTデバイス上のkevy

kevy-embeddedは、サーバーがスケールアップするのと同じだけ意図的にスケールダウンします。フィーチャで階層化されたビルドは、およそ411 KBのインメモリKVコアから、インデックスとレプリケーションを含む全面までを覆います。Linux級のIoTボード(Pi Zero 2、OpenWrtルーター、産業用ARM)向けにはmuslのクロスビルドがあり、no_stdコアはベアメタルのCortex-Mターゲット上で——コンパイルが通ることを確認しただけでなく——実際にブートし、動かされています。リソース予算はREADMEの約束ではなく、ゲートによって強制されます。

フィーチャの階層

kevy-embeddedはサブシステムごとにCargoフィーチャを1つ公開します。デフォルトは全部入りです。IoT向けのビルドはcoreから始めて、デバイスが実際にやることだけを足していきます。

フィーチャ何が加わるか引き込むもの
coreKV + TTL + カウンタ + pub/sub + atomic/pipelineのファサード(基底の面。実務上は常にオン。最小のアーキタイプを綴れるように名前が付いている)
persistスナップショット + AOFの耐久性。Config::with_persistsave_snapshotrewrite_aof、open時のAOFリプレイkevy-persist
index宣言型のセカンダリインデックス + materializedビューkevy-index
text全文(BM25)インデックスのセグメントindex + kevy-text
vectorANN / HNSWのベクトルインデックスセグメントindex + kevy-vector
replicate組み込みをレプリカにする、組み込みをライターにする、そしてCDCフィードpersist + kevy-replicate + kevy-resp
listener読み取り専用のRESPリスナー(docs/embedded-listener.md

階層間の依存関係はフィーチャ自身に符号化されています。textvectorindexを含意し、replicatepersistを含意します(レプリケートされたフレームはAOFのverbテーブルを通ってリプレイされるためです)。外したものは、リンカが落とす死んだコードになるのではありません——その背後にあるcrateは、そもそもビルドグラフに一切入りません。

6つのアーキタイプが、pushのたびにCIでコンパイルゲートされます。ワークスペースが動いても、それぞれの組み合わせがビルドされ続けるようにするためです。

core                        # sensor cache: RAM-only KV + TTL
core,persist                # + survives power cycles
core,index                  # + declared indexes / views
core,index,text,vector      # + search (BM25, HNSW)
core,persist,replicate      # + edge node feeding a hub
core,listener               # + redis-cli-able diagnostics port

Cargo.tomlではこう書きます。

[dependencies]
kevy-embedded = { version = "4", default-features = false, features = ["core"] }

APIの顔はどの階層でも同じです。Store::open(Config)KevyResult / KevyErrorのエラー、書き込みメソッドの借用スライスargv。coreに対して書いたコードは、デバイスがのちにpersistを得ても、そのまま再コンパイルできます。

リソース予算(願望ではなく、ゲート済み)

予算のラインは2本あり、bench/iotgate.shがラチェットとして強制します。どちらの数値も、引き上げるには文書化された裁定が必要です。

予算ライン実測
バイナリサイズ(coreのコンシューマ、静的musl)600 KB以下454 KB(x86_64)・411 KB(aarch64)・383 KB(armv7)
open直後の空ストアのRSS(Linux)2 MB以下336 KB(aarch64)

これらはbench/iot-consumer上で計測しています——ワークスペースの外側に意図的に置いた、唯一の依存がkevy-embeddedであるcrateです。それこそが、あなたが出荷する形です。このゲートの以前のバージョンはワークスペースの--exampleを測っていました。exampleのビルドはdev-dependencies(kevyサーバーcrate)を引き込むため、報告されるサイズがおよそ60%も水増しされていました。

iot cargoプロファイルはワークスペースのルートで定義されています——releaseのcodegenに、サイズを最優先させたものです。

[profile.iot]
inherits = "release"
opt-level = "z"     # size over speed
lto = true          # fat LTO: the linker drops unused subsystems
codegen-units = 1
strip = true

サイズの数値を再現するには、こうします。

cargo build --profile iot -p kevy-embedded --example iot_core \
  --no-default-features --features core
ls -l target/iot/examples/iot_core

センサーキャッシュの全体像

iot_coreのexampleは、たいていのデバイスが必要とする形です。TTL付きのインメモリKVと、手動で駆動する期限切れのsweepからなります(頼まない限り、バックグラウンドスレッドは存在しません)。

use kevy_embedded::{Config, Store};

fn main() -> kevy_embedded::KevyResult<()> {
    // Manual reaper: no thread is spawned; the device's own loop
    // drives expiry at whatever cadence it already runs.
    let s = Store::open(Config::default().with_ttl_reaper_manual())?;

    s.set(b"sensor:1", b"22.5")?;
    s.set(b"sensor:2", b"3.3")?;
    s.expire(b"sensor:2", core::time::Duration::from_secs(60))?;

    // Call from the main loop / timer ISR bottom half:
    let _expired = s.tick();
    Ok(())
}

スレッドが安く済むボードでは、リーパーはデフォルト(バックグラウンドスレッド)のままにしてください。ループを自分で持っているならwith_ttl_reaper_manual() + tick()を取ってください。TTLの精度はtickの刻みに追随します。

クロスコンパイル:muslとCIマトリクス

静的muslのバイナリは、Linux級IoTにおけるデプロイの通貨です。ファイル1つ、glibcとの結合なし、scpして走らせるだけ。

プラットフォームのカバレッジ——実際に走らせたもの

以下の行はどれも、コンパイルを確認しただけではなく、ビルドし、そして実行しました。core階層のコンシューマを、そのアーキテクチャの実カーネル上で走らせています。サイズは静的muslの成果物のものです。

ターゲットボードの分類サイズ状態
x86_64-unknown-linux-musl産業用x86、ゲートウェイ454 KB実行済み
aarch64-unknown-linux-muslPi Zero 2 / Pi 4-5 / 多くのARM64 SBC411 KB実行済み——ネイティブに。空ストアRSS 336 KB、2.86Mストアops/s
armv7-unknown-linux-musleabihfPi 2-3、旧来の32ビットARM383 KB実行済み(エミュレート)
arm-unknown-linux-musleabihfPi Zero / Pi 1(ARMv6)411 KB実行済み(エミュレート)
riscv64gc-unknown-linux-muslRISC-V SBC420 KB実行済み(エミュレート)——リンカとしてRISC-Vのクロスgccが必要(ターゲット仕様がlibgcc_sを要求し、rust-lldはそれを供給できない)。加えてcrt-staticも要る
thumbv7em-none-eabihfCortex-M4/M7 MCUファームウェア145 KBベアメタル上で実行済み——kevy-storeのみ。kevy-embeddedの全面ではない(そちらはstdを要します)。後述。

信頼できる性能値はaarch64の行です。この行はARM64上でネイティブに走りました。armv7/ARMv6の行は、コードが32ビットARM上で正しいことを証明していますが、その時間とRSSはエミュレータのオーバーヘッドを含んでおり、デバイスの数値として読むべきではありません。

出荷対象のターゲットはすべてCIでコンパイルゲートされ(pushのたびに、単独のコンシューマが各ターゲット向けにビルドされます)、Tier Aのターゲットについてはフルのデフォルト面も検査されます。

rustup target add aarch64-unknown-linux-musl
cargo build --profile iot --target aarch64-unknown-linux-musl -p kevy-embedded

rustup target add armv7-unknown-linux-musleabihf
cargo build --profile iot --target armv7-unknown-linux-musleabihf -p kevy-embedded

kevyのゼロ依存の法が、ここで報われます。クロスコンパイルすべきCライブラリはなく、-sys crateの動物園もありません——OSの境界はkevy自身のkevy-sysだけであり、coreより下の組み込みクロージャは、それすら含みません。

コンパイルの証明より先を見たいときは、エミュレーション下でテストスイートを走らせるのも1行です。

CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUNNER=qemu-aarch64 \
  cargo test -p kevy-embedded --target aarch64-unknown-linux-musl

no_stdコア

Linuxよりさらに下では、ストレージの石がstdなしでビルドされます。5つのcrateが、stdというデフォルトフィーチャの背後に#![no_std]のコアを抱えています——kevy-storekevy-hashkevy-byteskevy-mapkevy-madviseです。

CIは、それらをCortex-M向けにコンパイルするだけではありません。その上でブートさせますbench/mcu-probeはベアメタルのファームウェアであり——手書きのベクタテーブル、bumpアロケータ、パニックハンドラ、semihostingのI/Oを備えています。kevyは依存を取らないので、cortex-m-rtembedded-allocに手を伸ばせないからです——これがQEMU(mps2-an386、Cortex-M4)上で本物のStoreを立ち上げ、KVとTTLの面を動かします。pushのたびに走ります。MCU上でストアが誤動作すれば、ビルドが落ちます。

cd bench/mcu-probe && cargo run --release

そのMCU上で、64 KiBのヒープで計測した値です。

ファームウェアイメージ145 KB
空のStoreのヒープ0 B ——書き込むまで、ストアは何も確保しません
センサーキー64個分のヒープ17.8 KB
TTLファームウェアが自分の時計を進めれば、正しく期限切れになります

MCU上で走るのはkevy-storeであって、kevy-embeddedではありません。 組み込みのファサード(Config、バックグラウンドのリーパー、永続化、リスナー)はstdを必要とします。MCUが手にするのは、その下にあるストレージエンジンを、直接駆動したものです。

no_stdカットが実務上意味するのは、次のことです。

1つだけ、知っておく価値のある細部があります。ほかの場所なら黙って壊れる類のものだからです。ホスト供給の時計は64ビットのセル1つであり、アトミックに読めなければなりません。64ビットアトミックを持つISAでは、これはただのAtomicU64です。32ビットしかないMCU(ARMv7E-Mに64ビットアトミックはありません)では、2つのAtomicU32の半分にまたがる、単一ライターのseqlockに退化します——読み手はちぎれた読み出しを検出するとリトライし、ホストが1つのコンテキストから時計を供給する限り、リトライループは即座に落ち着きます。複数のコンテキストから並行して時計を供給することは、契約の外です。

サイジングの指針

使うものバイナリ差分
KV + TTL(core392 KB
+ 耐久性(スナップショット + AOF)601 KB+209 KB
+ RESPリスナー629 KB+28 KB
+ セカンダリインデックス667 KB+66 KB
+ 全文検索(BM25)691 KB+24 KB
+ ベクトルANN(HNSW)696 KB+29 KB

耐久性が、単独で群を抜いて最大の項目です。残りはその上に載る小さな増分にすぎません。空ストアのRSSも同じように動きます。coreで336 KB、全フィーチャをコンパイルすると656 KBなので、選んだ階層は、ディスク上のバイト数以上に、常駐メモリに姿を現します。

これは何ではないか

*サーバー*を小さくしようという試みは一切ありません。kevy(バイナリ)、kevy-rtkevy-uringとその仲間たちは本物のカーネルを前提としており、IoTのカットではスコープ外です。ここでの製品は組み込みライブラリのほうです——あなたのファームウェアこそが、サーバーなのです。