简介
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 的Reader
、Writer
和 dependency 的Reader
接口。
spanReader, err := storageFactory.CreateSpanReader()
spanWriter, err := storageFactory.CreateSpanWriter()
dependencyReader, err := storageFactory.CreateDependencyReader()
这里是实例化上面说的 span 的Reader
、Writer
和 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。其中zipkinSpansHandler
和jaegerBatchesHandler
实现了 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,可以处理 zipkinSpansHandler
和jaegerBatchesHandler
。
接下来启动 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 组件,那么打开网页就会看到追踪的情况。