ARTICLE DETAIL

资讯详情

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

统信 UOS 部署 .NET Core 服务:运行时、systemd 与避坑实战

统信 UOS 部署 .NET Core 服务:运行时、systemd 与避坑实战 1. 为什么把 .NET 服务搬到统信 UOS 上比想象中要费劲前两年接手过一个内部管理系统的迁移活儿原本跑在 Windows Server 上的一个 ASP.NET Core 接口服务因为整体环境要往国产化平台过渡要求落到统信 UOS 上。当时团队里不少人的第一反应是Linux 上跑 .NET 不是早支持了吗dotnet run一把梭。真上手才发现统信 UOS 和咱们平时折腾惯了的 Ubuntu、CentOS 还真不是一回事——它的软件源、底层库版本、桌面环境那一套东西都带着自己鲜明的特点照搬网上那些 Ubuntu 部署 .NET 的教程十有八九会在某个环节卡住。这篇内容就是把这套流程从头到尾捋一遍。核心聊的是在统信 UOS 上部署 DotNet(Core) 服务这件事包括运行时怎么装、服务怎么发布、怎么用 systemd 托管、权限端口怎么处理以及那些只有踩过才知道的坑。它不是一份官方文档的复述而是给那些手头正好有一台 UOS 机器、需要把 .NET 服务稳稳当当跑起来的人看的。不管你是刚接触 Linux 的 .NET 开发还是从 CentOS 迁过来的运维只要按着这个顺序走基本能少熬两个通宵。先摆一个结论UOS 上部署 .NET 服务的难点从来不是 .NET 本身而是环境适配。.NET 从 Core 时代就实现了跨平台官方对 Linux 的支持相当成熟问题在于 UOS 的生态位比较特殊——它是面向政企和信创场景打磨的发行版默认的库依赖、安全策略、软件源结构都和主流社区发行版有差异。搞清楚这个前提后面遇到的各种怪问题就都有了解释。我自己总结下来整个迁移链条里最容易出问题的三个环节分别是运行时安装方式的选择、自包含与框架依赖发布的取舍、服务常驻后的权限与依赖库处理。下面按实际操作的先后顺序一个环节一个环节拆开讲。2. 部署前的环境摸底与 .NET 运行时安装动手之前最忌讳的就是直接搜一条安装命令往终端里贴。UOS 的版本差异、CPU 架构差异都会直接影响你该用哪种安装方式。这一步花十分钟摸清楚能省掉后面一堆返工。2.1 先搞清楚你面对的是哪台 UOS统信 UOS 有桌面版、服务器版版本号也有一串底层还分 x86_64 和 arm64鲲鹏、飞腾这些国产 CPU 平台基本都是 arm64。.NET 的运行时包是按架构分的装错了直接跑不起来。先执行这两条命令把底细摸清楚cat /etc/os-release uname -m第一条会输出系统的发行版信息里面能看到 UOS 的具体版本和代号第二条看 CPU 架构x86_64对应微软官方的linux-x64运行时aarch64对应linux-arm64。我见过有人拿着 arm64 的机器去下 x64 的包解压完执行报无法执行二进制文件折腾半天才反应过来是架构不匹配。还有一个容易被忽略的点UOS 自带的 glibc 版本。.NET 运行时对 glibc 有最低版本要求新版本的 .NET比如 .NET 8在较老的 UOS 上可能会因为 glibc 太旧而报错。查一下ldd --version如果 glibc 版本偏低要么选一个对系统要求更宽松的 .NET 版本比如 .NET 6要么走自包含发布把依赖打包进去这个后面会细说。2.2 在线装运行时官方脚本与包管理器怎么选UOS 基于 Debian 系理论上是能用apt的。但现实情况是UOS 的官方软件源里通常不包含微软的 .NET 包你直接apt install dotnet-sdk-8.0大概率是找不到包。微软官方虽然有给 Debian 的源配置脚本但把 Debian 的源硬塞进 UOS很容易触发依赖冲突把系统的包管理搞乱。我个人不太推荐在 UOS 上这么干。更稳妥的方式是用微软官方的dotnet-install.sh脚本。它不走系统包管理器就是把运行时干净地解压到指定目录对系统侵入性小wget https://dot.net/v1/dotnet-install.sh chmod x dotnet-install.sh ./dotnet-install.sh --channel 8.0 --runtime aspnetcore这里有几个细节值得说清楚。--channel 8.0指定大版本脚本会自动拉最新的补丁版本--runtime aspnetcore表示只装 ASP.NET Core 运行时它已经包含了基础运行时如果你只是跑一个控制台程序用--runtime dotnet就够了体积更小。默认它会装到用户目录~/.dotnet下,如果想让所有用户都能用加一个--install-dir /usr/share/dotnet。装完之后还要让系统能找到dotnet命令。把安装目录加进 PATHexport PATH$PATH:/usr/share/dotnet echo export PATH$PATH:/usr/share/dotnet /etc/profile.d/dotnet.sh写到/etc/profile.d/下面的好处是全局生效而且重启后依然有效。这里踩过一次坑只写了~/.bashrc结果用 systemd 托管服务时怎么都找不到 dotnet因为 systemd 读不到用户级的 shell 配置。2.3 离线环境的安装方案信创场景里服务器大多是内网环境根本连不上外网。这时候就得用离线包。思路很简单在一台能上网的同架构机器上把运行时下载下来再拷进内网。./dotnet-install.sh --channel 8.0 --runtime aspnetcore --dry-run加--dry-run不会真正安装但会打印出它准备下载的那个包的 URL。把 URL 复制出来用下载工具拉到本地得到的是一个.tar.gz压缩包。拷到内网机器上解压即可mkdir -p /usr/share/dotnet tar -xzf aspnetcore-runtime-8.0.x-linux-x64.tar.gz -C /usr/share/dotnet提示离线包一定要选和目标机器架构一致的版本。内网没有网络装错了再传一遍非常麻烦。建议连同 SDK 包一起备好万一需要在服务器上重新发布或排查问题有 SDK 会方便很多。装完统一验证一下dotnet --info能正常输出运行时版本、架构信息说明这一步稳了。3. 服务怎么发布、目录怎么规划才不出乱子运行时装好只是第一步真正决定服务能不能长期稳定运行的是发布方式和目录结构。这块很多人图省事直接把开发机上dotnet run那套东西往服务器一丢跑是能跑但埋了一堆隐患。3.1 自包含发布还是框架依赖发布这是部署 .NET 服务绕不开的一个决策。两种方式的差异很实在对比项框架依赖发布自包含发布命令参数--self-contained false--self-contained true产物大小几百 KB 到几 MB几十 MB 到上百 MB是否依赖服务器装运行时需要不需要环境一致性受服务器运行时版本影响完全可控首次部署成本需先装运行时拷过去就能跑框架依赖发布产物体积小但要求服务器上装好对应版本的运行时而且如果服务器上同时跑多个 .NET 服务、对运行时版本要求不一致很容易打架。自包含发布则把运行时和你的程序打包在一起拷到任意同架构的 Linux 上都能直接跑环境完全隔离。我的建议是内网信创环境优先选自包含。原因很现实这类环境往往有严格的安全策略装东西、改配置都要走审批自包含能一次性把所有依赖带齐少一次审批就少一堆扯皮。代价就是产物大一点但现在磁盘空间根本不值钱。发布命令dotnet publish -c Release -r linux-x64 --self-contained true -o ./publish注意-r指定的 RID 要和目标机器架构对应x64 用linux-x64arm64 用linux-arm64。如果你想让产物尽量小可以加上裁剪、单文件这些选项但对多反射的框架支持不友好稳妥起见我一般不开。3.2 服务器上的目录结构设计目录规划这件事看起来不起眼但服务一旦多起来乱放的后果就是没人敢动、没人敢删。我习惯用这么一套结构/opt/myapp/ # 主程序目录放发布产物 /opt/myapp/logs/ # 应用日志 /opt/myapp/config/ # 外部配置文件 /etc/myapp/appsettings.Production.json # 敏感配置单独放主程序放/opt下是 Linux 的惯例。配置文件我特意拆出来能改的东西和不能改的东西分开。发布产物是一次性的重新发布时整个覆盖掉而配置文件、日志这些是需要长期保留甚至人工修改的必须放在发布产物之外。这样每次更新只要覆盖/opt/myapp里的程序文件配置和日志纹丝不动。.NET 读取配置的顺序是从近到远appsettings.Production.json会覆盖appsettings.json。你可以把数据库连接串、密钥这些放生产配置里通过环境变量或文件路径指定它的位置避免敏感信息混进代码仓库。3.3 环境变量的设置服务运行时依赖的环境变量最好在一个地方统一管理别东一处西一处。跑生产环境至少设这几个export ASPNETCORE_ENVIRONMENTProduction export ASPNETCORE_URLShttp://0.0.0.0:5000 export DOTNET_CLI_TELEMETRY_OPTOUT1ASPNETCORE_URLS决定服务监听哪个地址端口0.0.0.0表示监听所有网卡如果只写localhost那就只有本机能访问外网怎么都连不上——这个坑我替别人排查过不止一次。DOTNET_CLI_TELEMETRY_OPTOUT关掉遥测在内网无外网的情况下不关的话程序启动时可能会因为遥测上报超时而拖慢启动速度。这些环境变量如果是通过 systemd 托管的直接写在 unit 文件里最省心下面会讲。4. 用 systemd 让 .NET 服务稳定常驻很多人第一次部署时习惯用nohup dotnet MyApp.dll 这种方式把服务丢到后台。这招在临时测试时能用但绝不适合生产——SSH 一断、服务一崩就没人管了。生产环境的正确姿势是交给 systemd。4.1 写一份靠谱的 unit 文件在/etc/systemd/system/下新建myapp.service[Unit] DescriptionMy DotNet Core Service Afternetwork.target [Service] Typenotify WorkingDirectory/opt/myapp ExecStart/usr/share/dotnet/dotnet /opt/myapp/MyApp.dll Restartalways RestartSec10 Userdotnetuser Groupdotnetuser EnvironmentASPNETCORE_ENVIRONMENTProduction EnvironmentASPNETCORE_URLShttp://0.0.0.0:5000 EnvironmentDOTNET_CLI_TELEMETRY_OPTOUT1 [Install] WantedBymulti-user.target逐行说说关键的几个。Typenotify是个好东西.NET 的 Web 主机会通过 sd_notify 通知 systemd 自己已经就绪这样 systemd 能准确知道服务什么时候真正起来了而不是进程一创建就以为 OK。如果你的程序不是 ASP.NET Core 或没启用这个机制用Typesimple也行。Restartalways配合RestartSec10是保命设置进程意外退出后 10 秒自动拉起遇到内存泄漏或者偶发崩溃时能兜住。User和Group指定专用账户运行这个后面单独讲。如果用的是自包含发布ExecStart直接指向可执行文件就行ExecStart/opt/myapp/MyApp4.2 加载、启动与状态排查写完 unit 文件按顺序执行systemctl daemon-reload systemctl enable myapp systemctl start myapp systemctl status myappdaemon-reload是必须的改过 unit 文件不 reloadsystemd 用的还是老配置改了等于没改。enable让它开机自启不然重启一次服务器服务就没了。status里如果看到绿色的active (running)说明起来了。要是红色failed接着看日志journalctl -u myapp -n 100 --no-pager-n 100看最近 100 行--no-pager防止日志太多卡在翻页里。这个命令是排查启动失败的第一现场程序抛的异常、依赖缺失的报错基本都能在这里看到。4.3 日志接入 systemd 还是落盘.NET 默认会往标准输出写日志systemd 会把标准输出捕获进 journal。这样一来你可以用journalctl统一查看服务日志也能配合日志轮转策略防止磁盘被写满。默认情况下 journal 的日志大小是有上限的不需要额外配置。不过如果你用的是专业的日志框架比如 Serilog、NLog往文件里写日志就要注意日志文件的轮转和清理否则服务跑几个月日志能把磁盘撑爆。我一般两者结合应用自身的业务日志走框架落盘并配置按天轮转系统级、启动级的日志靠 journal 兜底。提示journalctl -u myapp -f可以实时跟踪日志类似tail -f调试时非常好用。追查历史问题时用--since 2024-01-01 10:00限定时间范围比一页页翻快得多。5. 用户权限、端口占用与安全加固服务能跑起来和能安全、稳定地跑在生产环境中间还差着一整套加固动作。这部分是最容易被忽略、出问题也最严重的环节。5.1 用专用账户运行服务默认用 root 跑 .NET 服务是很多新手图省事的做法。这等于把整个系统交给了这个进程万一程序有个漏洞被利用后果不堪设想。正确的做法是创建一个不能登录的专用系统账户groupadd -r dotnetuser useradd -r -g dotnetuser -s /sbin/nologin dotnetuser-r表示创建系统账户-s /sbin/nologin表示不允许登录 shell。然后把程序目录的属主交给它chown -R dotnetuser:dotnetuser /opt/myapp日志目录、配置目录也要一并处理。之前遇到过服务启动报拒绝访问排查半天发现是日志目录属主不对程序没权限写日志。这类问题一定要在部署时就规避掉。5.2 端口绑定与低端口权限Linux 有个安全机制1024 以下的端口比如 80、443只有 root 能绑定。.NET 服务如果要监听 80 端口用普通账户启动会直接报权限错误。但为了绑 80 端口就把服务跑成 root又不划算。两条路可选第一条,用反向代理。让 .NET 服务监听一个高端口比如 5000前面挂一层 Nginx 或 UOS 自带的 Web 服务器由它负责监听 80/443 再转发过来。这是生产环境最常见、也更安全的方式还能顺带做 TLS 卸载、静态资源缓存。第二条如果确实需要普通账户直接绑低端口可以用setcap给可执行文件授权setcap cap_net_bind_serviceep /usr/share/dotnet/dotnet但这样做等于给 dotnet 整个二进制授了权安全性上不如反向代理干净。我个人的偏好还是走 Nginx 反代配置清晰排查问题也方便。5.3 防火墙与只暴露必要端口UOS 上如果开了防火墙别忘了放行服务端口。查一下防火墙状态systemctl status firewalld如果在运行放行端口firewall-cmd --permanent --add-port5000/tcp firewall-cmd --reload如果是内网服务最稳妥的做法是只放行必要的来源 IP别一股脑0.0.0.0全开。安全这件事上少开一个口子就少一分风险。同时记得把ASPNETCORE_URLS的监听地址和防火墙策略对齐别出现服务在监听但防火墙没放、外部访问不通的情况这类问题排查起来最费时间。6. 只有在 UOS 上才会冒出来的那些坑前面讲的是通用流程跟 Ubuntu 上部署的逻辑大差不差。真正让人头疼的是那些在别的发行版上从来没见过、只在 UOS 上冒出来的问题。这部分是我印象最深、也最想分享给同行的经验。6.1 ICU 库缺失导致启动即崩第一次在纯服务器版 UOS 上启动 .NET 服务直接报错退出日志里有这么一句Couldnt find a valid ICU package installed on the system.这是 .NET 全球化Globalization功能依赖 ICU 库来做字符串比较、日期格式化、时区处理而精简版的 UOS 服务器环境往往没预装这个库。解决方式有两条。一是补上库apt install libicu-dev但内网环境下能不能装上、装的版本对不对又是另一个问题。更省心的方式二是直接在项目里关掉 ICU 依赖。在.csproj里加一行PropertyGroup InvariantGlobalizationtrue/InvariantGlobalization /PropertyGroup或者在运行时的runtimeconfig.json里把System.Globalization.Invariant设成true。关掉之后.NET 用内置的不变文化规则处理全球化不再依赖 ICU。代价是某些依赖特定区域文化的高级格式化行为会退化但对大多数业务服务来说完全够用。这个坑我在 Ubuntu 上从没遇到过是 UOS 服务器环境给我上的第一课。6.2 中文字体缺失引发的显示问题如果服务涉及生成 PDF、图片或者做了任何跟文字渲染相关的功能UOS 服务器版默认可能没有中文字体。表现就是生成的文档里中文全是方块或者干脆不显示。查一下系统装了哪些中文字体fc-list :langzh如果输出为空就得把字体装上。把常用的中文字体文件比如思源黑体、文泉驿拷到/usr/share/fonts/下然后刷新字体缓存fc-cache -fv生成 PDF 这类场景字体缺失是高频问题建议在服务上线前就检查一遍别等业务方反馈导出的文件乱码才手忙脚乱。6.3 时区不对导致的时间错乱新装的 UOS 服务器时区默认可能不是Asia/Shanghai。.NET 服务里的时间如果取的是系统本地时间就会比实际时间差好几个小时日志时间戳、业务时间全错。timedatectl看当前的时区设置。不对的话timedatectl set-timezone Asia/Shanghai改完之后最好重启一下服务让进程重新读取时区信息。这个问题隐蔽性强因为程序本身不报错只是数据悄悄地错等发现时可能已经产生了一批脏数据。我现在的习惯是部署清单里把时区检查作为固定项每次新装机都过一遍。6.4 系统自带 .NET 与手动安装版本的冲突有的 UOS 版本或某些预装环境里系统可能自带一个旧版本的 .NET 运行时。你手动装了一个新版本结果dotnet --info看到的还是旧的或者服务加载的是系统那个版本导致功能异常。根子在于 PATH 的查找顺序或者DOTNET_ROOT环境变量指向了旧目录。排查方式是which -a dotnet-a会列出所有能找得到的 dotnet看看是不是有多个。如果有冲突明确指定用哪个版本的路径或者在 systemd 的 unit 里把ExecStart写成绝对路径前面示例里就是这么做的避免依赖 PATH 查找。这个问题不常见但一旦撞上不知道原因的话能查到怀疑人生。7. 服务上线之后我还会盯的几个细节服务跑起来、能访问了不等于部署结束。这套东西上线之后有几个细节我会持续关注它们决定了这套部署方案能不能扛住时间和流量的考验。第一是运行时的版本管理。.NET 的补丁版本更新比较频繁里面有安全修复。但因为我们是手动装的运行时或者自包含发布不会自动更新。建议给自己定个节奏每隔一段时间核对一次官方发布的安全公告该更新时把新版本运行时或重新发布的产物替换上去。更新前一定要在测试环境验证一遍别直接上生产。第二是内存和句柄的长期表现。.NET 服务跑久了可以看看内存增长曲线用systemctl status或者top观察进程的常驻内存。如果发现内存只涨不降可能是代码里有对象没释放也可能是缓存策略不合理。句柄泄漏也一样lsof -p pid | wc -l能看打开的文件数持续增长就要警惕。第三是重启策略的有效性。我们在 unit 文件里配了Restartalways但这个机制得验证过才放心。可以手动 kill 掉服务进程看 systemd 是否在 10 秒内重新拉起kill -9 $(pgrep -f MyApp.dll) sleep 12 systemctl status myapp确认自愈机制真的有效而不是停留在配置里。生产环境里这套机制往往就是凌晨故障时的第一道防线。第四是日志的可观测性。服务上线初期我建议把日志级别调到Information甚至Debug观察一段时间正常运行时的日志输出确认没有异常的报错和警告。等稳定之后再调回合适的级别。光有日志还不够关键是要形成出问题能在两分钟内定位到原因的能力这就需要日志格式统一、关键操作有埋点。把这几点做到位一个在统信 UOS 上的 .NET 服务才算真正落地。整套流程里真正花时间的从来不是敲那几条命令而是踩平那些平台差异带来的坑以及把能跑打磨成敢让它长期跑。
返回列表