Brave是一个用于捕捉和报告分布式操作的延迟信息给Zipkin的工具库。
Zipkin 基于 Dapper,
Brave的无依赖性trace包基于JRE6+,这是用于记录时间和描述系统的基础api,这个library也包含解析X-B3-TraceId
头信息的代码
大多数用户不自己直接写tracing代码,相反,他们复用已经写好的基础代码。在开发自己的tracing前可以先check出instrumentation
和Zipkin'list
,这里已经有了通用的tracing library,如JDBC、Servlet和Spring等
如果你试图去tracing遗留系统,你也许对Spring XML Configuration
感兴趣,这允许你不写任何代码来配置tracing。
如果你想将trace IDs 放入日志文件,或者想改变本地线程的行为,参看Context libraries
,集成如SLF4J的日志系统
Brave是用于捕捉分布式操作的延迟信息并报告给zipkin的工具包,大多数人不直接使用Brave,他们使用libraries和framework而不是直接使用Brave来服务与自己的系统
该模块包含创建Tracer,连接spans,模拟潜在的分布式工作的延迟,该模块还包含在系统网络间传递跟踪上下文信息的工具包,如通过http headers。
至关重要的,你必须有一个Tracer,一个已经配置过的可以向zipkin报告信息的Tracer。
这里有一个例子,配置基于http(而不是kafka)发送跟踪信息(span)到zipkin的例子。
// 配置reporter,用于控制向zipkin发送span的频率
// (the dependency is io.zipkin.reporter2:zipkin-sender-okhttp3)
sender = OkHttpSender.create("http://127.0.0.1:9411/api/v2/spans");
spanReporter = AsyncReporter.create(sender);
// 创建一个你想在zipkin中看到的服务名称的跟踪组件
tracing = Tracing.newBuilder()
.localServiceName("my-service")
.spanReporter(spanReporter)
.build();
// 跟踪公开的可能需要的对象,最重要的时跟踪
tracer = tracing.tracer();
// Failing to close resources can result in dropped spans! When tracing is no
// longer needed, close the components you made in reverse order. This might be
// a shutdown hook for some users.
tracing.close();
spanReporter.close();
sender.close();
如果你需要连接老版本的zipkin api,你可以使用下面的方法连接,参考zipkin-reporter查看更多信息
sender = URLConnectionSender.create("http://localhost:9411/api/v1/spans")
reporter = AsyncReporter.builder(sender)
.build(SpanBytesEncoder.JSON_V1);
Tracer创建并连接spans,spans模拟分布式系统工作单元信息。Tracing可以采样,从而减轻进程中的开销,和防止大量的数据发送给zipkin。
Spans在完成时将一个Tracer报告给zipkin,不采样则不做任何事。开始一个span后,你可以标记感兴趣的event或添加tags,或查看当前的keys或详细信息。
Spans有一个上下文,其中包含tracer标识符,将其放在表示分布式操作的树的正确位置。
跟踪本地代码,只需要在一个范围内运行它
Span span = tracer.newTrace().name("encode").start();
try {
doSomethingExpensive();
} finally {
span.finish();
}
在上面的例子中,创建的span是trace的根span。很多情况下,创建的span是已经存在的tracer的一部分。这种情况下,调用newChild
替代newTrace
。
Span span = tracer.newChild(root.context()).name("encode").start();
try {
doSomethingExpensive();
} finally {
span.finish();
}
已经有一个span,你可以添加tags,它可以lookup keys或详细信息。例如,你可以添加一个tag说明当前运行时version。
span.tag("clnt/finagle.version", "6.36.0");
若希望向第三方暴露自定义spans的功能,用brave.SpanCustomizer
而不是brave.Span
。前者更易于理解和测试,并且不会触发用户span的lifecycle hooks。
interface MyTraceCallback {
void request(Request request, SpanCustomizer customizer);
}
brave.Span
实现brave.SpanCustomizer
,仅仅是为了易于使用。
例如:
for (MyTraceCallback callback : userCallbacks) {
callback.request(request, span);
}
有时,你需要知道trace是否还在进行之中或是已经结束,并且你不想用户做null检查。brave.CurrentSpanCustimizer
添加相应的正在进行的span或者drops数据。
例如:
// Some DI configuration wires up the current span customizer
@Bean SpanCustomizer currentSpanCustomizer(Tracing tracing) {
return CurrentSpanCustomizer.create(tracing);
}
// user code can then inject this without a chance of it being null.
@Inject SpanCustomizer span;
void userCode() {
span.annotate("tx.started");
...
}
开发你自己的RPC基础架构时,先Check instrumentation writtern here 和 Zipkin’list。
RPC tracing经常通过拦截器自动实现。下面场景中,添加tags和events来描述RPC操作的角色。
Client span:
// 在发送请求之前,添加描述信息
span = tracer.newTrace().name("get").type(CLIENT);
span.tag("clnt/finagle.version", "6.36.0");
span.tag(TraceKeys.HTTP_PATH, "/api");
span.remoteEndpoint(Endpoint.builder()
.serviceName("backend")
.ipv4(127 << 24 | 1)
.port(8080).build());
// 当请求开始被调度,开始span
span.start();
// if you have callbacks for when data is on the wire, note those events
span.annotate(Constants.WIRE_SEND);
span.annotate(Constants.WIRE_RECV);
// when the response is complete, finish the span
span.finish();
有时你需要创建一个异步操作,有Request,但是没有Response.在通常的RPC tracing中,使用span.finish()
表明接受到Response。在单向tracing中,使用span.flush()
,因为你不期望响应。
以下是client端模拟怎样构建一个单项tracing操作:
// start a new span representing a client request
oneWaySend = tracer.newSpan(parent).kind(Span.Kind.CLIENT);
// Add the trace context to the request, so it can be propagated in-band
tracing.propagation().injector(Request::addHeader)
.inject(oneWaySend.context(), request);
// fire off the request asynchronously, totally dropping any response
request.execute();
// start the client side and flush instead of finish
oneWaySend.start().flush();
下面是server处理单向跟踪:
// pull the context out of the incoming request
extractor = tracing.propagation().extractor(Request::getHeader);
// convert that context to a span which you can name and add tags to
oneWayReceive = nextSpan(tracer, extractor.extract(request))
.name("process-request")
.kind(SERVER)
... add tags etc.
// start the server side and flush instead of finish
oneWayReceive.start().flush();
// you should not modify this span anymore as it is complete. However,
// you can create children to represent follow-up work.
next = tracer.newSpan(oneWayReceive.context()).name("step2").start();
采样可以用来减少收集和报告的span数据。当span不采样时,不会增加开销。
抽样是预先就决定的,这意味着报告数据的决定是在一个tracer的第一个操作中做出的,而这个决定是向下游传播的。
默认情况下,有一个全局采样器将单一速率应用于所有的操作。Tracer.Builder.sampler
是表示采样信息这一点,它默认跟踪每个请求。
有时候需要根据Java方法或注释进行采样。
大多数用户将使用一个框架拦截器来自动执行这种策略。以下是他们如何在内部工作的:
// derives a sample rate from an annotation on a java method
DeclarativeSampler sampler = DeclarativeSampler.create(Traced::sampleRate);
@Around("@annotation(traced)")
public Object traceThing(ProceedingJoinPoint pjp, Traced traced) throws Throwable {
Span span = tracing.tracer().newTrace(sampler.sample(traced))...
try {
return pjp.proceed();
} finally {
span.finish();
}
}
你可能需要根据操作的内容来应用不同的策略。例如,你可能不想跟踪静态资源(如图片)的请求,或者你可能想将所以请求都追踪到新的api。
大多数用户将使用一个框架的拦截器来自动执行这种策略。以下是他们如何在内部工作的:
Span newTrace(Request input) {
SamplingFlags flags = SamplingFlags.NONE;
if (input.url().startsWith("/experimental")) {
flags = SamplingFlags.SAMPLED;
} else if (input.url().startsWith("/static")) {
flags = SamplingFlags.NOT_SAMPLED;
}
return tracer.newTrace(flags);
}
注意:以上内置Http采样器的基础
需要传播以确保源自同一个根的tracer在相同的轨迹中被收集在一起。最常见的propagating方法是从发送RPC请求的客户端向接收服务的服务器复制tracer上下文。
例如,当一个下游的Http调用被创建时,它的跟踪上下文和它一起被发送,被编码为request headers:
上面的名称来自B3 Propagation
,它时Brave内置的,并且具有许多语言和框架的实现。
client Propagation code:
// configure a function that injects a trace context into a request
injector = tracing.propagation().injector(Request.Builder::addHeader);
// before a request is sent, add the current span's context to it
injector.inject(span.context(), request);
以下是服务端propagation的代码:
// configure a function that extracts the trace context from a request
extracted = tracing.propagation().extractor(Request::getHeader);
// when a server receives a request, it joins or starts a new trace
span = tracer.nextSpan(extracted, request);
有时你需要传播额外的字段,例如请求ID或备用Tracing上下文。例如,如果你在Cloud Foundry环境中,则可能需要传递RequestID:
// when you initialize the builder, define the extra field you want to propagate
tracingBuilder.propagationFactory(
ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-vcap-request-id")
);
// later, you can tag that request ID or use it in log correlation
requestId = ExtraFieldPropagation.current("x-vcap-request-id");
TraceContext.Extractor
从传入请求或消息中读取跟踪标识符和采样状态。carrier通常是一个请求对象或头信息(headers)。
上面方式可以用于像HttpServletHandler这样的标准工具,也可用于自定义RPC或消息传递代码。
TraceContextOrSamplingFlags
通常只用于Tracer.nextSpan(extracted)
,除非你在客户端和服务端之间共享spanID。
正常的instrumentation pattern是创建一个代表RPC的服务端span。Extractor.extract
应用于传入的客户端请求时可能会返回完整的跟踪上下文。
Tracer.joinSpan
尝试继续此跟踪,使用相同的SpanID(如果支持),或者如果不支持则创建子span。
这是一个B3传播的例子:
一些传播系统只转发父spanID,检测时间 Propagation.Factory.supportsJoin() == false
。在这种情况下,一个新的跨度ID总是被配置,并且传入的上下文确定父ID。
注意:有些span报告器不支持共享spanID。例如,如果您设置Tracing.Builder.spanReporter(amazonXrayOrGoogleStackdrive)
,禁用连接通过Tracing.Builder.supportsJoin(false)
。这将迫使创建一个新的child spanTracer.joinSpan()
。
TraceContext.Extractor
由Propagation.Factory
插件实现。在内部,这段代码将TraceContextOrSamplingFlags
使用以下之一创建联合类型:
TraceContext
如果trace和spanID存在。TraceIdContext
如果trace标识存在,但不包含span标识。SamplingFlags
如果没有标识符存在一些Propagation实现从提取点(不包括传入头)读取额外的数据到injection中(不包括写出头文件)。例如,它可能带有一个请求ID。当实现有额外的数据时,这里是他们如何处理它。
TraceContext
已经提取,添加额外的数据为TraceContext.extra()
TraceContextOrSamplingFlags.extra()
,Tracer.nextSpan
处理。Brave 支持“current tracing component”的概念,只有当你没有其他的手段获得参考时才应该使用。这是针对JDBC连接的,因为它们通常在跟踪组件之前初始化。
可以通过Tracing.current()
实例化最新的跟踪组件。或者Tracing.currentTracer()。如果您使用这些方法中的任何一种,不要缓存结果。相反,每次需要时都要查看它们。
Brave支持“current span”的概念,代表了运行中的操作。Tracer.currentSpan()
可以用来添加自定义tags到一个span,Tracer.nextSpan()
可以用来创建任何在运行的child span。
许多框架允许您指定用于用户回调的执行程序。该类型CurrentTraceContext
实现了支持当前span所需的全部功能。它也暴露你可以用来装饰执行者的工具。
CurrentTraceContext currentTraceContext = new CurrentTraceContext.Default();
tracing = Tracing.newBuilder()
.currentTraceContext(currentTraceContext)
...
.build();
Client c = Client.create();
c.setExecutorService(currentTraceContext.executorService(realExecutorService));
在编写新的instrumentation时,重要的是将您创建的span作为当前span。这不仅允许用户访问它Tracer.currentSpan()
,还允许像SLF4J MDC这样的自定义功能查看当前的traceID。
Tracer.withSpanInScope(Span)
有利于这一点,并通过 try-with-resources 最方便地使用,这样不影响外部代码的调用。
try (SpanInScope ws = tracer.withSpanInScope(span)) {
return inboundRequest.invoke();
} finally { // note the scope is independent of the span
span.finish();
}
这极少情况下,你可能需要暂时清除当前的span。例如,启动一个不应该与当前请求关联的任务。要做到这一点,只需将null传递给withSpanInScope
。
try (SpanInScope cleared = tracer.withSpanInScope(null)) {
startBackgroundThread();
}
许多库公开了一个回调模型,而不是一个拦截器。当创建新的instrumentation时,你可能会发现需要在一个回调(例如onStart()
)中放置一个span,并在另一个回调(例如onFinish()
)中结束span。
如果库保证这些运行在同一个线程上,则可以简单地Tracer.withSpanInScope(Span)
将开始回调的结果传播到关闭的时。这通常是通过请求域属性完成的。
这是一个例子:
class MyFilter extends Filter {
public void onStart(Request request, Attributes attributes) {
// Assume you have code to start the span and add relevant tags...
// We now set the span in scope so that any code between here and
// the end of the request can see it with Tracer.currentSpan()
SpanInScope spanInScope = tracer.withSpanInScope(span);
// We don't want to leak the scope, so we place it somewhere we can
// lookup later
attributes.put(SpanInScope.class, spanInScope);
}
public void onFinish(Response response, Attributes attributes) {
// as long as we are on the same thread, we can read the span started above
Span span = tracer.currentSpan();
// Assume you have code to complete the span
// We now remove the scope (which implicitly detaches it from the span)
attributes.remove(SpanInScope.class).close();
}
}
有时你必须建立一个库,在请求和响应之间没有共享的Contex。对于这种情况,您可以使用ThreadLocalSpan
临时存储回调之间的span。
这是一个例子:
class MyFilter extends Filter {
final ThreadLocalSpan threadLocalSpan;
public void onStart(Request request) {
// Assume you have code to start the span and add relevant tags...
// We now set the span in scope so that any code between here and
// the end of the request can see it with Tracer.currentSpan()
threadLocalSpan.set(span);
}
public void onFinish(Response response, Attributes attributes) {
// as long as we are on the same thread, we can read the span started above
Span span = threadLocalSpan.remove();
if (span == null) return;
// Assume you have code to complete the span
}
}
上面的例子工作,回调发生在同一个线程。如果你无法在同一个线程上关闭该scope,则不应将span设置为scope。在一些异步库中可能会出现这种情况。通常,您需要直接在自定义属性中传播span。这将允许您跟踪RPC,即使这种方法不利于使用Tracer.currentSpan()
外部代码。
下面是一个显式传播的例子:
class MyFilter extends Filter {
public void onStart(Request request, Attributes attributes) {
// Assume you have code to start the span and add relevant tags...
// We can't open a scope as onFinish happens on another thread.
// Instead, we propagate the span manually so at least basic tracing
// will work.
attributes.put(Span.class, span);
}
public void onFinish(Response response, Attributes attributes) {
// We can't rely on Tracer.currentSpan(), but we can rely on explicit
// propagation
Span span = attributes.remove(Span.class);
// Assume you have code to complete the span
}
如果您处于需要在运行时关闭跟踪的情况,请调用Tracing.setNoop(true)
。这将把任何新的span变成“noop”span,并且丢弃所有数据直到Tracing.setNoop(false)
被调用。
Brave已经建立在性能的基础上。使用核心Span api,可以在几微秒内记录跨度。当跨度采样时,实际上没有开销(因为它是一个noop)。
与以前的实现不同,“Brave4”只需要一个时间戳。所有注释都是使用较便宜和更精确的System.nanoTime()功能以偏移量记录的。
在编写单元测试时,有一些技巧可以使错误更容易找到:
StrictCurrentTraceContext
露出微妙的传播错误以下是您的单元测试的一个示例设置:
ConcurrentLinkedDeque spans = new ConcurrentLinkedDeque<>();
Tracing tracing = Tracing.newBuilder()
.currentTraceContext(new StrictCurrentTraceContext())
.spanReporter(spans::add)
.build();
@After public void close() {
Tracing current = Tracing.current();
if (current != null) current.close();
}
注意:原创文章,欢迎转载,请注明出处。