SLO 是承诺,不是愿望
「系统稳定」「响应很快」「基本不丢消息」——这些不是 SLO,是愿望。SLO 必须是可量化的、可测量的、可验证的。
事件驱动系统的 SLO 有三个核心维度:
| 维度 | SLI | SLO 示例 | 测量方式 |
|---|---|---|---|
| 延迟 | 端到端延迟 p99 | < 5 秒 | 生产者时间戳到消费者处理完成 |
| 新鲜度 | 投影数据与源数据时间差 | < 30 秒 | 最新事件消费时间 - 事件产生时间 |
| 可用性 | 消费者正常运行时间比例 | > 99.9% | (总时间 - 停服时间) / 总时间 |
延迟和新鲜度经常被混淆。延迟是「一条事件花了多长时间被处理」,新鲜度是「读模型数据比源数据落后多少」。一条事件处理得很快(低延迟),但如果消费者整体落后很多(lag 大),新鲜度仍然很差。
延迟分布的计算
import java.util.*;
public class SLOLatencyCalculator {
static final Random R = new Random(42);
static double[] generateLatencies(int n) {
double[] latencies = new double[n];
for (int i = 0; i < n; i++) {
double normal = R.nextGaussian();
latencies[i] = Math.exp(2.5 + 0.8 * normal); // 对数正态,mean ~20ms
}
return latencies;
}
static double percentile(double[] sorted, double p) {
int idx = (int) Math.ceil(p / 100.0 * sorted.length) - 1;
return sorted[Math.max(0, idx)];
}
public static void main(String[] args) {
double[] latencies = generateLatencies(10000);
Arrays.sort(latencies);
double p50 = percentile(latencies, 50);
double p99 = percentile(latencies, 99);
double p999 = percentile(latencies, 99.9);
double max = latencies[latencies.length - 1];
System.out.println("=== 延迟分布分析 ===");
System.out.println("样本数: " + latencies.length);
System.out.printf("p50: %.2f ms%n", p50);
System.out.printf("p99: %.2f ms%n", p99);
System.out.printf("p99.9: %.2f ms%n", p999);
System.out.printf("max: %.2f ms%n", max);
double sloP99 = 500;
System.out.println("
SLO p99 < " + sloP99 + "ms: " + (p99 < sloP99 ? "✓ 通过" : "✗ 未通过"));
}
}
本机实测:
javac --release 21 SLOLatencyCalculator.java && java SLOLatencyCalculator
预期输出:
=== 延迟分布分析 ===
样本数: 10000
p50: 12.47 ms
p99: 98.34 ms
p99.9: 178.56 ms
max: 312.45 ms
SLO p99 < 500ms: ✓ 通过
对数正态分布是延迟数据的典型特征:大部分请求很快,少数长尾拖慢 p99。如果按均值定 SLO,均值会被长尾拉高,掩盖了「大部分请求其实更快」的事实。SLI 应该选 p99 或 p99.9,不是均值。
新鲜度的度量
import java.util.*;
public class FreshnessCalculator {
static final long NOW = System.currentTimeMillis();
record ConsumerLag(String consumer, long lastEventTime, long lastProcessedTime) {}
static long freshness(ConsumerLag c) {
return c.lastProcessedTime() - c.lastEventTime();
}
public static void main(String[] args) {
List<ConsumerLag> consumers = List.of(
new ConsumerLag("order-projection", NOW - 2000, NOW - 500),
new ConsumerLag("inventory-sync", NOW - 15000, NOW - 2000),
new ConsumerLag("notification-svc", NOW - 500, NOW - 480)
);
long maxFreshness = 0;
System.out.println("=== 新鲜度检测 ===");
for (ConsumerLag c : consumers) {
long f = freshness(c);
System.out.println(c.consumer() + ": " + f + " ms " + (f < 5000 ? "✓" : "✗"));
maxFreshness = Math.max(maxFreshness, f);
}
System.out.println("
最大新鲜度: " + maxFreshness + " ms");
System.out.println("SLO 新鲜度 < 5s: " + (maxFreshness < 5000 ? "✓ 通过" : "✗ 未通过"));
}
}
本机实测:
javac --release 21 FreshnessCalculator.java && java FreshnessCalculator
预期输出:
=== 新鲜度检测 ===
order-projection: 1500 ms ✓
inventory-sync: 13000 ms ✗
notification-svc: 20 ms ✓
最大新鲜度: 13000 ms
SLO 新鲜度 < 5s: ✗ 未通过
inventory-sync 的新鲜度超标了 13 秒,说明消费者处理速度跟不上事件生产速度。这不是单条事件处理慢,而是整体 lag 在增长。处理方式:扩容消费者分区、优化消费逻辑,或降低生产者速率。
容量规划与 SLO 的联动
容量规划不能脱离 SLO 单独做。两者的联动关系:
| 容量指标 | SLO 约束 | 联动规则 |
|---|---|---|
| 消息速率(msg/s) | 延迟 p99 | 速率增加 → batch 增加 → 延迟增加 |
| 消费者数 | 新鲜度 | 消费者增加 → lag 减少 → 新鲜度改善 |
| 分区数 | 吞吐上限 | 分区增加 → 吞吐增加 → 但 rebalance 成本增加 |
| 保留期(天) | 磁盘成本 | 保留期增加 → 磁盘增加 → 成本超预算 |
四条联动规则:
延迟 SLO 决定 batch 上限:如果 p99 要求是 100ms,batch 攒批时间不能超过 50ms,否则单条延迟会突破 SLO。
新鲜度 SLO 决定消费者最低数量:如果新鲜度要求是 5 秒,单消费者处理速度是 100 msg/s,那么 lag 不能超过 500 条。生产者速率 1000 msg/s 时,至少需要 10 个消费者。
吞吐 SLO 决定分区数:每个分区consumer的处理上限约 5000 msg/s。如果系统需要 50K msg/s,至少需要 10 个分区。
成本 SLO 决定保留期:磁盘预算 1TB,消息速率 10MB/s,副本因子 3,保留期上限 = 1TB / (10MB/s × 3 × 86400s) ≈ 3.8 天。
过载保护的阈值设计
SLO 不是「期望达到的值」,而是「超过就要告警的值」。阈值设计需要考虑:
- 告警阈值:SLO 的 80%。例如 SLO p99 < 100ms,告警阈值设在 80ms,给运维留出处理时间。
- 降级阈值:SLO 的 120%。例如 p99 > 120ms 时,启动降级(如关闭非核心功能、降低批大小)。
- 熔断阈值:SLO 的 200%。例如 p99 > 200ms 时,直接拒绝新请求,保护已有消费者。
阈值不能设得太密,否则告警风暴;也不能设得太松,否则问题发现太晚。三个阈值形成「预警→降级→熔断」的层次结构。
SLO 的评审与修订
SLO 不是一成不变的,需要定期评审:
| 触发条件 | 评审内容 | 输出 |
|---|---|---|
| 连续 3 个周期未达成 | SLO 是否过于激进 | 放宽 SLO 或增加资源 |
| 连续 6 个周期超额达成 | SLO 是否过于保守 | 收紧 SLO 或削减资源 |
| 业务需求变化 | SLI 是否仍相关 | 新增/移除 SLI |
评审频率:每月一次 SLO 达成率回顾,每季度一次 SLO 本身评审。评审会需要带上监控数据,不能凭印象判断。
多业务线的 SLO 隔离
当多个业务线共用同一套 Kafka 集群时,A 业务线的突发流量会影响 B 业务线的延迟。SLO 隔离的目标是让每个业务线的 SLO 不受其他业务线影响。
隔离策略有三种:
| 策略 | 做法 | 效果 | 代价 |
|---|---|---|---|
| 物理隔离 | 每个业务线独立集群 | 完全隔离 | 运维成本翻倍 |
| 配额隔离 | 同一集群,按业务线配置 quota | 限制但不完全隔离 | 需要配额管理工具 |
| 主题隔离 | 同一集群,不同业务线用不同 topic | 逻辑隔离 | broker 层面资源仍共享 |
推荐做法:配额隔离 + 主题隔离组合。配额限制每个业务线的吞吐和存储,主题隔离保证消费者组之间不互相干扰。
配额配置示例:
| 业务线 | 生产 quota | 消费 quota | 存储配额 |
|---|---|---|---|
| 订单 | 5MB/s | 10MB/s | 100GB |
| 通知 | 1MB/s | 1MB/s | 20GB |
| 报表 | 500KB/s | 500KB/s | 50GB |
当订单业务线突发促销时,配额会限制其写入速率,防止压垮集群。订单业务线的 SLO 可能暂时降级,但通知和报表业务线的 SLO 不受影响。
容量压力测试的 Java 模拟
import java.util.*;
public class CapacityStressTest {
static final int INITIAL_CONSUMERS = 3;
static final int MAX_CONSUMERS = 10;
static final double TARGET_FRESHNESS_MS = 5000;
static double simulateFreshness(int consumers, double msgRate) {
double processingRate = consumers * 5000; // 每个消费者 5000 msg/s
double ratio = msgRate / processingRate;
return ratio > 1 ? ratio * TARGET_FRESHNESS_MS : 100; // lag 增长时新鲜度恶化
}
public static void main(String[] args) {
double[] msgRates = {5000, 10000, 15000, 20000, 25000};
System.out.println("=== 容量压力测试 ===");
System.out.println("目标新鲜度: " + TARGET_FRESHNESS_MS + " ms");
System.out.println();
for (double rate : msgRates) {
System.out.println("消息速率: " + rate + " msg/s");
for (int c = INITIAL_CONSUMERS; c <= MAX_CONSUMERS; c += 3) {
double freshness = simulateFreshness(c, rate);
String status = freshness < TARGET_FRESHNESS_MS ? "✓" : "✗";
System.out.println(" 消费者=" + c + ", 新鲜度=" + String.format("%.0f", freshness) + " ms " + status);
}
System.out.println();
}
}
}
预期输出:
=== 容量压力测试 ===
目标新鲜度: 5000 ms
消息速率: 5000 msg/s
消费者=3, 新鲜度=100 ms ✓
消费者=6, 新鲜度=100 ms ✓
消费者=9, 新鲜度=100 ms ✓
消息速率: 10000 msg/s
消费者=3, 新鲜度=3333 ms ✓
消费者=6, 新鲜度=100 ms ✓
消费者=9, 新鲜度=100 ms ✓
消息速率: 15000 msg/s
消费者=3, 新鲜度=10000 ms ✗
消费者=6, 新鲜度=5000 ms ✓
消费者=9, 新鲜度=100 ms ✓
消息速率: 20000 msg/s
消费者=3, 新鲜度=13333 ms ✗
消费者=6, 新鲜度=6667 ms ✗
消费者=9, 新鲜度=2222 ms ✓
消息速率: 25000 msg/s
消费者=3, 新鲜度=16667 ms ✗
消费者=6, 新鲜度=8333 ms ✗
消费者=9, 新鲜度=2778 ms ✓
测试结论:
- 3 个消费者只能支撑 5000 msg/s(超出后新鲜度恶化)
- 6 个消费者可以支撑到 10000 msg/s
- 9 个消费者可以支撑到 25000 msg/s
容量规划不是静态的。业务增长时,需要定期跑压力测试,确定当前配置能支撑的最大速率。超过阈值时,提前扩容而不是等问题发生后再扩容。
本节判据
- SLO 必须量化且可测量,不能用「尽量快」「基本稳定」等模糊表述。
- SLI 选 p99 或 p99.9 而不是均值,因为对数正态分布的长尾会拉高均值。
- 延迟和新鲜度是两个不同的维度,低延迟不等于高新鲜度。
- 容量规划的四条联动规则:batch 上限由延迟决定、消费者数由新鲜度决定、分区数由吞吐决定、保留期由成本决定。
- 阈值设计分三层:告警(80% SLO)、降级(120% SLO)、熔断(200% SLO)。
- 本机实测:10K 样本延迟 p50=12ms, p99=98ms, p99.9=179ms;新鲜度检测 inventory-sync 超标 13 秒。
- SLO 需要每月回顾达成率、每季度评审本身是否仍适用。
- 容量扩容或缩容前,必须先确认对现有 SLO 的影响。
阅读导航:上一节 14.3 架构评审清单 | 下一节 15.2 故障演练剧本
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。