第一章:Go GC STW时间可控性的本质争议
Go 语言的垃圾回收器(GC)以“低延迟”为设计目标,其中 Stop-The-World(STW)阶段的时长是否真正可控,长期存在根本性技术分歧。争议核心不在于 GC 是否缩短了 STW,而在于其可控性是否依赖于运行时不可控的外部变量——如堆对象图拓扑、内存分配突发模式、以及 Goroutine 调度瞬时状态。
STW 的双重角色
STW 并非单一暂停点,而是分为两个独立阶段:
- Mark Start STW:用于原子切换 GC 状态、扫描全局根对象(如全局变量、栈指针寄存器),耗时通常在百纳秒级;
- Mark Termination STW:完成标记收尾、清理元数据并准备清扫,其耗时与当前存活对象数量强相关,可能达毫秒级(尤其在大堆、高存活率场景下)。
可控性幻觉的来源
Go 运行时通过 GOGC 参数调控触发频率,但无法承诺单次 STW 上限。例如,即使设置 GOGC=100,当突发分配 2GB 临时对象并在 STW 前未被释放时,Mark Termination 仍可能因需遍历大量新对象而延长:
# 观察实时 GC 暂停:使用 runtime/trace 分析 STW 细节
go run -gcflags="-m" main.go 2>&1 | grep -i "gc"
GODEBUG=gctrace=1 ./myapp # 输出含 "gc X @Ys X%: A+B+C+D ms",其中 D 即 Mark Termination STW 时间
关键不可控因素
| 因素 | 影响机制 | 是否可编程干预 |
|---|---|---|
| Goroutine 栈扫描延迟 | STW 中需安全挂起所有 G 并扫描其栈 | 否(受调度器抢占点限制) |
| 内存页归还延迟 | MADV_DONTNEED 系统调用异步,不阻塞 STW |
否 |
| 大对象标记开销 | >32KB 对象绕过 TCMalloc,直接进 mheap,标记粒度粗 | 否 |
真正的可控性必须建立在可证伪的时延边界上,而 Go GC 当前仅提供统计意义上的 P99 STW sync.Pool)、避免逃逸、预分配等方式主动收缩 GC 攻击面,而非依赖运行时自动保障。
第二章:C与Go内存管理模型的底层差异剖析
2.1 C手动管理的确定性边界:malloc/free调用链与页表映射实践
C语言内存管理的确定性源于开发者对malloc/free调用时机与底层映射行为的精确掌控。
页分配与映射层级
malloc小块内存通常复用堆内空闲区(brk/sbrk)- 大块内存(≥128KB)直接触发
mmap(MAP_ANONYMOUS),建立独立VMA并映射物理页 free对mmap分配区立即调用munmap,解除页表项(PTE)映射
典型调用链示例
// 触发mmap路径的malloc调用(glibc 2.34+)
void *p = malloc(512 * 1024); // ≥128KB → mmap
free(p); // → munmap → 清除页表项、释放物理页
该代码绕过堆管理器,直接与内核VM子系统交互;malloc内部根据M_MMAP_THRESHOLD阈值决策路径,free依据chunk元数据中的IS_MMAPPED标志选择释放方式。
页表映射状态对比
| 操作 | 页表项(PTE)状态 | TLB影响 | 物理页释放时机 |
|---|---|---|---|
malloc(mmap) |
新建有效映射 | 需TLB填充 | 分配时延迟分配 |
free(munmap) |
置为无效 | TLB shootdown | 立即归还伙伴系统 |
graph TD
A[malloc 512KB] --> B{size ≥ MMAP_THRESHOLD?}
B -->|Yes| C[mmap MAP_ANONYMOUS]
B -->|No| D[sbrk + heap management]
C --> E[建立VMA + 页表项]
E --> F[首次访问触发缺页中断]
F --> G[分配物理页 + 填充PTE]
2.2 Go GC三色标记-清除算法的STW触发路径与runtime.g0切换实测
Go 的 STW(Stop-The-World)由 runtime.gcStart() 触发,核心路径为:
gcStart → stopTheWorldWithSema → mcall(gcParkAssist) → 切换至 g0 栈执行清扫准备。
STW 中的 g0 切换关键点
// runtime/proc.go 中 mcall 的简化逻辑
func mcall(fn func(*g)) {
// 保存当前 g 的 SP、PC 到 g.sched
// 切换到 m.g0 栈
// 调用 fn(g0)
systemstack(func() {
fn(getg()) // 此时 getg() 返回的是 g0
})
}
该调用强制将协程上下文切换至 m.g0,确保 GC 全局操作在系统栈上安全执行,避免用户栈干扰。
GC 触发时的 goroutine 状态迁移
| 阶段 | 当前 Goroutine | 执行栈 | 作用 |
|---|---|---|---|
| 用户态运行 | user goroutine | 用户栈 | 可能正在分配或读写指针 |
| STW 进入 | m.g0 | 系统栈 | 安全扫描/暂停所有 P |
| 标记阶段 | worker goroutine | g0 或 dedicated G | 并发标记,受 barrier 保护 |
graph TD
A[GC 条件满足] --> B[gcStart]
B --> C[stopTheWorldWithSema]
C --> D[mcall gcParkAssist]
D --> E[切换至 m.g0 栈]
E --> F[执行 sweep termination & mark setup]
2.3 内存分配器视角:tcmalloc vs mheap.mcentral并发竞争的火焰图对比
竞争热点定位差异
tcmalloc 使用 per-CPU central cache,减少锁争用;Go 的 mheap.mcentral 则依赖全局 mcentral.lock,高并发下易成瓶颈。
典型竞争栈对比(火焰图关键帧)
| 分析维度 | tcmalloc | Go mheap.mcentral |
|---|---|---|
| 锁粒度 | per-size-class central cache | 全局 mcentral.lock |
| 热点函数 | CentralFreeList::Populate |
mcentral.cacheSpan |
| 平均等待延迟 | > 300ns(16+ goroutines) |
// runtime/mcentral.go 关键同步段
func (c *mcentral) cacheSpan() *mspan {
c.lock() // ← 全局互斥,无size分片
...
c.unlock()
}
该锁保护所有同 size-class 的 span 操作,导致跨 P 协作时频繁阻塞。而 tcmalloc 中 CentralFreeList::Populate() 仅在本地 central cache 耗尽时触发,且 lock scope 更窄。
数据同步机制
- tcmalloc:通过
SpinLock+ 批量迁移(TransferBatch)降低锁持有时间 - Go:
mcentral.lock为mutex,无批量优化,span 获取/归还均需独占
graph TD
A[goroutine 请求 32B span] --> B{mcentral.lock acquired?}
B -->|Yes| C[从 nonempty 链表取 span]
B -->|No| D[自旋等待 → CPU 火焰升高]
2.4 栈扫描开销量化:goroutine栈增长与C函数调用帧混合场景的压力复现
当 goroutine 在执行中频繁调用 cgo 函数,其栈布局呈现 Go 动态栈(可增长)与 C 固定帧(不可重定位)交错状态,GC 栈扫描需跨两种内存模型校验指针,显著抬升 STW 峰值。
混合栈压力复现代码
// #include <unistd.h>
import "C"
func stressMixedStack() {
for i := 0; i < 10000; i++ {
C.usleep(1) // 触发 cgo 调用,压入 C 帧
_ = make([]byte, 1024*i) // 触发 Go 栈增长
}
}
该循环强制每轮生成新 Go 栈段并嵌入 C 帧,使 runtime.scanstack 遍历非连续、类型混杂的栈区间,放大扫描路径分支判断开销。
关键开销来源对比
| 因子 | Go-only 栈 | Go+C 混合栈 | 增幅 |
|---|---|---|---|
| 栈段数量 | ~3–5 段 | 20+ 段(含 C 帧隔离区) | ×4.2 |
| 指针验证跳转次数 | 线性遍历 | 多条件分支(isCFrame? isStackMapValid?) | +68% |
graph TD
A[scanstack入口] --> B{当前帧是C帧?}
B -->|Yes| C[跳过栈映射,查cgo map]
B -->|No| D[按Go stackmap解析]
C --> E[合并指针集]
D --> E
E --> F[标记存活对象]
2.5 全局停顿敏感点定位:write barrier启用时机与compiler插入点反汇编验证
数据同步机制
Go 1.22+ 中,write barrier 在 GC 启动后由 gcenable() 触发启用,但实际生效依赖于编译器在指针写入路径插入的 wb 指令序列。关键插入点包括:
- 堆对象字段赋值(如
x.f = y) - slice/map append 引发的底层数组重分配
- interface{} 赋值时的底层结构体拷贝
反汇编验证方法
使用 go tool compile -S 提取目标函数汇编,搜索 call runtime.gcWriteBarrier 或 movb $1, (runtime.writeBarrier):
// 示例:slice赋值触发的write barrier调用
MOVQ "".y+24(SP), AX // 加载y的指针
LEAQ (CX)(SI*8), DX // 计算目标地址
CALL runtime.gcWriteBarrier(SB) // 插入屏障调用
该调用前需确保 AX(源)、DX(目标地址)已就绪;若缺失,则说明逃逸分析判定为栈分配,或编译器优化绕过屏障。
编译器屏障插入规则
| 场景 | 是否插入 barrier | 条件 |
|---|---|---|
| 堆上结构体字段赋值 | ✅ | 目标地址非常量且逃逸 |
| 栈变量间赋值 | ❌ | 不涉及堆引用 |
| unsafe.Pointer 转换 | ⚠️(需显式标记) | 依赖 //go:writebarrier 注释 |
graph TD
A[Go源码:x.f = y] --> B{逃逸分析}
B -->|逃逸至堆| C[SSA生成:store + writebarrier]
B -->|未逃逸| D[直接栈拷贝,无barrier]
C --> E[最终汇编含gcWriteBarrier调用]
第三章:17类边界Case中高频失效模式归因
3.1 大对象跨越GC周期导致的隐式STW延长(>100MB heap实测)
当堆内存超过100MB且存在长期存活的大对象(如缓存的Protobuf序列化数据、图像像素缓冲区),G1或ZGC可能无法在单次GC周期内完成其回收,被迫推迟至后续周期——此“跨周期滞留”会隐式延长安全点(Safepoint)等待时间,引发非预期的STW尖峰。
GC日志中的跨周期线索
# GC日志片段(G1)
[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.1823456 secs]
[Eden: 12M(12M)->0B(12M) Survivors: 2M->2M Heap: 85M(128M)->73M(128M)]
# 注意:OldGen中102MB大对象未被回收,下轮日志显示其仍标记为live
▶ 逻辑分析:Heap: 85M→73M 表明年轻代成功回收,但老年代102MB对象未移动/释放,因G1 Region大小(默认1MB)无法容纳该对象,触发“humongous allocation”特殊处理,其回收需等待完整并发周期,期间所有线程在safepoint阻塞等待。
关键参数影响表
| 参数 | 默认值 | 效果 |
|---|---|---|
-XX:G1HeapRegionSize |
自动推导(1–4MB) | 过小则大对象拆分失败,加剧跨周期滞留 |
-XX:MaxGCPauseMillis |
200ms | 无法约束跨周期对象的STW累积延迟 |
STW阻塞链路
graph TD
A[线程进入safepoint] --> B{大对象仍在old-gen且标记为live}
B -->|是| C[等待并发标记完成]
C --> D[并发标记未结束 → 持续阻塞]
B -->|否| E[快速通过]
3.2 CGO调用密集场景下P状态阻塞与GMP调度器死锁复现
当大量 Goroutine 频繁跨 C 边界执行阻塞式 CGO 调用(如 C.sleep, C.getaddrinfo)时,若 GOMAXPROCS 未预留空闲 P,会导致所有 P 均被绑定至 M 并陷入系统调用等待,而 runtime 无法调度新 G —— 此即 P 全占用 + 无可用 M 回收路径引发的调度器停滞。
关键触发条件
- CGO 调用未设超时且并发数 ≥
GOMAXPROCS CGO_ENABLED=1且GODEBUG=schedtrace=1000- Go 运行时无法抢占正在执行 CGO 的 M(M 脱离 P 管理)
复现实例
// 模拟密集阻塞型 CGO 调用
func cgoBlock() {
for i := 0; i < 100; i++ {
go func() {
C.usleep(C.useconds_t(1e6)) // 阻塞 1s
}()
}
}
此代码启动 100 个 Goroutine 同步调用
usleep。每个调用使 M 脱离 P 并进入syscall状态;若GOMAXPROCS=4,则仅 4 个 P 可用,其余 G 排队却无 P 可分配,GMP 循环断裂。
| 状态 | P 数量 | M 绑定状态 | 可调度 G 队列 |
|---|---|---|---|
| 正常运行 | 4 | 4 个 M 绑定 P | 非空 |
| 死锁临界点 | 4 | 4 个 M 全在 CGO | 空(积压中) |
graph TD
A[Goroutine 发起 CGO] --> B{M 是否空闲?}
B -->|否| C[新建 M 或复用阻塞 M]
C --> D[M 脱离 P,进入 syscall]
D --> E[P 释放但无其他 M 可绑定]
E --> F[新 G 无法获得 P → 永久等待]
3.3 碎片化内存池在C长期驻留指针与Go finalizer交叉引用下的崩溃复盘
根本诱因:生命周期错位
当 C 侧通过 malloc 分配的内存块被 Go 的 runtime.SetFinalizer 关联到一个 Go 对象时,若该对象被 GC 回收,finalizer 触发释放 C 内存;但此时 C 代码仍持有原始指针并尝试访问——即“悬垂指针访问”。
关键代码片段
// C side: long-lived pointer, never freed by C logic
static void* g_cached_ptr = NULL;
void init_cache() {
g_cached_ptr = malloc(4096); // allocated in fragmented heap region
}
void use_cached_data() {
memcpy(buf, g_cached_ptr, 128); // ❌ crash if finalizer already ran
}
g_cached_ptr在 C 全局作用域中长期驻留,无引用计数或弱引用机制;finalizer 执行后free()该地址,但 C 侧无法感知。memcpy触发 SIGSEGV。
复现路径(mermaid)
graph TD
A[Go struct allocated] --> B[SetFinalizer → free g_cached_ptr]
C[C calls init_cache] --> D[g_cached_ptr = malloc]
D --> E[Go object becomes unreachable]
E --> F[GC triggers finalizer]
F --> G[free g_cached_ptr]
G --> H[C calls use_cached_data]
H --> I[SIGSEGV on stale pointer]
解决方案对比
| 方案 | 安全性 | 性能开销 | 实施复杂度 |
|---|---|---|---|
runtime.KeepAlive + 显式 C.free |
✅ 高 | ⚡ 低 | ⭐⭐ |
sync.Map + 引用计数 |
✅ 高 | 🐢 中 | ⭐⭐⭐⭐ |
unsafe.Pointer + runtime.Pinner(Go 1.23+) |
✅✅ 待验证 | ⚡ 低 | ⭐⭐⭐ |
第四章:可工程化落地的STW压制策略矩阵
4.1 Go 1.22+ incremental marking调优参数组合与pprof trace验证
Go 1.22 引入更细粒度的增量标记(incremental marking)调度机制,可通过运行时环境变量调控关键行为:
GODEBUG=gctrace=1,GOGC=100,GOMAXPROCS=8
gctrace=1:启用 GC 追踪日志,输出每次标记阶段耗时与堆变化GOGC=100:维持默认触发阈值(堆增长100%触发GC),避免过早启动增量标记GOMAXPROCS=8:确保足够 P 并发执行标记任务,提升并发标记吞吐
pprof trace 验证要点
使用 go tool trace 可观测 GC/Mark/Start 与 GC/Mark/Done 之间的时间片分布,确认增量标记是否被合理切片。
| 参数 | 推荐值 | 影响面 |
|---|---|---|
GOGC |
80–120 | 控制标记启动频率 |
GOMAXPROCS |
≥4 | 决定并行标记 worker 数 |
GODEBUG=madvise=1 |
启用 | 减少内存归还延迟 |
// 在 init() 中注入 trace 标记点
import "runtime/trace"
func init() {
trace.Start(os.Stderr) // 或写入文件供 go tool trace 解析
}
该代码启用全局 trace 采集,配合 GODEBUG=gctrace=1 可交叉比对 GC 日志与 trace 时间线,精准定位标记卡顿点。
4.2 C内存池嵌入Go runtime:mmap匿名映射+runtime.SetFinalizer协同方案
在混合编程场景中,C侧预分配的内存池需与Go GC生命周期对齐。核心策略是:C端通过mmap(MAP_ANONYMOUS|MAP_PRIVATE)申请页对齐内存块,Go端用unsafe.Pointer封装为[]byte切片,并注册runtime.SetFinalizer绑定析构逻辑。
内存映射与所有权移交
// C side: mmap anonymous memory pool
void* pool = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
MAP_ANONYMOUS确保无文件后端;-1 fd参数合法;返回地址可直接被Go unsafe.Pointer接收。
Finalizer协同释放
// Go side: bind finalizer to trigger munmap
type MemPool struct{ ptr unsafe.Pointer; size uintptr }
runtime.SetFinalizer(&pool, func(p *MemPool) {
syscall.Munmap(p.ptr, p.size) // 必须显式释放,避免泄漏
})
Finalizer在对象不可达且GC标记后调用;syscall.Munmap精确匹配mmap参数,保障页表清理。
| 关键机制 | 作用 |
|---|---|
MAP_ANONYMOUS |
避免文件I/O开销,纯内存池 |
runtime.SetFinalizer |
实现C资源的自动生命周期托管 |
graph TD
A[C malloc/mmap] --> B[Go unsafe.Slice]
B --> C[SetFinalizer]
C --> D[GC发现不可达]
D --> E[调用munmap]
4.3 基于eBPF的STW事件实时捕获与用户态延迟注入压力测试框架
核心设计思想
将JVM GC STW(Stop-The-World)事件通过内核态eBPF探针实时捕获,并在用户态按需注入可控延迟,构建闭环可观测性+可编程压力测试能力。
eBPF探测逻辑(内核侧)
// trace_stw_start.c:挂钩JVM safepoint entry点
SEC("tracepoint/sched/sched_process_fork")
int trace_safepoint_begin(struct trace_event_raw_sched_process_fork *ctx) {
u64 pid = bpf_get_current_pid_tgid() >> 32;
bpf_map_update_elem(&stw_start_ts, &pid, &ctx->common_timestamp, BPF_ANY);
return 0;
}
逻辑分析:利用
sched_process_fork作为代理触发点(因JVM safepoint无原生tracepoint),结合/proc/pid/stack辅助验证;stw_start_ts为BPF_MAP_TYPE_HASH,键为PID,值为纳秒级时间戳,支持毫秒级STW持续时间计算。
用户态延迟注入控制流
graph TD
A[eBPF Map] -->|STW开始时间| B[userspace daemon]
B --> C{是否启用注入?}
C -->|是| D[调用nanosleep阻塞目标线程]
C -->|否| E[仅上报metrics]
关键参数对照表
| 参数 | 含义 | 典型值 | 生效层级 |
|---|---|---|---|
inject_ratio |
延迟注入概率 | 0.3 | 用户态配置 |
delay_us |
单次注入延迟 | 50000 | eBPF map传入 |
stw_threshold_ms |
STW告警阈值 | 10 | Prometheus exporter |
4.4 混合语言调用链路的GC友好数据结构设计:避免跨语言指针逃逸的ABI约束实践
在 Rust-C/Python 多语言协同时,GC 友好性核心在于数据所有权显式移交与零拷贝边界控制。
数据同步机制
Rust 侧暴露 #[no_mangle] pub extern "C" fn new_buffer(len: usize) -> *mut u8,返回堆分配但不注册为 GC root的裸指针;C 侧通过 free() 显式释放,规避 Go/Python GC 对跨语言指针的误回收。
// Rust 导出:内存由调用方负责生命周期
#[no_mangle]
pub extern "C" fn create_payload(size: usize) -> *mut u8 {
let mut buf = Vec::with_capacity(size);
let ptr = buf.as_mut_ptr();
std::mem::forget(buf); // 阻止 Drop,移交所有权
ptr
}
逻辑分析:
std::mem::forget(buf)防止Vec自动析构,ptr成为纯 C 兼容指针;size参数决定预分配容量,避免后续 realloc 引发 GC 扫描逃逸。
ABI 约束关键点
| 约束项 | 合规做法 | 违规风险 |
|---|---|---|
| 指针生命周期 | 单次移交,无回调传回 Rust | Rust 二次解引用触发 UAF |
| 内存对齐 | #[repr(C)] + align(16) |
Python ctypes 解包错位 |
graph TD
A[Rust: Vec::with_capacity] --> B[std::mem::forget]
B --> C[C/Python: raw ptr]
C --> D{调用方 free/free_python_buffer}
D --> E[内存归还系统堆]
第五章:超越语言之争的系统级内存治理新范式
内存隔离的硬件赋能:Intel TDX 与 AMD SEV-SNP 实战对比
现代云原生环境正从“进程级沙箱”迈向“硬件级内存围栏”。在某金融风控平台迁移中,团队将核心决策引擎容器部署于启用 AMD SEV-SNP 的 EPYC 服务器集群。实测显示:即使宿主机内核被提权攻击,攻击者无法读取加密内存页(/dev/sev 报告 SEV_ES_RESET 异常),而传统 cgroups+memcg 配置下,OOM Killer 误杀率高达17%。下表为两代机密计算技术关键指标对比:
| 特性 | Intel TDX (v1.5) | AMD SEV-SNP (v2) | Linux 6.8 原生支持 |
|---|---|---|---|
| 内存加密粒度 | 4KB 页面 | 16KB 安全区块 | ✅ |
| 虚拟机热迁移支持 | ❌(需暂停) | ✅(加密状态迁移) | ⚠️(需 QEMU 8.2+) |
| 内存带宽开销 | +3.2% | +1.9% | — |
eBPF 驱动的实时内存画像构建
某 CDN 边缘节点集群通过加载自定义 eBPF 程序 mem_tracer.o,在不修改应用代码前提下捕获所有 mmap()/brk() 调用链。以下为实际采集到的 Go 应用内存异常模式片段:
// bpf_mem_tracer.c 关键逻辑
SEC("tracepoint/syscalls/sys_enter_mmap")
int trace_mmap(struct trace_event_raw_sys_enter *ctx) {
u64 addr = bpf_probe_read_user(&ctx->args[0]);
u64 len = bpf_probe_read_user(&ctx->args[1]);
if (len > 1024*1024*100) { // 检测超大映射
bpf_map_update_elem(&large_allocs, &addr, &len, BPF_ANY);
}
return 0;
}
该方案使内存泄漏定位时间从平均47分钟缩短至21秒,且规避了 Go runtime GC 日志解析的版本兼容性问题。
跨语言内存池的统一编排实践
在混合技术栈微服务集群中,采用基于 Cilium 的 eBPF 内存仲裁器实现跨语言治理。当 Java 服务(JVM 17+ZGC)与 Rust 服务(mimalloc)共享同一 NUMA 节点时,仲裁器动态调整:
- Java 侧:通过 JMX 接口注入
-XX:MaxRAMPercentage=65 - Rust 侧:设置
MIMALLOC_LARGE_OS_PAGES=1并绑定numactl --cpunodebind=1 --membind=1
经 72 小时压测,内存碎片率稳定在 12.3%±0.8%,较传统静态分配降低 41%。
内存生命周期的声明式描述
Kubernetes CRD MemoryPolicy.v1alpha1 已在生产环境落地。某 AI 训练平台通过如下声明管控 GPU 显存与主机内存协同:
apiVersion: memory.k8s.io/v1alpha1
kind: MemoryPolicy
metadata:
name: llm-inference
spec:
memoryClass: high-bandwidth
evictionStrategy:
- condition: "container_memory_working_set_bytes > 80%"
action: "throttle_io"
- condition: "node_memory_MemAvailable_bytes < 4Gi"
action: "scale_down_replicas"
该策略使千卡集群显存利用率提升至 89.7%,同时避免因主机内存耗尽导致的 NCCL 通信中断。
硬件错误感知的内存降级机制
在某电信核心网 UPF 网元中,通过 edac-utils 监控 DDR5 ECC 错误计数。当单通道 ue_count 连续3次超过阈值时,自动触发内存降级流程:
- 使用
numactl --hardware识别故障 NUMA 节点 - 通过
echo offline > /sys/devices/system/node/node1/state下线节点 - 更新 kubelet
--system-reserved=memory=4Gi参数并滚动重启
该机制使单节点内存硬件故障导致的服务中断时间从平均18分钟降至23秒。
内存治理的可观测性数据流
flowchart LR
A[DRAM 温度传感器] -->|I2C| B(eBPF mem_health)
C[PMU L3_MISS_CNT] -->|Perf Event| B
D[EDAC ue_count] -->|Sysfs Poll| B
B --> E{Aggregation Layer}
E -->|Prometheus| F[Grafana Dashboard]
E -->|Webhook| G[Autoscaler]
E -->|gRPC| H[Memory Policy Controller] 