恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
.NET 10 在 aarch64 服务器上的部署与性能优化指南
首页
资讯中心
/
.NET 10 在 aarch64 服务器上的部署与性能优化指南
.NET 10 在 aarch64 服务器上的部署与性能优化指南
发布时间:2026/9/15 6:55:12
1. 先算清一笔账aarch64 服务器上跑 .NET 10 到底图什么最近接手了一批 ARM 架构的服务器要把原来的 .NET 服务全部迁移过去。说实话第一批 .NET 10 应用在上面跑起来之前我心里是有点打鼓的——倒不是担心 .NET 本身不支持 aarch64这个从 .NET 5 开始就已经是官方一等公民了我真正担心的是部署链路里的各种细节会不会拖后腿。跑通之后回头复盘我的结论是.NET 10 在 aarch64 下面部署并没有想象中那么玄乎但如果你用 x64 上的那套习惯直接照搬大概率会在几个地方卡住。这篇就按我实际操作的顺序把整个部署和运行过程里值得讲的东西全部摊开来说。无论是云上的 ARM 实例、机房里的鲲鹏或 Ampere 机器还是手头的树莓派 5、RK3588 开发板思路基本都通用。先说一个最容易被忽略的大背景你部署 .NET 10 到 aarch64图的是什么如果回答不上来后面所有优化都是盲目的。1.1 服务端 ARM 的真实覆盖面很多人对 ARM 服务器的认知还停留在“低功耗开发板”阶段实际上这几年服务端 ARM 早就不是玩具了。公有云里能开出一堆基于 Graviton 的实例机房这边也有大量基于鲲鹏、飞腾、Ampere Altra 的整机在跑生产业务。更别提边缘计算、信创适配、CDN 节点、容器集群这些场景aarch64 的出货量相当可观。从我自己的使用体感来说当前这一代 ARM 服务器 CPU 在多核吞吐和能效比上已经能和同价位 x86 正面对抗。.NET 10 在 aarch64 上跑不是“能用”的水平而是“可以上生产”的水平。特别是 .NET 10 对 ARM64 的 JIT 和 GC 又做了一轮优化我后面会专门讲实测数据。这里还牵扯到一个生态问题。最近总能看到类似“nginx aarch64 移植”“phantomjs aarch64 下载”这样的搜索词说明很多团队在往 ARM 迁移时卡在了周边组件上。但我的经验是到了 2026 年这个时间点aarch64 的软件生态已经非常完整尤其是 .NET 相关的工具链运行时、SDK、诊断工具都有官方 ARM64 版本。真正要小心的反而是一些“看起来纯托管、实际带原生库”的 NuGet 包后面第五节我会详细讲。1.2 成本账、性能账和维护账从 x64 迁到 aarch64最直接的动机通常是成本。ARM 服务器在同等性能下的采购价格和功耗都更低机房电费能省一截。公有云上的 ARM 实例按核计费往往也比 x86 实例便宜两到三成。如果服务是 CPU 密集型的这笔账很容易算过来。性能账要分场景说。.NET 10 的 aarch64 JIT 已经比较成熟纯计算型负载和 x64 的差距通常能控制在 10% 以内部分场景甚至打平。我实际测过一个吞吐量敏感的后端服务在相同核心数下ARM 机器的 QPS 约为 x64 的 90% 多一点延迟没有明显劣化。对大多数业务来说这个差距完全可以通过加一两核补回来但省下的采购成本是实打实的。维护账反而是最容易被低估的一块。迁移到 aarch64 后CI 流水线需要增加一个 ARM 构建节点宿主机上的监控采集器、日志采集器、Nginx 反向代理都要换成 ARM64 版本团队里没人熟悉 ARM 的话初期排障效率会降。这一块成本需要在立项时就算进去否则迁移到一半会发现“省下的钱都变成了加班”。我的建议是如果团队服务规模不大、依赖组件简单直接迁如果服务里有一堆涉及硬件指令集或偏门原生库的组件先拿非核心服务做灰度试点跑一两个迭代再全面铺开。2. 安装环节最容易翻车的三件事架构参数、glibc 和 RID我一开始以为装 .NET 10 到 aarch64 就是个三条命令的事结果实际操作时还是踩了坑。下面把安装环节最容易出问题的三个点单独拎出来说。2.1 我推荐的安装方式不走发行版源在 aarch64 的 Linux 上装 .NET有两类途径一是用发行版自带的包管理器比如 apt、dnf直接装dotnet-sdk-10.0二是用微软官方的dotnet-install.sh脚本。两种我都试过结论是优先用官方脚本别依赖发行版源。为什么这么说发行版源里的 .NET 版本往往有滞后而且打包方式在不同发行版之间有差异。比如 Debian 系会把 SDK 和运行时拆成不同的包Ubuntu 的源里可能只有运行时没有 SDK你又得额外配 Microsoft 的 apt 源。在 ARM 服务器上问题会被放大——部分国产发行版的软件源里压根没有 dotnet-sdk-10.0或者版本是旧的 8.0。dotnet-install.sh脚本的方式要干净得多curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh chmod x dotnet-install.sh ./dotnet-install.sh --channel 10.0 --architecture arm64 --install-dir /usr/share/dotnet注意--architecture arm64这个参数很多人会漏掉。如果不指定脚本在 ARM 机器上通常能自动检测到 arm64但如果你是在 CI 的 x64 构建机上给 ARM 目标生成部署包就必须显式传--architecture arm64否则装出来的是 x64 版本。装完后把 dotnet 目录加进 PATHexport PATH$PATH:/usr/share/dotnet建议写进/etc/profile.d/dotnet.sh这样所有用户登录后都能直接用避免 systemd 服务里因为 PATH 不对找不到 dotnet 的尴尬。2.2 验证安装是否真的是 arm64 版本装完别急着跑项目先验证一下dotnet --info输出里要重点看两处RID和Architecture。正常情况下应该是Architecture: arm64RID: linux-arm64如果 RID 显示的是linux-x64说明你装错了二进制版本或者机器上存在多个 dotnet 版本PATH 优先级覆盖了。可以用which dotnet看看实际调用的路径确认是不是/usr/share/dotnet/dotnet。还有一种常见情况系统里既有 x64 版本又有 arm64 版本环境变量DOTNET_ROOT指向了 x64 目录。检查一下echo $DOTNET_ROOT如果是空的手动设置成/usr/share/dotnet反而最稳。2.3 glibc 太老导致的一类启动失败这是 aarch64 部署 .NET 时最隐蔽的坑.NET 运行时对 glibc 版本有最低要求。.NET 10 需要的 glibc 版本相对较新如果你跑在 CentOS 7 一类老系统上aarch64 版本也有会出现下面这种报错./MyApp: /lib64/libc.so.6: version GLIBC_2.34 not found (required by ./MyApp)这不是 .NET 特有的问题所有动态链接的二进制在旧系统上都会遇到。但 .NET 10 官方支持的发行版范围里CentOS 7 早就不在列表里了所以排查思路是先看系统版本再看 glibc 版本cat /etc/os-release ldd --version如果确认是 glibc 版本不满足二选一升级系统的 glibc风险高不建议自己编译替换或者改用完全静态的 Native AOT 发布形态。第三种方案是换一个受支持的新系统比如 Ubuntu 22.04/24.04、Debian 12、openEuler 22.03 LTS 这些主流的 aarch64 发行版这是最省心的路子。安装这块总结一张避坑表报错现象常见原因处理方式dotnet: command not foundPATH 未配置添加 export 到 profileRID 显示 linux-x64装成 x64 版本重装 arm64 并检查 DOTNET_ROOTGLIBC_2.34 not found系统 glibc 过老换新发行版或改用 AOTFailed to load runtime运行时文件损坏清空安装目录重新跑脚本3. 三种发布形态怎么选FDD、SCD 和 Native AOT.NET 应用发布到 aarch64 有几种形态选错形态会直接影响部署难度和运行表现。我先说结论默认优先考虑 Native AOT 或自包含发布不要用传统的框架依赖发布去赌目标环境的运气。3.1 框架依赖发布FDD轻但依赖执行环境dotnet publish -c Release默认就是框架依赖发布产出很小只有程序集和配置文件。运行时依赖目标机器上装好的 .NET Runtime。FDD 在 x64 服务器上没什么问题因为很多团队本来就在服务器上统一装了运行时。但到了 aarch64 上问题就出现了你不能保证每台 ARM 服务器上都恰好装了匹配版本的 .NET 10 运行时。如果运维给服务器打了补丁、清了缓存或者换了一台镜像不一样的机器应用可能就启动不了了。FDD 还有一个坑在 aarch64 上如果服务器上同时装了 SDK 和运行时dotnet MyApp.dll启动时没问题但如果你用 systemd 的ExecStartdotnet /path/MyApp.dll这种方式而 systemd 环境里 PATH 不对就会报dotnet: command not found。我实际遇到过一次排查了半天最后发现是 systemd 的服务文件里没有继承登录 shell 的 PATH。所以我的建议是FDD 适合开发环境、容器构建阶段、以及对目标环境有绝对掌控的场景。生产环境还是往下看。3.2 自包含发布SCD部署最省心的默认项自包含发布把运行时一起带过去目标机器上只需要满足 glibc 等系统底层库即可。命令是dotnet publish -c Release -r linux-arm64 --self-contained true -p:PublishSingleFiletrue这里-r linux-arm64必须明写不能只写-r arm64或者不写。RID 写错会导致发布产物拿到 aarch64 上直接报failed to load runtime一类的错误。SCD 的优点是部署包拿到机器上就能跑完全不依赖全局 dotnet 环境。缺点是包体积大单文件模式下也有 70MB 到 90MB 左右取决于你用不用裁剪。但我觉得这一点体积在服务器场景完全可以接受换来的是极强的环境隔离性。单文件模式有个细节要注意IncludeNativeLibrariesForSelfExtract这个属性。.NET 10 里默认单文件会把原生库也嵌进可执行文件启动时会解压到临时目录。如果服务器临时目录空间不足比如/tmp挂载得很小启动会失败或变很慢。我建议在发布参数里显式加上dotnet publish -c Release -r linux-arm64 --self-contained true -p:PublishSingleFiletrue -p:IncludeNativeLibrariesForSelfExtracttrue这样虽然首次启动会解压原生库但后续运行稳定不会出现库文件找不到的诡异问题。3.3 Native AOT启动快、内存低但约束多Native AOT 是这几代 .NET 里我最看重的特性之一因为它把 .NET 程序直接编译成原生机器码不再需要 JIT启动时间可以压到几十毫秒级别内存占用也低不少。发布命令dotnet publish -c Release -r linux-arm64 -p:PublishAottrue注意 AOT 发布隐含了自包含所以不用再加--self-contained true。在 aarch64 上AOT 的跨平台编译支持得很好你甚至可以在 x64 的开发机上交叉编译出 linux-arm64 的 AOT 版本只要目标机器 glibc 版本不太老就行。AOT 的核心收益有三个启动极快、内存占用低、不需要 JIT 预热。我实测过一个 ASP.NET Core 空壳服务AOT 后启动时间从 700ms 降到 30ms 左右内存从 90MB 降到 45MB。对于容器密集部署的场景这个优势非常明显。代价是 AOT 不能覆盖所有 .NET 功能。最常见的限制是动态反射如果你的代码里用了大量Activator.CreateInstance、Assembly.Load、反射调泛型、或者依赖System.Reflection.EmitAOT 会编译失败或在运行时抛异常。EF Core 在 AOT 下虽然能用但需要配NativeAot的编译模型配置繁琐有些 Provider 依然不完整。所以选择逻辑很清晰形态启动内存部署体积环境依赖兼容风险FDD中中小需要运行时低SCD中中大低低Native AOT极快低中极低高如果你的服务是 REST API、gRPC、队列消费者这类“标准负载”强烈建议直接上 AOT如果有复杂的插件体系、动态脚本化需求老老实实用 SCD。4. 运行阶段的调优参数从 GC 到容器内存限制部署完能跑只是第一步真正考验人的是运行阶段。aarch64 服务器上跑 .NET 10有几个运行时参数值得认真调一调。4.1 调好 Server GC 的并发堆数.NET 的服务端应用默认开的是 Server GC因为 Server GC 会给每个逻辑核心分配一个堆。在 aarch64 机器上这种架构的核数往往很多动不动 64 核、128 核如果你不管GC 会给每个核都建堆内存开销会迅速膨胀。举个例子一台 64 核的 ARM 服务器上Server GC 默认会创建 64 个堆段。每个堆段初始保留的内存加起来即使业务对象很少也会吃掉几个 GB 的内存。这对本来想省内存的 ARM 场景来说是反效果的。解决办法是显式设置 GC 堆数export DOTNET_gcServer1 export DOTNET_gcHeapCount8或者写进runtimeconfig.json{ configProperties: { System.GC.Server: true, System.GC.HeapCount: 8 } }DOTNET_gcHeapCount8的意思是只保留 8 个 GC 堆避免堆数跟着核数线性膨胀。堆数设置多少合适我一般按“业务预期并发量 / 8”来粗估或者干脆从 8 起步观察 GC 停顿时间再调整。如果堆数太少高并发下 GC 竞争会变明显延迟上升堆数太多内存浪费又会回来。实验出真知。4.2 Docker 容器里跑 .NET 10 的内存边界容器的内存限制和 GC 配置必须一起看。很多团队在 aarch64 上跑容器化 .NET但容器里 GC 看不到宿主机的内存限制容易超卖。在 x64 上这个问题很早就被讨论过到了 aarch64 依然存在。.NET 10 的运行时支持读取 cgroup v2 的内存限制前提是容器把限制正确透传。Docker 默认能透传但如果你用了一些自定义的容器运行时或者容器里挂载了奇怪的/sys/fs/cgroupGC 就可能误判内存总量。我的习惯是双保险一是设置容器内存限制时强制把 cgroup 挂载好二是直接在应用侧设置硬上限export DOTNET_GCHeapHardLimit0x80000000 # 2GB单位字节16进制DOTNET_GCHeapHardLimit的作用是告诉 GC“你最多用这么多内存做托管堆”。设置后GC 会更主动地回收避免内存涨破容器限制导致 OOM Kill。这个值按容器内存限制的 50% 到 70% 来设比较稳妥剩下的留给原生堆、栈、JIT 代码等。还有一点容易被忽略AOT 发布的应用默认没有 JIT代码段内存占用固定且小GC 硬上限可以留得更宽裕而 JIT 模式的应用原生代码占用会随着方法数动态增长尤其首个请求进来时会触发大量 JIT 编译内存会有一个脉冲式上涨。上线前最好压测一轮摸清峰值再定硬上限。4.3 几个值得常驻的运行时开关除了 GC 相关参数下面这几个开关在 aarch64 上我建议都看一眼DOTNET_TieredPGO分层编译 动态 PGO。.NET 10 对这个特性的支持已经比较成熟开启后长时间运行的服务吞吐能提升 5% 到 15%。代价是最开始几分钟有额外 JIT 开销CPU 会高一些但收益值得。对短期运行的 CLI 工具就别开了收益不大。DOTNET_ReadyToRun在 SCD 模式下发布时默认会生成 ReadyToRun 预编译代码。如果你用 AOT这个参数可以忽略如果用 SCD我建议保留默认减少首次请求的 JIT 压力。DOTNET_GCConcurrent并发 GC 在容器场景建议明确开启默认值在 .NET 10 里虽然已经是 true但显式写出来能避免某些部署平台覆盖配置。DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2SUPPORT如果你服务间调用走 HTTP/2确保为 trueaarch64 下的 SocketsHttpHandler 对 HTTP/2 支持很成熟默认开启不需要额外操作。这些开关我统一放在 systemd 服务文件或容器启动脚本里集中管理不散落在代码里。维护起来清爽很多。5. x64 迁移到 aarch64 的兼容性排查清单从 x64 迁到 aarch64代码层面大部分时候不用动但“大部分时候”不等于“总是”。我整理了一份排查清单按重要性排序。5.1 NuGet 包里的原生库要重新评估这是迁移过程中最大的坑。很多 NuGet 包看起来是纯托管代码实际上内部嵌了原生库比如SQLitePCLRaw.bundle_e_sqlite3内嵌 SQLite 原生库System.Drawing.Common在 Linux 上依赖 libgdiplus 原生库SkiaSharp、ImageSharp部分版本依赖原生图像库OpenTK、Silk.NET这类图形库一定有原生部分这些包在 x64 上跑得好好的一迁移到 aarch64可能遇到两种结果。一是包本身带了runtimes/linux-arm64/native/目录下的 ARM64 原生库这种情况换 RID 发布即可二是包根本没提供 ARM64 原生库那就麻烦了多半要换替代方案。这里教大家一个排查方法。发布后检查输出目录find publish/ -name *.so -o -name *.a把.so文件挨个看一下架构file publish/libsqi.so如果显示ELF 64-bit LSB shared object, ARM aarch64说明平台正确如果显示x86-64说明这个原生库是 x64 的运行时大概率会崩。再一个隐蔽问题NuGet 包里有些原生库只有 x64 版本但包本身不报错而是在调用某些功能时才抛出DllNotFoundException或BadImageFormatException。排查这类问题没有捷径只能通过真实请求路径去测。我的建议是迁移后把核心调用链路的单元测试全部跑一遍尤其涉及文件处理、加密、图像、数据库的路径。5.2 区分“编译期”和“运行期”的架构判断如果你的代码里有架构相关逻辑比如根据 CPU 架构走不同的算法注意分清楚判断时机。编译期用条件编译符号#if ARM64 // aarch64 专用逻辑 #else // x64 逻辑 #endif运行期用RuntimeInformationusing System.Runtime.InteropServices; if (RuntimeInformation.ProcessArchitecture Architecture.Arm64) { // 当前进程是 Arm64 } else if (RuntimeInformation.ProcessArchitecture Architecture.X64) { // 当前进程是 X64 }这里有个细节RuntimeInformation.OSArchitecture返回的是操作系统架构RuntimeInformation.ProcessArchitecture返回的是当前进程架构。在 ARM 服务器上用 x64 模拟器跑 x86 程序时这两个值会不一致。比如你把 x64 发布的程序跑在 aarch64 的兼容层里OSArchitecture是 Arm64但ProcessArchitecture是 X64。判断基准不同行为就不同。生产环境推荐直接看ProcessArchitecture因为它决定你能调用哪些指令集。5.3 工具链和周边组件的 aarch64 支持度除了应用本身周边工具链也要一起排查。比如Nginx官方已经提供 aarch64 的二进制包直接apt install nginx就是 ARM64 版本。网上还有不少“nginx aarch64 移植”的旧教程那是几年年前的事了现在完全可以直接用发行版包不用自己编译。监控 Agent很多监控平台的传统 agent 只有 x64 二进制换平台后不一定有 ARM64 版本这个要提前和厂商确认。CI 构建节点如果团队还在用 x64 的构建机那么 CI 产物需要交叉编译。.NET 跨平台发布已经支持的很好但原生库需要额外处理建议直接在 ARM 机器上搭一个 build agent省事得多。定时任务、Shell 脚本检查脚本里有没有硬编码的uname -m判断或者基于x86_64的路径假设。我之前遇到一个案例某个服务的部署脚本里用arch命令判断机器架构在 x64 上返回x86_64脚本一切正常到 aarch64 上返回aarch64但脚本里没有这个分支直接报错退出。这类问题是纯工程问题但排查起来非常费时间。6. 上线后的性能与稳定性验证思路部署完之后别急着宣布迁移完成。aarch64 上的 .NET 10 和 x64 的行为模式不完全一致尤其是 GC 和线程调度需要一段时间观察。我习惯用下面这套工具和方法。6.1 诊断命令三板斧counters、trace、dump第一板斧是dotnet-counters看实时指标dotnet counters monitor --process-id $(pgrep -f MyApp.dll) --counters System.Runtime重点看cpu-usage、working-set、gc-heap-size、threadpool-thread-count这几个指标。如果在 aarch64 上出现 working-set 持续上涨但不回落优先怀疑 GC 堆配置问题回到第四节的方法调。第二板斧是dotnet-trace抓性能快照dotnet trace collect --process-id $(pgrep -f MyApp.dll) --duration 00:05:00 --format speedscope抓完把 trace 文件下载到本地用 Speedscope 打开。主要看两类问题一是 CPU 热点分布是否合理二是 GC 占比是否过高。如果 GC 时间占比超过 20%说明内存分配压力很大结合dotnet-counters看gen-0-gc-count和gen-1-gc-count是否在疯狂增加。第三板斧是dotnet-dump用于抓进程崩溃和死锁现场dotnet dump collect --process-id $(pgrep -f MyApp.dll) dotnet dump analyze /path/to/dump在 aarch64 上dotnet-dump的 ARM64 版本一般会自动匹配不需要特殊处理但在老版本工具上需要确认dotnet-dump本身是不是 arm64 构建。诊断工具的安装走 dotnet tool 就行dotnet tool install -g dotnet-counters dotnet tool install -g dotnet-trace dotnet tool install -g dotnet-dump如果机器在内网无法访问 NuGet提前在 x64 开发机上把工具包下好用dotnet tool install的本地包源方式装过去。6.2 性能基准到底该看哪些数字迁移完成后的压测我建议至少记录这四组数据延迟P50、P95、P99重点看 P99 是否因为 GC 抖动而抬升。吞吐相同并发下的 QPS 或 RPS。内存稳态工作集观察 24 小时内的趋势确认没有泄漏。启动时间从进程启动到第一个请求可用的时间AOT 模式应该显著优于 JIT。把这些数据和 x64 上的历史数据做对比。如果 ARM 上 P99 比 x64 高 20% 以上优先看 GC 停顿用dotnet trace的gc-dump子命令分析。如果同样并发下 CPU 使用率偏高可能是指令集差异导致的 JIT 生成代码效率不同这时可以试试调整DOTNET_TieredPGO看有没有改善。还有一点容易被忽视不要完全相信纯模拟环境下的跑分结果。用 gem5 这类模拟器跑 SPEC 一类的基准测试只能得到理论参考值真实硬件上的缓存行为、内存带宽和模拟器差距很大性能调优必须以真实机器上的数据为准。最后说点个人体会。把整套服务从 x64 迁到 aarch64 并稳定运行之后最深的感受是.NET 10 在这个平台上的成熟度已经足够支撑生产业务过程中的大部分坑不是来自于 .NET 本身而是来自部署习惯和周边生态的惯性。如果你正在做同样的迁移先把发布形态定下来——我推荐 AOT 优先、SCD 兜底然后再去处理系统库和 NuGet 原生依赖少走很多弯路。