30、可靠性测试与度量:单元测试、集成测试、压力测试、故障注入测试、可靠性指标计算、SLA/SLO/SLI
聊到可靠性,很多人第一反应是「代码写得稳不稳」。但说实话,代码写得再漂亮,没经过验证,我心里还是不踏实。我见过太多上线前信心满满、上线后手忙脚乱的案例了。
这一章,咱们就聊聊怎么用测试和度量,把「可靠性」从口号变成可量化、可验证的东西。说白了,就是给系统做一次全面体检,再给它的健康水平打个分。
单元测试:最基础的防线
单元测试,我个人的习惯是把它当作「代码的说明书」。你写一个函数,它的输入输出是什么?边界情况怎么处理?单元测试把这些都写清楚。
为什么要做?因为它是成本最低的 bug 发现方式。一个 bug 在单元测试阶段发现,改几行代码就行;要是流到线上,可能就是 P0 事故了。
- 每个函数/方法都要覆盖正常路径和异常路径
- 测试用例要独立,不依赖外部环境(数据库、网络等)
- 运行速度要快,最好秒级完成
举个例子,一个简单的用户注册函数:
// 用户注册函数
func RegisterUser(username, email string) error {
if username == "" || email == "" {
return ErrInvalidInput
}
// 检查用户名是否已存在
exists, err := db.CheckUsername(username)
if err != nil {
return err
}
if exists {
return ErrUserExists
}
// 创建用户
return db.CreateUser(username, email)
}
对应的单元测试:
func TestRegisterUser(t *testing.T) {
// 测试空用户名
err := RegisterUser("", "test@example.com")
if err != ErrInvalidInput {
t.Errorf("期望 ErrInvalidInput,得到 %v", err)
}
// 测试正常注册
err = RegisterUser("newuser", "new@example.com")
if err != nil {
t.Errorf("期望 nil,得到 %v", err)
}
// 测试重复用户名
err = RegisterUser("existing", "existing@example.com")
if err != ErrUserExists {
t.Errorf("期望 ErrUserExists,得到 %v", err)
}
}
集成测试:把零件组装起来看
单元测试测的是单个零件,集成测试就是把零件组装起来,看看接口能不能对上。我在项目中遇到过好几次,单元测试全绿,一集成就崩——因为两个模块对同一个字段的理解不一样。
集成测试的重点:
- 模块间的接口契约是否一致
- 数据流是否完整(A 模块输出 → B 模块输入)
- 外部依赖(数据库、缓存、消息队列)是否正常工作
举个例子,用户注册后要发欢迎邮件,这就是一个典型的集成点:
func TestUserRegistrationAndEmail(t *testing.T) {
// 模拟数据库和邮件服务
db := setupTestDB()
emailSvc := setupMockEmailService()
svc := NewUserService(db, emailSvc)
// 注册用户
user, err := svc.Register("test@example.com", "password123")
if err != nil {
t.Fatalf("注册失败: %v", err)
}
// 验证用户已存入数据库
savedUser, _ := db.GetUserByID(user.ID)
if savedUser == nil {
t.Error("用户未存入数据库")
}
// 验证邮件已发送
if !emailSvc.WasEmailSent(user.Email, "welcome") {
t.Error("欢迎邮件未发送")
}
}
压力测试:看看系统能扛多少
压力测试,说白了就是「往死里压」,看看系统在极限情况下表现如何。我习惯用三个指标来衡量:
| 指标 | 说明 | 典型目标值 |
|---|---|---|
| QPS(每秒查询数) | 系统能处理的请求量 | 根据业务需求,如 1000 QPS |
| 响应时间(P99) | 99% 的请求在多少毫秒内完成 | < 200ms |
| 错误率 | 请求失败的百分比 | < 0.1% |
压测工具我常用 wrk 和 Locust。举个例子,用 wrk 压一个 API:
# 压测 30 秒,12 个线程,400 个并发连接
wrk -t12 -c400 -d30s http://localhost:8080/api/users
输出结果:
Running 30s test @ http://localhost:8080/api/users
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 45.67ms 12.34ms 256.78ms 85.23%
Req/Sec 723.45 89.12 1.2k 70.45%
216789 requests in 30.00s, 34.56MB read
Requests/sec: 7226.30
Transfer/sec: 1.15MB
看到这个结果,我心里就有数了:这个接口能扛 7000+ QPS,P99 延迟在 100ms 以内,还不错。但如果 P99 超过 500ms,那就得优化了。
故障注入测试:主动搞破坏
故障注入,就是故意让系统出问题,看看它能不能优雅地处理。这招很狠,但很有效。我常用的故障注入手段:
- 网络故障:模拟网络延迟、丢包、断连
- 服务故障:让某个微服务返回 500 或超时
- 资源耗尽:模拟磁盘满、内存不足、CPU 过载
举个例子,用 Chaos Monkey 或 Litmus 工具,注入一个 Redis 故障:
# 使用 Litmus 注入 Redis 故障
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: redis-fault
spec:
appinfo:
appns: "default"
applabel: "app=redis"
appkind: "deployment"
chaosServiceAccount: litmus-admin
experiments:
- name: pod-delete
spec:
components:
env:
- name: TOTAL_CHAOS_DURATION
value: "60" # 故障持续 60 秒
- name: CHAOS_INTERVAL
value: "10" # 每 10 秒删除一个 Pod
然后观察系统行为:
- 是否有降级策略?比如从 Redis 降级到本地缓存
- 恢复后是否能自动重连?
- 数据是否丢失?
可靠性指标计算:用数据说话
测试完了,怎么量化可靠性?我常用这几个指标:
| 指标 | 公式 | 说明 |
|---|---|---|
| MTBF(平均故障间隔) | 总运行时间 / 故障次数 | 系统平均多久出一次故障 |
| MTTR(平均修复时间) | 总故障时间 / 故障次数 | 出故障后平均多久恢复 |
| 可用性(Availability) | MTBF / (MTBF + MTTR) × 100% | 系统正常运行时间的百分比 |
举个例子:
- 一个月内系统故障了 3 次,每次修复平均 30 分钟
- 总运行时间:30 天 × 24 小时 = 720 小时
- MTBF = 720 / 3 = 240 小时(平均 10 天出一次故障)
- MTTR = (30 分钟 × 3) / 3 = 30 分钟
- 可用性 = 240 / (240 + 0.5) × 100% ≈ 99.79%
这个 99.79% 意味着什么?一个月大概有 1 小时左右的不可用时间。如果业务要求 99.99%(四个九),那一个月只能宕机 4 分多钟——差距还是很大的。
SLA / SLO / SLI:把可靠性变成合同
这三个概念,我刚开始也容易搞混。简单来说:
- SLI(服务级别指标):你具体测量什么。比如「API 响应时间」、「错误率」
- SLO(服务级别目标):你希望达到的目标。比如「P99 响应时间 < 200ms」
- SLA(服务级别协议):你和客户签的合同。比如「如果可用性低于 99.9%,赔偿 10% 费用」
举个例子,一个典型的 SLO 定义:
# 服务级别目标(SLO)
apiVersion: "slo.sre/v1"
kind: "ServiceLevelObjective"
metadata:
name: "api-latency-slo"
spec:
description: "API 响应时间 SLO"
service: "user-service"
sli:
metric: "http_request_duration_seconds"
threshold: 0.2 # 200ms
percentile: 99 # P99
target: 99.9% # 99.9% 的请求要在 200ms 内完成
window: 30d # 滚动 30 天统计
然后基于这个 SLO,我们可以设置告警:
# 告警规则:当 SLO 接近违约时告警
- alert: SLOViolationRisk
expr: |
(1 - (sum(rate(http_request_duration_seconds_bucket{le="0.2"}[5m]))
/ sum(rate(http_request_duration_seconds_count[5m]))))
> 0.001 # 错误率超过 0.1%
for: 10m
labels:
severity: critical
annotations:
summary: "API 响应时间 SLO 面临违约风险"
知识体系总览
说了这么多,咱们用一张图把整个可靠性测试与度量的体系串起来:
这张图把整个流程串起来了:从测试方法开始,到测试产出,再到指标计算,最后落到可量化的可靠性保障。每一步都环环相扣,缺一不可。
好了,这一章的内容就到这里。记住,可靠性不是靠嘴说的,是靠测试和度量一步步验证出来的。把上面这些方法用起来,你的系统会越来越稳。