微服务架构带来了灵活性和可扩展性,但也让系统变得更加复杂。当一次请求跨越多个服务时,排查问题就像大海捞针。可观测性(Observability)正是解决这一痛点的关键——它通过日志(Logging)、指标(Metrics) 和链路追踪(Tracing) 三大支柱,帮助我们理解系统的内部状态。
Go 语言凭借其高并发性能和简洁的语法,成为构建微服务的热门选择。本文将带你从零开始,在 Go 微服务中集成这三类可观测性能力,并给出可落地的代码示例。
一、为什么可观测性对微服务至关重要
在单体应用中,我们通常只需要查看一个日志文件就能定位问题。但在微服务中:
- 一个请求可能经过 5 个以上的服务
- 每个服务独立部署、独立伸缩
- 故障可能由网络延迟、下游服务超时或资源竞争引起
没有可观测性,你只能看到“症状”(比如 500 错误),却看不到“病因”。三大支柱各司其职:
| 支柱 | 回答的问题 | 典型工具 |
|---|---|---|
| 日志 | 发生了什么具体事件? | Zap、Logrus、Loki |
| 指标 | 系统整体表现如何? | Prometheus、Grafana |
| 链路追踪 | 请求在哪个环节变慢或失败? | OpenTelemetry、Jaeger |
二、结构化日志:从 fmt.Println 到 Zap
很多 Go 初学者用 fmt.Println 或标准库 log 打日志,但在微服务中,结构化日志(JSON 格式)才是首选。它便于机器解析,能直接接入 ELK 或 Loki。
使用 Uber 的 Zap 库:
package main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("user login",
zap.String("user_id", "u123"),
zap.String("ip", "192.168.1.1"),
zap.Int("status", 200),
)
}
输出为 JSON:
{"level":"info","ts":1690000000,"msg":"user login","user_id":"u123","ip":"192.168.1.1","status":200}
最佳实践:
- 为每个请求生成
request_id,并注入到日志中,方便串联 - 使用
logger.With()预绑定公共字段(如服务名、版本) - 避免在热路径中打 Debug 日志,Zap 的
Sugar适合开发,Logger适合生产
三、指标暴露:Prometheus 与 Go 的集成
指标是聚合数据,用于监控趋势和告警。Prometheus 是事实标准,Go 官方客户端库 prometheus/client_golang 非常成熟。
3.1 定义指标
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
)
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
3.2 中间件采集指标
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(rw, r)
duration := time.Since(start).Seconds()
httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, http.StatusText(rw.status)).Inc()
httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
})
}
3.3 暴露 /metrics 端点
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":9090", nil)
Prometheus 抓取后,你可以在 Grafana 中绘制 QPS、P99 延迟等图表。
四、链路追踪:OpenTelemetry 统一标准
链路追踪记录请求在分布式系统中的完整路径。OpenTelemetry(OTel)已成为行业标准,它统一了 API 和 SDK,支持导出到 Jaeger、Zipkin、Tempo 等后端。
4.1 初始化 Tracer
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
func initTracer() (*sdktrace.TracerProvider, error) {
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(
jaeger.WithEndpoint("http://localhost:14268/api/traces"),
))
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("order-service"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
4.2 在 HTTP 中间件中创建 Span
func tracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
tracer := otel.Tracer("http-server")
ctx, span := tracer.Start(ctx, r.URL.Path)
defer span.End()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
4.3 跨服务传播
调用下游服务时,需要将 TraceContext 注入到 HTTP Header:
req, _ := http.NewRequestWithContext(ctx, "GET", "http://inventory-service/stock", nil)
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
client.Do(req)
这样,Jaeger 中就能看到完整的调用链:api-gateway → order-service → inventory-service。
五、整合:一个完整的可观测性中间件
将日志、指标、追踪合并到一个中间件中,避免重复代码:
func observabilityMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
ctx, span := otel.Tracer("http").Start(ctx, r.URL.Path)
defer span.End()
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
logger := zap.L().With(zap.String("request_id", requestID))
rw := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(rw, r.WithContext(ctx))
duration := time.Since(start).Seconds()
httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, http.StatusText(rw.status)).Inc()
httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
logger.Info("request completed",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.Int("status", rw.status),
zap.Float64("duration", duration),
zap.String("trace_id", span.SpanContext().TraceID().String()),
)
})
}
这样,每条日志都带有 trace_id,你可以在 Grafana 中从日志直接跳转到 Jaeger 的追踪详情。
六、生产环境注意事项
- 采样策略:不要追踪所有请求,使用
ParentBased(TraceIDRatioBased(0.1))采样 10% - 日志级别动态调整:使用
zap.AtomicLevel支持运行时修改 - 指标基数控制:避免将
user_id作为标签,否则会导致时间序列爆炸 - 资源限制:OTel 的 BatchSpanProcessor 会占用内存,设置合理的队列大小
- 统一 SDK:尽量使用 OpenTelemetry 的日志桥接,避免多套 API 混用
七、总结
可观测性不是事后补救,而是微服务设计的一等公民。在 Go 中,借助 Zap、Prometheus 和 OpenTelemetry,你可以用较少的代码获得生产级的可观测能力。记住三个关键点:
- 日志要结构化,并关联
trace_id - 指标要聚合,避免高基数标签
- 追踪要贯穿全链路,利用上下文传播
从今天开始,为你的下一个 Go 微服务加上这三件套。当故障再次发生时,你将不再盲目猜测,而是精准定位。
未经允许不得转载:任鹏个人博客 » 用 Go 构建可观测的微服务:日志、指标与链路追踪集成


朋友圈点赞图在线生成源码