只有累積,沒有奇蹟

2024年3月24日 星期日

[NET] .NET Aspire From 0 To 1 : 可觀測性儀錶板

前言
上一篇與大家介紹關於 .NET Asipre 方案基本組成與概念,今天要介紹自己覺得 .NET Aspire 框架中重要的儀表板功能,此系列文目前會分為三篇分別是
  • .NET Aspire 快速入門 : 介紹 .NET Aspirea 的基礎知識,包括其設計理念、主要功能
  • .NET Aspire 可觀測性儀錶板 : 解釋 .NET Aspire 如何提供應用程式的可觀測性,包括如何蒐集遙測數據、監控和日誌記錄
  • .NET Aspire 整合 : 說明 .NET Aspire 如何與其他微軟雲服務(如 Azure)整合
希望可以這系列文的介紹與分享,讓有興趣的開發夥伴們可以更進一步了解對 .NET Aspire 框架的認識,與若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論。


探索 .NET Aspire 儀錶板
在 Visual Studio 執行後即可以看到 .NET Aspire Dashboard,Dashboard 是透過 Blazor 所撰寫的應用程式,開發者可以在 .NET Aspire 儀表板頁面顯示正在執行的應用程式,透過此儀表板可以看到應用程式的各項資訊,包括日誌 (Logs)、遙測數據、Metrics 和環境配置等,提供開發者對於應用程式的狀態更進一步的理解與掌握即時資訊。
Resource
如同前一篇介紹的在專案 Apphost 中所設定 Redis、apiService 跟 webFrontend 名字,三個各自型別 (Type) 也有顯示在畫面的欄位上,其中 Redis 是透過 docker 啟動,開啟 Docker Desktop Dashboard 可以看到對應到相同的資料與狀態,分別是 in use 跟 Running,還有應用程式啟動時間與位置資訊
Logs
點擊 Logs 欄位可以顯示應用程式的 log 資訊,以 webfrondend 為例會看到 Log 內容是應用程式相關的 console logs 資訊,還包含 log 的 level 與 message 與 log 紀錄的時間。另外 Watch logs 旁邊還可以進行專案的切換,可以更方便的看到不同 instance 的 console log 重要資訊
Detail
在 .NET Aspire 會自動解析應用程式相關的 endpoint 與環境變數等 config 設定資訊,在將其資訊呈現在 Dashboard 頁面,像是跟第三方串接時串接的 endpoint url、feature toogle 的開關 flag 設定,預設這些內容是隱藏看不到的,需要在點擊右方的 icon 才看到的。這邊 OTEL 開頭的到專案中 config 檔可以發現找不到這些相關的設定參數,OTEL 是與 opentelemetry 相關的設定資訊,這些資訊說明晚點會在後面介紹。

執行應用程式
前面介紹了 Resources 與 Console 之後,下面三個重要功能 Structure、Traces 與 Metrics 是可觀測性常提到的三大支柱,在 .NET Aspire 之所以可以輕鬆地蒐集這些資訊,主要是因為在 .NET 8 底層及組件實做 OpenTelemetry 的標準並提供相關的 API,並在應用程式啟動時設定蒐集相關遙測數據資料,才可以在 .NET Aspire dashboard 中讓開發者看到相關的資訊。因此在介紹 Structure、Traces 與 Metrics 前要先透過執行應用程式 API 與功能,才可以蒐集相關遙測數據在 Dashboard 上觀察到應用程式執行的狀況。
透過 Resources 中 webfrontend 點擊 endpoint 可以看到上面畫面,這是一個簡單的畫面資訊是透過後端載入回傳當天天氣相關的資料。我們可以重新整理畫面試著多呼叫 API,測試中可以發現有設定 cache 功能將資料 cache 10 秒後失效。接著我們回到功能看各數據收集的狀況。

Structured logs
Structured logs 功能可以查看應用程式所記錄的 Log 資訊,像是 log 的來源 (Resources)、Log Level 等級、發生的時間與 log 內容等重要資訊,以下是該功能中自己覺得實用的功能
  • 專案切換 : 進行 resources 的切換,當應用程式變多時這個功能就很需要
  • 搜尋 : 針對 log message 內容進行搜尋的動作,方便快速過濾不必要的資訊
  • 篩選 : 提供 log level 的篩選
  • Trace : 切換畫面到 Trace,找到此 request 相關的請求 log
  • Log entry detail : log 詳細內容,將所有 log 相關的 attribute 資訊全部呈現在畫面上
Traces
Traces 可以看到該請求所相依的服務或是依賴 service,當服務越切越細時當異常發生我們要找到其中哪個服務異常就會是一個重要的議題,舉例來說我們今天系統中有個登入 login 服務,系統背後可能經過自己系統的服務、第三方 SSO 像是 github、微軟 AAD 等第三方系統整合,最後再將資訊整合到既有系統的 Auth 服務,在將整體登入的結果回到前端的 Client Device,即使各應用程式有紀錄 log 但彼此這些 log 是沒有關聯,在盤查問題實就會花費很多時間,Trace 就是透過 TraceID 來將各自的 log 資訊串聯再一起,讓開發者可以透過類似上列畫面更快的定位可能發生異常的服務。
上圖是 sample 專案中 /weather 請求作為範例進行簡單介紹
  • 請求資訊 : 該請求資訊所發生的時間、花費多久 (Duration)、經過多少服務 (Resource) 及經過多少個 Span
  • /weather API 的請求路徑,可以看到在此請求路徑(Span)中的 endpoint 及 httpmethod 方法
  • 呈現這個請求中每個 Span 的細節,各自占比以及所花費的時間,並以 Dashboard 方式呈現讓大家更容易理解可能的瓶頸點
  • Detail : 分別記錄 Span、Application 與 Event 三個資訊的 log 資訊,Span 是紀錄請求類的相關資訊像是 spanID、http method、使用的 Url 及 port。Application 類則是該 service 相關資訊,像是服務名稱、instance ID (多個服務時方便識別)、收集遙測數據實使用的 sdk Name / language 及版本,如下圖所示。
Metrics
Metrics 可以做為監控應用程式即時的狀況,在 .NET Aspire 中會依據應用程式列出需要的 metrics 指標內容
以上圖為例是查看 http.client.request.durtion 的資訊,詳細資訊可以參考 MSDN ASP.NET Core metrics 了解更多資訊。

Dashboard 實作原理
.NET Aspire 提供可視化的 dashboard,讓開發人員在開發雲端應用程式時變得更簡單,那麼在背後是如何做到的呢 ? 我們可以看到 AspireSampleApp.ApiService 與 AspireSampleApp.Web 兩個專案中的啟動呼叫 builder.AddServiceDefaults(); 方法,在程式一開始透過 ConfigureOpenTelemetry 來蒐集應用程式相關的遙測數據資料,其內容如下
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
{
    builder.Logging.AddOpenTelemetry(logging =>
    {
        logging.IncludeFormattedMessage = true;
        logging.IncludeScopes = true;
    });

    builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics =>
        {
            metrics.AddAspNetCoreInstrumentation()
                   .AddHttpClientInstrumentation()
                   .AddProcessInstrumentation()
                   .AddRuntimeInstrumentation();
        })
        .WithTracing(tracing =>
        {
            if (builder.Environment.IsDevelopment())
            {
                // We want to view all traces in development
                tracing.SetSampler(new AlwaysOnSampler());
            }

            tracing.AddAspNetCoreInstrumentation()
                   .AddGrpcClientInstrumentation()
                   .AddHttpClientInstrumentation();
        });

    builder.AddOpenTelemetryExporters();

    return builder;
}  
  

OpenTelemetry 是什麼
由於在 Dashboard 分為三塊分別是 Logging、Traces 與 Metrics 三者,因此在上述程式碼的說明我分為三部分來說明,首先先來看一下程式方法中提到的 OpenTelemetry。如果提到可觀測性 Observability 一定會討論到 OpenTelemetry 這套收集遙測數據的標準,OpenTelemetry 是什麼呢
OpenTelemetry 提供單一的開放原始碼標準和技術組合,可從雲端原生應用程式和基礎架構中擷取及匯出指標、追蹤記錄和記錄檔。
目的是為了解決雲端原生應用程式在分散式系統中收集應用程式的遙測據據資料問題,像是指標和追蹤在過去有不同種蒐集方式,透過 OpenTelemetry 統一的標準,可以簡化及更有效地蒐集需要的資料內容,彙整到相關的技術廠商或是 open Source 專案,各程式語言可以透過 OpenTelemetry 所定義的標準將其其程式語言的方法實做出來,提供給各自的開發者使用,如果想要了解更多關於 OpenTelemetry 介紹,可以參考我之前在 Will 保哥粉絲團直播分享的 初探 OpenTelemetry 工具組:蒐集遙測數據的新標準,這裡就不在多加介紹


Logging
程式碼中 3~7 行,設定 logging 相關資訊主要執行程式碼如下
builder.Logging.AddOpenTelemetry(logging =>
{
    logging.IncludeFormattedMessage = true;
    logging.IncludeScopes = true;
});  
在 logging 中主要透過程式碼中的 Logging.AddOpenTelemetry 往下查看是使用底層 AddOpenTelemetryInternal 方法,透過 Visual Studio IDE 查看定義 sourece code 來探討背後做了哪些事情
private static ILoggingBuilder AddOpenTelemetryInternal(
	ILoggingBuilder builder,
	Action? configureBuilder,
	Action? configureOptions)
{
	Guard.ThrowIfNull(builder);

	builder.AddConfiguration();

	var services = builder.Services;

	// Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions
	RegisterLoggerProviderOptions(services);

	services.AddOpenTelemetrySharedProviderBuilderServices();

	if (configureOptions != null)
	{
		// Note: Order is important here so that user-supplied delegate
		// fires AFTER the options are bound to Logging:OpenTelemetry
		// configuration.
		services.Configure(configureOptions);
	}

	var loggingBuilder = new LoggerProviderBuilderBase(services).ConfigureBuilder(
		(sp, logging) =>
		{
			var options = sp.GetRequiredService>().CurrentValue;

			if (options.ResourceBuilder != null)
			{
				logging.SetResourceBuilder(options.ResourceBuilder);

				options.ResourceBuilder = null;
			}

			foreach (var processorFactory in options.ProcessorFactories)
			{
				logging.AddProcessor(processorFactory);
			}

			options.ProcessorFactories.Clear();
		});

	configureBuilder?.Invoke(loggingBuilder);

	services.TryAddEnumerable(
		ServiceDescriptor.Singleton(
			sp => new OpenTelemetryLoggerProvider(
				sp.GetRequiredService(),
				sp.GetRequiredService>().CurrentValue,
				disposeProvider: false)));

	return builder;

#if NET6_0_OR_GREATER
	[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "OpenTelemetryLoggerOptions contains only primitive properties.")]
	[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "OpenTelemetryLoggerOptions contains only primitive properties.")]
#endif
	static void RegisterLoggerProviderOptions(IServiceCollection services)
	{
		LoggerProviderOptions.RegisterProviderOptions(services);
	}
}    
上述程式碼重點是在 .NET 程式中與 OpenTelemetry 進行配置日誌紀錄,通過 services.AddOpenTelemetrySharedProviderBuilderServices() 向 DI 加上 OpenTelemetry 相關服務,並在 RegisterLoggerProviderOptions(services) 綁定 OpenTelemetryLoggerOptions,使用 Singleton 方式新增到 service 中 (IServiceCollection)。

Metrics
程式碼 9~16 行,是用來設定 Metrics 相關資訊主要執行程式碼如下
builder.Services.AddOpenTelemetry()
	.WithMetrics(metrics =>
	{
		metrics.AddAspNetCoreInstrumentation()
			   .AddHttpClientInstrumentation()
			   .AddProcessInstrumentation()
			   .AddRuntimeInstrumentation();
	})
AddOpenTelemetry() 是 IServiceCollection 的擴充方法,在這擴充方法中允許在 IServiceCollection 的 instance 直接使用,並在 service 中新增 TelemetryHostedService 的服務 (DI Lifecycle 同樣為 Singleton),最後方法回傳 OpenTelemetryBuilder 實例。並調用其 WithMetrics 方法,我們來看看該方法做了哪些事情
/// 
/// Adds metric services into the builder.
/// 
/// 
/// Notes:
/// 
/// This is safe to be called multiple times and by library authors.
/// Only a single  will be created for a given
/// .
/// This method automatically registers an  named 'OpenTelemetry' into the .
/// 
/// 
/// The supplied  for chaining
/// calls.
public OpenTelemetryBuilder WithMetrics()
    => this.WithMetrics(b => { });

/// 
/// Adds metric services into the builder.
/// 
/// 
/// 
/// configuration callback.
/// The supplied  for chaining
/// calls.
public OpenTelemetryBuilder WithMetrics(Action configure)
{
    OpenTelemetryMetricsBuilderExtensions.RegisterMetricsListener(
        this.Services,
        configure);

    return this;
}
透過上述代碼我們可以得知下列資訊
  • WithMetrics 是 OpenTelemetryBuilder 的擴充方法,用於在 .NET 應用程式中加上 Metrics 中的標準服務 (Builder)
  • 方法一的 WithMetrics() 沒有參數,第二個 WithMetrics(Action configure) 接收一個參數 configure,用於將 Metrics 度量標準服務添加到建構器中
  • 內部調用 RegisterMetricsListener 來執行實際的註冊跟配置

接者在回到 sample 專案程式碼中可以看到 WithMetrics 有四個設定值分別是 AddAspNetCoreInstrumentationAddHttpClientInstrumentationAddProcessInstrumentationAddRuntimeInstrumentation,透過上述程式碼可以蒐集應用程式的內建指標包括 Process、Memory、GC、HttpClient 與伺服器相關指標等資訊。提供開發者可以使用簡單幾行程式簡化啟用所有內建重要指標的過程,因此可以呼應上面在介紹 .NET Aspire Dashboard 時為何可以蒐集到這麼多與應用程式相關的 metrics 資訊,就是在這段程式碼所設定的才可以在 dashboard 看到。 如果對於 ASP.NTE Core Metrics 有興趣探索可以參考 built in metrics aspnetcore

Traces
程式碼 17~28 行,是用來設定 Traces 相關資訊主要執行程式碼如下
.WithTracing(tracing =>
{
    if (builder.Environment.IsDevelopment())
    {
        // We want to view all traces in development
        tracing.SetSampler(new AlwaysOnSampler());
    }

    tracing.AddAspNetCoreInstrumentation()
           .AddGrpcClientInstrumentation()
           .AddHttpClientInstrumentation();
});
並調用其 WithTracing 方法,我們來看看該方法做了哪些事情
/// 
/// Adds tracing services into the builder.
/// 
/// 
/// Note: This is safe to be called multiple times and by library authors.
/// Only a single  will be created for a given
/// .
/// 
/// The supplied  for chaining
/// calls.
public OpenTelemetryBuilder WithTracing()
    => this.WithTracing(b => { });

/// 
/// Adds tracing services into the builder.
/// 
/// 
/// 
/// configuration callback.
/// The supplied  for chaining
/// calls.
public OpenTelemetryBuilder WithTracing(Action configure)
{
    Guard.ThrowIfNull(configure);

    var builder = new TracerProviderBuilderBase(this.Services);

    configure(builder);

    return this;
}
  
與 withMetrics 相似,兩者都是 OpenTelemetryBuilder 的公開擴充方法。並且都是一個方法沒參數,第二個方法提供 config 設定參數,在 withTraces 中會給特定的 IServiceCollection 建立 TracerProvider。

在回到前面程式碼,預設會希望在開發環境自動將 trace 資料蒐集起來,方便開發者在測試環境時可以捕捉相關資訊,因此有類似判斷當符合條件時將採集器設定為 AlwaysOnSampler
  • AddAspNetCoreInstrumentation : 加上與 ASP.NET Core 相關的 tracing 資訊,這有助於開發者追蹤 Web 應用程式的請求和回應。
  • AddGrpcClientInstrumentation : 加上 gRPC 使 gRPC 呼叫可以被追蹤。
  • AddHttpClientInstrumentation : 加上 HttpClient 相關的 tracing 資訊,它有助於追蹤 HTTP 客戶端請求。

Exporters
上面提到很多蒐集遙測數據資料的設定與方法,最後這些資料會蒐集到何處呢 ? 在程式後面第 30 行透過 builder.AddOpenTelemetryExporters() 方法設定 OpenTelemetry 加上一種或是多種的導出器(Exporter),設定完後將蒐集到的各項資訊透過 config 設定發送到各個後端系統,例如 Azure Monitor、 Prometheus、Jaeger、Zipkin、Elasticsearch、Grafana 等。從 source code 可以看到蒐集位置是透過 config 的 OTEL_EXPORTER_OTLP_ENDPOINT,我們可以在透過 dashboard 中的 resource 查看此 config 設定位置為何。
補充 : 如果有需要調整可以過 config 中的設定來修改。

小結
以上快速介紹了關於 .NET Aspire 提供好用的框架 dashboard,除了功能介紹之外也一起探索了其背後關於可觀測性三支柱 logging、Tracing、Metrics 在 .NET Aspire 的實現背後原理與程式碼說明。相信透過今天的文章各位夥伴對於 .NET Aspire dashboard 有更進一步的理解,身為開發者的我們在了解後相信也可以在自己所開發的應用程式中加上相關好用的功能,但 .NET Aspire 與 .NET 8 對於 Cloud Native 所提供的強大功能不僅於如此,在下篇文章我們將再繼續介紹其他好玩的功能與講解背後程式碼原理,happy Coding !

參考
.NET Aspire documentation (Preview)

2024年3月4日 星期一

[NET] .NET Aspire From 0 To 1 : 快速入門

前言
.NET Conf 一直是微軟對開發者展示火力的重要來源之一,在今年 .NET Conf 2023 上 .NET 平台團隊的 PM Glenn condrin 與 David Flower 介紹新一代雲原生框架 .NET Asipre。在議程中 David Fowler Demo 使用 .NET Aspire 專案,讓開發者可以輕鬆開始使用 .NET Aspire 框架提升開發者的體驗,並增進生產力,並帶來所有雲原生可以帶來的遙測數據、可觀察性、可擴展性和靈活性。另外讓我覺得興奮的是 .NET 8 與 OpenTelemetry 的深度整合,對於開發者在實踐可觀測性上更是方便許多。
自己在研究 Aspire 接著會預計整理成相關系列文章,這篇是研究 .NET Aspire 的系列文第一篇,這系列主要會分為幾篇分別是
  • .NET Aspire 快速入門 : 介紹 .NET Aspire 的基礎知識,包括其設計理念、主要功能
  • .NET Aspire 中的可觀測性 : 解釋 .NET Aspire 如何提供應用程式的可觀測性,包括如何蒐集遙測數據、監控和日誌記錄
  • .NET Aspire 整合 : 說明 .NET Aspire 如何與其他微軟雲服務(如 Azure)整合
希望可以透過各種官網的文件與說明,加上自己的理解針對新一代 .NET Aspire 雲原生框架進行說明,讓有興趣的開發夥伴們可以更進一步了解對開發者帶來的好處與效益,與若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論。


背景
近幾年互聯網發展的速度可以說是有增無減,新的科技名詞層出不窮,當中 ABCDE 可以代表新興科技發展的五個重要方向,分別是指 AI(人工智慧)、Big data(大數據)、Cloud(雲端)、Device(裝置)、Ecosystem(生態)。隨著雲端技術的成熟,將應用程式上雲一直是這幾年熱度不減的議題之一,身為開發者的我們,除了要了解或熟悉雲端上的服務 Service、將應用程式服務容器化,在 Cloud Native 雲原生概念出現後,開發者要將傳統的應用程式搬遷到雲端上也是一個不容忽視的挑戰,在 CNCF 雲原生基金會所提供各式各樣的 Gituhb 專案中,開發者可以找到許多用於建購或部署雲原生服務的工具或組件。這些組件或工具包含了架構、容器化、持續集成或是持續部署(CI/CD),在到上線後的監控與日誌等方面,身為開發者的我們,還須了解它們各自的優勢與限制將其組合起來,應用在開發專案中,有沒有一個框架可以協助開發者快速進度入雲原生應用 ? 讓開發者可以更專注的在開發應用程式的服務呢 ?

.NET Aspire 簡介
在官方 github 介紹如下
.NET Aspire is an opinionated, cloud ready stack for building observable, production ready, distributed applications. .NET Aspire is delivered through a collection of NuGet packages that handle specific cloud-native concerns. Cloud-native apps often consist of small, interconnected pieces or microservices rather than a single, monolithic code base. Cloud-native apps generally consume a large number of services, such as databases, messaging, and caching.
簡單翻譯可以理解為
  • .NET Aspire 是微軟為雲原生應用開發推出的一個新框架,它在 .NET 8 中提供了許多重要功能,專注於提升開發效率和簡化雲原生應用的開發過程。它包含了與雲端服務更緊密的整合,以及支持容器化、微服務架構和分散式系統管理的先進工具和實踐。
.NET Aspire 的目標是簡化複雜的雲原生應用開發,同時提供強大的功能,如改進的遙測數據、可觀察性、可擴展性和靈活性。這對於追蹤和管理分散式系統至關重要,尤其是在面對快速發展的雲端環境時。

準備條件
了解完 .NET Asipre 想要解決的問題與目的後,接著我們來看如果想要使用 .NET Aspire 的前置作業是甚麼,在使用前需安装以下軟體:
  • .NET 8.0
  • .NET Aspire workload
  • Use the Visual Studio installer
  • Use the dotnet workload install aspire command
  • Docker Desktop
  • Integrated Developer Environment (IDE) or code editor, such as:
  • Visual Studio 2022 Preview version 17.9 or higher (Optional)
  • Visual Studio Code (Optional)
透過 cli 安裝 .NET Aspire workload 示意圖
Use the Visual Studio installer,記得確認右方要有 .NET Aspire SDK (Preview)
建立專案
在本機安裝完上述的軟體後,接著我們透過 Visual Studio IDE 來建立 Aspire 應用程式模板方案,在 Visual Studio 2022 17.9 版本有提供 .NET Aspire 專案模板,可以提供開發者設定出初始組態設定作業。

Step 1 : 開啟 Visual Studio 並建立新專案,右方對話框中搜尋 Aspire 後選擇或者是在 project type 直接選擇 .NET Aspire,點選 .NET Aspire Starter Application,點選下一步
Step 2 : 專案名稱輸入 AspireSampleApp,其餘設定保持預設值,點選下一步
Step 3 : Framework 選擇 .NET 8(Long term support),接著按下建立按鈕
創建完畢之後,我們來看一下方案中預設有哪些專案跟內容
└───📂 AspireSample
     ├───📂 AspireSample.ApiService
     │    ├───📂 Properties
     │    │    └─── launchSettings.json
     │    ├─── appsettings.Development.json
     │    ├─── appsettings.json
     │    ├─── AspireSample.ApiService.csproj
     │    └─── Program.cs
     ├───📂 AspireSample.AppHost
     │    ├───📂 Properties
     │    │    └─── launchSettings.json
     │    ├─── appsettings.Development.json
     │    ├─── appsettings.json
     │    ├─── AspireSample.AppHost.csproj
     │    └─── Program.cs
     ├───📂 AspireSample.ServiceDefaults
     │    ├─── AspireSample.ServiceDefaults.csproj
     │    └─── Extensions.cs
     ├───📂 AspireSample.Web
     │    ├───📂 Components
     │    │    ├───📂 Layout
     │    │    │    ├─── MainLayout.razor
     │    │    │    ├─── MainLayout.razor.css
     │    │    │    ├─── NavMenu.razor
     │    │    │    └─── NavMenu.razor.css
     │    │    ├───📂 Pages
     │    │    │    ├─── Counter.razor
     │    │    │    ├─── Error.razor
     │    │    │    ├─── Home.razor
     │    │    │    └─── Weather.razor
     │    │    ├─── _Imports.razor
     │    │    ├─── App.razor
     │    │    └─── Routes.razor
     │    ├───📂 Properties
     │    │    └─── launchSettings.json
     │    ├───📂 wwwroot
     │    │    ├───📂 bootstrap
     │    │    │    ├─── bootstrap.min.css
     │    │    │    └─── bootstrap.min.css.map
     │    │    ├─── app.css
     │    │    └─── favicon.png
     │    ├─── appsettings.Development.json
     │    ├─── appsettings.json
     │    ├─── AspireSample.Web.csproj
     │    ├─── Program.cs
     │    └─── WeatherApiClient.cs
     └─── AspireSample.sln
稍早建立的方案 AspireSampleApp 其中有內建四個專案,分別是
  • AspireSampleApp.ApiService
  • AspireSampleApp.AppHost
  • AspireSampleApp.ServiceDefaults
  • AspireSampleApp.Web
接著我們來針對這些重要的專案分別作重點介紹

AppHost
做為跨專案的協調器 (orchestrator) 項目,用於連結和配置應用程式不同項目和服務。此專案會設定為方案的啟動項目,並且依賴於 AspireSample.ApiService 和 AspireSample.Web 專案。專案命名以 *.AppHost 做結尾,且開啟專案屬性的設定為 true
前面提到 AppHost 專案作為協調器,那麼在這專案要如何設定呢 ? 我們可以開啟 AspireSampleApp.AppHost 中的 Program.cs 程式碼來一探究竟
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

var apiService = builder.AddProject("apiservice");

builder.AddProject("webfrontend")
    .WithReference(cache)
    .WithReference(apiService);

builder.Build().Run();
  • 以上代碼開始透過 CreateBuilder 方法建立 IDistributedApplicationBuilder 的 instance,透過 IDistributedApplicationBuilder 的擴充方法來設定其相依資源 (resource)
  • 程式碼中描述定義包含三個資源 (resource),要設定 Redis 快取就是透過 .AddRedis 擴充方法並設定其容器 (Container) 名稱是 Cache
  • 接著加入兩個專案 (Project) 分別是 apiservice 跟 webfrontend,因此使用 AddProjects 方法來將 project 加入到 AppHost 專案中
  • WithReference 方法是動態注入 service discovery 的一部分,將其資訊 endpoint 注入到應用程式的配置或資源中,使得在運行時可以根據配置或環境進行適當的連結,查看 source code 可以發現其 format 是 "services__{sourceResourceName}__{endpointIndex}={endpointNameQualifiedUriString}。
  • 使用 Build.Run() 方法來啟動應用程式與所有相依性
MSDN 有針對各自用途做詳細介紹並整理成可視化的圖形如下
另外也提供多種資源類型,像是 ProjectResource、ContainerResource 和 ExecutableResource,詳細可以參考 app-host-overview 介紹

ServiceDefaults
ServiceDefaults 專案目的是放置共用的項目。

過去在開發雲原生應用程式時,遇到好用的 package 或是套件時可以說是每個專案都會同步加上,例如像是 log 可能會使用 serilog 搭配合適的 skin,如果想要加上 retry 機制可能會加上 polly 等廣為人知的好用套件,但如果應用程式的架構或是服務拆分越來越細時,這些套件會在各個專案都重複看見與被使用,假設其套件的組態設定需要調整時,也需要在各個所使用的專案上進行調整,才有機會將其設定統一設定好不遺漏。這可以說是開發者不方便的點之一,在 .NET Aspire 中為了解決這問題,提供開發者可以將其設定移置 serviceDefault 專案中,來提供開發者更好的開發體驗,也可以在其專案中加上工具、OpenTelemetry 狀態檢查獲是環境變數等管理。
以 AspireSampleApp 專案為例建立完後會有 AspireSampleApp.Web 與 AspireSampleApp.ApiService 等應用程式專案,兩者其共用的組態設定與配置是定義在 AspireSampleApp.ServiceDefaults 的 AddServiceDefaults 擴充方法中。程式碼如下
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
    builder.ConfigureOpenTelemetry();

    builder.AddDefaultHealthChecks();

    builder.Services.AddServiceDiscovery();

    builder.Services.ConfigureHttpClientDefaults(http =>
    {
        // Turn on resilience by default
        http.AddStandardResilienceHandler();

        // Turn on service discovery by default
        http.UseServiceDiscovery();
    });

    return builder;
}
  • ConfigureOpenTelemetry 方法 : 用來設定 openTelemetry 收集遙測數據的設定
  • AddDefaultHealthChecks 方法 : 定義應用程式 healthcheck (AddCheck 方法) 服務正常時需要回傳的值
  • AddServiceDiscovery 方法 : 新增 service discovery 功能
  • ConfigureHttpClientDefaults : 新增在使用 httpClient 的預設值
ApiService 與 Web
  • ApiService : 隨機回傳氣溫結果
  • Web : 作為 Client 負責呼叫 apiService 並將請求呈現在網頁上
目的是演示 Demo 用的氣溫應用程式,若有興趣可以查看專案內容,在此就不在多介紹說明

測試專案
在上面的描述與說明中我們可以初步了解 .NET Aspire sample project 方案中各自專案的用途,設定 AspireSampleApp.Web 為起始專案後,在 Visual Studio 按下 F5 來執行應用程式,可以看到天氣的頁面
專案啟動時同步也會啟動 .NET Aspire 儀表板示意圖如下
今天快速介紹了關於微軟推出的新框架 .NET Aspire,以及透過 Visual Studio IDE 建立 .NET Aspire 新專案的過程,也探索了其方案建立後專案的組成與各自的設計含意與目的,相信透過今天的文章對於 .NET Aspire 有了初步的認識,.NET 8 整合 openTelemetry 後將其遙測數據收集到 .NET Aspire 所提供的 Dashboard 上,讓開發者對於應用程式的掌握度更高,下一篇我們再來探索 .NET Aspire 儀錶板其奧妙之處 !


參考
.NET Aspire documentation (Preview)

2024年2月9日 星期五

[Free] 電子書 : Observability 101

前言
在國外,可觀測性 (Observability) 這個詞已經成為熱門話題引起熱烈討論,在 CNCF (Cloud Native Computing Foundation) landscape 中有一個區域專門介紹可觀測性 (Observability) 相關工具與技術。 在國外研討會中,發現越來越多人探討如何透過可觀測性來實現 DevOps 更高效率。 近幾年台灣也開始看到越來越多相關文章與研討會分享,分享有關可觀測性的觀念與實踐經驗。在吸收這些知識與寶貴知識後,對這主題有更深入的了解與認識。因此希望自己可以藉由 ITHome 鐵人賽的機會,整理並分享自己關於可觀測性的理解與小小心得。希望藉由透過這次不要臉分享,可以讓對這主題也有興趣的朋友們能夠更快了解可觀測性的重要觀念,一起入坑。

參加鐵人賽完賽後將此參加過程的技術文章整理後放置在 github 上,並統整成 pdf 電子檔方便提供給有需要的人下載。

此書章節如下
  • 第 01 天 : 起心動念
  • 第 02 天 : Observability in DevOps
  • 第 03 天 : Business Continuity
  • 第 04 天 : Service Level Agreement
  • 第 05 天 : Disaster Recovery
  • 第 06 天 : 災難復原計畫 Disaster Recovery Plan
  • 第 07 天 : 高可用性與可靠性 High Availability & Reliability
  • 第 08 天 : 監控與指標分析 Monitor
  • 第 09 天 : DevOps x Observability 小結
  • 第 10 天 : 可觀測性 Observability
  • 第 11 天 : 解析監控和可觀測性:從哨塔到全景地圖
  • 第 12 天 : 可觀察性的演進史:從控制理論到重新定義
  • 第 13 天 : 可觀測性信號(Signals)的進化之旅
  • 第 14 天 : 可觀測性管道:解析現代數據收集與分析
  • 第 15 天 : 開賽至今的回顧與反思
  • 第 16 天 : 可觀測性與它的工具小夥伴們
  • 第 17 天 : OpenTelemetry : 在收集遙測數據中加上一點新標準
  • 第 18 天 : OpenTelemetry : 核心元件大解密 (1/2)
  • 第 19 天 : OpenTelemetry : 核心元件大解密 (2/2)
  • 第 20 天 : OpenTelemetry : Demo 專案快速入門 (1/2)
  • 第 21 天 : OpenTelemetry : Demo 專案快速入門 (2/2)
  • 第 22 天 : 可觀測性摘要 cheetsheet 及小結
  • 第 23 天 : 可觀測性工具的整合與挑戰
  • 第 24 天 : Grafana Cloud : 雲端監控的未來
  • 第 25 天 : Grafana Cloud : 蒐集應用程式遙測數據
  • 第 26 天 : Grafana Cloud : 蒐集應用程式遙測數據 (2/2)
  • 第 27 天 : Grafana Cloud : 使用 Grafana Pyroscope 優化應用程式性能
  • 第 28 天 : Grafana 事件管理 : 從告警到修復的問題排除流程 (1/2)
  • 第 29 天 : Grafana 事件管理 : 從告警到修復的問題排除流程 (2/2)
  • 第 30 天 : 從傳統開發到可觀測性驅動開發 (ODD)
  • 第 31 天 : 30 天之後 ?

如何取得電子書
基於「取之網路,用之網路」的精神,此電子書是免費的,如果您喜歡這本電子書或者它對您有幫助,請您高抬貴手在 GitHub 電子書項目專案給我一些鼓勵 ⭐️⭐️⭐️ : 傳送門

2024年1月15日 星期一

[NET] 再探 Task.WaitAll 與 Task.WhenAll 差異

前言
之前自己對於 Task.WaitAll 與 WhenAll 有些一知半解的地方,因此進行研究後並撰寫了 [.NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll 文章,最近在與公司同事 Code Review 再度討論起兩者的使用方式,主管也不吝的與團隊成員分享對於 WaitAll 與 WhenAll 主要的看法與使用上的差異,這篇筆記簡單記錄當時討論的內容與結論,內容若有問題歡迎提出來一起討論。

差異性
首先,在之前文章[.NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll 中思考的出發點是以效率出發,而建議使用 Task.WhenAll 取代 Task.WaitAll 方法,但可能在使用上還是有其他需要考慮的部分,其實效率並不是兩個方法的主要差異,差異性有以下兩點

例外處理 Exception
Task.WaitAll() 處理所有返回 Task 的 Exception
Task.WhenAll() 只能處理第一個返回 Task 的 Exception
我們接下來可以透過簡單的 Code 來說明例外處理的部分,下列代碼定義了兩個 Function 分別是 throw IndexOutOfRangeException 與 NullReferenceException,在 main 的方法使用 WaitAll 並使用 try catch 將觀察例外處理 exception 部分為何
class Program
{
    static async Task Main(string[] args)
    {
        try 
        {
            Task.WaitAll(ThrowIndexOfRangeException(), ThrowNullReferenceException());
        }
        catch (Exception ex) 
        {
            Console.WriteLine(ex);
        }
    }
    
    
    static private Task ThrowIndexOfRangeException()
    {
        return Task.Run(() => { throw new IndexOutOfRangeException();});
    }

    static private Task ThrowNullReferenceException()
    {
        return Task.Run(() => { throw new NullReferenceException(); });
    }
}
開啟 Rider在 catch 下中斷點,可以從 Console 可以看到有抓到兩個 exception,拋出的例外 excption 都有抓到看起來合情合理
接著將 WhenAll 來替換定原有的 WaitAll 方法,再重新跑一次觀察例外處理 exception 是否相同
static async Task Main(string[] args)
{
    var tasks = new [] { ThrowIndexOfRangeException(), ThrowNullReferenceException() };
    try 
    {
        await Task.WhenAll(tasks);
    }
    catch (Exception ex) 
    {
        Console.WriteLine(ex);
    }
}
執行結果如下
可以發現 exception 僅僅抓到第一個拋出的例外也就是 indexOutOfRangeException,並不會抓到拋出來的第二個例外,也就是說 catch 抓到的 exception 僅僅是第一個 exception 其餘的都被河蟹掉了(希望的可能是 catch 到所有的例外),有開發者在微軟的 Github 反映 Task.WhenAll - Inner exceptions are lost #31494 此問題,內部有針對此問題做深入討論,原先設計方向是使用 WhenAll 時會發生例外時會將錯誤包在 AggregateException 中,後來因為下列原因修改這設計(怕誤解意思直接使用原文)
a) the vast majority of such cases had fairly homogenous exceptions, such that propagating all in an aggregate wasn't that important 
b) propagating the aggregate then broke expectations around catches for the specific exception types, and 
c) for cases where someone did want the aggregate, they could do so explicitly with the two lines like I wrote.
了解設計初衷之後,如果還是希望在 WhenAll 取得所有的例外可以參考討論串中 noseratio 的回答撰寫 Task 擴充方法蒐集 exception ,代碼如下
public static Task WithAggregatedExceptions(this Task @this)
{
    return @this.ContinueWith(ante =>
        ante.IsFaulted && 
            (ante.Exception.InnerExceptions.Count > 1 || 
            ante.Exception.InnerException is AggregateException) ? 
            Task.FromException(ante.Exception.Flatten()) : 
            ante,
        TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}
加入擴充方法後,下一步就是將既有的 WhenAll 代碼加上 WithAggregatedExceptions 擴充方法
static async Task Main(string[] args)
{
    var tasks = new [] { ThrowIndexOfRangeException(), ThrowNullReferenceException() };
    try
    {
        await Task.WhenAll(tasks).WithAggregatedExceptions();
    }
    catch (Exception ex) 
    {
        Console.WriteLine(ex);
    }
}
再重新看一下 Catch 到的錯誤種類,即可發現被河蟹掉的都找的到了 (大師兄回來了
主線程阻塞
是否會造成主線程雍塞,影響用戶使用
另一個要關注的點是使用後是否會造成阻塞的狀況,由於主線程雍塞的議題已經不是一兩天的事,這裡就簡單整理大神所提到的重點
ASP.NET async 基本心法
閱讀筆記 - 使用 .NET Async/Await 的常見錯誤
.NET 程式鎖死與 SynchronizationContext
或是參考由強者前同事所撰寫的電子書 : .NET 本事-非同步程式設計 :)


感想
魔鬼藏在細節裡。以上就針對 Task.WaitAll 與 Task.WhenAll 做更進一步的說明,以及在討論時所提到的兩個主要的差異內容,這些細節如果一沒有注意到勢必會造成很大的影響,在開發使用也請多加留意或是查相關資料。

參考
C# Thread: What is the difference between Task.WaitAll & Task.WhenAll
Why doesn't await on Task.WhenAll throw an AggregateException?
Task.WhenAll Method
Task.WhenAll - Inner exceptions are lost




2024年1月8日 星期一

[NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll

前言 
在開發偶爾會遇到需要起多個 Task ,接著等待這些 Task 都完成在去做後續邏輯處理,.NET 中提供 Task.WaitAll 與 Task.WhenAll 靜態方法來知道所有任務是否執行完成,過去自己對於兩者的差異性不太明白,因此這篇文章整理自己對於兩者的相關資訊與用法,希望有不清楚或是自己研究錯誤的地方歡迎提出討論

探索問題
Task.WaitAll
在以下的 Sample Code 中使用 Task.Run 建立三個 Task 分別 sleep 1、2、3 秒鐘,接著使用 Task.WaitAll 方法來知道三者是否已執行完成 
static void Main(string[] args)
{
    Task Task1 = Task.Run(() => Thread.Sleep(1000));
    Task Task2 = Task.Run(() => Thread.Sleep(2000));
    Task Task3 = Task.Run(() => Thread.Sleep(3000));

    Task.WaitAll(Task1, Task2, Task3);

    // todo something..
}
在 Task.WaitAll 有提供另一組 API ,可以限定想要等待的時間秒數才不用一直無止境等待下去,這裡在原本的 sample code 再加上等待時間 2.5 秒及透過 Task.IsCompleted 顯示各自是否已完成
static void Main(string[] args)
{
    Task Task1 = Task.Run(() => Thread.Sleep(1000));
    Task Task2 = Task.Run(() => Thread.Sleep(2000));
    Task Task3 = Task.Run(() => Thread.Sleep(3000));

    Task.WaitAll(new Task[] {Task1, Task2, Task3 }, 2500);

    Console.WriteLine("Task1.IsCompleted:{0}", Task1.IsCompleted);
    Console.WriteLine("Task2.IsCompleted:{0}", Task2.IsCompleted);
    Console.WriteLine("Task3.IsCompleted:{0}", Task3.IsCompleted);
}

// result 
// Task1.IsCompleted:True
// Task2.IsCompleted:True
// Task3.IsCompleted:False

在非同步情境時使用 WaitAll 會阻礙執行緒或鎖定 ( blocks thread ),會造成在所有的工作結束之前,當前使用到的執行緒無法自由處理其他工作,在此篇 How and Where Concurrent Asynchronous I/O with ASP.NET Web API 文章有提到,如果某項任務無法正確執行最後引起 deadlocks 狀況發生,此時需要使用 ConfigureAwait 來避免執行緒 lock,更詳細的內容可以參考 Best Practices in Asynchronous Programming

Task.WhenAll
另一個等待所有任務完成的方法是 Task.WhenAll,使用的 Sample Code 中是相同於上述代碼,使用方式不難,在 MSDN Task.Whenall 方法簽章可以看到,使用 Task.WhenAll 方法時會回傳 Task,因此與剛剛差異的是其中等待完成任務方法使用 WhenAll 進行,在使用一個 taskWhenAll 變數用 wait 方法等待完成
static void Main(string[] args)
{
    Task Task1 = Task.Run(() => Thread.Sleep(1000));
    Task Task2 = Task.Run(() => Thread.Sleep(2000));
    Task Task3 = Task.Run(() => Thread.Sleep(3000));

    var taskWhenAll = Task.WhenAll(Task1, Task2, Task3);

    taskWhenAll.Wait();
}
在執行到 Task.WhenAll 時候,會新增一個 Task 並等待該任務的結果 (自己理解上是有專門 Task 在 handle 後續處理),因此使用 WhenAll 不會造成執行緒阻礙的情況發生

Task.WaitAll v.s Task.WhenAll
上面分別介紹兩者的用法與說明,但光看文字與簡單代碼還不過癮,因此小弟參考網路上資料針對 WaitAll 與 WhenAll 執行時間做比較讓數據說話,sample Code 如下
public class Program
{
    static void Main(string[] args)
    {
        IEnumerable<Worker> workerObjects = new List<Worker>
        {
            new Worker {Id = 1, SleepTimeout = 1000},
            new Worker {Id = 2, SleepTimeout = 2000},
            new Worker {Id = 3, SleepTimeout = 3000},
            new Worker {Id = 4, SleepTimeout = 4000},
            new Worker {Id = 5, SleepTimeout = 5000},
        };

        // WaitAll
        TaskWaitAll(workerObjects);

        // WhenAll
        var task = TaskWhenAll(workerObjects);
        
        Console.ReadKey();
    }

    static void TaskWaitAll(IEnumerable<Worker> workers)
    {
        var startTime = DateTime.Now;
        Console.WriteLine("Starting : Task.WaitAll...");

        Task.WaitAll(workers.Select(worker => worker.DoWork(startTime)).ToArray());

        var endTime = DateTime.Now;
        Console.WriteLine("Test finished after {0:F2} seconds.\n", (endTime - startTime).TotalSeconds);
    }

    static Task TaskWhenAll(IEnumerable<Worker> workers)
    {
        var startTime = DateTime.Now;
        Console.WriteLine("Starting test: Task.WhenAll...");

        var task = Task.WhenAll(workers.Select(worker => worker.DoWork(startTime)));
        task.Wait();

        var endTime = DateTime.Now;
        Console.WriteLine("Test finished after {0:F2} seconds.\n", (endTime - startTime).TotalSeconds);

        return task;
    }
}

public class Worker
{
    public int Id;
    public int SleepTimeout;

    public async Task DoWork(DateTime testStart)
    {
        var workerStart = DateTime.Now;
        Console.WriteLine("Worker {0} started on thread {1}, beginning {2:F2} seconds after test start.", Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds);

        await Task.Run(() => Thread.Sleep(SleepTimeout));

        var workerEnd = DateTime.Now;
        Console.WriteLine("Worker {0} stopped; the worker took {1:F2} seconds, and it finished {2:F2} seconds after the test start.", Id, (workerEnd - workerStart).TotalSeconds, (workerEnd - testStart).TotalSeconds);
    }
}
簡單說明 sample code 內容
  • Worker 類別 : 提供 doWork async方法透過 task.run 執行 thread.sleep,可傳入要 sleep 時間與 id,執行前後分別記錄起始、結束時間 
  • main : 主要測試程式進入點,做三件事情
    • 初始化測試的 workerObjects,建立五筆 worker instance 分別測試 1~5 秒
    • 執行測試  TaskWaitAll、TaskWhenAll 方法 
  • TaskWaitAll 方法,裡面紀錄 waitAll 方法起始與結束的時間
  • TaskWhenAll 方法,裡面紀錄 whenAll 方法起始與結束的時間 
執行結果
從以下得知 Task.WaitAll 執行時間為 5.07 秒,Task.WhenAll 執行時間為 5.01 秒
執行多次時間比較都是 WhenAll 會優於 WaitAll,有興趣的客官可以自行下載試試

後記
為了避免執行緒阻塞的情形發生,使用上建議 Task.WhenAll 來取代 Task.WaitAll,從最後的簡單測試代碼執行時間比較來看也是 WhenAll 會優於 WaitAll,其中為了比較 Task.waitAll 與 Task.whenAll 差異性閱讀很多相關的文章與 blog,花了很多時間才產生這篇文章,希望可以透過以上的說明能幫助到有需要的網友(咦 誰是你網友)如果文章中有謬誤或不正確的部分,也請各位大大給予正確指教,最後推薦 MSDN 文章 : Best Practices in Asynchronous Programming 讀完對這方面會很有幫助,謝謝

參考
Async/Await - Best Practices in Asynchronous Programming
Using async/await for multiple tasks
How and Where Concurrent Asynchronous I/O with ASP.NET Web API
await, WhenAll, WaitAll, oh my!!

Copyright © m@rcus 學習筆記 | Powered by Blogger

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com