微服务可观察性问题
当我们将一个集中式项目拆分成多个小的服务程序时,这时服务与服务之间的调用关系是错综复杂的,如何追踪服务之间的调用关系变得十分必要。分布式链路追踪不仅可以解决服务之间调用链路问题,还可以查看服务各个服务的调用时长,以此对相关服务程序做优化。此外,还可以集中收集各个服务的日志,等等功能。
Jaeger
在说Jaeger之前,先引入OpenTracing。OpenTracing是一个链路追踪的规范,Jaeger对此做了相关的实现。OpenTracing提出了链路追踪相关的数据模型。Opentracing后被纳入到OpenTelemetry。
Span
可以理解为一个服务调用(函数调用),拥有操作名称,开始时间,结束时间、Tags和Logs等属性。
Tracer
表示整个服务调用链路,由一个接着一个的Span所组成。程序中会用到的函数有StartSpan、Inject和Extract
SpanContext
表示上下文信息,用于连接Span。比如一个Span是另一个Span的Child或Follow
Jaeger程序
初次使用,我这里只是关心jaeger-client、jaeger-agent和UI。jaeger-client就是jaeger提供的客户端库,用来将追踪信息发送给jaeger-agent,然后jaeger的内部组件处理追踪信息,我们就可以直接在UI上查看。具体使用看官网。
上图是一个请求进入MYSERVICE,然后出的过程。在这个in和out的过程中,只有TracID被传播了,像一些分析数据(operation name,timing,tags和logs)会被直接通过client库发送给jaeger。为了使一个个Span连接起来,就要将TraceID好好传播。在MYSERVICE中需要先Extract出TraceID,然后再将TraceID给Inject进去,便能建立起追踪链路。
实验
这里使用了go-micro作为微服务框架, 使用了其链路追踪plugins。创建了三个服务,其中一个API网关,和两个业务服务。gin-api-gateway作为网关服务,其他服务分别是rand-service和user-service。下图是服务之间的依赖关系。
初始化jaeger
var Tracer opentracing.Tracer
func NewJaegerTracer(serviceName string, jaegerHostPort string) (opentracing.Tracer, io.Closer, error) {
cfg := config.Configuration{
ServiceName: serviceName,
Sampler: &config.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &config.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
LocalAgentHostPort: jaegerHostPort,
},
}
var closer io.Closer
var err error
Tracer, closer, err = cfg.NewTracer(
config.Logger(jaeger.StdLogger),
)
if err != nil {
panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
}
opentracing.SetGlobalTracer(Tracer)
return Tracer, closer, err
}
网关-Start
gin服务中添加中间件
func JaegerGatewayMiddleware(tracer opentracing.Tracer) gin.HandlerFunc {
return func(ctx *gin.Context) {
var md = make(metadata.Metadata, 1)
opName := ctx.Request.URL.Path + "-" + ctx.Request.Method // 操作名称
parentSpan := tracer.StartSpan(opName)
defer parentSpan.Finish()
injectErr := tracer.Inject(parentSpan.Context(), opentracing.TextMap, opentracing.TextMapCarrier(md)) // 将TraceID注入到md中
if injectErr != nil {
logger.Fatalf("%s: Couldn't inject metadata", injectErr)
}
newCtx := metadata.NewContext(ctx.Request.Context(), md) // 利用context传递TraceID
ctx.Request = ctx.Request.WithContext(newCtx)
ctx.Next()
}
}
各个RPC服务链接
直接调用go-micro提供的链路追踪plugins
import opentracingplugins "github.com/go-micro/plugins/v4/wrapper/trace/opentracing"
micro.WrapHandler(opentracingplugins.NewHandlerWrapper(tracer)),
内部主要实现, 先找提取父级Span的TraceID,启动本层的Span并携带这个TraceID,利用context传递下去。
func StartSpanFromContext(ctx context.Context, tracer opentracing.Tracer, name string, opts ...opentracing.StartSpanOption) (context.Context, opentracing.Span, error) {
md, ok := metadata.FromContext(ctx)
if !ok {
md = make(metadata.Metadata)
}
// Find parent span.
// First try to get span within current service boundary.
// If there doesn't exist, try to get it from go-micro metadata(which is cross boundary)
if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
opts = append(opts, opentracing.ChildOf(parentSpan.Context()))
} else if spanCtx, err := tracer.Extract(opentracing.TextMap, opentracing.TextMapCarrier(md)); err == nil {
opts = append(opts, opentracing.ChildOf(spanCtx))
}
// allocate new map with only one element
nmd := make(metadata.Metadata, 1)
sp := tracer.StartSpan(name, opts...)
if err := sp.Tracer().Inject(sp.Context(), opentracing.TextMap, opentracing.TextMapCarrier(nmd)); err != nil {
return nil, nil, err
}
for k, v := range nmd {
md.Set(strings.Title(k), v)
}
ctx = opentracing.ContextWithSpan(ctx, sp)
ctx = metadata.NewContext(ctx, md)
return ctx, sp, nil
}
效果图
相关链接
Jaeger: open source, end-to-end distributed tracing (jaegertracing.io)