qemu exception throwing on x64 emulator for docker with .NET6/.NET Core on the Apple M1 chip


Apple M1 Chip 的電腦上使用 container 搭配 .NET Core 還會遇到 QEMU模擬器不兼容的問題與處理方式

Apple M1 無法運行 Linux-x64 的 .NET Core 3/.NET6

Apple M1 Chip & QEMU emulator

Apple M1是蘋果公司第一款基於ARM架構的自行研發的處理器單晶片系統,以往Mac和PC都會以多個晶片來處理CPU、GPU、I/O、神經網路引擎等眾多功能,而Apple M1將這些技術全部整合在單一個晶片上,同時還滿載160億個電晶體,Apple也預計採用M1晶片的Mac陣容需要兩年左右的時間進行轉換,轉換計畫會是令人驚喜的第一步。(2020/11 ~ 2022/11 左右)

而由於 M1 是 ARM 的架構,為了讓大部分的應用程序能夠相容,M1 透過了 ROSETTA 2(在Mac OS X上的一個二進位編譯器軟體)協助轉譯與執行 x64/x86 的 Intel CPU 指令.所以當M1 要執行x86 的應用程式時,需要經過Rosetta 2 轉譯後才能使用,大多數的軟體在 M1 目前都是可以使用的.只是透過 Rosetta 2 轉譯的效能會較低、部分功能不一定完全支援.

想知道當前在 Intel cpu 的 macOS 上用的軟體能否轉移到 M1 的機體上,可以參考 https://isapplesiliconready.com 等網站整理

M1 是否支援該軟體

而 QEMU 則作為在 M1 上面運行 x64/x86 Linux container 時的模擬器.QEMU是一個主機上的VMM(virtual machine monitor),通過動態二進制轉換來模擬CPU,並提供對應的硬體模組支援,透過這層墊片的仿作、使其 Guest OS 認為自己和硬體能進行指令交換與運行,但其實是與 QEMU 模擬出來的硬體交流而已、QEMU再將這些指令翻譯給真正硬體進行操作。

通過這種模式,可以和主機上的硬盤,網卡,CPU,CD-ROM,音頻設備和USB設備進行交互。但由於所有指令都需要經過QEMU來轉譯,因而性能會比較差。就如同 QEMU 的模擬速度約為 實機的25%

相關 ARM 的虛擬化可以參考:[Linux KVM] Linux KVM concept - CPU

KVM concept

Rosetta is for AMD64 binaries of Docker itself, QEMU is for running the containers

Intel-based container 的趨勢是用 QEMU 來作為 M1 的主要模擬編譯器(支援程度比較高、但效能更差),而 Rosetta 作為其軟體要找AMD64 binaries 的模擬轉譯器(支援比較有限但效能比QEMU好).

Rosetta2: x86-64 Translation Performance

.NET6/.NET Core 3 running on linux-x64 with QEMU exception

整個 SouceCode 可以參考 https://gitlab.com/hand-on-lab/m1-dotnet-container-x64-lab

將此程式放入後於 M1 的 macOs 上透過 Container 跑起來的畫面(使用 Failed 資料夾):

 M1 的 macOs : 運行後的錯誤內容

預期於 Intel-based 的 macOS 上跑起來的畫面:

Intel-based 的 macOs : 運行後的正常內容

Problem Background & Solution

這邊問題的原因很簡單就是 Linux 的 inotify 於當前的執行環境(QEMU模擬器)並沒有支援所導致的.可以參考
Linux OS with .NET- The configured user limit (128) on the number of inotify instances has been reached #19814 這篇的問題點.

而找到的另外一篇說明更完整的交代了一下歷史,這邊只接引用了說明的原文:

The way the hot reload works is it uses Linux’s inotify feature to listen for file changes. Still, the number of file listeners is usually limited, depending on your base image and host environment. The limitation may cause some unexpected errors related to .NET’s FileSystemWatcher when starting containers.

上面的說明我們可以知道一個 container 內的 .NET Core 運行

  • base image : 決定如何運行應用程式,選擇可有兩種

    • framework-dependent deployments (FDDs) : 選用有 sdk 的 base image (可參考dotnet/sdk: .NET SDK )並將程式碼(.cs, .proj, .sln 等)源碼放入即可透過 dotnet run 運行. 這邊也可發佈(publish) 成執行 .dll 檔案後執行.
    • self-contained deployments (SCDs) : 選用有 runtime 的 base image(可參考[dotnet/runtime-deps: .NET Runtime Dependencies][https://hub.docker.com/_/microsoft-dotnet-runtime-deps/]) 並透過 SCD 方式發佈(publish) 成可執行 .dll 檔案後執行.
  • host environment : 決定運行上的宿主環境實際於 OS 層提供的功能支援有什麼.

所以具體來說 base image 只是依據接口來提供功能是否可以實做,而 host environment 才是是否可以實際運行的最後依據.

而回到本次問題的發生點就是 app configuration reload on change 這個功能預設是開啟的而模擬器的host environment卻沒有支援 Linux’s inotify.

.NET Core 3.x

改寫 Program.cs 於 appsetting.json 的熱載入設定

using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace WebApplication1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    var env = hostingContext.HostingEnvironment;

                    // Original from Host.CreateDefaultBuilder under  Microsoft.Extensions.Hosting.Host.cs
                    
                    // config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    //     .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
                         .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
                    
                    if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
                    {
                        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                        if (appAssembly != null)
                        {
                            config.AddUserSecrets(appAssembly, optional: true);
                        }
                    }

                    config.AddEnvironmentVariables();

                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    
    }
}

這邊可以看到關鍵的程式碼是下方針對 reloadOnChange 設定為 false(總共有兩處要修改)

AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)

這邊在執行後的成功樣子

.NET 6

建議用方法 2 或是 3,可在啟動 Docker 時透過 docker run … -e DOTNET_USE_POLLING_FILE_WATCHER=1 … 參數指定.而大多微服務運行實際應用是在 Dockerfile 宣告 ENV 內嵌到容器裡.

方法 1 : 在程式碼內開關

同 .NET Core 3.x 作法

方法 2 : DOTNET_USE_POLLING_FILE_WATCHER

新增環境變數 DOTNET_USE_POLLING_FILE_WATCHER 並將此變數設為 1true,改用 4 秒一次的輪詢取代。

而該參數的設定可以參考 File Providers in ASP.NET Core - Watch for changes 這邊說明.

方法 3 : DOTNET_hostBuilder__reloadConfigOnChange (.NET 5 or later support)

新增環境變數 DOTNET_hostBuilder__reloadConfigOnChange 或是 ASPNETCORE_hostBuilder__reloadConfigOnChange 並將此變數設為 false,關閉動態載入 appsetting.json

  • Key: hostBuilder:reloadConfigOnChange
  • Type: bool (true or false)
  • Default: true
  • Command-line argument: hostBuilder:reloadConfigOnChange
  • Environment variable: {PREFIX_}hostBuilder:reloadConfigOnChange
    兩項注意事項:
  • 上面環境變數的:真實設定方法參考 Environment variables.
  • {PREFIX_} 的部分針對使用的是 DOTNET_ 或是 ASPNETCORE_ 就要看運行的專案類型做對應的設定(在 Host.Build 內運行會有所影響).

此功能原先開啟所帶來影響的部分,則是在我們用開發模式(debug) 搭配 IDE(Rider/VSCode/VS) 時原先如果 ASP.NET Core 框架有支援 appsetting.json 熱載入,修改此檔案會幫你自動建立新的對應 instance 的觸發、以利開發能夠更加便捷.

所以如果只考量運行不考慮開發是可以大膽關閉的.

而該參數的設定可以參考 .NET Generic Host in ASP.NET Core - Disable app configuration reload on change 這邊說明.

.NET 6 and OS Arch

針對 M1 與 .NET 的各環境與 Container 支援可以參考:[Support for .NET debugging on Apple M1 #2884)[https://github.com/microsoft/vscode-docker/issues/2884].

補充 FDD, SCD 與 Docker Image

FDD vs SCD

SCD better part

這邊順便說明 dockerhub 上的 ASP.NET Core Runtime.NET Runtime Dependencies 差異在運用 docker image 的 layer 緩存的特型:

多個同開發依賴的專案可以用一個 runtime 即可執行,可以更省大小(但我個人在微服務比較不會選擇使用 runtime, 多用 .NET Runtime Dependencies)

  • ASP.NET Core Runtime: This image contains the .NET runtimes and libraries and is optimized for running .NET apps in production.
  • .NET Runtime Dependencies: This image contains the native dependencies needed by .NET. It does not include .NET. It is for self-contained applications.

Reference


作者: Blackie
版權聲明: 本站所有文章除特別聲明外,均採用 CC BY 4.0 許可協議。轉載請註明來源 Blackie !
  目錄