由于分布式应用是由多个组件组成的,且这些组件往往是由不同的团队拥有和操作,所以在与应用程序发生交互时,就会需要跨多个组件执行代码的分布式跟踪。如果用户遇到了问题,想要确定是哪个组件出现了差错,基本就是一件不可能完成的事情。
与单体应用程序相比,分布式应用程序的特点之一就是很难将单个分布式跟踪的遥测(如日志)关联起来。虽然用户可以通过查看日志了解到每个组件是如何处理每个请求的,但是很难知道一个组件中的请求和另一个组件中的请求是否属于同一分布式跟踪。
为了解决这个问题,应用性能管理厂商(APM)会提供从一个组件到另一个组件的分布式跟踪上下文传播功能。但是因为很多环境具有异构性、组件归属不同的团队,并通过不同的工具进行监视,因此对分布式应用程序进行一致的测试仍是难点。
随着W3C Trace Contex 规范逐步过渡到 Proposed Recommendation maturity 级别,再加上其它供应商和平台对该规范的支持,上下文传播的复杂性正在降低。W3C Trace Contex定义了分布式跟踪上下文的语义和格式,这使得分布式应用程序中的每个组件都可以理解上下文,并将其传播到调用的组件中。
为了使得分布式应用程序开发更容易,微软做了很多努力,例如Orleans 框架和 Dapr项目,而在分布式跟踪上下文传播,微软的服务和平台将采用 W3C Trace Contex 规范。同时,针对分布式跟踪和日志,.NET Core 3.0 做了很多新工作。
.NET Core 3.0在分布式跟踪方面的最新改进:
为了帮助大家更深刻的理解.NET Core 3.0的新改进,下面我们做了一个示例。
该示例中,我们需要用到三个简单的组件:ClientApp、FrontEndApp和BackEndApp。BackEndApp是一个名为WeatherApp的模板ASP.NET Core应用程序,可以通过公开的REST API来获取天气预报。而FrontEndApp会通过控制权将所有传入的请求发送给BackEndApp。
[ApiController]
[Route("[controller]")]
public class WeatherForecastProxyController : ControllerBase
{
private readonly ILogger<WeatherForecastProxyController> _logger;
private readonly HttpClient _httpClient;
public WeatherForecastProxyController(
ILogger<WeatherForecastProxyController> logger,
HttpClient httpClient)
{
_logger = logger;
_httpClient = httpClient;
}
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
var jsonStream = await
_httpClient.GetStreamAsync("http://localhost:5001/weatherforecast");
var weatherForecast = await
JsonSerializer.DeserializeAsync<IEnumerable<WeatherForecast>>(jsonStream);
return weatherForecast;
}
}
ClientApp 是一个 .NET Core 3.0 Windows Forms app,会调用FrontEndApp进行天气预报。
private async Task<string> GetWeatherForecast()
{
return await _httpClient.GetStringAsync(
"http://localhost:5000/weatherforecastproxy");
}
需要注意的是,在这段示例中,没有安装任何额外的SDK和库。
我们通过ClientApp调用来观察FrontEndApp和BackEndApp生成的日志。
FrontEndApp :
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
=> ConnectionId:0HLR1BR0PL1CH
=> RequestPath:/weatherforecastproxy
RequestId:0HLR1BR0PL1CH:00000001,
SpanId:|363a800a-4cf070ad93fe3bd8.,
TraceId:363a800a-4cf070ad93fe3bd8,
ParentId:
Executed endpoint 'FrontEndApp.Controllers.WeatherForecastProxyController.Get (FrontEndApp)'
BackEndApp:
info: BackEndApp.Controllers.WeatherForecastController[0]
=> ConnectionId:0HLR1BMQHFKRL
=> RequestPath:/weatherforecast
RequestId:0HLR1BMQHFKRL:00000002,
SpanId:|363a800a-4cf070ad93fe3bd8.94c1cdba_,
TraceId:363a800a-4cf070ad93fe3bd8,
ParentId:|363a800a-4cf070ad93fe3bd8.
Executed endpoint 'FrontEndApp.Controllers.WeatherForecastController.Get (BackEndApp)'
与magic一样,来自两个独立应用程序的日志共享相同的TraceId。ASP.NET Core 3.0应用程序会初始化分布式跟踪上下文,并将其传递到头文件中。
FrontEndApp并没有收到任何其它的头文件,出现这种情况的原因是在ASP.NET Core应用程序中,分布式跟踪是由ASP.NET Core 框架自身在每次传入请求时启动的。
相信很多人都注意到了Windows 窗体应用 ClientApp和 ASP.NET Core FrontEndApp在行为上的差异。ClientApp 未设置任何分布式跟踪上下文,因此FrontEndApp也不会收到。设置分布式操作最简单的方法是,使用DiagnosticSource包中名为Activity的API。
private async Task<string> GetWeatherForecast()
{
var activity = new Activity("CallToBackend").Start();
try
{
return await _httpClient.GetStringAsync(
"http://localhost:5000/weatherforecastproxy");
}
finally
{
activity.Stop();
}
}
启动之后,HttpClient就知道需要传播分布式跟踪上下文。需要注意的是,ClientApp、FrontEndApp和BackEndApp都共享同一个TraceId。
上下文使用Request-Id进行传播,这是从ASP中引入的,是默认生效的。但是,W3C Trace Context 正被广泛采用,因此,我们建议切换到W3C Trace Context的上下文传播格式。
在.NET Core 3.0中,切换到W3C Trace Context 格式只需要在主方法中添加一行代码:
static void Main()
{
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
…
Application.Run(new MainForm());
}
当FrontEndApp收到来自ClientApp的请求时,你会在请求中看到一个traceparent头文件:
通过这个头文件,.NET Core 应用程序就会明白现在需要通过W3C Trace Context 格式来进行传播调用。需要注意的是,.NET Core应用程序可以自动识别W3C Trace Context 的正确格式,但是将分布式跟踪上下文的默认格式切换到W3C Trace Context,可以在异构环境中实现更好的互操作性。
所有属性为TraceId和SpanId的日志:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
=> ConnectionId:0HLQV2BC3VP2T
=> RequestPath:/weatherforecast
RequestId:0HLQV2BC3VP2T:00000001,
SpanId:da13aa3c6fd9c146,
TraceId:f11a03e3f078414fa7c0a0ce568c8b5c,
ParentId:5076c17d0a604244
Request starting HTTP/1.1 GET http://localhost:5000/weatherforecast
OpenTelemetry提供了一组API、库、代理和控制器服务,用于从应用程序捕获分布式跟踪和度量。
在调用时启动AddOpenTelemetry,就可以在BackEndApp上启用OpenTelemetry:
services.AddOpenTelemetry(b =>
b.UseZipkin(o => {
o.ServiceName="BackEndApp";
o.Endpoint=new Uri("http://zipkin /api/v2/spans");
})
.AddRequestCollector());
FrontEndApp日志中的TraceId与BackEndApp中的TraceId匹配。
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
=> ConnectionId:0HLR2RC6BIIVO
=> RequestPath:/weatherforecastproxy
RequestId:0HLR2RC6BIIVO:00000001,
SpanId:54e2de7b9428e940,
TraceId:e1a9b61ec50c954d852f645262c7b31a,
ParentId:69dce1f155911a45
=> FrontEndApp.Controllers.WeatherForecastProxyController.Get (FrontEndApp)
Executed action FrontEndApp.Controllers.WeatherForecastProxyController.Get (FrontEndApp) in 3187.3112ms
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
=> ConnectionId:0HLR2RLEHSKBV
=> RequestPath:/weatherforecast
RequestId:0HLR2RLEHSKBV:00000001,
SpanId:0e783a0867544240,
TraceId:e1a9b61ec50c954d852f645262c7b31a,
ParentId:54e2de7b9428e940
=> BackEndApp.Controllers.WeatherForecastController.Get (BackEndApp)
Executed action BackEndApp.Controllers.WeatherForecastController.Get (BackEndApp) in 3085.9111ms
此外,Zipkin将报告相同的跟踪,因此,可以将分布式跟踪工具收集的分布式跟踪与来自计算机的日志关联起来。当ClientApp遇到问题时,可以将此TraceId提供给用户,由于用户和应用程序可以共享,因此能够更加容易的跨组件发现相应的日志和分布式跟踪。
另外,用户可以轻松启用对三个组件的监控,并在甘特图上查看:
APM厂商收集到的遥测数据和ASP .NET Core 使用的分布式跟踪上下文是相关的。因此,ASP .NET Core 3.0 应用程序非常适合不同团队拥有不同组件的场景。
例如,下图中的两个应用A和 C,启用了类似于OpenTelemetry的SDK遥测采集。如果不使用ASP .NET Core 3.0,那么应用程序B就会“破坏”跟踪,导致分布式跟踪无法起作用。
在大多数部署中,ASP.NET Core应用程序配置为启用基本日志记录,因此应用程序B将传播分布式跟踪上下文。而来自A和C的分布轨迹将相互关联。在以前的应用程序中,如果ClientApp和BackEndApp 被感知,而FrontEndApp没有被感知,仍然可以看到分布式跟踪是相关的:
ASP.NET Core应用程序非常适合服务网格环境。在服务网格部署中,上图中的A和C可以表示服务网格。为了让服务网格请求进入和离开组件B,应用程序必须包含某些头文件。
虽然Istio能够自动发送span,但是仍需要一些提示来连接整个跟踪。应用程序需要使用合适的HTTP头文件,以便在发送span消息时,能够正确关联到单个跟踪中。如果是采用W3C Trace Context格式,ASP.NET Core应用程序则无需做任何改变。
如果你希望能够在分布式应用程序的组件之间共享更多上下文,可以添加以下属性:
private async Task<string> GetWeatherForecast()
{
var activity = new Activity("CallToBackend")
.AddBaggage("appVersion", "v1.0")
.Start();
try
{
return await _httpClient.GetStringAsync(
"http://localhost:5000/weatherforecastproxy");
}
finally
{
activity.Stop();
}
}
服务器端,在FrontEndApp和BackEndApp可以看到一个额外的头文件Correlation-Context。
使用 Activity.Baggage:
var appVersion = Activity.Current.Baggage.FirstOrDefault(b => b.Key == "appVersion").Value;
using (_logger.BeginScope($"appVersion={appVersion}"))
{
_logger.LogInformation("this weather forecast is from random source");
}
作用域中包含appVersion:
info: FrontEndApp.Controllers.WeatherForecastController[0]
=> ConnectionId:0HLQV353507UG
=> RequestPath:/weatherforecast
RequestId:0HLQV353507UG:00000001,
SpanId:37a0f7ebf3ecac42,
TraceId:c7e07b7719a7a3489617663753f985e4,
ParentId:f5df77ba38504846
=> FrontEndApp.Controllers.WeatherForecastController.Get (BackEndApp)
=> appVersion=v1.0
this weather forecast is from random source
随着ASP.NET Core 3.0的改进,很多ASP .NET Core 包含的功能可能就难以使用了,比如开发人员和DevOps想要做一个交钥匙遥测解决方案,需要和很多APM的厂商来做。但是,我们在OpenTelemetry方面会加大投入,使得更多ASP .NET Core用户能够在监控和故障排除方面变得更容易。
我们会帮助用户采用W3C Trace Context,并且在ASP .NET Core的未来版本中可能将其作为默认的分布式跟踪上下文传播格式。
另外,我们还会专注于改进分布式上下文传播场景。与Monolits相比,分布式应用程序在单个分布式跟踪的生存期缺少公共共享状态,而这种共享状态可以用于基本的日志记录、用于请求的高级路由、实验、A/B测试、业务上下文传播等。
原文链接:
领取专属 10元无门槛券
私享最新 技术干货