L3 - Monitoring Platform - Agent - WAL và Sender (Bộ đệm đĩa, gửi lô, truyền tải)
Thông tin tài liệu đầy đủ
| Trường | Giá trị |
|---|---|
| Tên trang | L3 - Monitoring Platform - Agent - WAL và Sender |
| Trạng thái | BẢN NHÁP (tài liệu chưa sẵn sàng trình thẩm định) |
| Phiên bản | v0.1, 2026-09-30: bản nháp đầu tiên, dựng từ mã tại commit a23f929 (internal/buffer, internal/sender, internal/backoff, internal/transport, internal/wire, internal/stats, internal/agent/agent.go, internal/cli/run.go) và hợp đồng Agent - Collector ở docs/10-wire-contract.md |
| Tên dự án | Monitoring Platform (Access Hub Monitoring) |
| Bên thẩm định / Phê duyệt | chưa chỉ định. Chưa ai sign-off |
| Tài liệu tầng trên (Parent) | L2 - Monitoring Platform - Agent - SAD, thành phần CMP-3 WAL Buffer, CMP-4 Sender và Backoff, CMP-5 Transport Client (mục 2.3) và vòng cycle của CMP-1 Runtime Core (L2 D-14). FR-08 đến FR-13, FR-03, L2-NFR-04, 05, 07, 10, 11, 13, 14, 15, 18. Truy vết tiếp lên L1: L1 HLD mục 2 (Mục tiêu 4), mục 3 (T5, T7), mục 4 |
| Tài liệu anh em | L3 Collectors, L3 Enroll, Credentials, Config, L3 Service và Packaging |
| Mục lục | 0 Governance, 1 Phạm vi, 2 Yêu cầu, 3 Kiến trúc, 4 Domain model, 5 Hợp đồng API, 6 Dữ liệu vật lý, 7 Thuật toán, 8 Xử lý lỗi, 9 Suy thoái, 10 Đồng thời, 11 Bảo mật, 12 Cấu hình, 13 Telemetry, 14 Kiểm thử, 15 Trình tự xây dựng, Phụ lục A, B, C |
Quy ước nhãn trạng thái (giống L2): ĐÃ HIỆN THỰC (đã xác minh trong mã), THIẾT KẾ, CHƯA XÂY (chỉ có trong tài liệu thiết kế), MỘT PHẦN, ĐỀ XUẤT. Khi mã và tài liệu khác nhau, mã thắng và sai lệch được nêu trong tài liệu này và gom về L2 mục 16.2. Mọi con số chưa có trong mã hoặc giao thức được gắn nhãn "đề xuất".
0. Front Matter & Approvals
Governance metadata
| Trường | Giá trị |
|---|---|
| Component | CMP-3 WAL Buffer (internal/buffer), CMP-4 Sender và Backoff (internal/sender, internal/backoff), CMP-5 Transport Client (internal/transport, internal/wire), cộng vòng cycle của CMP-1 (internal/agent/agent.go, internal/cli/run.go, internal/stats) |
| Truy vết L2 | L2-SAD-agent.md mục 2.3, 3 (FR-03, FR-08 đến FR-13), 4 (L2-NFR-04, 05, 07, 10, 11, 13, 14, 15, 18), 7, 16 (R-07, R-09, R-10, D-08, D-09, D-10, D-14, D-15, D-16) |
| Phân loại rủi ro | Đề xuất, chưa xác nhận: Tier 2, cao hơn mức kế thừa của L2 mục 14. Lý do: đây là đường duy nhất đưa số liệu ra khỏi máy, giữ bearer token trong bộ nhớ và là nơi mất số liệu âm thầm (R-07, R-10) |
| Data classification | Nội bộ (số liệu vận hành máy chủ, không PII) cho lô và WAL. Bí mật cho bearer token trong bộ nhớ và trên dây (chỉ giữ ở Sender.Token và header Authorization, không ghi đĩa ở thành phần này) |
| Blast radius | Một máy chủ. Lỗi nặng nhất: WAL hỏng nhiều đoạn làm mất số liệu đệm của máy đó, hoặc bug hợp đồng làm rơi hàng loạt lô (R-10). Thành phần không ảnh hưởng máy khác. Nhiều agent cùng thử lại có thể tạo đỉnh tải lên Collector, giảm bằng phân pha và full jitter |
Sign-off gate
| Vai trò | Tên | Trách nhiệm duyệt | Trạng thái | Ngày |
|---|---|---|---|---|
| Tech lead thành phần | chưa chỉ định | Đúng đắn của WAL, backoff, xử lý mã trạng thái | Chưa duyệt | chưa có |
| SA hệ thống Agent | chưa chỉ định | Nhất quán với L2 và giao thức | Chưa duyệt | chưa có |
| Bảo mật | chưa chỉ định | Kênh TLS, xử lý token, quyền tệp WAL | Chưa duyệt | chưa có |
| SA Collector | chưa chỉ định | Nhất quán hợp đồng /metrics, giới hạn tốc độ, cửa sổ thời gian điểm | Chưa duyệt | chưa có |
1. Component Scope & Non-Goals
Vai trò: cụm này là bộ gửi số liệu theo mẫu hàng đợi bền có xác nhận (durable at-least-once queue) đi kèm bộ gửi thử lại có lùi bước (retrying sender with backoff). Mỗi chu kỳ, mẫu đã chuẩn hóa được đóng thành một lô protobuf, ghi xuống WAL đĩa (có fsync), rồi Sender lấy lô cũ nhất, nén gzip và gửi HTTPS. Lô chỉ bị xóa khỏi WAL khi Collector xác nhận (200 hoặc 202) hoặc khi bị quyết định bỏ theo mã trạng thái. Gửi lặp an toàn vì TSDB khử trùng theo (series, ts) (giao thức, mục idempotency).
Sơ đồ ngữ cảnh
flowchart LR
classDef bc fill:#1f3a5f,stroke:#4a90d9,color:#fff
classDef owned fill:#2d4a3e,stroke:#5fb37a,color:#fff
classDef datastore fill:#3a2d4a,stroke:#a06fd9,color:#fff
classDef infra fill:#444,stroke:#aaa,color:#fff
COLL(["Collector: /agent/v1"]):::infra
DISK[("Thư mục state: wal/")]:::datastore
subgraph BC["WAL, Sender, Transport"]
RT["cycle: thu, xếp hàng, gửi"]:::owned
SND["Sender và Backoff"]:::owned
WAL["WAL Buffer"]:::owned
TRN["Transport Client"]:::owned
end
ENG["Collector Engine"]:::bc
CFG["Config và Credentials"]:::bc
ENG -->|"mẫu đã chuẩn hóa"| RT
RT -->|"đóng lô và gửi"| SND
SND -->|"ghi và đọc lô"| WAL
WAL -->|"đoạn tệp có fsync"| DISK
SND -->|"POST metrics đã nén"| TRN
TRN -->|"HTTPS protobuf"| COLL
CFG -.->|"token, URL, tham số"| RTMô tả quan hệ
| Chiều | Bên | Nội dung |
|---|---|---|
| Vào | Collector Engine | Result.Samples mỗi chu kỳ (L3 Collectors). cycle gọi Collect(now) rồi Sender.Enqueue(now, samples) |
| Vào | Config và Credentials | agent_id, agent_token, machine_id (từ creds), collector_url, ca_file, proxy_url, insecure_skip_verify, send_timeout, interval, buffer.*, limits.memory_limit (từ config) |
| Vào | Tín hiệu hệ điều hành | SIGTERM và SIGINT hủy ngữ cảnh. SIGHUP nạp lại cấu hình cục bộ. Tín hiệu trạng thái ghi log Status() (internal/cli/run.go) |
| Ra | Thư mục state_dir/wal | Đoạn %016x.seg và tệp checkpoint. Thư mục quyền 0700, tệp 0600 |
| Ra | Collector | POST /agent/v1/metrics (lô nén gzip, application/x-protobuf) và GET /agent/v1/config (chỉ thăm dò lúc 401). Ping và Enroll có trong client, Enroll thuộc L3 Enroll |
| Ra | Thống kê dùng chung (internal/stats) | Ghi SendFailures, DroppedSamples, WALBytes, WALBatches, độ lệch đồng hồ. Bộ thu tự thân đọc lại để phát agent_*, và AgentStats mang các số này trong từng lô |
| Ra | Log (logx) | Sự kiện agent started, delivery failed, backing off, collector rejected a batch, dropping it, wal dropped old batches, wal recovered with damage |
Trong phạm vi (bao phủ)
| Trong phạm vi | Ghi chú |
|---|---|
Interface Queue và Entry | internal/buffer/buffer.go. ĐÃ HIỆN THỰC |
Hàng đợi RAM có giới hạn Memory (phương án dự phòng) | internal/buffer/buffer.go. ĐÃ HIỆN THỰC. Mất dữ liệu khi khởi động lại |
| WAL đĩa: bản ghi có CRC, đoạn, checkpoint, phục hồi, giới hạn dung lượng và tuổi | internal/buffer/wal.go. ĐÃ HIỆN THỰC |
Đóng lô (BuildBatch), seq, AgentStats, lô rỗng làm heartbeat | internal/sender/sender.go. ĐÃ HIỆN THỰC |
Máy trạng thái giao nhận (ok, backoff, unauthorized, agent_revoked) | internal/sender/sender.go. ĐÃ HIỆN THỰC |
| Xử lý từng mã trạng thái, tách lô khi 413, gửi bù, thăm dò cấu hình khi 401 | internal/sender/sender.go. ĐÃ HIỆN THỰC |
Lịch backoff full jitter, Retry-After, độ lệch pha khởi đầu | internal/backoff/backoff.go. ĐÃ HIỆN THỰC |
| Client HTTPS: TLS, ghim CA, proxy, header giao thức, giới hạn kích thước, không theo redirect | internal/transport/transport.go. ĐÃ HIỆN THỰC |
Mã hóa dây (protobuf sinh từ agent.proto) | internal/wire. ĐÃ HIỆN THỰC |
Vòng cycle, dừng khi lệch machine_id, chọn WAL hay RAM, nạp lại cấu hình truyền tải | internal/agent/agent.go, internal/cli/run.go. ĐÃ HIỆN THỰC. CMP-1 chưa có L3 riêng nên mô tả ở đây (L2 D-14) |
Gọi OnConfigETag để kéo cấu hình từ xa | MỘT PHẦN: Sender có móc OnConfigETag và có kiểm thử, nhưng agent.go không gán (L2 D-01). THIẾT KẾ, CHƯA XÂY ở phía Runtime (AGT-10) |
POST /inventory | ĐÃ XÂY (AGT-9): transport.PostInventory, bộ gửi riêng internal/inventory.Reporter ngoài WAL (báo cáo mới nhất thay báo cáo cũ, không cần đệm) |
POST /credentials/renew | ĐÃ HIỆN THỰC (AGT-10). Sender đọc token qua TokenFunc nên token mới có hiệu lực ngay |
GET /update | THIẾT KẾ, CHƯA XÂY (L2 D-08, AGT-12) |
Ngoài phạm vi
| Không thuộc BC | Thuộc về |
|---|---|
Đọc /proc, chuẩn hóa mẫu, cắt series | L3 Collectors |
Đọc và kiểm tra cấu hình, ghi credentials.json, luồng enroll, SIGHUP nạp tệp | L3 Enroll, Credentials, Config |
Thư mục state_dir, quyền tài khoản, unit systemd, gói cài | L3 Service và Packaging |
| Nhận lô, kiểm tra hợp lệ, giới hạn tốc độ, ghi TSDB, phát hiện im lặng | Collector (L2 Collector) |
| Hiệu chỉnh mẫu theo độ lệch đồng hồ | ĐỀ XUẤT, chưa có. Agent chỉ ghi và báo độ lệch (L2 FR-12) |
2. Detailed Requirements & Acceptance Criteria
Functional Requirements
| # | Trách nhiệm | Giải thích | Hiện thực ở |
|---|---|---|---|
| FR-W01 | Đóng lô | Mỗi mẫu thành một Series với một Point, dấu thời gian là thời điểm thu (now, ms), không phải lúc gửi. Seq tăng dần, khởi đầu từ unix ms. Kèm SentAtMs và AgentStats | sender.go (BuildBatch, SeedSeq). ĐÃ HIỆN THỰC |
| FR-W02 | Heartbeat | Chu kỳ không có mẫu vẫn tạo và gửi lô rỗng. Yêu cầu metrics đồng thời là heartbeat | sender.go (Enqueue). ĐÃ HIỆN THỰC |
| FR-W03 | Ghi bền | Mỗi lô được nén deflate, gắn tiêu đề 24 byte có CRC32C, ghi vào đoạn hiện hành rồi Sync. Lỗi ghi thì cắt lại về kích thước cũ | wal.go (Append). ĐÃ HIỆN THỰC |
| FR-W04 | Phục hồi | Khi mở: quét đoạn, cắt đuôi rách của đoạn cuối, bỏ qua bản ghi sai CRC hoặc id không tăng, đọc checkpoint, không cấp lại id đã dùng | wal.go (OpenWAL, recover, scanRecords). ĐÃ HIỆN THỰC |
| FR-W05 | Giới hạn WAL | Xóa bản ghi quá buffer.max_age (từ đầu tới bản ghi tươi đầu tiên), rồi xóa nguyên đoạn cũ nhất khi tổng vượt buffer.max_bytes, không bao giờ xóa đoạn cuối | wal.go (enforce). ĐÃ HIỆN THỰC |
| FR-W06 | Gửi theo thứ tự cũ nhất trước, có gửi bù | Mỗi tick gửi lô cũ nhất, rồi thêm tối đa catch_up_batches lô nữa, dừng ở lỗi đầu tiên | sender.go (Tick). ĐÃ HIỆN THỰC |
| FR-W07 | Backoff | Lỗi thử lại: chờ ngẫu nhiên đều trong [0, hiện tại], hiện tại bắt đầu 5 s, nhân đôi tới 300 s, lấy giá trị lớn hơn giữa jitter và Retry-After (trần 1 giờ). Thành công thì đặt lại | backoff.go, sender.go (failure, success). ĐÃ HIỆN THỰC |
| FR-W08 | Xử lý mã trạng thái | 200, 202: xóa lô. 400, 422: bỏ cả lô và đếm điểm. 413: tách đôi và gửi mỗi nửa một lần. 401: dừng gửi và thăm dò GET /config mỗi 10 phút. 403: dừng vĩnh viễn, xóa token trong bộ nhớ, giữ dữ liệu. 426: đặt cờ nâng cấp, giữ lô, thử lại theo backoff. 429, 5xx, lỗi mạng: giữ lô, backoff | sender.go (post, handle, failure, splitAndSend, probe). ĐÃ HIỆN THỰC |
| FR-W09 | Kênh truyền an toàn | TLS 1.2 trở lên, xác minh chứng chỉ (InsecureSkipVerify chỉ khi bật rõ), ghim CA từ tệp PEM, proxy từ môi trường hoặc cấu hình, không theo redirect, giới hạn 1 MiB nén và 20.000 điểm | transport.go. ĐÃ HIỆN THỰC |
| FR-W10 | Giao thức | Mọi yêu cầu có X-AH-Proto: 1, X-AH-Agent-Version, X-Request-Id (16 ký tự hex), User-Agent, và Authorization: Bearer khi có token. Chấp nhận phản hồi JSON hoặc protobuf | transport.go (do, decodeInto). ĐÃ HIỆN THỰC |
| FR-W11 | Độ lệch đồng hồ | Sau mỗi ack có server_time_ms, ghi (server_time_ms - now)/1000 giây vào thống kê. Chỉ ghi và báo | sender.go (success). ĐÃ HIỆN THỰC |
| FR-W12 | Phân pha | Chu kỳ đầu lệch sha256(agent_id)[0:8] mod interval để các agent không gửi cùng lúc | backoff.go (PhaseOffset), agent.go. ĐÃ HIỆN THỰC |
| FR-W13 | Vòng chu kỳ | Mồi engine một lần, rồi mỗi chu kỳ: Collect, Enqueue, Tick. Lỗi Enqueue được ghi log nhưng Tick vẫn chạy | agent.go (Run, cycle). ĐÃ HIỆN THỰC |
| FR-W14 | Bảo vệ danh tính | Nếu machine_id hiện tại khác lúc enroll, dừng gửi, ghi lỗi identity_mismatch, chờ tới khi hủy ngữ cảnh | agent.go (Run). ĐÃ HIỆN THỰC (L2 FR-03) |
| FR-W15 | Phương án dự phòng RAM | Nếu không mở được WAL, dùng hàng đợi RAM giới hạn buffer.max_bytes, ghi lỗi | agent.go (openQueue), buffer.go (Memory). ĐÃ HIỆN THỰC |
| FR-W16 | Nạp lại truyền tải | SIGHUP đổi interval, catch_up_batches, và dựng lại client nếu khóa truyền tải (url|ca|proxy|insecure|send_timeout) đổi | agent.go (Reload, vòng chính). ĐÃ HIỆN THỰC. Không đổi kích thước WAL và không dựng lại engine |
| FR-W17 | Kéo cấu hình từ xa khi ack đổi config_etag | Sender gọi OnConfigETag một lần cho mỗi etag mới. Runtime chưa gán móc | MỘT PHẦN (L2 D-01, AGT-10) |
| FR-W18 | Tắt êm | Khi ngữ cảnh bị hủy, trả về nil ("agent stopped"), không xả lô. Lô chưa gửi ở lại WAL | agent.go. ĐÃ HIỆN THỰC (không có bước xả, OQ-W7) |
Non-Functional Requirements
| NFR | Target (Ý nghĩa) | Parent L2-NFR (Kiểu) | Satisfied-by (Tactic → Mục) |
|---|---|---|---|
| NFR-W01 | Sau mỗi lần ghi, đĩa cho WAL không vượt buffer.max_bytes (mặc định 50 MiB) khi còn từ hai đoạn trở lên, và không giữ lô quá buffer.max_age (24 giờ) | L2-NFR-04 (Allocated) | Xóa theo tuổi rồi theo đoạn (7.4.2), tham số ở 12.1. Đoạn cuối không bao giờ bị xóa, nên một bản ghi đơn lẻ lớn hơn max_bytes là ngoại lệ (7.4.2) |
| NFR-W02 | Mất điện đột ngột chỉ mất lô đang ghi, WAL không hỏng | L2-NFR-07 (Allocated) | fsync mỗi Append, CRC32C, cắt đuôi rách, checkpoint ghi bằng tệp tạm và đổi tên (7.2 luồng 8, 7.4.1). TestWALSurvivesKill9 |
| NFR-W03 | Kích thước lô gửi không vượt 1 MiB nén và 20.000 điểm, lô quá lớn được tách một lần | L2-NFR-05 (Allocated) | Kiểm cục bộ, ErrTooLarge, splitAndSend (7.4.4). Mục tiêu 10 KB chưa đo (L2 D-16) |
| NFR-W04 | Kênh TLS 1.2 trở lên, xác minh chứng chỉ, không theo redirect | L2-NFR-11 (Allocated) | New dựng tls.Config, CheckRedirect trả ErrUseLastResponse (11) |
| NFR-W05 | Token không xuất hiện trong log, lỗi hay String() | L2-NFR-13 (Allocated) | Lỗi mạng chỉ mang URL, Sender.String() không có token (11). TestTokenNeverReachesTheLogs, TestNetworkErrorsNeverContainTheToken |
| NFR-W06 | Chu kỳ gửi mặc định 30 s (10 s đến 300 s) với pha lệch ổn định theo agent_id | L2-NFR-14 (Allocated) | PhaseOffset (7.4.3), interval (12.1). TestPhaseOffsetIsStableBoundedAndSpread |
| NFR-W07 | Lịch backoff 5 s tới 300 s, full jitter, Retry-After tối đa 1 giờ | L2-NFR-15 (Allocated) | backoff.Next (7.4.3). TestScheduleDoublesToCapWithFullJitter |
| NFR-W08 | Xử lý 426 không mất lô và không giả vờ nâng cấp | L2-NFR-18 (Allocated) | Cờ upgrade, giữ lô (7.3). TestUpgradeRequiredIsFlaggedAndBatchKept |
| NFR-W09 | Dừng êm khi SIGTERM trong 10 giây | L2-NFR-10 (Allocated một phần) | Hủy ngữ cảnh truyền tới Tick và HTTP. Chưa có phép đo (L2 D-16, OQ-W7) |
| NFR-W10 | Mọi lô bị bỏ hoặc bị WAL xóa phải quan sát được bằng số đếm và log | Không có tương ứng đầy đủ trong L2 (Owned) | Điểm bỏ vì 400, 422, không chia được đã vào DroppedSamples. Lô bị WAL xóa mới chỉ có log (D-10, OQ-W5). Metric ở 13.1 |
| NFR-W11 | Thử lại không tạo bão yêu cầu lên Collector: tốc độ trung bình không vượt hạn mức của giao thức | Kế thừa ANFR (truyền) và giới hạn 4 yêu cầu mỗi phút mỗi agent của giao thức (Owned) | Backoff, catch_up_batches. Rủi ro đã biết ở OQ-W1: gửi bù tối đa 3 yêu cầu mỗi tick |
Acceptance Criteria
| AC | Kịch bản đặc tả (Given / When / Then) | Truy vết → Test ID |
|---|---|---|
| AC-W01 | Given lô được Append rồi Remove, When đọc Oldest, Then thứ tự vào trước ra trước và Oldest không tự xóa | TestWALFifoRoundTrip, TestWALOldestDoesNotRemove |
| AC-W02 | Given lô đã ack rồi tiến trình khởi động lại, When mở WAL, Then lô đã ack không gửi lại và id không bao giờ quay về | TestWALSurvivesRestartWithoutResendingAcknowledged, TestWALIdsNeverRestartAfterEmptyAndReopen |
| AC-W03 | Given WAL đầy nhiều đoạn, When Append vượt giới hạn, Then đoạn cũ nhất bị xóa và đoạn đã xong bị dọn | TestWALRotatesAndDeletesFinishedSegments, TestWALSizeCapDropsOldestSegments |
| AC-W04 | Given bản ghi quá max_age, When Append, mở hoặc Oldest, Then bản ghi bị bỏ và đếm | TestWALAgeCapDropsExpiredBatches, TestWALAgeCapAppliesOnOpenAndOldest |
| AC-W05 | Given bản ghi sai CRC (lúc phục hồi hoặc sau khi mở), When đọc, Then bản ghi bị bỏ, đếm là hỏng, các bản ghi khác còn | TestWALSkipsRecordWithBadCRCOnRecovery, TestWALDetectsCorruptionAfterOpen |
| AC-W06 | Given tiến trình bị kill -9 giữa lúc ghi, When mở lại, Then WAL đọc được và đuôi rách bị cắt | TestWALSurvivesKill9, TestWALTruncatesTornTail, FuzzWALScanRecords |
| AC-W07 | Given Remove sai thứ tự hoặc id lạ, When gọi, Then không lỗi và không xóa nhầm | TestWALOutOfOrderRemoveAndUnknownIDs |
| AC-W08 | Given WAL đã tạo, When kiểm quyền, Then thư mục 0700 và tệp 0600. Given WAL đã đóng, Then mọi thao tác bị từ chối | TestWALFilesAreOwnerOnly, TestWALClosedRejectsUse, TestWALOpenRequiresDir |
| AC-W09 | Given hàng đợi RAM đầy, When thêm lô, Then bỏ lô cũ nhưng luôn giữ lô mới nhất, dữ liệu được sao chép | TestMemoryEvictsOldestWhenFull, TestMemoryKeepsNewestEvenWhenOversized, TestAppendCopiesData |
| AC-W10 | Given Collector trả 200, When Tick, Then lô bị xóa, độ lệch đồng hồ cập nhật, lô rỗng vẫn được gửi làm heartbeat | TestSuccessDeliversAndUpdatesSkew, TestEmptyBatchIsAHeartbeat |
| AC-W11 | Given 5xx hoặc mất kết nối, When Tick, Then lô được giữ, Sender vào trạng thái backoff, Retry-After được tôn trọng | TestServerErrorKeepsBatchAndBacksOff, TestDroppedConnectionKeepsBatch, TestRetryAfterIsHonored |
| AC-W12 | Given 400 hoặc 422, When Tick, Then lô bị bỏ và điểm được đếm vào DroppedSamples | TestBadRequestAndUnprocessableAreDropped |
| AC-W13 | Given 413, When Tick, Then lô tách đôi và mỗi nửa gửi một lần. Nửa vẫn quá lớn thì bỏ. Nửa gặp lỗi tạm thời thì xếp lại | TestTooLargeIsSplitAndSentOnce, TestSplitHalfStillTooLargeIsDropped, TestSplitRequeuesUnsentHalvesOnRetryableFailure |
| AC-W14 | Given lô có hơn 20.000 điểm hoặc không thể tách, When gửi, Then tách hoặc bỏ cục bộ, không gọi mạng vô ích | TestOversizePointCountIsSplitLocally, TestUnsplittableOversizeBatchIsDropped |
| AC-W15 | Given 401, When Tick, Then dừng gửi và thăm dò GET /config. Given 403, Then dừng vĩnh viễn, giữ dữ liệu | TestUnauthorizedStopsSendingAndProbesConfig, TestRevokedStopsPermanentlyAndKeepsData |
| AC-W16 | Given 426, When Tick, Then đặt cờ nâng cấp, giữ lô, cờ xóa sau lần gửi thành công | TestUpgradeRequiredIsFlaggedAndBatchKept |
| AC-W17 | Given hàng đợi 6 lô và CatchUp 2, When một Tick, Then gửi 3 lô cũ nhất trước | TestCatchUpSendsOldestFirstWithinLimit |
| AC-W18 | Given ack đổi config_etag, When Tick, Then OnConfigETag gọi một lần cho mỗi etag mới | TestConfigETagCallbackFiresOncePerChange |
| AC-W19 | Given bản ghi không đọc được (giải mã protobuf lỗi), When gửi, Then bị bỏ có log và không chặn hàng đợi | TestUnreadableEntryIsDropped |
| AC-W20 | Given nhiều lô, When đóng, Then seq tăng dần và khởi đầu từ thời gian | TestSequenceNumbersIncreaseAndSeedFromTime |
| AC-W21 | Given lỗi mạng hoặc log, When gửi, Then token không xuất hiện | TestTokenNeverReachesTheLogs, TestNetworkErrorsNeverContainTheToken |
| AC-W22 | Given yêu cầu gửi, When kiểm header, Then có X-AH-Proto, phiên bản agent, request id, bearer. Redirect không được theo. Lô nén quá 1 MiB bị chặn cục bộ | TestPostMetricsSendsProtocolHeaders, TestRedirectsAreNotFollowed, TestOversizeBatchIsRejectedLocally |
| AC-W23 | Given phản hồi lỗi có JSON, When phân tích, Then lấy code và message (cắt 200 ký tự), hiểu Retry-After dạng giây hoặc ngày HTTP | TestAPIErrorParsing, TestRetryAfterHTTPDate |
| AC-W24 | Given GET /config có ETag, When gọi lại, Then gửi If-None-Match và 304 cho kết quả rỗng | TestGetConfigETag |
| AC-W25 | Given phản hồi enroll thiếu trường, When phân tích, Then bị từ chối. Given phản hồi proto hoặc JSON, Then giải mã đúng | TestEnrollDecodesProtoAndJSON, TestEnrollRejectsIncompleteResponse |
| AC-W26 | Given tùy chọn client sai, When New, Then bị từ chối | TestNewValidatesOptions |
| AC-W27 | Given lịch backoff, When gọi Next nhiều lần, Then nhân đôi tới trần với jitter trong khoảng, Retry-After lớn hơn thắng và bị chặn ở 1 giờ, Reset bắt đầu lại | TestScheduleDoublesToCapWithFullJitter, TestJitterStaysInRange, TestRetryAfterWinsWhenLargerAndIsCapped, TestResetRestartsSchedule |
| AC-W28 | Given nhiều agent_id, When tính pha, Then ổn định, nằm trong interval và phân tán | TestPhaseOffsetIsStableBoundedAndSpread |
| AC-W29 | Given agent chưa enroll, When Run, Then trả ErrNotEnrolled. Given machine_id lệch, Then dừng gửi. Given bị thu hồi, Then không liên lạc Collector nữa | TestRunWithoutCredentialsIsNotEnrolled, TestMachineIDMismatchStopsDelivery, TestRevokedAgentStopsContactingTheCollector |
| AC-W30 | Given agent chạy, When qua mỗi interval, Then một lô được gửi. Pha làm trễ lần đầu. Reload đổi được Collector đích | TestRunDeliversBatchesEveryInterval, TestPhaseOffsetDelaysTheFirstSend, TestReloadSwitchesCollector |
| AC-W31 | Given lô đang đệm và agent khởi động lại, When chạy lại, Then lô được gửi. Given thư mục state không dùng được, Then dùng RAM | TestBufferedBatchesSurviveRestart, TestUnusableStateDirFallsBackToMemory |
| AC-W32 | Given engine hỏng, When Run, Then trả lỗi | TestBrokenEngineFailsRun |
Quality Attribute Scenarios
| Mã kịch bản / NFR | Nguồn & Kích thích | Môi trường | Phản hồi của hệ thống (Tactic) | Thước đo chất lượng (Measure) |
|---|---|---|---|---|
| QAS-W01 / NFR-W02 | Mất điện hoặc kill -9 đúng lúc ghi lô | Chạy bình thường | WAL cắt đuôi rách, đọc lại các bản ghi còn nguyên vẹn, log wal recovered with damage | Mất tối đa lô đang ghi (khoảng một interval). Không có bản ghi hỏng được gửi đi. Mô phỏng 20 lần mất điện chưa chạy (L2 D-16) |
| QAS-W02 / NFR-W01 | Mất kết nối tới Collector trong nhiều giờ | Mạng lỗi kéo dài | Lô tích trong WAL, xóa theo tuổi rồi theo đoạn khi vượt giới hạn, ghi log cảnh báo | Đệm giữ tối thiểu 2 giờ (L2-NFR-07). Đĩa không vượt max_bytes sau mỗi lần ghi (tạm thời vượt trong lúc ghi, tối đa một bản ghi). Lỗ hổng số liệu quan sát được ở log và agent_wal_*, chưa ở dropped_samples (D-10) |
| QAS-W03 / NFR-W07 | Collector trả 5xx hoặc 429 với Retry-After | Collector quá tải | Backoff full jitter, tôn trọng Retry-After, giữ lô | Khoảng chờ trong [0, 5 s] rồi nhân đôi tới [0, 300 s], không dưới Retry-After (trần 1 giờ). agent_send_failures_total tăng mỗi lần |
| QAS-W04 / NFR-W11 | Collector hồi phục sau sự cố, hàng nghìn agent có đệm | Sau sự cố | Phân pha, jitter và catch_up_batches giới hạn tốc độ xả | Mỗi agent tối đa 1 + catch_up_batches yêu cầu mỗi tick (mặc định 3, có thể vượt hạn mức 4 yêu cầu mỗi phút, OQ-W1) |
| QAS-W05 / NFR-W03 | Lô vượt 1 MiB nén (nhiều mount, nhiều giao diện) | Chạy bình thường | Tách đôi theo series hoặc theo điểm, gửi mỗi nửa một lần | Không lô nào vượt 1 MiB gửi lên. Nếu không tách được, bỏ và đếm điểm |
| QAS-W06 / NFR-W04, NFR-W05 | Kẻ tấn công chặn đường hoặc chuyển hướng, hoặc lỗi cấu hình proxy | Mạng không tin cậy | Xác minh chứng chỉ, không theo redirect, lỗi chỉ mang URL | Không có token trong log. Kết nối tới chứng chỉ không hợp lệ bị từ chối |
| QAS-W07 / NFR-W08 | Collector nâng giao thức và trả 426 | Nâng cấp lệch phiên bản | Đặt cờ, giữ lô, thử lại theo backoff, hiện upgrade_required ở Status() | Lô không mất. Cờ hiện trong String(). Không có nâng cấp tự động (AGT-12) |
3. Kiến trúc ứng dụng
3.1. Kiến trúc runtime
Toàn bộ cụm chạy trong một tiến trình. Vòng chu kỳ, engine, Enqueue và Tick cùng chạy trên một goroutine (Sender.Enqueue và Sender.Tick bắt buộc gọi từ một goroutine, nếu không seq và nextAttempt không an toàn). Chỉ trạng thái đọc bởi Status() được bảo vệ bằng mutex. Không có goroutine gửi nền và không có hàng đợi trong bộ nhớ giữa engine và WAL. Đây là quyết định ngầm của mã (ADR-W03 ở Phụ lục B).
flowchart LR
classDef bc fill:#1f3a5f,stroke:#4a90d9,color:#fff
classDef owned fill:#2d4a3e,stroke:#5fb37a,color:#fff
classDef datastore fill:#3a2d4a,stroke:#a06fd9,color:#fff
classDef infra fill:#444,stroke:#aaa,color:#fff
RT["Vòng cycle · thu, xếp hàng, gửi"]:::owned
ENG["Collector Engine"]:::bc
SND["Sender · trạng thái giao nhận"]:::owned
BO["Backoff · lịch chờ"]:::owned
WAL["WAL Buffer · bản ghi có CRC"]:::owned
MEM["Hàng đợi RAM · dự phòng"]:::owned
TRN["Transport Client · HTTPS"]:::owned
ST[("Thống kê · bộ đếm chung")]:::datastore
DISK[("Tệp đoạn và checkpoint")]:::datastore
COLL(["Collector · /agent/v1"]):::infra
RT -->|"gọi thu một chu kỳ"| ENG
RT -->|"Enqueue rồi Tick"| SND
SND -->|"Append, Oldest, Remove"| WAL
SND -.->|"dùng khi WAL không mở được"| MEM
WAL -->|"ghi và fsync"| DISK
SND -->|"hỏi thời gian chờ"| BO
SND -->|"POST metrics đã nén"| TRN
TRN -->|"HTTPS protobuf"| COLL
SND -.->|"ghi lỗi, độ lệch, WAL"| STChú giải: nét liền là lời gọi đồng bộ trên cùng goroutine (riêng Transport là HTTP chặn tới send_timeout), nét đứt là lựa chọn lúc khởi động hoặc truy cập trạng thái dùng chung.
Bảng connector
| Connector | Từ | Tới | Cơ chế | Đồng bộ | Ghi chú |
|---|---|---|---|---|---|
| CN-W1 | Vòng cycle | Engine | Engine.Collect(ctx, now) | Đồng bộ | Xem L3 Collectors. Thời gian thu cộng vào chu kỳ |
| CN-W2 | Vòng cycle | Sender | Enqueue(now, samples) rồi Tick(ctx) | Đồng bộ, cùng goroutine | Lỗi Enqueue được ghi log, Tick vẫn chạy |
| CN-W3 | Sender | Hàng đợi (buffer.Queue) | Append(data), Oldest(), Remove(id), Stats() | Đồng bộ. Append chặn tới khi fsync xong | Hiện thực là WAL hoặc Memory, chọn một lần lúc khởi động |
| CN-W4 | WAL | Đĩa | os.File.Write, Sync, Truncate, Rename, Remove | Đồng bộ | Thư mục 0700, tệp 0600 |
| CN-W5 | Sender | Backoff | Next(retryAfter), Reset() | Đồng bộ | Backoff không có trạng thái ngoài bước hiện tại. Nguồn ngẫu nhiên có thể tiêm khi kiểm thử |
| CN-W6 | Sender | Transport | Interface API: PostMetrics(ctx, token, gz), GetConfig(ctx, token, etag) | Đồng bộ, chặn tới send_timeout | Lỗi trả dạng *transport.APIError (mã trạng thái, Retry-After) hoặc ErrTooLarge hoặc lỗi mạng |
| CN-W7 | Transport | Collector | HTTPS, HTTP/2 nếu được, application/x-protobuf, gzip | Đồng bộ | TLS từ 1.2, không theo redirect |
| CN-W8 | Sender, WAL | Thống kê | atomic và sync.Mutex | Đồng bộ | SendFailures, DroppedSamples, WALBytes, WALBatches, độ lệch đồng hồ |
| CN-W9 | Sender | Móc OnConfigETag | Hàm gọi lại | Đồng bộ | Runtime không gán (L2 D-01) nên không tới đâu |
3.2. Kiến trúc module
flowchart TB
classDef pub fill:#1f3a5f,stroke:#4a90d9,color:#fff
classDef intn fill:#3a3a3a,stroke:#888,color:#fff
classDef ext fill:#3a3a3a,stroke:#888,color:#fff
subgraph LA["Lớp điều phối: agent, cli"]
AGT["Run, cycle, Reload, Status"]:::pub
CLIRUN["cli/run: tín hiệu, GOMEMLIMIT"]:::intn
end
subgraph LB["Lớp giao nhận: sender"]
SEND["Sender, API, State"]:::pub
SPLIT["split, requeue, probe"]:::intn
end
subgraph LC["Lớp hạ tầng"]
BUF["buffer: Queue, WAL, Memory"]:::pub
BOFF["backoff: Backoff, PhaseOffset"]:::pub
TRAN["transport: Client, APIError"]:::pub
WIRE["wire: agentv1 MetricsBatch, MetricsAck"]:::pub
end
EXT(["stats, logx, config, creds, collector"]):::ext
CLIRUN --> AGT
AGT --> SEND
SEND --> BUF
SEND --> BOFF
SEND --> TRAN
SEND --> WIRE
TRAN --> WIRE
SEND --> SPLIT
AGT -.-> EXT
SEND -.-> EXT
BUF -.-> EXTChú giải: xanh dương là bề mặt công khai của gói, xám là nội bộ hoặc bên ngoài. Mũi tên liền là phụ thuộc biên dịch, mũi tên đứt là phụ thuộc vào gói dùng chung. sender phụ thuộc vào buffer qua interface Queue và vào transport qua interface API (định nghĩa ở sender), nên kiểm thử thay được cả hai bằng bản giả. buffer không biết wire (nó lưu byte đã marshal, chưa nén).
3.2.1. Cấu trúc: gói buffer
| Giao diện | Người dùng | Hợp đồng |
|---|---|---|
Queue (Append(data) (id, error), Oldest() (Entry, bool, error), Remove(id), Stats(), Close()) | sender, agent | Vào trước ra trước. Oldest không xóa. Remove xóa sau khi xác nhận. Entry{ID, Data} mang MetricsBatch đã marshal, chưa nén |
OpenWAL(WALOptions) | agent.openQueue | WALOptions{Dir, MaxBytes, MaxAge, SegmentBytes, Now, Log}. SegmentBytes bằng 0 lấy mặc định 1 MiB. Tạo thư mục 0700 |
NewMemory(maxBytes) | agent.openQueue (dự phòng) | Sao chép dữ liệu, bỏ lô cũ khi đầy nhưng luôn giữ lô mới nhất |
Stats{Batches, Bytes, Dropped} | sender.syncStats | Batches là số lô còn sống, Dropped cộng dồn theo lô (không phải theo điểm) |
3.2.2. Cấu trúc: gói sender và backoff
| Thành phần | Tệp | Vai trò | Trạng thái giữa các lần gọi |
|---|---|---|---|
Sender | sender/sender.go | Đóng lô, xếp hàng, gửi, xử lý mã trạng thái | seq, nextAttempt, nextProbe, lastETag, lastDropped, và trạng thái có mutex: state, upgrade, lastErr, lastSent |
API (interface) | sender/sender.go | PostMetrics, GetConfig | Không |
State (ok, backoff, unauthorized, agent_revoked) | sender/sender.go | Máy trạng thái giao nhận, xem 7.3 | Có |
Backoff | backoff/backoff.go | Base 5 s, Max 300 s, MaxRetryAfter 1 giờ, nguồn ngẫu nhiên | Bước hiện tại cur |
PhaseOffset(agentID, interval) | backoff/backoff.go | Hàm thuần, không trạng thái | Không |
3.2.3. Cấu trúc: gói transport, wire và vòng cycle
| Thành phần | Tệp | Vai trò | Ghi chú |
|---|---|---|---|
Client (New, Ping, Enroll, PostMetrics, GetConfig) | transport/transport.go | Client HTTPS cho giao thức /agent/v1 | Enroll thuộc L3 Enroll. Ping chưa được vòng chạy dùng |
APIError, ErrTooLarge | transport/transport.go | Lỗi có kiểu để Sender phân nhánh | Thông điệp lỗi cắt 200 ký tự |
agentv1 | wire/accesshub/agent/v1/agent.pb.go | Mã protobuf sinh từ agent.proto | agent.proto.ref là bản chép hợp đồng của Collector, kiểm lệch bằng scripts/check-proto-drift.sh |
Agent.Run, cycle, Reload, Status | agent/agent.go | Điều phối vòng chu kỳ (CMP-1, L2 D-14) | halted khi lệch machine_id. Con trỏ cấu hình nguyên tử |
cli/run.go | cli/run.go | Đặt GOMEMLIMIT, xử lý SIGINT, SIGTERM, SIGHUP, tín hiệu trạng thái | Mã thoát ExitNotEnrolled khi chưa enroll |
4. Domain model
classDiagram
namespace Vung_Giao_Nhan {
class Sender {
<<Aggregate Root>>
+State DeliveryState
+Seq uint64
+NextAttempt time
+CatchUp int
}
class Batch {
<<Entity>>
+Seq uint64
+SentAtMs int64
+Series list
+Stats AgentStats
}
class WalRecord {
<<Entity>>
+Id uint64
+TsMs int64
+Done bool
+Payload bytes
}
class Segment {
<<Entity>>
+FirstId uint64
+Size int64
+Live int
}
class Checkpoint {
<<ValueObject>>
+Watermark uint64
}
class DeliveryState {
<<Enumeration>>
ok
backoff
unauthorized
agent_revoked
}
class BackoffSchedule {
<<ValueObject>>
+Current duration
+Base duration
+Max duration
}
}
Sender "1" o-- "0..*" Batch : đóng và gửi
Sender ..> DeliveryState : đang ở
Sender ..> BackoffSchedule : hỏi thời gian chờ
Batch "1" .. "0..1" WalRecord : được lưu thành
Segment "1" *-- "1..*" WalRecord : chứa
Segment "1..*" --o "1" Checkpoint : dọn tới mốcSơ đồ dùng Sender làm Aggregate Root vì mọi quyết định giữ hay bỏ lô đi qua nó. WalRecord, Segment và Checkpoint là phần bền do buffer sở hữu, Sender chỉ chạm tới chúng qua interface Queue.
Bất biến (invariants)
| Mã | Bất biến | Nơi bảo đảm |
|---|---|---|
| INV-W1 | Id bản ghi tăng ngặt trong WAL và không bao giờ cấp lại, kể cả khi WAL rỗng rồi mở lại (nextID = max(watermark+1, maxid+1)) | recover, Append. TestWALIdsNeverRestartAfterEmptyAndReopen |
| INV-W2 | Một lô chỉ bị Remove khỏi hàng đợi khi (a) Collector trả 200 hoặc 202, hoặc (b) bị quyết định bỏ có log (400, 422, không chia được, không giải mã được, đã tách xong), hoặc (c) WAL tự xóa theo tuổi hoặc dung lượng. Không có đường xóa nào khác | sender.handle, sendEntry, wal.enforce |
| INV-W3 | Mọi bản ghi đọc ra đều đã qua kiểm CRC32C, id khớp và giải nén thành công. Bản ghi hỏng bị bỏ và đếm, không bao giờ được gửi | scanRecords, Oldest |
| INV-W4 | Watermark chỉ tiến, và bằng id lớn nhất mà mọi bản ghi từ đầu tới đó đã xong. WAL rỗng thì bằng nextID - 1 | settle |
| INV-W5 | Đoạn cuối không bao giờ bị xóa bởi giới hạn dung lượng. Sau mỗi lần ghi, tổng dung lượng không vượt MaxBytes trừ khi chỉ còn một đoạn | enforce |
| INV-W6 | seq của Sender tăng ngặt trong một phiên. Khởi đầu bằng unix ms lúc chạy nên không lặp qua các lần khởi động (nếu đồng hồ không lùi). Hai nửa của một lô đã tách dùng chung seq gốc | BuildBatch, SeedSeq, split |
| INV-W7 | Dấu thời gian của điểm là thời điểm thu, không đổi khi thử lại hay gửi bù | BuildBatch |
| INV-W8 | Khi agent_revoked, Sender.Token rỗng và Sender không gọi mạng nữa cho tới khi tiến trình khởi động lại | revoke, Tick |
| INV-W9 | Backoff Next không bao giờ trả về nhỏ hơn min(Retry-After, 1 giờ) | backoff.Next. TestRetryAfterWinsWhenLargerAndIsCapped |
5. API Contract Specification
Cụm này không mở cổng mạng và không có API cho bên ngoài gọi vào. Hợp đồng gồm hai phần: (1) interface Go trong tiến trình (Queue, API) và (2) hợp đồng HTTPS mà cụm này đóng vai client gọi tới Collector (docs/10-wire-contract.md). Mục 5.1 đến 5.5 được điền theo hai phần đó (Phụ lục C).
5.1. Operations (Public API)
| # | Operation / Kênh Event | Loại hình | Hướng gọi (Caller → BC) | Ngữ nghĩa nghiệp vụ |
|---|---|---|---|---|
| 1 | Sender.Enqueue(now, samples) | Lời gọi hàm | cycle → Sender | Đóng lô (kể cả rỗng), marshal, Append vào hàng đợi, đồng bộ thống kê WAL |
| 2 | Sender.Tick(ctx) | Lời gọi hàm | cycle → Sender | Gửi lô cũ nhất và tối đa CatchUp lô nữa. Áp dụng backoff, thăm dò 401, dừng ở lỗi đầu tiên |
| 3 | Sender.SeedSeq(now) | Lời gọi hàm | Run → Sender | Đặt seq khởi đầu bằng unix ms |
| 4 | Sender.State(), String() | Lời gọi hàm | Status() → Sender | Chuỗi delivery=<state> queued=N [upgrade_required=true], không chứa token |
| 5 | buffer.Queue (5 thao tác) | Interface Go | Sender → WAL hoặc Memory | Xem 3.2.1 |
| 6 | POST /agent/v1/metrics | HTTPS, protobuf nén gzip | Transport → Collector | Gửi một lô. Đồng thời là heartbeat |
| 7 | GET /agent/v1/config | HTTPS | Transport → Collector | Chỉ dùng để thăm dò khi 401 (và cho AGT-10 sau này). If-None-Match |
| 8 | GET /agent/v1/ping | HTTPS | Transport → Collector | Có trong client, chưa được vòng chạy gọi |
| 9 | POST /agent/v1/enroll | HTTPS | Transport → Collector | Thuộc L3 Enroll. Ghi ở đây vì dùng chung Client |
| 10 | Kênh sự kiện ra | Không có | Chỉ log |
5.2. Request / Response Schema
[POST /agent/v1/metrics]
| Field | Kiểu | Ghi chú | ||
|---|---|---|---|---|
| Req | Authorization | header | ! | Bearer <agent_token>. Không gửi khi token rỗng |
| Req | X-AH-Proto | header | ! | 1 |
| Req | X-AH-Agent-Version | header | ! | Phiên bản agent |
| Req | X-Request-Id | header | ! | 16 ký tự hex ngẫu nhiên mỗi yêu cầu |
| Req | Content-Type | header | ! | application/x-protobuf |
| Req | Content-Encoding | header | ! | gzip (D-09: giao thức cho phép gzip hoặc zstd) |
| Req | body | MetricsBatch nén gzip | ! | Tối đa 1 MiB nén và 20.000 điểm (kiểm cục bộ trước khi gửi) |
| Req | MetricsBatch.seq | uint64 | ! | Tăng dần, khởi đầu unix ms |
| Req | MetricsBatch.sent_at_ms | int64 | ! | Bằng thời điểm thu của chu kỳ, không phải lúc gửi thật (xem OQ-W9) |
| Req | MetricsBatch.series[] | Series{name, labels, points[]} | ? | Rỗng nghĩa là heartbeat. Mỗi series hiện có đúng một Point{ts_ms, value} |
| Req | MetricsBatch.stats | AgentStats | ? | cpu_percent, rss_bytes, wal_bytes, wal_batches, dropped_samples_total, send_failures_total, clock_skew_seconds. Có khi Stats được cấu hình |
| Resp | MetricsAck.server_time_ms | int64 | ? | Dùng tính độ lệch đồng hồ |
| Resp | MetricsAck.ack_seq | uint64 | ? | Bị bỏ qua (OQ-W8) |
| Resp | MetricsAck.config_etag | string | ? | Khi đổi, gọi OnConfigETag một lần |
| Resp | MetricsAck.accepted_points, dropped_points, warnings[] | uint32, uint32, []string | ? | Chỉ ghi log (OQ-W8). Thân ack là tùy chọn |
| Errors có thể | 400, 401, 403, 413, 422, 426, 429, 5xx, lỗi mạng. Xem 5.3 |
[GET /agent/v1/config]
| Field | Kiểu | Ghi chú | ||
|---|---|---|---|---|
| Req | Authorization, X-AH-Proto, X-Request-Id | header | ! | Như trên |
| Req | If-None-Match | header | ? | ETag đã biết |
| Resp | 200 | AgentConfig (JSON hoặc proto) | ! | ETag lấy từ thân hoặc header ETag |
| Resp | 304 | không có thân | Client trả nil, nil | |
| Errors có thể | 401 (vẫn chưa hợp lệ), 403 (thu hồi), 429 (2 yêu cầu mỗi phút mỗi agent), 5xx |
[buffer.Queue]
| Field | Kiểu | Ghi chú | ||
|---|---|---|---|---|
| Req | Append(data) | []byte | ! | MetricsBatch đã marshal. WAL nén deflate. Lỗi nếu bản ghi nén quá 16 MiB |
| Resp | id | uint64 | ! | Id tăng ngặt |
| Resp | Oldest() | Entry, ok, error | ! | ok sai khi rỗng. Đọc lại và kiểm CRC mỗi lần |
| Req | Remove(id) | uint64 | ! | Id lạ bị bỏ qua |
| Resp | Stats() | Stats{Batches, Bytes, Dropped} | ! | |
| Errors có thể | wal: batch of N bytes is too large, lỗi I/O, "wal closed" |
5.3. Error Codes
code của Collector | HTTP | Điều kiện phát sinh | Hành động của Sender |
|---|---|---|---|
| (không có) | 200, 202 | Chấp nhận | Xóa lô, đặt lại backoff, trạng thái ok |
bad_request | 400 | Thân sai định dạng | Bỏ cả lô, cộng điểm vào DroppedSamples, log Error |
invalid_series | 422 | Dữ liệu không hợp lệ | Bỏ cả lô (giao thức ghi "bỏ phần lỗi", lệch, OQ-W2) |
unauthorized | 401 | Token sai hoặc hết hạn | Trạng thái unauthorized, dừng gửi, thăm dò GET /config mỗi 10 phút |
agent_revoked | 403 | Agent bị thu hồi | Trạng thái agent_revoked, xóa token trong bộ nhớ, giữ dữ liệu, không thử lại |
too_large | 413 | Lô vượt giới hạn của Collector | Tách đôi, gửi mỗi nửa một lần |
upgrade_required | 426 | Giao thức agent quá cũ | Đặt cờ upgrade, giữ lô, backoff (OQ-W3) |
rate_limited | 429 | Vượt hạn mức tốc độ | Giữ lô, backoff, tôn trọng Retry-After |
server_error, unavailable | 5xx | Collector lỗi | Giữ lô, backoff |
| Mọi mã khác (kể cả 409, 404, 3xx) | khác | Ngoài bảng giao thức | Giữ lô, backoff (mã trong bảng giao thức của 409 chỉ dành cho enroll, OQ-W10) |
| (không có) | Không áp dụng | Đứt kết nối, hết send_timeout, DNS, TLS | Giữ lô, backoff. Lỗi không mang token |
ErrTooLarge (cục bộ) | Không áp dụng | Nén quá 1 MiB hoặc quá 20.000 điểm | Đi nhánh 413 mà không gọi mạng |
Ghi chú: chuỗi code do Collector trả, Sender phân nhánh theo mã HTTP chứ không theo code. Nhánh "mọi mã khác" dựa trên đọc mã (mọi trạng thái không có nhánh riêng thành kRetry). Redirect 3xx không được theo (11) nên rơi vào nhánh này.
5.4. Versioning
Header X-AH-Proto: 1 trên mọi yêu cầu. Chỉ có một phiên bản giao thức nên N-1 chưa kiểm chứng được (L2 R-09). Lược đồ dây là agent.proto: agent.proto.ref trong agent là bản chép, nguồn sự thật là tệp của Collector. scripts/check-proto-drift.sh so hai tệp và trả mã 1 khi lệch. Thêm trường protobuf theo quy tắc tương thích ngược của protobuf (số trường không đổi). Client chấp nhận phản hồi JSON (protojson bỏ qua trường lạ) hoặc proto.
5.5. Authz - shared responsibility & permission matrix
Cụm này không phân quyền người dùng. Danh tính là bearer token của agent, do Collector cấp lúc enroll, và Collector xác thực ở mỗi yêu cầu.
Phân định trách nhiệm Auth
| Lớp | Ai sở hữu | Gồm |
|---|---|---|
| Xác thực agent | Collector | Kiểm token, gắn company_id và server_id từ token. Agent không tự khai hai giá trị này trong mẫu (L3 Collectors INV-2) |
| Giữ và gửi token | Cụm này | Sender.Token trong RAM, header Authorization. Xóa token khi 403 |
| Lưu token trên đĩa | L3 Enroll, Credentials, Config | credentials.json 0600 |
| Bảo vệ kênh | Cụm này (transport) | TLS từ 1.2, xác minh chứng chỉ, ghim CA, không theo redirect |
| Quyền tệp WAL | Cụm này, Service | Thư mục 0700, tệp 0600, tài khoản accesshub-agent |
Permission matrix
| Public API (5.1) | service-principal | user-role |
|---|---|---|
| 1 đến 5 (trong tiến trình) | Mã cùng tiến trình | Không áp dụng |
6, 7 (metrics, config) | Agent có token hợp lệ. Sai token 401, thu hồi 403 | Không áp dụng |
8, 9 (ping, enroll) | Chưa có token cho enroll (dùng License một lần). ping không cần token | Không áp dụng |
6. Data Schema (Physical)
Cụm này ghi đĩa ở một nơi duy nhất: thư mục state_dir/wal (mặc định theo cấu hình, chi tiết ở L3 Service và Packaging). Mục 6.1 mô tả bố cục tệp thay cho lược đồ cơ sở dữ liệu.
6.1. Cài đặt vật lý
Bảng ánh xạ
| Phân hệ (3.2) | Aggregate (4) | CSDL (Store) | Cấu trúc vật lý cốt lõi |
|---|---|---|---|
buffer (WAL) | WalRecord, Segment | Tệp trên đĩa, state_dir/wal/%016x.seg (đặt tên theo id đầu tiên, tối đa khoảng 1 MiB) | Chuỗi bản ghi: tiêu đề 24 byte (little-endian) rồi payload deflate |
buffer (WAL) | Checkpoint | Tệp state_dir/wal/checkpoint | Số nguyên thập phân và xuống dòng, ghi bằng tệp .tmp, fsync, rename |
buffer (Memory) | Batch | RAM tiến trình | Danh sách các mảng byte, giới hạn MaxBytes |
sender | Sender | RAM tiến trình | seq, nextAttempt, nextProbe, lastETag, trạng thái |
Lược đồ (bố cục WAL trên đĩa)
classDiagram
namespace Thu_Muc_WAL {
class walDir {
path state_dir_wal
mode 0700
}
class segmentFile {
name 16_hex_first_id.seg
mode 0600
}
class recordHeader {
length u32_LE
crc32c u32_LE
id u64_LE
unix_ms i64_LE
}
class recordPayload {
deflate_of_MetricsBatch bytes
}
class checkpointFile {
watermark decimal_text
mode 0600
}
class tmpFile {
name checkpoint_dot_tmp
}
}
walDir *-- segmentFile : chứa nhiều
walDir *-- checkpointFile : chứa một
segmentFile *-- recordHeader : mỗi bản ghi
recordHeader *-- recordPayload : theo sau
checkpointFile ..> tmpFile : ghi qua rồi đổi tênGhi chú lược đồ: trường crc32c (Castagnoli) bao phủ mọi byte đứng sau nó (id, thời gian, payload), không bao gồm length. length là độ dài payload nén, tối đa 16 MiB (walMaxRecord). Không có khóa ngoại. Bản ghi không có trường phiên bản định dạng (L2 D-15, OQ-W6).
6.2. Phân loại dữ liệu & retention
| Phần tử / Trường dữ liệu | Phân lớp dữ liệu | Thời hạn lưu trữ (Retention) | Cơ chế bảo vệ kỹ thuật |
|---|---|---|---|
| Bản ghi WAL (lô số liệu) | Nội bộ | Đến khi được ack, hoặc quá buffer.max_age (mặc định 24 giờ), hoặc bị xóa theo đoạn khi vượt buffer.max_bytes (mặc định 50 MiB) | Quyền 0600 và 0700, CRC32C phát hiện hỏng, deflate, không mã hóa ở trạng thái nghỉ (ĐỀ XUẤT: không cần vì không chứa bí mật) |
checkpoint | Nội bộ | Cập nhật mỗi lần settle | Ghi bằng tệp tạm, fsync rồi rename. Thư mục không được fsync sau rename nên sau mất điện có thể quay về giá trị cũ, hậu quả chỉ là gửi lặp (an toàn vì Collector khử trùng) |
| Hàng đợi RAM (dự phòng) | Nội bộ | Đời tiến trình | Chỉ ở RAM, mất khi khởi động lại |
Sender.Token | Bí mật | Đời tiến trình (lưu bền ở credentials.json, L3 Enroll) | Không vào log, String() hay thông báo lỗi. Xóa khi 403 |
seq, nextAttempt | Nội bộ | Đời tiến trình | seq được gieo lại từ thời gian khi khởi động |
7. Thuật toán & Luồng nghiệp vụ
7.1. Luồng nghiệp vụ chính (happy path)
Luồng 1: một chu kỳ bình thường (thu, ghi WAL, gửi, xóa)
sequenceDiagram
participant RT as cycle
participant ENG as Engine
participant SND as Sender
participant WAL as WAL
participant TRN as Transport
participant COL as Collector
RT->>ENG: Collect(now)
ENG-->>RT: mẫu đã chuẩn hóa
RT->>SND: Enqueue(now, mẫu)
SND->>WAL: Append(lô đã marshal)
WAL-->>SND: id sau khi fsync
RT->>SND: Tick(ctx)
SND->>WAL: Oldest()
WAL-->>SND: Entry gồm id và lô
SND->>TRN: PostMetrics(gzip của lô)
TRN->>COL: POST /agent/v1/metrics
COL-->>TRN: 200 kèm MetricsAck
SND->>WAL: Remove(id)
Note over SND: Backoff.Reset, ghi độ lệch đồng hồ, đồng bộ thống kê WALThứ tự luôn là Enqueue rồi Tick: mẫu của chu kỳ được ghi bền trước khi gửi, nên mất mạng hay tắt máy giữa chừng không làm mất lô. Lô rỗng (heartbeat) đi qua đúng đường này.
Luồng 2: gửi bù khi có tồn đọng
sequenceDiagram
participant SND as Sender
participant WAL as WAL
participant COL as Collector
Note over SND: Tick với 6 lô trong hàng đợi và CatchUp bằng 2
SND->>WAL: Oldest() lô 1
SND->>COL: POST metrics lô 1
COL-->>SND: 200
SND->>WAL: Remove(lô 1)
SND->>WAL: Oldest() lô 2
SND->>COL: POST metrics lô 2
COL-->>SND: 200
SND->>WAL: Remove(lô 2)
SND->>WAL: Oldest() lô 3
SND->>COL: POST metrics lô 3
COL-->>SND: 200
Note over SND: Dừng sau 1 cộng CatchUp lô, còn 3 lô cho tick sauMỗi tick gửi tối đa 1 + catch_up_batches lô (mặc định 3), cũ nhất trước, và dừng ở lỗi đầu tiên hoặc khi hàng đợi rỗng (TestCatchUpSendsOldestFirstWithinLimit). Xem 7.4.5 về hệ quả với hạn mức tốc độ.
7.2. Luồng thay thế và lỗi
Luồng 3: lỗi tạm thời (5xx, 429, mạng, 426) và backoff
sequenceDiagram
participant SND as Sender
participant BO as Backoff
participant WAL as WAL
participant COL as Collector
SND->>WAL: Oldest()
SND->>COL: POST metrics
COL-->>SND: 503 hoặc 429 kèm Retry-After hoặc mất kết nối
SND->>BO: Next(Retry-After)
BO-->>SND: thời gian chờ jitter, không dưới Retry-After
Note over SND: SendFailures tăng, state backoff, nextAttempt bằng now cộng chờ
Note over SND: Lô ở nguyên trong WAL, Tick sau đó bỏ qua tới nextAttempt
SND->>COL: POST metrics (tick sau nextAttempt)
COL-->>SND: 200
SND->>WAL: Remove(id)
Note over SND: Backoff.Reset, state okVới 426, Sender đặt thêm cờ upgrade trước khi đi cùng đường này. Cờ chỉ xóa sau một lần gửi thành công, và hiện ở String() dạng upgrade_required=true.
Luồng 4: 413, tách lô một lần
sequenceDiagram
participant SND as Sender
participant WAL as WAL
participant COL as Collector
SND->>COL: POST metrics (lô nguyên)
COL-->>SND: 413
Note over SND: split theo series, hoặc theo điểm nếu chỉ một series
SND->>COL: POST nửa 1
COL-->>SND: 200
SND->>COL: POST nửa 2
COL-->>SND: 503
SND->>WAL: Append(nửa 2 đã marshal) ở cuối hàng đợi
SND->>WAL: Remove(lô gốc)
Note over SND: failure cho nhánh 503, backoff, mất thứ tự FIFOMỗi nửa chỉ gửi một lần. Nửa vẫn 413 hoặc bị 400, 422 thì bị bỏ và đếm điểm. Nửa gặp lỗi tạm thời được ghi lại vào cuối hàng đợi (sau các lô mới hơn), rồi lô gốc bị xóa. Hai nửa dùng chung seq gốc. Lô không tách được (dưới 2 series và dưới 2 điểm) bị bỏ ngay (OQ-W4).
Luồng 5: 400, 422 (bỏ lô)
sequenceDiagram
participant SND as Sender
participant WAL as WAL
participant COL as Collector
participant ST as Thống kê
SND->>COL: POST metrics
COL-->>SND: 400 bad_request hoặc 422 invalid_series
SND->>ST: DroppedSamples cộng số điểm của lô
Note over SND: log Error collector rejected a batch, dropping it, kèm seq
SND->>WAL: Remove(id)
Note over SND: không backoff, vòng gửi bù tiếp tục với lô kếĐây là nơi hai rủi ro R-10 và OQ-W2 gặp nhau: lỗi hợp đồng (ví dụ một tên chỉ số lạ làm Collector trả 422) có thể làm rơi cả lô, dù giao thức chỉ yêu cầu bỏ phần lỗi.
Luồng 6: 401, dừng gửi và thăm dò cấu hình
sequenceDiagram
participant SND as Sender
participant COL as Collector
SND->>COL: POST metrics
COL-->>SND: 401 unauthorized
Note over SND: state unauthorized, nextProbe bằng now cộng 10 phút, log Error
Note over SND: Enqueue vẫn ghi vào WAL, Tick không gửi metrics
SND->>COL: GET /config (khi tới nextProbe)
COL-->>SND: 200 hoặc 304
Note over SND: log credentials accepted again, state ok
SND->>COL: POST metrics (cùng tick)
COL-->>SND: 200Nếu thăm dò trả 403 thì chuyển sang thu hồi (luồng 7). Mọi lỗi thăm dò khác (401, 5xx, mạng) giữ nguyên trạng thái và hẹn lại sau 10 phút (DefaultProbeInterval). Số lô tích trong WAL bị giới hạn bởi 7.4.2.
Luồng 7: 403, thu hồi vĩnh viễn
sequenceDiagram
participant SND as Sender
participant COL as Collector
participant RT as cycle
participant OS as Tiến trình
SND->>COL: POST metrics hoặc GET config
COL-->>SND: 403 agent_revoked
Note over SND: Token bị xóa khỏi RAM, state agent_revoked, log Error
RT->>SND: State()
SND-->>RT: agent_revoked
RT->>OS: chờ ctx hủy (idle)
Note over RT: Tiến trình vẫn sống và systemd thấy active, dữ liệu giữ trong WALKhông có đường tự phục hồi: cần enroll lại rồi khởi động lại dịch vụ (L3 Enroll). Đây là hành vi đã kiểm bằng TestRevokedStopsPermanentlyAndKeepsData và TestRevokedAgentStopsContactingTheCollector.
Luồng 8: khởi động, mở WAL, phục hồi sau mất điện
sequenceDiagram
participant RT as Run
participant WAL as WAL
participant FS as Hệ thống tệp
RT->>WAL: OpenWAL(dir, MaxBytes, MaxAge)
WAL->>FS: xóa tệp tmp, liệt kê đoạn theo id
WAL->>FS: quét từng đoạn, kiểm CRC và id tăng
Note over WAL: Đuôi rách của đoạn cuối bị cắt, đuôi hỏng ở đoạn cũ được giữ và tính là hỏng
WAL->>FS: đọc checkpoint, đánh dấu id bằng hoặc dưới mốc là xong
Note over WAL: nextID bằng max của mốc cộng 1 và id lớn nhất cộng 1
WAL->>WAL: settle, enforce tuổi và dung lượng
WAL-->>RT: hàng đợi sẵn sàng, log recovered buffered batches from diskNếu OpenWAL lỗi (thư mục không ghi được, hết chỗ), openQueue ghi log Error và chuyển sang Memory: agent vẫn chạy nhưng lô mất khi khởi động lại (TestUnusableStateDirFallsBackToMemory, ADR-W02).
Luồng 9: WAL đầy hoặc quá tuổi (loại bỏ lô cũ)
sequenceDiagram
participant SND as Sender
participant WAL as WAL
participant FS as Hệ thống tệp
SND->>WAL: Append(lô mới)
WAL->>FS: ghi và fsync vào đoạn hiện hành
WAL->>WAL: enforce(now)
Note over WAL: Bước 1, đánh dấu xong các bản ghi quá MaxAge từ đầu tới bản ghi tươi đầu tiên
Note over WAL: Bước 2, khi còn từ 2 đoạn và tổng vượt MaxBytes thì xóa nguyên đoạn cũ nhất
WAL->>FS: xóa đoạn, ghi checkpoint
WAL-->>SND: id
Note over SND: syncStats thấy Dropped tăng, log Warn buffer full, oldest batches droppedSố lô bị xóa chỉ đi vào log (cả wal dropped old batches ở WAL và buffer full, oldest batches dropped ở Sender) và không vào DroppedSamples (L2 D-10, OQ-W5).
7.3. State machines
Trạng thái giao nhận của Sender (một thực thể: Sender.state)
stateDiagram-v2
[*] --> ok
ok --> backoff : lỗi tạm [5xx, 429, mạng, 426] / SendFailures cộng 1, hẹn nextAttempt
backoff --> backoff : lỗi tạm [tiếp tục thất bại] / tăng bước chờ
backoff --> ok : gửi thành công / Backoff.Reset
ok --> ok : gửi thành công / xóa lô
ok --> unauthorized : 401 / hẹn nextProbe
backoff --> unauthorized : 401 / hẹn nextProbe
unauthorized --> ok : GET config thành công [tới nextProbe] / tiếp tục gửi
unauthorized --> agent_revoked : 403 khi thăm dò / xóa token
ok --> agent_revoked : 403 / xóa token
backoff --> agent_revoked : 403 / xóa token
agent_revoked --> [*] : tiến trình dừng| Đường (trigger) | Nơi hiện thực | Kiểm thử |
|---|---|---|
ok sang backoff | failure (kRetry), post (5xx, 429, 426, mạng) | TestServerErrorKeepsBatchAndBacksOff, TestDroppedConnectionKeepsBatch, TestRetryAfterIsHonored, TestUpgradeRequiredIsFlaggedAndBatchKept |
backoff sang ok | success | TestSuccessDeliversAndUpdatesSkew |
ok hoặc backoff sang unauthorized | failure (kUnauthorized) | TestUnauthorizedStopsSendingAndProbesConfig |
unauthorized sang ok | probe | TestUnauthorizedStopsSendingAndProbesConfig |
bất kỳ sang agent_revoked | revoke (từ failure hoặc probe) | TestRevokedStopsPermanentlyAndKeepsData |
| Đường 400, 422, 413 | Không đổi trạng thái (bỏ hoặc tách lô rồi xử tiếp) | TestBadRequestAndUnprocessableAreDropped, TestTooLargeIsSplitAndSentOnce |
Trạng thái halted (identity_mismatch) không thuộc Sender: nó nằm ở Agent.halted (internal/agent/agent.go), chỉ đặt một lần lúc khởi động khi machine_id khác, khi đó Sender chưa được tạo (TestMachineIDMismatchStopsDelivery). Trạng thái Status() hiển thị state=identity_mismatch.
Vòng đời một bản ghi WAL (một thực thể: WalRecord)
stateDiagram-v2
[*] --> live : Append thành công và fsync
live --> done : Remove(id) [sau ack hoặc quyết định bỏ] / Live giảm
live --> done : hết tuổi hoặc đoạn bị xóa / Dropped cộng 1
live --> done : CRC hoặc id sai khi đọc / Dropped cộng 1, log
done --> deleted : mọi bản ghi đầu đoạn xong / settle xóa tệp, ghi checkpoint
deleted --> [*]| Đường | Nơi hiện thực | Kiểm thử |
|---|---|---|
live sang done do Remove | Remove (BinarySearch trong danh sách bản ghi của đoạn) | TestWALFifoRoundTrip, TestWALOutOfOrderRemoveAndUnknownIDs |
live sang done do hết tuổi hoặc dung lượng | enforce | TestWALAgeCapDropsExpiredBatches, TestWALSizeCapDropsOldestSegments |
live sang done do hỏng | Oldest, scanRecords | TestWALDetectsCorruptionAfterOpen, TestWALSkipsRecordWithBadCRCOnRecovery |
done sang deleted | settle | TestWALRotatesAndDeletesFinishedSegments |
Lưu ý: dấu done của bản ghi bị Remove sai thứ tự chỉ nằm trong RAM. Sau khởi động lại chỉ watermark (checkpoint) được khôi phục, nên bản ghi đã ack nhưng nằm sau một bản ghi chưa ack có thể gửi lại (L2 D-15). Trong vận hành bình thường Remove luôn theo thứ tự cũ nhất trước, riêng nhánh tách lô 413 và bản ghi bị bỏ vì hỏng là ngoại lệ.
7.4. Thuật toán, thuật toán cốt lõi
7.4.1. Ghi bền và phục hồi WAL
Vấn đề: ghi mẫu ra đĩa sao cho mất điện hay kill -9 không làm hỏng hàng đợi và không gửi lại lô đã ack, mà không cần thư viện ngoài.
Giải pháp:
- Mỗi lô: nén deflate, ghép tiêu đề 24 byte (độ dài, CRC32C của phần còn lại, id, thời gian ms), ghi một lần vào đoạn hiện hành,
Sync. Nếu ghi lỗi giữa chừng thìTruncatevề kích thước đoạn trước lần ghi. - Đoạn mới khi
size + rec > SegmentBytes(chỉ khi đoạn đã có dữ liệu), tạo bằngO_EXCL, rồisyncDirthư mục để tên đoạn bền. - Ack:
Removechỉ đánh dấu,settleđẩy watermark qua các bản ghi xong ở đầu và xóa đoạn đã xong, sau đó ghicheckpointbằng tệp tạm,fsync,rename. - Phục hồi: quét từng đoạn, dừng ở bản ghi độ dài 0, độ dài quá 16 MiB, hoặc đuôi cụt. Bản ghi sai CRC hoặc id không tăng bị bỏ qua và đếm hỏng. Đuôi hỏng ở đoạn cuối được cắt, ở đoạn cũ hơn thì giữ lại và cảnh báo.
nextID = max(watermark+1, id lớn nhất+1).
Trade-off: fsync mỗi lần Append (mỗi chu kỳ, mặc định 30 s) nên chi phí I/O thấp và độ bền cao. Đổi lại không có định dạng phiên bản (D-15), không fsync thư mục sau rename checkpoint (chỉ gây gửi lặp), và cắt đuôi bằng cách ghi đè đoạn cuối. Đoạn cũ có đuôi hỏng được giữ nên có thể để lại các bản ghi không đọc được ở giữa, Oldest sẽ bỏ qua chúng khi gặp.
7.4.2. Giới hạn tuổi và dung lượng
Vấn đề: đĩa không được đầy vì mất mạng dài, nhưng số liệu mới quan trọng hơn số liệu cũ.
Giải pháp: enforce(now) chạy sau mỗi Append, khi mở và mỗi lần Oldest.
- Tuổi: đi từ bản ghi cũ nhất, đánh dấu xong mọi bản ghi có thời gian nhỏ hơn
now - MaxAgecho tới bản ghi tươi đầu tiên (dừng ở đó, không quét hết). - Dung lượng: khi còn nhiều hơn một đoạn và tổng kích thước vượt
MaxBytes, đánh dấu xong toàn bộ đoạn cũ nhất rồisettle(xóa tệp). Lặp lại. - Ghi log Warn một lần mỗi lần loại bỏ, kèm
expiredvàevicted_by_size.
Trade-off: xóa theo đoạn (khoảng 1 MiB, khoảng 2% của 50 MiB) là thô nhưng đơn giản và tránh ghi lại tệp, đổi lại có thể loại nhiều hơn mức cần tối đa một đoạn. Đoạn cuối không bao giờ bị xóa nên trần cứng chỉ không giữ được khi một bản ghi đơn lẻ lớn hơn MaxBytes (không xảy ra với dải cấu hình 4 MiB đến 1 GiB và lô tối đa 1 MiB nén khi gửi, nhưng Append chấp nhận tới 16 MiB). Số bị loại không vào DroppedSamples (D-10). Cơ chế tuổi dựa trên dấu thời gian của bản ghi lúc ghi, không phải thời điểm của các điểm bên trong (xem 7.4.4 về ràng buộc cửa sổ 24 giờ của Collector).
7.4.3. Backoff full jitter và phân pha
Vấn đề: hàng nghìn agent không được thử lại đồng loạt sau sự cố, và không được khởi động cùng giây.
Giải pháp: Next(retryAfter): bước hiện tại cur bằng 0 thì lấy Base (5 s), từ Max/2 trở lên thì lấy Max (300 s), ngược lại nhân đôi. Lấy d = rnd() * cur (đều trong [0, cur]), trả về max(d, min(retryAfter, 1 giờ)). Reset đặt cur về 0 sau thành công. PhaseOffset(agentID, interval) là 8 byte đầu của sha256(agentID) (big-endian) lấy dư cho interval, dùng làm độ trễ của timer đầu tiên.
Trade-off: full jitter phân tán tốt nhất nhưng có thể chờ rất ngắn. Vì Tick chỉ chạy mỗi interval, thời gian chờ ngắn hơn interval (ví dụ 5 s, 10 s, 20 s so với 30 s) thực tế không làm chậm thêm: lần thử kế là tick kế. Backoff chỉ có tác dụng thật khi bước chờ vượt interval (từ khoảng bước 40 s đến 80 s trở lên với 30 s) hoặc khi Retry-After lớn. Chu kỳ kế của timer là interval cộng thời gian chu kỳ (timer đặt lại sau cycle), nên pha lệch dần theo thời gian chạy (không ảnh hưởng độ đúng).
7.4.4. Tách lô và dấu thời gian điểm
Vấn đề: Collector giới hạn 1 MiB nén, 8 MiB giải nén, 20.000 điểm, 500 series mỗi giờ, và chỉ nhận điểm không cũ hơn 24 giờ và không quá 5 phút trong tương lai.
Giải pháp: kiểm điểm cục bộ (> 20.000 là kTooLarge ngay), nén gzip rồi kiểm 1 MiB (ErrTooLarge). Khi 413 (hoặc cục bộ), split chia đôi theo series (khi có từ 2 series) hoặc theo điểm (một series từ 2 điểm), mỗi nửa gửi một lần. Dấu thời gian điểm là now của chu kỳ thu (INV-W7), không đổi khi gửi lại.
Trade-off: dấu thời gian thu giữ đúng ngữ nghĩa khi gửi bù, nhưng lô bị đệm gần 24 giờ có thể bị Collector loại do cửa sổ thời gian (mặc định buffer.max_age 24 giờ đã bằng ngưỡng của Collector, nên các điểm sát ngưỡng có thể bị dropped_points mà agent chỉ ghi log, OQ-W8). sent_at_ms cũng bằng thời điểm thu chứ không phải lúc gửi, nên độ lệch đồng hồ phía Collector không dựa vào trường này (OQ-W9). Tách lô mất thứ tự FIFO (OQ-W4) nhưng an toàn vì khử trùng theo (series, ts).
7.4.5. Gửi bù và hạn mức tốc độ
Vấn đề: sau khi mạng phục hồi, xả nhanh tồn đọng nhưng không vượt hạn mức của Collector (giao thức: metrics 4 yêu cầu mỗi phút mỗi agent, cho phép bùng nổ 10, config 2 mỗi phút, vượt thì 429).
Giải pháp: mỗi tick gửi 1 + catch_up_batches lô (mặc định 3, dải 1 đến 10 cho catch_up_batches). Trạng thái ổn định chỉ có 1 lô mỗi tick (2 yêu cầu mỗi phút ở chu kỳ 30 s, dưới hạn mức 4).
Trade-off (suy ra từ số liệu, chưa đo): khi có tồn đọng ở cấu hình mặc định, mỗi tick dùng tối đa 3 yêu cầu, tức tối đa 6 mỗi phút, vượt 4 mỗi phút bền vững. Bùng nổ 10 cho phép chịu khoảng vài phút (10 chia cho mức vượt 2 mỗi phút, ước khoảng 5 phút), sau đó Collector trả 429 và Sender lùi bước, giữ nguyên lô. Kết quả vẫn không mất dữ liệu (429 là lỗi tạm), nhưng làm mất hiệu quả của gửi bù và có thể làm tăng agent_send_failures_total. Chưa rõ Collector đếm hạn mức theo cửa sổ nào. Đây là OQ-W1. Với catch_up_batches lớn hơn (đến 10) rủi ro này tăng.
8. Xử lý lỗi
8.1. Xử lý các nhánh lỗi (error branches)
| Nhánh | Điều kiện | Xử lý | Quan sát |
|---|---|---|---|
| Thu lỗi tạm | Enqueue trả lỗi (marshal hoặc Append lỗi) | Ghi log Error cycle failed, Tick vẫn chạy để xả tồn đọng | Log |
Append bản ghi quá 16 MiB | Lô nén vượt walMaxRecord | Trả lỗi, lô mất, log | Log, không có metric |
| Ghi WAL lỗi (đĩa đầy, I/O) | Write hoặc Sync lỗi | Truncate về cỡ cũ, trả lỗi, lô của chu kỳ đó mất. Agent tiếp tục | Log cycle failed |
| WAL không mở được | OpenWAL lỗi | Dùng Memory, log Error | Log. Nên có cảnh báo (OQ-W7 mở rộng) |
| Bản ghi WAL hỏng | CRC, id hoặc giải nén sai | Bỏ, đếm Dropped, log dropping a corrupt wal record | Log Warn, Stats.Dropped |
| Lô không giải mã được | proto.Unmarshal lỗi | Bỏ, log Error, Remove, thử lô kế | Log Error |
| 400, 422 | Xem 5.3 | Bỏ lô, cộng DroppedSamples, log Error | agent_dropped_samples_total |
| 413 hoặc quá lớn cục bộ | Xem luồng 4 | Tách một lần | Log khi bỏ |
| 401 | Xem luồng 6 | Dừng gửi, thăm dò | Log Error, trạng thái unauthorized |
| 403 | Xem luồng 7 | Dừng vĩnh viễn | Log Error, Status() |
| Lỗi tạm (5xx, 429, mạng, 426) | Xem luồng 3 | Backoff, giữ lô | agent_send_failures_total, log Warn delivery failed, backing off |
Giải mã MetricsAck lỗi | Thân 200 không hợp lệ | Coi là lỗi tạm: lô giữ lại và gửi lặp dù Collector đã nhận (an toàn nhờ khử trùng, nhưng tính là lỗi gửi) | Log Warn |
| Checkpoint không ghi được | Lỗi I/O | Chỉ log, gửi lặp có thể xảy ra sau khởi động lại | Log Warn |
| Ngữ cảnh bị hủy giữa lúc gửi | SIGTERM khi đang PostMetrics | Lỗi mạng, lô giữ trong WAL, log delivery failed (không phải lỗi thật) | Log |
8.2. Fail-fast
| Điều kiện | Hành vi | Mã thoát hoặc lỗi |
|---|---|---|
Chưa enroll (không có credentials.json) | Run trả ErrNotEnrolled, không chạy | ExitNotEnrolled (L3 Enroll) |
machine_id khác lúc enroll | Không gửi, không thu, chờ ctx hủy, state=identity_mismatch | Tiến trình vẫn sống (không thoát). Cần enroll lại với --force |
| Dựng engine lỗi (regex sai) | Run trả build collectors: ... | Lỗi khởi động (TestBrokenEngineFailsRun) |
| Tùy chọn client sai (URL sai, tệp CA không đọc được) | clientFor lỗi, Run trả lỗi | Lỗi khởi động (TestNewValidatesOptions). Sau SIGHUP thì chỉ giữ client cũ và log |
Không đọc được machine_id | Bỏ qua kiểm tra, log Warn | Không fail (ưu tiên vận hành) |
| Không mở được WAL | Không fail, dùng RAM | Xem 8.1 |
8.3. Race conditions và đồng thời nhẹ
| Tình huống | Cơ chế hoặc hậu quả | Ghi chú |
|---|---|---|
| Sender bị gọi từ nhiều goroutine | Không hỗ trợ: Enqueue và Tick phải cùng goroutine | Hiện đúng, mọi thứ trong cycle. Trạng thái đọc bởi Status() được mutex bảo vệ |
| SIGHUP nạp lại cấu hình lúc đang gửi | Reload thay con trỏ nguyên tử, vòng chính đọc con trỏ ở đầu chu kỳ | Đổi có hiệu lực từ chu kỳ kế. Không đổi engine hay kích thước WAL |
Status() từ tín hiệu trạng thái | Mutex trong Agent và Sender | An toàn |
| Đóng WAL giữa lúc ghi | closed kiểm dưới mutex, trả lỗi | TestWALClosedRejectsUse |
kill -9 lúc Append | Đuôi rách bị cắt lúc mở lại | TestWALSurvivesKill9 |
rename checkpoint chưa bền lúc mất điện | Watermark quay về giá trị cũ, các bản ghi đã ack gửi lại | Chấp nhận (gửi lặp an toàn) |
Hai tiến trình agent cùng state_dir | Không có khóa tệp (flock) trong mã đã đọc, hai tiến trình có thể giẫm lên WAL | Ngoài phạm vi nhưng rủi ro nếu người dùng chạy tay song song với dịch vụ (OQ-W11) |
9. Suy thoái dịch vụ & Khả năng phục hồi
9.1. Ma trận suy thoái theo phụ thuộc
| Phụ thuộc | Lỗi | Suy thoái | Bảo vệ | Phục hồi |
|---|---|---|---|---|
| Collector (mạng, TLS, DNS, 5xx) | Không tới được | Tích lô trong WAL, hàng đợi lớn dần tới giới hạn | Backoff, WAL bền, giới hạn tuổi và dung lượng | Tự động: xả bằng gửi bù |
| Collector quá tải (429) | Từ chối | Như trên, tôn trọng Retry-After | Full jitter, phân pha | Tự động |
| Collector từ chối token (401) | Không xác thực được | Dừng gửi, vẫn ghi WAL, thăm dò mỗi 10 phút | Thăm dò GET /config | Tự động khi token hợp lệ lại (ví dụ Hub cấp lại). Nếu không thì cần enroll lại |
| Collector thu hồi (403) | Vĩnh viễn | Ngừng hẳn, không ghi thêm | Giữ dữ liệu cũ trên đĩa | Thủ công: enroll lại và khởi động lại |
Đĩa (state_dir) đầy hoặc chỉ đọc | Ghi WAL lỗi hoặc không mở được | Mất lô của chu kỳ (nếu lỗi lúc chạy), hoặc dùng RAM (nếu lỗi lúc mở) | Truncate phục hồi, dự phòng Memory | Sau khi khởi động lại nếu đĩa ổn |
| Đồng hồ hệ thống lệch hoặc nhảy | Dấu thời gian sai, seq có thể lùi | Điểm có thể bị Collector loại (ngoài cửa sổ) | Ghi clock_skew_seconds để cảnh báo, chưa hiệu chỉnh | Thủ công (đồng bộ NTP) |
| Bộ nhớ | Vượt GOMEMLIMIT 64 MiB | GC gắt hơn, hết thì systemd MemoryMax=96M giết | limits.memory_limit, hàng đợi RAM có trần | Systemd khởi động lại, WAL còn |
| Engine (thu) | Bộ thu lỗi | Lô thiếu số liệu của bộ thu đó | Bulkhead theo bộ thu (L3 Collectors) | Tự động |
| Proxy | Lỗi proxy | Coi là lỗi mạng | Như Collector không tới được | Sửa cấu hình rồi SIGHUP (dựng lại client) |
9.2. Backup, DR, RTO và RPO
WAL là bộ đệm tạm, không phải nguồn sự thật (nguồn sự thật là TSDB của Collector). Không sao lưu WAL. Mất WAL nghĩa là mất số liệu chưa gửi của máy đó, không ảnh hưởng máy khác.
| Chỉ số | Giá trị | Ghi chú |
|---|---|---|
| RPO khi mất điện | Tối đa lô đang ghi (khoảng một interval, mặc định 30 s) | fsync mỗi Append (mã: WAL.Append gọi Sync). Mô phỏng 20 lần mất điện chưa chạy |
| RPO khi mất kết nối | Không mất tới 24 giờ hoặc 50 MiB (mặc định), sau đó mất lô cũ nhất | Có thể tăng tới 7 ngày và 1 GiB |
| RTO của agent sau mất điện | Thời gian khởi động cộng quét WAL. Mục tiêu L2 dưới 3 giây (L2-NFR-06) | Chưa đo. Thời gian quét tỉ lệ với kích thước WAL (tối đa vài chục MiB) |
Khôi phục sau khi mất state_dir | Enroll lại (mất credentials.json) và mất WAL | L3 Enroll. Số liệu chưa gửi mất |
10. Đồng thời & Toàn vẹn dữ liệu
10.1. Ranh giới giao dịch
Không có giao dịch cơ sở dữ liệu. Các đơn vị nguyên tử là thao tác tệp:
| Đơn vị | Ranh giới | Đảm bảo |
|---|---|---|
| Ghi một bản ghi | Một Write rồi Sync dưới WAL.mu | Hoặc bản ghi đầy đủ, hoặc đuôi rách bị cắt lại lúc mở (CRC phát hiện). Lỗi thì Truncate |
| Ghi checkpoint | Tệp tạm, fsync, rename | Đọc thấy giá trị cũ hoặc mới, không bao giờ nửa vời |
| Xóa đoạn | os.Remove sau khi checkpoint đã ghi | Mất điện giữa hai bước chỉ để lại đoạn thừa, dọn ở lần mở kế |
| Gửi một lô | PostMetrics rồi Remove | Ít nhất một lần (at-least-once): mất điện giữa hai bước gây gửi lặp (an toàn nhờ khử trùng theo (series, ts)) |
| Tách lô 413 | Append các nửa chưa gửi, rồi Remove lô gốc | Không nguyên tử: mất điện giữa hai bước có thể để cả lô gốc lẫn nửa đã ghi lại, gây gửi lặp nhẹ (an toàn) |
10.2. Cơ chế đồng thời
| Cơ chế | Nơi dùng | Mục đích |
|---|---|---|
Một goroutine cho cycle | Agent.Run | Loại bỏ tranh chấp giữa thu, xếp hàng, gửi |
sync.Mutex trong WAL | Mọi phương thức WAL | An toàn khi Status() hay kiểm thử gọi Stats() |
sync.Mutex trong Sender (mu) | state, upgrade, lastErr, lastSent | Đọc bởi Status() |
sync.Mutex trong Agent (mu) | halted, send | Đọc bởi Status() |
atomic.Pointer[config.Config] | Agent.cfg | Nạp lại cấu hình không khóa |
atomic trong Stats | Bộ đếm | Đọc bởi bộ thu tự thân |
| Không dùng khóa tệp | state_dir/wal | Xem OQ-W11 |
context | Mọi lời gọi mạng và vòng gửi bù | Dừng êm khi SIGTERM |
11. Bảo mật
11.1. Bảo mật ba lớp
| Lớp | Biện pháp | Bằng chứng |
|---|---|---|
| Kênh truyền | TLS tối thiểu 1.2, xác minh chứng chỉ (bỏ qua chỉ khi insecure_skip_verify bật rõ), ghim CA từ tệp PEM, HTTP/2, không theo redirect (tránh gửi token tới máy chủ khác), giới hạn thân phản hồi 1 MiB | TestRedirectsAreNotFollowed, TestNewValidatesOptions |
| Danh tính và bí mật | Bearer token chỉ trong RAM ở Sender và header. Lỗi mạng chỉ chứa URL, thông điệp lỗi API cắt 200 ký tự, String() không có token | TestTokenNeverReachesTheLogs, TestNetworkErrorsNeverContainTheToken |
| Dữ liệu nghỉ | WAL thư mục 0700, tệp 0600, tài khoản accesshub-agent. Lô không chứa bí mật, không mã hóa ở trạng thái nghỉ | TestWALFilesAreOwnerOnly |
11.2. Đường ống phân quyền năm bước
Cụm này không có yêu cầu vào. Với các yêu cầu ra tới Collector, năm bước theo trách nhiệm:
| Bước | Ai làm | Ghi chú |
|---|---|---|
| 1. Xác thực kênh | Cụm này (TLS, CA) | Từ chối chứng chỉ không hợp lệ |
| 2. Xác thực agent | Collector (kiểm token) | 401 hoặc 403 |
| 3. Ràng buộc danh tính | Collector (gắn company_id, server_id từ token, không tin nhãn từ agent) | Agent không thể giả nhãn (L3 Collectors INV-2) |
| 4. Giới hạn tốc độ | Collector (429) | Cụm này lùi bước |
| 5. Kiểm tra nội dung | Collector (catalog, cửa sổ thời gian) | 400 hoặc 422 |
11.3. Điểm neo zero-trust
| Điểm neo | Ý nghĩa |
|---|---|
| Không lắng nghe | 0 cổng mở (L2-NFR-08), chỉ client HTTP |
| Không tin redirect và không tin Collector là duy nhất | Redirect không theo. collector_url từ xa chưa được dùng (D-11) |
| Token không rời RAM và header | Không log, không String(), xóa khi 403 |
| Danh tính máy | Lệch machine_id thì dừng gửi (FR-W14), để ảnh máy ảo nhân bản không mạo danh |
| Ghim CA tùy chọn | ca_file dành cho CA riêng. insecure_skip_verify chỉ để thử nghiệm, và check-config đã in cảnh báo WARNING khi cờ này bật (printSummary trong internal/cli/checkconfig.go, ĐÃ HIỆN THỰC) |
12. Cấu hình & Tinh chỉnh
12.1. Tunables
Nguồn cấu hình và thứ tự ưu tiên (mặc định, YAML, AH_*, cờ CLI) thuộc L3 Enroll, Credentials, Config. Bảng dưới chỉ liệt kê tham số tác động lên WAL và Sender. SIGHUP nạp lại tệp cục bộ: interval và catch_up_batches có hiệu lực từ chu kỳ kế, thông số Collector (URL, CA, proxy, send_timeout) dựng lại client (Reload, TestReloadSwitchesCollector). Riêng buffer.* chỉ được đọc lúc OpenWAL khi khởi động (WAL không đổi kích thước lúc chạy), và limits.memory_limit chỉ áp dụng lúc khởi động.
| Tham số | Mặc định (dải hợp lệ) | Ý nghĩa | Mục liên quan |
|---|---|---|---|
interval | 30 s (10 s đến 300 s) | Chu kỳ thu và gửi. Cũng là mốc của PhaseOffset | 7.4.3, NFR-W06 |
send_timeout | 15 s (1 s đến 1 phút) | Hạn cho một yêu cầu HTTP (ResponseHeaderTimeout và Timeout của client) | Mục 5 |
buffer.max_bytes | 50 MiB (4 MiB đến 1 GiB) | Trần dung lượng WAL, loại theo đoạn khi vượt | 7.4.2, NFR-W01 |
buffer.max_age | 24 h (1 phút đến 7 ngày) | Tuổi tối đa của bản ghi WAL | 7.4.2, NFR-W01 |
catch_up_batches | 2 (1 đến 10) | Số lô gửi bù thêm mỗi tick | 7.4.5, OQ-W1 |
limits.memory_limit | 64 MiB (16 MiB đến 4 GiB) | debug.SetMemoryLimit (bỏ qua nếu đã đặt GOMEMLIMIT) | 9.1 |
collector_url | không có (bắt buộc, phải hợp lệ khi kiểm tra cấu hình) | Đích gửi | 11.1 |
ca_file, proxy_url, insecure_skip_verify | rỗng, rỗng, false | CA riêng, proxy tường minh (mặc định lấy từ biến môi trường), bỏ kiểm chứng chỉ (chỉ để thử nghiệm) | 11.1 |
state_dir | DefaultStateDir() theo nền tảng | WAL nằm ở <state_dir>/wal, cạnh credentials.json | Mục 6 |
Tên khóa lấy từ internal/config/types.go, giá trị mặc định và dải từ defaults.go và validate.go. Ưu tiên nguồn cấu hình ở L3 Enroll, Credentials, Config.
Hằng số biên dịch (không cấu hình được)
| Hằng | Giá trị | Vai trò |
|---|---|---|
walMaxRecord | 16 MiB | Độ dài payload nén tối đa của một bản ghi |
walDefaultSegment | 1 MiB | Kích thước đoạn WAL |
DefaultProbeInterval | 10 phút | Chu kỳ thăm dò cấu hình khi unauthorized |
Backoff.Base, Backoff.Max, MaxRetryAfter | 5 s, 300 s, 1 giờ | Lịch backoff và trần Retry-After |
MaxCompressedBytes, MaxPointsPerBatch | 1 MiB, 20.000 | Giới hạn gửi (theo giao thức) |
maxResponseBytes | 1 MiB | Trần thân phản hồi đọc vào bộ nhớ |
TLSHandshakeTimeout, IdleConnTimeout, MaxIdleConns | 10 s, 90 s, 2 | Tham số kết nối |
| TLS tối thiểu | 1.2 | Không cấu hình được |
12.2. Feature flags
Không có cờ tính năng nào cho WAL hoặc Sender. Tham số allow_remote_config được phân tích và mặc định true nhưng chưa được dùng (L2 D-01, D-11): OnConfigETag của Sender không được nối trong agent.go, thân 200 của GET /config khi thăm dò bị bỏ qua, nên không có cấu hình từ xa nào được áp dụng.
13. Telemetry & Vận hành
13.1. Metrics
Cụm này không có endpoint /metrics. Số liệu tự thân do bộ thu self (L3 Collectors) phát vào chính luồng gửi đi, kèm cùng dữ liệu ở trường AgentStats của mỗi lô. Mọi chỉ số là gauge, kể cả tên có _total (chứa tổng tích lũy từ lúc khởi động, Collector phải xử lý như bộ đếm).
| Tên | Nguồn cập nhật | Ngữ nghĩa | Ghi chú |
|---|---|---|---|
agent_wal_bytes | syncStats sau Append và Remove (từ Queue.Stats) | Byte đang có trên đĩa | AgentStats.wal_bytes |
agent_wal_batches | như trên | Số lô chưa gửi | AgentStats.wal_batches. Đây là "độ sâu hàng đợi" |
agent_send_failures_total | failure() nhánh lỗi tạm | Số lần gửi thất bại tạm thời | Không tăng khi 401, 403, 400, 422 (chúng có đường riêng). AgentStats.send_failures_total |
agent_dropped_samples_total | Engine loại mẫu, cộng điểm của lô bị bỏ ở 400, 422 hoặc không tách được | Mẫu bị mất vì lỗi hợp đồng hoặc danh mục | Không gồm lô bị WAL xóa vì đầy hoặc quá tuổi (D-10, OQ-W5). AgentStats.dropped_samples_total |
agent_clock_skew_seconds | success() khi ack có thời gian máy chủ | Lệch đồng hồ so với Collector | AgentStats.clock_skew_seconds |
Khoảng trống (ĐỀ XUẤT, OQ-W5): chưa có chỉ số riêng cho số lô bị WAL xóa và số lần vào trạng thái backoff, unauthorized, agent_revoked. Trạng thái giao nhận hiện chỉ nhìn được qua log và Status().
13.2. Log schema
Log dùng slog qua logx (định dạng text mặc định, json cấu hình được, ra stderr cho journald hoặc tệp xoay 10 MiB giữ 1 bản sao). maskingHandler che token và trường có tên chứa token, authorization, password, secret. Trường chung: time, level, msg.
Sự kiện (msg) | Mức | Trường thêm | Khi nào |
|---|---|---|---|
agent started | Info | version, collector, ... | Khởi động |
recovered buffered batches from disk | Info | batches, bytes | Mở WAL thấy lô tồn |
cannot open the disk buffer, batches will be kept in memory only and lost on restart | Error | err | Dự phòng RAM |
wal recovered with damage | Warn | corrupt_records, truncated_bytes | Phục hồi thấy hỏng |
wal segment has an unreadable tail, keeping the valid records | Warn | segment | Đoạn cũ có đuôi hỏng |
wal checkpoint is unreadable, acknowledged batches may be sent again | Warn | Checkpoint không đọc được | |
cannot store the wal checkpoint, acknowledged batches may be sent again after a restart | Warn | err | Ghi checkpoint lỗi |
cannot delete a finished wal segment | Warn | segment, err | Xóa đoạn xong lỗi |
wal dropped old batches | Warn | expired, evicted_by_size | Loại theo tuổi hoặc dung lượng |
dropping a corrupt wal record | Warn | id | Oldest gặp bản ghi hỏng |
buffer full, oldest batches dropped | Warn | batches | Sender thấy Dropped tăng |
delivery failed, backing off | Warn | err, wait | Lỗi tạm |
collector rejected a batch, dropping it | Error | seq, points, err | 400, 422, hoặc không tách được |
collector rejected the agent token, will retry periodically | Error | 401 | |
credentials accepted again, resuming delivery | Info | Thăm dò thành công | |
agent revoked by the collector, delivery stopped permanently | Error | 403 | |
dropping unreadable buffered batch | Error | err | Lô trong WAL không giải mã được |
collector warning, collector dropped points | Warn | warning, points | Ack có cảnh báo hoặc điểm bị loại (OQ-W8) |
requeue split batch | Error | err | Ghi lại nửa lô lỗi |
machine id changed since enrollment (cloned or restored image), delivery stopped, re-enroll with --force | Error | Lệch machine_id | |
cycle failed | Error | err | Lỗi thu hoặc ghi WAL |
status | Info | state | Nhận SIGUSR1 |
Quan sát: thông điệp mã bằng tiếng Anh cố định, có thể dùng làm khóa cho luật cảnh báo log.
13.3. Alert to runbook
Cụm này không tự phát cảnh báo. Các điều kiện dưới đây do Collector hoặc hệ thống giám sát đánh giá (ĐỀ XUẤT, ngưỡng chưa hiệu chỉnh vì chưa có số đo thực tế).
| Điều kiện (đề xuất) | Runbook |
|---|---|
agent_wal_batches tăng liên tục nhiều chu kỳ | Collector hoặc mạng có vấn đề: kiểm tra curl tới Collector từ máy, xem log delivery failed và trường err. Chưa mất dữ liệu khi còn dưới 24 giờ và 50 MiB |
agent_wal_bytes gần buffer.max_bytes | Sắp mất lô cũ: khôi phục kết nối, hoặc tăng buffer.max_bytes rồi khởi động lại |
Log wal dropped old batches hoặc buffer full xuất hiện | Đã mất số liệu (D-10). Đánh dấu khoảng trống khi phân tích. Xem OQ-W5 về việc thiếu chỉ số |
agent_dropped_samples_total tăng | Lỗi hợp đồng: xem log collector rejected a batch, so phiên bản agent với catalog của Collector (R-10) |
agent_clock_skew_seconds lớn (trên 5 phút là ngưỡng cửa sổ tương lai của Collector) | Điểm bị loại: đồng bộ NTP trên máy |
Log collector rejected the agent token | Token bị vô hiệu hoặc rotate: Hub cấp lại hoặc enroll lại |
Log agent revoked | Enroll lại với token mới rồi khởi động lại dịch vụ |
Log machine id changed | Máy nhân bản: enroll lại với --force |
| Không có lô nào tới Collector trong vài chu kỳ và không thấy log lỗi | Kiểm systemctl status, SIGUSR1 để in trạng thái. Tiến trình agent_revoked vẫn active (OQ-W7 liên quan) |
13.4. Probes
Không có endpoint sức khỏe (cụm không mở cổng). Hai kênh quan sát cục bộ:
| Kênh | Nội dung |
|---|---|
Tín hiệu SIGUSR1 | Ghi log status với chuỗi Agent.Status(): version, uptime, collector, interval, và delivery=<state> queued=N [upgrade_required=true], hoặc state=identity_mismatch khi dừng vì lệch máy |
Lệnh accesshub-agent status | Chưa phải chẩn đoán đầy đủ (stub, AGT-10, L3 Enroll, Credentials, Config) |
Sức khỏe từ xa: Collector suy ra từ việc lô đến đều (mỗi lô là heartbeat kể cả khi rỗng).
13.5. Trace propagation
Mỗi yêu cầu gắn X-Request-Id (16 ký tự hex ngẫu nhiên) và X-AH-Agent-Version. Chưa có trace phân tán. X-Request-Id chưa được ghi vào log phía agent, nên chưa thể đối chiếu trực tiếp log hai phía trừ khi Collector ghi lại (ĐỀ XUẤT, OQ-W13). Mã tương quan bền của lô là seq, xuất hiện trong log khi lô bị bỏ.
14. Kế hoạch kiểm thử
| Loại Test & Phạm vi | Ánh xạ mục tiêu | Mục tiêu kỹ thuật | Ví dụ kịch bản (Test ID) |
|---|---|---|---|
| Unit: WAL ghi, đọc, xóa | AC-W01, W02, W07 | FIFO, khởi động lại không gửi lại lô đã ack, xóa sai thứ tự | TestWALFifoRoundTrip, TestWALOldestDoesNotRemove, TestWALSurvivesRestartWithoutResendingAcknowledged, TestWALOutOfOrderRemoveAndUnknownIDs |
| Unit: WAL giới hạn | AC-W03, W04, NFR-W01 | Xoay đoạn, loại theo tuổi và theo dung lượng | TestWALRotatesAndDeletesFinishedSegments, TestWALSizeCapDropsOldestSegments, TestWALAgeCapDropsExpiredBatches, TestWALAgeCapAppliesOnOpenAndOldest |
| Phục hồi và hỏng | AC-W05, W06, NFR-W02 | Cắt đuôi rách, bỏ CRC sai, phát hiện hỏng sau khi mở, chịu kill -9 | TestWALSurvivesKill9, TestWALTruncatesTornTail, TestWALSkipsRecordWithBadCRCOnRecovery, TestWALDetectsCorruptionAfterOpen, FuzzWALScanRecords |
| Quyền tệp, vòng đời và hàng đợi RAM | AC-W08, W09 | 0700 và 0600, từ chối dùng sau khi đóng, RAM giữ lô mới nhất | TestWALFilesAreOwnerOnly, TestWALClosedRejectsUse, TestMemoryEvictsOldestWhenFull, TestMemoryKeepsNewestEvenWhenOversized |
| Sender: gửi và giao nhận | AC-W10 đến W17, W19, W20, NFR-W03, W08 | Thành công, lỗi tạm, Retry-After, 426, 401, 403, 400, 422, 413, gửi bù | TestSuccessDeliversAndUpdatesSkew, TestServerErrorKeepsBatchAndBacksOff, TestDroppedConnectionKeepsBatch, TestRetryAfterIsHonored, TestUpgradeRequiredIsFlaggedAndBatchKept, TestUnauthorizedStopsSendingAndProbesConfig, TestRevokedStopsPermanentlyAndKeepsData, TestBadRequestAndUnprocessableAreDropped, TestTooLargeIsSplitAndSentOnce, TestCatchUpSendsOldestFirstWithinLimit, TestUnreadableEntryIsDropped, TestSequenceNumbersIncreaseAndSeedFromTime |
Gọi lại OnConfigETag | AC-W18 | Sender gọi lại một lần mỗi etag mới. Mới kiểm ở mức Sender, agent.go chưa nối (D-01) | TestConfigETagCallbackFiresOncePerChange |
| Transport | AC-W21 đến W26, NFR-W04, W05 | Header giao thức, không theo redirect, token không lộ, phân tích lỗi, ETag, tùy chọn sai | TestRedirectsAreNotFollowed, TestTokenNeverReachesTheLogs, TestNetworkErrorsNeverContainTheToken, TestGetConfigETag, TestNewValidatesOptions |
| Backoff và pha | AC-W27, W28, NFR-W06, W07 | Lịch nhân đôi, jitter trong khoảng, pha ổn định | TestScheduleDoublesToCapWithFullJitter, TestPhaseOffsetIsStableBoundedAndSpread |
| Tích hợp Agent | AC-W29 đến W32, NFR-W09 | Chưa enroll, lệch máy, thu hồi, khởi động lại giữ lô, dự phòng RAM, engine hỏng | TestRunWithoutCredentialsIsNotEnrolled, TestMachineIDMismatchStopsDelivery, TestRevokedAgentStopsContactingTheCollector, TestBufferedBatchesSurviveRestart, TestUnusableStateDirFallsBackToMemory, TestBrokenEngineFailsRun |
| Chưa có: 20 lần mất điện mô phỏng | NFR-W02 | Tiêu chí thoát giai đoạn 1 của L2-NFR-07 | ĐỀ XUẤT: kịch bản chạy lặp kill -9 và cắt đuôi ngẫu nhiên trên máy thử (OQ-W6, OQ-W7) |
| Chưa có: hạn mức tốc độ so với gửi bù | NFR-W11 | Khẳng định tốc độ trung bình dưới 4 yêu cầu mỗi phút khi có tồn đọng | ĐỀ XUẤT: kiểm thử với Collector giả có token bucket (OQ-W1) |
| Chưa có: đo dừng êm 10 giây | NFR-W09 | Dừng êm trong lúc gửi | ĐỀ XUẤT: kiểm thử SIGTERM khi máy chủ giả chậm (OQ-W7) |
| Chưa có: thứ tự khi tách lô | AC-W13 | Xác nhận hành vi hàng đợi khi nửa lô ghi lại | ĐỀ XUẤT (OQ-W4) |
Chưa có: FuzzWALScanRecords trong CI | NFR-W02 | make fuzz và fuzz-smoke chỉ chạy FuzzLoadNeverPanics. Bộ fuzz WAL chỉ chạy như kiểm thử hạt giống trong go test | ĐỀ XUẤT: thêm vào make fuzz (OQ-W12) |
Chưa có: khóa đồng thời state_dir | Chưa có bất biến tương ứng | Hai tiến trình cùng thư mục | ĐỀ XUẤT (OQ-W11) |
Kiểm thử chạy cả với -race trong CI (go test -race -count=1 ./..., cần cgo).
15. Trình tự triển khai
15.1. Ma trận milestone
| Milestone | Nội dung | Phụ thuộc | Đóng góp nghiệm thu |
|---|---|---|---|
| M1 | WAL bền theo đoạn, CRC, checkpoint, hàng đợi RAM dự phòng. ĐÃ HIỆN THỰC | Không | AC-W01 đến W09 |
| M2 | Transport HTTPS, Backoff, PhaseOffset. ĐÃ HIỆN THỰC | Không | AC-W21 đến W28 |
| M3 | Sender: gửi, phân loại trạng thái, tách lô, thăm dò, thu hồi, gửi bù. ĐÃ HIỆN THỰC | M1, M2 | AC-W10 đến W20 |
| M4 | Nối Agent: vòng chu kỳ, openQueue, kiểm machine_id, SIGHUP. ĐÃ HIỆN THỰC | M1, M2, M3 | AC-W29 đến W32 |
| M5 | Khép khoảng trống quan sát và độ bền: chỉ số lô bị WAL xóa và trạng thái giao nhận, fsync thư mục checkpoint, khóa state_dir, nối OnConfigETag, xả khi tắt, tách lô giữ FIFO. ĐỀ XUẤT | M4 | AC mới (chưa có) |
| M6 | Khép rủi ro hợp đồng: giảm gửi bù theo hạn mức (OQ-W1), bỏ phần lỗi thay cả lô (OQ-W2), dùng dropped_points của ack. ĐỀ XUẤT, cần quyết định phía Collector | M4 và Collector | AC mới (chưa có) |
| M7 | Đo và mô phỏng: 20 lần mất điện, dừng êm 10 giây, fuzz WAL trong CI. THIẾT KẾ CHƯA XÂY | M4 | NFR-W02, W09 |
15.2. Sơ đồ phụ thuộc milestone
flowchart LR
M1["M1 · WAL"]
M2["M2 · Transport và Backoff"]
M3["M3 · Sender"]
M4["M4 · Nối Agent"]
M5["M5 · Khép độ bền"]
M6["M6 · Khép hợp đồng"]
M7["M7 · Đo và mô phỏng"]
M1 --> M3
M2 --> M3
M3 --> M4
M4 --> M5
M4 --> M6
M4 --> M7
style M1 fill:#2d4a3e,stroke:#5fb37a,color:#fff
style M2 fill:#2d4a3e,stroke:#5fb37a,color:#fff
style M3 fill:#2d4a3e,stroke:#5fb37a,color:#fff
style M4 fill:#2d4a3e,stroke:#5fb37a,color:#fff
style M5 fill:#3a3320,stroke:#d9b84a,color:#fff
style M6 fill:#3a3320,stroke:#d9b84a,color:#fff
style M7 fill:#444,stroke:#aaa,color:#fffChú giải: xanh lá là đã hiện thực, vàng là đề xuất, xám là thiết kế chưa xây. Đường găng tới các khoản còn lại là M1 hoặc M2, M3, M4 rồi mới đến M5, M6, M7 (các khoản này độc lập nhau).
Phụ lục A: Open Questions
| # | Câu hỏi | Hành vi tạm thời | Owner | Mã theo dõi |
|---|---|---|---|---|
| OQ-W1 | Gửi bù tối đa 3 yêu cầu mỗi tick (6 mỗi phút) vượt hạn mức bền vững 4 mỗi phút của giao thức. Có cần giảm mức gửi bù hoặc nhận biết 429 để tự hãm không? | Chạy nguyên, 429 được xử lý như lỗi tạm và lùi bước, không mất dữ liệu | chưa chỉ định | L3-WAL-OQ1 |
| OQ-W2 | 422 làm rơi cả lô, trong khi giao thức ghi "bỏ phần lỗi". Collector có trả chi tiết phần lỗi để agent gửi lại phần còn lại không? | Bỏ cả lô, đếm điểm vào DroppedSamples | chưa chỉ định | L3-WAL-OQ2 |
| OQ-W3 | 426 hiện chỉ đặt cờ và giữ lô để lùi bước, trong khi giao thức nói "tiếp tục gửi nếu có thể". Chọn hành vi nào? | Giữ lô, backoff, hiện upgrade_required=true ở Status() | chưa chỉ định | L3-WAL-OQ3 |
| OQ-W4 | Nửa lô của lần tách 413 ghi vào cuối hàng đợi, phá thứ tự FIFO, hai nửa chung seq. Có cần ghi lại ở đầu hoặc đổi seq? | Chấp nhận, an toàn nhờ khử trùng theo (series, ts) | chưa chỉ định | L3-WAL-OQ4 |
| OQ-W5 | Lô bị WAL xóa (đầy hoặc quá tuổi) không vào DroppedSamples, chỉ có log. Có thêm chỉ số riêng không? | Chỉ log (D-10) | chưa chỉ định | L3-WAL-OQ5 |
| OQ-W6 | WAL chưa có phiên bản định dạng (D-15). Khi đổi định dạng thì nâng cấp thế nào? | Không có. Đổi định dạng cần xóa hoặc chuyển đổi WAL | chưa chỉ định | L3-WAL-OQ6 |
| OQ-W7 | Không xả lô khi tắt và giới hạn dừng 10 giây chưa được đo (D-16). Có cần bước xả có hạn, và có nên thoát hẳn khi bị thu hồi thay vì ngủ? | Dừng ngay, lô ở lại WAL. Tiến trình bị thu hồi vẫn active | chưa chỉ định | L3-WAL-OQ7 |
| OQ-W8 | dropped_points, warnings chỉ được log và ack_seq bị bỏ qua. Có dùng để hiệu chỉnh gửi hoặc cảnh báo không? | Chỉ log | chưa chỉ định | L3-WAL-OQ8 |
| OQ-W9 | sent_at_ms bằng thời điểm thu, không phải lúc gửi. Có đổi thành lúc gửi để Collector đo độ trễ tồn đọng không? | Giữ nguyên | chưa chỉ định | L3-WAL-OQ9 |
| OQ-W10 | Mã ngoài bảng (409, 404, 3xx) rơi vào nhánh thử lại, có thể lặp vô hạn với lô không bao giờ được nhận. Có cần bỏ lô sau số lần nhất định? | Thử lại với backoff, WAL bị giới hạn tuổi và dung lượng | chưa chỉ định | L3-WAL-OQ10 |
| OQ-W11 | Không có khóa tệp trên state_dir. Hai tiến trình cùng thư mục có thể làm hỏng WAL. Có cần flock? | Không xử lý, dựa vào dịch vụ systemd một bản | chưa chỉ định | L3-WAL-OQ11 |
| OQ-W12 | FuzzWALScanRecords không nằm trong make fuzz hay CI fuzz-smoke. Có thêm không? | Chỉ chạy như hạt giống trong go test | chưa chỉ định | L3-WAL-OQ12 |
| OQ-W13 | X-Request-Id không được ghi vào log agent, khó đối chiếu hai phía. Có ghi vào log khi lỗi không? | Không ghi | chưa chỉ định | L3-WAL-OQ13 |
Phụ lục B: ADR nội bộ
| Mã ADR | Quyết định | Trạng thái | Động lực |
|---|---|---|---|
| ADR-W01 | WAL tự hiện thực bằng thư viện chuẩn (đoạn, CRC32C, deflate, checkpoint), không dùng thư viện lưu trữ ngoài | Đã hiện thực | Không thêm phụ thuộc, tệp nhị phân nhỏ, hành vi kiểm soát được. Đánh đổi: tự chịu trách nhiệm về độ bền (OQ-W6, OQ-W12) |
| ADR-W02 | Dự phòng hàng đợi RAM khi không mở được WAL | Đã hiện thực | Ưu tiên agent vẫn chạy và gửi được. Đánh đổi: mất lô khi khởi động lại, chỉ có log Error báo (mục 8.1) |
| ADR-W03 | Một goroutine cho thu, xếp hàng và gửi, không có gửi nền | Đã hiện thực | Loại tranh chấp, đơn giản, xác định. Đánh đổi: lần gửi chậm làm trễ chu kỳ kế, tốc độ gửi bị chặn bởi interval |
| ADR-W04 | WAL nén deflate, phần truyền nén gzip | Đã hiện thực (D-09) | Deflate nhẹ cho tệp, gzip là yêu cầu của giao thức. Đánh đổi: nén hai lần khác nhau khi gửi lại từ đĩa |
| ADR-W05 | Giao nhận at-least-once, dựa vào khử trùng theo (series, ts) phía TSDB | Đã hiện thực | Cho phép gửi lặp an toàn nên không cần ack hai pha hay fsync thư mục checkpoint |
| ADR-W06 | Loại theo đoạn nguyên khi WAL đầy, bỏ cũ giữ mới | Đã hiện thực | Số liệu mới quan trọng hơn. Đơn giản hơn ghi lại tệp. Đánh đổi: thô, mất tới một đoạn |
| ADR-W07 | Thu hồi (403) dừng vĩnh viễn nhưng tiến trình không thoát | Đã hiện thực | Tránh systemd khởi động lại liên tục khi bị thu hồi. Đánh đổi: giám sát dịch vụ vẫn thấy active (OQ-W7) |
| ADR-W08 | 400, 422 bỏ cả lô, không thử lại | Đã hiện thực, cần xác nhận (OQ-W2) | Lô hỏng không bao giờ được nhận, thử lại chỉ tốn tài nguyên. Đánh đổi: mất cả phần hợp lệ |
| ADR-W09 | Tách lô 413 một lần, ghi nửa lỗi vào cuối hàng đợi | Đã hiện thực, cần xác nhận (OQ-W4) | Không bao giờ lặp tách. Đánh đổi: mất FIFO |
Phụ lục C: Section Profile
| Phân mục | Hồ sơ quy chuẩn | Trạng thái điền | Giải trình |
|---|---|---|---|
| §0 Metadata & Sign-off | Bắt buộc | Đã điền | Người ký để "chưa chỉ định" |
| §1 Scope | Bắt buộc | Đã điền | |
| §2 Yêu cầu | Bắt buộc | Đã điền | |
| §3 Kiến trúc | Bắt buộc | Đã điền | |
| §4 Domain model | Bắt buộc | Đã điền | |
| §5 API contract | Bắt buộc | Điền thu gọn | Cụm là client, không có API mạng phục vụ. Hợp đồng gồm interface Go và HTTPS gọi ra |
| §6 Data schema | Tùy chọn | Đã điền | Lược đồ tệp WAL và checkpoint thay cho CSDL |
| §7 Thuật toán | Bắt buộc | Đã điền | |
| §8 Xử lý lỗi | Bắt buộc | Đã điền | |
| §9 Suy thoái | Bắt buộc | Đã điền | 9.2 mô tả WAL là bộ đệm tạm, không sao lưu |
| §10 Đồng thời | Bắt buộc | Điền thu gọn | Không có giao dịch CSDL, dùng ranh giới thao tác tệp |
| §11 Bảo mật | Bắt buộc | Điền thu gọn | 11.2 chỉ mô tả phía gọi ra, không có yêu cầu vào |
| §12 Cấu hình | Bắt buộc | Đã điền | Không có feature flag hoạt động |
| §13 Telemetry | Bắt buộc | Điền thu gọn | Không có probe mạng và trace phân tán. 13.5 chỉ có X-Request-Id |
| §14 Kiểm thử | Bắt buộc | Đã điền | Có 6 khoảng trống đề xuất |
| §15 Triển khai | Bổ sung | Đã điền |