Zigによる静的メモリ割り当て
108日前原文(nickmonad.blog)
概要
- ZigでのRedis互換キーバリューサーバ「kv」開発記録
- 静的メモリ割り当てによる安定性・保守性向上の実践
- 接続管理・コマンド解析・ストレージ各段階の設計詳細
- 設定によるリソース上限の明確化とトレードオフ
- Zigのメモリアロケータ設計活用例
Zigによる静的メモリ割り当て型キーバリューサーバ設計
- ZigでRedis互換の小型キーバリューサーバ「kv」開発
- 目標は本番運用可能な最小限コマンド実装
- TigerBeetleや「TigerStyle」ガイドラインに影響を受けた設計思想
- 初期化時に全メモリ確保、実行中の動的割り当て・解放禁止
- パフォーマンスの予測可能性・安定性、設計の単純化・保守性向上
静的メモリ割り当ての課題と利点
- システム設計時に「どれだけメモリを確保するか」を明確化
- 最大接続数・各接続のバッファサイズ・想定データ量などを事前設定
- 設計時に全リソース要求量を洗い出すことで、プログラム理解が深まる
- Zigは明示的なメモリアロケーションと多様なAllocatorインターフェースで設計容易
接続管理
- Connection構造体で、各クライアントとの通信情報を保持
- io_uring対応のため、リクエストライフサイクル中に必要な情報を管理
- 初期化時に以下の3つのプールを確保
- Connection構造体用
- 受信バッファ用(recv_buffer)
- 送信バッファ用(send_buffer)
- 各プールはstd.heap.MemoryPoolやカスタムByteArrayPoolで実装
- ランタイムではプールから切り出し・解放するだけで動的確保不要
- 最大接続数・各接続のバッファサイズはConfig構造体で設定
- プール枯渇時はリクエスト拒否、サーバの安定性・予測性向上
- 設定例:1000接続程度が現実的、用途に応じて調整可能
コマンド解析
- Redis互換のため、RESPフォーマットのコマンド解析を実装
- 受信バッファをイテレータで走査し、CRLF区切りで分割
- parse関数でバッファを解析し、コマンド内容を抽出
- 解析時の一時的なメモリ管理にFixedBufferAllocatorを活用
- 初期化時にバッファ確保、リクエスト毎にresetで再利用
- シングルスレッド処理なら一つのAllocatorを使い回し可能
- 最大コマンド長・最大レスポンス長を考慮してバッファサイズを設定
- 解析時は「ゼロコピー」方式で効率化、必要最小限のコピーのみ
- コマンド実行後、レスポンスデータはsend_bufferへコピー
キーバリューストレージ
- 基本構造はハッシュマップ(std.StringHashMapUnmanaged)
- Zigの「unmanaged」バージョンを利用し、初期化時に容量確保
- 実行時はputAssumeCapacityで追加、追加時の動的確保不要
- 初期化時に容量不足時はエラーで即停止、実行時の予期せぬOutOfMemoryを回避
- 最大キー数・最大バリューサイズ等もConfigで事前設定
設計・運用上のトレードオフ
- 静的割り当ては柔軟性に欠けるが、堅牢性・保守性向上
- 設計段階でリソース上限を明確化することで、システム全体の安定運用
- ZigのAllocator設計や型安全性が、このような設計に非常に適している
- 本実装は学習目的であり、さらなる最適化や設計改善の余地あり
これらの設計思想・実装例は、Zigやシステムプログラミングの学習や堅牢なサーバ設計に役立つ知見と言える。