我们团队在引入 Scrum 后,交付速度确实提升了。但一个幽灵般的问题开始浮现:稳定性。新功能在 Sprint 评审会上看起来光鲜亮丽,但线上服务的 PagerDuty 告警却越来越频繁。产品负责人关心的是功能点,而工程师则疲于奔命地救火。我们在 Sprint 计划会上争论不休,一方要“冲刺”,另一方要“还债”,但这种讨论缺乏客观的数据支撑。我们都听说过 Google SRE 的“错误预算”(Error Budget)概念,但它对我们来说一直是个飘在天上的理论。问题归结为一点:如何让团队直观、量化地感知到服务的可靠性,并将这种感知融入到 Scrum 流程中?
我们的初步构想是:构建一个内部仪表盘,它不做全功能的监控,只回答两个核心问题:
- 我们核心服务的服务等级目标(SLO)在过去一个滚动周期(例如28天)内的达成率是多少?
- 我们消耗“错误预算”的速度有多快?这个速度是否可持续?
这个仪表盘将成为我们 Sprint 计划会和回顾会上的一个关键数据源。如果错误预算消耗过快,就触发一个明确的团队共识:暂停新功能开发,优先处理稳定性问题。这为工程团队和产品负责人之间关于“可靠性 vs. 功能”的博弈提供了一个量化的决策框架。
技术选型决策很快就清晰了。我们的服务早已容器化并运行在 AWS EKS 上,这为我们提供了标准化的部署和运维环境。监控方面,Prometheus Operator 已经成为我们集群内监控的事实标准,它的声明式配置和强大的 PromQL 查询语言是实现 SLO 计算的基石。对于前端,我们需要快速构建一个专业、数据驱动的界面,Ant Design Pro 框架成熟的图表库和布局能力能让我们专注于业务逻辑而非基础组件。后端服务则需要一个轻量级、高性能的语言来暴露业务指标,Go 语言及其官方的 Prometheus 客户端库是这个场景下的不二之选。
整个系统的架构数据流如下:
graph TD
subgraph "AWS EKS Cluster"
A[Go Microservice] -- Exposes /metrics --> B(Prometheus)
B -- Scrapes Metrics --> A
C[ServiceMonitor CRD] -- Configures --> B
end
subgraph "User Interface"
D[Ant Design Pro Frontend]
end
B -- PromQL Queries via API --> D
E[Scrum Team] -- Views Dashboard in Sprint Meeting --> D
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style D fill:#9f9,stroke:#333,stroke-width:2px
接下来的工作就是将这个构想一步步变为现实。
第一步:为核心服务植入 SLI 指标
要计算 SLO,首先要有服务等级指标(SLI)。SLI 是对服务某方面性能的量化度量。我们选择两个最常见的 SLI:可用性和延迟。
- 可用性 SLI: 成功处理的请求数 / 总请求数。
- 延迟 SLI: 延迟低于阈值(例如 500ms)的请求数 / 总请求数。
我们用 Go 编写一个简单的订单服务作为示例。在真实项目中,这些指标会嵌入到你的核心业务微服务中。这里的关键是使用 prometheus/client_golang 库来创建和暴露自定义指标。
// main.go
package main
import (
"log"
"math/rand"
"net/http"
"os"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// http_requests_total: 跟踪请求总数,按方法、路径和状态码分类
// 这是可用性 SLI 的基础
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "order_service_http_requests_total",
Help: "Total number of HTTP requests for the order service.",
},
[]string{"method", "path", "code"},
)
// http_request_duration_seconds: 跟踪请求延迟
// 这是延迟 SLI 的基础,使用 Histogram 类型可以计算分位数
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "order_service_http_request_duration_seconds",
Help: "HTTP request latency distributions for the order service.",
Buckets: []float64{0.1, 0.2, 0.5, 1, 2.5, 5, 10}, // 单位:秒
},
[]string{"method", "path"},
)
)
func init() {
// 注册指标,这样 Prometheus 客户端库才能暴露它们
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
// prometheusMiddleware 是一个 HTTP 中间件,用于包裹业务处理器
// 它负责记录请求总数和延迟
func prometheusMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 使用一个自定义的 responseWriter 来捕获状态码
rec := &statusRecorder{ResponseWriter: w, StatusCode: http.StatusOK}
// 调用下一个处理器
next.ServeHTTP(rec, r)
duration := time.Since(start).Seconds()
path := r.URL.Path
// 记录延迟
httpRequestDuration.WithLabelValues(r.Method, path).Observe(duration)
// 记录请求总数和状态码
httpRequestsTotal.WithLabelValues(r.Method, path, http.StatusText(rec.StatusCode)).Inc()
log.Printf("request: %s %s, status: %d, duration: %fs", r.Method, path, rec.StatusCode, duration)
})
}
// statusRecorder 用于捕获 HTTP 状态码
type statusRecorder struct {
http.ResponseWriter
StatusCode int
}
func (rec *statusRecorder) WriteHeader(statusCode int) {
rec.StatusCode = statusCode
rec.ResponseWriter.WriteHeader(statusCode)
}
// 模拟的订单处理接口
func orderHandler(w http.ResponseWriter, r *http.Request) {
// 模拟处理延迟
time.Sleep(time.Duration(rand.Intn(800)) * time.Millisecond)
// 模拟随机失败
if rand.Intn(100) < 5 { // 5% 的失败率
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal Server Error"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Order processed successfully"))
}
func main() {
// 从环境变量获取服务端口,这是云原生应用的最佳实践
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
// 应用 Prometheus 中间件
finalOrderHandler := prometheusMiddleware(http.HandlerFunc(orderHandler))
mux.Handle("/api/orders", finalOrderHandler)
// 暴露 /metrics 端点供 Prometheus 抓取
mux.Handle("/metrics", promhttp.Handler())
log.Printf("Order service starting on port %s", port)
if err := http.ListenAndServe(":"+port, mux); err != nil {
log.Fatalf("could not start server: %v", err)
}
}
这个 Go 服务非常直接。核心在于 prometheusMiddleware,它像一个洋葱皮一样包裹了我们的业务逻辑 orderHandler。无论业务逻辑成功还是失败,这个中间件都能精确地记录下请求的方法、路径、状态码和处理时长,并将这些数据喂给 httpRequestsTotal 和 httpRequestDuration 这两个 Prometheus 指标。
第二步:在 EKS 上部署并暴露指标
现在我们需要将这个服务部署到 EKS 集群,并让 Prometheus Operator 能够自动发现并抓取它的 /metrics 端点。这需要三个 Kubernetes 资源:Deployment、Service 和 ServiceMonitor。
一个常见的错误是手动在 Prometheus 配置文件中添加抓取目标。在 Kubernetes 环境中,正确的方式是使用 Prometheus Operator 提供的 CRD(Custom Resource Definition),例如 ServiceMonitor,来实现服务的自动发现。
# order-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
# 替换为你自己的镜像仓库地址
image: your-registry/order-service:v1.0.0
ports:
- name: http
containerPort: 8080
env:
- name: PORT
value: "8080"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
---
# order-service-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: order-service
labels:
app: order-service
# 这里的 annotations 是关键,它告诉 Prometheus Operator 这个 Service 可以被抓取
annotations:
prometheus.io/scrape: 'true'
prometheus.io/path: '/metrics'
prometheus.io/port: 'http'
spec:
selector:
app: order-service
ports:
- name: http
port: 80
targetPort: 8080
---
# order-service-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: order-service-monitor
# 确保 ServiceMonitor 和 Prometheus Operator 在同一个 namespace
# 或者使用 label selector 来匹配
labels:
release: prometheus # 这是 Prometheus Helm chart 的默认 label
spec:
selector:
matchLabels:
app: order-service # 匹配上面定义的 Service
endpoints:
- port: http
path: /metrics
interval: 15s # 每15秒抓取一次
将这些 YAML 文件应用到 EKS 集群后,Prometheus Operator 会检测到新的 ServiceMonitor 资源。它会根据 selector 找到 order-service 这个 Service,再通过 Service 找到后端的 Pods,最后配置 Prometheus 开始定期抓取这些 Pod 的 /metrics 端点。
第三步:用 PromQL 定义和计算 SLO
这是整个系统的核心大脑。我们需要编写 PromQL 来将原始的 SLI 指标转化为 SLO 达成率。假设我们的 SLO 定义如下:
- 可用性 SLO: 28天内,99.9% 的
/api/orders请求成功(非 5xx 状态码)。 - 延迟 SLO: 28天内,99% 的
/api/orders请求延迟在 500ms 以下。
可用性 SLO 查询
# 计算 28 天内的成功请求总数
sum(rate(order_service_http_requests_total{path="/api/orders", code!~"5.."}[28d]))
/
# 计算 28 天内的总请求数
sum(rate(order_service_http_requests_total{path="/api/orders"}[28d]))
这个查询的精妙之处在于 rate() 函数。它计算的是一个时间窗口内计数器增长的平均速率。直接用 sum() 会因为 Pod 重启等原因导致计数器重置而不准确。使用 rate() 可以平滑地处理这些问题。我们将非 5xx 状态码的请求速率相加,除以总请求速率,就得到了可用性 SLI。
延迟 SLO 查询
延迟的计算要复杂一些,因为它依赖于 Histogram 指标类型。
# 计算 28 天内延迟小于等于 0.5s 的请求总数
sum(rate(order_service_http_request_duration_seconds_bucket{path="/api/orders", le="0.5"}[28d]))
/
# 计算 28 天内的总请求数
sum(rate(order_service_http_request_duration_seconds_count{path="/api/orders"}[28d]))
这里的 le="0.5" 是 Histogram 的魔法。_bucket{le="<value>"} 这个指标表示延迟小于等于 <value> 的请求总数。所以我们直接取 le="0.5" 的桶的增长率,就得到了满足延迟要求的请求速率。
错误预算消耗率查询
错误预算的计算比 SLO 本身更能指导行动。它告诉我们“犯错”的速度。一个常用的指标是“错误预算消耗速度”,如果这个速度大于1,意味着我们正在以不可持续的速度消耗预算,并将在周期结束前耗尽。
# 可用性错误预算消耗率(基于过去1小时的消耗速度,推算到28天周期)
# 目标是 99.9%,所以允许的错误率是 0.1% (0.001)
(
# 过去1小时的错误率
sum(rate(order_service_http_requests_total{path="/api/orders", code=~"5.."}[1h]))
/
sum(rate(order_service_http_requests_total{path="/api/orders"}[1h]))
)
/
# 允许的错误率
(1 - 0.999)
这个查询结果如果为 2,就意味着在过去一小时里,我们的服务出错速度是“允许”速度的两倍。这是一个非常强烈的信号,表明需要立即关注服务稳定性。
第四步:Ant Design Pro 可视化
前端的目标是清晰地展示 SLO 状态和错误预算消耗。我们将创建一个新的页面 /slo-dashboard。
首先,我们需要一个服务来与 Prometheus API 通信。
// src/services/prometheus.ts
import { request } from 'umi';
const PROMETHEUS_API_URL = 'http://your-prometheus-server/api/v1';
// 查询瞬时数据
export async function query(promql: string) {
return request<{ status: string; data: any }>(`${PROMETHEUS_API_URL}/query`, {
method: 'GET',
params: { query: promql },
});
}
// 查询范围数据
export async function queryRange(promql: string, start: number, end: number, step: number) {
return request<{ status:string; data: any }>(`${PROMETHEUS_API_URL}/query_range`, {
method: 'GET',
params: {
query: promql,
start, // Unix timestamp
end, // Unix timestamp
step, // in seconds
}
});
}
接下来是我们的仪表盘组件。我们将使用 Ant Design Pro 的 Statistic 和 Gauge 图表来显示当前的 SLO,使用 Line 图表来显示错误预算的消耗趋势。
// src/pages/SLODashboard/index.tsx
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { Card, Row, Col, Statistic } from 'antd';
import { Gauge, Line } from '@ant-design/plots';
import { query } from '@/services/prometheus';
import moment from 'moment';
const SLODashboard: React.FC = () => {
const [availabilitySLO, setAvailabilitySLO] = useState(0);
const [latencySLO, setLatencySLO] = useState(0);
const [errorBudgetBurnRate, setErrorBudgetBurnRate] = useState(0);
const fetchSLOData = async () => {
// 这里的 PromQL 查询应该与上面定义的保持一致
const availabilityQuery = 'sum(rate(order_service_http_requests_total{path="/api/orders", code!~"5.."}[28d])) / sum(rate(order_service_http_requests_total{path="/api/orders"}[28d])) * 100';
const latencyQuery = 'sum(rate(order_service_http_request_duration_seconds_bucket{path="/api/orders", le="0.5"}[28d])) / sum(rate(order_service_http_request_duration_seconds_count{path="/api/orders"}[28d])) * 100';
const burnRateQuery = '(sum(rate(order_service_http_requests_total{path="/api/orders", code=~"5.."}[1h])) / sum(rate(order_service_http_requests_total{path="/api/orders"}[1h]))) / (1 - 0.999)';
try {
const [availRes, latRes, burnRes] = await Promise.all([
query(availabilityQuery),
query(latencyQuery),
query(burnRateQuery),
]);
if (availRes.status === 'success' && availRes.data.result.length > 0) {
setAvailabilitySLO(parseFloat(availRes.data.result[0].value[1]));
}
if (latRes.status === 'success' && latRes.data.result.length > 0) {
setLatencySLO(parseFloat(latRes.data.result[0].value[1]));
}
if (burnRes.status === 'success' && burnRes.data.result.length > 0) {
setErrorBudgetBurnRate(parseFloat(burnRes.data.result[0].value[1]));
}
} catch (error) {
console.error("Failed to fetch SLO data:", error);
// 在真实项目中,这里应该有更完善的错误处理
}
};
useEffect(() => {
fetchSLOData();
const interval = setInterval(fetchSLOData, 60000); // 每分钟刷新一次
return () => clearInterval(interval);
}, []);
const gaugeConfig = (percent: number, title: string) => ({
percent: percent / 100,
range: {
color: percent > 99.9 ? '#30BF78' : (percent > 99.5 ? '#FAAD14' : '#F4664A'),
},
indicator: {
pointer: { style: { stroke: '#D0D0D0' } },
pin: { style: { stroke: '#D0D0D0' } },
},
axis: {
label: { formatter: (v: any) => Number(v) * 100 },
subTickLine: { count: 3 },
},
statistic: {
content: {
formatter: ({ percent }: any) => `${(percent * 100).toFixed(3)}%`,
style: { fontSize: '36px', lineHeight: 1 },
},
title: {
content: title,
},
},
});
return (
<PageContainer>
<Row gutter={24}>
<Col span={8}>
<Card title="可用性 SLO (28天滚动)">
<Gauge {...gaugeConfig(availabilitySLO, 'Availability (99.9%)')} height={200} />
</Card>
</Col>
<Col span={8}>
<Card title="延迟 SLO (28天滚动)">
<Gauge {...gaugeConfig(latencySLO, 'Latency < 500ms (99%)')} height={200} />
</Card>
</Col>
<Col span={8}>
<Card title="错误预算消耗速度">
<Statistic
title="当前消耗率 (基于过去1小时)"
value={errorBudgetBurnRate}
precision={2}
valueStyle={{ color: errorBudgetBurnRate > 1 ? '#cf1322' : '#3f8600' }}
suffix="x"
prefix={errorBudgetBurnRate > 1 ? "🔥" : "✅"}
/>
</Card>
</Col>
</Row>
{/* 可以在这里添加一个 Line chart 来展示错误预算消耗的历史趋势 */}
</PageContainer>
);
};
export default SLODashboard;
这段 React 代码通过轮询 Prometheus API,获取计算好的 SLO 数据,并使用 Ant Design Plots 的 Gauge 组件和 Statistic 组件进行展示。当错误预算消耗率超过 1 时,界面会显示一个醒目的红色火焰图标,这在 Sprint 会议上能立刻抓住所有人的注意力。
当前方案的局限性与未来展望
我们成功地将抽象的 SLO 和错误预算概念,转化为了一个具体的、可交互的工程产品。它为我们的 Scrum 流程注入了数据驱动的决策能力。然而,这个方案并非银弹。
首先,当前的 SLO 计算是针对单个服务的。在一个复杂的微服务系统中,定义面向用户的端到端 SLO 需要聚合来自多个服务的数据,这会极大地增加 PromQL 的复杂性,甚至可能需要借助流处理引擎进行预计算。
其次,我们只覆盖了可用性和延迟。对于某些系统,“数据正确性”可能是更重要的 SLI。如何为正确性定义 SLI 并通过指标进行度量,是一个更具挑战性的问题,通常需要应用层进行更深度的埋点。
最后,这个仪表盘目前是被动展示。未来的迭代方向可以是主动推送,例如,当错误预算消耗率持续高于某个阈值时,自动在团队的 Slack 频道发送警告,甚至可以与 Jira 集成,自动创建高优先级的技术债任务项,从而形成一个完整的从监控到行动的闭环。