kevyのチューニング
kevyのopあたりのコストを左右するランタイムノブのリファレンスです。CPUレイアウト、リアクターの選択、永続化、メモリ上限、ネットワークトランスポート、そしてLinux側のいくつかのレバーを扱います。
このドキュメントが必要になるとき
次のような場面で参照してください:
- ベンチマークでkevyがスループットや遅延の目標に届いておらず、次にどのノブを回すべきか知りたい。
- デフォルト(TCPループバック、io_uring自動検出、
appendfsync everysec、maxmemoryなし)がワークロードに合わないホストにkevyをデプロイする。たとえばスパースコネクションのサービス、NVMe前提の耐久性要件、メモリ上限付きのキャッシュ層など。 perfでkevyをプロファイルしていて、デバッグ行テーブルを残すビルドプロファイルが必要。
ノートPCでkevyを立ち上げて数字に問題がないなら、このページは不要です。デフォルトはどのワークロードでもそこそこ良い性能が出るよう調整されています。
中心となる考え方
kevyはスレッド・パー・コアのサーバーです。OSスレッドごとに1シャード、CRC16ハッシュタグで分割したシェアードナッシングのキー空間、各シャードにbusy-pollリアクター、という構成です。デフォルトは「どのワークロードでもまずまず」を狙っています。チューニングとは、シャード数・リアクター・永続化ポリシーを、実際のパフォーマンスデータがボトルネックだと示しているものに合わせる作業です。先回りしてノブを切り替えないでください。計測し、コストを特定し、変数を1つずつ変えてください。
チューニングプレイブック
CPUとシャード
| ノブ | どこ | デフォルト | 効果 |
|---|---|---|---|
--threads N / KEVY_THREADS | CLI / env | オンラインコア数 | シャード数。OSスレッドごとに1シャード |
--accept-shards K | CLI | 全シャードがaccept | 最初のKシャードだけがリスナーをbindし、残りはcompute-only |
| CPU pinning | taskset / numactl | なし | シャードを固定のコア集合にロック |
--threadsの選び方。 ワークロードに実際に存在する並列性に合わせてください。シングルクライアントのパイプラインベンチ(-c 1 -P 16)は1シャードを飽和させます。ここで--threads 10にすると、9つのシャードが仕事のないbusy-pollを回し、シャード0のキャッシュラインを奪うだけです。本物のマルチクライアントなら、min(cores, 想定同時クライアント数 / 4)から始めて計測してください。
--accept-shardsの選び方。 コネクション対シャード比が低い場合(スパースコネクションのワークロード。たとえば50クライアントを10シャードで受けると5 conns/shard)、イテレーションごとのbusy-pollオーバーヘッドが償却できなくなり、スループットが落ちます。経験則はceil(conns / 20)です。50 connsなら--accept-shards 3とし、3つのlistenシャードがそれぞれ約17接続を受け持ちます。残りのシャードはcompute-onlyですが、内部ディスパッチャ経由でクロスシャードの仕事は受け続けます。経験的なスイートスポットは点推定より広めです。フルスイープの結果と、クロスシャードホップの税がaccept集中の利得を上回るケースの議論はdocs/accept-shards.mdを参照してください。
CPU pinning。 ベンチ用や単一テナントのホストでは、kevyを固定コア集合にpinすると、NIC IRQ→softirq→ユーザースレッドのパスを同じL1/L2に保てます:
taskset -c 0-9 kevy --port 6004 --threads 10同じマシン上でクライアントも動かすなら、サーバーとクライアントを互いに素なコアレンジにpinします(サーバー0-9、クライアント10-15)。コアを共有すると、リアクターの利得を吹き飛ばすスケジューラのping-pongが復活します。
リアクターの選択
| プラットフォーム | デフォルト | 上書き |
|---|---|---|
| Linux ≥ 5.19 | io_uring(自動検出) | KEVY_IO_URING=0でepollを強制。KEVY_IO_URING=1でio_uringを必須にし、seccompでio_uring_setupがブロックされていれば大きな音を立てて終了 |
| macOS / *BSD | kqueue | 設定不可 |
| 旧Linux | epoll | n/a |
Linuxの自動検出は起動時にio_uring_setupを試します。syscallがブロックされている場合(seccompプロファイル、ロックダウンされたコンテナ)、kevyは黙ってepollにフォールバックします。黙って劣化するのではなく大きな音を立てて失敗してほしい硬化デプロイでは、KEVY_IO_URING=1を設定してください。io_uringが本当に使えない限りサーバーは起動を拒否します。逆に、再現可能なepoll対io_uringベンチのため、あるいはカーネルリグレッションの回避のためにio_uringを外したい場合は、KEVY_IO_URING=0を設定します。
KEVY_IO_URING=1 kevy --port 6004 # io_uring 必須、ブロックされていれば終了
KEVY_IO_URING=0 kevy --port 6004 # epoll 強制永続化
AOFポリシーはappendfsync(configファイルまたはCONFIG SET)で制御します。3つの値はRedisのセマンティクスと一致します:
appendfsync | 耐久性 | コスト |
|---|---|---|
always | 各書き込みを応答前にfsync | 遅延は最大。NVMeのsync遅延で律速 |
everysec(デフォルト) | バックグラウンドスレッドで毎秒fsync | データロス窓は最大1秒。ホットパスのコストはほぼゼロ |
no | fsyncしない。カーネルが自分のスケジュールでflush | 最速。データロス窓はページキャッシュのflush間隔 |
everysecのバックグラウンドfsyncは、シャードのホットパスから外れた専用bioスレッドで走るため、シャードのテール遅延がディスク遅延と結合することはありません。純粋なキャッシュや読み取りレプリカでは、--no-aofでAOFを完全に無効化する(AOFファイルは一切書かれず、バッファにも積まれない)選択肢もあります。
メモリ
| ノブ | デフォルト | 何をするか |
|---|---|---|
maxmemory | 無制限 | バイト単位のハードなメモリ上限。達するとエビクションポリシーが動く |
maxmemory-policy | noeviction | 上限に達したときどのキーを落とすか |
maxmemory-samples | 5 | 近似LRU/LFUポリシーのサンプルサイズ |
エビクションポリシーはRedisをミラーしています:noeviction、allkeys-lru、allkeys-lfu、allkeys-random、volatile-lru、volatile-lfu、volatile-random、volatile-ttl。noevictionは上限到達後の書き込みをOOMで失敗させるもので、プライマリストアの安全なデフォルトです。allkeys-*系は、どのキーも使い捨てにできるキャッシュ層での正解です。
maxmemory-samplesは近似ポリシーの品質とコストを調整するダイヤルです。サンプルするキーを増やすほど真のLRU/LFUに近づきますが、エビクションごとのCPUコストが増えます。デフォルトの5はほとんどのキャッシュワークロードで十分です。アクセスパターン上、エビクションが悪い犠牲者を選んでいるのが見えるなら10に上げ、エビクション自体がプロファイルに現れるときに限って3に下げてください。
ネットワーク
デフォルトのトランスポートはTCPです。クライアントが同一ホストにいるなら、Unixドメインソケットに切り替えてループバックTCPスタックを丸ごとスキップできます:
KEVY_UNIX_SOCKET=/tmp/kevy.sock kevy --port 6004
redis-cli -s /tmp/kevy.sock SET foo barサーバーはデュアルバインドします。TCPはリモートクライアント用に上がったままで、UDSがローカルを受け持ちます。RESPセマンティクスも、シャードランタイムも同じです。ローカルクライアントのワークロードでは利得が大きくなります(小さいペイロードサイズではループバックTCPパスが支配的なコストになるためです)。詳細な数値、権限モデル、UDSが向かないケースはdocs/uds.mdを参照してください。
bindアドレスの警告。 現時点のkevyにはAUTHもTLSもありません。非ループバックアドレス(--bind 0.0.0.0や公開インタフェース)にbindすると起動時に警告が出ます。ネットワーク上の誰でもコマンドを発行できてしまうためです。kevyはプライベートネットワーク境界の内側か、認証を終端するプロキシの背後で動かしてください。
コネクションのイントロスペクション。 INFO clientsは全シャードを合算したライブのconnected_clientsゲージを報告します。CLIENT LIST/CLIENT INFOは、実在するクライアント接続ごとにRedis 7.x形の行を1つ描画します——ピアアドレス、グローバルに一意なid、name、購読数、MULTIキュー深さ、入出力バッファサイズ(cmd=NULL:最後のコマンド名は追跡しません)。CLIENT SETNAMEはLIST用に接続へラベルを付けます。CLIENT KILL ID <id> | ADDR <ip:port> | LADDR <ip:port>(またはレガシーな位置引数形CLIENT KILL <ip:port>)は、ブロッキングコマンドで待機中のものも含め、一致するすべての接続を閉じます。teardownは犠牲となる接続の保留出力がはけるのを待つので、自分自身をkillした接続も自分のリプライは受け取れます。
レプリケーションと可用性
プライマリ/レプリカのトポロジで動かすときだけ関係します(docs/replication.md、docs/availability.md)。
ポートレイアウト。 各ノードは3つのプレーンを使い、デフォルトではすべてクライアントポートから導出されます:
| プレーン | ポート | 備考 |
|---|---|---|
| クライアントRESP | port(例:6004) | クライアントとpeersのclient-portが指すもの |
| レプリケーション | listen_port_base + shard_i。デフォルトのbase = port + 10000 | nshards個の連続ポート。レプリカもこのレンジをbindする(v3.15) |
| 選挙 | elect_port_base。デフォルト = port + 200 | ノードごとに1本のコントロールプレーンリスナー |
1台のマシンに複数インスタンスを同居させる場合は、クライアントポートを最低nshards離してください。そうしないとデフォルトのレプリケーションレンジが衝突します。FAILOVERと自動再ターゲットはport + 10000の慣習を前提にしているため、フェイルオーバーを使うデプロイではlisten_port_baseをデフォルトのままにしてください。
整合性ノブ。 2つの[replication]キーで、可用性とより強い保証を交換できます(ラダーの全体像はdocs/availability.md):
| ノブ | デフォルト | 何をするか |
|---|---|---|
replica_max_staleness_ms | 0(オフ) | 最後のプライマリハートビートが境界より古いレプリカは、読み出しを-STALEで拒否する。ハートビートは1Hzでストリームに乗るため、約2秒未満の境界は健全なリンクでも発火する |
min_replicas_to_write | 0(オフ) | 健全なレプリカがN台未満のとき、プライマリは書き込みを-NOREPLICASで拒否する |
呼び出し単位のバリアなら、常設のconfigの代わりに、ブロックする呼び出し1回分のコストで済みます。プライマリではWAIT n timeout、レプリカでのread-your-writesにはREPL.TOKEN + REPL.WAITです。どちらもtimeout 0を「永遠に待つ」と解釈し、60秒でハードキャップされます。
Linuxカーネルノブ
kevyの下にあるカーネル側のフロアを動かすホストレベルのレバーが2つあります。どちらもベンチ用・単一テナント専用です。適用する前にトレードオフを読んでください。
Spectre / BHBミティゲーション。 ミティゲーションが有効(デフォルト)のLinux 6.x系カーネルでは、すべてのsyscallがclear_bhb_loopなどの代金を払います。小ペイロードの-c 1ワークロードでは、これがkevyの実行中の単独最大のCPU消費です。カーネルコマンドラインで無効化します:
# GRUB_CMDLINE_LINUX_DEFAULT に `mitigations=off` を追加、その後:
sudo update-grub && sudo reboot
cat /proc/cmdline | grep mitigationsこれが許されるのは、untrustedなコードが一切走らない単一テナントマシンだけです(ワイヤから来るLuaなし、サードパーティプラグインなし、マルチテナントコンテナなし)。マルチテナントホスト、共有CIランナー、untrustedなユーザーコードを処理するものには適用しないでください。利得は-c 1で+10〜15%程度で、ワークロードがパイプライニングするほど縮みます。
.textセグメントのヒュージページ。 kevyは自分のコードセグメントにmadvise(MADV_HUGEPAGE)を呼べます。これによりカーネルはkevyバイナリの命令を4KiBではなく2MiBページで裏付けます。利得は、ホットなdispatchループのiTLBフットプリントが小さくなることです。ランタイムコストは実質ゼロなので、/sys/kernel/mm/transparent_hugepage/enabledがalwaysかmadviseのLinuxホストでは有効にする価値があります。トレードオフは起動時のmadvise呼び出し1回分の小さなコストだけで、mitigations=offと違ってセキュリティ上の代償はありません。
プロファイリング
実際のシンボルに解決されるperf recordのフレームグラフを取るには、release-perfプロファイルでビルドしてください。最適化レベルはreleaseと同じで、デバッグ行テーブルだけが残ります:
cargo build --profile release-perf
./target/release-perf/kevy --port 6004 --threads 1 &
KEVY_PID=$!
perf record -F 999 -p $KEVY_PID -g --call-graph=fp -- sleep 30
perf report --stdio | head -60
# インライン展開シンボルの生アドレスを解決:
addr2line -e ./target/release-perf/kevy -f -i 0x<addr>標準のreleaseプロファイルは行テーブルをstripするため、perfはシンボルのない生アドレスを返し、addr2lineは??を返します。releaseバイナリをプロファイルしないでください。まずrelease-perfでリビルドしてください。
clear_bhb_loopなどカーネル側のコストをシンボル単位で帰属させるには、fpの代わりに--call-graph=dwarfでキャプチャし、同じaddr2lineのフローを使ってください。dwarfのアンワインダは遅いですが、syscall境界を越えて正しく展開します。
トレードオフ
| ノブ | コスト | 買えるもの |
|---|---|---|
--threads N(上げる) | Nがワークロードの並列性を超えると、遊休busy-pollシャードにCPUを浪費 | 同時クライアント容量の増加 |
--threads N(下げる) | クロスシャードホップの税を1シャード分回避 | スパースコネクションでのCPU浪費の削減 |
--accept-shards K | リスナーの集中。クライアントが生のconnectをするならエントリポイントが減る | acceptする各シャード上で、イテレーションごとのオーバーヘッドがより多くのコネクションに償却される |
KEVY_IO_URING=1(強制) | seccompがio_uringをブロックしているとサーバーが起動拒否 | 硬化ホストでepollへ黙って劣化しない |
KEVY_IO_URING=0(epoll強制) | io_uringのopあたりの節約を諦める | 再現可能なepollベースライン。カーネルリグレッションの回避 |
appendfsync always | 全書き込みがfsyncでブロック | データロスゼロの耐久性 |
appendfsync no | データロス窓 = ページキャッシュのflush間隔 | 最速の書き込みパス |
--no-aof | 永続化なし | ディスクI/Oの最小化。レプリカ/キャッシュ用途 |
maxmemory設定 | 書き込みが失敗(noeviction)またはエビクト(allkeys-*)し得る | メモリフットプリントの有界化 |
maxmemory-samplesを上げる | エビクションごとのCPUコスト | 近似LRU/LFUの犠牲者選択が改善 |
| Unixドメインソケット | ローカル限定。ファイルシステム権限のセキュリティモデル | TCPループバックスタックをスキップ |
replica_max_staleness_ms設定 | 遅れているレプリカでの読み出しは追いつくまで失敗(-STALE) | 読み出しステイルネスの有界化 |
min_replicas_to_write設定 | 書き込み可用性がレプリカの健全性と結合(-NOREPLICAS) | 虚空への書き込みをしない |
mitigations=off | Spectre / Meltdown / MDSなどのミティゲーションが全オフ | syscallパスの税を取り戻す |
.textへのMADV_HUGEPAGE | 意味のあるコストなし | dispatchループのiTLBフットプリント縮小 |
release-perfビルド | バイナリが大きくなる(デバッグ行テーブル) | perfがシンボル解決できる |
FAQ
--accept-shardsは常に設定すべきですか?
いいえ。このノブは、conns/shardsが低くbusy-pollの本体が償却できないスパースコネクション向けです。デンスコネクション(たとえば1000クライアント×10シャード = 100 conns/shard)では、デフォルトの「全シャードがacceptする」が正解です。リスナーを均等に広げたほうがaccept側の競合が減るからです。ceil(conns / 20)は、実際にスパースコネクションのケースになっているときだけ適用してください。
io_uringは常にepollより速いですか?
Linux ≥ 5.19で投入(submission)をバッチできるワークロードなら、はい、目に見えて速いです。古いカーネル、io_uring_setupをブロックするseccompフィルタ、バッチ機会がなくopごとに1 syscallに支配されるワークロードでは差が縮みます。自動検出が正しいデフォルトです。上書きするのは、実測に基づく理由があるか、黙ってフォールバックせず大きな音を立てて失敗すべき硬化デプロイの場合だけにしてください。
appendfsyncの本番でのスイートスポットは?
ほぼ全員にとってeverysecです。データロスを1秒に有界化し、fsyncをホットパスから外し、テール遅延への影響はほぼゼロです。alwaysを使うのは、耐久性の要件が本当にゼロデータロスを求めるときだけです(その場合、NVMeのfsync遅延がテール遅延を律速するのは受け入れることになります)。noは、AOFがウォームリスタートの高速化のためだけに存在する純粋キャッシュに限ってください。
MADV_HUGEPAGEはいつ必要ですか?
perfがホットなdispatchループ上のiTLBミスを示すとき、あるいはホストの/sys/kernel/mm/transparent_hugepage/enabledがmadviseのとき(この場合、ほかにkevyをオプトインさせるものがありません)です。THPが有効なLinuxホストではコストのかからないノブなので、デフォルトの姿勢は「オンのままにしておく」です。macOS / BSDに等価物はありません。
perfレポートが生アドレスばかりです。何を間違えましたか?
cargo build --releaseのバイナリをプロファイルしています。標準のreleaseプロファイルはデバッグ行テーブルをstripするため、perfにもaddr2lineにも解決の手がかりがありません。cargo build --profile release-perfでリビルドして録り直してください。