凌晨 3 点,P99 延迟飙升,你打开 Grafana 看指标、查 Jaeger 追踪、搜 Loki 日志——折腾 40 分钟才定位到根因。但如果当初写代码时,那个错误路径就输出了结构化日志、那个关键操作就记录了耗时指标、那个跨服务调用就传播了 TraceID——排障时间可能从 40 分钟缩短到 40 秒。可观测性驱动开发(Observability-Driven Development, ODD)的核心主张:可观测性不是运维的事后补救,而是开发时的设计约束。
一、什么是可观测性驱动开发
1.1 从运维可观测性到开发可观测性
传统观念:可观测性是运维的事——部署 Prometheus、配置 Grafana、设置告警。开发只管写功能代码。但现实是:如果代码不可观测,再好的工具也救不了你。
| 维度 | 传统开发 | 可观测性驱动开发 |
|---|---|---|
| 日志 | log.Println("error") | 结构化日志 + 上下文传播 |
| 错误 | return err | 结构化错误 + 错误链 + 元数据 |
| 指标 | 无 | 业务指标 + RED + USE |
| 追踪 | 无 | Span 注解 + 关键路径标记 |
| 代码审查 | 只查功能 | 同时查可观测性 |
1.2 ODD 的核心原则
- 编写可调试的代码:每个关键操作都应该留下可追踪的信号
- 结构化优于非结构化:机器可解析的信号远比人类可读的文本有用
- 业务语义优于技术细节:
order.created=1比http.200=1更有意义 - 左移优于右移:在开发阶段嵌入可观测性,而非部署后补救
二、编写可观测代码
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}Span 注解不是”加日志”——它是给分布式追踪系统提供结构化数据。当你在 Jaeger 中查看一个 Trace 时,每个 Span 的属性都会显示出来,让你无需看日志就能理解请求的完整路径。
2.3 指标仪器化
// RED 方法:Rate, Errors, Durationvar ( 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 err | fmt.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业务指标的最大优势:不需要理解系统内部实现就能判断系统是否健康。产品经理看”订单创建速率”比看”HTTP 500 错误率”更直观,也更有业务意义。
五、可观测性左移
5.1 从部署后到开发中
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 名称用固定字符串,变量放属性 |
指标基数爆炸是最常见的可观测性反模式。一个标签如果有 10000 个不同值,就会产生 10000 个时间序列。Prometheus 每个时间序列约 3KB 内存,10000 个就是 30MB。多个高基数标签组合后,内存使用指数级增长。
七、总结
| ODD 实践 | 开发阶段 | 收益 |
|---|---|---|
| 结构化日志 | 编码 | 日志可搜索、可关联 |
| Span 注解 | 编码 | 追踪可视化、关键路径可见 |
| 业务指标 | 编码 | 业务健康度实时可见 |
| 结构化错误 | 编码 | 错误可分类、可统计 |
| 可观测性代码审查 | 代码审查 | 防止不可观测代码上线 |
| 可观测性测试 | 测试 | 验证信号正确性 |
| 告警配置 | 发布 | 上线即可观测 |
可观测性驱动开发的本质是把排障能力内置到代码中。当你写代码时问自己:“如果这个操作出问题,我能从日志/指标/追踪中找到根因吗?“如果答案是否,那就需要添加可观测性。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






