作者: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,有問題的地方共同探討~