使用Elastic APM监控你的.NET Core应用
作者:Jax
前言
在应用实际的运维过程中,我们需要更多的日志和监控来让我们对自己的应用程序的运行状况有一个全方位的了解。然而对于大部分开发者而言,平时大家所关注的更多的是如何更优雅的实现业务,或者是如何让应用的响应速度更快等等与编码相关的技术,对于应用程序的监控,可能还停留在日志文件的层面,而且大多数是出了事故被人为发现后,才通过日志尝试去定位问题。
本文所准备介绍的Elastic APM是一套用于监控应用各项指标,比如系统响应时间、异常、EF执行的SQL记录等等,并且可以将这些记录组织成一个可追溯的链路,方便查询问题。此外,Elastic APM还可以通过Kibana来做非常漂亮的可视化展示,方便我们定位和发现问题。
废话不再多说,我们开始实战~
Elastic APM介绍
Elastic APM的由下面四个组件所组成,如下图:
APM Agent
APM Agent是安装到你的.NET Core程序中的一个Nuget包,他用于性能、错误等各类数据的收集,并将收集到的数据缓存起来分批发送到APM Server。当然,除了.NET Core使用的Nuget包,他还可以支持很多其他的语言,比如Java,Node.Js,Python等
支持的语言列表请参考这里:https://www.elastic.co/guide/en/apm/agent/index.html
APM Server
APM Server是部署在服务器端的一个用于接收Agent发来的数据包的应用程序,并根据这些数据包自动创建文档,将数据转存到Elastic Server中。
Elastic Search
这个相信大家都很熟悉了,他就是一个基于Lucene实现的高性能、分布式的全文搜索引擎,用于快速、实时的存储、搜索和分析大量数据。在这里来说,他提供的是数据存储和搜索能力!
Kibana
如果你熟悉Elastic Search,那么你一定多少会了解Kibana,Kibana是开源的分析和可视化平台,他能与Elastic Search进行很好的协同,帮助你快速的可视化存储在Elastic Search中的数据,并做成各种各样漂亮的报表、图形等。
环境准备
在本次的实战过程中,我们需要以下的东西:
- Elastic Search
- Kibana
- APM Server
- 一个基于.NET Standard 2.0 + 的项目
Elastic Search的安装:https://www.cnblogs.com/baiyunchen/p/11227144.html
Kibana的安装:
我的环境是Centos 7,所以照着https://www.elastic.co/guide/en/kibana/7.3/rpm.html 这个官网教程安装的,整个过程很简单:
- 下载Kibana RPM包(采用这种方式是因为用yum install网速太慢,所以我用迅雷下载完成rpm文件后上传到Linux机器中)
- 执行命令rpm --install “下载的文件名” 进行安装
- 安装完成后,到/etc/kibana/kibana.yml文件中在文件末尾增加以下配置:
server.host: 0.0.0.0 server.name: 主机IP server.port: 一个你喜欢的端口号 elasticsearch.hosts: ["已安装好的ES地址,多个之间用逗号隔开"] logging.dest: /var/log/kibana.log //需要提前把这个文件创建好,然后把权限给够
- 将Kibana安装为系统服务并启动
sudo /bin/systemctl daemon-reload sudo /bin/systemctl enable kibana.service sudo systemctl start kibana.service
这里大家一定要注意Elastic Search的版本和Kibana一定要匹配,不然会报错的。(我的ES是前段时间装的,所以会有这问题,如果大家一口气安装所有的,应该没啥问题)
如果不幸遇到了问题,可以通过配置文件中logging.dest中配置的路径查看日志。
APM Server的安装
APM Server的安装跟Kibana的安装类似,过程如下:
- 下载RPM包,包在这个页面找你需要的版本,也需要跟ES、Kibana的版本一致,不然你懂得~ https://www.elastic.co/cn/downloads/past-releases#apm-server
- 执行rpm --install “下载的文件名”进行安装
- 在文件夹/etc/amp-server中修改配置文件apm-server.yml,将配置文件最开始的host: “localhost:8200”修改成“0.0.0.0:8200”,以便让他能允许通过ip:端口号的方式访问, 并在配置的最后面添加如下配置:
output.elasticsearch: hosts: ["已安装好的ES地址,多个之间用逗号隔开"]
- 将apm-server安装为系统服务并启动
sudo /bin/systemctl daemon-reload sudo /bin/systemctl enable apm-server.service sudo systemctl start apm-server.service
执行上述操作完成后,在浏览器中尝试打开服务器Ip:8200,最终如果APM Server安装的没有问题,则浏览器中会打印出类似于如下的内容:
{ "build_date": "2019-06-20T14:39:23Z", "build_sha": "9a099b63c53eac8c55707df96193143ec66337e9", "version": "7.2.0" }
此时我们在浏览器中打开Kibana,然后点击Add APM
然后将新打开的页面往下滚动,点击Check APM Server Status按钮,如果出现You have correctly setup APM Server则说明安装完成~
到这里为止,我们的安装工作就全部完成了,接下来,我们尝试将.NET Core与Elastic APM集成起来,一起继续吧~
.NET Core 应用集成
我们创建一个Demo项目,来用于测试APM的各项功能。
项目的地址请参考GitHub:
引用依赖包
我们需要从Nuget引用相关的SDK来与我们的应用做集成,其实就是引用我们最开始说的APM Agent的部分,在Nuget中,我们引用Elastic.Apm.NetCoreAll这个包。
依赖这个包其实相当于自动依赖了如下三个包,你也可以根据需要只依赖其中的一部分。
这里我们为了简单起见,直接印用Elastic.Apm.NetCoreAll这个包
将Agent添加到.NET Core
找到.NET Core的StartUp文件,在里面的Configure方法中添加如下代码:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAllElasticApm(Configuration); }
然后在application.json中添加如下内容:
{ "ElasticApm": { "LogLevel": "Error", // Log级别,根据自己的需要来定"ServerUrls": "http://localhost:8200", //设置前面安装好的APM Server URL,默认端口号是8200 "ServiceName" : "MyApp", //应用的名字,跟着实际情况起就行,allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application } }
此时我们将项目启动起来,随便的刷新几下,然后回到Kibana中,在刚才的页面中往下滚动,选择.NET,然后点击Check Agent Status按钮,如果顺利,就会显示“Data successfully received from one or more agents”,如果不幸没能显示这句话,可以通过VS的Diagnostic Tools中的Event跟踪一下,看看是不是哪里没有配置对
监控数据查看
在Kibana的Add APM页面的最下方,找到Load Kibana Objects,来创建索引,然后点击APM dashboard按钮,就可以进入APM数据的查看页面。
点击APM Dashboard按钮后,展示的页面如下:
该页面中的功能分为Services、Traces两个大的功能模块,先来简单了解一下这两个Tab页中对应的功能。
Services
下面的列表中显示的XianDotnetCommunity其实就是你在配置文件中配置的ServiceName,点击这个名字进入,又可以看到如下的报表,里面有Transactions,Errors,Metrics三个Tab页。
其中
Transactions:展示的当前应用请求情况的概览,包括请求响应时长、请求调用次数等等
Errors:程序中的异常列表
Metrics:应用程序所在机器的CPU/内存使用情况
PS:其实我觉得非常需要一个当前应用程序所消耗的内存和CPU的值,但是貌似.NET Core版本的代理没有实现这些功能,期待未来的更新吧
Traces
里面是用于做链路追踪的视图,首页包含所有事务的名称列表以及响应时间等
点击具体的事务进去,可以看到这个事务经过的链路列表以及更详细的一些响应信息,从而帮你分析出整个链路中的瓶颈,更多内容我们在下面细讲。
探索更多
Elastic APM还有很多其他的功能,比如链路追踪、数据库调用执行,让我们来一起探索吧~
监控API调用链路追踪
如果你了解过微服务架构,那你一定了解链路追踪这个概念。那什么是链路追踪呢?举个栗子:
有个服务A,他会依赖服务B,C,而服务B又会依赖服务D,E,服务C又依赖F,G(省略无数依赖关系),然后有一天,服务A变得非常慢,那到底该怎么定位是哪个服务慢呢?此时链路最终就派上用场了~
我们来简单模拟一下这种嵌套的调用:
在一个WebAPI项目Demo1中有一个ConsumerController,他里面有一个API A,里面调用了另外一个WEB API项目Demo2中的接口B/C/D/E。代码大致如下:
项目甲:
[Route("api/consumer")] [ApiController] public class ConsumerController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; public ConsumerController(IHttpClientFactory httpClientFactory) { //使用HttpClientFactory时需要先在StartUp中调用services.AddHttpClient(); _httpClientFactory = httpClientFactory; } private const string baseUri = "http://localhost:54597"; [HttpGet("a")] public async Task<string> A() { //HttpClient client = new HttpClient(); var client = _httpClientFactory.CreateClient(); Thread.Sleep(new Random().Next(1, 1500)); var b = await client.GetStringAsync($"{baseUri}/api/data-source/b"); var c = await client.GetStringAsync($"{baseUri}/api/data-source/c"); var d = await client.GetStringAsync($"{baseUri}/api/data-source/d"); var e = await client.GetStringAsync($"{baseUri}/api/data-source/e"); return $"b={b} & c={c} & d={d} & e={e}"; } }
项目乙:
[Route("api/data-source")] [ApiController] public class DataSourceController : ControllerBase { [HttpGet("b")] public async Task<string> B() { Thread.Sleep(new Random().Next(1, 1500)); return "B"; } [HttpGet("c")] public async Task<string> C() { Thread.Sleep(new Random().Next(1, 1500)); return "C"; } [HttpGet("d")] public async Task<string> D() { Thread.Sleep(new Random().Next(1, 1500)); return "D"; } [HttpGet("e")] public async Task<string> E() { Thread.Sleep(new Random().Next(1, 1500)); return "E"; } }
此时我们请求Demo1中的API A (xxx/api/consumer/a),然后在Kibana中打开APM中的Traces,找到”GET Consumer/A” 这条记录(看起来默认是根据Controller的名字+Action的名字命名的),然后点击查看详情。
在详情中的最下方,我们找到TimeLine,可以看到如下图所示的图形:
我们可以看到我们在请求API A时的时间分别花费在调用4个API中的时间,也可以看出调用第三个API花费的时间更长,点击蓝色的条可以看到请求的详细信息。
这里不太好的一点是默认显示的名字是GET localhost这样的,其实我们更期望的是显示成调用的api uri对吧?这个我提了一个pr给他们,大家可以关注下:https://github.com/elastic/apm-agent-dotnet/pull/463
监控EF执行记录
这个不需要过多的解释,就是在EF执行DB操作时,进行监控,以便发现性能等问题,我的代码大致如下:
[HttpGet("person")] public void TestEfCore() { using (var db = new ApmDbContext()) { var jax = new Person { Name = "西安.NET社区", Age = 26, Remark = "做最好的技术社区~" }; db.Persons.Add(jax); db.SaveChanges(); db.Persons.FirstOrDefault(x => x.Id == jax.Id ); db.Persons.FirstOrDefault(x => x.Name == "西安.NET社区"); jax.Name = ".NET西安社区"; db.SaveChanges(); db.Persons.Remove(jax); db.SaveChanges(); } }
当我们使用Kibana查看这次请求时,TimeLine显示如下:
我们可以比较清晰直观的看到在这次请求中,执行了哪些SQL语句,各耗时多少,对我们的请求分析来说,还是蛮有用处的。点击具体的蓝条,还可以看到更详细的数据,但比较遗憾的是,数据中并没有记录SQL Params ,这对于我们想完全重现这次请求来说,还是不够友好~
自行埋点记录
相对来说,Elastic APM目前生态圈还不够好,比sky walking还是稍微差一些组件的支持,如果要使用Elastic APM,免不了自己去做一些性能数据的埋点记录,或者在为第三方组件、类库做支持时,也需要做一些数据的埋点。接下来我们就在我们的请求中,埋一些我们想额外记录的信息,示例代码如下:
[HttpGet] public void RecordMyApmData() { var transaction = Agent.Tracer.CurrentTransaction; var span1 = transaction.StartSpan("Stage 1", "Customize"); Thread.Sleep(300); span1.End(); Thread.Sleep(200); var span2 = transaction.StartSpan("Stage 2", "Customize"); Thread.Sleep(100); span2.End(); Thread.Sleep(100); var span3 = transaction.StartSpan("Stage 3", "Customize"); Thread.Sleep(500); span3.End(); }
最终记录的效果如下:
这个Demo虽然写的很简单,但是我相信你已经能大概脑补如何使用Elastic Apm Agent这个类去自定义自己需要捕捉的一些监控数据了~
异常监控
当我们的程序发生了异常时,Elastic APM能帮助你记录,这个功能和日志差不多,但可能比日志稍微好用那么一点点。我们一起来看看吧~
示例代码如下:
[HttpGet] public void TestException() { try { throw new Exception("捕获的异常"); } catch (Exception) { } throw new Exception("未捕获的异常"); }
执行代码后,我们可以通过点击Service Name,然后在Errors这个Tab页中查看到这次的异常
点击详情,我们能看到详细的堆栈调用信息:
此外,我们可以在Trasactions Tab中,找到发生异常的这个请求,然后点击查看详情,在详情中我们也能看到这次异常的发生:
总结
本文介绍了如何使用Elastic APM在.NET Core应用中收集性能和异常数据,并使用Kibana进行可视化分析,整体来说,Elastic APM还是挺强大的,对于性能监控、链路追踪、异常监控基本是够用了。
目前来说,Elastic APM 支持的组件还是比较有限,比如对数据库查询还只是支持EF Core,并不支持更多的组件,链路追踪也仅支持HTTP请求的追踪,也没用支持其他的方式。另外,个人认为Elastic APM把监控报警(Watcher) 给放到X-Pack收费包中也是挺让人伤心的,异常监控报警其实还是蛮关键的。
欢迎大家尝试Elastic APM,有问题的地方共同探讨~