Skip to content

Commit 49dba07

Browse files
committed
docs(storage): document Bulk write semantics, retries, and data-loss cases
Update the storage best-practice docs to reflect the BulkIndexer-based remote write path: flush triggers (5 MB / 1 s / shutdown), two-tier retry policy (whole-batch 429/5xx with exponential backoff; per-item failures dropped via OnFailure), the three data-loss scenarios, and the four problem classes this design addresses (TLS handshake CPU, RTT ceiling, transient jitter, capture/backend decoupling).
1 parent 25b3bba commit 49dba07

2 files changed

Lines changed: 119 additions & 37 deletions

File tree

docs/best-practice/storage_en.md

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,9 @@ curl -k -u elastic:123456 -X GET "http://localhost:9200/huatuo_bamai/_count?pret
389389

390390
### System Architecture
391391

392-
The HUATUO Storage module runs on each node. It writes kernel events captured by the Tracer concurrently to the local directory and to Elasticsearch or OpenSearch. Both storage backends share the same `[Storage.ES]` configuration interface and are differentiated by address.
392+
The HUATUO Storage module runs on each node. It writes kernel events captured by the Tracer to the local directory and to Elasticsearch or OpenSearch. Both backends share the same `[Storage.ES]` configuration interface and are differentiated by address.
393+
394+
The remote write path uses the ES/OpenSearch **Bulk API** (`_bulk`): events are queued in an in-memory buffer and submitted in batches by background workers based on size and time thresholds, with transport-layer retries on transient failures.
393395

394396
```mermaid
395397
graph TB
@@ -401,7 +403,7 @@ graph TB
401403
subgraph huatuo["HUATUO Agent (node-level)"]
402404
T["Tracer Layer"]
403405
L["Local Directory\nhuatuo-local/"]
404-
S["Storage Module\n(concurrent write)"]
406+
S["Storage Module\nBulkIndexer Buffer"]
405407
end
406408
407409
subgraph backends["Storage Backends"]
@@ -412,41 +414,80 @@ graph TB
412414
kernel --> T
413415
T --> L
414416
T --> S
415-
S -->|Index API| ES
416-
S -->|Index API| OS
417+
S -->|Bulk API + auto retry| ES
418+
S -->|Bulk API + auto retry| OS
417419
```
418420

419421
### Write Flow
420422

421-
After the Tracer captures a kernel event, the Storage module writes it concurrently to the local directory and the remote storage backend. The two write paths execute in parallel — the local directory retains a copy while the remote backend provides durable storage and query capabilities.
423+
`Save` returns immediately after the event is buffered. Background workers flush the buffer to the remote backend when **any** of the following triggers fire: byte threshold, time threshold, or process shutdown. The local directory write is synchronous and independent of the remote Bulk path.
422424

423425
```mermaid
424426
sequenceDiagram
425427
participant T as Tracer Layer
426428
participant L as Local Directory (huatuo-local/)
427-
participant S as Storage Module
429+
participant S as Storage Module (BulkIndexer)
428430
participant B as ES / OpenSearch
429431
430432
T->>S: Kernel event captured, serialized to JSON
431-
par concurrent write
433+
par Local path (sync)
432434
S->>L: Write to local file
433-
and
434-
S->>B: Write to remote storage (Index API)
435-
B-->>S: Write acknowledged (200 OK)
435+
and Remote path (async batch)
436+
S->>S: Enqueue into bulk buffer, return immediately
437+
Note over S: Flush on 5 MB / 1 s / shutdown
438+
S->>B: POST /_bulk (multiple records)
439+
B-->>S: 200 OK + per-item results
440+
Note over S: Failed items reported via OnFailure callback
436441
end
437442
```
438443

439-
### Storage Pipeline
444+
### Bulk Write Mechanism
440445

441-
From kernel event to storage backend, the process involves three stages: capture, serialization, and concurrent write. The local directory and remote backend are written to in parallel without blocking each other.
446+
#### Buffering and Flush Triggers
442447

443-
```mermaid
444-
flowchart LR
445-
A([Kernel Event]) --> B["Tracer Capture\nSerialize to JSON"]
446-
B --> C["Storage Module\n(concurrent write)"]
447-
C --> D["Write to Local Directory\nhuatuo-local/"]
448-
C --> E["Write to ES / OpenSearch\nIndex API"]
449-
```
448+
| Parameter | Value | Meaning |
449+
|-------------------|---------------------|-------------------------------------------------|
450+
| `FlushBytes` | 5 MB | Flush when accumulated bytes reach the threshold |
451+
| `FlushInterval` | 1 s | Force-flush 1 second after the previous flush |
452+
| `NumWorkers` | 4 | Concurrent workers submitting Bulk requests |
453+
| Process shutdown | `Close(ctx)` | SIGTERM/SIGINT triggers a 10 s bounded drain |
454+
455+
#### Two-Tier Retry Policy
456+
457+
Bulk failures are split into two layers with different retry semantics:
458+
459+
| Layer | Trigger | Behavior | Retried? |
460+
|----------------------|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|----------|
461+
| **Whole-batch retry** | Transport error (connect / timeout / TLS)<br>HTTP status: `429 / 502 / 503 / 504` | Client retries with exponential backoff: 100 ms → 200 ms → 400 ms → 800 ms, up to **3 attempts** | ✅ auto |
462+
| **Whole-batch reject**| HTTP status: `400 / 401 / 403 / 404 / 413`, etc. | Not retried; all records in the batch are dropped, an error is logged via `OnError` | ❌ drop |
463+
| **Per-item failure** | 200 OK with per-item error: version conflict, mapping error, document too large| Not retried; only the failed item is dropped, `OnFailure` logs `index/id/status/type/reason` | ❌ drop |
464+
| **Per-item success** | 200 OK with per-item success | Considered durably indexed ||
465+
466+
**Why this design**: 429/5xx and transport errors signal transient remote unavailability where retries are effective; 4xx (except 429) and per-item errors are client-side semantic issues (data shape, permissions) where retries would only amplify the failure — they should be surfaced via logs for human investigation.
467+
468+
#### Data-Loss Scenarios
469+
470+
In all three scenarios below, `Save` returns `nil` but the event never reaches the index:
471+
472+
1. **Abnormal process exit**: `SIGKILL` or host power loss drops whatever is still buffered in the BulkIndexer (the local directory still keeps a copy).
473+
- Mitigation: SIGTERM/SIGINT trigger graceful shutdown; `Close` force-flushes the buffer with a 10 s deadline.
474+
2. **Whole-batch permanent rejection**: 4xx (non-429) errors discard every record in the batch. Common causes: disabled index, expired credentials, document exceeding the cluster's `http.max_content_length`.
475+
- Diagnosis: `OnError` log includes ES's `type` and `reason`.
476+
3. **Permanent per-item failure**: mapping conflict, version conflict, malformed document.
477+
- Diagnosis: `OnFailure` log identifies the record by `index/id`.
478+
479+
> **The local directory is always a fallback**: even if remote writes are lost, events remain available in `huatuo-local/` as the eventual-consistency safety net.
480+
481+
#### Problems This Solves
482+
483+
Replacing per-event Index API calls with a buffered BulkIndexer + auto-retry addresses four classes of problems:
484+
485+
| Problem | Old approach bottleneck | Bulk approach improvement |
486+
|------------------------------------------|---------------------------------------------------------|----------------------------------------------------------------------|
487+
| **TLS handshake CPU cost** | One HTTPS handshake per event saturated CPU under FIPS/RSA-PSS | Many events share one connection and one handshake; TLS PSK tickets cached |
488+
| **Remote RTT throughput ceiling** | One round-trip per event capped node-level write rate | One Bulk request carries up to 5 MB; throughput scales with batch size |
489+
| **Transient remote jitter / 429 throttle** | A single failure dropped the event with no retry | Client-level retry absorbs short-lived faults |
490+
| **Decoupling tracer layer from backend** | Slow remote backed pressure into capture, delaying tracing | Async buffer decouples capture from network — capture is no longer blocked on remote latency |
450491

451492
---
452493

docs/best-practice/storage_zh.md

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,8 @@ curl -k -u elastic:123456 \
387387

388388
HUATUO Storage 模块部署在节点上,将采集到的内核事件同时写入本地目录和 Elasticsearch 或 OpenSearch。两种存储后端共用同一套 `[Storage.ES]` 配置接口,通过地址区分。
389389

390+
写入远端时使用 ES/OpenSearch 的 **Bulk API**`_bulk`):事件先进入节点内的批量缓冲,由后台 worker 按"大小或时间"的阈值聚合后一次提交多条记录,并在传输层失败时按策略自动重试。
391+
390392
```mermaid
391393
graph TB
392394
subgraph kernel["Linux 内核"]
@@ -397,7 +399,7 @@ graph TB
397399
subgraph huatuo["HUATUO Agent(节点级)"]
398400
T["采集层"]
399401
L["本地目录\nhuatuo-local/"]
400-
S["Storage 模块\n同步写入"]
402+
S["Storage 模块\nBulkIndexer 缓冲"]
401403
end
402404
403405
subgraph backends["存储后端"]
@@ -408,41 +410,80 @@ graph TB
408410
kernel --> T
409411
T --> L
410412
T --> S
411-
S -->|Index API| ES
412-
S -->|Index API| OS
413+
S -->|Bulk API + 自动重试| ES
414+
S -->|Bulk API + 自动重试| OS
413415
```
414416

415417
### 数据写入流程
416418

417-
HUATUO 采集到内核事件后,Storage 模块将事件同时写入本地目录和远端存储后端。两路写入并发执行,本地目录保留副本,远端存储提供持久化与查询能力
419+
采集层调用 `Save` 后立即返回,事件落入 BulkIndexer 缓冲;后台 worker 在满足"字节阈值 / 时间阈值 / 进程退出"任一条件时将批次提交至远端。本地目录写入是同步落盘,与远端 Bulk 路径相互独立
418420

419421
```mermaid
420422
sequenceDiagram
421423
participant T as 采集层
422424
participant L as 本地目录(huatuo-local/)
423-
participant S as Storage 模块
425+
participant S as Storage 模块(BulkIndexer)
424426
participant B as ES / OpenSearch
425427
426428
T->>S: 采集到内核事件,序列化为 JSON
427-
par 同时写入
429+
par 本地路径(同步)
428430
S->>L: 写入本地文件
429-
and
430-
S->>B: 写入远端存储(Index API)
431-
B-->>S: 写入确认(200 OK)
431+
and 远端路径(异步批量)
432+
S->>S: 加入 Bulk 缓冲,立即返回
433+
Note over S: 满足 5 MB / 1 s / 退出 任一条件
434+
S->>B: POST /_bulk(多条记录)
435+
B-->>S: 200 OK + per-item 结果
436+
Note over S: 失败项通过 OnFailure 回调记录日志
432437
end
433438
```
434439

435-
### 存储写入流程
440+
### Bulk 批量写入机制
436441

437-
从内核事件产生到写入存储后端,经过采集、序列化、同步写入三个阶段。本地目录与远端存储并发写入,互不阻塞。
442+
#### 缓冲与刷新
438443

439-
```mermaid
440-
flowchart LR
441-
A([内核事件触发]) --> B["采集\n序列化为 JSON"]
442-
B --> C["Storage 模块\n同步写入"]
443-
C --> D["写入本地目录\nhuatuo-local/"]
444-
C --> E["写入 ES / OpenSearch\nIndex API"]
445-
```
444+
| 参数 || 含义 |
445+
|-------------------|---------------------|---------------------------------|
446+
| `FlushBytes` | 5 MB | 缓冲累计达到该字节数立即刷新 |
447+
| `FlushInterval` | 1 s | 距上次刷新满 1 秒后强制刷新 |
448+
| `NumWorkers` | 4 | 并发提交 Bulk 请求的后台 goroutine 数 |
449+
| 进程退出 | `Close(ctx)` | SIGTERM/SIGINT 触发,限时 10 s 内排空缓冲 |
450+
451+
#### 两级重试策略
452+
453+
Bulk 请求的失败语义分为两层,重试范围不同:
454+
455+
| 层级 | 触发条件 | 处理方式 | 是否重试 |
456+
|----------------|-----------------------------------------------------|-------------------------------------------------------------------------------------------------------|----------|
457+
| **整批失败** | 传输错误(连接失败、超时、TLS)<br>HTTP 状态:`429 / 502 / 503 / 504` | 客户端按指数退避自动重试:100 ms → 200 ms → 400 ms → 800 ms,最多 **3 次** | ✅ 自动 |
458+
| **整批拒绝** | HTTP 状态:`400 / 401 / 403 / 404 / 413`| 不重试,整批所有记录全部丢弃,并通过 `OnError` 写错误日志 | ❌ 丢弃 |
459+
| **单条失败** | 200 OK 但 per-item 失败:版本冲突、字段映射错误、文档过大 | 不重试,仅该单条丢弃,通过 `OnFailure` 回调记录 `index/id/status/type/reason` | ❌ 丢弃 |
460+
| **单条成功** | 200 OK 且 per-item 成功 | 视为已落库 ||
461+
462+
**为什么这样设计**:429/5xx 与传输错误是远端短暂不可用的信号,重试有效;4xx(除 429)与 per-item 错误是客户端语义问题(数据格式、权限),重试只会放大错误,应交给开发与运维侧排查日志后修正。
463+
464+
#### 数据丢失场景
465+
466+
下列三种情况下,调用方调用 `Save` 时返回 `nil`,但事件最终未进入索引:
467+
468+
1. **进程异常退出**`SIGKILL` 或宿主机断电时,BulkIndexer 内存缓冲尚未刷新的部分直接丢失(仅本地目录保留副本)。
469+
- 缓解:SIGTERM/SIGINT 走优雅退出路径,shutdown 时调用 `Close` 强制 flush,最长等待 10 秒。
470+
2. **整批被永久拒绝**:4xx(非 429)类错误一次性丢弃整批所有记录。常见诱因:索引被禁用、密码失效、单条文档超过集群 `http.max_content_length`
471+
- 排查:`OnError` 错误日志包含 ES 返回的 `type``reason`
472+
3. **单条永久失败**:mapping 冲突、版本冲突、文档语法错误。
473+
- 排查:`OnFailure` 错误日志按 `index/id` 定位失败记录。
474+
475+
> **本地目录始终保留副本**:即使远端写入丢失,事件仍可从 `huatuo-local/` 中找回,作为最终一致性的兜底。
476+
477+
#### 解决的问题
478+
479+
将"逐事件 Index API"换成"BulkIndexer 批量 + 自动重试"主要解决以下四类问题:
480+
481+
| 问题 | 旧方案瓶颈 | Bulk 方案的改进 |
482+
|-----------------------------------|---------------------------------------------|-------------------------------------------------------|
483+
| **TLS 握手 CPU 开销** | 每事件一次 HTTPS,握手在 FIPS / RSA-PSS 下占满 CPU | 多条事件复用单连接 + 单次握手;TLS PSK ticket 缓存复用 |
484+
| **远端 RTT 与吞吐** | 每事件一次往返,节点级写入受 RTT 限制 | 单次 Bulk 请求最多 5 MB,吞吐随批大小线性提升 |
485+
| **远端短暂抖动 / 限流(429)** | 单次失败立即丢弃,无重试 | 客户端层面自动重试,吸收瞬态故障 |
486+
| **采集层对存储后端解耦** | 远端慢会回压采集,影响内核事件采集时延 | 异步缓冲将采集与远端写入解耦,采集路径不被网络阻塞 |
446487

447488
---
448489

0 commit comments

Comments
 (0)