L3 - Nền tảng giám sát máy chủ - Collector - Registry, Enroll, Presence
Quy ước tên: L3 - <P&L> - <Hệ thống L2> - <Thành phần>. P&L: chưa chỉ định. Hệ thống L2: Collector. Thành phần: Registry (danh tính agent), Resolver (tra ngược Access Hub), Syncer (đồng bộ), hub client, enroll proxy và Presence (lần thấy cuối). Mã: internal/registry, internal/hubclient, internal/presence, phần enroll của internal/ingest/handlers.go.
Quy ước đánh dấu: "Đã làm" là có mã và kiểm thử. "Chỉ thiết kế" là chỉ có trong docs/. "Đề xuất" là ý của tác giả bản nháp. Mã thắng khi lệch tài liệu.
0. Front matter
Thông tin tài liệu đầy đủ
| Trạng thái | BẢN NHÁP, phiên bản 0.1, 2026-09-30. Người phê duyệt: chưa chỉ định |
| Thành phần (Component) | Registry (memory và Redis), Resolver, Syncer, hub client HTTP, enroll proxy, Presence tracker (memory và Redis) |
| Truy vết L2 | L2 SAD Collector: thành phần "Registry, Resolver, Enroll proxy, Presence" (mục 2.3, 6.1), FR 1, 2 (mục 3), NFR 7, 9 (mục 4), mục 7.1 (AgentRecord, PresenceEntry), rủi ro AR-005, AR-009. Mục tiêu L1: G1, G5 (qua L2) |
| Tài liệu cha | L2 SAD Collector |
| Tài liệu anh em | Ingest API, Alerting, Outbox, Admin API |
| Tier | Đề xuất Cấp 3 (kế thừa L2, chưa xác nhận) |
| Phân loại dữ liệu | Nhạy cảm vừa: băm token, định danh agent, công ty, máy chủ. Không lưu token rõ |
| Blast radius | Registry sai làm agent bị từ chối (401, 403) hoặc, tệ hơn, gắn sai tenant. Mất Redis: ingest hỏi lại Access Hub có giới hạn. Presence sai gây báo down nhầm hoặc bỏ sót |
| Sign-off gate | Chưa chỉ định. Không có ai đã sign-off |
Truy vết mục L3 đến thành phần L2:
| Mục L3 | Thành phần hoặc mục L2 |
|---|---|
| 1 Phạm vi | 2.3 Các thành phần chính |
| 2 Yêu cầu | 3 Functional (1, 2), 4 NFR |
| 3 Kiến trúc | 6.1 Component table |
| 4, 6 Domain, dữ liệu | 7.1 Data Model |
| 5 Hợp đồng | 6.2 Integration (Ingest đến Access Hub, đồng bộ registry) |
| 7 Thuật toán | 8.1, 8.2 (enroll, state agent) |
| 8, 9 Lỗi, suy thoái | 6.3 Resilience, 12.2 Reliability |
| 11 Bảo mật | 9.1 Identity, 9.3 Secrets |
| 13 Telemetry | 13 Observability |
1. Scope and Non-Goals
1.1 Vai trò
Trả lời câu hỏi "token này là agent nào, của công ty nào, máy chủ nào, còn dùng được không" mà không phải hỏi Access Hub ở mỗi request. Registry là bản sao đọc của dữ liệu agent do Access Hub sở hữu (nguồn sự thật). Presence lưu lần thấy cuối của mỗi agent để Detector phát hiện mất tín hiệu.
1.2 Context diagram
flowchart LR
classDef bc fill:#1f3a5f,stroke:#4a90d9,color:#fff;
classDef owned fill:#2d4a3e,stroke:#5fb37a,color:#fff;
classDef entity fill:#3a3320,stroke:#d9b84a,color:#fff;
classDef datastore fill:#3a2d4a,stroke:#a06fd9,color:#fff;
ING["Ingest API"]:::bc
ADM["Admin API"]:::bc
DET["Detector"]:::bc
HUB(["Access Hub (ext)"]):::entity
RG["Registry và Resolver"]:::owned
PS["Presence"]:::owned
RED[("Redis")]:::datastore
ING -->|"tra token, ghi sau enroll"| RG
ING -->|"cập nhật lần thấy cuối"| PS
ADM -->|"đẩy và thu hồi agent"| RG
DET -->|"lấy agent quá hạn"| PS
RG -->|"tra ngược, enroll, đồng bộ"| HUB
RG -->|"lưu bản ghi"| RED
PS -->|"lưu lần thấy"| RED| Chiều | Bên | Nội dung |
|---|---|---|
| Vào | Ingest API | Lookup theo băm token, Put sau enroll, Touch, Ensure, Remove |
| Vào | Admin API | Put (đẩy agent), Revoke, DeleteServer, DeleteCompany, ByServer, Counts |
| Vào | Detector, Reporter | Expired, DrainSeen, Counts, CompanyCounts (L3 Alerting) |
| Ra | Access Hub | POST /enroll, GET /agents?token_hash=, GET /agents?since=&limit= |
| Ra | Redis | Khóa reg:* và pres:* (tiền tố redis.prefix, mặc định ah:) |
1.3 Trong phạm vi
- Định nghĩa
AgentRecord, trạng thái, băm token, hạn, phiên bản. - Hai hiện thực Registry (bộ nhớ, Redis), cache nút và cache âm.
- Resolver: tra registry, rồi Access Hub có bảo vệ (cache âm, giới hạn, single-flight).
- Syncer kéo thay đổi theo
since. - hub client
HTTP(internal/hubclient). - Luồng enroll proxy (phần nghiệp vụ, không phần HTTP của Ingest).
- Presence tracker (bộ nhớ, Redis).
1.4 Ngoài phạm vi
| Nội dung | Không thuộc BC | Thuộc về |
|---|---|---|
| Tạo agent, xác thực License, ràng buộc máy chủ, sinh agent token | Có | Access Hub (HUB-*) |
Xoay token (renew) | Có | Chưa làm (stub 503 ở Ingest) |
HTTP của /agent/v1/enroll (header, giới hạn, mã lỗi) | Có | L3 Ingest API |
| Quyết định "agent down" và sự kiện | Có | L3 Alerting, Outbox |
| Endpoint Admin đẩy agent | Có | L3 Admin API |
| Dọn bản ghi theo TTL | Có | Chưa làm (registry và presence không TTL, AR-009 của L2) |
2. Requirements
2.1 Functional Requirements
| # | Trách nhiệm | Giải thích | Hiện thực ở |
|---|---|---|---|
| FR-REG-01 | Tra danh tính theo băm token | Trả AgentRecord gồm agent, công ty, máy chủ, trạng thái, hạn | internal/registry/registry.go, TestLookupFlow |
| FR-REG-02 | Chống ghi đè bằng phiên bản cũ | Put không cho Version cũ ghi đè mới (Version 0 luôn được áp dụng) | TestStaleVersionIgnored |
| FR-REG-03 | Thu hồi có tombstone | Băm thu hồi được giữ để trả 403 thay vì 401 | TestRevokeKeepsTombstone, TestPutRevokedStateTombstones |
| FR-REG-04 | Token cũ còn hiệu lực đến hạn | prev_token_hash có hạn theo prev_token_expires_at; thiếu hoặc sai nghĩa là đã hết hạn | TestFromRecordPreviousToken, TestTokenRotationKeepsPreviousUntilExpiry |
| FR-REG-05 | Hết hạn và pending | Bản ghi hết hạn hoặc pending coi như không dùng được | TestPendingAndExpiredTokens |
| FR-REG-06 | Xóa theo phạm vi | DeleteServer, DeleteCompany, ByServer giới hạn trong công ty | TestDeleteScopes, TestByServerIsCompanyScoped |
| FR-REG-07 | Tra ngược Access Hub khi miss | Cache âm, giới hạn toàn cục, single-flight | TestResolveFallsBackToHubAndCaches, TestNegativeCache, TestGlobalLimiterProtectsHub, TestConcurrentMissesCollapse |
| FR-REG-08 | Access Hub lỗi không thành token sai | Trả ErrHubUnavailable (ingest trả 503) | TestHubDownIsNotAnInvalidToken |
| FR-REG-09 | Đồng bộ theo trang | SyncOnce kéo 500 bản ghi mỗi trang đến hết; Run chạy ngay rồi theo chu kỳ | TestSyncerPagesAndAdvances |
| FR-REG-10 | Redis hỏng thì hỏi Access Hub | Lỗi Redis đọc thành ErrNotFound | TestRedisRegistryDownFallsBackToNotFound |
| FR-ENR-01 | Enroll proxy | Chuyển tiếp sang Access Hub, lưu agent active phiên bản 0, xóa cache âm, trả agent token một lần | TestEnrollProxy (Ingest) |
| FR-ENR-02 | Ánh xạ lỗi enroll | 409 thành already_enrolled hoặc token_used, 422 thành binding_failed, 429 thành rate_limited, tạm thời thành 503, khác thành 401 | TestEnrollErrors, TestEnrollPassesIPAndErrors (hubclient) |
| FR-PRS-01 | Lần thấy cuối | Touch cập nhật, Ensure gieo thời gian ân hạn, Remove xóa | TestMemoryTracker, TestRedisTracker, TestEnsureSeedsGraceAndIsNotReported |
| FR-PRS-02 | Một chủ duy nhất cho chuyển down | Tiến trình SADD vào pres:down trả 1 sở hữu chuyển trạng thái | TestRedisTrackerSharedAcrossProcesses |
| FR-PRS-03 | Chỉ báo lần thấy thật | Real=false nghĩa là thời gian chỉ là hạt giống, không báo hub | TestDrainSeenOnlyReportsProgress, TestLastSeenNeverGoesBackwards |
| FR-PRS-04 | Phục hồi | Touch trên agent đang down đưa về up và kích hoạt OnAgentUp | TestTouchRecoversDownAgent |
2.2 Non-Functional Requirements
Allocated:
| NFR L2 | Target | Cách đáp ứng ở L3 |
|---|---|---|
| NFR-08 (chịu mất Access Hub) | Ingest chạy bằng registry cache | Registry Redis không TTL, cache nút cache_ttl 15 giây |
| NFR-12 (cô lập tenant) | Không lẫn tenant | Bản ghi mang company_id; chỉ mục theo company_id; ByServer theo công ty |
| NFR-02 (sẵn sàng) | Mất một node không mất dữ liệu | Trạng thái dùng chung trong Redis |
Inherited: timeout client Redis 500 ms mặc định và lệnh bị chặn từ internal/redisx; client HTTP từ internal/hubclient.
Owned:
| ID | Target | Parent L2-NFR | Satisfied-by |
|---|---|---|---|
| NFR-REG-01 | Một miss đồng thời chỉ gây một lần gọi Access Hub | NFR-08 | single-flight, TestConcurrentMissesCollapse |
| NFR-REG-02 | Tra Access Hub tối đa 50 mỗi giây mỗi nút (mặc định) | NFR-08 | hub.lookup_per_second, TestGlobalLimiterProtectsHub |
| NFR-REG-03 | Cache âm 60 giây, tối đa 100.000 mục | NFR-08 | TestNegativeCache |
| NFR-REG-04 | Cache nút tối đa 200.000 mục, TTL 15 giây | NFR-01 | internal/registry/redis.go |
| NFR-REG-05 | Một tiến trình sở hữu mỗi chuyển down | NFR-13 | internal/presence/redis.go |
2.3 Acceptance Criteria
| AC | Given / When / Then | Test ID |
|---|---|---|
| AC-REG-01 | Given token chưa có trong registry, When tra, Then hỏi Access Hub một lần, lưu, tra lại thành công | TestResolveFallsBackToHubAndCaches |
| AC-REG-02 | Given Access Hub báo không tồn tại, When tra lại trong 60 giây, Then không gọi Access Hub lần nữa | TestNegativeCache |
| AC-REG-03 | Given Access Hub lỗi, When tra, Then ErrHubUnavailable, không phải "token sai" | TestHubDownIsNotAnInvalidToken |
| AC-REG-04 | Given bản ghi version 5, When Put version 3, Then bản ghi giữ version 5 | TestStaleVersionIgnored |
| AC-REG-05 | Given agent bị thu hồi, When tra băm cũ, Then ErrRevoked | TestRevokeKeepsTombstone |
| AC-REG-06 | Given Redis hỏng, When tra, Then ErrNotFound rồi hỏi Access Hub | TestRedisRegistryDownFallsBackToNotFound |
| AC-REG-07 | Given hai tiến trình cùng quét, When agent quá hạn, Then chỉ một tiến trình nhận chuyển down | TestRedisTrackerSharedAcrossProcesses |
| AC-REG-08 | Given bản ghi công ty A, When ByServer với công ty B, Then rỗng | TestByServerIsCompanyScoped |
2.4 Quality Attribute Scenarios
| Nguồn | Kích thích | Môi trường | Phản hồi | Thước đo |
|---|---|---|---|---|
| Agent | Gửi token lần đầu sau restart Redis | Redis rỗng | Ingest hỏi Access Hub (có giới hạn), lưu lại | Không quá 50 lần tra mỗi giây mỗi nút |
| Kẻ tấn công | Gửi hàng loạt token ngẫu nhiên | Bất kỳ | Cache âm 60 giây và giới hạn chặn tải lên Access Hub | Số lần gọi Access Hub không vượt trần |
| Access Hub | Mất kết nối | Token đã cache | Ingest vẫn xác thực | Không 401 sai |
| Quản trị | Thu hồi agent | Bình thường | Ingest trả 403 trong vòng cache_ttl (15 giây) sau khi bản ghi cập nhật | 403 sau tối đa cache TTL (đề xuất xác nhận khi đo) |
3. Application Architecture
3.1 Kiến trúc thời chạy (C&C)
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;
RS["Resolver"]:::bc
RGM["Registry memory"]:::bc
RGR["Registry Redis"]:::bc
NC["Cache âm và limiter"]:::bc
HC["Hub client"]:::owned
SY["Syncer"]:::bc
PT["Presence tracker"]:::bc
RED[("Redis")]:::datastore
RS --> RGR
RS --> NC
RS --> HC
SY --> HC
SY --> RGR
RGR --> RED
RGM -.-> RGR
PT --> REDMũi tên chỉ chiều khởi tạo lời gọi. Registry memory là hiện thực thay thế của cùng interface (dùng khi không có Redis), vẽ nét đứt.
| Thành phần | Trách nhiệm | Vòng đời |
|---|---|---|
| Resolver | Lookup theo băm: registry, cache âm, limiter toàn cục, single-flight, hub, Put, tra lại | Theo tiến trình |
| Registry memory | Hai map byID, byHash, tập revoked, hook OnUpsert, OnRevoke, OnDelete | Theo tiến trình |
| Registry Redis | Bản ghi trong Redis, cache nút (tối đa 200.000 mục, cache_ttl) | Bền trong Redis |
| Cache âm và limiter | Cache âm 60 giây tối đa 100.000 mục; limiter hub-lookup toàn cục | Theo tiến trình |
Hub client (HTTP) | Gọi <hub>/api/v1/monitoring/collector, Bearer hub.token | Theo tiến trình |
| Syncer | Kéo since theo trang 500; chạy trên vai trò worker và admin khi có hub | Theo tiến trình |
| Presence tracker | Ghi và đọc lần thấy cuối trong bộ nhớ hoặc Redis | Bền trong Redis |
| Kết nối | Kiểu | Chi tiết |
|---|---|---|
| Resolver đến Registry | Đồng bộ | Timeout client Redis 500 ms |
| Resolver đến Hub client | Đồng bộ, single-flight | hub.timeout 10 giây, một lần gọi cho nhiều người chờ |
| Syncer đến Hub client | Đồng bộ, định kỳ | hub.sync_interval 1 phút, 500 mỗi trang |
| Tất cả đến Redis | Đồng bộ | Client internal/redisx (RESP2) với danh sách lệnh bị chặn |
3.2 Module view
flowchart TB
classDef bc fill:#1f3a5f,stroke:#4a90d9,color:#fff;
classDef owned fill:#2d4a3e,stroke:#5fb37a,color:#fff;
APP["internal/app"]:::bc
ING["internal/ingest"]:::bc
ADM["internal/admin"]:::bc
RGS["internal/registry"]:::owned
HCP["internal/hubclient"]:::owned
PRE["internal/presence"]:::owned
RX["internal/redisx"]:::owned
RLP["internal/ratelimit"]:::owned
TS["internal/tsdb (ValidID)"]:::owned
APP --> RGS
APP --> PRE
ING --> RGS
ING --> PRE
ING --> HCP
ADM --> RGS
ADM --> PRE
RGS --> HCP
RGS --> RX
RGS --> RLP
PRE --> RXTệp: registry.go (interface, bộ nhớ), redis.go (Redis), resolver.go (Resolver, Syncer, FromRecord), hubclient.go, presence.go (interface, bộ nhớ), redis.go (Redis).
4. Domain Model
classDiagram
class AgentRecord {
<<registry>>
agent_id
company_id
server_id
state
token_hash
expires_at
prev_token_hash
prev_token_expires_at
version
}
class AgentState {
<<enum>>
pending
active
revoked
}
class Registry {
<<interface>>
Lookup
Put
Revoke
Get
ByServer
DeleteCompany
DeleteServer
Count
}
class Resolver {
<<service>>
Lookup
}
class PresenceEntry {
<<presence>>
agent_id
company_id
server_id
last_seen_ms
real
down
}
Registry "1" --> "*" AgentRecord
AgentRecord --> AgentState
Resolver --> Registry
PresenceEntry ..> AgentRecord : ref agent_idBất biến:
- Token chỉ lưu dạng băm SHA-256 (64 ký tự hex thường).
token_hashbắt buộc khi trạng tháiactive. Putkhông cho phiên bản cũ ghi đè phiên bản mới. Phiên bản 0 (từ enroll) luôn áp dụng.- Băm đã thu hồi được giữ làm tombstone để trả 403.
company_id,server_id,agent_idphải khớp^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$(tsdb.ValidID, dùng ở Admin).
5. API Contract
Thành phần này không có API HTTP riêng phơi bày cho agent. Hợp đồng là: (a) interface Go nội bộ, (b) các lời gọi ra Access Hub, (c) Admin API đẩy agent (L3 Admin API).
5.1 Operations
Interface Registry (Go): Lookup(hash), Put(record), Revoke(agentID), Get(agentID), ByServer(company, server), DeleteCompany(company), DeleteServer(company, server), Count().
Hub client (cơ sở <hub.base_url>/api/v1/monitoring/collector, Bearer hub.token, phản hồi có thể bọc trong data, trần đọc 8 MiB, timeout mặc định 15 giây nếu không đặt):
| Method | Path | Dùng cho |
|---|---|---|
| POST | /enroll | Enroll proxy |
| GET | /agents?token_hash= | Tra ngược một token |
| GET | /agents?since=&limit= | Đồng bộ theo trang |
| POST | /events | Gửi sự kiện (xem L3 Alerting) |
| POST | /agents/seen | last_seen |
| POST | /inventory | Kiểm kê |
| POST | /heartbeat | Heartbeat collector |
APIError.Temporary đúng khi HTTP 429 hoặc 5xx.
5.2 Request và Response schema
Ký hiệu ! bắt buộc, ? tùy chọn. Hợp đồng chi tiết phía Access Hub là HUB-* (docs/06-access-hub-integration.md), API thật chưa xây, dev dùng cmd/mockhub.
AgentRecord (registry, JSON ở Admin và hub):
agent_id!: string
company_id!: string
server_id!: string
state!: "pending" | "active" | "revoked"
token_hash?: string (64 hex thường, bắt buộc khi active)
expires_at?: timestamp
prev_token_hash?: string
prev_token_expires_at?: timestamp
version!: int >= 0
GET /agents?token_hash= -> AgentRecord hoặc 404
GET /agents?since=&limit= -> { agents![]: AgentRecord, more!: bool, next_since?: cursor }
POST /enroll (Access Hub) -> { agent_id!, company_id!, server_id!, token_hash!, agent_token!, config! }Nếu kết quả enroll thiếu trường, Ingest trả 503 (kết quả không đầy đủ).
Errors: ErrNotFound, ErrRevoked, ErrExpired, ErrPending, ErrHubUnavailable.
5.3 Error codes
| Lỗi nội bộ | HTTP ở Ingest | Ghi chú |
|---|---|---|
ErrNotFound, ErrExpired, ErrPending | 401 unauthorized | |
ErrRevoked | 403 agent_revoked | |
ErrHubUnavailable | 503, Retry-After: 30 | Bị giới hạn hoặc Access Hub lỗi |
| Khác | 503 server_error | |
| Enroll: hub 409 | 409 already_enrolled hoặc token_used | |
| Enroll: hub 422 | 422 binding_failed | |
| Enroll: hub 429 | 429 rate_limited, Retry-After: 60 | |
| Enroll: lỗi tạm thời | 503, Retry-After: 30 | |
| Enroll: lỗi khác | 401 |
Hợp đồng mã HTTP đầy đủ ở L3 Ingest API mục 5.3.
5.4 Versioning
Interface Go là nội bộ, đổi được cùng mã. Hợp đồng với Access Hub: tiền tố /api/v1/monitoring/collector. Trường version của AgentRecord là đồng hồ logic chống ghi đè. Không có cơ chế tương thích N và N-1 cho hub API (đề xuất: bổ sung khi HUB-* ổn định).
5.5 Authz
| Hướng | Cơ chế |
|---|---|
| Collector gọi Access Hub | Bearer hub.token (đọc từ hub.token_file hoặc AHC_HUB_TOKEN(_FILE)) |
| Access Hub gọi Collector (đẩy agent) | Qua Admin API, Bearer admin token (L3 Admin API) |
| Chủ thể | Đọc registry | Ghi registry | Xóa theo tenant |
|---|---|---|---|
| Ingest (nội bộ) | Có | Chỉ sau enroll thành công | Không |
| Admin (qua token admin) | Có | Có | Có |
| Syncer | Không | Có (từ Access Hub) | Không |
6. Physical Data Schema
6.1 Mapping
Redis (tiền tố ah:, cấu hình redis.prefix). Không đặt TTL cho khóa registry.
| Khóa | Kiểu | Nội dung |
|---|---|---|
reg:tok:{hash} | hash | agent_id, company_id, server_id, state, exp, ver |
reg:agent:{id} | hash | company_id, server_id, state, ver, tokens (danh sách băm) |
reg:srv:{co}:{s} | set | agent của một máy chủ |
reg:co:{co} | set | agent của một công ty |
reg:all | set | mọi agent |
pres:a:{agent} | hash | trạng thái presence của agent |
pres:up, pres:up:{co} | zset | agent đang up, điểm là lần thấy cuối (ms) |
pres:down, pres:down:{co} | set | agent đang down |
pres:unrep | zset | agent có lần thấy thật chưa báo hub |
flowchart TB
classDef datastore fill:#3a2d4a,stroke:#a06fd9,color:#fff;
TOK[("reg:tok:hash")]:::datastore
AGT[("reg:agent:id")]:::datastore
SRV[("reg:srv:co:s")]:::datastore
CO[("reg:co:co")]:::datastore
ALL[("reg:all")]:::datastore
PA[("pres:a:agent")]:::datastore
UP[("pres:up")]:::datastore
DN[("pres:down")]:::datastore
TOK --> AGT
AGT --> SRV
AGT --> CO
AGT --> ALL
PA --> UP
PA --> DNSơ đồ cho thấy quan hệ tham chiếu giữa khóa (băm trỏ tới agent, agent nằm trong các tập chỉ mục).
Put xóa khóa token cũ không còn trong danh sách rồi cập nhật các tập. Đọc Redis lỗi được coi như ErrNotFound.
6.2 Phân loại và lưu giữ
| Dữ liệu | Phân loại | Lưu giữ |
|---|---|---|
reg:* | Nhạy cảm vừa (băm token) | Không TTL. Xóa khi đồng bộ, Admin, DeleteServer, DeleteCompany. Tombstone thu hồi giữ vô thời hạn (đề xuất: đặt chính sách dọn, AR-009) |
pres:* | Thấp | Xóa khi Remove, thu hồi, xóa máy chủ |
Redis dev: AOF everysec, maxmemory 256mb, noeviction. Khi Redis đầy ghi lỗi chứ không xóa khóa âm thầm.
7. Algorithms
7.1 Sequences (đường thành công)
Tra token lần đầu (miss rồi hỏi Access Hub):
sequenceDiagram
participant IN as Ingest
participant RS as Resolver
participant RG as Registry
participant HB as Access Hub (ext)
IN->>RS: Lookup(băm token)
RS->>RG: Lookup
RG-->>RS: ErrNotFound
RS->>RS: cache âm, limiter hub-lookup, single-flight
RS->>HB: GET /agents?token_hash=
HB-->>RS: AgentRecord active
RS->>RG: Put(record)
RS->>RG: Lookup lại
RG-->>RS: AgentRecord
RS-->>IN: danh tính (công ty, máy chủ)Enroll:
sequenceDiagram
participant AG as Agent (ext)
participant IN as Ingest
participant HB as Access Hub (ext)
participant RG as Registry
AG->>IN: POST /agent/v1/enroll
IN->>HB: POST /enroll (X-Forwarded-For chứa IP client, timeout 20s)
HB-->>IN: agent, công ty, máy chủ, token băm, agent_token
IN->>RG: Put(active, version 0)
IN->>IN: xóa cache âm của băm token
IN-->>AG: agent_token, collector_url, config7.2 Sequences (đường lỗi)
sequenceDiagram
participant IN as Ingest
participant RS as Resolver
participant RG as Registry
participant HB as Access Hub (ext)
IN->>RS: Lookup(băm token)
RS->>RG: Lookup
alt Redis hỏng
RG-->>RS: ErrNotFound (lỗi Redis đọc như không thấy)
RS->>HB: GET /agents?token_hash=
else thu hồi
RG-->>RS: ErrRevoked
RS-->>IN: ErrRevoked (403)
end
alt Access Hub không có token
HB-->>RS: 404
RS->>RS: ghi cache âm 60 giây
RS-->>IN: ErrNotFound (401)
else Access Hub lỗi hoặc bị giới hạn
HB--)RS: lỗi hoặc không gọi được
RS-->>IN: ErrHubUnavailable (503)
end7.3 State machines
stateDiagram-v2
direction LR
state "Chờ (pending)" as PEND
state "Hoạt động (active)" as ACT
state "Thu hồi (revoked)" as REV
[*] --> PEND
PEND --> ACT: Put active
ACT --> ACT: Put phiên bản mới hơn
ACT --> REV: Revoke hoặc Put revoked
PEND --> REV: Revoke
REV --> [*]: DeleteServer hoặc DeleteCompany
ACT --> [*]: DeleteServer hoặc DeleteCompanystateDiagram-v2
direction LR
state "Up" as UP
state "Down" as DOWN
[*] --> UP: Ensure (gieo ân hạn)
UP --> UP: Touch
UP --> DOWN: quá hạn, SADD pres:down trả 1
DOWN --> UP: Touch
UP --> [*]: Remove
DOWN --> [*]: Remove| Chuyển trạng thái | Sequence |
|---|---|
| pending đến active | Enroll (7.1) hoặc Syncer kéo bản ghi active |
| active đến revoked | Admin DELETE /agents/{id} hoặc Syncer nhận bản ghi revoked |
| Up đến Down | L3 Alerting mục 7.1 |
| Down đến Up | Touch từ Ingest rồi OnAgentUp |
Trạng thái pending tồn tại trong mô hình; Ingest luôn coi là 401.
7.4 Core algorithms
| Vấn đề | Giải pháp | Trade-off |
|---|---|---|
| Bão miss khi Redis rỗng hoặc token giả | Cache âm 60 giây (tối đa 100.000), limiter toàn cục hub-lookup 50/giây, single-flight theo băm | Token mới hợp lệ có thể bị từ chối tạm (503) khi quá tải tra ngược |
| Ghi chồng bản cũ | So sánh Version, bỏ bản cũ hơn | Phụ thuộc Access Hub tăng version đúng |
| Token xoay chồng lấn | prev_token_hash có hạn; thiếu hoặc sai hạn tính là đã hết (Unix(1,0)) | Hạn sai làm token cũ chết sớm (an toàn hơn giữ quá hạn) |
| Hai tiến trình cùng thấy agent quá hạn | SADD pres:down trả 1 thì sở hữu chuyển trạng thái | Nếu tiến trình chết sau SADD, sự kiện có thể mất (AR-001 của L2) |
Báo last_seen một lần | DrainSeen dùng ZPOPMIN nên mỗi mục đến một bên gọi | Nếu gửi hub lỗi sau khi rút, mục đó không báo lại cho đến lần Touch sau |
| Khởi động lại, ân hạn | Ensure gieo thời gian làm hạt giống, Real=false không báo hub | Agent chưa từng báo cáo vẫn bị coi down sau ngưỡng |
| Lỗi Redis | Đọc lỗi thành ErrNotFound để rơi về Access Hub | Che lỗi Redis (đã có metric ahc_registry_lookups_total{result} để thấy) |
| Bản ghi mồ côi | Đồng bộ và Admin xóa theo phạm vi | Không TTL nên cần chính sách dọn (AR-009) |
Quét presence theo lô 500 mỗi lượt.
8. Error Handling
8.1 Bảng lỗi theo bước
| Bước lỗi | Nguyên nhân | Cơ chế xử lý | Trạng thái cuối |
|---|---|---|---|
| Tra Redis | Mất kết nối, timeout 500 ms | Thành ErrNotFound, rơi về Access Hub | Bản ghi nạp lại sau khi hub trả lời |
| Tra Access Hub | Timeout 10 giây, 5xx | ErrHubUnavailable, không ghi cache âm | Ingest 503, agent thử lại |
| Tra Access Hub | 404 | Ghi cache âm 60 giây | 401 |
| Tra Access Hub | Bị limiter chặn | ErrHubUnavailable | 503 Retry-After: 30 |
| Enroll | Access Hub 409, 422, 429 | Ánh xạ mã lỗi (mục 5.3) | Agent nhận mã lỗi, exit code 5 ở phía agent |
| Enroll | Access Hub timeout, 5xx | 503 | Agent thử lại (exit code 6) |
| Enroll | Kết quả thiếu trường | 503 | Agent thử lại |
| Enroll | Không có hub | 503 | |
| Đồng bộ | Access Hub lỗi | Ghi log, thử lại chu kỳ sau; không xóa gì | Registry cũ tiếp tục dùng |
| Put Redis | Lỗi ghi | Trả lỗi cho người gọi | Enroll trả lỗi, Admin trả 503 |
| Presence | Lỗi Redis | Trả lỗi cho người gọi | Xem L3 Alerting |
8.2 Fail-fast
hub.tokenbắt buộc khi cóhub.base_url(lỗi cấu hình, thoát mã 3).- Redis là bắt buộc khi tách vai trò (
TestSplitRolesNeedRedis). TestNewRedisRequiresClients,TestNewValidatesOptionskiểm tra tham số khởi tạo.
8.3 Race conditions
| Tình huống | Xử lý |
|---|---|
| Hai request cùng token miss | Single-flight |
Put chồng nhau (Syncer và Admin) | So sánh Version |
| Hai tiến trình quét presence | SADD trả 1 chọn một bên |
| Thu hồi đồng thời với tra | Tombstone giữ băm; cache nút có thể còn bản cũ tối đa cache_ttl (15 giây) |
9. Degradation
Nguyên tắc: mất Access Hub hay Redis không được biến thành "token sai". Nếu không xác định được thì từ chối tạm (503) thay vì từ chối xác thực (401).
9.1 Dependency matrix
| Dependency lỗi | Hành vi | Kiểu suy thoái | Hệ quả |
|---|---|---|---|
| Access Hub | Tra bằng registry và cache; enroll 503 | Giảm chức năng | Agent mới không enroll được |
| Redis | Registry đọc lỗi rơi về Access Hub có giới hạn; presence không cập nhật được | Giảm chức năng | Tăng tải Access Hub; Detector mù trong lúc Redis mất (AR-005 của L2) |
| Cả Redis và Access Hub | Mọi token chưa có trong cache nút bị 503 | Từ chối | Agent đệm WAL |
9.2 Backup và recovery
Registry dựng lại được: từ Access Hub qua Syncer (kéo since từ 0) hoặc dần qua tra ngược. RTO khởi tạo lại registry rỗng: chưa đo; đề xuất vài phút với 10.000 agent ở 500 mỗi trang (20 trang). Presence dựng lại bằng Ensure và các lần Touch. RPO không áp dụng cho registry (nguồn sự thật ở Access Hub). Redis dev có AOF everysec và replica. Sao lưu Redis riêng: chưa có.
10. Concurrency
10.1 Ranh giới giao dịch
Mỗi Put Redis cập nhật nhiều khóa (token, agent, các tập) không nằm trong một giao dịch nguyên tử theo như mã đã đọc. Đề xuất xác minh: nếu có MULTI hoặc pipeline thì ghi lại. Hệ quả tiềm ẩn: một lượt Put dở dang có thể để khóa chỉ mục lệch tạm thời, lần Put sau sửa lại.
10.2 Cơ chế
| Cơ chế | Mô tả | Mối nguy |
|---|---|---|
| Single-flight theo băm | Gộp tra ngược trùng | Lỗi của một lần gọi lan tới mọi người chờ |
Limiter hub-lookup | Token bucket toàn nút | Tải hợp lệ bị 503 khi tăng vọt |
So sánh Version | Loại bản ghi cũ | Phiên bản không tăng đơn điệu làm mất cập nhật |
SADD pres:down | Chọn chủ chuyển down | Mất sự kiện nếu chủ chết ngay sau |
ZPOPMIN (DrainSeen) | Mỗi mục một bên nhận | Mất mục nếu báo hub lỗi |
Hook OnUpsert, OnRevoke, OnDelete (bộ nhớ) | Dọn series limiter và presence | Hook chạy trong khóa, phải nhanh |
11. Security
11.1 Ba lớp
| Lớp | Biện pháp |
|---|---|
| Truyền thông | Collector đến Access Hub dùng HTTPS (triển khai) và Bearer hub.token; Redis có mật khẩu và TLS tùy chọn (redis.tls, tls_ca_file) |
| Mã | Token chỉ dạng băm SHA-256; company_id bắt buộc ở mọi thao tác theo phạm vi; ID kiểm tra định dạng; client Redis chặn lệnh nguy hiểm |
| Dữ liệu | Không lưu token rõ; hub.token và mật khẩu Redis đọc từ tệp hoặc biến môi trường, không có trong YAML trong repo |
11.2 Pipeline phân quyền năm bước (áp dụng khi ingest tra token)
- Băm token.
- Tra registry (Redis rồi cache nút).
- Nếu miss: cache âm, limiter, single-flight, tra Access Hub.
- Kiểm tra trạng thái và hạn (
active, chưa hết hạn;prev_tokencòn hạn). - Trả
AgentRecord; Ingest gắncompany_id,server_idtừ bản ghi này vào dữ liệu.
11.3 Chỉ mục neo
| Nguyên tắc L2 | Biện pháp nội bộ |
|---|---|
| P1 danh tính từ registry | Bước 5, bản ghi chỉ ghi qua enroll thành công, Syncer hoặc Admin |
| 9.1 token mờ | Băm SHA-256, tombstone thu hồi |
| 9.3 secrets | hub.token, redis.password từ tệp hoặc biến môi trường |
| NFR-12 cô lập tenant | ByServer(company, server), chỉ mục reg:co:{co} |
Rủi ro còn mở: License đi qua Collector (proxy) nên log phải che; Collector không ghi License ở log (đề xuất rà soát internal/ingest/handlers.go và hubclient).
12. Configuration
12.1 Tunables
Biến môi trường tương ứng: AHC_HUB_URL, AHC_HUB_TOKEN(_FILE), AHC_REDIS_ADDR, AHC_REDIS_USERNAME, AHC_REDIS_PASSWORD(_FILE), AHC_REDIS_PREFIX, AHC_REDIS_DB, AHC_REDIS_TLS, AHC_REDIS_TLS_CA_FILE. Các khóa còn lại chỉ YAML.
| Tham số | Default | Ý nghĩa và tác động | Mục liên quan |
|---|---|---|---|
hub.base_url | rỗng | Địa chỉ Access Hub. Rỗng thì không có tra ngược, enroll trả 503 | 7.1 |
hub.token (hoặc token_file) | rỗng | Bắt buộc khi có base_url | 11 |
hub.timeout | 10 giây | Timeout gọi Access Hub | 9 |
hub.sync_interval | 1 phút | Chu kỳ Syncer | 7.1 |
hub.lookup_per_second | 50 | Trần tra ngược toàn nút | 7.4 |
redis.addr | rỗng | Rỗng thì registry và presence dùng bộ nhớ | 3.1 |
redis.prefix | ah: | Tiền tố khóa | 6.1 |
redis.db | 0 | DB Redis | 6.1 |
redis.cache_ttl | 15 giây | TTL cache nút của registry | 8.3 |
redis.tls, tls.ca_file | tắt | TLS tới Redis | 11.1 |
Hằng số cố định trong mã (không cấu hình được): cache âm 60 giây tối đa 100.000 mục, cache nút tối đa 200.000 mục, trang đồng bộ 500, lô quét presence 500, timeout enroll 20 giây, trần phản hồi hub 8 MiB, timeout mặc định client hub 15 giây.
12.2 Feature flags
Không có. redis.addr rỗng là công tắc chọn hiện thực bộ nhớ thay Redis (chỉ hợp lý cho một tiến trình, dev đơn giản).
13. Telemetry
13.1 Metrics
| Tên | Kiểu | Nhãn | Ngữ nghĩa |
|---|---|---|---|
ahc_registry_lookups_total | counter | result | Kết quả tra token |
ahc_registry_agents | gauge | Số agent trong registry (từ Count) | |
ahc_ingest_enroll_total | counter | result | Kết quả enroll (xem L3 Ingest) |
ahc_agents_down | gauge | Agent đang down (xem L3 Alerting) |
Giá trị nhãn result của ahc_registry_lookups_total: lấy từ mã khi rà soát. docs/10 liệt kê mem, redis, hub, miss nhưng chưa xác minh với mã.
13.2 Log schema
JSON một dòng: time, level, msg, request_id, collector_id. Sự kiện đáng ghi: đồng bộ registry (số bản ghi, trang), lỗi tra Access Hub, enroll thất bại (không kèm token). Không ghi băm token đầy đủ (đề xuất: chỉ 8 ký tự đầu nếu cần).
13.3 Cảnh báo đến runbook
Chưa có luật. Đề xuất: cảnh báo khi ahc_registry_lookups_total miss tăng mạnh (Redis rỗng hoặc tấn công), khi Syncer không chạy thành công quá 10 phút, khi ahc_registry_agents giảm đột ngột. Runbook: docs/09 mục 6.
13.4 Probes
| Probe | Điểm cuối | Ý nghĩa |
|---|---|---|
| Startup | collector -check trong ExecStartPre | Cấu hình hợp lệ |
| Liveness | /healthz | Tiến trình sống |
| Readiness | /readyz ("redis") | Redis trả lời trong 2 giây |
13.5 Trace propagation
X-Request-Id từ Ingest; hub client chuyển X-Forwarded-For với IP client khi enroll. Chưa có trace phân tán.
14. Test Plan
| Loại | Kiểm thử | Vị trí |
|---|---|---|
| Đơn vị Registry | TestLookupFlow, TestRevokeKeepsTombstone, TestPutRevokedStateTombstones, TestPendingAndExpiredTokens, TestTokenRotationKeepsPreviousUntilExpiry, TestStaleVersionIgnored, TestDeleteScopes, TestByServerIsCompanyScoped | internal/registry/registry_test.go |
| Đơn vị Resolver | TestResolveFallsBackToHubAndCaches, TestNegativeCache, TestHubDownIsNotAnInvalidToken, TestGlobalLimiterProtectsHub, TestConcurrentMissesCollapse, TestSyncerPagesAndAdvances, TestFromRecordPreviousToken | internal/registry/resolver_test.go |
| Hợp đồng Redis | TestRedisRegistryContractFake, TestRedisRegistryContractLive (Redis thật tùy chọn), TestRedisRegistryDownFallsBackToNotFound | internal/registry/redis_test.go |
| Hub client | TestEnrollPassesIPAndErrors, TestSeenAndInventory, TestTemporaryErrors, TestBadCollectorTokenRejected | internal/hubclient/hubclient_test.go |
| Presence | TestMemoryTracker, TestRedisTracker, TestRedisTrackerSharedAcrossProcesses, TestRedisTrackerCleansUp, TestEnsureSeedsGraceAndIsNotReported, TestTouchRecoversDownAgent, TestDrainSeenOnlyReportsProgress, TestLastSeenNeverGoesBackwards, TestCountsAndRemove | internal/presence |
| Enroll (Ingest) | TestEnrollProxy, TestEnrollErrors, TestEnrollRateLimitPerIP, TestEnrollWithoutHub | internal/ingest/ingest_test.go |
| Tích hợp | TestSplitProcessesShareStateThroughRedis | internal/app/events_test.go hoặc app_test.go |
| Chưa có | Kiểm thử hợp đồng với API Access Hub thật (HUB-*), đo tải tra ngược, kiểm tra dọn tombstone |
TestRedisRegistryContractLive chỉ chạy khi có Redis thật qua biến môi trường kiểm thử. Bản nháp này không chạy kiểm thử để tránh đụng Redis 6380.
15. Implementation Sequence
15.1 Milestone matrix
| Milestone | Nội dung | Phụ thuộc | Đóng góp nghiệm thu |
|---|---|---|---|
| COL-3 | Registry bộ nhớ và Redis, Resolver, giới hạn, lần thấy cuối | COL-1 | AC-REG-01 đến 06, 08 |
| COL-4 | Enroll proxy, Syncer, thu hồi | COL-3 | AC-REG-05 |
| COL-R | Presence Redis, SADD một chủ, tách vai trò | COL-3 | AC-REG-07 |
| COL-14 (chưa làm) | Redis HA, cache nhiều tầng cho registry | COL-R | Cải thiện AR-005 |
| Renew (chưa làm) | prev_token_hash thật từ Access Hub | HUB-* | FR-REG-04 ở mức đầu cuối |
| HUB-* (chưa xây) | API Access Hub thật thay mockhub | Hợp đồng đầu cuối |
15.2 Dependency flowchart
flowchart LR
C3["COL-3 registry, resolver"] --> C4["COL-4 enroll, syncer"]
C3 --> CR["COL-R presence Redis"]
C4 --> HUBX["HUB-* API thật"]
CR --> C14["COL-14 Redis HA"]
HUBX --> RN["renew thật"]Appendix A. Open Questions
| # | Câu hỏi | Hành vi tạm thời | Owner | Mã theo dõi |
|---|---|---|---|---|
| 1 | Chính sách dọn registry và presence không TTL (tombstone, agent mồ côi) | Chỉ xóa qua đồng bộ và Admin | Chưa chỉ định | OQ-REG-1 |
| 2 | Put Redis có nguyên tử không, hay cần MULTI? | Chưa xác minh | Chưa chỉ định | OQ-REG-2 |
| 3 | Giá trị nhãn result của ahc_registry_lookups_total | Theo mã | Chưa chỉ định | OQ-REG-3 |
| 4 | Hợp đồng GET /agents và /enroll thật của Access Hub (HUB-*) | Mockhub | Chưa chỉ định | OQ-REG-4 |
| 5 | Thu hồi có cần đẩy (push) tức thời thay vì chờ tối đa cache_ttl? | Admin đẩy, cache nút 15 giây | Chưa chỉ định | OQ-REG-5 |
| 6 | Log enroll có bảo đảm không chứa License? | Đề xuất rà soát | Chưa chỉ định | OQ-REG-6 |
Appendix B. ADR nội bộ
| Mã ADR | Quyết định | Trạng thái | Driver |
|---|---|---|---|
| ADR 0007 | Token mờ, đồng bộ registry, tra ngược | Chấp nhận | Thu hồi nhanh |
| ADR 0004 | Access Hub là nguồn sự thật | Chấp nhận | Tách control và data plane |
| ADR 0012 (đề xuất) | Redis dùng chung cho trạng thái nóng | Đề xuất | COL-R |
| ADR nội bộ (đề xuất) | Lỗi Redis đọc thành ErrNotFound để rơi về Access Hub | Đề xuất, là hành vi mã | Giữ ingest sống khi Redis hỏng |
| ADR nội bộ (đề xuất) | Registry không TTL, dọn bằng đồng bộ | Đề xuất, là hành vi mã | Tránh mất danh tính khi mất Access Hub lâu |
Appendix C. Section Profile
| Phân mục | Hồ sơ | Trạng thái điền | Giải trình |
|---|---|---|---|
| 0 đến 4 | Bắt buộc | Đã điền | |
| 5 API contract | Bắt buộc | Điền theo interface nội bộ | Không có API HTTP riêng; hợp đồng hub là HUB-* chưa xây |
| 6 Physical data | Bắt buộc | Đã điền | Bảng khóa Redis |
| 7.1, 7.2, 7.3, 7.4 | Bắt buộc | Đã điền | |
| 8, 9, 10, 11 | Bắt buộc | Đã điền | Mục 10.1 cần xác minh tính nguyên tử |
| 12, 13 | Bắt buộc | Đã điền | Nhãn result cần xác minh |
| 14 | Bắt buộc | Đã điền | Kiểm thử chưa chạy trong bản nháp |
| 15 | Bắt buộc | Đã điền |