第7章:闭环控制 —— AdaptX 折叠进 AURA
AURA 控制面的两条反馈环(owner-side back-pressure / NIC counter ingestion)来自 AdaptX,5ms 窗口选定的频谱分析、抖动 vs 反应速度的工程权衡
AURA 的决策不是一次性做完——它需要持续观察工作负载、持续调整 cohort 与 owner 位置。这个闭环来自原本独立的 AdaptX 工作的两条 Loop——Loop A(owner-side back-pressure)和 Loop B(NIC counter ingestion)。本章把这两条 Loop 折进 AURA 的控制面,解释 5ms 窗口为什么不能更短也不能更长,并展开 AIMD 反压算法的实现细节。读完你应该能解释”如果把窗口调到 1ms 会怎样”,并能用频谱分析的语言描述抖动来源。
📑 目录
- 1. 控制面闭环鸟瞰
- 2. Loop A:owner-side back-pressure
- 3. Loop B:NIC counter 摄取
- 4. AIMD 反压算法实现
- 5. 5ms 窗口的频谱分析
- 6. 多信号融合:避免单点 NIC counter 失真
- 7. 抖动来源与抑制
- 自我检验清单
- 参考资料
1. 控制面闭环鸟瞰
1.1 AdaptX 三条 Loop 的回顾
AdaptX 是 AURA 的前身设计,定义了三条独立的反馈 Loop:
| Loop | 功能 | 频率 | 当前在 AURA 里的位置 |
|---|---|---|---|
| Loop A | owner-side back-pressure(防 owner CN 过载) | 5ms | 折进 OwnerLockTable + AffinityRouter |
| Loop B | NIC IOPS counter 摄取(防 MN atomic 触顶) | 5ms | 折进 AccessGraphProfiler + OwnershipPlanner |
| Loop C | 锁置换(hypothetical) | 5ms | 直接成为 AURA 主体(cohort 划分 + 迁移) |
🌟 关键事实:AURA = AdaptX Loop C 主干 + Loop A/B 折进控制面。原本独立的 AdaptX 工作被吸收成为 AURA 的支撑机制。
1.2 完整闭环图
┌────────── 数据面 (per-txn, µs 级) ──────────┐
│ │
│ TxnExecutor → OwnerLockTable │
│ │ │ │
│ ▼ ▼ │
│ AccessGraph Stats │
│ (trace) (latency, qps) │
│ │
└──────────┬──────────────┬───────────────────┘
│ │
│ 5ms 一次 │
▼ ▼
┌────────────────────── 控制面 (5ms tick) ──────────┐
│ │
│ AccessGraphProfiler ◄── trace samples │
│ │ │
│ ▼ │
│ Cohort merge/split │
│ │ │
│ ▼ │
│ OwnershipPlanner ◄── { Loop A signals, │
│ │ Loop B signals } │
│ ▼ │
│ TransferController │
│ │ │
│ ▼ │
│ OwnerMapPublisher → 广播 epoch │
│ │
└───────────────────────────────────────────────────┘
▲
│
┌─────────────────┴─────────────────────────────┐
│ 监测面(高频采样) │
│ │
│ Loop A: owner-side counters │
│ - in-flight tx queue depth │
│ - lock_table acquire latency P99 │
│ - CPU pct │
│ │
│ Loop B: NIC counters (MN side, RDMA) │
│ - port_xmit_wait │
│ - atomic_queue_depth (vendor specific) │
│ - abort_rate (derived) │
└────────────────────────────────────────────────┘
1.3 三层信号的融合策略
每 5ms 一次 OwnershipPlanner.solve() 时输入三类信号:
──────────────────────────────────────────────────
1. 访问图(来自 AccessGraphProfiler)
→ 决定 cohort 边界
2. owner-side 信号(来自 Loop A)
→ 决定 owner CN 是否需要降级 / cohort 是否需要 split
3. NIC counter 信号(来自 Loop B)
→ 决定是否需要更激进迁移 / 进入 FALLBACK
🧠 关键洞察:单信号容易失真,多信号 voting 才稳健——这是 AURA 闭环的核心设计原则。
2. Loop A:owner-side back-pressure
2.1 为什么 owner CN 会成为新瓶颈
把锁从 MN 提到 CN,就是把 atomic 压力从 MN RNIC 转移到 owner CN 的 OwnerLockTable + RPC 队列。owner CN 也有自己的容量上限:
| 维度 | owner CN 上限 |
|---|---|
| OwnerLockTable QPS | ~10M ops/s(CPU 决定) |
| OwnerRpc QPS | ~5M ops/s |
| 网卡接收 QPS | ~150 Mpps(远超 atomic) |
| CPU 利用率 | 80% threshold(保留 20% 应急) |
典型触发:单个 cohort 太热 + cohort 内有 hot key → owner CN CPU 100% → 后续请求 queue 起来。
2.2 Loop A 的反馈机制
class LoopABackPressure {
DecayingCounter cpu_pct_; // 5ms 半衰期的 CPU 利用率
DecayingCounter queue_depth_;
DecayingCounter lock_p99_;
SignalLevel current_signal() {
if (cpu_pct_.read() > 0.8) return OVERLOAD_HIGH;
if (queue_depth_.read() > 1000) return OVERLOAD_HIGH;
if (lock_p99_.read() > 50_us) return OVERLOAD_MEDIUM;
return NORMAL;
}
void on_window_end() {
if (current_signal() == OVERLOAD_HIGH) {
// 通知 OwnershipPlanner:该 owner 过载
planner_.flag_owner_overload(self_cn_id_);
// AIMD 减速:少接新 cohort 任务
admission_budget_.multiplicative_decrease();
} else {
admission_budget_.additive_increase();
}
}
};
2.3 触发的具体动作
| 信号 | 动作 |
|---|---|
| OVERLOAD_HIGH 持续 1 窗口 | OwnershipPlanner 优先选别的 CN 接收新 cohort |
| OVERLOAD_HIGH 持续 3 窗口 | 主动 split 该 owner 上最热的 cohort |
| OVERLOAD_HIGH 持续 5 窗口 | 强制把部分 cohort 迁出 |
| NORMAL 持续 N 窗口 | 缓慢恢复 admission budget |
🌟 关键作用:Loop A 防止 AURA 把 atomic 瓶颈”从 MN 搬到某个 CN”——保证 cohort 在 owner 之间均衡分布。
2.4 Loop A 与 cohort split 的协同
owner CN 过载有两种解法:
| 解法 | 何时选 |
|---|---|
| 把热 cohort 整个迁走 | 该 owner 上的总负载偏高、cohort 之间没有内聚力 |
| split 热 cohort | 单个 cohort 太大、split 后能分散到其他 CN |
LockCohortGenerator 与 Loop A 协作:
void OwnershipPlanner::handle_overload(cn_id_t cn) {
auto cohorts_on_cn = get_cohorts(cn);
auto hottest = max(cohorts_on_cn, key=load);
// 决策:split or migrate?
if (hottest.size > 100 and hottest.internal_cut_score > 0.5) {
// 内部能切干净 → split
cohort_generator_.force_split(hottest);
} else {
// 整体迁走
auto target = pick_least_loaded_cn();
transfer_controller_.elect(hottest, target);
}
}
2.5 Loop A 的信号噪声
CPU pct 和 queue depth 都有短时 burst(GC、page fault)。Loop A 用衰减计数 + 多窗口确认避免误触发:
| 噪声源 | 抑制 |
|---|---|
| GC pause(10–50ms) | 衰减半衰期 5ms 自动滤掉 |
| 短时 burst | 必须连续 N 窗口才升级 signal |
| 测量抖动 | 多信号 voting(CPU + queue + p99) |
3. Loop B:NIC counter 摄取
3.1 NIC counter 是什么
ConnectX 网卡暴露一组硬件计数器(/sys/class/infiniband/<dev>/ports/<port>/counters/):
| 计数器 | 含义 | 用途 |
|---|---|---|
port_xmit_data | 发出字节数 | 带宽监测 |
port_rcv_data | 接收字节数 | 带宽监测 |
port_xmit_wait | 发出但未 ack 的累积时间 | atomic 排队压力 |
port_rcv_wait | 接收 backpressure | 接收过载 |
port_xmit_packets | 发出包数 | IOPS |
⭐ 关键计数:port_xmit_wait 是 atomic 排队压力的代理指标——atomic 在 NIC 内部排队时,发出的请求数减但 wait 累积。
3.2 perfquery 与 sysfs 两种读法
# 方式 1: perfquery(OFED 自带)
perfquery -x mlx5_2 1
# 输出:
# PortXmitData: 12345678
# PortRcvData: 9876543
# PortXmitWait: 56789
# ...
# 方式 2: sysfs(rdma-core 也支持)
cat /sys/class/infiniband/mlx5_2/ports/1/counters/port_xmit_wait
# 输出:56789
| 方式 | 优点 | 缺点 |
|---|---|---|
| perfquery | 一次拿全计数 | 子进程开销 ~1ms |
| sysfs | 单次读 ~50µs | 多个计数要读多次 |
AURA 默认 sysfs——按 5ms 窗口频率,子进程开销不可接受。
3.3 5ms 周期采样
class NicCounterSampler {
std::string base_path_;
uint64_t prev_xmit_wait_ = 0;
uint64_t prev_xmit_pkts_ = 0;
void tick(uint64_t now_us) {
auto cur_xmit_wait = read_sysfs(base_path_ + "port_xmit_wait");
auto cur_xmit_pkts = read_sysfs(base_path_ + "port_xmit_packets");
// 速率
auto wait_delta = cur_xmit_wait - prev_xmit_wait_;
auto pkts_delta = cur_xmit_pkts - prev_xmit_pkts_;
// 喂给 Loop B 信号
loop_b_.observe(wait_delta, pkts_delta);
prev_xmit_wait_ = cur_xmit_wait;
prev_xmit_pkts_ = cur_xmit_pkts;
}
};
3.4 三个关键派生量
| 派生量 | 公式 | 用途 |
|---|---|---|
| wait_per_pkt | wait_delta / pkts_delta | 单包平均排队时间 |
| atomic_qpd_proxy | wait_delta / 窗口长度 | atomic queue depth 代理 |
| abort_rate | from AuraStats | 观察事务级 abort |
auto wait_per_pkt = pkts_delta == 0 ? 0 : wait_delta / pkts_delta;
auto atomic_qpd = wait_delta * 1000.0 / window_us;
if (atomic_qpd > THRESHOLD_ATOMIC_QPD) {
// 触发紧急动作
planner_.flag_mn_atomic_pressure();
}
3.5 NIC counter 的局限:固件依赖
⚠️ 重要:不同 ConnectX 固件版本对 counter 的支持不同:
| 版本 | port_xmit_wait | atomic_queue_depth | 备注 |
|---|---|---|---|
| ConnectX-3 mlx4 | 支持但不准 | 不支持 | 只能用 abort rate 间接 |
| ConnectX-5 firmware ≥ 16.x | 支持 | 部分支持(需 vendor counter) | 可靠 |
| ConnectX-6 Dx firmware ≥ 22.x | 支持 | 支持 | 推荐 |
实战策略:单点 NIC counter 不可靠 → §6 多信号融合。
4. AIMD 反压算法实现
4.1 AIMD 是什么
Additive Increase / Multiplicative Decrease:
- 没有过载信号 → 配额(budget)线性增加
- 收到过载信号 → 配额折半
经典源自 TCP CC,AURA 借鉴用作 owner CN 的 acquire 接受配额。
4.2 AIMD 控制器实现
class AIMDController {
static constexpr uint64_t INITIAL_BUDGET = 10000;
static constexpr uint64_t MAX_BUDGET = 1'000'000;
static constexpr uint64_t STEP_AI = 1000;
std::atomic<uint64_t> budget_{INITIAL_BUDGET};
public:
bool can_admit() {
auto b = budget_.load();
if (b == 0) return false;
budget_.fetch_sub(1); // 消耗 1 配额
return true;
}
void on_window_end(SignalLevel s) {
if (s == OVERLOAD_HIGH) {
// MD:折半
budget_.store(budget_.load() / 2);
} else if (s == NORMAL) {
// AI:加 STEP_AI
auto b = std::min<uint64_t>(budget_.load() + STEP_AI, MAX_BUDGET);
budget_.store(b);
}
}
};
4.3 AIMD 与 PID 的对比
| 算法 | 优点 | 缺点 |
|---|---|---|
| AIMD | 简单、对噪声鲁棒、有理论保证(公平性 + 收敛性) | 反应慢(增长是线性的) |
| PID | 反应快、可调优 | 调参困难 |
| Slow Start + AIMD(TCP) | 启动快、稳定后 AIMD | 实现复杂 |
🍎 直觉比喻:AIMD 像”小心翼翼地试水”——慢慢加,遇到问题就大步退。控制理论里的”鲁棒性优先”哲学。
4.4 AIMD 在 AURA 里的具体应用
| 应用场景 | budget 单位 |
|---|---|
| owner CN admission | 每窗口允许接受的 acquire 数 |
| 跨 cohort RPC | 每窗口允许发出的 OwnerRpc 数 |
| migration 频率 | 每秒允许的 migration 数 |
⭐ 多个 AIMD 控制器互不干涉——每个解决一个独立 bottleneck。
4.5 AIMD 与全局信号的协同
单 CN 的 AIMD 是局部决策。全局视图由 OwnershipPlanner 持有:
Local AIMD (each owner CN)
↓ throttles per-CN admission
↓
Global Planner (sees all CN budgets)
↓ if many CNs throttled → cohort 迁移更激进
↓ if all CNs healthy → 可以新 cohort 上线
🧠 关键洞察:AIMD 是 micro 调节,Planner 是 macro 决策。两者协同,不是替代。
5. 5ms 窗口的频谱分析
5.1 工作负载漂移频率分布(实测)
实测 TPC-C / SmallBank / TATP 的访问 pattern 漂移频率:
Power Spectral Density (log scale)
│
│ ★ 主能量带:50–100 Hz(漂移基频)
│ ▒
│ ▒▒▒
│ ▒▒▒▒
│ ▒▒▒▒▒
│ ▒▒▒▒▒▒
│ ▒▒▒▒ ← 干扰带 (>1kHz, NIC counter 噪声)
│ ▒▒▒ ▒▒▒▒▒▒▒▒
│ ▒▒▒▒
│ ▒
└────┴────┴────┴────┴────┴────► 频率 (Hz)
10 100 1000 10k 100k
▲
5ms 决策窗口对应这里 (200 Hz Nyquist)
🌟 核心结论:5ms 窗口对应 100 Hz Nyquist 频率上限——刚好覆盖工作负载主要漂移频段(50–100 Hz)。
5.2 为什么不是 1ms
| 1ms 窗口 | 问题 |
|---|---|
| Nyquist 上限 500 Hz | 远超漂移频段,过度采样 |
| 单窗口 sample 数 ~200 | 统计噪声大(√N 噪声占比 ~7%) |
| 决策开销 ~500µs / 1ms | 50% 时间在做控制 |
| 抖动放大 | 每 1ms 决策一次,OwnerMap 抖动 5× |
5.3 为什么不是 50ms 或 100ms
| 50–100ms 窗口 | 问题 |
|---|---|
| Nyquist 上限 5–10 Hz | 错过主漂移带(50–100 Hz) |
| 反应迟缓 | 漂移已发生 50ms 才检测到 |
| LOTUS 用 100ms 反应式 | 已知的失效场景 |
5.4 频谱分析的工程价值
⭕ 互补:5ms 不是数学上的”最优”,是工程上的”够用”:
- 比 1ms 大 5× → 噪声小、决策开销低
- 比 100ms 小 20× → 反应快、跟得上漂移
🧠 关键洞察:控制系统的窗口长度选择往往是”够用就好”,不是”越短越好”。频谱分析帮你确定”够用”的下界。
5.5 自适应窗口大小(未来工作)
理论上窗口长度应该自适应:
if 当前漂移频率 < 50 Hz:
窗口 = 10ms(更稳)
elif 漂移频率 > 200 Hz:
窗口 = 2ms(更快)
else:
窗口 = 5ms(默认)
AURA 当前不做(实现复杂、收益有限)——作为开放问题留在第 3 章 §7。
6. 多信号融合:避免单点 NIC counter 失真
6.1 单信号不可靠的实例
实测中遇到的真实事故:
| 事故 | 现象 |
|---|---|
| ConnectX-3 firmware bug | port_xmit_wait 不递增(永远 0) |
| 测量周期对齐 | 多 CN 的 sysfs 读发生在 NIC counter 刷新边界 |
| 时钟漂移 | CN 间时钟差异 → wait 速率计算错 |
| 内存 page out | sysfs 读取突发延迟 100ms |
6.2 三信号 voting
AURA 的策略:三个信号至少 2/3 同向才触发动作:
struct AtomicPressureSignals {
bool nic_xmit_wait_high; // Loop B 信号
bool abort_rate_high; // 数据面信号
bool acquire_p99_high; // Loop A 信号
};
bool voted_atomic_pressure(const AtomicPressureSignals& s) {
int votes = (int)s.nic_xmit_wait_high
+ (int)s.abort_rate_high
+ (int)s.acquire_p99_high;
return votes >= 2;
}
🌟 关键性质:任意一个信号失真都不会单独触发动作——必须至少 2 个独立来源同向。
6.3 信号源的独立性
三个信号必须真正独立(共因故障不算独立):
| 信号 | 来源 | 失败 mode |
|---|---|---|
| nic_xmit_wait_high | sysfs / NIC firmware | firmware bug |
| abort_rate_high | OCC commit 路径计数 | TxnExecutor crash |
| acquire_p99_high | OwnerLockTable timing | 时钟问题 |
不同来源 → 不同 failure mode,voting 才有意义。
6.4 自适应阈值
固定阈值(如 port_xmit_wait_rate > 1M/s)容易被工作负载变化打脸。AURA 用自适应阈值:
class AdaptiveThreshold {
DecayingCounter mean_;
DecayingCounter stddev_;
bool is_anomaly(double sample) {
// 3-sigma rule
return sample > mean_.read() + 3 * stddev_.read();
}
void observe(double sample) {
mean_.add(sample);
stddev_.add(std::pow(sample - mean_.read(), 2));
}
};
🍎 直觉比喻:阈值不是”超过 1M/s 算异常”,而是”超过通常水平 3σ 算异常”——让阈值跟着工作负载走。
6.5 信号融合的代码实现
class SignalFusion {
AdaptiveThreshold nic_th_;
AdaptiveThreshold abort_th_;
AdaptiveThreshold p99_th_;
AtomicPressureSignals current() {
return {
.nic_xmit_wait_high = nic_th_.is_anomaly(loop_b_.wait_rate()),
.abort_rate_high = abort_th_.is_anomaly(stats_.abort_rate()),
.acquire_p99_high = p99_th_.is_anomaly(stats_.acquire_p99()),
};
}
void tick() {
auto signals = current();
if (voted_atomic_pressure(signals)) {
planner_.flag_atomic_pressure();
}
}
};
7. 抖动来源与抑制
7.1 抖动主要来源
| 来源 | 频率 | 表现 |
|---|---|---|
| GC pause(CN 进程) | 偶尔(~1Hz) | 短时 CPU 100% |
| 短时 workload burst | 高(数 Hz) | 1ms 内访问突增 |
| NIC counter 周期对齐 | 周期性(取决于固件) | 多 CN 同时刷新 |
| 时钟漂移 | 慢(小时级) | 衰减计数器误差 |
| OS scheduling jitter | 高 | sysfs 读取延迟变化 |
7.2 抑制策略
| 来源 | 抑制方法 |
|---|---|
| GC pause | 衰减半衰期 5ms 自动滤掉 |
| burst | 必须 N 窗口连续才升级信号 |
| 周期对齐 | 错开 CN 之间的采样相位(每 CN 加 random offset) |
| 时钟漂移 | 用相对时间(now - prev)而不是绝对时间 |
| OS jitter | 关键路径用 sched_setattr(SCHED_FIFO) 提优先级 |
7.3 衰减半衰期的选择
| 半衰期 | 滤掉的频率 | 适合 |
|---|---|---|
| 1ms | > 1kHz 噪声 | 太敏感 |
| 5ms | > 200Hz 噪声 | AURA 默认 |
| 50ms | > 20Hz 噪声 | 反应过慢 |
🌟 结论:5ms 半衰期 ≈ 5ms 窗口长度——这是 AURA 选 5ms 的另一个理由。
7.4 抖动检测的兜底机制
如果衰减 + voting 仍然不能稳定(极少数情况),AURA 有最后兜底:
class AntiOscillation {
std::unordered_map<cohort_id_t, CircularBuffer<bool>> migrate_history_;
bool should_lock_state(cohort_id_t c) {
auto& hist = migrate_history_[c];
// 1 秒内 migrate > 3 次 → 锁定 5 秒不动
return hist.count_recent(1000) >= 3;
}
};
⭕ 互补:兜底机制不是 always on——只在监测到反复抖动时启动。日常情况下不影响响应速度。
7.5 抖动的实测案例
实测中遇到的真实抖动:
| 案例 | 原因 | 解决 |
|---|---|---|
| OwnerMap 每秒变 100 次 | TPC-C 50/50 split 工作负载,cohort 在两个 CN 之间反复迁移 | 增大滞回带宽 0.05 → 0.10 |
| 单 CN CPU 利用率周期震荡 | GC pause 与采样周期对齐 | 采样相位加 random offset |
| Loop B 信号日夜规律 | 数据中心温度 → NIC firmware 自动调频 | 阈值用日夜段的 mean,不用全局 mean |
🧠 工程经验:真实抖动总是出在你没想过的地方——所以 AURA 设计上层层防御(衰减 + voting + 兜底锁定)。
✅ 自我检验清单
- 三 Loop:能默写 Loop A / B / C 各自的功能
- AURA = AdaptX 折叠:能解释 Loop A/B/C 在 AURA 里的位置
- owner-side 反压:能解释 Loop A 触发时 OwnershipPlanner 的行为
- Loop A 与 split 协同:能描述何时选 split / 何时选 migrate
- NIC counter:能列出至少 3 个 ConnectX 计数及其物理含义
- port_xmit_wait:能解释为什么它是 atomic 排队压力的代理
- AIMD 算法:能写一个最简化的 AIMD 实现
- AIMD vs PID:能解释为什么生产系统选 AIMD
- 5ms 窗口频谱:能用 Nyquist 频率论证为什么不是 1ms 或 100ms
- 多信号融合:能描述 voting 机制的好处
- 信号独立性:能列出 3 个独立信号源 + 各自 failure mode
- 自适应阈值:能写 3-sigma 自适应阈值的代码
- 抖动抑制:能列出至少 5 种抖动来源 + 抑制手段
- 衰减半衰期:能解释为什么半衰期 ≈ 窗口长度
📚 参考资料
概念入门
- TCP Congestion Control (Jacobson et al., 1988) —— AIMD 算法原创
- 频谱分析教科书 —— Oppenheim & Schafer, Discrete-Time Signal Processing
- Mellanox NIC counter 文档 —— Mellanox Performance Tuning Guide
关键论文
- TCP CUBIC / BBR —— AIMD 后续改进的对照
- AdaptX 设计文档(本仓库
caesar/项目下)—— Loop A/B/C 原始定义 - AURA 论文 §3.5 / §4.2 —— 控制面闭环的详细描述
行业讨论
- Robust Control vs Optimal Control —— 控制论中的”鲁棒性 vs 最优性”权衡
- NIC Counter Reliability —— Mellanox community 关于 firmware bug 的讨论
框架文档
- rdma-core sysfs counter API:github.com/linux-rdma/rdma-core
- 本仓库
caesar/早期 AdaptX 实现 —— 折叠进 AURA 的源码版本