背景
微服务架构成为了平台端的主要架构方向,它将庞大的单体应用拆分成多个子系统和公共的组件单元。这一理念带来了许多好处:复杂系统的拆分简化与隔离、公共模块的重用性提升与更合理的资源分配、大大提升了系统变更迭代的速度、更灵活的可扩展性以及在云计算中的适用性,等等。
但是微服务架构也带来了新的问题:拆分后每个用户请求可能需要数十个子系统的相互调用才能最终返回结果,如果某个请求出错可能需要逐个子系统排查定位问题;或者某个请求耗时比较高,但是很难知道时间耗在了哪个子系统中。
框架选型
跑腿目前的应用类型有很多种,为了使跨语言的调用链路串联在一起,我们选择了Zipkin作为链路追踪服务,它是基于OpenTracing链路追踪协议的。
OpenTracing的基本思路使在链路的入口应用中生成 traceId 和 spanId,在后续的各节点调用中,traceId 保持不变并全链路透传,各节点只产生自己的新的 spanId。这样,通过 traceId 唯一标识一条链路,spanId 标识链路中的具体节点的方式串起整个链路。
在记录调用链路的同时,还把请求发出、接收、处理的时间都记录下来,计算业务处理耗时和网络耗时,然后用可视化界面展示出来每个调用链路,性能,故障。
除了这些还可以自定义要记录的信息,比如发起调用服务名称、被调服务名称、返回结果、IP、调用服务的名称等。
C#埋点SDK
- 使用HttpModule 对所有请求进行拦截
public class TracingModule : IHttpModule
{
public void Init(HttpApplication application)
{
ZipkinTracing.Start();
if (ZipkinTracing.EnableTracing)
{
application.BeginRequest += Application_BeginRequest;
application.EndRequest += Application_EndRequest;
}
}
private void Application_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
var traceExtractor = Propagations.B3String.Extractor<NameValueCollection>((carrier, key) => carrier[key]);
var traceContext = traceExtractor.Extract(app.Request.Headers);
var traces = traceContext == null ? Trace.Create() : Trace.CreateFromId(traceContext);
ZipkinTracing.CurrentTrace = traces;
var headerkeys = app.Request.Headers.AllKeys;
if (!headerkeys.Contains(ZipkinHttpHeaders.TraceId, StringComparer.OrdinalIgnoreCase))
app.Request.Headers.Add(ZipkinHttpHeaders.TraceId, traces.CurrentSpan.SerializeTraceId());
traces.Record(Annotations.ServiceName(App.Instance.ConfigurationManager.GetAppSettings("LogApplicationName")));
traces.Record(Annotations.Rpc(app.Request.HttpMethod + " " + app.Request.Url.AbsolutePath));
traces.Record(Annotations.Tag("server.name", Environment.MachineName));
traces.Record(Annotations.Tag("http.host", app.Request.Url.Host));
traces.Record(Annotations.Tag("http.path", app.Request.Url.PathAndQuery));
traces.Record(Annotations.Tag("http.url", app.Request.Url.OriginalString));
traces.Record(Annotations.ServerRecv());
}
private void Application_EndRequest(object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
var error = app.Server.GetLastError();
if (error != null)
{
StringBuilder errorInfo = new StringBuilder();
while (error != null)
{
errorInfo.AppendLine(error.Source + " " + error.Message);
error = error.InnerException;
}
ZipkinTracing.CurrentTrace?.Record(Annotations.Event("error"));
ZipkinTracing.CurrentTrace?.Record(Annotations.Tag("error", errorInfo.ToString()));
}
ZipkinTracing.CurrentTrace?.Record(Annotations.ServerSend());
}
public void Dispose()
{
}
}
- 注册自定义的应用模块
通过代码进行模块注册,避免修改Web.cofing。这种模块注册的执行顺序将在Web.config中配置的模块之后执行。
[assembly: PreApplicationStartMethod(typeof(TracingModuleFactory), "Start")]
namespace PaoTui.ServiceTracing
{
public class TracingModuleFactory
{
public static void Start()
{
HttpApplication.RegisterModule(typeof(TracingModule));
}
}
}
- 自定义一个 http 请求的处理管道,用来在发出请求前附加链路信息
public class HttpClientTracingDelegating : DelegatingHandler
{
string _serviceName = "";
public HttpClientTracingDelegating(HttpMessageHandler httpMessageHandler) : base(httpMessageHandler ?? new HttpClientHandler())
{
_serviceName = App.Instance.ConfigurationManager.GetAppSettings("LogApplicationName");
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
var _trace = ZipkinTracing.CurrentTrace?.Child();
if (ZipkinTracing.EnableTracing && _trace != null)
{
_trace.Record(Annotations.ServiceName(_serviceName));
request.Headers.Add(ZipkinHttpHeaders.TraceId, _trace.CurrentSpan.SerializeTraceId());
request.Headers.Add(ZipkinHttpHeaders.SpanId, _trace.CurrentSpan.SerializeSpanId());
if (_trace.CurrentSpan.ParentSpanId != null) request.Headers.Add(ZipkinHttpHeaders.ParentSpanId, _trace.CurrentSpan.SerializeParentSpanId());
if (_trace.CurrentSpan.Sampled.HasValue) request.Headers.Add(ZipkinHttpHeaders.Sampled, _trace.CurrentSpan.SerializeSampledKey());
request.Headers.Add(ZipkinHttpHeaders.Flags, _trace.CurrentSpan.SerializeDebugKey());
_trace.Record(Annotations.Rpc(request.Method.ToString() + " " + request.RequestUri.AbsolutePath));
_trace.Record(Annotations.ClientSend());
_trace.Record(Annotations.Tag("server.name", Environment.MachineName));
_trace.Record(Annotations.Tag("http.host", request.RequestUri.Host));
_trace.Record(Annotations.Tag("http.path", request.RequestUri.PathAndQuery));
_trace.Record(Annotations.Tag("http.url", request.RequestUri.OriginalString));
}
var httptask = base.SendAsync(request, cancellationToken);
httptask.ContinueWith(task =>
{
if (task.Exception != null)
{
_trace?.Record(Annotations.Event("error"));
_trace?.Record(Annotations.Tag("error", task.Exception.Source + " " + task.Exception.Message));
}
_trace?.Record(Annotations.ClientRecv());
}, cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
return httptask;
}
}
链路分析
- 调用链路
我们通过对一次请求进行还原,形成一个完整的调用链路图,如下图。界面中展示请求依次通过的各个服务节点的情况。比如请求经过每个服务节点的 IP、发送与响应时间点、处理结果与处理时长,网络传输时长、异常描述等信息。
- 问题快速定位
我们现在可以在界面中快速的定位到问题出现的服务节点、机器 IP 与问题原因。以前我们遇到线上问题时,通常需要同时登录 N 台机器抓日志排查,有时还可能需要关联上下游项目一起排查。
- 瓶颈节点
能够快速定位链路调用种的瓶颈节点,由于该节点的耗时,导致了整个链路调用的耗时延长,因此针对该节点进行优化,进而达到优化整个链路的效率。
- 代码优化
能够检测到同一个接口的重复调用,通过对这种请求优化成批量请求或者相同参数只请求一次,能够快速提升节点的服务质量。
- 服务性能分析
通过对链路趋势的变化跟踪,能够直观的得到接口的请求量、请求耗时波动。
- 流量预警
通过对实时链路数据的分析,感知服务的流量和耗时变化,及时对异常的流量和耗时波动进行预警。
总结
当前链路追踪系统已经在平台、后台上线使用,追踪监控着C#,JAVA超过150个应用,每日链路数量超过10亿。链路追踪系统在故障排查,性能分析等场景起都起到了非常重要的作用,极大的提高了工作效率。