ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

统信UOS部署.NET Core服务:运行时选型与systemd托管

统信UOS部署.NET Core服务:运行时选型与systemd托管 1. 先摸清底座UOS 的系统版本决定后面所有选择“单位新配的统信 UOS 机器要跑一个 .NET 服务怎么弄”这是我这两年接得最多的一类咨询。说实话在统信 UOS 上部署 DotNet(Core) 服务本身并不复杂真正浪费时间的地方在于很多人上来就找安装包装完才发现 glibc 太老、libssl 版本对不上、CPU 架构不是 x86_64于是推倒重来。我自己第一次在 UOS 上折腾就栽在这上面一个本来半小时能收工的活拖成了两天。这篇内容主要面向三类人一是有 .NET 后端服务需要落到 UOS 上的开发同学二是负责信创环境交付的运维三是需要在内网、无外网环境下完成部署实施的朋友。我会从系统版本判定、运行时选型一直讲到离线分发、systemd 托管、反向代理和排查手段力求你照着做就能落地。文中所有操作都基于我在 UOS 桌面专业版和服务器版上的实际踩坑经验涉及参数的地方会说明为什么这么选涉及操作的地方会说明这一步到底在干什么。1.1 三条命令判断你的 UOS 到底长什么样统信 UOS 不是一个单一系统它至少分桌面版和服务器版桌面版又分个人版、专业版不同代际的底座差别很大有的基于 Debian 体系用 apt/dpkg 管包有的走 openEuler/CentOS 体系用 dnf/yum。所以第一步不是装 .NET是搞清楚自己站在哪块地上。cat /etc/os-release # 看发行版标识和版本号 uname -m # 看 CPU 架构x86_64 / aarch64 / loongarch64 ldd --version # 看 glibc 版本这一条最关键这三条命令的输出决定了后面所有分支。如果你的uname -m出来是aarch64鲲鹏、飞腾机器上很常见那你去下载 x64 的运行时包装上去只会得到一句“cannot open shared object file”或者更迷惑的“Exec format error”。如果ldd --version出来是 2.17 这种偏老的版本那新版本 .NET 就别硬上了。我再补两条判断包管理器和已有运行环境which apt dpkg dnf yum rpm 2/dev/null # 谁在管包 apt search dotnet 2/dev/null | head # 仓库里有没有现成的 dotnet 包有些国产发行版的官方仓库里其实已经带了一部分 .NET 相关的包或者带了 mono。先去仓库里搜一搜比直接去外面下 tar 包省事得多。这一步花两分钟能省掉后面两小时。1.2 为什么 .NET 版本要对着 glibc 和 ICU 挑.NET 的 Linux 版本并不是“有 Linux 就能跑”它对系统底层库有一组硬性要求最典型的是三样东西glibc、ICU、OpenSSL。glibc 是 C 运行时的地基.NET 的运行时原生库都是链接它的。经验值是想跑 .NET Core 3.1 或者 .NET 6glibc 2.17 这条线基本是可用的RHEL 7 那一档就是这个数想上 .NET 8官方给出的发行版支持列表基本落在 Debian 11 / RHEL 8 这一档对应的 glibc 是 2.28 左右。所以判定逻辑很简单glibc 低于 2.28 的机器优先选 .NET 6 这条 LTS 线glibc 2.28 以上的放心上 .NET 8。硬要在 glibc 2.17 上跑 .NET 8可能能启动也可能在加载某个原生库时直接崩而且这种崩往往不是启动时崩是跑到某个业务分支才崩排查成本极高。ICU 这个坑更隐蔽。.NET 5 之后默认走 ICU 做全球化处理字符串比较、排序、日期格式、大小写规则都靠它。系统里没有 libicu或者版本太离谱启动时会抛“Couldnt find a valid ICU package”然后退出。这个报错信息其实挺友好看到它你就知道该干嘛了。OpenSSL 是给 HTTPS、加密、证书相关功能用的。.NET 6 对 OpenSSL 1.0.2/1.1/3.0 都还能接受.NET 7 之后基本只认 1.1 和 3.0 了。如果你的 UOS 底座只提供 libssl1.1那就别上太新的 .NET 版本或者想办法把 libssl3 也补进去。1.3 架构选型x64、arm64 和更特殊的平台这一步经常被忽略但它是部署失败率最高的原因之一。你在 x86 笔记本上开发的 .NET 服务直接拷到飞腾或鲲鹏的 UOS 机器上是跑不起来的。解决办法有两个方向一是在目标机上有 SDK 的情况下直接用目标架构发布。命令里带上-r linux-arm64框架相关的原生库会自动换成 arm64 的。这种做法适合目标机配置还行、能装完整 SDK 的场景。二是在一台同架构的构建机上发布好再把产物整目录拷过去。这种做法适合目标机资源紧张、或者压根不让装 SDK 的生产环境。注意“同架构”这条你在一台 aarch64 的机器上dotnet publish -r linux-arm64产物拷到另一台 aarch64 上是没问题的但如果你用 x64 机器发布 arm64 产物那需要 SDK 里带上对应的运行时包配置上会更绕。至于更特殊的架构比如某些国产 CPU 平台官方主线的 .NET 支持可能不完整通常需要依赖对应的移植版本。这种情况建议先确认你手上的 .NET 发行包是不是专门为那个架构编译过的别拿通用包硬试浪费时间。提示发布前先在目标机把uname -m结果记下来构建命令里的-r参数必须和它严格对应x86_64 对应linux-x64aarch64 对应linux-arm64。这两者写反了程序哪怕启动成功也会在运行时出现难以理解的行为。2. 装运行时的三条路源装、脚本装、离线解压运行时怎么装取决于你的网络条件。能连外网的开发机怎么都好办内网生产机才是真正的考验。我把三条路都走了一遍下面分别说清楚适用场景和具体操作。2.1 路线一配置包源用 apt/dnf 装适合能连外网这是最省心的方式装完之后包管理器能帮你处理依赖、升级和卸载。Debian 体系的操作大致是这样# 下载并安装微软的包源配置Debian 系底座 wget -O packages-microsoft-prod.deb https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb sudo apt update # 只跑服务的话装 ASP.NET Core 运行时就够了 sudo apt install -y aspnetcore-runtime-8.0 # 需要在目标机上编译就装 SDK sudo apt install -y dotnet-sdk-8.0如果你的 UOS 是 RPM 体系服务器版某些代际走这条路把dpkg换成rpm -ivh把apt换成dnf或yum即可。这里有个细节源配置文件里带的发行版代号比如debian/11要和你机器的实际底座大致对得上对不上会出现“找不到包”或者依赖版本冲突。我的习惯是先用cat /etc/os-release里的VERSION_ID确认一下再决定用哪个路径。再补一句如果你只需要跑服务不要装 SDK。SDK 体积大几百 MB而且会往系统里塞一堆编译工具链生产机上没必要。2.2 路线二用官方脚本安装适合半内网、可控的机器dotnet-install.sh这个脚本是官方维护的安装脚本它的好处是可以指定频道、安装目录、只装运行时而且装在哪个目录完全由你说了算。内网机器如果有一台能出网的跳板机把脚本先下下来再想办法传进去剩下的操作就可以离线执行了。# 在有网的机器上先下脚本 wget https://dot.net/v1/dotnet-install.sh chmod x dotnet-install.sh # 安装 ASP.NET Core 运行时 8.0 到指定目录 sudo ./dotnet-install.sh --channel 8.0 --runtime aspnetcore --install-dir /usr/share/dotnet # 建立全局软链接 sudo ln -sf /usr/share/dotnet/dotnet /usr/bin/dotnet装完一定要dotnet --info看一眼重点确认三件事版本号对不对、运行时列表里有没有Microsoft.AspNetCore.App、RID运行时标识是不是你期望的linux-x64或linux-arm64。这三项任何一个不对后面都是白忙。2.3 路线三离线 tar.gz 手工解压最稳生产首选真正隔离的内网环境上面两条路都走不通这时候就靠离线包。好处是过程完全透明装了什么、装到哪你自己心里有数坏处是依赖得自己补。# 1. 在能出网的机器上下载对应的 tar.gz注意架构和版本 # 2. 传到目标机后解压到统一目录 sudo mkdir -p /usr/share/dotnet sudo tar -zxvf aspnetcore-runtime-8.0.*-linux-x64.tar.gz -C /usr/share/dotnet # 3. 建立软链接保证 dotnet 命令全局可用 sudo ln -sf /usr/share/dotnet/dotnet /usr/bin/dotnet # 4. 权限修正避免非 root 用户无法执行 sudo chmod -R 755 /usr/share/dotnet # 5. 验证 dotnet --info这套流程我在十几台机器上重复过稳定得很。唯一要注意的是解压前先确认目标目录干净别让两个版本的运行时文件混在一起。如果你确实需要多版本共存.NET支持同一目录下放多个版本dotnet --list-runtimes能看到全部程序会按自身的目标框架自动挑选但目录结构不能被破坏。2.4 三条路线的对比与选型维度包源 apt/dnf官方脚本离线 tar.gz网络要求需持续可访问外部源需先下脚本可离线执行完全离线依赖处理自动部分自动全靠手工升级维护最简单需重新执行脚本手工替换目录目录可控性低系统默认路径高任意目录高适用场景开发机、外网服务器半内网、测试环境生产内网、信创交付我个人的选择顺序是生产环境一律走离线 tar.gz把运行时目录固定下来做进交付文档开发和测试环境用脚本或包源图个方便。这样做最直接的好处是出问题时你能确定“环境是这个版本而且从来没被包管理器悄悄改过”。2.5 装完必做的依赖体检装完运行时先别急着部署业务花三分钟做一次依赖体检能提前拦掉一大批问题。# 1. 看运行时目录下的核心原生库有没有缺失依赖 ldd /usr/share/dotnet/shared/Microsoft.NETCore.App/8.0.*/libcoreclr.so | grep not found # 2. 看系统里 ICU、OpenSSL、libunwind 的实际情况 ldconfig -p | grep -E libicu|libssl|libunwind # 3. 直接跑一次自检 dotnet --info dotnet --list-runtimes只要第 1 条命令有输出也就是有not found就说明缺库必须补上再往下走。ICU 缺失的典型表现是启动时报全球化相关错误OpenSSL 缺失的典型表现是 HTTPS 或者加密相关代码一跑就抛异常。这两个都是“不体检发现不了、上线后才炸”的类型。注意如果确实补不上 ICU比如系统仓库里没有对应版本可以退而求其次设置环境变量DOTNET_SYSTEM_GLOBALIZATION_INVARIANT1。但这会改变字符串比较和排序行为如果你的业务里有中文排序、区域性日期格式务必先做一轮功能回归别直接上生产。3. 发布与打包把服务做成能“搬家”的形态运行时装好了接下来是把你的服务打包成可以搬来搬去的形态。这一步的核心决策是依赖框架发布还是自包含发布。选错了部署时会很别扭。3.1 依赖框架发布 vs 自包含发布的取舍依赖框架发布framework-dependent产出体积小通常几 MB 到几十 MB但它要求目标机必须装好对应版本的 .NET 运行时。自包含发布self-contained会把运行时一起打进去产物体积通常在 60MB 到 100MB 以上但目标机不需要额外装运行时。我的一般原则是目标机多、版本要统一管控的用依赖框架发布把运行时当作基础设施统一装一次后面所有服务共享。目标机少、或者交付方要求“拷贝即用”的用自包含发布虽然包大一点但交付时少一轮沟通成本。需要特别说明的是自包含并不等于“完全脱离系统”。它仍然依赖系统的 glibc、libssl、ICU 这些底层库。所以第 1 节的体检步骤在自包含方案里同样要做不能省。3.2 发布命令和产物目录说明# 依赖框架发布x64 dotnet publish -c Release -r linux-x64 --self-contained false -o ./publish-fd # 自包含发布x64 dotnet publish -c Release -r linux-x64 --self-contained true -o ./publish-sc # ARM64 平台把 -r 换成 linux-arm64 dotnet publish -c Release -r linux-arm64 --self-contained true -o ./publish-sc-arm参数逐个说清楚-c Release走发布配置别用 Debug-r指定目标运行时标识这一步决定了哪些原生库被打进去--self-contained决定是否捆绑运行时-o指定输出目录。发布完成后产物目录里应该能看到你的主程序 dll、一堆依赖 dll、以及appsettings.json之类的配置文件。如果是自包含还会看到dotnet可执行文件和shared目录。这里有个容易踩的坑-r参数一旦指定bin目录下的中间产物也会按这个 RID 组织如果你中途改了 RID建议先dotnet clean再重新发布别在脏目录上接着编否则可能混进上一轮的旧文件。3.3 单文件发布与裁剪的两个坑很多人喜欢单文件发布把所有东西打成一个可执行文件部署时看着清爽。命令是在发布参数里加-p:PublishSingleFiletrue。它确实方便但要注意两点。第一单文件模式下程序启动时会先把内容解压到临时目录再加载冷启动会慢一点通常零点几秒到一两秒取决于包大小和磁盘速度。对延迟敏感的接口建议配合-p:PublishReadyToRuntrue做预编译或者干脆不用单文件。第二裁剪trimming要慎用。-p:PublishTrimmedtrue能显著减小体积但它靠静态分析判断哪些代码没用到而 .NET 里大量依赖反射和动态加载分析不出来的部分会被误删表现为运行到某个功能才抛MissingMethodException。我的建议是只有在你做过完整回归测试、并且清楚项目里没有重度反射依赖时才开裁剪。实操心得发布产物里的配置文件appsettings.json、appsettings.Production.json建议不要打进包管流程而是部署时单独分发。这样改配置不用重新发布而且不同环境的配置文件可以明确区分减少“把测试库连接串带到生产”这类事故。4. systemd 托管让服务开机自启、崩了自拉把发布产物拷到目标机只是第一步。真正要让服务像个正经服务一样跑起来得交给 systemd 托管。手工nohup dotnet xxx.dll 那套在开发机上玩玩可以生产环境绝对不能这么干。4.1 目录布局和专用账号先定目录。我习惯用/opt下面放应用按服务名建子目录sudo mkdir -p /opt/myapi sudo cp -r ./publish-sc/* /opt/myapi/ # 建一个专用系统账号不给登录 shell sudo useradd -r -s /usr/sbin/nologin -d /opt/myapi appsrv # 目录归属交给这个账号 sudo chown -R appsrv:appsrv /opt/myapi sudo chmod -R 750 /opt/myapi为什么要建专用账号因为用 root 跑服务的风险太高一旦服务被拿下攻击者直接拿到系统最高权限。专用账号即使被攻破影响面也限制在这个目录里。这一步很多人嫌麻烦跳过但它是安全基线里性价比最高的一条。4.2 unit 文件逐行解读在/etc/systemd/system/myapi.service里写[Unit] DescriptionMy API Service (dotnet) Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple WorkingDirectory/opt/myapi ExecStart/usr/share/dotnet/dotnet /opt/myapi/MyApi.dll Userappsrv Groupappsrv Restartalways RestartSec5 KillSignalSIGINT TimeoutStopSec30 LimitNOFILE65535 EnvironmentASPNETCORE_ENVIRONMENTProduction EnvironmentASPNETCORE_URLShttp://127.0.0.1:5000 EnvironmentDOTNET_CLI_TELEMETRY_OPTOUT1 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target逐条解释我为什么这么写。After和Wants保证网络就绪后再启动避免服务起来时网络还没通导致连数据库失败。Typesimple是最通用的类型如果你在代码里集成了 systemd 通知机制Microsoft.Extensions.Hosting.Systemd提供的UseSystemd()可以改成Typenotify这样 systemd 能准确知道服务什么时候真正就绪而不是“进程起来了就算成功”。Restartalways配合RestartSec5是自愈的关键。服务因为未捕获异常崩掉5 秒后自动拉起来运维不用半夜爬起来重启。KillSignalSIGINT是给 .NET 的优雅停机信号让程序有机会把正在处理的请求做完、把连接池关干净而不是被SIGKILL硬砍。TimeoutStopSec30是给优雅停机留的时间窗业务处理慢的话可以调大。LimitNOFILE这条看着不起眼但高并发场景下非常关键。默认的文件描述符上限可能只有 1024一个连接占一个 fd压力一上来就“Too many open files”。我一般都直接拉到 65535。ASPNETCORE_URLS写127.0.0.1:5000而不是0.0.0.0:5000是有意为之服务只监听本机外网流量必须经过 Nginx 那一层。这样 TLS 证书、限流、访问日志都集中在 Nginx 上管应用本身不管这些事。4.3 配置分离和环境变量优先级.NET 的配置体系有个约定环境变量的优先级高于配置文件。这个特性在容器化和 systemd 场景下特别好用——同一个发布包靠环境变量区分环境。在 systemd 里注入环境变量有两种写法。上面那种是直接写Environment简单直接。另一种是把变量集中放到一个文件里EnvironmentFile-/etc/myapi/myapi.env文件内容长这样ASPNETCORE_ENVIRONMENTProduction ASPNETCORE_URLShttp://127.0.0.1:5000 ConnectionStrings__DefaultServer127.0.0.1;Databaseappdb;Uidappuser;Pwdxxxxxx注意连接串里的层级分隔符是双下划线__对应 JSON 配置里的ConnectionStrings:Default。这个映射规则记牢能把配置文件和应用包彻底解耦。前面那个减号-表示文件不存在也不报错方便首次部署时逐步补配置。配置文件权限记得收紧里面有密码sudo chmod 600 /etc/myapi/myapi.env sudo chown appsrv:appsrv /etc/myapi/myapi.env4.4 启动、验证与日志排查sudo systemctl daemon-reload sudo systemctl enable --now myapi sudo systemctl status myapi # 实时看日志 sudo journalctl -u myapi -f # 只看最近 200 行 sudo journalctl -u myapi -n 200 --no-pagerdaemon-reload这一步新手最容易忘。改完 unit 文件不 reloadsystemd 用的还是旧配置然后你就会陷入“明明改了却不起作用”的困惑。验证环节我给一套组合拳systemctl status看进程状态ss -lntp | grep 5000看端口有没有真的监听curl -v http://127.0.0.1:5000/health打一下健康检查接口。三条都过了才算真正部署成功。只看 status 显示 active 是不够的进程活着不代表端口通了。注意如果journalctl里看到服务反复“start → 退出 → restart”的循环先看退出前的最后几行日志。九成情况是工作目录不对、dll 路径写错、或者配置文件权限不够导致读取失败。这类问题 systemd 本身不会给你明确提示得靠日志里的异常堆栈定位。5. 反向代理与对外暴露服务在本机跑通了接下来是让外部能访问。我不建议让 Kestrel 直接对外原因和具体配置在下面说。5.1 为什么中间要加一层 NginxKestrel 是 .NET 自带的 Web 服务器性能很好但它定位是应用服务器不是边缘网关。让它直接对外有几个现实问题一是绑定 80/443 这类特权端口需要额外授权普通账号没有权限二是 TLS 证书管理、HTTP/2、静态文件缓存这些活它做起来不顺手三是访问日志、限流、灰度分流这些运维需求在 Nginx 层处理要方便得多。所以标准架构是Kestrel 监听127.0.0.1:5000Nginx 监听 80/443把请求转发过去。server { listen 80; server_name api.example.internal; client_max_body_size 100m; location / { proxy_pass http://127.0.0.1:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_connect_timeout 10s; } }几个配置点的用意proxy_http_version 1.1加Upgrade和Connection头是为了支持 WebSocket 和 SignalR不加这两个长连接会握手失败。client_max_body_size默认只有 1MB文件上传接口必崩按业务实际需要放大。proxy_read_timeout默认 60 秒遇到导出报表、批量处理这类慢接口会被截断我一般给到 300 秒。转发头这块要特别注意光在 Nginx 里设X-Forwarded-For是不够的应用侧还得主动去读它。在 .NET 的程序启动代码里要启用转发头中间件并配置可信代理否则你拿到的客户端 IP 永远是127.0.0.1日志分析和风控都做不了。// 在 Program.cs 的中间件管道前段启用 app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, KnownNetworks { new IPNetwork(IPAddress.Parse(127.0.0.1), 8) } });KnownNetworks这一步别省它限定了只信任来自本机的转发头避免外部伪造 IP。5.2 防火墙与端口策略UOS 桌面版和服务器版用的防火墙不太一样先判断systemctl status firewalld # 有输出说明走 firewalld sudo ufw status # 有输出说明走 ufwfirewalld 开放 80 端口sudo firewall-cmd --permanent --add-port80/tcp sudo firewall-cmd --reloadufw 的话sudo ufw allow 80/tcp原则是只开放必须的端口5000 端口对外坚决不开。如果实在有临时调试需求可以只对特定来源开放完事立刻关掉。实操心得在信创交付环境里很多机器的防火墙策略是由统一平台下发的本地改了可能一会儿就被覆盖回去。这种情况别硬刚先和网络管理员确认端口开放策略走正式流程申请。我见过有人反复改本地防火墙改了三天最后发现是上层策略在回滚。6. 踩坑速查表与排查手法前面讲的都是顺利路径。实际干活时出问题才是常态。这一节把我在 UOS 上遇到过的高频故障整理成对照表附上定位手段。6.1 高频报错对照表报错关键词大概率原因处理方向Exec format error运行时包架构和目标机不匹配确认uname -m换对应架构的包cannot open shared object file缺 glibc/libssl/libicu 等依赖ldd查缺失项补对应库Couldnt find a valid ICU package系统无 libicu 或版本差异大装 libicu或设 globalization invariantNo usable version of libssl was foundOpenSSL 版本不在支持列表装 libssl1.1 或 libssl3或降 .NET 版本Address already in use端口被占ss -lntp查占用进程改端口或停冲突服务Permission denied绑 80 端口普通账号无权绑特权端口改用高位端口 Nginx 转发服务启动即退出、无日志工作目录或 dll 路径错核对WorkingDirectory和ExecStart中文显示为乱码缺 locale 或 ICU 不可用配置系统 locale检查 ICU时区报错、时间差 8 小时缺 tzdata安装 tzdatatimedatectl set-timezone Asia/Shanghai上传大文件返回 413Nginx 体积限制调大client_max_body_size长连接频繁断开Nginx 超时过短调大proxy_read_timeout这张表里的每一项我都真实遇到过。其中“服务启动即退出、日志还看不清”这一类最折磨人因为它信息量太少。下面说排查套路。6.2 三层排查法系统层、运行时层、应用层我一直用“三层排查法”来定位 .NET 服务问题从下往上一层层排除效率最高。系统层先看。进程在不在systemctl status、端口通不通ss -lntp、依赖全不全ldd。这一层解决的是“能不能跑起来”的问题。运行时层再看。dotnet --info确认运行时版本和 RIDdotnet --list-runtimes确认 ASP.NET Core 运行时装没装。这一步经常能发现“只装了 .NET Runtime 没装 ASP.NET Core Runtime”这种低级但常见的问题——服务启动时报找不到Microsoft.AspNetCore.App就是这个原因。应用层最后看。直接手工前台运行把错误暴露在终端里cd /opt/myapi sudo -u appsrv ASPNETCORE_ENVIRONMENTProduction /usr/share/dotnet/dotnet MyApi.dll手工跑和 systemd 跑的关键差别是手工跑能看到标准输出和标准错误抛异常时完整的堆栈直接打在屏幕上。这一步能把“配置文件读不到”“数据库连不上”“某段初始化代码抛异常”这类问题瞬间定位。定位完了再回到 systemd 方式跑。还有两个我常用的深挖工具# 看程序到底在找哪个文件 strace -f -e traceopenat \ /usr/share/dotnet/dotnet /opt/myapi/MyApi.dll 21 | grep -i no such file # 看原生库加载情况 LD_DEBUGlibs /usr/share/dotnet/dotnet /opt/myapi/MyApi.dll 21 | head -50strace那条命令能直接告诉你“程序尝试打开某个文件但没找到”的完整路径对于缺依赖、缺配置、缺证书这类问题比看日志猜要快得多。6.3 几个容易忽略的稳定性调优服务跑起来之后还有些调优项值得关注尤其是迁移到信创环境后性能表现和原来的 x86 服务器不一致的情况。GC 模式是个大项。默认的工作站 GC 适合单核或低负载场景多核大内存的服务器上可以考虑启用服务器 GC在环境变量里设DOTNET_gcServer1。但它不是无脑开——服务器 GC 会为每个核心分配独立堆如果机器核心很多但内存不大反而容易触发内存压力。我一般在核心数 4 到 16、内存 16GB 以上的机器上开。预编译也值得做。发布时加上-p:PublishReadyToRuntrue把 IL 提前编译成部分原生代码冷启动能快不少代价是包体积变大十几到几十 MB。对启动速度有要求的服务这笔买卖划算。线程池方面如果服务里有大量同步阻塞调用可以适当调ThreadPool.SetMinThreads。但这是治标根子上还是应该把阻塞调用改成异步。我在一个老项目上见过因为没改异步、只调线程池最后线程数飙到几千、上下文切换把 CPU 吃满的情况。日志这块systemd 的 journal 默认会一直写长期跑下来能吃掉几个 G 的磁盘。建议在 journald 配置里限制总量或者把应用日志单独输出到文件并配轮转。我一般会设一个上限避免磁盘被日志写满导致服务挂掉——这种故障排查起来特别冤。7. 我在信创环境里积累的几条经验做了这么多次 UOS 上的 .NET 部署有几个体会是文档里不会写、但特别影响交付效率的单独拿出来说说。第一条把运行时版本和 glibc 版本的对应关系做成一张表随交付文档一起给出去。我现在的做法是每次交付前先跑一遍目标机的系统信息采集脚本把发行版、内核、glibc、架构、已装运行时全部记下来归档到交付材料里。这样下次升级或者排障不用再重新问一遍。这个习惯帮我省掉了很多“上次那台机器的环境到底是什么来着”的来回确认。第二条依赖库尽量提前准备好离线包。内网环境最怕的不是装不上是装到一半发现缺东西然后出不了网。我的做法是提前把可能用到的库包libicu、libssl、tzdata、libgdiplus 等按架构全部下好放在同一个目录里带进去。哪怕最后只用上一两个也比现场抓瞎强。特别是tzdata缺了它时区处理会出各种诡异问题而这个包体积很小多带一份完全没负担。第三条不要在信创机器上直接开发。有些同事图方便直接在 UOS 机器上装 SDK 写代码。能做但不推荐一是 SDK 体积大、机器资源占用高二是开发过程中引入的临时包会污染环境导致“开发机跑得好好的一发布就报错”。我的建议是在开发机上用同样的 RID 发布把产物拷到目标机上跑保持目标机环境干净可控。第四条关于扩展场景的一句提醒。现在不少 .NET 服务后面还要对接本地的推理服务或者向量库比如在机器上另外跑一个模型推理进程.NET 这边通过 HTTP 去调用。这种架构下我建议把所有对外入口统一收到 Nginx 后面用不同的location前缀区分同时注意推理服务的内存占用要和服务做隔离别让推理把内存吃光导致你的 .NET 服务被系统杀掉。这一点我自己踩过排查的时候一度以为是 .NET 内存泄漏最后发现是旁边那个进程把整机内存榨干了。第五条给服务留一条优雅停机的通道。前面 unit 文件里写的KillSignalSIGINT和TimeoutStopSec不是摆设。我在一个订单服务上遇到过重启时因为有请求正在处理数据库事务被硬杀之后留下了一批状态不一致的数据人工修数据花了大半天。从那以后凡是涉及写操作的服务我都会确认优雅停机路径是通的并且TimeoutStopSec给得足够宽。最后分享一个我自己常用的小技巧部署完成后写一个自检脚本放在机器上内容包括检查进程状态、检查端口监听、打健康检查接口、检查磁盘和内存余量输出一份简短的报告。每次升级或者重启后跑一遍几十秒就能确认服务状态比一条条敲命令快得多。这个脚本我一般还会加到运维手册里交接给别人的时候对方也能直接用省掉大量重复沟通。
返回列表