これまでの multi_market_probe_v1 は、「見えるものはすべて保存する」という思想で設計してきました。
板も約定も、複数市場を同時に、可能な限り高頻度で記録する。
観測機としては正しいアプローチに思えました。
しかし、7時間の稼働でストレージが約10GB増加し、さらに Raw logger の queue full と dropped が発生。
“高精度で取っているはずのデータ” に欠損が混じっているという、設計上の矛盾が露呈しました。
今回の再設計では、観測を二層に分離します。
- WS神経系:リアルタイム性を担保する層(signal・health監視)
- 保存最小化プロファイル:欠損ゼロを優先する検証骨格層
すべてを保存するのではなく、
目的に対して十分な情報だけを、安定して残す。
multi_market_probe_v1 はここから、
「全部取る観測機」から「壊れない観測機」へ進化します。
Ⅰ. 7時間で+10GB ― 何が起きたのか
今回の観測ランでは、multi_market_probe_v1 を全市場・全チャネル保存モードで約7時間稼働させました。
結果はシンプルです。
ストレージ +10GB
Raw logger queue full 発生
dropped 累積 50,000件超(最終 50,825件)
数字だけ見れば「保存しすぎた」で済みます。
しかし実際に起きていたことは、もう少し構造的でした。
1. 保存構成:何を“全部”取っていたのか
当時の構成では、以下を常時保存していました。
- bf_fx: ticker + executions + board + board_snapshot
- bf_spot: ticker
- binance_perp: ticker + board + executions(WS)
- snapshot_stream(market/spread)
- event_stream(SIGNAL/FEED/STATE)
raw_stream は以下のような設定でした。
raw_stream:
enabled: true
queue_maxsize: 50000
batch_size: 200
flush_interval_sec: 0.25
partition_by_market_dir: true
partition_by_kind_dir: true
channel_split: true
つまり、
- 市場別
- 種別別(ticker/executions/board/board_snapshot)
- さらにWS生データそのまま保存
という完全保存プロファイルでした。
2. 何が最も流量を生んでいたか
実測では、流量最大は以下でした。
- binance_perp の bookTicker(WS)
- bf_fx の board + executions
- snapshot_stream(0.2秒間隔 emit)
特に Binance の WS は流量制御がなく、価格変動が荒れる局面では瞬間的にスパイクします。
このとき発生したのが、
Raw logger queue full: dropped=50000 queue_size=50000
です。
ここで重要なのは、
queue_size=50000は現在のキュー占有数dropped=50000は捨てた件数の累積
という点です。
実装上、asyncio.Queue.put_nowait() が QueueFull を投げると、
except asyncio.QueueFull:
self._stats["dropped"] += 1
という処理でデータは破棄されます。
つまり、
“保存しているつもり” のデータの一部は
実際には保存されていなかった
という状態でした。
3. 何がボトルネックだったのか
構造は単純です。
- 生産(WS入力) > 消費(ディスク書き込み)
- それが継続
- queue_maxsize 到達
- drop開始
raw_stream の書き込み設定は
batch_size=200
flush_interval_sec=0.25
でした。
これは低レイテンシ寄りの設定です。
しかし観測ログに求められるのは「即時性」より「欠損しないこと」です。
この時点で、
- 高頻度入力
- 細かいflush
- 複数市場
- partition_by_kind_dirでファイル分割
が重なり、I/O が追いつかなくなっていました。
4. さらに悪いのは“気づきにくい”こと
dropが起きても、
- ファイルは増え続ける
- Grafanaも更新される
- サービスは落ちない
つまり、
表面上は正常稼働に見える
しかし実際は、
- 板の連続性が欠損
- 約定の一部が消失
- その上に作るメトリクスが歪む
という、静かなデータ破壊が起きていました。
5. ストレージの内訳
当日観測時点のディレクトリ容量は以下。
- data/mmarb_raw: 534MB(圧縮後)
- data/mmarb_snapshot: 4.7GB
- data/mmarb_logs: 4.5GB
- data/mmarb_signal: 112MB
rawよりも snapshot/log の方が大きいのも重要な発見でした。
(mmarb_snapshot: 4.7GBもmmarb_logs: 4.5GBも後に圧縮ローテに編入しました)
つまり問題は
「WSが重い」だけではない
「保存レイヤー全体が重い」
ということ。
6. 本質的に何が間違っていたか
当初の思想はこうでした。
見えるものは全部保存すれば後から検証できる
しかし実際は、
- 保存しきれない量を取る
- dropで穴あきになる
- 精度が落ちる
という逆転が起きました。
ここで気づいたのは、
高解像度 ≠ 高精度
ということです。
10ms単位で取れていても、
途中で欠損すればそれは「精密」ではありません。
7. ここから得た結論
7時間 +10GB は失敗ではありませんでした。
これは、
- 保存戦略の限界点
- queue設計の限界
- WS流量の現実
を数値で確認した実験でした。
そしてこの出来事が、今回の再設計
「WS神経系 + 保存最小化プロファイル」
へと繋がります。
次章では、
なぜWSは万能ではないのかを掘り下げます。
Ⅱ. dropped=50000 の意味
ログに出ていた一行。
Raw logger queue full: dropped=50000 queue_size=50000
これを「キューが満杯になった」程度に読むのは危険です。
この数字が意味しているのは、もっと重い事実です。
1. dropped は「現在のサイズ」ではない
まず誤解しやすい点から。
queue_size=50000はその瞬間のキュー占有数dropped=50000は 捨てた件数の累積
実装上は、asyncio.Queue.put_nowait() が QueueFull を投げたときに、
except asyncio.QueueFull:
self._stats["dropped"] += 1
return False
という処理が走ります。
つまり、
50000件、ログとして記録されなかった
という意味です。
これは「遅れた」ではありません。
消えたです。
2. 何が消えていたのか
消えていたのは raw ログです。
raw には
- WSの板更新
- 約定情報
- ticker payload
- 市場別生データ
が含まれます。
つまり、
- 板の更新の一部
- 約定の一部
- 価格変動の瞬間
が保存されていない可能性がある。
そしてこの raw の上に、
- snapshot
- spread算出
- premium状態判定
- signal生成
が乗っています。
基礎データが欠けると、上位レイヤーは静かに歪みます。
3. なぜ queue full が起きるのか
構造は単純です。
入力(WS受信) > 出力(ディスク書き込み)
この状態が続くと、キューは必ず満杯になります。
今回の設定は以下でした。
queue_maxsize: 50000
batch_size: 200
flush_interval_sec: 0.25
つまり、
- 200件ずつ
- 0.25秒間隔で
- ディスクへ書き出す
という設計です。
しかし入力は、
- binance_perp WS
- bf_fx board
- bf_fx executions
- snapshot_stream(0.2秒間隔)
という多層構造。
スパイク局面では入力が急増し、
出力が追いつかなくなります。
そして queue が溢れた瞬間から、
データは無言で破棄され始めます。
4. dropped が意味する3つの危険
(1) “事実ログ”の欠損
板の連続性が壊れると、
- 価格が飛んだように見える
- 流動性が急減したように見える
が、実際は「保存されなかっただけ」かもしれない。
(2) メトリクスの歪み
lead/lagやpremium持続時間の計算は、
- 連続したtick列
- 欠損のないmid系列
を前提としています。
欠損が入ると、
- persistence が短く見える
- slope が急に変化したように見える
- regime判定が乱れる
原因が市場ではなくロガーになります。
(3) 気づきにくい
最も厄介なのはこれです。
- サービスは落ちない
- Grafanaは更新される
- ファイルは増える
つまり、
正常に動いているように見える
しかし内部では欠損が進行している。
これが一番危険です。
5. 高解像度データのパラドックス
今回の観測は「高精度に取ろう」とした結果、
- 高頻度
- 全量保存
- 全市場同時
を選択しました。
しかし drop が発生した瞬間、
高解像度 ≠ 高精度
になります。
10ms単位で受信しても、
途中で抜ければそれは精密ではありません。
むしろ、
- 500msサンプリングでも欠損ゼロ
の方が、統計的には信頼できます。
6. dropped=50000 が示したもの
この数字は単なる警告ではありません。
それは、
- 保存設計の限界点
- 流量制御の不在
- レイヤー分離の必要性
を示すシグナルでした。
multi_market_probe_v1 は
「全部取る観測機」から
「壊れない観測機」へ
移行する必要がある。
dropped=50000 は、その転換点でした。
Ⅲ. WSは万能ではない
これまでの自分の前提は、ほぼ無意識にこうなっていました。
リアルタイム観測なら WebSocket が最適解
低レイテンシ=高精度
実際、multi_market_probe_v1 も WS 前提で設計されています。transport: websocket を指定すれば、各市場ごとに WS アダプタが立ち上がります。
例えば bitFlyer の場合は、
async def _run_bitflyer_ws_stream(...)
Binance なら、
async def _run_binance_ws_stream(...)
といった具合に、WS ストリームを常時監視する実装になっています。 multi_market_probe_v1
設計としては正しい。
しかし今回の実験で分かったのは、
WSは「速い」が、「壊れない」わけではない
ということでした。
1. WSの強み:神経系としては最適
WebSocket の強みは明確です。
- 低レイテンシ
- push型
- イベント駆動
- 板や約定の瞬間を取り逃さない
リアルタイムシグナル生成や health 監視には最適です。
multi_market_probe_v1 の内部でも、
- MARKET_TICK
- FEED_STATE
- SIGNAL
などは、WSからの即時入力を前提にしています。 multi_market_probe_service
このレイヤーは、まさに「神経系」です。
2. しかし、WSには“流量制御”がない
WSの弱点はここです。
- 入力レートをこちらで制御できない
- スパイクがそのまま飛び込んでくる
- 市場の荒れ=そのまま流量増加
今回 queue full が起きたのは、
入力(WS) > 出力(ディスク)
が継続したためでした。
WSは止まりません。
しかしロガーのキューには上限があります。
そして上限に達した瞬間から、
低レイテンシのはずのデータが「捨てられる」
という逆転現象が起きます。
3. 高解像度の罠
WSで10ms単位の更新を受け取っても、
- 途中で500件落ちる
- 板の一部が保存されない
- 約定の一部が抜ける
と、それはもう「精密」ではありません。
むしろ、
- 200ms間隔で
- 欠損ゼロで
- 長時間安定
の方が、状態量分析には向いている。
WSは解像度を上げますが、
完全性を保証しません。
4. pollingは“劣化”ではない
ここで polling を選ぶと、
精度が下がるのでは?
という直感が働きます。
しかし、精度には二種類あります。
| 種類 | WS | polling |
|---|---|---|
| 時間解像度 | ◎ | △ |
| 欠損耐性 | △ | ◎ |
今回必要だったのは、後者でした。
雑スクリーニング段階では、
- premium の持続時間
- regime 分類
- 比較軸作成
が目的です。
そのために必要なのは、
10ms刻みの板ではなく
欠損のない mid 系列
でした。
5. WSをやめるのではない
重要なのはここです。
結論は、
WSを捨てる
ではありません。
正しくは、
WSを“全量保存しない”
です。
WSは神経系として維持する。
しかし保存は、
- サンプリング
- 集約
- polling併用
で流量を制御する。
multi_market_probe_v1 の再設計は、
WS=リアルタイム層
保存=検証骨格層
という分離へ向かいます。
6. 今回の教訓
WSは速い。
しかし速さは万能ではない。
- 保存できなければ意味がない
- 欠損が出れば信頼性が崩れる
- 高解像度はコストを伴う
今回の queue full は、
WSが強すぎた瞬間
でした。
そしてその経験が、
WS神経系+保存最小化プロファイル
という設計へと繋がっています。
WSは万能ではない。
だが、正しく使えば最強である。
次章では、その“正しい使い方”としての
二層構造設計について書きます。
Ⅳ. 観測を“2レーン”に分ける
queue full と dropped=50000 を経て分かったことは単純でした。
「観測」は一枚岩ではない。
リアルタイムに反応するための観測と、
長時間比較・検証のための観測は、
求める特性がまったく違います。
そこで multi_market_probe_v1 を、
2つのレーンに分離するという設計に切り替えました。
レーン1:WS神経系(リアルタイム層)
これは即時性を最優先する層です。
役割
- SIGNAL生成
- FEED_STATE監視
- latency/stale検知
- distortion判定
実装上は、
_run_bitflyer_ws_stream()
_run_binance_ws_stream()
_run_bybit_ws_stream()
といった WebSocket アダプタが担います。 multi_market_probe_v1
この層の特徴は:
- レイテンシが重要
- 最新状態が重要
- 過去全量保存は不要
ここで必要なのは「今の状態」です。
多少過去が抜けても、
“今LIVEかどうか”が分かれば役割は果たせます。
だからこのレーンでは、
- WSは維持
- raw保存は最小化
- health重視
という思想になります。
レーン2:検証骨格(保存層)
こちらは逆です。
役割
- premium分布作成
- persistence分析
- regime比較
- lead/lag検証
- 将来リターン検証
ここで重要なのは、
- 欠損ゼロ
- 比較可能性
- 流量上限管理
時間解像度はある程度落としても構いません。
例えば:
- 200msサンプリング
- 500ms集約
- 1秒リサンプル
でも、統計的な比較には十分です。
むしろ重要なのは、
同じ条件で、長時間、安定して残ること。
なぜ1レーンではダメだったのか
これまでの設計は、
WSで受けたものを全量保存
という単一レーンでした。
しかしこの設計だと、
- WSの流量が保存層を圧迫
- queue full発生
- 欠損
- 信頼性低下
という構造的な矛盾が起きます。
リアルタイム性と保存完全性は、
同じレイヤーで両立しにくい。
だから分ける。
実際の切り替え
今回のA+軽いBの反映では、
- bf_fx board_snapshot 常時OFF
- binance_perp を polling に切替
- raw_stream batch/flush 緩和
という第一段階を実施しました。
次段階では、
- raw_stream.enabled=false(全量raw保存停止)
- snapshot_stream / event_stream のみ最小限維持
- WS tickerを内部リサンプルして保存
へ進みます。
つまり、
WSは動かす
保存は制御する
という構造です。
2レーン設計の本質
この分離は、単なる負荷対策ではありません。
それは観測思想の転換です。
- 神経系は速く
- 骨格は強く
速さは神経の役割。
安定は骨格の役割。
multi_market_probe_v1 はここから、
全部取る観測機
から
構造を分けた観測機
へ進化します。
そしてこの分離が、
メトリクス生成の段階化へと繋がっていきます。
Ⅴ. 壊れない観測機への第一歩
queue full と dropped の発生を受けて、
今回まず実施したのが A(入力削減)+軽いB(書き出し強化) です。
これは設計思想の大転換というより、
「まず壊れない状態に戻す」ための安定化パッチです。
A:入力削減(最優先)
まずやったのは、流量そのものを減らすことです。
1. bf_fx の board_snapshot 常時OFF
bf_fx:
raw_ws_channels:
executions: true
board_snapshot: false
board: true
board_snapshot は情報量が大きく、
かつスクリーニング段階では常時不要。
短期検証時のみONにする方針に変更しました。
2. binance_perp を websocket → polling に切替
binance_perp:
transport: polling
poll_interval_sec: 0.75
ここが最も効きました。
観測結果では、
binance lines/min ≈ 80
まで流量が低下。
WSでは市場スパイクがそのまま流量増加に直結しますが、
pollingはアプリ側で流量上限を強制できる。
今回のフェーズ(雑スクリーニング)では、
この方が適切でした。
軽いB:書き出し強化(補助)
入力を絞った上で、書き出し側も少し強化。
raw_stream:
batch_size: 500
flush_interval_sec: 0.5
変更前:
- batch_size=200
- flush=0.25秒
変更後:
- batch_size=500
- flush=0.5秒
これは「ディスクI/O回数を減らして、
一回あたりの吐き出し量を増やす」調整です。
低レイテンシよりも欠損ゼロ優先の設定に寄せました。
結果
再起動後の観測では、
- Raw logger queue full 未発生
- dropped 増加なし
- board_snapshot 2分間 +0 lines
- binance流量安定
- Prometheus更新継続
つまり、
観測機は壊れなくなった
という状態まで戻せました。
重要なのは“まだ途中”ということ
今回のA+軽いBは、あくまで安定化です。
まだ、
- WS神経系の最適化
- raw保存の完全停止プロファイル
- 内部リサンプル層の実装
は未着手。
しかし、
queue full が出ない
dropped が増えない
という状態を確保できたことで、
ようやく次フェーズ(メトリクス段階化)に進めます。
今回の学び
- 入力削減が最も効く
- 出力強化は補助
- 流量を制御できないWSは危険
- 保存は“全部”でなく“必要十分”
A+軽いBは地味ですが、
観測機の再設計における重要な土台です。
ここから先は、
WS神経系 + 保存最小化プロファイル
という本丸に入ります。
Ⅵ. 明日やること
A+軽いBで「壊れない状態」には戻りました。
しかしこれはあくまで安定化です。
明日やることは、multi_market_probe_v1 を本来の構造へ切り替えることです。
1. WS神経系へ戻す
今回、流量抑制のために binance_perp を polling に落としました。
しかし最終的な構造は、
WS=神経系
保存=最小化
です。
したがって明日は:
binance_perp:
transport: websocket
へ戻します。
目的は、リアルタイム性の回復。
- distortion検知
- lead/lag観測
- premium状態変化
これらはWS前提の方が自然です。
ただし今回は、全量保存しません。
2. raw_stream を停止する
これが最大の変更です。
raw_stream:
enabled: false
今回の検証で分かったことは、
生データ全量保存はスクリーニング段階では不要
という事実でした。
rawは短期調査時のみONにします。
これで、
- queue圧迫ゼロ
- droppedゼロ
- I/O負荷低下
- ストレージ増加抑制
が同時に達成できます。
3. snapshot / event を“必要最小限”にする
snapshot_stream と event_stream は維持します。
しかし、
- 出力頻度
- 保存対象
- ローテーション間隔
を再確認します。
目的は、
「観測可能」ではなく
「比較可能」にすること。
メトリクス生成に必要な粒度だけを残します。
4. 再起動後の即時検証
明日のチェックポイントは明確です。
- Prometheusメトリクスが継続更新していること
- Raw logger started が出ないこと(raw無効確認)
- Grafanaの主要パネル(market/spread/signal)がリアルタイム更新すること
- queue full / dropped が出ないこと
ここがクリアできれば、
WS神経系+保存最小プロファイル
への移行成功です。
5. 余力があれば:検証骨格層の設計
次のステップとして考えているのは、
- WS ticker を内部で 200ms / 500ms / 1s にリサンプル
- それだけを保存
- board/executions は短期フラグでON
という構造。
これは、
リアルタイム神経
統計骨格
の明確な分離です。
6. 明日の本質
明日の作業は単なる設定変更ではありません。
それは、
「全部取る」から
「目的に十分なものだけ残す」
への転換です。
multi_market_probe_v1 はここから、
観測装置ではなく、
構造化された観測機になります。
明日はその第一歩です。
Ⅶ. 今日の結論
今日はコードを書いたというより、
観測の前提を書き換えた日でした。
7時間で+10GB。
Raw logger queue full。
dropped=50000超。
最初は単なる負荷問題に見えました。
しかし本質は違いました。
高解像度は高精度ではない
WSで全量を取れば精密になる。
そう思っていました。
しかし実際には、
- 保存しきれない
- キューが溢れる
- データが欠損する
結果として、
速いが穴あき
という状態になっていました。
それは高精度ではありません。
観測は一枚岩ではない
今日一番大きな気づきはこれです。
- リアルタイム監視と
- 長時間検証
は同じではない。
WSは神経系としては最適。
しかし保存骨格としては強すぎる。
だから分ける。
A+軽いBは安定化にすぎない
- 入力削減
- 書き出し強化
- queue full解消
これは土台の修復です。
本丸はこれからです。
multi_market_probe_v1 は次の段階へ
ここからは、
WS神経系 + 保存最小化プロファイル
という構造へ移行します。
全部を保存する観測機ではなく、
- 壊れず
- 欠損せず
- 比較可能で
- 構造を抽出できる
観測機へ。
今日の一文
観測可能 ≠ 保存可能
保存可能 ≠ 比較可能
今日の10GBは無駄ではありません。
それは、
観測設計の限界を
数字で確認できた証拠でした。
multi_market_probe_v1 は、
ここから本当に“使える観測機”になります。