程序记录的日志通常有两种做用,故障排查、显式程序运行状态,当程序发生故障时,咱们能够经过日志定位问题,日志能够给咱们留下排查故障的依据。不少时候,每每会认为日志记录很是简单,例如不少程序只是 try-catch{}
,直接输出到 .txt
,可是这些日志每每没法起到帮助定位问题的做用,甚至日志充斥了大量垃圾内容;日志内容全靠人眼一行行扫描,或者 Ctrl+F
搜索,没法高效率审查日志;日志单纯输出到文本文件中,没有很好地管理日志。html
接下来,咱们将一步步学习日志的编写技巧,以及 OpenTracing API 、Jaeger 分布式链路跟踪的相关知识。前端
最简单的日志,就是控制台输出,利用 Console.WriteLine()
函数直接输出信息。git
下面时一个简单的信息输出,当程序调用 SayHello
函数时,SayHello
会打印信息。github
public class Hello { public void SayHello(string content) { var str = $"Hello,{content}"; Console.WriteLine(str); } } class Program { static void Main(string[] args) { Hello hello = new Hello(); hello.SayHello("any one"); Console.Read(); } }
经过控制台,咱们能够看到,为了记录日志,咱们必须在函数内编写输入日志的代码,优缺点这些就很少说了,咱们能够经过 AOP 框架,实现切面编程,同一记录日志。web
这里可使用笔者开源的 CZGL.AOP 框架,Nuget 中能够搜索到。docker
编写统一的切入代码,这些代码将在函数被调用时执行。shell
Before
会在被代理的方法执行前或被代理的属性调用时生效,你能够经过 AspectContext
上下文,获取、修改传递的参数。编程
After 在方法执行后或属性调用时生效,你能够经过上下文获取、修改返回值。api
public class LogAttribute : ActionAttribute { public override void Before(AspectContext context) { Console.WriteLine($"{context.MethodInfo.Name} 函数被执行前"); } public override object After(AspectContext context) { Console.WriteLine($"{context.MethodInfo.Name} 函数被执行后"); return null; } }
改造 Hello 类,代码以下:缓存
[Interceptor] public class Hello { [Log] public virtual void SayHello(string content) { var str = $"Hello,{content}"; Console.WriteLine(str); } }
而后建立代理类型:
static void Main(string[] args) { Hello hello = AopInterceptor.CreateProxyOfClass<Hello>(); hello.SayHello("any one"); Console.Read(); }
启动程序,会输出:
SayHello 函数被执行前 Hello,any one SayHello 函数被执行后
你彻底不须要担忧 AOP 框架会给你的程序带来性能问题,由于 CZGL.AOP 框架采用 EMIT 编写,而且自带缓存,当一个类型被代理过,以后无需重复生成。
CZGL.AOP 能够经过 .NET Core 自带的依赖注入框架和 Autofac 结合使用,自动代理 CI 容器中的服务。这样不须要 AopInterceptor.CreateProxyOfClass
手动调用代理接口。
CZGL.AOP 代码是开源的,能够参考笔者另外一篇博文:
http://www.javashuo.com/article/p-smjfxqls-ns.html
Microsoft.Extensions.Logging
有些公司无技术管理规范,不一样的开发人员使用不一样的日志框架,一个产品中可能有 .txt
、NLog
、Serilog
等,而且没有同一的封装。
.NET Core 中的日志组件有不少,可是流行的日志框架基本都会实现 Microsoft.Extensions.Logging.Abstractions
,所以咱们能够学习Microsoft.Extensions.Logging
。Microsoft.Extensions.Logging.Abstractions
是官方对日志组件的抽象,若是一个日志组件并不支持 Microsoft.Extensions.Logging.Abstractions
那么这个组件很容易跟项目糅合的,后续难以模块化以及下降耦合程度。
Microsoft.Extensions.Logging
软件包中包含 Logging API ,这些 Logging API 不能独立运行。它与一个或多个日志记录提供程序一块儿使用,这些日志记录提供程序将日志存储或显示到特定输出,例如 Console, Debug, TraceListeners。
下图是 .NET Core 中 Loggin API 的层次结构:
图片来源:https://www.tutorialsteacher.com/
说实话,Microsoft.Extensions.Logging
刚开始是学着很懵,配置感受很复杂。所以,有一张清晰的结构图很重要,能够帮助你们理解里面的 Logging API。
.NET Core 中不少标准接口都实践了工厂模式的思想,ILoggerFactory 正是工厂模式的接口,而 LoggerFactory 是工厂模式的实现。
其定义以下:
public interface ILoggerFactory : IDisposable { ILogger CreateLogger(string categoryName); void AddProvider(ILoggerProvider provider); }
ILoggerFactory 工厂接口的做用是建立一个 ILogger 类型的实例,即 CreateLogger
接口。
经过实现ILoggerProvider
接口能够建立本身的日志记录提供程序,表示能够建立 ILogger 实例的类型。
其定义以下:
public interface ILoggerProvider : IDisposable { ILogger CreateLogger(string categoryName); }
ILogger 接口提供了将日志记录到基础存储的方法,其定义以下:
public interface ILogger { void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter); bool IsEnabled(LogLevel logLevel); IDisposable BeginScope<TState>(TState state); }
logging providers 称为日志记录程序。
Logging Providers 将日志显示或存储到特定介质,例如 console, debugging event, event log, trace listener 等。
Microsoft.Extensions.Logging
提供了如下类型的 logging providers,咱们能够经过 Nuget 获取。
而 Serilog 则有 File、Console、Elasticsearch、Debug、MSSqlServer、Email等。
这些日志提供程序有不少,咱们没必要细究;若是一个日志组件,不提供兼容 Microsoft.Extensions.Logging
的实现,那么根本不该该引入他。
实际上,不少程序是直接 File.Write("Log.txt")
,这种产品质量能好到哪里去呢?
前面,介绍了 Microsoft.Extensions.Logging
的组成,这里将学习如何使用 Logging Provider 输入日志。
起码提到,它只是提供了一个 Logging API,所以为了输出日志,咱们必须选择合适的 Logging Provider 程序,这里咱们选择
Microsoft.Extensions.Logging.Console
,请在 Nuget 中引用这个包。
下图是 Logging Provider 和 ConsoleLogger 结合使用的结构图:
从常规方法来弄,笔者发现,无法配置呀。。。
ConsoleLoggerProvider consoleLoggerProvider = new ConsoleLoggerProvider( new OptionsMonitor<ConsoleLoggerOptions>( new OptionsFactory<ConsoleLoggerOptions>( new IEnumerable<IConfigureOptions<TOptions>(... ... ...))));
因此只能使用如下代码快速建立工厂:
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(options => { options.IncludeScopes = true; options.SingleLine = true; options.TimestampFormat = "hh:mm:ss "; }));
或者:
ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
固然工厂中能够添加其它日志提供程序,示例:
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(...) .AddFile(...) .Add()... );
而后获取 ILogger 实例:
ILogger logger = loggerFactory.CreateLogger<Program>();
记录日志:
logger.LogInformation("记录信息");
Logging API 中,规定了 7 种日志等级,其定义以下:
public enum LogLevel { Debug = 1, Verbose = 2, Information = 3, Warning = 4, Error = 5, Critical = 6, None = int.MaxValue }
咱们能够经过 ILogger 中的函数,输出如下几种等级的日志:
logger.LogInformation("Logging information."); logger.LogCritical("Logging critical information."); logger.LogDebug("Logging debug information."); logger.LogError("Logging error information."); logger.LogTrace("Logging trace"); logger.LogWarning("Logging warning.");
关于 Microsoft.Extensions.Logging
这里就再也不赘述,读者能够等级如下连接,了解更多相关知识:
https://www.tutorialsteacher.com/core/fundamentals-of-logging-in-dotnet-core
Debug 、Trace 这两个类的命名空间为 System.Diagnostics
,Debug 、Trace 提供一组有助于调试代码的方法和属性。
读者能够参考笔者的另外一篇文章:
http://www.javashuo.com/article/p-uxojcwwb-ny.html
输出到控制台:
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); Debug.WriteLine("信息");
链路追踪能够帮助开发者快速定位分布式应用架构下的性能瓶颈,提升微服务时代的开发诊断效率。
前面提到的 Trace 、Debug 是 .NET Core 中提供给开发者用于诊断程序和输出信息的 API,而接着提到的 trace 只 OpenTracing API 中的 链路跟踪(trace)。
普通的日志记录有很大的缺点,就是每一个方法记录一个日志,咱们没法将一个流程中被调用的多个方法联系起来。当一个方法出现异常时,咱们很难知道是哪一个任务过程出现的异常。咱们只能看到哪一个方法出现错误,已经它的调用者。
在 OpenTracing 中,Trace 是具备 Span(跨度) 的有向无环图。一个 Span 表明应用程序中完成某些工做的逻辑表示,每一个 Span 都具备如下属性:
为了弄清楚,Trace 和 Span 是什么,OpenTracing 又是什么,请在 Nuget 中引入 OpenTracing
。
编写 Hello 类以下:
public class Hello { private readonly ITracer _tracer; private readonly ILogger<Hello> _logger; public Hello(ITracer tracer, ILoggerFactory loggerFactory) { _tracer = tracer; _logger = loggerFactory.CreateLogger<Hello>(); } public void SayHello(string content) { // 建立一个 Span 并开始 var spanBuilder = _tracer.BuildSpan("say-hello"); // ------------------------------- var span = spanBuilder.Start(); // | var str = $"Hello,{content}"; // | _logger.LogInformation(str); // | span.Finish(); // | // --------------------------------- } }
启动程序,并开始追踪:
static void Main(string[] args) { using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); Hello hello = new Hello(GlobalTracer.Instance, loggerFactory); hello.SayHello("This trace"); Console.Read(); }
在以上过程当中,咱们使用了 OpenTracing API,下面是关于代码中一些元素的说明:
say-hello
;Start()
开始一个 Span;使用 Finish()
结束一个 Span;固然,咱们运行上面的程序时,是没有出现别的信息以及 UI 界面,这是由于 GlobalTracer.Instance
会返回一个无操做的 tracer。当咱们定义一个 Tracer 时,能够观察到链路追踪的过程。
在 Nuget 中,引入 Jaeger
。
在 Program 中,添加一个静态函数,这个函数返回了一个自定义的 Tracer:
private static Tracer InitTracer(string serviceName, ILoggerFactory loggerFactory) { var samplerConfiguration = new Configuration.SamplerConfiguration(loggerFactory) .WithType(ConstSampler.Type) .WithParam(1); var reporterConfiguration = new Configuration.ReporterConfiguration(loggerFactory) .WithLogSpans(true); return (Tracer)new Configuration(serviceName, loggerFactory) .WithSampler(samplerConfiguration) .WithReporter(reporterConfiguration) .GetTracer(); }
修改 Main 函数内容以下:
static void Main(string[] args) { using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); var tracer = InitTracer("hello-world", loggerFactory); Hello hello = new Hello(tracer, loggerFactory); hello.SayHello("This trace"); Console.Read(); }
完整代码:https://gist.github.com/whuanle/b57fe79c9996988db0a9b812f403f00e
可是,日志直接输出 string 是很不友好的,这时,咱们须要结构化日志。
固然,ISpan 提供告终构化日志的方法,咱们能够编写一个方法,用于格式化日志。
在 Hello 类中添加如下代码:
private string FormatString(ISpan rootSpan, string helloTo) { var span = _tracer.BuildSpan("format-string").Start(); try { var helloString = $"Hello, {helloTo}!"; span.Log(new Dictionary<string, object> { [LogFields.Event] = "string.Format", ["value"] = helloString }); return helloString; } finally { span.Finish(); } }
另外,咱们还能够封装一个输出字符串信息的函数:
private void PrintHello(ISpan rootSpan, string helloString) { var span = _tracer.BuildSpan("print-hello").Start(); try { _logger.LogInformation(helloString); span.Log("WriteLine"); } finally { span.Finish(); } }
将 SayHello 方法改为:
public void SayHello(string content) { var spanBuilder = _tracer.BuildSpan("say-hello"); var span = spanBuilder.Start(); var str = FormatString(span, content); PrintHello(span,str); span.Finish(); }
改以上代码的缘由是,不要在一个方法中糅合太多代码,能够尝试将一些代码复用,封装一个统一的代码。
可是,本来咱们只须要调用 SayHello 一个方法,这里一个方法会继续调用另外两个方法。本来是一个 Span,最后变成三个 Span。
info: Jaeger.Configuration[0] info: Jaeger.Reporters.LoggingReporter[0] Span reported: 77f1a24676a3ffe1:77f1a24676a3ffe1:0000000000000000:1 - format-string info: ConsoleApp1.Hello[0] Hello, This trace! info: Jaeger.Reporters.LoggingReporter[0] Span reported: cebd31b028a27882:cebd31b028a27882:0000000000000000:1 - print-hello info: Jaeger.Reporters.LoggingReporter[0] Span reported: 44d89e11c8ef51d6:44d89e11c8ef51d6:0000000000000000:1 - say-hello
注:0000000000000000
表示一个 Span 已经结束。
优势:从代码上看,SayHello -> FormaString ,SayHello -> PrintHello,咱们能够清晰知道调用链路;
缺点:从输出来看,Span reported 不一样,咱们没法中输出中判断三个函数的因果关系;
咱们不可能时时刻刻都盯着代码来看,运维人员和实施人员也不可能拿着代码去对比以及查找代码逻辑。
ITracer 负责建立链路追踪,所以 ITracer 也提供了组合多个 Span 因果关系的 API。
使用方法以下:
var rootSapn = _tracer.BuildSpan("say-hello"); // A var span = _tracer.BuildSpan("format-string").AsChildOf(rootSpan).Start(); // B // A -> B
咱们建立了一个 rootSpan ,接着建立一个延续 rootSpan 的 sapn
,rootSpan -> span
。
info: Jaeger.Reporters.LoggingReporter[0] Span reported: 2f2c7b36f4f6b0b9:3dab62151c641380:2f2c7b36f4f6b0b9:1 - format-string info: ConsoleApp1.Hello[0] Hello, This trace! info: Jaeger.Reporters.LoggingReporter[0] Span reported: 2f2c7b36f4f6b0b9:9824227a41539786:2f2c7b36f4f6b0b9:1 - print-hello info: Jaeger.Reporters.LoggingReporter[0] Span reported: 2f2c7b36f4f6b0b9:2f2c7b36f4f6b0b9:0000000000000000:1 - say-hello
Span reported: 2f2c7b36f4f6b0b9
输出顺序为执行完毕的顺序,say-hello 是最后才执行完成的。
从什么代码中,你们发现,代码比较麻烦,由于:
try-finally{}
确保可以完成 Span为此, OpenTracing API 提供了一种更好的方法,咱们能够避免将 Span 做为参数传递给代码,能够统一自行调用 _tracer 便可。
修改 FormatString
和 PrintHello
代码以下:
private string FormatString(string helloTo) { using var scope = _tracer.BuildSpan("format-string").StartActive(true); var helloString = $"Hello, {helloTo}!"; scope.Span.Log(new Dictionary<string, object> { [LogFields.Event] = "string.Format", ["value"] = helloString }); return helloString; } private void PrintHello(string helloString) { using var scope = _tracer.BuildSpan("print-hello").StartActive(true); _logger.LogInformation(helloString); scope.Span.Log(new Dictionary<string, object> { [LogFields.Event] = "WriteLine" }); }
修改 SayHello 代码以下:
public void SayHello(string helloTo) { using var scope = _tracer.BuildSpan("say-hello").StartActive(true); scope.Span.SetTag("hello-to", helloTo); var helloString = FormatString(helloTo); PrintHello(helloString); }
经过上面的代码,咱们实现去掉了那些烦人的代码。
StartActive()
代替Start()
,经过将其存储在线程本地存储中来使 span 处于“活动”状态;StartActive()
返回一个IScope
对象而不是一个对象ISpan
。IScope是当前活动范围的容器。咱们经过访问活动跨度scope.Span
,一旦关闭了做用域,先前的做用域将成为当前做用域,从而从新激活当前线程中的先前活动范围;IScope
继承 IDisposable
,它使咱们可使用using
语法;StartActive(true)
告诉Scope,一旦它被处理,它就应该完成它所表明的范围;StartActive()
自动建立 ChildOf
对先前活动范围的引用,所以咱们没必要AsChildOf()
显式使用 builder 方法;若是运行此程序,咱们将看到全部三个报告的跨度都具备相同的跟踪ID。
微服务将多个程序分开部署,每一个程序提供不一样的功能。在前面,咱们已经学会了 OpenTracing 链路跟踪。接下来,咱们将把代码拆分,控制台程序将再也不提供 FormatString 函数的实现,咱们使用 一个 Web 程序来实现 FormatString 服务。
建立一个 ASP.NET Core 应用程序,在模板中选择带有视图模型控制器的模板。
添加一个 FormatController
控制器在 Controllers 目录中,其代码以下:
using Microsoft.AspNetCore.Mvc; namespace WebApplication1.Controllers { [Route("api/[controller]")] public class FormatController : Controller { [HttpGet] public string Get() { return "Hello!"; } [HttpGet("{helloTo}", Name = "GetFormat")] public string Get(string helloTo) { var formattedHelloString = $"Hello, {helloTo}!"; return formattedHelloString; } } }
Web 应用将做为微服务中的其中一个服务,而这个服务只有一个 API ,这个 API 很简单,就是提供字符串的格式化。你也能够编写其它 API 来提供服务。
将 Program 的 CreateHostBuilder 改一下,咱们固定这个服务的 端口。
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseUrls("http://*:8081"); webBuilder.UseStartup<Startup>(); });
再到 Startup
中删除 app.UseHttpsRedirection();
。
修改以前控制台程序的代码,把 FormatString
方法改为:
private string FormatString(string helloTo) { using (var scope = _tracer.BuildSpan("format-string").StartActive(true)) { using WebClient webClient = new WebClient(); var url = $"http://localhost:8081/api/format/{helloTo}"; var helloString = webClient.DownloadString(url); scope.Span.Log(new Dictionary<string, object> { [LogFields.Event] = "string.Format", ["value"] = helloString }); return helloString; } }
启动 Web 程序后,再启动 控制台程序。
控制台程序输出:
info: Jaeger.Reporters.LoggingReporter[0] Span reported: c587bd888e8f1c19:2e3273568e6e373b:c587bd888e8f1c19:1 - format-string info: ConsoleApp1.Hello[0] Hello, This trace! info: Jaeger.Reporters.LoggingReporter[0] Span reported: c587bd888e8f1c19:f0416a0130d58924:c587bd888e8f1c19:1 - print-hello info: Jaeger.Reporters.LoggingReporter[0] Span reported: c587bd888e8f1c19:c587bd888e8f1c19:0000000000000000:1 - say-hello
接着,咱们能够将 Formating 改为:
private string FormatString(string helloTo) { using (var scope = _tracer.BuildSpan("format-string").StartActive(true)) { using WebClient webClient = new WebClient(); var url = $"http://localhost:8081/api/format/{helloTo}"; var helloString = webClient.DownloadString(url); var span = scope.Span .SetTag(Tags.SpanKind, Tags.SpanKindClient) .SetTag(Tags.HttpMethod, "GET") .SetTag(Tags.HttpUrl, url); var dictionary = new Dictionary<string, string>(); _tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(dictionary)); foreach (var entry in dictionary) webClient.Headers.Add(entry.Key, entry.Value); return helloString; } }
SetTag
能够设置标签,咱们为本次请求到 Web 的 Span,设置一个标签,而且存储请求的 URL。
var span = scope.Span .SetTag(Tags.SpanKind, Tags.SpanKindClient) .SetTag(Tags.HttpMethod, "GET") .SetTag(Tags.HttpUrl, url);
经过 Inject
将上下文信息注入。
_tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(dictionary));
这些配置规范,能够到 https://github.com/opentracing/specification/blob/master/semantic_conventions.md 了解。
在上面,咱们实现了 Client 在不一样进程的追踪,可是尚未实如今 Server 中跟踪,咱们能够修改 Startup.cs 中的代码,将如下代码替换进去:
using Jaeger; using Jaeger.Samplers; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenTracing.Util; using System; namespace WebApplication1 { public class Startup { private static readonly ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); private static readonly Lazy<Tracer> Tracer = new Lazy<Tracer>(() => { return InitTracer("webService", loggerFactory); }); private static Tracer InitTracer(string serviceName, ILoggerFactory loggerFactory) { var samplerConfiguration = new Configuration.SamplerConfiguration(loggerFactory) .WithType(ConstSampler.Type) .WithParam(1); var reporterConfiguration = new Configuration.ReporterConfiguration(loggerFactory) .WithLogSpans(true); return (Tracer)new Configuration(serviceName, loggerFactory) .WithSampler(samplerConfiguration) .WithReporter(reporterConfiguration) .GetTracer(); } public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc(); GlobalTracer.Register(Tracer.Value); services.AddOpenTracing(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } } }
这样不一样的进程各类均可以实现追踪。
OpenTracing 是开放式分布式追踪规范,OpenTracing API 是一致,可表达,与供应商无关的API,用于分布式跟踪和上下文传播。
Jaeger 是 Uber 开源的分布式跟踪系统。
OpenTracing 的客户端库以及规范,能够到 Github 中查看:https://github.com/opentracing/
详细的介绍能够自行查阅资料。
这里咱们须要部署一个 Jaeger 实例,以供微服务以及事务跟踪学习须要。
使用 Docker 部署很简单,只须要执行下面一条命令便可:
docker run -d -p 5775:5775/udp -p 16686:16686 -p 14250:14250 -p 14268:14268 jaegertracing/all-in-one:latest
访问 16686 端口,便可看到 UI 界面。
Jaeger 的端口做用以下:
Collector 14250 tcp gRPC 发送 proto 格式数据 14268 http 直接接受客户端数据 14269 http 健康检查 Query 16686 http jaeger的UI前端 16687 http 健康检查
接下来咱们将学习如何经过代码,将数据上传到 Jaeger 中。
要注意,数据上传到 Jaeger ,上传的是 Span,是不会上传日志内容的。
继续使用上面的控制台程序,Nuget 中添加 Jaeger.Senders.Grpc
包。
咱们能够经过 UDP (6831端口)和 gRPC(14250) 端口将数据上传到 Jaeger 中,这里咱们使用 gRPC。
修改控制台程序的 InitTracer
方法,其代码以下:
private static Tracer InitTracer(string serviceName, ILoggerFactory loggerFactory) { Configuration.SenderConfiguration.DefaultSenderResolver = new SenderResolver(loggerFactory) .RegisterSenderFactory<GrpcSenderFactory>(); var reporter = new RemoteReporter.Builder() .WithLoggerFactory(loggerFactory) .WithSender(new GrpcSender("180.102.130.181:14250", null, 0)) .Build(); var tracer = new Tracer.Builder(serviceName) .WithLoggerFactory(loggerFactory) .WithSampler(new ConstSampler(true)) .WithReporter(reporter); return tracer.Build(); }
分别启动 Web 和 控制台程序,而后打开 Jaeger 界面,在 ”Service“ 中选择 hello-world
,而后点击底下的 Find Traces
。
经过 Jaeger ,咱们能够分析链路中函数的执行速度以及服务器性能状况。