Jaeger源码解析 -- All in One 模式

文章目录

    • 简介
    • 代码解析
      • 入口
        • 准备工作
          • storageFactory
          • strategyStoreFactory
          • 初始化配置
        • 启动
          • 启动 Agent
          • 启动 Collector
          • 启动Query
    • 参考文档

简介

Jaeger 的 All-in-one 模式主要是用来快速启动一个本地服务用来测试,其中包含 Jaeger UI、collector、query、agent、这些组件。这个模式下的存储数据是放在内存中的。

启动 All-in-one 模式的 jaeger 最简单的方式是使用 Docker 镜像的来启动。

$ docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.8

下面的表格是 Jaeger All-in-one 暴露端口的列表:

端口 协议 组件 功能
5775 UDP agent 接受zipkin.thrift compact thrift 协议(已过时,仅旧客户端使用)
6831 UDP agent 接受jaeger.thrift compact thrift 协议
6832 UDP agent 接受jaeger.thrift binary thrift 协议
5778 HTTP agent 服务配置
16686 HTTP query 服务前端
14268 HTTP collector 直接从客户端接受jaeger.thrift协议
9411 HTTP collector 兼容 Zipkin 服务(可选的)

代码解析

本博文中使用的代码是 v190 版本。

入口

All-in-one 的入口在cmd/all-in-one/main.go中。实际上所有的组件入口都在 cmd 这个包下。

All-in-one 需要启动 agent query 和 collector 这三个组件,都在入口的启动函数中实现的。下面看下具体的代码。

准备工作

var signalsChannel = make(chan os.Signal)
signal.Notify(signalsChannel, os.Interrupt, syscall.SIGTERM)

首先创建一个用来接收信号的 channel,用来接收系统中断(os.Interrupt)和系统的 kill 指令(syscall.SIGTERM)。

if os.Getenv(storage.SpanStorageTypeEnvVar) == "" {
	os.Setenv(storage.SpanStorageTypeEnvVar, "memory") // other storage types default to SpanStorage
}

设置环境变量。

storageFactory, err := storage.NewFactory(storage.FactoryConfigFromEnvAndCLI(os.Args, os.Stderr))

strategyStoreFactory, err := ss.NewFactory(ss.FactoryConfigFromEnv())

上面的一行初始化了存储工厂,下面的是采样策略的工厂。接下来就看一下这两个工厂是如何初始化的,以及他们的作用是什么。

storageFactory

storage.NewFactory的入参是storage.FactoryConfigFromEnvAndCLI,先来看下这个方法。

func FactoryConfigFromEnvAndCLI(args []string, log io.Writer) FactoryConfig {
	// 从环境变量中获取span的存储类型
	spanStorageType := os.Getenv(SpanStorageTypeEnvVar)
	if spanStorageType == "" {
		// 如果环境变量中没有设置,从命令行中取值
		spanStorageType = spanStorageTypeFromArgs(args, log)
	}
	if spanStorageType == "" {
		// 设置默认的存储为cassandraStorageType
		spanStorageType = cassandraStorageType
	}
	// 考虑到有多个存储的情况
	spanWriterTypes := strings.Split(spanStorageType, ",")
	// 如果有多个存储的话,那么读操作只会从第一个存储类型中读取
	if len(spanWriterTypes) > 1 {
		fmt.Fprintf(log,
			"WARNING: multiple span storage types have been specified. "+
				"Only the first type (%s) will be used for reading and archiving.\n\n",
			spanWriterTypes[0],
		)
	}
	// 获取dependency存储类型
	depStorageType := os.Getenv(DependencyStorageTypeEnvVar)
	if depStorageType == "" {
		depStorageType = spanWriterTypes[0]
	}
	return FactoryConfig{
		SpanWriterTypes:         spanWriterTypes,
		SpanReaderType:          spanWriterTypes[0],
		DependenciesStorageType: depStorageType,
	}
}

从上面的代码可以看出,这个方法的作用是从环境变量或者命令行中读取出存储配置,现在有如下两种类型的存储。

// 后端用来存储spans的方式
SpanStorageTypeEnvVar = "SPAN_STORAGE_TYPE"

// 后端用来存储dependencies的方式
DependencyStorageTypeEnvVar = "DEPENDENCY_STORAGE_TYPE"

span 中存储的是链路追踪的数据,dependency 中存储和依赖相关的数据。Dependencies 就是在 Jaeger UI 上方菜单中的 Dependencies 一栏。

在我们的情境中,FactoryConfigFromEnvAndCLI方法的返回值都是memory类型的存储。

然后我们再看一下NewFactory方法。

func NewFactory(config FactoryConfig) (*Factory, error) {
	f := &Factory{FactoryConfig: config}
	uniqueTypes := map[string]struct{}{
		f.SpanReaderType:          {},
		f.DependenciesStorageType: {},
	}
	// 遍历并去重
	for _, storageType := range f.SpanWriterTypes {
		uniqueTypes[storageType] = struct{}{}
	}
	f.factories = make(map[string]storage.Factory)
	// 根据去重后的结果实例化工厂
	for t := range uniqueTypes {
		ff, err := f.getFactoryOfType(t)
		if err != nil {
			return nil, err
		}
		f.factories[t] = ff
	}
	return f, nil
}

从上面的代码可以看出,NewFactory方法是判断需要用到哪几种存储类型,并分别实例化的过程。

strategyStoreFactory

这里的NewFactory方法同样有一个入参FactoryConfigFromEnv,首先看下这个方法。

func FactoryConfigFromEnv() FactoryConfig {
	strategyStoreType := os.Getenv(SamplingTypeEnvVar)
	if strategyStoreType == "" {
		strategyStoreType = staticStrategyStoreType
	}
	return FactoryConfig{
		StrategyStoreType: strategyStoreType,
	}
}

通过上面的代码可以看出,FactoryConfigFromEnv方法作用就是从环境变量中获取采样类型。共有两种采样类型。

staticStrategyStoreType   = "static"
adaptiveStrategyStoreType = "adaptive"

默认的采样类型是 static。

NewFactory方法也与上面的方法类似,获取配置的采样的类型并实例化,值得注意的是,这里的采样类型只实现了 static ,没有实现 adaptive 类型的工厂。

func NewFactory(config FactoryConfig) (*Factory, error) {
	f := &Factory{FactoryConfig: config}
	uniqueTypes := map[string]struct{}{
		f.StrategyStoreType: {},
	}
	f.factories = make(map[string]strategystore.Factory)
	for t := range uniqueTypes {
		ff, err := f.getFactoryOfType(t)
		if err != nil {
			return nil, err
		}
		f.factories[t] = ff
	}
	return f, nil
}

值得一提的是,这里的 f 并不是真正的工厂,而是存在 f.factories 里面。

初始化配置
v := viper.New()

在这里新建一个 viper 实例,作用是存储配置。viper 是Go应用程序的完整配置解决方案。

接下来跳过下面一大段关于 command 配置的代码,直接看后面的部分。

flags.SetDefaultHealthCheckPort(collector.CollectorDefaultHealthCheckHTTPPort)

config.AddFlags(
	v,
	command,
	flags.AddConfigFileFlag,
	flags.AddFlags,
	storageFactory.AddFlags,
	agentApp.AddFlags,
	agentRep.AddFlags,
	agentTchanRep.AddFlags,
	agentGrpcRep.AddFlags,
	collector.AddFlags,
	queryApp.AddFlags,
	pMetrics.AddFlags,
	strategyStoreFactory.AddFlags,
)

在这里设置了默认的健康检查的端口,并在AddFlags方法中,将一些默认的参数写入到之前新建的 viper 当中。

写入配置参数之后,准备工作也就完成了。接下来就到了启动阶段。

启动

启动的方式使用了cobra配置的命令行,直接看RunE方法。

sFlags := new(flags.SharedFlags).InitFromViper(v)
logger, err := sFlags.NewLogger(zap.NewProductionConfig())

初始化日志配置,默认的日志级别是 info。

hc, err := sFlags.NewHealthCheck(logger)

监听健康检查接口。

mBldr := new(pMetrics.Builder).InitFromViper(v)
// 实例化Prometheus工厂
rootMetricsFactory, err := mBldr.CreateMetricsFactory("")
// 嵌套创建一个工厂,以上面的Prometheus工厂为父工厂
metricsFactory := rootMetricsFactory.Namespace(metrics.NSOptions{Name: "jaeger", Tags: nil})

metrics 收集相关配置,默认的 metrics 服务是 Prometheus。

storageFactory.InitFromViper(v)
if err := storageFactory.Initialize(metricsFactory, logger); err != nil {
	logger.Fatal("Failed to init storage factory", zap.Error(err))
}

初始化存储工厂配置,其实就是返回一个初始化好的结构体,在这里这个结构体是memory类型的,这个结构体中实现了 span 的ReaderWriter和 dependency 的Reader接口。

spanReader, err := storageFactory.CreateSpanReader()

spanWriter, err := storageFactory.CreateSpanWriter()

dependencyReader, err := storageFactory.CreateDependencyReader()

这里是实例化上面说的 span 的ReaderWriter和 dependency 的Reader接口这三个接口。我们具体看下这三个接口是如何实例化的。(仅限于基于 memory 的实现)

func (f *Factory) CreateSpanReader() (spanstore.Reader, error) {
	factory, ok := f.factories[f.SpanReaderType]
	if !ok {
		return nil, fmt.Errorf("No %s backend registered for span store", f.SpanReaderType)
	}
	return factory.CreateSpanReader()
}

func (f *Factory) CreateSpanReader() (spanstore.Reader, error) {
	return f.store, nil
}

从上面的代码中可以看出,CreateSpanReader方法直接将store结构返回了,因为这个结构里面已经实现了 reader 相关接口。

func (f *Factory) CreateSpanWriter() (spanstore.Writer, error) {
	var writers []spanstore.Writer
	for _, storageType := range f.SpanWriterTypes {
		factory, ok := f.factories[storageType]
		if !ok {
			return nil, fmt.Errorf("No %s backend registered for span store", storageType)
		}
		writer, err := factory.CreateSpanWriter()
		if err != nil {
			return nil, err
		}
		writers = append(writers, writer)
	}
	if len(f.SpanWriterTypes) == 1 {
		return writers[0], nil
	}
	return spanstore.NewCompositeWriter(writers...), nil
}

func (f *Factory) CreateSpanWriter() (spanstore.Writer, error) {
	return f.store, nil
}

SpanWriter 的逻辑和 reader 的逻辑大致一样,不同的是 writer 可能会有多个,所以需要放到切片当中。

func (f *Factory) CreateDependencyReader() (dependencystore.Reader, error) {
	factory, ok := f.factories[f.DependenciesStorageType]
	if !ok {
		return nil, fmt.Errorf("No %s backend registered for span store", f.DependenciesStorageType)
	}
	return factory.CreateDependencyReader()
}

func (f *Factory) CreateDependencyReader() (dependencystore.Reader, error) {
	return f.store, nil
}

DependencyReader 和 SpanReader 逻辑一致。

下面是采样策略工厂的实例化。

strategyStoreFactory.InitFromViper(v)
strategyStore := initSamplingStrategyStore(strategyStoreFactory, metricsFactory, logger)

首先看下InitFromViper方法。

func (f *Factory) InitFromViper(v *viper.Viper) {
	for _, factory := range f.factories {
		if conf, ok := factory.(plugin.Configurable); ok {
			conf.InitFromViper(v)
		}
	}
}

// static/factory.go
func (f *Factory) InitFromViper(v *viper.Viper) {
	f.options.InitFromViper(v)
}

func (opts *Options) InitFromViper(v *viper.Viper) *Options {
	opts.StrategiesFile = v.GetString(samplingStrategiesFile)
	return opts
}

首先遍历所有之前存入的工厂,然后进到InitFromViper方法的实现当中。这里是进入到static/factory.go这个实现方法里面。然后取出samplingStrategiesFile对应的值,放入到opts变量当中。在这里这个值是空字符串。

接下来是initSamplingStrategyStore方法。

func initSamplingStrategyStore(
	samplingStrategyStoreFactory *ss.Factory,
	metricsFactory metrics.Factory,
	logger *zap.Logger,
) strategystore.StrategyStore {
	if err := samplingStrategyStoreFactory.Initialize(metricsFactory, logger); err != nil {
		logger.Fatal("Failed to init sampling strategy store factory", zap.Error(err))
	}
	strategyStore, err := samplingStrategyStoreFactory.CreateStrategyStore()
	if err != nil {
		logger.Fatal("Failed to create sampling strategy store", zap.Error(err))
	}
	return strategyStore
}

上面的代码做了两件事情,调用Initialize方法,这个方法的在这里只是将 logger 赋值 factor 内变量,不做深入到讨论。另外一个方法CreateStrategyStore是新建一个采样策略存储,这里用的是静态策略,然后把这些策略存储起来。下面省略几步跳转,直接进入'strategy_store.go代码里面。

func NewStrategyStore(options Options, logger *zap.Logger) (ss.StrategyStore, error) {
	h := &strategyStore{
		logger:            logger,
		serviceStrategies: make(map[string]*sampling.SamplingStrategyResponse),
	}
	strategies, err := loadStrategies(options.StrategiesFile)
	if err != nil {
		return nil, err
	}
	h.parseStrategies(strategies)
	return h, nil
}

首先新建了一个 strategyStore的数据结构,其中serviceStrategies是一个 map 结构的变量,存储的就是具体服务的采样策略。map 的 key 是服务的名称。在loadStrategies方法中,根据在 viper 中配置的策略路径来读取文件,这里的路径为空,所以这个方法就直接返回,返回的数值为空。

接下来的parseStrategies方法中,由于入参为空,所以将采样策略赋值为默认参数,并返回。

defaultStrategy = sampling.SamplingStrategyResponse{
	StrategyType: sampling.SamplingStrategyType_PROBABILISTIC,
	ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{
		SamplingRate: defaultSamplingProbability,
	},
}

默认的采样策略是概率采样,使用的采样概率是 0.001。

接下来是从 viper 中获取参数,为下面初始化做准备。

// agent相关参数
aOpts := new(agentApp.Builder).InitFromViper(v)
// agent上报给query的请求类型,是grpc还是tchannel
repOpts := new(agentRep.Options).InitFromViper(v)
// 设置tchannel连接参数
tchannelRepOpts := agentTchanRep.NewBuilder().InitFromViper(v, logger)
// 设置grpc连接参数
grpcRepOpts := new(agentGrpcRep.Options).InitFromViper(v)
// collector相关参数
cOpts := new(collector.CollectorOptions).InitFromViper(v)
// query相关参数
qOpts := new(queryApp.QueryOptions).InitFromViper(v)

接下来分别启动这三个服务。

startAgent(aOpts, repOpts, tchannelRepOpts, grpcRepOpts, cOpts, logger, metricsFactory)
grpcServer := startCollector(cOpts, spanWriter, logger, metricsFactory, strategyStore, hc)
startQuery(qOpts, spanReader, dependencyReader, logger, rootMetricsFactory, metricsFactory, mBldr, hc, archiveOptions(storageFactory, logger))

分别看下这几个启动的过程。

启动 Agent
func startAgent(
	b *agentApp.Builder,
	repOpts *agentRep.Options,
	tchanRep *agentTchanRep.Builder,
	grpcRepOpts *agentGrpcRep.Options,
	cOpts *collector.CollectorOptions,
	logger *zap.Logger,
	baseFactory metrics.Factory,
) {
	// 创建特定的metrics工厂
	metricsFactory := baseFactory.Namespace(metrics.NSOptions{Name: "agent", Tags: nil})

	cp, err := createCollectorProxy(cOpts, repOpts, tchanRep, grpcRepOpts, logger, metricsFactory)
	if err != nil {
		logger.Fatal("Could not create collector proxy", zap.Error(err))
	}

	agent, err := b.CreateAgent(cp, logger, baseFactory)
	if err != nil {
		logger.Fatal("Unable to initialize Jaeger Agent", zap.Error(err))
	}

	logger.Info("Starting agent")
	if err := agent.Run(); err != nil {
		logger.Fatal("Failed to run the agent", zap.Error(err))
	}
}

func createCollectorProxy(
	cOpts *collector.CollectorOptions,
	repOpts *agentRep.Options,
	tchanRepOpts *agentTchanRep.Builder,
	grpcRepOpts *agentGrpcRep.Options,
	logger *zap.Logger,
	mFactory metrics.Factory,
) (agentApp.CollectorProxy, error) {
	switch repOpts.ReporterType {
	case agentRep.GRPC:
		grpcRepOpts.CollectorHostPort = append(grpcRepOpts.CollectorHostPort, fmt.Sprintf("127.0.0.1:%d", cOpts.CollectorGRPCPort))
		return agentGrpcRep.NewCollectorProxy(grpcRepOpts, mFactory, logger)
	case agentRep.TCHANNEL:
		tchanRepOpts.CollectorHostPorts = append(tchanRepOpts.CollectorHostPorts, fmt.Sprintf("127.0.0.1:%d", cOpts.CollectorPort))
		return agentTchanRep.NewCollectorProxy(tchanRepOpts, mFactory, logger)
	default:
		return nil, errors.New(fmt.Sprintf("unknown reporter type %s", string(repOpts.ReporterType)))
	}
}

在启动 Agent 服务之前,首先调用createCollectorProxy方法创建了 Collector 的代理,用于向 Collector 上报数据。在这个方法中,根据通信协议的类型创建不同的代理。

接下来调用CreateAgent方法创建一个 Agent 实例,在入参中包含刚刚创建的 Collector 代理。由于创建 Agent 实例属于 Agent 模块范围,这个方法以后再详细展开。

如果上述过程都没有错误,那么就启动 Agent。

启动 Collector

启动 Collector 的代码较长,下面分步拆解一下。

	spanBuilder, err := collector.NewSpanHandlerBuilder(
		cOpts,
		spanWriter,
		basic.Options.LoggerOption(logger),
		basic.Options.MetricsFactoryOption(metricsFactory),
	)
	zipkinSpansHandler, jaegerBatchesHandler, grpcHandler := spanBuilder.BuildHandlers()

新建一个 spanBuilder,并创建了三个 handler。其中zipkinSpansHandlerjaegerBatchesHandler 实现了 TChanCollector 接口,可以处理 Tchan RPC 的调用。

	{
		ch, err := tchannel.NewChannel("jaeger-collector", &tchannel.ChannelOptions{})
		server := thrift.NewServer(ch)
		server.Register(jc.NewTChanCollectorServer(jaegerBatchesHandler))
		server.Register(zc.NewTChanZipkinCollectorServer(zipkinSpansHandler))
		// 设置一个handler可以处理采样策略
		server.Register(sc.NewTChanSamplingManagerServer(sampling.NewHandler(strategyStore)))
		portStr := ":" + strconv.Itoa(cOpts.CollectorPort)
		listener, err := net.Listen("tcp", portStr)
		logger.Info("Starting jaeger-collector TChannel server", zap.Int("port", cOpts.CollectorPort))
		// 启动tchan服务
		ch.Serve(listener)
	}

上面的代码是启动一个 tchannel,可以处理 zipkinSpansHandlerjaegerBatchesHandler

接下来启动 GRPC 服务。

func startGRPCServer(
	port int,
	handler *collectorApp.GRPCHandler,
	samplingStore strategystore.StrategyStore,
	logger *zap.Logger,
) (*grpc.Server, error) {
	server := grpc.NewServer()
	// 传入grpc handler
	_, err := grpcserver.StartGRPCCollector(port, server, handler, samplingStore, logger, func(err error) {
		logger.Fatal("gRPC collector failed", zap.Error(err))
	})
	if err != nil {
		return nil, err
	}
	return server, err
}

后面的代码是用来处理 Zipkin Http Api 的,Zipkin 服务可以直接将数据上报到 Collecotor,这里就不在展开。

到这里,Agent 组件就启动完成了,一共启动了三个服务,Tchannel、GRPC、和一个专门处理 Zipkin 的 Http 服务。

启动Query
	tracer, closer, err := jaegerClientConfig.Configuration{
		Sampler: &jaegerClientConfig.SamplerConfig{
			Type:  "const",
			Param: 1.0,
		},
		RPCMetrics: true,
	}.New(
		"jaeger-query",
		jaegerClientConfig.Metrics(rootFactory),
		jaegerClientConfig.Logger(jaegerClientZapLog.NewLogger(logger)),
	)
	opentracing.SetGlobalTracer(tracer)

上面的代码首先创建了一个 tracer,用来收集自身的信息并上报给 Agent 组件。

spanReader = storageMetrics.NewReadMetricsDecorator(spanReader, baseFactory.Namespace(metrics.NSOptions{Name: "query", Tags: nil}))

接下来在spanReader中添加了一些 metrics 相关的方法,以便收集运行中的指标信息。这里用到了装饰器的设计模式。

handlerOpts = append(handlerOpts, queryApp.HandlerOptions.Logger(logger), queryApp.HandlerOptions.Tracer(tracer))
apiHandler := queryApp.NewAPIHandler(
	spanReader,
	depReader,
	handlerOpts...)

r := mux.NewRouter()
if qOpts.BasePath != "/" {
	r = r.PathPrefix(qOpts.BasePath).Subrouter()
}
// 注册 url
apiHandler.RegisterRoutes(r)
queryApp.RegisterStaticHandler(r, logger, qOpts)
// 注册处理metrics的handler
if h := metricsBuilder.Handler(); h != nil {
		logger.Info("Registering metrics handler with jaeger-query HTTP server", zap.String("route", metricsBuilder.HTTPRoute))
		r.Handle(metricsBuilder.HTTPRoute, h)
	}

在上面的代码中,apiHandler相当与是所有内置 API 的合集,同时也承接着转发流量的功能。

go func() {
		defer closer.Close()
		if err := http.ListenAndServe(portStr, recoveryHandler(r)); err != nil {
			logger.Fatal("Could not launch jaeger-query service", zap.Error(err))
		}
		hc.Set(healthcheck.Unavailable)
	}()

最后启动一个协程,在里面运行 http 服务。

三个组件都启动后,jaeger all-in-one 就成功启动了。如果有数据汇报给了 agent 组件,那么打开网页就会看到追踪的情况。

参考文档

Golang的 signal
SIGKILL和SIGTERM、SIGINT

你可能感兴趣的:(Jaeger,Kubernetes)