最近需要对NodeJS中的调用进行链路跟踪,所以需要使用Jaeger进行跟踪,这里我们选用的是阿里云ARMS中的Jaeger链路跟踪,但是在Golang项目中使用没问题的链路跟踪,却在NodeJS踩了个大坑。

什么是Jaeger

Jaeger 是用于追踪分布式服务之间事务的开源软件。它用来监控复杂的微服务环境并对其进行故障排除。它实现了OpenTracing API,非常好用,同时它也是CNCF毕业的项目,使用Go语言编写。类似的开源软件还有skywalking等。

接入Jaeger

在Golang语言中接入Jaeger非常简单,仅需几行代码就可以搞定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// SDK上报需要:设置链路追踪的网关(不同region对应不同的值,从http://tracing.console.aliyun.com/ 的配置查看中获取)
const TracingAnalysisEndpoint = "http://tracing-analysis-dc-hz.aliyuncs.com/adapt_xxxxxx_xxxxx/api/traces"

sender := transport.NewHTTPTransport(
    TracingAnalysisEndpoint,
  )
  tracer, _ := jaeger.NewTracer(service,
    jaeger.NewConstSampler(true),
    jaeger.NewRemoteReporter(sender, jaeger.ReporterOptions.Logger(jaeger.StdLogger)),
  )

这里的TracingAnalysisEndpoint 是链路跟踪的接入点,我这里是以阿里云的为例,你也可以换成自己的。 service 表示微服务的名称,同时我这里使用ConstSampler固定采样率

有了 tracer 后就可以使用它来创建自己的span进行链路跟踪了:

1
2
3
4
5
6
7
8
// 创建Span。
span := tracer.StartSpan("myspan")
// 设置Tag。
clientSpan.SetTag("mytag", "123")
// 透传traceId。
tracer.Inject(span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
...
defer  span.Finish()

这里不再对链路跟踪展开讲,感兴趣的可以参考 OpenTracing API。

远程采样策略配置

在上面的示例中,我们使用的是固定的采样率,但是这种方式很不灵活,粒度也比较粗,针对这样情况,jaeger支持远程采样策略配置,其实就是在服务端配置后,客户端拉取使用,这样方式可以让服务端控制客户端的采样率,同时也可以根据每个span定制不同的采样率,所以非常方便。

在Go语言中,使用远程采样策略配置同样简单,客户端把上面示例中的 jaeger.NewConstSampler 换成 jaeger.NewRemotelyControlledSampler 即可。

1
2
3
4
5
6
7
8
9
  const samplingServerURL = "http://tracing-analysis-dc-hz.aliyuncs.com/adapt_xxxxxx_xxxxx/api/sampling"
  options := []jaeger.SamplerOption{
      jaeger.SamplerOptions.Metrics(metrics),
      jaeger.SamplerOptions.SamplingServerURL(samplingServerURL),
  }
  tracer, _ := jaeger.NewTracer(service,
    jaeger.NewRemotelyControlledSampler(serviceoptions...),
    jaeger.NewRemoteReporter(sender, jaeger.ReporterOptions.Logger(jaeger.StdLogger)),
  )

以上就配置好了一个使用远程采样策略的tracer,它每隔一段时间会自动更新远程配置,默认是1分钟。

NodeJS 中的坑

但是这在NodeJS中就不可行了,因为在NodeJS中配置远程采样并不是一个完整的URL,而是被分为了host、port等好几部分,就是这个拆分引起了巨坑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
tracerConfig: {
  serviceName: 'demo',
  sampler: {
    type: 'remote',
    param: 1,
    host: '<host>',
    port: '<port>'
  },
  reporter: {
    collectorEndpoint: "<endpoint>"
  }
}

以上就是一个远程采样的配置,需要把里面的 hostport 换成自己的,比如阿里云的给的这样的采样配置URL:

1
http://tracing-analysis-dc-hz.aliyuncs.com/adapt_xxxxxx_xxxxx/api/sampling

我们拆解出来host和port就是:

  1. host:tracing-analysis-dc-hz.aliyuncs.com
  2. port:80

用这两个值可以替换以上的配置,但是配置完之后使用,会发现不行,不行的原因是404,为什么是404?这个也是我读了它的源代码才知道的。

因为它最终拼接出来的url是:‘http’+host+port+'/sampling',比如上面阿里云的,拼接出来的URL就是 http://tracing-analysis-dc-hz.aliyuncs.com/sampling ,和阿里云给我们的完全不一样,所以就会提示404,没这个URL。

导致这个坑的原因是NodeJS SDK 发现没有 samplingPath ,就使用了默认的 /sampling ,所以拼接出来的URL,无法拿到远程采样配置,但是NodeJS Tracer Config又没有提供samplingPath 的配置,陷入僵局。

出现这个问题的原因,我认为有两种:

  1. Jaeger NodeJS SDK 比较古板,对OpenTracing API 规范执行的太严格,只有path是samplingPath/sampling 才能使用。
  2. 阿里云没有完全按照OpenTracing API规范来定义自己的Server config URL,在samplingPath 中添加了多余的授权信息。

那么如何做更好的,其实我更愿意对SDK做一些改变,让它灵活一些,比如就像Golang一样,直接用给的URL就可以了,没必要拆分为host、port让填写,反而搞出来坑。

如何解决

既然NodeJS SDK搞出来这么一个坑,那么如何解决呢?我把目光转移到了 initTracer 函数上,这个函数除了 config 这个参数外,还有一个 options 配置的参数,并且还用于创建sampler。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static initTracer(config, options = {}) {
	    let sampler;
	    if (!config.serviceName) {
	      throw new Error(`config.serviceName must be provided`);
	    }
	    if (config.sampler) {
	      sampler = Configuration._getSampler(config, options);
	    } else {
	      sampler = new RemoteSampler(config.serviceName, options);
	    }
}

从以上NodeJS SDK的源代码可以看到,如果我们自己在 config 配置中不定义 sampler 这个配置,它会new一个 RemoteSampler ,也就是我们正需要的远程采样,并且把 options 当做参数传给了 RemoteSampler ,感觉有戏,继续去看RemoteSampler 的构造函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  /**
	   * Creates a sampler remotely controlled by jaeger-agent.
	   *
	   * @param {string} [serviceName] - name of the current service / application, same as given to Tracer
	   * @param {object} [options] - optional settings
	   * @param {object} [options.sampler] - initial sampler to use prior to retrieving strategies from Agent
	   * @param {object} [options.logger] - optional logger, see _flow/logger.js
	   * @param {object} [options.metrics] - instance of Metrics object
	   * @param {number} [options.refreshInterval] - interval in milliseconds before sampling strategy refreshes (0 to not refresh)
	   * @param {string} [options.hostPort] - host and port for jaeger-agent, defaults to 'localhost:5778'
	   * @param {string} [options.host] - host for jaeger-agent, defaults to 'localhost'
	   * @param {number} [options.port] - port for jaeger-agent for SamplingManager endpoint
	   * @param {string} [options.samplingPath] - path on jaeger-agent for SamplingManager endpoint
	   * @param {number} [options.maxOperations] - max number of operations to track in PerOperationSampler
	   * @param {function} [options.onSamplerUpdate]
	   */
	  constructor(serviceName: string, options: any = {}) {
  
}

哈哈,配置全的一比,host、port、samplingPath都有,这样我们就可以自定义samplingPath了,所以解决方案也有了,不再使用tracer config配置,使用构造好的options即可。

1
2
3
4
5
6
7
8
const options = { 
    logger: console,
    host: 'tracing-analysis-dc-hz.aliyuncs.com',
    port: 80,
    samplingPath: '/adapt_xxxxxx_xxxxx/api/sampling'
}
options.sampler = new ProbabilisticSampler(1)
const tracer = initTracer(config, options)

一个URL由三部分组成,我们把缺少的 samplingPath 这部分配置上就可以了。

小结

这次踩坑可以总结出来几个不错的经验:

  1. SDK在满足规范API的基础上要稍微灵活点,不要太严格
  2. 服务提供商方比如阿里云,要尽可能遵守规范
  3. 我们自己不要放弃,要找,总会有解决办法的。

好了,再给jaeger提交个PR,修复这个问题。

本文为原创文章,转载注明出处,欢迎扫码关注公众号flysnow_org或者网站 https://www.flysnow.org/ ,第一时间看后续精彩文章。觉得好的话,请猛击文章右下角「在看」,感谢支持。

扫码关注