语言: English 中文版
[!WARNING] 目前并非所有代码文件都已完成整理(例如注释完善、代码结构调整、风格统一等)。部分工作已经进行,但仍有许多尚未完成。所有代码将在学期结束、项目收尾之前整理为一份专业的工程代码。现阶段还有些凌乱,敬请谅解。🙏🙏🙏
Prof: Ramesh Govindan

Proposal Report: Proposal.pdf

check #1 report: checkin1.pdf
本项目用 C++ 从零构建了一个裸机 FaaS(函数即服务)工作节点,专门针对边缘无服务器环境中的冷启动问题。
核心研究问题: 一个轻量级、O(1) 复杂度的预测启发式算法(EWMA + CUSUM + Little’s Law)能否在周期性流量下显著降低冷启动惩罚——同时比 ARIMA 这类重量级时序模型消耗少得多的资源开销?
DispatchPool。预测器与事件循环同线程运行,零竞争。| 模块 | 文件 | 状态 |
|---|---|---|
| C++ Epoll Reactor(ET 模式,非阻塞) | tcp_server.hpp |
✅ 完成 |
| 64 线程 DispatchPool(所有阻塞 I/O 在此) | DispatchPool.hpp |
✅ 完成 |
| WorkerPool:fork + 缩容清道夫线程 | WorkerPool.hpp |
✅ 完成 |
| Predictor:EWMA + CUSUM + Little’s Law | Predictor.hpp |
✅ 完成 |
动态 T 反馈(UpdateServiceTime) |
web_server.hpp |
✅ 完成 |
| Python Worker(模拟 T=500ms AI 推理) | worker.py |
✅ 完成 |
| 两轮对比测试 + Drain 阶段 | load_tester.py |
✅ 完成 |
| 缺项 | 类型 | 优先级 |
|---|---|---|
ARIMA 基线(Python statsmodels) |
代码 + 实验 | 🔴 高——导师明确要求 |
| Reactive 基线(关掉预测器,只走冷启动兜底) | 代码 + 实验 | 🔴 高——最坏情况对比,必须有 |
| Static 基线(固定 N 个 worker,不扩缩容) | 代码 + 实验 | 🟡 中——Pareto 曲线需要 |
| 内存 / CPU 开销测量 | 实验 | 🔴 高——Pareto 曲线的核心数据 |
| 预测器推理延迟测量 | 实验 | 🟡 中——证明 EWMA/CUSUM 远快于 ARIMA |
| CloudLab 裸机测试 | 环境 | 🟡 中——本机 loopback 没有真实网络噪声 |
| 周期性突发流量 workload(wrk) | 实验 | 🟡 中——Proposal 中承诺用 wrk |
日志:test_20260303_013519.log
V1.0 时 Python worker 处理的是 1×1 像素的图片,服务时间 T ≈ 0.001s。T 这么小,Little’s Law 算出来的结果是 N = ⌈60 × 0.001⌉ + 1 = 2,哪怕流量到了 60 RPS,2 个 worker 就够用了。预测器根本没有扩容的必要,冷启动从来不会发生。
更关键的问题是:当时还没做缩容(Scale-Down)。Cycle 1 期间 fork 出来的 worker 一直活着,Cycle 2 开始时进程池已经是满的,当然没有冷启动——这和 EWMA 预测没有任何关系,是因为 worker 从来没死过。
结果: P50=1.4ms,P95=1.7ms,P99=2.1ms,0 次冷启动。看起来很完美,实际上毫无意义。
根因: T 太小 → N 永远是 2 → 预测器无效。没有缩容 → Cycle 1 给 Cycle 2 免费”预热”了。
日志:test_20260303_022336.log、test_20260303_022743.log
在 worker.py 里加了 time.sleep(0.5) 之后(T=500ms),热路径延迟变成了约 502ms。但 load_tester.py 里的冷热判断阈值还是 rtt > 500——每个热请求(502ms)都被判为 COLD,结果 100% COLD,0 次温热。
修复方案:把阈值改为 700ms(热路径 ~502ms 和冷路径 ~800ms 的中点)。
日志:test_20260303_220742.log
修复阈值(700ms)、添加缩容(idle_timeout=6s)、在两轮之间加入 8 秒 drain 阶段(确保清道夫线程把所有 worker 都杀掉)后,得到了有意义的实验数据:
| 阶段 | 总计 | WARM | COLD | P50 | P99 |
|---|---|---|---|---|---|
| C1-Warmup(2 RPS,10s) | 20 | 18 | 2 | 503 ms | 806 ms |
| C1-Spike(30 RPS,5s) | 150 | 55 | 95 | 801 ms | 806 ms |
| C1-Cooldown(2 RPS,6s) | 12 | 12 | 0 | 502 ms | 504 ms |
| [Drain:8s 零流量——清道夫杀掉所有 worker] | |||||
| C2-Warmup(2 RPS,5s) | 10 | 8 | 2 | 515 ms | 812 ms |
| C2-Spike(30 RPS,5s) | 150 | 64 | 86 | 801 ms | 804 ms |
核心对比——仅看 Spike 阶段:
为什么 Cycle 2 会好一点? C1 Spike 结束后,EWMA 爬升到约 15 RPS,经过 Cooldown 和 Drain 衰减到约 2.9。Little’s Law 给出 ⌈2.9 × 0.5⌉ + 1 = 3 个 worker——比 C1 Warmup 时多 1 个。这个多出来的预热 worker 贡献了 C2 Spike 的那额外 +9 次热命中。
为什么提升幅度不大? CUSUM 是反应式的,不是预测式的:它在 Spike 第 2 秒才触发,而新 worker 需要 0.9s 冷启动,所以 CUSUM 的扩容对 Spike 阶段几乎来不及生效。Spike 阶段的改善主要来自 EWMA 记忆在 Warmup 阶段多预热了 1 个 worker,而不是 CUSUM 预见了流量峰值。根本上,30 RPS × 0.5s = 需要 15 个并发 worker,我们只预热了约 3 个,差距太大。
要达到论文级别的评估,按优先级顺序需要:
statsmodels)。没有这三条基线,就没有 Pareto 曲线,也就没有论文 Claim 的落脚点。# 依赖
pip install Pillow
# 编译
make clean && make
# 运行服务(端口 8080)
./server
# 运行两轮对比测试
python3 load_tester.py
日志输出: logs/test_<时间戳>.log

check #2 report: checkin2.pdf
| 改动 | 说明 |
|---|---|
| CoW 模板进程 | 新增 worker_template.py,启动时一次性加载 Pillow,后续 worker 全部 CoW fork。每个 worker 冷启动从 ~800ms 降到 ~100ms |
| 叙事重构 | 不再说”绕过 KVM 为了干净测量”。改为:边缘推理节点资源受限,跑不起 MicroVM,OS 进程池是合适的数据面 |
| Reactive 基线 | ./server reactive,纯响应式,无预测,作为冷启动下界对比 |
| Static 基线 | ./server static 15,固定 15 个 worker 全程保活,作为资源开销上界对比 |
| ARIMA 基线 | ./server arima,独立 Python 进程跑 ARIMA(2,1,2),验证重量级预测的开销代价 |
| 4 cycle 测试 | load_tester.py 升级为 4 个周期(C1: warmup 8s;C2–C4: warmup 35s),给 ARIMA 足够历史数据 |
| 自动化实验脚本 | run_experiments.sh 一键串行跑 4 个基线,自动归档日志到 logs/exp_<mode>_<ts>/ |
| 资源监控 | resource_monitor.py 每秒采样 RSS、CPU、worker 数,输出 CSV 供 Pareto 分析 |
已完成:
worker_template.py + WorkerPool.hpp 重构)待完成(Final Deliverable):
wrk 高频负载生成# 依赖
pip install Pillow statsmodels
# 编译
make clean && make
# 单独运行某个 mode
./server ewma # 默认,EWMA+CUSUM 预测
./server reactive # 纯响应式
./server static 15 # 固定 15 个 worker
./server arima # ARIMA 预测
# 运行 4-cycle 负载测试
python3 load_tester.py
# 一键跑完 4 个基线(推荐,自动归档)
./run_experiments.sh
# 单独跑某个基线
./run_experiments.sh ewma
日志输出: logs/exp_<mode>_<时间戳>/(包含 test.log、server.log、resource.csv)
主要发现:
已知问题 / 局限:
本节是 2026-04-30 课堂演示(CSCI 599)前整理的最终结果,对 Check #2 之后新增的 CoW 量化、Adaptive CUSUM 基线、Warmup Sweep 消融 做了系统化补充。所有图见
figures/pre/目录及MANIFEST.md。
| 新增 | 文件 / 命令 | 说明 |
|---|---|---|
| CoW 冷启动量化 | figures/plot_cow.py → slide05_cow.png |
从 worker.py 模拟 cold start + server.log “Worker N ready (CoW fork)” 日志手测:Naive (exec Python + import) ≈ 900 ms vs CoW (fork from warm parent) ≈ 100 ms,9× 提速 · 无 runtime 依赖 |
| CUSUM 实测 trace | figures/plot_rps_cusum.py → slide06_rps_cusum.png |
用 sweep #3 真实 server.log(90 个 predictor tick、11 个 SPIKE DETECTED)重建 CUSUM 累积轨迹(drift=5, h=8),验证报警全部落在 ramp 爬升段 |
| Adaptive CUSUM 基线 | ./server ewma_adaptive |
用 EWMSD(running σ)做 z-score 归一化,作为 fixed-drift CUSUM 的对照 |
| Workload 设计可视化 | figures/plot_workload.py → slide07_workload.png |
把 load_tester.py 的 4-cycle Bursty-Ramp 参数画成时间轴,标出 Ramp = CUSUM 检测窗口 |
| 5-mode 主结果 | figures/plot_main_result.py → slide08_main_result.png |
2026-04-20 重跑 5 个模式 × 4 cycle,cold counts 从各 load_tester_output.txt 的 SPIKE COMPARISON 表解析 |
| Warmup-Sweep 消融 | figures/plot_sweep.py → slide10_sweep.png |
sweep #1(W=5, 120 s 端点)+ sweep #3(W=10, 20, 35, 60 s 内点),对比 fixed vs adaptive |

| 模式 | C1 | C2 | C3 | C4 | 总计 | 说明 |
|---|---|---|---|---|---|---|
| Static-15(过度配置) | 0 | 0 | 0 | 0 | 0 | 15 个 worker 全程钉死 —— 上界参照线 |
| Adaptive CUSUM(EWMSD z-score) | 0 | 0 | 0 | 0 | 0 | W=35 落在 sweet spot |
| Fixed CUSUM(我们) | 0 | 14 | 33 | 0 | 47 | C3 出现 clock-aliasing 事件 |
| Reactive(按 backlog 扩缩) | 20 | 15 | 20 | 12 | 67 | 反应式基线 |
| ARIMA(smoothed Target) | 20 | 18 | 31 | 16 | 85 | 历史时序预测 |
总冷启动数 = 4 个 cycle 中 600 个 spike 请求里被判为 cold(RTT > 700 ms)的总数。
主要发现:
server.log 在 t=1776671064 时 CUSUM=18.24,而非预期 ~8–10)。这是 fixed-drift CUSUM 的已知 failure mode,不是 bug。不计这次事件,total ≈ 14,几乎贴着 Static-15 的 floor —— 但我们没钉死 15 个 worker
启动一次 template Python 进程预先 import 好 Pillow + 建好 socket 骨架,后续每个 worker 通过 fork() 从 template 复制。Linux 的 copy-on-write 让 fork 几乎免费 —— Pillow 代码 / import 表是只读的,不会触发页复制。
→ No image. No snapshot. No registry. Just fork() from a warm parent.

上面 panel:蓝线是测得 RPS,橙虚线是 EWMA baseline(α=0.2)—— 故意滞后让 RPS 一脱离就显出 gap。
下面 panel:绿色是 CUSUM 累加器,越过红色虚线 h = 8 时 ★ 报警 —— 11 次报警全部落在 ramp 爬升段,0 次落在 peak 之后。这就是 “catch the ramp, not the peak” 的实证。

每个 cycle 模拟一次”列车到站”周期:Warmup → Ramp(30 s) → Spike(30 RPS × 5 s) → Cooldown → Drain。

| W (s) | Fixed CUSUM | Adaptive CUSUM |
|---|---|---|
| 5 | 48 | 287 |
| 10 | 45 | 135 |
| 20 | 0 | 0 |
| 35 | 0 | 0 |
| 60 | 0 | 0 |
| 120 | 32 | 0 |
⚠ Framing:两个 failure modes,没有赢家。Fixed 适合紧节奏,Adaptive 适合松节奏。Predictor 不是一个选项 —— 是一个 knob。Adaptive 真正的贡献是 scale invariance + aliasing robustness,而不是更少的 cold starts。
wrk 或 Rust async)docs/pre_how_4.md(13 页 Slide 稿,中英双语)+ figures/pre/ 5 张图