Skip to content

Commit 3fe46c0

Browse files
committed
docs: document RDMA BLOB replication redesign
Add TODO.md describing the redesign plan for the RDMA BLOB replication bug. Update the regression test to reproduce the current RDMA BLOB replication failure.
1 parent 23cb1f0 commit 3fe46c0

2 files changed

Lines changed: 191 additions & 5 deletions

File tree

TODO.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# RDMA BLOB レプリケーション修正 TODO
2+
3+
## 目的
4+
5+
RDMA BLOB レプリケーションで、大きな BLOB を sender / receiver のどちらでも
6+
全量メモリに載せず、streaming で転送できるように修正する。
7+
8+
現在失敗している regression test は次のとおり。
9+
10+
```sh
11+
./build-rdma-rep-tests/test/limestone-test \
12+
--gtest_filter=log_channel_handler_test.handle_rdma_data_event_accepts_blob_split_by_rdma_socket_io
13+
```
14+
15+
## 方針検討
16+
17+
### 受信処理方式と性能
18+
19+
RDMA 版を TCP 版のような stream 処理に寄せる案も検討した。
20+
設計としてはきれいで、既存の `socket_io` / `blob_socket_io` に近づけやすい。
21+
しかし RDMA write size は 4 KB から 64 KB 程度と小さいため、callback ごとに
22+
worker thread を起こす素朴な stream 実装では context switch の回数が多くなり、
23+
性能面で不利になる可能性が高い。
24+
25+
一方、callback 内で BLOB chunk を file に直接 `write()` する方式は、
26+
余分な user-space buffer copy と payload ごとの thread wakeup を避けられる。
27+
通常の buffered file write は多くの場合 page cache への copy なので、
28+
「callback 内で I/O するから必ず遅い」とは言えない。
29+
30+
ただし、callback 内 write には次のリスクがある。
31+
32+
- page cache / filesystem / memory pressure によって `write()` が詰まる可能性がある。
33+
- callback が RDMA receive progress thread 上で長く止まると、RDMA 側の進行に影響する可能性がある。
34+
- 小さい chunk が大量に来るため、syscall 回数は多くなる。
35+
36+
現時点の第一候補は次の方針とする。
37+
38+
```text
39+
RDMA BLOB 専用 protocol
40+
+ callback 内 state machine
41+
+ callback 内 buffered file write
42+
```
43+
44+
callback 内では `fsync()` / `fdatasync()` のような同期 I/O は行わない。
45+
将来 callback blocking が問題になった場合に備えて、BLOB 受信処理は
46+
`rdma_blob_receiver` のような独立したクラス境界に閉じ込め、
47+
内部実装を batched writer thread に差し替えられるようにする。
48+
49+
### Flush と ACK の分離
50+
51+
TCP 版と RDMA 版では ACK の見え方が異なる。
52+
53+
TCP 版では、transport ACK は TCP protocol ACK として OS の TCP stack が処理するため、
54+
limestone からは見えない。limestone が明示的に扱う ACK は `COMMON_ACK` だけである。
55+
56+
RDMA 版では、ACK は rdma-comm-lib の内部メッセージとして明示的に扱われる。
57+
この ACK は RDMA frame の `sequence_number` 単位で返され、送信側 `flush()`
58+
pending ACK が 0 になるまで待つ。
59+
60+
重要なのは、rdma-comm-lib の現行 ACK は単なる「sender buffer 解放通知」ではなく、
61+
「受信側ユーザハンドラの処理完了通知」として定義されていること。
62+
受信側は RDMA 通知をデコードし、正常イベントまたはエラーイベントをユーザハンドラへ渡し、
63+
そのハンドラが return した後に ACK を返す。
64+
したがって `flush()` の成功は、
65+
66+
- 送信済み RDMA frame すべてについて ACK が返ったこと
67+
- その ACK は、受信側ユーザハンドラの処理完了後に送られたこと
68+
69+
を意味する。
70+
71+
一方で、この ACK は durable ACK ではない。
72+
`fsync()` / `fdatasync()` の完了やアプリケーションレベルの成功は `flush()` だけでは保証されない。
73+
アプリケーションレベルの成否は、必要に応じて ACK body で通知し、
74+
`flush()` 後に `take_ack_body()` で回収する設計である。
75+
76+
BLOB 対応では、巨大 BLOB を多数の RDMA frame に分割するため、
77+
どの単位で「受信側ユーザハンドラの処理完了」とみなすかが問題になる。
78+
`BLOB_DATA` frame ごとに file write が終わればすぐ ACK を返すのか、
79+
あるいは BLOB 全体や WAL apply 完了まで ACK を遅延させるのかで、`flush()` の意味が変わる。
80+
81+
現時点では、次の順序で扱う前提で検討する。
82+
83+
```text
84+
RDMA frame callback
85+
-> protocol / sequence / state check
86+
-> BLOB chunk を buffered file write
87+
-> write 成功
88+
-> callback return
89+
-> rdma-comm-lib が transport ACK を送信
90+
-> sender 側 rdma-comm-lib が pending ACK を完了し、send buffer slot を解放
91+
```
92+
93+
この方針でよいか、rdma-comm-lib の仕様変更が必要かを先に整理する。
94+
95+
## 作業方針
96+
97+
### 1. rdma-comm-lib で ACK / flush の責務を整理する
98+
99+
- 現行の `flush()` / ACK 仕様を維持するか、変更するかを決める。
100+
- ACK を「受信側ユーザハンドラの処理完了通知」として維持するのか、
101+
buffer 解放用 ACK と application completion を分離するのかを決める。
102+
- BLOB_DATA frame ごとに ACK を返すのか、BLOB 全体または transaction 単位で ACK を返すのかを決める。
103+
- 仕様変更する場合、`flush()` が待つ対象を transport ACK のままにするのか、
104+
application completion を待つ API に変えるのかを決める。
105+
- `ack_body` を application result / error 通知に使う場合の保持方式を決める。
106+
- 現状は stream あたり 1 件だけ保持し、未取得の body があると後続 body が drop される。
107+
- logical ACK に使うなら queue 化や sequence number との対応付けが必要か検討する。
108+
- limestone 側の RDMA wrapper に必要な API を expose する。
109+
- receiver 側: `receive_handler_with_ack` 相当。
110+
- sender 側: `take_ack_body()` または logical ACK 取得 API。
111+
- この整理が終わるまで、limestone 側で ACK の意味を独自に増やさない。
112+
113+
### 2. RDMA BLOB sub-protocol を設計する
114+
115+
- 最小限必要な RDMA BLOB frame / message 種別を決める。
116+
- 各種別の payload format を決める。
117+
- protocol 定義を `src/limestone/rdma/` に置くか、
118+
`src/limestone/replication/` に置くか決める。
119+
- `message_log_entries` の metadata と BLOB data をどう分離するか決める。
120+
- receiver がどの時点で WAL entry を apply するか決める。
121+
- BLOB 転送が中断または失敗したときに、partial file をどう cleanup するか決める。
122+
123+
初期案:
124+
125+
```text
126+
RDMA_STREAM_BEGIN
127+
RDMA_STREAM_BYTES
128+
RDMA_BLOB_BEGIN
129+
RDMA_BLOB_DATA
130+
RDMA_BLOB_END
131+
RDMA_STREAM_END
132+
```
133+
134+
receiver は `RDMA_BLOB_DATA` を replica 側の BLOB file に直接書き込む。
135+
BLOB 全体を `std::vector``std::string` に集約してはいけない。
136+
137+
### 3. Protocol 型とテストを追加する
138+
139+
- frame / message type 定義を追加する。
140+
- encode / decode helper を追加する。
141+
- 各 frame 種別に対する focused unit test を追加する。
142+
- 不正な frame type と不正な payload の test を追加する。
143+
144+
### 4. Sender 側を実装する
145+
146+
- `rdma_socket_io::send_blob()` の暗黙的な byte-stream 分割をやめる。
147+
- BLOB metadata と BLOB chunk を、明示的な RDMA BLOB protocol frame として送る。
148+
- BLOB を含まない RDMA replication path は変えない。
149+
- sender 側で frame の順序と chunk 境界を確認する test を追加する。
150+
- 100 MB から 1 GB 超の BLOB でも streaming で動作することを前提にする。
151+
152+
### 5. Receiver 側を実装する
153+
154+
- `log_channel_handler` が RDMA BLOB sub-protocol を処理できるようにする。
155+
- BLOB chunk を replica datastore の BLOB file に直接書き込む。
156+
- 必要な BLOB がすべて揃うまで `message_log_entries` を apply しない。
157+
- BLOB 全体を `std::vector``std::string` に集約しない。
158+
- sequence number、payload size、channel validation の既存チェックを維持する。
159+
160+
### 6. End-to-end regression test を通す
161+
162+
- 必要なら、新 protocol に合わせて failing regression test を調整する。
163+
- 分割された RDMA BLOB payload が正常に処理されることを確認する。
164+
- replica 側の BLOB file が作成され、内容が sender 側と一致することを確認する。
165+
- BLOB 転送完了前に WAL entry が書かれないことを確認する。
166+
167+
### 7. 異常系 test を追加する
168+
169+
- BLOB chunk 欠落。
170+
- BLOB size 不一致。
171+
- 予期しない `RDMA_BLOB_END`
172+
- duplicate frame または out-of-order frame。
173+
- BLOB 受信中の transfer abort または connection close。
174+
- partial BLOB file の cleanup。
175+
- 1 つの log entry に複数 BLOB が含まれるケース。
176+
- 1 session に複数の BLOB log entry が含まれるケース。

test/limestone/replication/log_channel_handler_test.cpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ TEST_F(log_channel_handler_test, handle_rdma_data_event_with_blob_entry_writes_b
707707
}
708708

709709
TEST_F(log_channel_handler_test,
710-
handle_rdma_data_event_with_blob_split_by_rdma_socket_io_fails_before_fix) {
710+
handle_rdma_data_event_accepts_blob_split_by_rdma_socket_io) {
711711
auto ctx = make_rdma_handler_with_channel(base_location);
712712
ASSERT_NE(ctx.handler, nullptr);
713713
ASSERT_GE(ctx.read_fd, 0);
@@ -750,10 +750,20 @@ TEST_F(log_channel_handler_test,
750750
ASSERT_GE(stream.calls_.size(), 2U)
751751
<< "rdma_socket_io should split a BLOB message into multiple RDMA sends";
752752

753-
auto first = make_rdma_event_from_payload(stream.calls_[0], 0U);
754-
EXPECT_THROW(
755-
{ ctx.handler->handle_rdma_data_event(first); },
756-
limestone_io_exception);
753+
for (std::size_t i = 0; i < stream.calls_.size(); ++i) {
754+
auto event = make_rdma_event_from_payload(
755+
stream.calls_[i], static_cast<std::uint16_t>(i));
756+
EXPECT_NO_THROW({ ctx.handler->handle_rdma_data_event(event); });
757+
}
758+
759+
auto& server_ds = ctx.server->get_datastore();
760+
auto received_path = server_ds.get_blob_file(blob_id).path();
761+
ASSERT_TRUE(boost::filesystem::exists(received_path))
762+
<< "Blob file not created at: " << received_path;
763+
std::ifstream ifs(received_path.string(), std::ios::binary);
764+
std::ostringstream oss;
765+
oss << ifs.rdbuf();
766+
EXPECT_EQ(oss.str(), blob_content);
757767

758768
sender_ds.reset();
759769
boost::filesystem::remove_all(sender_dir);

0 commit comments

Comments
 (0)