[[Concurrency - eBPF Docs]]より、次の5つの制御方法がある。
- Atomic operations
- Spin locks
- Per CPU maps
- Map RCU
- Map-in-Map swapping
# ChatGPT o1
```
eBPFの世界では、カーネル内のあらゆる箇所で同じプログラムが同時に実行され得るため、eBPFのMapを共有データ構造として扱う場合はさまざまな並行制御(Concurrency Control)を意識する必要があります。以下では、LinuxカーネルやeBPFの知識を踏まえて、eBPFマップで並行制御が必要となる代表的なケースを列挙し、その背景を解説します。
---
## 1. 複数のBPFプログラムやスレッドから同時アクセスされる場合
### 1-1. 同じマップを複数のBPFプログラムが同時実行するケース
- eBPFプログラムはカーネル内の多数の箇所(例えば複数のkprobeやtracepointなど)で呼び出され得るため、同じマップが共有されるときには同時アクセスが起こる可能性があります。
- 特に、値の更新(書き込み)を行うBPFプログラムが複数ある場合は、レースコンディションを避けるためにスピンロックや原子操作(atomic)などが必要になります。
### 1-2. 同じマップを単一のBPFプログラムが複数CPU上で並行に呼び出されるケース
- 同じプログラムでも、複数のCPUコアから同時に呼び出されるときにレースコンディションが発生し得ます。
- カウンタのインクリメントのように単一の整数値を書き換えるだけであっても、非原子的な操作(`i += 1`など)は複数命令に分解されるため、更新レースが起こる余地があります。
### 1-3. eBPFプログラム同士(またはプログラム内の複数コードパス)で同じマップ要素の複数フィールドを同時に更新するケース
- 構造体の複数フィールドにわたる更新を行う場合、それぞれを単独のatomic操作で行うと「フィールドAだけが更新されてフィールドBはまだ古い値」という不整合が生じる可能性があります。
- そのため、複数フィールドを一括で更新する場合には、スピンロックなどでデータ全体を保護する方法が選択されます。
---
## 2. ユーザ空間とBPFプログラムが同時にマップにアクセスする場合
### 2-1. ユーザ空間によるマップのread/writeとカーネル内eBPFプログラムのread/writeが競合するケース
- ユーザ空間から`bpf()`システムコールを使ってマップを更新する(例えば`BPF_MAP_UPDATE_ELEM`)ときに、同時にeBPFプログラムが同じキーに対して書き込みを行う場合があります。
- レースコンディションが起こると、意図しない値の上書きや不整合が発生し得るため、atomic操作やスピンロック、あるいはRCU機構を利用して防ぐ必要があります。
### 2-2. BPF_F_LOCKフラグを使ったユーザ空間でのロック取得
- ユーザ空間からマップ要素を更新・取得する際に`BPF_F_LOCK`フラグを指定すると、BPFプログラム内での`bpf_spin_lock()`と同じロックを取った状態で要素アクセスが行われます。
- 一貫したロック制御ができる一方で、ロック競合により読み書きの遅延が発生する可能性もあります。
---
## 3. マップ要素に対するRead-Modify-Writeパターンが発生する場合
### 3-1. 単一のカウンタやフラグを原子操作で更新する
- カウンタの加算などの単純な操作でも、下記のように「読み -> 加算 -> 書き込み」の3段階に分かれるため、排他制御がないとレースが発生し得ます。
- LLVM/Clangの組み込み関数 (`__sync_fetch_and_add` など)を利用すると、ハードウェアレベルで一度に更新できるため、レースコンディションを回避できます。
```c
__sync_fetch_and_add(&counter, 1);
```
### 3-2. 複数フィールドの整合性を保ちながら更新する
- 例えば、(count1, count2) の2つのカウンタを「一度のイベントですべてインクリメントしたい」などのケースでは、片方だけが更新されて片方が古いままになると整合性が崩れます。
- 原子操作では複数の更新を同時に扱えないため、スピンロックなどによって一括で保護する必要があります。
---
## 4. スピンロックを利用する必要があるケース
### 4-1. 複数フィールドをまとめて更新したい場合
- 上述のように、複数フィールドを「同一ロック」で保護して同時更新する必要がある場合は、`bpf_spin_lock()`/`bpf_spin_unlock()`による排他制御が最も確実な方法です。
- eBPFでは`struct bpf_spin_lock`をマップの先頭に置いておく必要があるなど、マップの設計が制約される点に注意します。
### 4-2. 原子操作では扱えない大きなサイズのデータを更新する場合
- eBPFの原子操作は1, 2, 4, 8バイトの更新しか保証されていないため、それ以上のサイズのフィールドを安全に更新するにはスピンロックが必要となります。
### 4-3. デッドロックを避けるためロック取得の順序や再入不可を考慮する場合
- BPFのスピンロックは「一度に一つしか取得できない」という制約があるため、複数のロックを同時に取得するコードパスが存在すると、Verifierによる拒否(“code path that might cause deadlock”)が行われる可能性があります。
- カーネルモジュールなどでよくあるような複雑なロック階層の設計は、eBPFでは(Verifier制限により)難易度が高いです。
---
## 5. Per-CPUマップでの並行制御が不要(もしくは最小限で済む)ケース
### 5-1. 書き込み頻度の高いメトリクス収集など
- Per-CPUマップを使うと、CPUごとに独立したメモリ領域が確保されるため、同じキーに対しても各CPUがアクセスする実体は異なります。
- したがってロックを取る必要がなく、高頻度の書き込み(例えばパケットカウントなど)では性能が大きく向上します。
### 5-2. ユーザ空間側での集計タイミングでのみ同期を取れば良いケース
- Per-CPUマップは、ユーザ空間がすべてのCPUの値を読み出して合算する必要があるため、メモリ使用量や集計の手間がかかります。
- しかし、BPFプログラムの実行パスではほぼ並行制御を意識せずに済むため、レースコンディションを避けるには非常に便利な方法です。
---
## 6. RCU機構(Map RCU)での「コピーオンアップデート」アプローチを取る場合
### 6-1. ポインタ経由で直接更新せずに、コピー→更新→再投入を行うケース
- `bpf_map_lookup_elem()`で得たポインタを直接書き換えるのではなく、一度スタックにコピーして更新し、`bpf_map_update_elem()`する形で整合性を保つ方法があります。
- これにより、途中の中間状態を参照されることがなくなるため、フィールド間の整合性を保ちやすくなります。
- ただし、「更新が丸ごと上書きされる」ため、多数の同時書き込みがあると最新の更新が一部取りこぼされる(“missing updates”)可能性があります。
### 6-2. 高いスループットを重視してロックを排除したいケース
- RCU自体はロックを使わない(Read-Copy-Update)メカニズムですが、eBPFのマップは内部的にRCUを利用し、ユーザ空間アクセスでも同様の原理(コピーしてから更新)が使われます。
- “更新の一貫性”よりも“パフォーマンス”を優先して、ある程度の更新の飛びこぼしを許容できるケースで有効です。
---
## 7. Map-in-Mapを使ってスナップショットを取りたい場合
### 7-1. 長時間かかるユーザ空間側でのイテレーション中にデータを固定化したいケース
- 大きなマップをユーザ空間で「全キーを列挙して読み取る」ときに、その最中に値が変化すると、時系列的に不整合が生じる可能性があります。
- このとき、Map-in-Mapの仕組みを使って「今見せるべき内部マップ」をスワップしてしまい、ユーザ空間はスナップショット(変更されない状態)を読むことができます。
### 7-2. 計測や統計用途で、特定のタイミングでの一貫した状態を欲するケース
- 計測やモニタリングでは、ある瞬間におけるマップ全体の状態を正確に把握したいという要望がよくあります。
- スワップ方式により「古い世代のマップ」を読み取り中に新しい世代のマップでカーネル側の更新を進める、といったダブルバッファリング的な実装が可能です。
---
## まとめ
eBPFマップの並行制御は、主に以下のような視点で必要になります。
1. **単一値(カウンタなど)の原子更新が必要なケース**
- ハードウェアレベルのatomic命令を利用して、シンプルな更新レースを回避する。
2. **複数フィールドの整合性を保ちながら同時更新が必要なケース**
- スピンロック(`bpf_spin_lock`)によるロック保護が有効。
- BPFの制約(複数ロックの同時取得はNG など)を考慮する必要あり。
3. **高頻度アクセスで同期コストを極力抑えたいケース**
- Per-CPUマップで書き込みを分散し、ユーザ空間で集計時にのみ同期する。
4. **コピーオンアップデート方式で一貫した更新を行いたいケース(RCU的手法)**
- 更新の中間状態をカーネルやユーザ空間から見せず、既存値を一括で上書きする。
- ただし同時更新の取りこぼしが起こる可能性がある。
5. **スナップショットを取りたいケース**
- Map-in-Mapのスワップ機構で、読み取り中のマップを固定化しつつ、新しいマップを差し替えられる。
どの方法を選択するかは、
- 更新頻度と読み取り頻度のバランス
- メモリコスト(Per-CPUマップはCPU数分だけ要領が増える)
- 更新内容の粒度(単一値か複数フィールドか)
- 同時更新がどの程度起き得るか
- スナップショット取得の必要性
などに左右されます。eBPFプログラムとユーザ空間の両面でどれが最適か、さらにVerifierによる制限(ロックのネスト不可など)を考慮しつつ設計することが重要です。
```