mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1344 字
4 分钟
可观测性驱动开发
2025-11-27

凌晨 3 点,P99 延迟飙升,你打开 Grafana 看指标、查 Jaeger 追踪、搜 Loki 日志——折腾 40 分钟才定位到根因。但如果当初写代码时,那个错误路径就输出了结构化日志、那个关键操作就记录了耗时指标、那个跨服务调用就传播了 TraceID——排障时间可能从 40 分钟缩短到 40 秒。可观测性驱动开发(Observability-Driven Development, ODD)的核心主张:可观测性不是运维的事后补救,而是开发时的设计约束。

一、什么是可观测性驱动开发#

1.1 从运维可观测性到开发可观测性#

传统观念:可观测性是运维的事——部署 Prometheus、配置 Grafana、设置告警。开发只管写功能代码。但现实是:如果代码不可观测,再好的工具也救不了你

graph TB subgraph "传统开发流程" CODE["写代码"] --> DEPLOY["部署"] DEPLOY --> ALERT["告警触发"] ALERT --> DEBUG["排障<br/>缺少信号"] end subgraph "可观测性驱动开发" OCODE["写可观测代码"] --> ODEPLOY["部署"] ODEPLOY --> OALERT["告警触发"] OALERT --> ODEBUG["排障<br/>信号充足"] OCODE --> REVIEW["代码审查<br/>可观测性检查"] end style DEBUG fill:#ffcdd2,stroke:#c62828 style ODEBUG fill:#c8e6c9,stroke:#2e7d32
维度传统开发可观测性驱动开发
日志log.Println("error")结构化日志 + 上下文传播
错误return err结构化错误 + 错误链 + 元数据
指标业务指标 + RED + USE
追踪Span 注解 + 关键路径标记
代码审查只查功能同时查可观测性

1.2 ODD 的核心原则#

  1. 编写可调试的代码:每个关键操作都应该留下可追踪的信号
  2. 结构化优于非结构化:机器可解析的信号远比人类可读的文本有用
  3. 业务语义优于技术细节order.created=1http.200=1 更有意义
  4. 左移优于右移:在开发阶段嵌入可观测性,而非部署后补救

二、编写可观测代码#

2.1 结构化日志#

// 传统日志:无法搜索、无法关联
log.Printf("user %d created order %d", userID, orderID)
// 结构化日志:可搜索、可关联、可解析
logger.Info("order created",
zap.Int64("user_id", userID),
zap.Int64("order_id", orderID),
zap.String("trace_id", traceID),
zap.Duration("duration", elapsed),
)
# Python 结构化日志
import structlog
logger = structlog.get_logger()
# 传统日志
logger.info(f"Processing order {order_id} for user {user_id}")
# 结构化日志
logger.info("order.processing",
order_id=order_id,
user_id=user_id,
items=len(items),
total_amount=total,
trace_id=trace_id,
)

2.2 Span 注解#

// 在关键操作上添加 Span 注解
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
ctx, span := otel.Tracer("order").Start(ctx, "CreateOrder")
defer span.End()
// 添加业务属性
span.SetAttributes(
attribute.Int64("user.id", req.UserID),
attribute.Int("items.count", len(req.Items)),
attribute.String("currency", req.Currency),
)
// 步骤 1:验证库存
ctx, validateSpan := otel.Tracer("order").Start(ctx, "ValidateInventory")
err := s.inventory.Validate(ctx, req.Items)
validateSpan.End()
if err != nil {
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
return nil, err
}
// 步骤 2:计算价格
ctx, priceSpan := otel.Tracer("order").Start(ctx, "CalculatePrice")
total := s.pricing.Calculate(ctx, req.Items)
priceSpan.SetAttributes(attribute.Float64("order.total", total))
priceSpan.End()
// 步骤 3:持久化
order, err := s.repo.Save(ctx, &Order{
UserID: req.UserID,
Items: req.Items,
Total: total,
})
if err != nil {
span.RecordError(err)
return nil, err
}
span.SetAttributes(attribute.Int64("order.id", order.ID))
return order, nil
}
Note

Span 注解不是”加日志”——它是给分布式追踪系统提供结构化数据。当你在 Jaeger 中查看一个 Trace 时,每个 Span 的属性都会显示出来,让你无需看日志就能理解请求的完整路径。

2.3 指标仪器化#

// RED 方法:Rate, Errors, Duration
var (
orderCreated = otel.Meter("order").Int64Counter("order.created",
metric.WithDescription("Number of orders created"),
)
orderDuration = otel.Meter("order").Float64Histogram("order.duration",
metric.WithDescription("Order creation duration"),
metric.WithUnit("ms"),
)
orderErrors = otel.Meter("order").Int64Counter("order.errors",
metric.WithDescription("Number of order creation errors"),
)
)
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
start := time.Now()
order, err := s.createOrderInternal(ctx, req)
// 记录指标
elapsed := time.Since(start).Milliseconds()
orderDuration.Record(ctx, float64(elapsed),
metric.WithAttributes(attribute.String("currency", req.Currency)),
)
if err != nil {
orderErrors.Add(ctx, 1,
metric.WithAttributes(attribute.String("error.type", classifyError(err))),
)
} else {
orderCreated.Add(ctx, 1)
}
return order, err
}

三、结构化错误处理#

3.1 错误链与上下文#

// 传统错误处理:丢失上下文
func (s *Service) Process(ctx context.Context, id int64) error {
order, err := s.repo.GetOrder(ctx, id)
if err != nil {
return err // "sql: no rows in result set" — 哪个订单?哪个查询?
}
// ...
}
// 结构化错误:保留完整上下文
func (s *Service) Process(ctx context.Context, id int64) error {
order, err := s.repo.GetOrder(ctx, id)
if err != nil {
return fmt.Errorf("get order %d: %w", id, err)
// "get order 12345: sql: no rows in result set" — 现在知道是哪个订单了
}
// ...
}

3.2 自定义错误类型#

// 业务错误类型
type BusinessError struct {
Code string // 错误码:"ORDER_NOT_FOUND"
Message string // 人类可读消息
Metadata map[string]interface{} // 结构化元数据
Cause error // 原始错误
}
func (e *BusinessError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *BusinessError) Unwrap() error {
return e.Cause
}
// 使用示例
func (s *OrderService) GetOrder(ctx context.Context, id int64) (*Order, error) {
order, err := s.repo.GetOrder(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, &BusinessError{
Code: "ORDER_NOT_FOUND",
Message: fmt.Sprintf("Order %d not found", id),
Metadata: map[string]interface{}{
"order_id": id,
"user_id": ctx.Value(userIDKey),
},
Cause: err,
}
}
return nil, &BusinessError{
Code: "ORDER_FETCH_FAILED",
Message: "Failed to fetch order",
Metadata: map[string]interface{}{"order_id": id},
Cause: err,
}
}
return order, nil
}

3.3 错误与可观测性集成#

// 错误处理器:自动记录到日志和追踪
func HandleError(ctx context.Context, err error) {
var bizErr *BusinessError
if errors.As(err, &bizErr) {
// 结构化日志
logger.Error("business error",
zap.String("code", bizErr.Code),
zap.String("message", bizErr.Message),
zap.Any("metadata", bizErr.Metadata),
)
// 追踪记录
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, bizErr.Code)
span.SetAttributes(
attribute.String("error.code", bizErr.Code),
attribute.String("error.message", bizErr.Message),
)
span.RecordError(bizErr)
// 指标记录
errorCounter.Add(ctx, 1,
metric.WithAttributes(attribute.String("error.code", bizErr.Code)),
)
}
}
错误处理模式传统ODD
错误创建errors.New("msg")BusinessError{Code, Message, Metadata}
错误传播return errfmt.Errorf("context: %w", err)
错误日志log.Println(err)结构化日志 + 错误码 + 元数据
错误追踪Span RecordError + 错误码属性
错误指标按 error.code 分类的 Counter

四、业务指标设计#

4.1 RED 方法#

RED 方法适用于请求驱动的服务(HTTP API、gRPC 服务):

指标含义示例
Rate请求速率http_requests_total{method="POST", path="/orders"}
Errors错误率http_errors_total{method="POST", path="/orders", code="500"}
Duration请求延迟分布http_request_duration_seconds{method="POST", path="/orders"}

4.2 USE 方法#

USE 方法适用于资源(CPU、内存、磁盘、网络):

指标含义示例
Utilization资源使用率node_cpu_seconds_total{mode="user"}
Saturation资源饱和度(排队程度)node_load1 / node_cpu_seconds_total
Errors资源错误率node_disk_io_errors_total

4.3 业务指标#

// 业务指标:比技术指标更有价值
var (
// 订单指标
ordersCreated = meter.Int64Counter("business.orders.created")
ordersCompleted = meter.Int64Counter("business.orders.completed")
orderValue = meter.Float64Histogram("business.orders.value",
metric.WithUnit("USD"),
)
// 支付指标
paymentsProcessed = meter.Int64Counter("business.payments.processed")
paymentLatency = meter.Float64Histogram("business.payments.latency",
metric.WithUnit("ms"),
)
// 用户指标
activeUsers = meter.Int64UpDownCounter("business.users.active")
userRegistrations = meter.Int64Counter("business.users.registered")
)
// 在业务逻辑中记录
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
order, err := s.createOrder(ctx, req)
if err != nil {
return nil, err
}
// 记录业务指标
ordersCreated.Add(ctx, 1,
attribute.String("market", req.Market),
attribute.String("channel", req.Channel),
)
orderValue.Record(ctx, float64(order.Total),
attribute.String("currency", order.Currency),
)
return order, nil
}
# 业务指标告警规则
# 订单创建速率下降 50%
- alert: OrderRateDrop
expr: |
rate(business_orders_created_total[5m])
<
0.5 * rate(business_orders_created_total[5m] offset 1h)
for: 10m
# 支付成功率低于 99%
- alert: PaymentSuccessRateLow
expr: |
rate(business_payments_processed_total{status="success"}[5m])
/
rate(business_payments_processed_total[5m])
< 0.99
for: 5m
# 平均订单金额异常
- alert: AbnormalOrderValue
expr: |
histogram_avg(business_orders_value)
>
2 * histogram_avg(business_orders_value offset 1d)
for: 15m
Tip

业务指标的最大优势:不需要理解系统内部实现就能判断系统是否健康。产品经理看”订单创建速率”比看”HTTP 500 错误率”更直观,也更有业务意义。

五、可观测性左移#

5.1 从部署后到开发中#

graph LR subgraph "传统:可观测性右移" D1["设计"] --> C1["编码"] --> T1["测试"] --> R1["发布"] --> O1["运维<br/>可观测性在这里"] end subgraph "ODD:可观测性左移" D2["设计<br/>+ 可观测性设计"] --> C2["编码<br/>+ 仪器化"] --> T2["测试<br/>+ 可观测性测试"] --> R2["发布<br/>+ 告警配置"] --> O2["运维<br/>+ 持续优化"] end style O1 fill:#ffcdd2,stroke:#c62828 style D2 fill:#c8e6c9,stroke:#2e7d32 style C2 fill:#c8e6c9,stroke:#2e7d32 style T2 fill:#c8e6c9,stroke:#2e7d32

5.2 代码审查检查清单#

## 可观测性代码审查清单
### 日志
- [ ] 使用结构化日志(非 fmt.Println)
- [ ] 包含 trace_id 和 span_id
- [ ] 日志级别正确(Debug/Info/Warn/Error)
- [ ] 不包含敏感信息(密码、token)
### 错误
- [ ] 错误包含上下文信息(哪个实体、哪个操作)
- [ ] 使用 %w 包装错误(保留错误链)
- [ ] 定义业务错误码(非通用 "internal error")
- [ ] 错误记录到追踪系统
### 指标
- [ ] 关键操作有 Counter(调用次数)
- [ ] 关键操作有 Histogram(延迟分布)
- [ ] 指标标签基数合理(< 10 个值)
- [ ] 有业务指标(非只有 HTTP 指标)
### 追踪
- [ ] 关键函数有 Span
- [ ] Span 包含业务属性
- [ ] 上下文正确传播(ctx 传递)
- [ ] 跨服务调用有追踪传播

5.3 测试可观测性#

// 测试:验证关键操作产生正确的信号
func TestCreateOrder_Observability(t *testing.T) {
// 设置测试追踪提供者
provider := tracetest.NewTracerProvider()
tracer := provider.GetTracer("test")
// 执行操作
ctx, span := tracer.Start(context.Background(), "test")
_, err := service.CreateOrder(ctx, validRequest)
span.End()
// 验证产生了正确的 Span
spans := provider.GetSpans()
assert.Len(t, spans, 3) // test + CreateOrder + Save
// 验证 CreateOrder Span 的属性
orderSpan := spans[1]
assert.Equal(t, "CreateOrder", orderSpan.Name)
assert.Equal(t, int64(12345), orderSpan.Attributes["user.id"])
assert.Equal(t, 3, orderSpan.Attributes["items.count"])
// 验证错误被记录
if err != nil {
assert.Equal(t, codes.Error, orderSpan.Status.Code)
assert.Equal(t, "ORDER_NOT_FOUND", orderSpan.Attributes["error.code"])
}
}

六、反模式#

6.1 日志反模式#

反模式问题修复
log.Println(err)非结构化,无法搜索结构化日志 + 错误码
log.Printf("user %d", id)无 trace_id,无法关联传播 trace_id
每行一个 log.Info日志洪水,淹没有用信息合并为一条结构化日志
日志包含密码/token安全风险脱敏处理

6.2 指标反模式#

反模式问题修复
http_requests_total{user_id="12345"}高基数,内存爆炸移除 user_id 标签
gauge 记录请求延迟丢失分布信息使用 histogram
每个函数一个 Counter指标爆炸按端点/操作分组

6.3 追踪反模式#

反模式问题修复
不传播 ctx追踪断裂所有函数签名包含 ctx
Span 嵌套错误追踪树不正确使用 ctx, span := tracer.Start(ctx, ...)
Span 名称含变量追踪不可聚合Span 名称用固定字符串,变量放属性
Warning

指标基数爆炸是最常见的可观测性反模式。一个标签如果有 10000 个不同值,就会产生 10000 个时间序列。Prometheus 每个时间序列约 3KB 内存,10000 个就是 30MB。多个高基数标签组合后,内存使用指数级增长。

flowchart LR CODE["编写代码"] --> INSTR["嵌入仪器化<br/>Span/Metric/Log"] --> TEST["测试验证<br/>可观测性契约"] TEST --> DEPLOY["部署"] --> OBSERVE["观察生产<br/>指标/追踪/日志"] OBSERVE --> ALERT["告警触发"] --> FIX["修复优化"] --> CODE style INSTR fill:#bbdefb,stroke:#1565c0 style OBSERVE fill:#c8e6c9,stroke:#2e7d32
graph TB subgraph 仪器化层["仪器化层"] SPAN["Span 追踪"] METRIC["Metric 指标"] LOG2["Log 日志"] end subgraph 采集层["采集层"] OTEL["OTel SDK"] COLLECTOR["OTel Collector"] end SPAN --> OTEL --> COLLECTOR METRIC --> OTEL LOG2 --> OTEL style 仪器化层 fill:#e8eaf6,stroke:#283593 style 采集层 fill:#e0f2f1,stroke:#00695c

七、总结#

ODD 实践开发阶段收益
结构化日志编码日志可搜索、可关联
Span 注解编码追踪可视化、关键路径可见
业务指标编码业务健康度实时可见
结构化错误编码错误可分类、可统计
可观测性代码审查代码审查防止不可观测代码上线
可观测性测试测试验证信号正确性
告警配置发布上线即可观测
Tip

可观测性驱动开发的本质是把排障能力内置到代码中。当你写代码时问自己:“如果这个操作出问题,我能从日志/指标/追踪中找到根因吗?“如果答案是否,那就需要添加可观测性。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

可观测性驱动开发
https://blog.souloss.com/posts/observability/observability-driven-development/
作者
Souloss
发布于
2025-11-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时