Skip to content
Merged
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ sudo systemctl status nodelite-agent.service
sudo journalctl -u nodelite-agent.service -f
```

反向代理、负载均衡器和告警系统接入前,请确认 [`/healthz` 与 `/readyz` 的探针语义](docs/readiness.md),避免把诊断降级误判为实例不可接流量。

升级建议由管理员手动触发:先确认 release notes 和协议兼容,再升级 Server 或 Agent。

## 常见排障
Expand Down
8 changes: 8 additions & 0 deletions config/server.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ history_db_path = "./data/history.sqlite3"
history_query_concurrency = 4
# 每个历史只读连接的 SQLite 私有 page cache(KiB),允许 64-1024,不等同于 Linux filesystem page cache。
history_read_cache_kib = 512
# 历史 writer 单次 SQLite 事务最多写入的记录数,允许 1-4096。
history_writer_batch_max = 128
# 历史 writer 最大攒批时间(毫秒),必须至少为 10,更小会导致高频唤醒。
history_writer_flush_interval_ms = 100
# 运行态快照文件,用于 Server 重启后秒级恢复最近视图。
snapshot_path = "./data/snapshot.json"
# 多少秒未收到心跳即视为节点离线。
Expand Down Expand Up @@ -73,6 +77,10 @@ enabled = true
db_path = "./data/audit.sqlite3"
# 仅保留最近 N 天的安全事件。
retention_days = 90
# 审计 writer 单次 SQLite 事务最多写入的事件数,允许 1-4096。
writer_batch_max = 128
# 审计 writer 最大攒批时间(毫秒),必须至少为 10。
writer_flush_interval_ms = 100
# 是否记录成功认证/握手事件。
log_successful_auth = true
# 是否记录失败认证/TOTP 失败事件。
Expand Down
34 changes: 34 additions & 0 deletions docs/readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 健康检查与就绪探针

NodeLite Server 提供两个无需认证的探针端点:

- `/healthz`:进程存活检查。只要 HTTP 服务还能响应就返回 `200 OK`。
- `/readyz`:流量就绪检查和结构化运行诊断。HTTP 状态码与 JSON 的 `ready` 字段表示能否承载流量,`status` 和 `problems` 表示更广泛的运行健康度。

## `/readyz` 语义

`/readyz` 将暴露内容分为三类:

| 类别 | JSON 位置或条件 | 当前判定语义 |
| --- | --- | --- |
| 硬就绪检查 | `checks.history_available`、`checks.registry_reload_healthy` | 任一失败时加入对应的 `problems`,返回 `status: "degraded"`、`ready: false` 和 `503 Service Unavailable` |
| 降级诊断条件 | `signals.audit_enabled && !signals.audit_available`;三个写入计数器中的任一个大于 `0` | 分别加入 `audit_unavailable`、`history_dropped_writes`、`audit_dropped_writes` 或 `audit_write_failures`,并返回 `status: "degraded"`;这些条件本身不参与 HTTP 状态码和 `ready` 的判定 |
| 原始观测字段 | `signals` 中 history/audit 队列深度与容量、Agent/Browser WebSocket 当前连接数、总容量及单 IP 限制 | 只报告当前值;服务端目前不为这些字段设置阈值,因此它们本身不会加入 `problems`,也不会改变 `status`、`ready` 或 HTTP 状态码 |

审计日志是安全诊断能力,但不是 Server 接收 Agent、查询节点状态所需的硬依赖。因此,仅审计写入器不可用时,响应组合是:

```json
{
"status": "degraded",
"ready": true,
"problems": ["audit_unavailable"]
}
```

对应的 HTTP 状态码仍为 `200 OK`。如果历史存储或注册表重载检查失败,则响应为 `ready: false`、`status: "degraded"`,HTTP 状态码为 `503 Service Unavailable`。

## 探针与告警配置

- Kubernetes、systemd watchdog 或负载均衡器的就绪判断应使用 `/readyz` 的 HTTP 状态码;需要解析 JSON 时,应读取 `ready`,不要把 `status == "ok"` 当作接流量条件。
- 告警系统应另外监控 `status`、`problems` 和 `signals`。`ready: true` 且 `status: "degraded"` 表示服务仍可接流量,但存在需要运维处理的诊断异常。队列和 WebSocket 容量字段仅提供原始数据,需要由外部监控按部署规模设置阈值。
- 不要仅用 `/healthz` 判断是否应把实例加入流量池;它只验证进程仍能响应。
11 changes: 10 additions & 1 deletion docs/token-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,16 @@ NodeLite 使用 Argon2id 验证 Agent Token。每次冷缓存验证约需要 19
- `nodelite_token_verify_limit`:当前配置的最大并发数;
- `nodelite_token_verify_active`:正在执行 Argon2 的任务数;
- `nodelite_token_verify_waiting`:正在等待并发许可的请求数;
- `nodelite_token_verify_wait_seconds_total`:所有请求累计等待许可的秒数。
- `nodelite_token_verify_wait_seconds_total`:所有请求累计等待许可的秒数;
- `nodelite_token_cache_hits_total`:由未过期缓存直接返回的验证次数,包括获取并发许可后的二次检查命中;
- `nodelite_token_cache_misses_total`:实际执行 Argon2 验证的次数;
- `nodelite_token_cache_evictions_total`:缓存达到容量上限后发生的 LRU 驱逐次数。

缓存命中包含成功和失败的验证结果。`evictions_total` 只统计容量驱逐,不统计 TTL 过期
清理或 Token 轮换触发的显式清空。诊断命中率时可比较
`rate(nodelite_token_cache_hits_total[5m])` 与 hits、misses 两者速率之和;如果 miss 较高但
eviction 保持为 0,应优先检查并发冷启动、注册表 revision 变化或 Token 轮换,而不是直接
增大缓存。

如果 `waiting` 长时间大于 0,且主机仍有足够内存,可以逐步提高并发。小内存主机应优先
保持 2 或 4,并结合进程 RSS、cgroup `MemoryCurrent` 和 OOM 日志判断,而不是只看单次
Expand Down
28 changes: 27 additions & 1 deletion nodelite-proto/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ use ipnet::IpNet;
use serde::{Deserialize, Serialize};

use self::defaults::{
default_audit_writer_batch_max, default_audit_writer_flush_interval_ms,
default_connect_timeout_secs, default_hello_timeout_secs, default_history_query_concurrency,
default_history_read_cache_kib, default_insecure_transport_warn_interval_secs,
default_history_read_cache_kib, default_history_writer_batch_max,
default_history_writer_flush_interval_ms, default_insecure_transport_warn_interval_secs,
default_max_incoming_message_bytes, default_max_outstanding_pings, default_max_sanitized_disks,
default_max_sanitized_string_bytes, default_metric_anomaly_session_limit,
default_metrics_export_node_disk_metrics, default_metrics_export_node_resource_metrics,
Expand Down Expand Up @@ -60,6 +62,10 @@ pub const DEFAULT_HISTORY_WRITE_INTERVAL_SECS: u64 = 30;
pub const DEFAULT_HISTORY_QUERY_CONCURRENCY: usize = 4;
/// 历史只读连接的 SQLite 私有 page cache 默认大小(KiB)。
pub const DEFAULT_HISTORY_READ_CACHE_KIB: u64 = 512;
/// 历史写入器单次事务默认最多写入的记录数。
pub const DEFAULT_HISTORY_WRITER_BATCH_MAX: usize = 128;
/// 历史写入器默认最大攒批时间(毫秒)。
pub const DEFAULT_HISTORY_WRITER_FLUSH_INTERVAL_MS: u64 = 100;
/// 历史查询至少保留一个并发槽位。
pub const MIN_HISTORY_QUERY_CONCURRENCY: usize = 1;
/// 防止大量并发 SQLite page cache 抬高匿名内存峰值。
Expand Down Expand Up @@ -106,6 +112,14 @@ pub const MIN_TOKEN_VERIFY_MAX_PARALLELISM: usize = 1;
pub const MAX_TOKEN_VERIFY_MAX_PARALLELISM: usize = 8;
/// 审计日志默认保留天数。
pub const DEFAULT_AUDIT_RETENTION_DAYS: u64 = 90;
/// 审计写入器单次事务默认最多写入的记录数。
pub const DEFAULT_AUDIT_WRITER_BATCH_MAX: usize = 128;
/// 审计写入器默认最大攒批时间(毫秒)。
pub const DEFAULT_AUDIT_WRITER_FLUSH_INTERVAL_MS: u64 = 100;
/// 历史与审计写入器单次事务允许的最大记录数。
pub const MAX_WRITER_BATCH_SIZE: usize = 4096;
/// 写入器 flush 间隔下限,避免极短定时器形成忙循环。
pub const MIN_WRITER_FLUSH_INTERVAL_MS: u64 = 10;
/// GeoIP 数据库默认更新间隔(天)。
pub const DEFAULT_GEOIP_UPDATE_INTERVAL_DAYS: u64 = 30;
/// Agent 连接超时(秒)。
Expand Down Expand Up @@ -180,6 +194,12 @@ pub struct ServerConfig {
#[serde(default = "default_history_read_cache_kib")]
/// 每个历史只读连接的 SQLite 私有 page cache 大小(KiB)。
pub history_read_cache_kib: u64,
#[serde(default = "default_history_writer_batch_max")]
/// 历史写入器单次事务最多写入的记录数。
pub history_writer_batch_max: usize,
#[serde(default = "default_history_writer_flush_interval_ms")]
/// 历史写入器最大攒批时间(毫秒)。
pub history_writer_flush_interval_ms: u64,
/// 最新快照持久化文件路径。
pub snapshot_path: PathBuf,
/// 超过该秒数未收到上报后,节点视为离线。
Expand Down Expand Up @@ -281,6 +301,12 @@ pub struct AuditConfig {
pub db_path: PathBuf,
/// 审计记录保留天数。
pub retention_days: u64,
#[serde(default = "default_audit_writer_batch_max")]
/// 审计写入器单次事务最多写入的记录数。
pub writer_batch_max: usize,
#[serde(default = "default_audit_writer_flush_interval_ms")]
/// 审计写入器最大攒批时间(毫秒)。
pub writer_flush_interval_ms: u64,
/// 是否记录成功认证事件。
pub log_successful_auth: bool,
/// 是否记录失败认证事件。
Expand Down
23 changes: 21 additions & 2 deletions nodelite-proto/src/config/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ use super::{
DEFAULT_ALERT_INSPECTION_LOCAL_TIME, DEFAULT_ALERT_INSPECTION_LOOKBACK_HOURS,
DEFAULT_ALERT_INSPECTION_MEMORY_WARN_PERCENT, DEFAULT_ALERT_INSPECTION_OFFLINE_GRACE_MINUTES,
DEFAULT_ALERT_RULE_COOLDOWN_MINUTES, DEFAULT_ALERT_RULE_WINDOW_MINUTES,
DEFAULT_AUDIT_RETENTION_DAYS, DEFAULT_CONNECT_TIMEOUT_SECS, DEFAULT_GEOIP_UPDATE_INTERVAL_DAYS,
DEFAULT_HELLO_TIMEOUT_SECS, DEFAULT_HISTORY_QUERY_CONCURRENCY, DEFAULT_HISTORY_READ_CACHE_KIB,
DEFAULT_AUDIT_RETENTION_DAYS, DEFAULT_AUDIT_WRITER_BATCH_MAX,
DEFAULT_AUDIT_WRITER_FLUSH_INTERVAL_MS, DEFAULT_CONNECT_TIMEOUT_SECS,
DEFAULT_GEOIP_UPDATE_INTERVAL_DAYS, DEFAULT_HELLO_TIMEOUT_SECS,
DEFAULT_HISTORY_QUERY_CONCURRENCY, DEFAULT_HISTORY_READ_CACHE_KIB,
DEFAULT_HISTORY_WRITER_BATCH_MAX, DEFAULT_HISTORY_WRITER_FLUSH_INTERVAL_MS,
DEFAULT_INSECURE_TRANSPORT_WARN_INTERVAL_SECS, DEFAULT_MAX_INCOMING_MESSAGE_BYTES,
DEFAULT_MAX_MESSAGE_BYTES, DEFAULT_MAX_OUTSTANDING_PINGS, DEFAULT_MAX_SANITIZED_DISKS,
DEFAULT_MAX_SANITIZED_STRING_BYTES, DEFAULT_METRIC_ANOMALY_SESSION_LIMIT,
Expand All @@ -29,6 +32,14 @@ pub(super) fn default_history_read_cache_kib() -> u64 {
DEFAULT_HISTORY_READ_CACHE_KIB
}

pub(super) fn default_history_writer_batch_max() -> usize {
DEFAULT_HISTORY_WRITER_BATCH_MAX
}

pub(super) fn default_history_writer_flush_interval_ms() -> u64 {
DEFAULT_HISTORY_WRITER_FLUSH_INTERVAL_MS
}

pub(super) fn default_node_registry_path() -> PathBuf {
PathBuf::from("./config/server.json")
}
Expand Down Expand Up @@ -137,6 +148,14 @@ pub(super) fn default_audit_retention_days() -> u64 {
DEFAULT_AUDIT_RETENTION_DAYS
}

pub(super) fn default_audit_writer_batch_max() -> usize {
DEFAULT_AUDIT_WRITER_BATCH_MAX
}

pub(super) fn default_audit_writer_flush_interval_ms() -> u64 {
DEFAULT_AUDIT_WRITER_FLUSH_INTERVAL_MS
}

pub(super) fn default_audit_log_successful_auth() -> bool {
true
}
Expand Down
46 changes: 41 additions & 5 deletions nodelite-proto/src/config/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ mod alerts;
use super::defaults::{
default_audit_db_path, default_audit_enabled, default_audit_log_failed_auth,
default_audit_log_rate_limit, default_audit_log_successful_auth,
default_audit_log_token_events, default_audit_retention_days, default_connect_timeout_secs,
default_audit_log_token_events, default_audit_retention_days, default_audit_writer_batch_max,
default_audit_writer_flush_interval_ms, default_connect_timeout_secs,
default_geoip_auto_update, default_geoip_database_path, default_geoip_edition,
default_geoip_enabled, default_geoip_provider, default_geoip_update_interval_days,
default_hello_timeout_secs, default_history_db_path, default_history_query_concurrency,
default_history_read_cache_kib, default_ignored_filesystems,
default_history_read_cache_kib, default_history_writer_batch_max,
default_history_writer_flush_interval_ms, default_ignored_filesystems,
default_insecure_transport_warn_interval_secs, default_max_incoming_message_bytes,
default_max_message_bytes, default_max_outstanding_pings, default_max_sanitized_disks,
default_max_sanitized_string_bytes, default_metric_anomaly_session_limit,
Expand All @@ -31,9 +33,9 @@ use super::helpers::{
use super::{
AgentConfig, AlertingConfig, AuditConfig, ConfigError, GeoIpConfig, GeoIpEdition,
GeoIpProvider, MAX_HISTORY_QUERY_CONCURRENCY, MAX_HISTORY_READ_CACHE_KIB,
MAX_NODE_IDENTITY_TEXT_BYTES, MAX_TOKEN_VERIFY_MAX_PARALLELISM, MIN_HISTORY_QUERY_CONCURRENCY,
MIN_HISTORY_READ_CACHE_KIB, MIN_TOKEN_VERIFY_MAX_PARALLELISM, MetricsConfig,
ReadonlyAuthConfig, ServerConfig, WsConfig,
MAX_NODE_IDENTITY_TEXT_BYTES, MAX_TOKEN_VERIFY_MAX_PARALLELISM, MAX_WRITER_BATCH_SIZE,
MIN_HISTORY_QUERY_CONCURRENCY, MIN_HISTORY_READ_CACHE_KIB, MIN_TOKEN_VERIFY_MAX_PARALLELISM,
MIN_WRITER_FLUSH_INTERVAL_MS, MetricsConfig, ReadonlyAuthConfig, ServerConfig, WsConfig,
};
use crate::validation::{
ValidationError, normalize_string_list, validate_bounded_text, validate_identifier,
Expand Down Expand Up @@ -88,6 +90,10 @@ struct RawServerSection {
history_query_concurrency: usize,
#[serde(default = "default_history_read_cache_kib")]
history_read_cache_kib: u64,
#[serde(default = "default_history_writer_batch_max")]
history_writer_batch_max: usize,
#[serde(default = "default_history_writer_flush_interval_ms")]
history_writer_flush_interval_ms: u64,
#[serde(default = "default_snapshot_path")]
snapshot_path: PathBuf,
#[serde(default = "default_stale_after_secs")]
Expand Down Expand Up @@ -154,6 +160,10 @@ struct RawAuditSection {
db_path: PathBuf,
#[serde(default = "default_audit_retention_days")]
retention_days: u64,
#[serde(default = "default_audit_writer_batch_max")]
writer_batch_max: usize,
#[serde(default = "default_audit_writer_flush_interval_ms")]
writer_flush_interval_ms: u64,
#[serde(default = "default_audit_log_successful_auth")]
log_successful_auth: bool,
#[serde(default = "default_audit_log_failed_auth")]
Expand Down Expand Up @@ -226,6 +236,8 @@ impl Default for RawAuditSection {
enabled: default_audit_enabled(),
db_path: default_audit_db_path(),
retention_days: default_audit_retention_days(),
writer_batch_max: default_audit_writer_batch_max(),
writer_flush_interval_ms: default_audit_writer_flush_interval_ms(),
log_successful_auth: default_audit_log_successful_auth(),
log_failed_auth: default_audit_log_failed_auth(),
log_token_events: default_audit_log_token_events(),
Expand Down Expand Up @@ -341,6 +353,8 @@ impl RawServerConfigFile {
history_db_path: self.server.history_db_path,
history_query_concurrency: self.server.history_query_concurrency,
history_read_cache_kib: self.server.history_read_cache_kib,
history_writer_batch_max: self.server.history_writer_batch_max,
history_writer_flush_interval_ms: self.server.history_writer_flush_interval_ms,
snapshot_path: self.server.snapshot_path,
stale_after_secs: self.server.stale_after_secs,
ping_interval_secs: self.server.ping_interval_secs,
Expand Down Expand Up @@ -434,11 +448,23 @@ impl RawServerConfigFile {
"audit.retention_days must be greater than 0",
));
}
if !(1..=MAX_WRITER_BATCH_SIZE).contains(&self.audit.writer_batch_max) {
return Err(ConfigError::new(format!(
"audit.writer_batch_max must be between 1 and {MAX_WRITER_BATCH_SIZE}"
)));
}
if self.audit.writer_flush_interval_ms < MIN_WRITER_FLUSH_INTERVAL_MS {
return Err(ConfigError::new(format!(
"audit.writer_flush_interval_ms must be at least {MIN_WRITER_FLUSH_INTERVAL_MS} ms"
)));
}

Ok(AuditConfig {
enabled: self.audit.enabled,
db_path: self.audit.db_path.clone(),
retention_days: self.audit.retention_days,
writer_batch_max: self.audit.writer_batch_max,
writer_flush_interval_ms: self.audit.writer_flush_interval_ms,
log_successful_auth: self.audit.log_successful_auth,
log_failed_auth: self.audit.log_failed_auth,
log_token_events: self.audit.log_token_events,
Expand Down Expand Up @@ -596,6 +622,16 @@ impl RawServerConfigFile {
"server.history_read_cache_kib must be between {MIN_HISTORY_READ_CACHE_KIB} and {MAX_HISTORY_READ_CACHE_KIB} KiB",
)));
}
if !(1..=MAX_WRITER_BATCH_SIZE).contains(&self.server.history_writer_batch_max) {
return Err(ConfigError::new(format!(
"server.history_writer_batch_max must be between 1 and {MAX_WRITER_BATCH_SIZE}"
)));
}
if self.server.history_writer_flush_interval_ms < MIN_WRITER_FLUSH_INTERVAL_MS {
return Err(ConfigError::new(format!(
"server.history_writer_flush_interval_ms must be at least {MIN_WRITER_FLUSH_INTERVAL_MS} ms"
)));
}
Ok(())
}

Expand Down
Loading
Loading