.NET6接入Skywalking鏈路追蹤完整流程

2022-06-27 12:03:56

一、Skywalking介紹

Skywalking是一款分散式鏈路追蹤元件,什麼是鏈路追蹤?

隨著微服務架構的流行,服務按照不同的維度進行拆分,一次請求往往需要涉及到多個服務。網際網路應用構建在不同的軟體模組集上,這些軟體模組,有可能是由不同的團隊開發、可能使用不同的程式語言來實現、有可能布在了幾千臺伺服器,橫跨多個不同的資料中心。

然後微服務面臨了這些問題:

  • 某個核心服務掛了,導致大量報錯,如何快速確定哪裡出了問題?
  • 使用者請求響應延遲高,怎麼確定是哪些服務導致的?
  • 應用程式有效能瓶頸,怎樣確定瓶頸在哪裡?
  • 如何準實時的瞭解應用部署環境(CPU、記憶體、程序、執行緒、網路、頻寬)情況,以便快速擴容/縮容、流量控制、業務遷移
  • 如何統計各個呼叫的效能指標,比如:吞吐量(TPS)、響應時間及錯誤記錄等

分散式鏈路跟蹤系統就是為了解決這些問題應運而生。

分散式鏈路追蹤元件

  • 阿里巴巴鷹眼(EagleEye)
  • 美團CAT
  • 京東Hydra
  • Twitter Zipkin (Java經常用到) 【.NET Java】
  • Apache SkyWalking (APM) 【go,python,.NET, Java】
  • Pinpoint(APM)

.NET用的最多的兩款是SkyWalking、Zipkin。這裡介紹Skywalking使用。

Skywalking有哪些功能?

  • 多種監控手段。可以通過語言探針和 service mesh 獲得監控是資料。
  • 多個語言自動探針。包括 Java,.NET Core 和 Node.JS。
  • 輕量高效。無需巨量資料平臺,和大量的伺服器資源。
  • 模組化。UI、儲存、叢集管理都有多種機制可選。
  • 支援告警。
  • 優秀的視覺化解決方案。
    **Skywalking整體架構 **

整個架構,分成上、下、左、右四部分:

  • 探針基於不同的來源可能是不一樣的, 但作用都是收集資料, 將資料格式化為 SkyWalking 適用的格式.
  • 平臺後端是一個支援叢集模式執行的後臺, 用於資料聚合, 資料分析以及驅動資料流從探針到使用者介面的流程. 平臺後端還提供了各種可插拔的能力, 如不同來源資料(如來自 Zipkin)格式化, 不同儲存系統以及叢集管理. 你甚至還可以使用觀測分析語言來進行自定義聚合分析.
  • 儲存是開放式的. 你可以選擇一個既有的儲存系統, 如 ElasticSearch, H2 或 MySQL 叢集(Sharding-Sphere 管理), 也可以選擇自己實現一個儲存系統. 當然, 我們非常歡迎你貢獻新的儲存系統實現.
  • 使用者介面對於 SkyWalking 的終端使用者來說非常炫酷且強大. 同樣它也是可客製化以匹配你已存在的後端的

Tracing、Logging和Metrics

在微服務領域,很早以來就形成了Tracing、Logging和Metrics相輔相成,合力支撐多維度、多形態的監控體系,三類監控各有側重:

Tracing:它在單次請求的範圍內,處理資訊。 任何的資料、後設資料資訊都被繫結到系統中的單個事務上。例如:一次呼叫遠端服務的RPC執行過程;一次實際的SQL查詢語句;一次HTTP請求的業務性ID;

Logging:紀錄檔,不知道大家有沒有想過它的定義或者邊界。Logging即是記錄處理的離散事件,比如我們應用的偵錯資訊或者錯誤資訊等傳送到ES;審計跟蹤時間資訊通過Kafka處理送到BigTable等資料倉儲等等,大多數情況下記錄的資料很分散,並且相互獨立,也許是錯誤資訊,也許僅僅只是記錄當前的事件狀態,或者是警告資訊等等。

Metrics:當我們想知道我們服務的請求QPS是多少,或者當天的使用者登入次數等等,這時我們可能需要將一部分事件進行聚合或計數,也就是我們說的Metrics。可聚合性即是Metrics的特徵,它們是一段時間內某個度量(計數器或者直方圖)的原子或者是後設資料。例如接收的HTTP數量可以被建模為計數器,每次的HTTP請求即是我們的度量後設資料,可以進行簡單的加法聚合,當持續了一段時間我們又可以建模為直方圖。

二、Skywalking搭建

這裡用Docker搭建

資料儲存用ES,搭建ES

docker run -d -p 9200:9200 -p 9300:9300 --name es -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms128m -Xmx256m" elasticsearch:7.16.3

搭建skywalking-oap服務,這裡用的是8.9.1版本,oap服務需要關聯ES

docker run --name skywalking-oap \
--restart always \
-p 11800:11800 -p 12800:12800 -d \
-e TZ=Asia/Shanghai \
-e SW_ES_USER= \
-e SW_ES_PASSWORD= \
-e SW_STORAGE=elasticsearch \
-e SW_STORAGE_ES_CLUSTER_NODES=192.168.101.10:9200 \
-v /etc/localtime:/etc/localtime:ro \
apache/skywalking-oap-server:8.9.1

搭建skywalking-ui介面,需要關聯oap服務

docker run -d \
--name skywalking-ui \
--restart always \
-p 8080:8080 \
--link skywalking-oap:skywalking-oap \
-e TZ=Asia/Shanghai \
-e SW_OAP_ADDRESS=http://skywalking-oap:12800 \
-v /etc/localtime:/etc/localtime:ro \
apache/skywalking-ui:8.9.1

搭建完成,開啟ip:8080檢視skywalking介面

三、.NET6接入Skywalking

1、單個服務接入

新建一個.NET6站點,安裝Nuget包

SkyAPM.Agent.AspNetCore

Properties下launchSettings.json增加

"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore", //必須設定
"SKYWALKING__SERVICENAME": "Service1" // 必須設定,在skywalking做標識,服務名稱

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:6413",
      "sslPort": 44308
    }
  },
  "profiles": {
    "NET6AndSkyWalking": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5025",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore", //必須設定
        "SKYWALKING__SERVICENAME": "Service1" // 必須設定,在skywalking做標識,服務名稱 
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

如果不在launchSettings.json加,也可以在Program.cs加

Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", "SkyAPM.Agent.AspNetCore");
Environment.SetEnvironmentVariable("SKYWALKING__SERVICENAME", "Service1");

生成skywalking.json檔案

1、安裝CLI(SkyAPM.DotNet.CLI),cmd命令:

dotnet tool install -g SkyAPM.DotNet.CLI

2、自動生成skyapm.json檔案,cmd命令:

1.dotnet skyapm config [service name] [server]:11800
2.eg: dotnet skyapm config service1 192.168.101.10:11800

執行了命令會生成skywalking.json檔案,把skywalking.json放到專案根目錄,設定較新複製到輸出目錄,然後修改ServiceName設定為專案名,和上面的SKYWALKING__SERVICENAME一致。

skyapm.json檔案

{
  "SkyWalking": {
    "ServiceName": "service1",
    "Namespace": "",
    "HeaderVersions": [
      "sw8"
    ],
    "Sampling": {
      "SamplePer3Secs": -1,
      "Percentage": -1.0
    },
    "Logging": {
      "Level": "Information",
      "FilePath": "logs\\skyapm-{Date}.log"
    },
    "Transport": {
      "Interval": 3000,
      "ProtocolVersion": "v8",
      "QueueSize": 30000,
      "BatchSize": 3000,
      "gRPC": {
        "Servers": "192.168.101.10:11800",
        "Timeout": 10000,
        "ConnectTimeout": 10000,
        "ReportTimeout": 600000,
        "Authentication": ""
      }
    }
  }
}

SamplePer3Secs:每三秒取樣的Trace數量 ,預設為負數,代表在保證不超過記憶體Buffer區的前提下,採集所有的Trace

Percentage:取樣率,預設負數,採集全部,其它數值格式:0.5,0.8...

這時候執行專案已經有基本的鏈路追蹤功能了!

試一下執行,專案,然後看鏈路追蹤介面(注意一下時間搜尋的時間範圍向後選一些),這個可能會延遲幾秒鐘才顯示出來。
儀表盤

追蹤

自定義鏈路紀錄檔
自定義鏈路紀錄檔可以在重要的地方加上,這樣就能知道程式跑到這個地方時的關鍵資訊了。

  public class HomeController : Controller
    {
        private readonly IEntrySegmentContextAccessor _segContext;
        public HomeController(IEntrySegmentContextAccessor segContext)
        {
            _segContext = segContext;
        }

        public IActionResult Index()
        {
            return View();
        }
        
        /// <summary>
        /// 自定鏈路紀錄檔
        /// </summary>
        /// <returns></returns>
        public string SkywalkingLog()
        {
            //獲取全域性traceId
            var traceId = _segContext.Context.TraceId;
            _segContext.Context.Span.AddLog(LogEvent.Message("自定義紀錄檔1"));
            Thread.Sleep(1000);
            _segContext.Context.Span.AddLog(LogEvent.Message("自定義紀錄檔2"));
            return traceId;
        }
    }

呼叫/Home/SkywalkingLog後Skywalking介面效果,看到了程式新增的紀錄檔

2、多服務追蹤

鏈路追蹤在多服務的時候才能體現它的精髓,一個鏈路能跟蹤到請求涉及的所有服務。

這裡新增一個.NET6的web專案,前面的步驟和上面的Service1一樣,只是把服務名改為Service2。

然後在Service2增加一個介面 /UserInfo/GetUserInfo

 public class UserInfoController : Controller
    {
        private readonly IEntrySegmentContextAccessor _segContext;
        public UserInfoController(IEntrySegmentContextAccessor segContext)
        {
            _segContext = segContext;
        }
        [HttpGet]
        public string GetUserInfo(string userId)
        {
            string result = $"userId:{userId},userName:張三";
            _segContext.Context.Span.AddLog(LogEvent.Message(result));

            return result;
        }
    }

然後在Service1增加一個介面/Home/GetUser呼叫Service2
後在Service2增加一個介面 /UserInfo/GetUserInfo

 public class UserInfoController : Controller
    {
        private readonly IEntrySegmentContextAccessor _segContext;
        public UserInfoController(IEntrySegmentContextAccessor segContext)
        {
            _segContext = segContext;
        }
        [HttpGet]
        public string GetUserInfo(string userId)
        {
            string result = $"userId:{userId},userName:張三";
            _segContext.Context.Span.AddLog(LogEvent.Message(result));

            return result;
        }
    }

然後在Service1增加一個介面/Home/GetUser呼叫Service2

 public async Task<string> GetUser()
        {
            var client = new HttpClient();
            //呼叫Service2
           var response=await client.GetAsync("http://localhost:5199/UserInfo/GetUserInfo");
            var result = await response.Content.ReadAsStringAsync();
            return result;
        }

然後呼叫Service1的介面/Home/GetUser

然後看鏈路追蹤,會顯示出對應的Service對應的耗時,點進去還能看到當前服務的詳情和打的紀錄檔。

多服務的時候還能看到服務之間對應的呼叫關係

四、微服務閘道器接入Skywalking

新建一個.NET6的Web專案,參照Nuget包

SkyAPM.Agent.AspNetCore
Ocelot

把Web專案改為Ocelot閘道器

首先在根目錄新增ocelot.json檔案

{
  "Routes": [
    {
      //轉發到下游服務地址--url變數
      "DownstreamPathTemplate": "/{url}",
      //下游http協定
      "DownstreamScheme": "http",
      //負載方式,
      "LoadBalancerOptions": {
        "Type": "RoundRobin" // 輪詢
      },
      "DownstreamHostAndPorts": [
        {
          "Host": "127.0.0.1",
          "Port": 5025 //伺服器埠
        } //可以多個,自行負載均衡

      ],
      //上游地址
      "UpstreamPathTemplate": "/T1/{url}", //閘道器地址--url變數   //衝突的還可以加權重Priority
      "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ]
    },
    {
      //轉發到下游服務地址--url變數
      "DownstreamPathTemplate": "/{url}",
      //下游http協定
      "DownstreamScheme": "http",
      //負載方式,
      "LoadBalancerOptions": {
        "Type": "RoundRobin" // 輪詢
      },
      "DownstreamHostAndPorts": [
        {
          "Host": "127.0.0.1",
          "Port": 5199 //伺服器埠
        } //可以多個,自行負載均衡

      ],
      //上游地址
      "UpstreamPathTemplate": "/T2/{url}", //閘道器地址--url變數   //衝突的還可以加權重Priority
      "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ]
    }
  ]
}

然後把Program.cs修改為Ocelot閘道器

using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using SkyApm.Utilities.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);//ocelot
builder.Services.AddSkyApmExtensions(); // 新增Skywalking相關設定

builder.Services.AddOcelot(); //ocelot
var app = builder.Build();


app.UseOcelot().Wait(); //ocelot

app.Run();

修改Properties下的launchSettings.json檔案

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:5127",
      "sslPort": 44306
    }
  },
  "profiles": {
    "Ocelot.Web": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:7019;http://localhost:5019",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore", //skywalking必須設定
        "SKYWALKING__SERVICENAME": "Ocelot.Web" // 必須設定,在skywalking做標識
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

把skyapm.json複製過來修改一下

{
  "SkyWalking": {
    "ServiceName": "Ocelot.Web",
    "Namespace": "",
    "HeaderVersions": [
      "sw8"
    ],
    "Sampling": {
      "SamplePer3Secs": -1,
      "Percentage": -1.0
    },
    "Logging": {
      "Level": "Information",
      "FilePath": "logs\\skyapm-{Date}.log"
    },
    "Transport": {
      "Interval": 3000,
      "ProtocolVersion": "v8",
      "QueueSize": 30000,
      "BatchSize": 3000,
      "gRPC": {
        "Servers": "192.168.101.10:11800",
        "Timeout": 10000,
        "ConnectTimeout": 10000,
        "ReportTimeout": 600000,
        "Authentication": ""
      }
    }
  }
}

到這裡就完成了,啟動閘道器專案,Service1專案,Service2專案,存取/T1/Home/GetUser

存取成功,看Skywalking追蹤介面,三個站點都追蹤到了

五、設定Skywalking告警

進入容器

 docker exec -it 7c21 /bin/bash

如果報資料夾不存在就用下面的,因為版本不一樣可能會不一樣

 docker exec -it 7c21 /bin/sh

進入config目錄

組態檔規則解讀

通過cat alarm-settings.yml可以查閱檔案內容,如下:

# Sample alarm rules.
rules:
  # Rule unique name, must be ended with `_rule`.
  service_resp_time_rule:
    metrics-name: service_resp_time
    op: ">"
    threshold: 1000
    period: 10
    count: 3
    silence-period: 5
    message: Response time of service {name} is more than 1000ms in 3 minutes of last 10 minutes.
  service_sla_rule:
    # Metrics value need to be long, double or int
    metrics-name: service_sla
    op: "<"
    threshold: 8000
    # The length of time to evaluate the metrics
    period: 10
    # How many times after the metrics match the condition, will trigger alarm
    count: 2
    # How many times of checks, the alarm keeps silence after alarm triggered, default as same as period.
    silence-period: 3
    message: Successful rate of service {name} is lower than 80% in 2 minutes of last 10 minutes
  service_resp_time_percentile_rule:
    # Metrics value need to be long, double or int
    metrics-name: service_percentile
    op: ">"
    threshold: 1000,1000,1000,1000,1000
    period: 10
    count: 3
    silence-period: 5
    message: Percentile response time of service {name} alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000
  service_instance_resp_time_rule:
    metrics-name: service_instance_resp_time
    op: ">"
    threshold: 1000
    period: 10
    count: 2
    silence-period: 5
    message: Response time of service instance {name} is more than 1000ms in 2 minutes of last 10 minutes
  database_access_resp_time_rule:
    metrics-name: database_access_resp_time
    threshold: 1000
    op: ">"
    period: 10
    count: 2
    message: Response time of database access {name} is more than 1000ms in 2 minutes of last 10 minutes
  endpoint_relation_resp_time_rule:
    metrics-name: endpoint_relation_resp_time
    threshold: 1000
    op: ">"
    period: 10
    count: 2
    message: Response time of endpoint relation {name} is more than 1000ms in 2 minutes of last 10 minutes
#  Active endpoint related metrics alarm will cost more memory than service and service instance metrics alarm.
#  Because the number of endpoint is much more than service and instance.
#
#  endpoint_avg_rule:
#    metrics-name: endpoint_avg
#    op: ">"
#    threshold: 1000
#    period: 10
#    count: 2
#    silence-period: 5
#    message: Response time of endpoint {name} is more than 1000ms in 2 minutes of last 10 minutes

webhooks:
#  - http://127.0.0.1/notify/
#  - http://127.0.0.1/go-wechat/

規則常用指標解讀
rule name: 規則名稱,必須唯一,必須以 _rule結尾;
metrics name: oal(Observability Analysis Language)指令碼中的度量名;名稱在SkyWalking後端服務中已經定義,進入容器skywalking-oap之後,進入如下目錄就可以找到。

include names: 本規則告警生效的實體名稱,如服務名,終端名;
exclude-names:將此規則作用於不匹配的實體名稱上,如服務名,終端名;
threshold: 閾值,可以是一個陣列,即可以設定多個值;
op: 操作符, 可以設定 >, <, =;
period: 多久檢查一次當前的指標資料是否符合告警規則;以分鐘為單位
count: 超過閾值條件,達到count次數,觸發告警;
silence period:在同一個週期,指定的silence period時間內,忽略相同的告警訊息;
更多告警規則詳情,請參照這個地址:

https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/backend-alarm.md

修改告警規則

rules:
	service_sal_rule:
		# 指定指標名稱
    	metrics-name: service_sal
    	# 小於
    	op: "<"
    	# 指定閾值
    	threshold: 8000
    	# 每10分鐘檢測告警該規則
    	period: 10
    	# 觸發2次規則就告警
    	count: 2
    	# 設定三分鐘內容相同告警,不重複告警
    	silence-period: 3
    	# 設定告警資訊
    	message: Successful rate of service {name} is lower than 80% in 2 minutes of last 10 minutes

概要:服務成功率在過去2分鐘內低於80%

告警API編寫

這個本質還是SkyWalking根據規則進行檢查,如果符合規則條件,就通過WebHook、gRPCHook、WeChat Hook、Dingtalk Hook、Feishu Hook等方式進行訊息通知;接收到告警資料資訊之後,可以自行處理訊息。這裡為了方便,就採用WebHook的方式進行演示,即觸發告警條件之後,SkyWalking會呼叫設定的WebHook 介面,並傳遞對應的告警信

定義資料模型

public class AlarmMsg
{
    public int scopeId { get; set; }
    public string? scope { get; set; }
    public string? name { get; set; }
    public string? id0 { get; set; }
    public string? id1 { get; set; }
    public string? ruleName { get; set; }
    public string? alarmMessage { get; set; }
}

定義WebHook呼叫API,這裡在Service1下的HomeController里加介面接收告警資訊

 		/// <summary>
        /// 故意報錯測試告警
        /// </summary>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public  string Error()
        {
            //故意報錯
            throw new Exception($"出錯啦:{DateTime.Now}");
        }
        /// <summary>
        /// 告警
        /// </summary>
        /// <param name="msgs"></param>
        [HttpPost]
        public void AlarmMsg([FromBody]List<AlarmMsg>List<AlarmMsg> msgs)
        {
           string msg = $"{DateTime.Now},觸發告警:";
            msg += msgs.FirstOrDefault()?.alarmMessage;
            Console.WriteLine(msg);
           //todo 發郵件或傳簡訊
        }

設定webkook

webhooks:
- http://192.168.101.9:5025/Home/AlarmMsg

重啟 Skywalking-oap服務

請求幾次/Home/Error產生錯誤請求

等待告警webhook呼叫

到這裡,告警據完成了。

六、Skywalking無入侵原理解密

為什麼要在launchSettings.json檔案裡面加SkyAPM.Agent.AspNetCore呢,為什麼加了就可以了呢?

其實用的是.NET Core框架裡面的擴充套件,它是怎做到的呢,舉個例子

在Service1做測試,建一個CustomHostingStartup.cs

namespace NET6AndSkyWalking.Models
{
    /// <summary>
    /// 必須實現IHostingStartup 介面
    /// 必須標記HostingStartup特性
    /// 發生在HostBuild時候,IOC容器初始化之前,無侵入式擴充套件
    /// </summary>
    public class CustomHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            Console.WriteLine("自定義擴充套件執行...");
            //拿到IWebHostBuilder,一切都可做
        }
    }
}

啟動Service1,可以看到新增的擴充套件列印了

如果這個擴充套件是在其他類庫呢?

新建一個Common類庫,把剛才的類移到Common類庫,然後Service新增對Common的參照。

這時候就要修改launchsettings.json檔案,加入Common的程式集了

啟動Service1,成功執行

同樣的道理,通過檢視原始碼可以看到,SkyAPM.Agent.AspNetCore元件裡面也有這樣的一個類,把Skywalking的程式碼無侵入擴充套件進來了。

演示原始碼:https://github.com/weixiaolong325/NET6AndSkyWalking