Neon 自动扩缩容核心组件 vm-monitor 深度解析:cgroup 内存监控、LFC 文件缓存与 WebSocket 控制面
【免费下载链接】neonNeon: Serverless Postgres. We separated storage and compute to offer autoscaling, code-like database branching, and scale to zero.项目地址: https://gitcode.com/GitHub_Trending/ne/neon
vm-monitor(下文简称 monitor)是 Neon 自动扩缩容(autoscaling)系统的核心组件之一。它与autoscale-scheduler、autoscaler-agent协同工作,承担两项关键职责:在内存接近上限时向 agent 发出即时扩容请求,以及通过管理 Postgres 的本地文件缓存(LFC)与 cgroup 来落地扩容/缩容决策。本文以 libs/vm_monitor/README.md 为骨架,结合仓库源码,深入讲解其架构、通信协议、内存监控与文件缓存管理机制,帮助读者理解 Neon 是如何在虚拟机粒度上实现"按需伸缩"的。
1. 定位:monitor 在自动扩缩容体系中的角色
Neon 的自动扩缩容系统由三部分构成:
- autoscale-scheduler:负责全局调度决策,决定某个 VM 何时扩容、缩容;
- autoscaler-agent:运行在每个 VM 上的代理,负责与 Kubernetes/NeonVM 交互执行资源调整;
- vm-monitor:运行在 VM 内部(由
compute_ctl拉起),承担"内务总管"角色——它感知 Postgres 的真实内存压力,代表 Postgres 向 agent 提出扩容诉求,并在扩容获批后把新资源落实到文件缓存与 cgroup 上。
monitor 的两大职责(README 原文)可以概括为:
- 通知 agent 进行即时扩容:当 Postgres 所在 cgroup 的内存使用量超过阈值时,monitor 通过 WebSocket 向 agent 发送扩容请求,避免 Postgres 被 OOM-kill;
- 执行伸缩决策:管理 Postgres 的本地文件缓存(file cache)以及承载 Postgres 的 cgroup,把扩容/缩容目标具体落地。
CPU 与内存的扩缩容通过NeonVM(Neon 自研的、面向 Kubernetes 的 QEMU 工具)完成;而控制"何时收到内存告警"的手段,则是把 Postgres 启动在neon-postgres这个 cgroup 中,并设置其memory.{max,high}阈值。monitor 的早期原型在独立的 vm-monitor 仓库中开发,该仓库现已不再维护,但其提交历史对排查问题仍有参考价值——当前实现已迁移至本仓库的libs/vm_monitor目录。
2. 架构总览:四个松散耦合的子系统
monitor 由几个松散耦合的子系统组成(README 的 Structure 一节),源码层面分别对应libs/vm_monitor/src/下的模块:
| 子系统 | 源码模块 | 职责 |
|---|---|---|
| Server(HTTP/WS 服务端) | lib.rs | 基于axum的 HTTP 服务器,接受连接并将其升级为 WebSocket;同一时刻只允许一个连接 |
| Filecache(文件缓存管理器) | filecache.rs | 与 Postgres 文件缓存(LFC)通信的结构体;启动时建立连接并保持整个 monitor 生命周期 |
| Cgroup watcher(cgroup 监控器) | cgroup.rs | 轮询neon-postgrescgroup 的内存使用量,并把滚动聚合结果发送给 runner |
| Runner(核心协调器) | runner.rs | 将 filecache 与 cgroup watcher 结合,通过Dispatcher与 agent 通信,按需调用两者的函数完成扩容/缩容 |
其中 Dispatcher(dispatcher.rs)和协议类型(protocol.rs)负责与 agent 之间的数据交换与连接管理。
模块之间的关系:Runner是主入口(runner.rs的模块注释明确写道"This is the 'Monitor' part of the monitor binary and is the main entrypoint for all functionality"),它持有Dispatcher、Option<FileCacheState>与Option<CgroupState>——后两者是可选的原因在于:是否传入pgconnstr(管理 LFC)与 cgroup 名称由启动参数决定。
3. 启动方式与命令行参数
monitor 提供了两种启动途径(Cargo.toml 的[[bin]]声明):
- 独立二进制
vm-monitor:入口为 monitor.rs,用于在完整的自动扩缩容系统中单独测试 monitor(monitor 之前由 vm-builder 启动,现在可以用这个二进制模拟当时的场景);仅支持 Linux(非 Linux 平台直接panic!("the monitor requires cgroups, which are only available on linux"))。 - 由
compute_ctl内嵌启动:作为计算节点进程的一部分运行。
3.1 命令行参数
monitor 的命令行参数定义在 lib.rs 的 Args 结构,使用clap解析:
| 参数 | 说明 | 典型值 |
|---|---|---|
--cgroup/-c | 需要监控memory.high事件的 cgroup 名称,即 Postgres 运行的 cgroup | neon-postgres |
--pgconnstr/-p | 用于管理 Postgres 文件缓存的连接串 | compute 节点的内部连接串 |
--addr/-a | 监听连接请求的地址 | 面向 agent:0.0.0.0:10301;面向 informant:127.0.0.1:10369 |
3.2 compute_ctl 如何拉起 monitor
在 Neon 生产路径中,monitor 由compute_ctl拉起。compute.rs 的 start_vm_monitor 展示了完整的启动逻辑:
- 当环境变量
AUTOSCALING存在时才启动 monitor,否则不启动; - 若计算规格中
disable_lfc_resizing为真,则不向 monitor 传pgconnstr(即不启用 LFC 自动调整); - 传入参数为:
cgroup(来自params.cgroup)、pgconnstr(来自params.filecache_connstr)、addr(来自params.vm_monitor_addr); - 同时创建
CancellationToken,用于在 Postgres 退出后统一取消 monitor 的所有线程(见 compute.rs 的清理逻辑)。
可见 monitor 的生命周期与 Postgres 绑定:Postgres 退出时 monitor 被取消,释放文件 watcher。
4. 通信层:单连接 WebSocket 与协议版本协商
4.1 单连接模型
Server 是一个简单的axum服务器,路由为GET /monitor(lib.rs),请求会被升级为 WebSocket 连接。同一时刻只允许一个连接:当新连接到达时,ws_handler会先向broadcast::Sender发送信号关闭旧连接(源码注释形象地称之为 "the cycle of death and rebirth"),再启动新的 monitor 实例(lib.rs)。
start_monitor使用 4 秒超时等待Runner::new完成初始化;初始化失败或超时都会记录日志并返回,允许后续新连接重试(lib.rs)。所有线程通过spawn_with_cancel包裹,收到CancellationToken取消信号(例如 Postgres 退出或新连接到来)后优雅退出(lib.rs)。
4.2 协议版本协商
Dispatcher::new(dispatcher.rs)在建连时执行协议握手:
- 等待 agent 发送其支持的协议版本区间(JSON,形如
{min, max}); - monitor 计算双方最高共同版本(
highest_shared_version,取两个区间交集的 max 值); - 若区间无交集,向 agent 返回
ProtocolResponse::Error并终止。
当前协议的PROTOCOL_MIN_VERSION与PROTOCOL_MAX_VERSION均为V1_0(protocol.rs),即当前只有 v1.0 一个版本。
4.3 消息类型
协议消息通过serde序列化为 JSON 文本,tag 字段为type。出站消息(monitor → agent,protocol.rs OutboundMsgKind):
| 消息 | 含义 |
|---|---|
UpscaleRequest {} | monitor 发现内存压力过大,紧急请求扩容 |
UpscaleConfirmation {} | 处理完 agent 下发的扩容通知后回执 |
DownscaleResult { ok, status } | 缩容尝试的结果;ok=false表示因内存仍在高位等原因暂不可缩容,status说明原因 |
InvalidMessage { error }/InternalError { error } | 协议或内部处理错误 |
HealthCheck {} | 双向心跳(由 agent 发起) |
入站消息(agent → monitor,protocol.rs InboundMsgKind):
| 消息 | 含义 |
|---|---|
UpscaleNotification { granted } | agent 告知已获批新资源(Resources{cpu, mem}),monitor 需落地扩容并回复UpscaleConfirmation |
DownscaleRequest { target } | agent 请求降低资源占用,monitor 处理后回复DownscaleResult |
InvalidMessage/InternalError | 对端错误通告 |
HealthCheck {} | 心跳 |
Resources(protocol.rs)包含 vCPU 数(cpu: f64)与内存字节数(mem: u64),序列化时使用Allocation这一字段名,以匹配 agent 侧的api.Allocation类型。值得注意的细节是Runner.counter(消息 ID 计数器)始终保持奇数,用于避免与 agent 生成的偶数 ID 冲突(runner.rs)。
5. 内存监控:CgroupWatcher 的工作原理
5.1 cgroup v2 与采样配置
CgroupWatcher使用cgroups-rscrate 管理 cgroup(仅 Linux,见 Cargo.toml 的target.'cfg(target_os = "linux")'.dependencies)。构造时强制要求系统处于cgroups v2(unified)模式,否则直接报错(cgroup.rs)。
采样配置(cgroup.rs Config)及默认值:
| 配置项 | 默认值 | 含义 |
|---|---|---|
memory_poll_interval | 100ms | 拉取内存统计的间隔 |
memory_history_len | 5 | 用于构建滚动聚合的样本数,即使用约 500ms 的历史做决策 |
memory_history_log_interval | 20 | 周期性日志的最近样本数(约每 2s 输出一次,避免刷屏) |
memory_history_log_noskip_interval | 15s | 数据无明显变化时,最多跳过日志的时长 |
5.2 监控主循环
watch(cgroup.rs)是 CgroupWatcher 的入口,以watch::Sender<(Instant, MemoryHistory)>向 runner 推送聚合结果,具体流程:
- 以 100ms 为周期读取 cgroup 内存子系统统计;
- 内存样本取自
memory_stat的active_anon + inactive_anon,即"不可回收内存"(non-reclaimable)的近似(cgroup.rs); - 用环形缓冲区(
ring_buf_recent_values_iter)维护最近若干样本,计算滚动平均值avg_non_reclaimable,连同样本数与时间跨度构成MemoryHistory(cgroup.rs); - 周期性输出日志;若数据与上次记录"足够接近"(
status_is_close_or_similar:差值小于较小值的 1/8 且不超过 128MiB,cgroup.rs),则跳过日志以降低噪音。
环形缓冲区与相似度判定逻辑分别有单元测试覆盖(ring_buf_iter、check_similarity_behaviour,见 cgroup.rs),可作为理解边界行为的参考。
6. 文件缓存管理:LFC 的大小计算与动态调整
6.1 Postgres 侧的 LFC 与 GUC
Neon 的 Postgres 本地文件缓存(Local File Cache, LFC)由pgxn/neon/file_cache.c实现,涉及两个 GUC(file_cache.c):
neon.max_file_cache_size:缓存上限,PGC_POSTMASTER级别、MB 单位、默认 0(禁用);neon.file_cache_size_limit:当前软限制,PGC_SIGHUP级别、MB 单位,可在运行期通过pg_reload_conf()热更新;lfc_check_limit_hook保证它不能大于neon.max_file_cache_size(file_cache.c)。
6.2 monitor 侧的配置
FileCacheConfig(filecache.rs)默认值如下:
| 配置项 | 默认值 | 含义 |
|---|---|---|
resource_multiplier | 0.75 | 缓存目标大小 = 总资源 × 75% |
min_remaining_after_cache | 256 MiB | 扣掉缓存后系统必须保留的最小内存;低于共享内存场景是因为"超额分配是安全的"——内存不足时内核会从 page cache 驱逐,而不是杀掉进程 |
spread_factor | 0.1 | 控制缓存从 0 增长到目标大小的速率;约等于"每给缓存增加 1 字节,系统预留 N 字节" |
validate(filecache.rs)对配置做一致性校验:resource_multiplier必须在 (0,1) 之间;且resource_multiplier × (spread_factor + 1) < 1,否则两条增长曲线无法在合理区间相交,配置不合法。
6.3 缓存大小计算公式
calculate_cache_size(filecache.rs)根据总内存计算期望缓存大小,取两条曲线的较小值,并向下取整到 MiB:
size = min( available / (1 + spread_factor), total × resource_multiplier ) 其中 available = max(total − min_remaining_after_cache, 0)直观理解:当内存较少时,spread_factor曲线占主导(为系统其余部分保留增长空间);内存充足时,resource_multiplier曲线封顶(不超过总内存的 75%)。
6.4 连接、查询与设置
- 连接:
FileCacheState::new用tokio_postgres建立连接,连接对象以spawn_with_cancel独立运行(filecache.rs); - 读取当前大小:
get_file_cache_size查询SELECT pg_size_bytes(current_setting('neon.file_cache_size_limit')),得到字节数(filecache.rs); - 设置大小:
set_file_cache_size先读取neon.max_file_cache_size封顶,再以整数 MB 形式执行ALTER SYSTEM SET neon.file_cache_size_limit = <N>;,随后调用SELECT pg_reload_conf();使其生效(filecache.rs); - 失败重试:
query_with_retry在查询失败时重建数据库连接并重试一次(filecache.rs)。
monitor 在启动时会显式设置一次初始缓存大小(即使当前值相同),目的是确认自己具备修改权限(runner.rs)。
7. 伸缩决策:Runner 主循环
7.1 阈值计算
monitor 通过"提前预警"避免 Postgres 被 OOM-kill。cgroup 阈值由Config::cgroup_threshold计算(runner.rs):
threshold = total_mem × (1 − cgroup_min_overhead_fraction)cgroup_min_overhead_fraction默认 0.15,即保证阈值之上至少保留总内存的 15%,从而保证"当 cgroup 使用量超过总内存的 85% 时必定触发扩容请求"。
Runner 的整体配置(runner.rs Config)默认值:
| 配置项 | 默认值 | 含义 |
|---|---|---|
sys_buffer_bytes | 100 MiB | 内核占用的估计内存(/proc/meminfo的 MemTotal 与实际物理内存之差);计算可用内存时先从总内存中扣除 |
cgroup_min_overhead_fraction | 0.15 | 阈值之上必须保留的最小内存比例 |
cgroup_downscale_threshold_buffer_bytes | 100 MiB | 缩容时新阈值必须高于"当前使用量 + 该缓冲",否则拒绝缩容 |
7.2 扩容路径
扩容触发条件与动作(runner.rs run):
- runner 监听 cgroup watcher 的
watch::Receiver,每次收到新的MemoryHistory时检查avg_non_reclaimable是否 ≥ 当前阈值; - 若超阈值,且距上次扩容请求已超过 1 秒(避免刷屏,对应 issue #5865),则向 agent 发送
UpscaleRequest; - 当 agent 回传
UpscaleNotification{granted}后,handle_upscale(runner.rs)执行落地动作:- 按
usable_system_memory = new_mem − sys_buffer_bytes计算新的期望缓存大小并set_file_cache_size; - 用同样的内存口径重算 cgroup 阈值并更新
CgroupState.threshold; - 完成后回复
UpscaleConfirmation。
- 按
注意handle_upscale中使用了 agent 告知的资源量(而非系统实际自报内存),因此sys_buffer_bytes的校准尤为重要——这也是该字段 TODO 注释强调的"当前仍需信任 agent 的扩容资源量"的原因(runner.rs)。
7.3 缩容路径
try_downscale(runner.rs)是缩容的核心,逻辑较谨慎:
- 若 monitor 未管理任何 cgroup/LFC,直接返回成功;
- 从
watch::Receiver读取最近一次内存历史,做两道安全闸门:- 若距上次统计已超过 5 秒(
last_time.elapsed() > 5s),bail 报错——既覆盖"启动后一直没收到指标"的情形,也覆盖"指标断流"的情形; - 若累计样本数 ≤ 1,拒绝缩容(返回
ok=false),等待积累足够数据;
- 若距上次统计已超过 5 秒(
- 计算新阈值
cgroup_threshold(usable_system_memory);若新阈值 < 当前使用量 + 100 MiB 缓冲,拒绝缩容并在status中给出数值化原因; - 通过校验后,先缩 LFC(
set_file_cache_size),再更新 cgroup 阈值,返回DownscaleResult{ok: true, status}。
可见缩容被设计成"宁可多等、不可误伤":内存仍在高位或数据不足时一律暂缓。
7.4 消息分派
process_message(runner.rs)按InboundMsgKind分发:UpscaleNotification→handle_upscale+ 回执;DownscaleRequest→try_downscale+DownscaleResult;HealthCheck→ 原样回HealthCheck;收到 agent 的InvalidMessage/InternalError通告时仅记录warn日志。
8. 关键参数速查与调优提示
结合上述源码,将 monitor 相关的全部可调参数汇总如下:
启动参数(vm-monitor二进制 / compute_ctl)
--cgroup:要监控的 cgroup 名(如neon-postgres),Postgres 必须运行于其中;--pgconnstr:Postgres 连接串,用于管理 LFC;由disable_lfc_resizing控制是否传入;--addr:监听地址,agent 侧0.0.0.0:10301,informant 侧127.0.0.1:10369。
代码级配置(当前为硬编码默认值)
- Runner:
sys_buffer_bytes=100MiB、cgroup_min_overhead_fraction=0.15、cgroup_downscale_threshold_buffer_bytes=100MiB; - CgroupWatcher:
memory_poll_interval=100ms、memory_history_len=5、日志间隔 20 次/约 2s、跳日志上限 15s; - FileCache:
resource_multiplier=0.75、min_remaining_after_cache=256MiB、spread_factor=0.1; - 阈值规则:扩容阈值 = 总内存 × 0.85;1 秒内不重复请求扩容;缩容需满足"新阈值 ≥ 当前使用量 + 100MiB"且指标新鲜(≤5s)、样本充足(>1)。
Postgres 侧 GUC
neon.max_file_cache_size(MB,PGC_POSTMASTER,默认 0 禁用);neon.file_cache_size_limit(MB,PGC_SIGHUP,运行时经ALTER SYSTEM+pg_reload_conf()调整,且不得大于 max)。
调优时需特别注意FileCacheConfig::validate的约束:resource_multiplier与spread_factor必须满足resource_multiplier × (spread_factor + 1) < 1,否则配置非法(例如resource_multiplier=0.75搭配spread_factor=1会因两条增长曲线永不达到 75% 而失败)。
9. 测试与验证
monitor 的正确性依赖两处测试支撑:
- 单元测试:
cgroup.rs内置了环形缓冲区迭代(含环绕边界:values(0,4)==[7,8,9,0]等)与内存相似度判定(1/8 与 128MiB 双重边界)的测试(cgroup.rs); - 端到端集成:独立二进制
vm-monitor(monitor.rs)的注释说明,它用于"将 monitor 作为整个自动扩缩容系统的一部分进行测试",模拟此前由 vm-builder 启动的部署形态,可配合 agent 验证完整的握手—扩容—缩容闭环。
运行测试只需在仓库根目录执行标准的 Cargo 测试命令(monitor 依赖 Linux 的 cgroups,测试/运行均需在 Linux 上进行):
cargo test -p vm_monitor10. 总结
vm-monitor是 Neon 自动扩缩容体系里"离 Postgres 最近"的一环:它把"Postgres 的真实内存压力"翻译成对 agent 的扩容诉求,把"调度器批复的资源"翻译成 LFC 大小与 cgroup 阈值的具体调整。本文从 README 的四个子系统出发,逐一对照源码揭示了其单连接 WebSocket 模型、v1.0 协议的消息语义、cgroup v2 内存采样与滚动聚合、LFC 大小计算公式与ALTER SYSTEM热更新路径,以及 Runner 主循环中"扩容果断、缩容谨慎"的完整决策逻辑。对于想要深入理解 Neon 扩缩容机制,或希望复现/扩展其内存监控与缓存管理能力的开发者,libs/vm_monitor 下的源码是最直接、最完整的参考实现。
【免费下载链接】neonNeon: Serverless Postgres. We separated storage and compute to offer autoscaling, code-like database branching, and scale to zero.项目地址: https://gitcode.com/GitHub_Trending/ne/neon
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考