Go中的微服务:OpenTelemetry

628 阅读4分钟

什么是OpenTelemetry?

OpenTelemetry是一套API、SDK、工具和集成,旨在创建和管理遥测数据,如跟踪、指标和日志。该项目提供了一个与供应商无关的实现,可以配置成将遥测数据发送到你选择的后端。

在某种程度上,OpenTelemetry可观察性的间接结果,因为需要监控用不同技术编写的多个服务,所以更难收集和汇总可观察性数据。

OpenTelemetry 并没有提供具体的可观察性后端,而是提供了配置、发射、收集、处理和输出遥测数据的标准方式;这些后端有商业支持的选择,如NewRelicLightstep,也有一些开源项目可以与之配合使用,对于这篇文章,我具体介绍一下。

OpenTelemetry对Go中日志的支持,到目前为止,还没有实现,不过我还是向大家展示如何使用Uber的zap来记录数据。


在Go中使用OpenTelemetry

目前,以下是关于Go中的支持的OpenTelemetry 状态。

  • 追踪测试版
  • 衡量标准阿尔法
  • 记录尚未实施

[官方文档]描述了在 Go 程序中添加 OpenTelemetry 支持所需的步骤,尽管该实现尚未达到主要的官方版本,但我们仍然可以在生产中使用它。还有一个注册表,列出了实现仪表或跟踪的不同Go软件包。

衡量标准

为了收集指标,将使用Prometheus。为了定义这个导出器,我们使用了官方的OpenTelemetry-Go Prometheus导出器,如下所示。

promExporter, _ := prometheus.NewExportPipeline(prometheus.Config{}) // XXX: error omitted for brevity
global.SetMeterProvider(promExporter.MeterProvider())

这个输出器实现了 http.Handler,所以我们可以把它定义为我们的HTTP服务器的另一个端点部分,然后Prometheus将用它来轮询度量值。

r := mux.NewRouter()
r.Handle("/metrics", promExporter)

其中一个重要的事情是,因为Prometheus从我们的服务中轮询值,我们必须说明使用什么地址和端口,因为我们使用docker,所以有一个配置文件,包括。

scrape_configs:
- job_name: to-do-api
  scrape_interval: 5s
  static_configs:
  - targets: ['host.docker.internal:9234']

访问http://localhost:9090/ ,在输入一个指标并选择它之后,应该显示一个像下面这样的用户界面。

OpenTelemetry Prometheus

追踪

对于收集度量标准,将使用Jaeger。与度量标准相比,定义这个导出器的过程要稍微复杂一些。它需要以下内容。

jaegerEndpoint, _ := conf.Get("JAEGER_ENDPOINT")

jaegerExporter, _ := jaeger.NewRawExporter(
	jaeger.WithCollectorEndpoint(jaegerEndpoint),
	jaeger.WithSDKOptions(sdktrace.WithSampler(sdktrace.AlwaysSample())),
	jaeger.WithProcessFromEnv(),
) // XXX: error omitted for brevity

tp := sdktrace.NewTracerProvider(
	sdktrace.WithSampler(sdktrace.AlwaysSample()),
	sdktrace.WithSyncer(jaegerExporter),
)

otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))

定义它是在我们的程序中获得完整的跟踪支持的第一步,接下来我们必须定义 跨度的定义,可以是手动的,也可以是自动的,这取决于我们打算使用什么现有的包装器来追踪;在我们的例子中,我们是手动进行的。

例如,在postgresql 包中,每种类型的每个方法都定义了像下面这样的指令。

ctx, span := trace.SpanFromContext(ctx).Tracer().Start(ctx, "<name>")
 // If needed: N calls to "span.SetAttributes(...)"
defer span.End()

例如,在 postgres.Task.Create,我们有。

ctx, span := trace.SpanFromContext(ctx).Tracer().Start(ctx, "Task.Create")
span.SetAttributes(attribute.String("db.system", "postgresql"))
defer span.End()

OpenTelemetry 规范定义了一些关于属性名称和支持值的约定,所以我们上面使用的属性db.system数据库约定的一部分;在某些情况下, go.opentelemetry.io/otel/semconv包定义了我们可以使用的常量,但这并不总是如此,所以要注意一下。

同样地,相当于 service.Task.Create方法做了以下事情。

ctx, span := trace.SpanFromContext(ctx).Tracer().Start(ctx, "Task.Create")
defer span.End()

这是为了让我们能够记录每一层的调用,以衡量它完成的时间以及任何可能发生的错误。访问http://localhost:16686/search ,应该会显示一个像下面这样的用户界面。

OpenTelemetry Jaeger

在点击 "查找痕迹 "后,我们应该看到如下内容。

OpenTelemetry Jaeger

其中每个Span是一个层,描述了整个调用所采取的交互方式。关于OpenTelemetry跨度的有趣之处在于,如果有包括其他支持OpenTelemetry的调用的交互,我们可以它们连接起来,看到所有这些调用之间的完整交互。

日志

尽管OpenTelemetry 还不支持Go中的日志,但在构建微服务时应考虑记录某些消息;我喜欢推荐的方法是定义一个日志器实例,然后在需要时将其作为参数在初始化具体类型时传递,我们将要使用的包是 uber-go/zap.

我们的代码实现了一个中间件来记录所有的请求。

logger, _ := zap.NewProduction()
defer logger.Sync()

middleware := func(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		logger.Info(r.Method,
			zap.Time("time", time.Now()),
			zap.String("url", r.URL.String()),
		)

		h.ServeHTTP(w, r)
	})
}

然后,我们用它来包装多路复用器,因此也包括所有的处理程序。

srv := &http.Server{
	Handler: middleware(r),
	// ... other fields ...
}

结论

OpenTelemetry对于Go来说,这是一个正在进行中的工作,这绝对是一个好主意,但很难让你自己的代码与小版本保持同步,因为这些小版本时常会破坏API。对于长期运行的项目,我建议等到主要版本发布后再使用,否则找到一个非OpenTelemetry的选项可能更有意义。