天天看點

深入了解.NET Core的基元(二) - 共享架構

本篇是之前翻譯過的《深入了解.NET Core的基元: deps.json, runtimeconfig.json, dll檔案》的後續,這個系列作者暫時隻寫了3篇,雖然有一些内容和.NET Core 3.0已經不相容了,但是大部分的原理還都是相通的。

原文:Deep-dive into .NET Core primitives, part 2: the shared framework

作者:Nate McMaster

譯文:深入了解.NET Core的基元(二) - 共享架構

作者: Lamond Lu

本篇是之前翻譯過的《深入了解.NET Core的基元: deps.json, runtimeconfig.json, dll檔案》的後續,這個系列作者暫時隻寫了3篇,雖然有一些内容和.NET Core 3.0已經不相容了,但是大部分的原理還都是相通的,是以後面的第三篇我也會翻譯。

  • 深入了解.NET Core的基元(一):deps.json, runtimeconfig.json, dll檔案
  • 深入了解.NET Core的基元(二):共享架構
  • 深入了解.NET Core的基元(三):深入了解runtimeconfig.json

前言

深入了解.NET Core的基元(二) - 共享架構

自.NET Core 1.0起,共享架構(Shared Framework)就已經成為了.NET Core的重要組成部分。自.NET Core 2.1起,ASP.NET Core就已經作為共享架構的第一次出現。你可能從來注意過這一點,但是在設計它的時候,我們經曆了許多反複和持續的讨論。在本篇文章中,我們将深入共享架構并讨論一些開發人員經常遇到的一些陷阱。

基礎部分

.NET Core應用可以在兩種模式下運作, 分别是架構依賴模式(Framework - Dependent) 和獨立運作模式(Self Contained) 。在我的Macbook上,一個最小的可獨立運作的ASP.NET Core網站應用,大約擁有350個檔案,檔案大小總共是93MB。相對的,一個最小的架構依賴應用,大約5個檔案,檔案大小總共239KB。

你可以如下指令行生成基于兩種不同模式的應用。

dotnet new web
dotnet publish --runtime osx-x64 --output bin/self_contained_app/
dotnet publish --output bin/framework_dependent_app/
           
深入了解.NET Core的基元(二) - 共享架構

當程式運作的時候,他們的功能是一樣的。那麼這兩種模式有什麼差別麼?其實正如官網文檔中的解釋:

架構依賴部署(framework-dependant deployment) 依賴目标中安裝的.NET Core共享元件。獨立部署(self-contained deployment)不依賴目标系統中安裝的共享元件,程式所需的所有元件都已經包含在目前應用程式中。

這篇官方文檔(https://docs.microsoft.com/en-us/dotnet/core/deploying/)中很好的解釋了不同模式的優勢。

PS: 作者當時寫這邊文章的時候, 沒有引入Framework-dependent executables (FDE),有興趣的同學可以自行檢視。

共享架構

這裡,簡單的說,.NET Core的共享架構就是一個程式集(*.dll檔案)集合的目錄,這些程式集不需要出現在你的.NET Core的應用目錄中。這個目錄是.NET Core的共享系統範圍版本的一部分,通常你可以在

C:\Program Filres\dotnet\shared

中發現它。

當你運作

dotnet.exe WebApi1.dll

指令時,.NET Core宿主程式會

  • 嘗試發現你的應用依賴的程式集名稱和版本
  • 在某些固定位置中嘗試查找該程式集

這些程式集可以在許多不同的位置被發現了,包含且不限于共享架構。在我之前的文章中,我主要解釋了如果通過

deps.json

runtimeconfig.json

檔案配置宿主程式的行為。希望了解更多的同學,可以檢視那篇文章。

.NET Core宿主程式會讀取

*.runtimeconfig.json

檔案來确定加載哪個版本的共享架構。這個檔案的内容類似:

{
  "runtimeOptions": {
    "framework": {
      "name": "Microsoft.AspNetCore.App",
      "version": "2.1.1"
    }
  }
}
           

這裡,共享架構名稱隻是一個名字。按照約定,這個名字應該是以

.App

結尾的,但是實際上它可以是任何字元串,例如"FooBananaShark"。

對于共享架構的版本,這裡隻是配置了一個最低的版本。.NET Core宿主程式會根據配置,加載對應版本的共享架構,或者更高版本的共享架構,但是它永遠不會加載比指定版本低的共享架構。

那麼,我到底安裝了哪些共享架構呢?

運作

dotnet --list-runtimes

, 你就可以看到你電腦中安裝了哪些共享架構,以及它們的版本和檔案位置。

對比

Microsoft.NETCore.App

,

AspNetCore.App

以及

AspNetCore.All

這裡,以.NET Core 2.2為例。

架構名稱 描述
Microsoft.NETCore.App 基礎運作時。它主要提供了

System.Object

List<T>

string

類,以及記憶體管理,檔案管理,網絡I/O, 線程管理等功能
Microsoft.AspNetCore.App 預設Web運作時。它主要提供了使用API建立Web伺服器的功能,這裡主要包含Kestral, Mvc, SignalR, Razor, 以及EF Core的部分功能。
Microsoft.AspNetCore.All 與第三方的內建庫。它追加了EF Core + Sqlite的支援,以及一些擴充功能, 例如Redis, Azure Key Valut等。(在.NET Core 3.0中已經不再使用)

共享架構與Nuget包的關系

.NET Core SDK生成了

runtimeconfig.json

檔案。在.NET Core 1和2中,SDK使用了項目配置中的兩部分來确定

runtimeconfig.json

檔案中架構部分内容。

  • MicrosoftNETPlatformLibrary

    屬性。對于所有.NET Core項目,它預設是

    Microsoft.NETCore.App

  • Nuget包管理工具的還原結果集,結果集中可能包含了相同名稱的包

這裡針對所有的.NET Core項目, .NET Core SDK都會添加一個隐式的包來引用

Microsoft.NETCore.App

。ASP.NET Core通過修改預設配置

MicrosoftNETPlatformLibrary

, 将其改為

Microsoft.AspNetCore.App

但是這裡需要注意,Nuget包管理工具不提供任何共享架構!不提供任何共享架構! 不提供任何共享架構! 重要的事情說三遍_。Nuget包管理工具隻提供編譯器使用的一些API,以及少量SDK。共享架構的擷取來源可以是運作時安裝器 https://aka.ms/dotnet-download, 或者捆綁在Visual Studio中,Docker鏡像中,以及一些Azure伺服器中。

版本前滾政策

正如我上面提到的,

runtimeconfig.json

隻是指定了一個最小版本。實際使用的版本會依賴于一個版本前滾政策(詳細内容可以參閱官方文檔。例如

  • 如果應用使用的共享架構最小版本是2.1.0, 那麼程式最高會加載的共享架構版本是2.1.*。

針對這一部分,可以參見《深入了解.NET Core的基元(三):深入了解runtimeconfig.json》

作者:《深入了解.NET Core的基元(三):深入了解runtimeconfig.json》後續會補上

分層的共享架構

在.NET Core 2.1版本中引入了分層共享架構的特性。

共享架構可以依賴于其他共享架構。引入此特性是為了支援ASP.NET Core, 這個特性可以将程式包的運作時存儲轉換為一個共享架構。

如果你檢視一下

$DOTNET_ROOT/shared/Microsoft.AspNetCore.All/$version/

檔案夾,你會發現一個名為

Microsoft.AspNetCore.All.runtimeconfig.json

的檔案,其内容如下

$ cat /usr/local/share/dotnet/shared/Microsoft.AspNetCore.All/2.1.2/Microsoft.AspNetCore.All.runtimeconfig.json
{
  "runtimeOptions": {
    "tfm": "netcoreapp2.1",
    "framework": {
      "name": "Microsoft.AspNetCore.App",
      "version": "2.1.2"
    }
  }
}
           

多級檢索

在.NET Core 2.0中引入了多級檢索特性。

宿主程式在啟動時會探查多個位置,以尋找合适的共享架構。程式首先會查找dotnet根目錄,即包含一個

dotnet.exe

可執行檔案的目錄。這裡我們可以通過配置

DOTNET_ROOT

的環境變量來覆寫此配置。根據此配置,程式檢索的第一個目錄是:

$DOTNET_ROOT/shared/$name/$version
           

如果這個目錄不存在,宿主程式會嘗試使用多級檢索機制,檢索預定的全局路徑清單。這個機制可以通過設定全局變量

DOTNET_MULTILEVEL_LOOKUP=0

來關閉。預設情況下,預定的全局路徑清單如下:

OS Location
Windows

C:\Program Files\dotnet

(64位程序)

C:\Program Files (x86)\dotnet

(32位程序) (檢視源代碼)
macOS

/usr/local/share/dotnet

(檢視源代碼)
Unix

/usr/share/dotnet

最終宿主程式會在找到的全局目錄中檢索以下目錄

$GLOBAL_DOTNET_ROOT/shared/$name/$version
           

ReadyToRun特性

共享架構中的程式集,都是經過

crossgen

工具預優化過的。使用這個工具可以生成"ReadyToRun"版本的程式集,這些程式集都是針對指定作業系統和CPU架構優化過的。這裡主要的性能提升是,減少了JIT在啟動時準備代碼所花費的時間。

Crossgen相關文檔:https://github.com/dotnet/coreclr/blob/v2.1.3/Documentation/building/crossgen.md

一些陷阱

我相信每個.NET Core程式員都會遇到以下陷阱中的一部分。我将盡力解釋這些問題是如何産生的。

Http Error 502.5 Process Failure

到目前為止,開發人員,最常遇到的陷阱是在IIS中或者Azure Web Services中托管ASP.NET Core應用程式。這個問題通常發生在開發人員更新了一個項目,或者當應用部署的時候,目标機器沒有更新。這個錯誤的真正原因通常是應用所需版本的共享架構找不到,導緻.NET Core應用程式無法正常啟動。當

dotnet

無法啟動應用程式時,IIS會傳回HTTP 502.5的錯誤,但是不會顯示内部的錯誤消息。

"The specified framework was not found"

It was not possible to find any compatible framework version
The specified framework 'Microsoft.AspNetCore.App', version '2.1.3' was not found.
  - Check application dependencies and target a framework version installed at:
      /usr/local/share/dotnet/
  - Installing .NET Core prerequisites might help resolve this problem:
      http://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409
  - The .NET Core framework and SDK can be installed from:
      https://aka.ms/dotnet-download
  - The following versions are installed:
      2.1.1 at [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
      2.1.2 at [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
           

這個錯誤通常出現在HTTP 502.5錯誤之後,或者Visual Studio Test Explorer故障。

如上所述,當

runtimeconfig.json

檔案指定了一個架構名稱和版本,但是經過多級檢索特性和前滾政策之後,主機依然無法找到一個合适的架構版本的時候,就會出現以上錯誤。

更新

Microsoft.AspNetCore.App

程式集的Nuget包

Microsoft.AspNetCore.App

程式集的Nuget包,并不提供共享架構。它隻是提供了C#/F#編譯器使用的一些API以及少量SDK. 是以你必須要單獨下載下傳并安裝共享架構。

同時,由于前滾政策,你并不需要更新Nuget包的版本,來讓你的程式運作在新版本的共享架構中。

這可能是ASP.NET Core團隊的一個設計失誤,我們無法将共享架構作為Nuget包出現在項目檔案中。共享架構所提供的程式包并不是通常意義上的程式包。與大部分程式包不同,它們不是自給自足的。我們希望當項目使用

<PackageReference>

時,Nuget能夠安裝所需的所有引用,但是令人沮喪的是,這些特殊程式包的設計偏離我們期望的模式。當然,現在我們已經得到了各種建議來解決這個問題。我希望它能早日實作。

在ASP.NET Core 2.1的新項目模闆和文檔中,微軟向開發人員展示了,他們隻需要在項目檔案中添加如下的一行代碼。

<PackageReference Include="Microsoft.AspNetCore.App" />
           

其他的

<PackageReference>

引用代碼都必須要包含

Version

屬性。隻有當項目檔案是以

<Project Sdk="Microsoft.NET.Sdk.Web">

開頭的,那麼以上這句與版本無關的程式包引用才會起作用,并且這裡隻對

Microsoft.AspNetCore.{App, All}

程式集包起作用。Web SDK将根據項中的其他配置, 例如:

<TargetFramework>

<RuntimeIdentifier>

, 來自動選擇一個合适的程式包版本。

如果你在包引用的部分加入的

Version

屬性,并指定了版本,或者你沒有使用Web SDK作為項目檔案的開頭,則無法使用此功能。這裡我很難推薦一個好的解決方案,因為最好的實作方式是基于你對此的了解水準和項目類型的。

釋出修剪(Publish Trimming)

當你使用

dotnet publish

指令釋出一個架構依賴的應用時,SDK會使用Nuget的還原結果(restore result)來決定哪些程式集應該出現在釋出目錄中。有一些程式集是通過Nuget程式集包拷貝的,而有一些就不是,因為他們已經出現在共享架構中。

這很容易産生一些錯誤,因為ASP.NET Core作為共享架構和Nuget程式包都是可用的。項目釋出修剪特性會嘗試通過圖形數學來檢查依賴傳遞,以及更新等,并以此選擇正确的程式封包件。

下面我們以如下的項目引用為例:

<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.9" />
           

MVC實際上是

Microsoft.AspNetCore.App

的一部分,但是當調用

dotnet publish

指令釋出項目後,你會發現你的項目選用了更新後的

Microsoft.AspNetCore.Mvc.dll

程式包,這個程式包比

Microsoft.AspNetCore.App

中包含的2.1.1版本要高,是以最終

Microsoft.AspNetCore.Mvc.dll

會被拷貝到釋出目錄中。

這就不太理想了,因為随着你的應用程式大小不斷增長,你永遠得不到

ReadyToRun

優化版本的

Microsoft.AspNetCore.Mvc.dll

PS: 這個問題以前很少注意到,不過真的很常見。

混淆目标架構的别稱與共享架構

如果簡單認為

"netcoreapp2.0" == "Microsoft.NETCore.App, v2.0.0"

, 你就大錯特錯了。目标架構别稱(Target Framework Moniker簡稱TFM)隻通過項目檔案中

<TargetFramework>

節點指定的。"netcoreapp2.0"隻是希望以人類友好的方式來表達你要使用哪個版本的.NET Core。

TFM的陷阱在于它的名稱太短了。它不能表達出多種共享架構,特定更新檔的版本控制,版本前滾,輸出類型以及是獨立釋出還是架構依賴釋出等内容。SDK會嘗試從TFM推斷許多設定,但是無法推斷所有内容。

是以,更準确的說“netcoreapp2.0”意味着"Microsoft.NETCore.App v2.0.0及以上版本"。

混淆項目配置

最後一個陷阱和項目配置有關。在這裡有很多術語和配置名稱,它們不總是一緻的。這些術語非常令人困惑,是以,如果混淆了這些術語,也沒有關系,那不是你的錯。

下面,我就列出一些常見的項目設定及其實際含義。

<PropertyGroup>
  <TargetFramework>netcoreapp2.1</TargetFramework>
  <!--
    實際意義:
      * 從Nuget包解析編譯引用時使用的API集合的版本
  -->

  <TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks>
  <!--
    實際意義:
      * 使用兩個不同的API集合版本進行編譯。但這并不代表多層共享架構
  -->

  <MicrosoftNETPlatformLibrary>Microsoft.AspNetCore.App</MicrosoftNETPlatformLibrary>
  <!--
    實際意義:
      * 最頂層的共享架構名稱
  -->

  <RuntimeFrameworkVersion>2.1.2</RuntimeFrameworkVersion>
  <!--
    實際意義:
      * 指定了Microsoft.AspNetCore.App程式包的版本,這個版本就是最小的共享架構版本
  -->

  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
  <!--
    實際意義:
      * 作業系統種類 + CPU架構
  -->

  <RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers>
  <!--
    實際意義:
      * 運作此項目可能使用的作業系統種類和CPU架構清單,你必須要通過RuntimeIdentifier配置選擇其中一個
  -->

</PropertyGroup>

<ItemGroup>

  <PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.2" />
  <!--
    實際意義:
      * 使用Microsoft.AspNetCore.App作為共享架構
      * 最低版本2.1.2
  -->

  <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.2" />
  <!--
    實際意義:
      * 引用Microsoft.AspNetCore.Mvc程式包
      * 實際版本2.1.2
  -->

  <FrameworkReference Include="Microsoft.AspNetCore.App" />
  <!--
    實際意義:
      * 使用Microsoft.AspNetCore.App作為共享架構.
  -->

</ItemGroup>
           

總結

共享架構作為.NET Core的可選功能,盡管存在缺陷,但是我認為對于絕大部分使用者來說,這是一個合理的預設設定。我依然認為對于.NET Core開發人員來說,了解背後的原理是一件好事,希望本文是對共享架構功能的良好概述。我盡可能的關聯了一些官網文檔和指南,以便你可以找到更多的資訊。如果還有其他問題,請在下面發表評論。