.NET vs Java:物联网边缘采集为何选.NET?从内存占用到部署实战 同一个产线项目Java 版本被客户运维指着鼻子问“是不是得换机器”转头我把采集网关用 .NET 重写了一遍跑在同一台工控机上运维看完监控数据不说话了。这事情搁谁身上都得想清楚一个问题物联网系统选 .NET 还是 Java真不是开发人员拍脑袋的偏好问题而是项目能不能验收、客户会不会把你扫地出门的生存问题。先说清楚我并不是 Java 黑。做了十多年后端Java 在互联网高并发大流量场景下的生态和稳定性我比谁都清楚。但物联网系统的运行环境跟互联网后端完全是两个世界客户的产线工控机可能是十年前买的内存只有 4GCPU 是老赛扬系统还是 Win7 嵌入式现场没有专职的运维团队甚至连 Docker 都没听过。在这种环境下把一套 JVM 程序硬塞进去客户运维看一眼任务管理器里那 800MB 到 1GB 的内存占用再听你说“得配一台 16G 内存的服务器”他不赶你下台才怪。这篇文章我就拿这个真实项目当主线把物联网系统技术选型这件事彻底讲透。我会拆解 .NET 在边缘侧、产线侧、工业现场的真实优势也会老实交代 Java 在什么场景下依然值得选最后附上我用 .NET 搭建边缘采集网关的完整实操思路和踩坑记录。不管你是做工业物联网、智慧园区还是设备数据采集这篇文章都值得读完再决定技术栈。1. 事情是怎么闹到“换机器”这一步的1.1 客户现场的真实硬件条件我接的这个项目是一条汽车零配件产线的数据采集系统核心需求是把 PLC、电表、气表、RFID 读写器的数据每秒钟采一次经过清洗、聚合之后写入现场数据库同时把实时状态推到车间大屏。听起来不复杂对吧但下面这些现场条件是大多数后端开发根本没体会过的第一工控机不是服务器。客户产线的上位机是 2015 年前后买的研华工控机CPU 是赛扬 J1900 四核内存 4GB硬盘是机械盘。这配置放今天连办公都费劲但它要在 7×24 小时不关机的情况下同时跑组态软件、数据库客户端、OPC 通信程序现在还要加上我们的采集服务。第二操作系统五花八门。这条生产线有 8 台工控机3 台 Win7、3 台 Win10、2 台 Windows Server 2012。Win7 那几台还没有加装更新补丁.NET Framework 的版本也是乱七八糟。你要是在这种环境上装 JRE、配置 JAVA_HOME、调 GC 参数客户运维会觉得你在变魔术。第三现场没有“专职运维”这回事。客户的运维其实是产线电工兼的他能做到断电重启、看任务管理器、杀毒软件放行就已经是天花板了。你跟他说“先安装 JDK 17配置环境变量再用 systemd 托管”他的表情就像你让他用 C 语言写 PLC 程序一样茫然。这些条件叠加起来技术选型的答案就已经不是“哪个语言更流行”的问题了而是“哪个方案能在客户的破机器上安稳跑起来并且让不懂技术的运维也能看得懂”。这也是我在标题里说“选 .NET 不选 Java竟然是为了不被客户赶下台”的真正原因。1.2 Java 版本现场被嫌弃的具体表现项目一开始其实是 Java 团队接的我当时只是被拉去评审。那套 Java 版本的采集服务用的是 Spring Boot 3 Netty MyBatis代码写得挺规整微服务拆了四五个模块有网关、有采集器、有消息队列消费者。但到了客户现场问题一个接一个冒出来。首先是内存。Spring Boot 应用默认启动就要吃 300MB 到 500MB 内存我们拆了 4 个服务再加上 Kafka、Redis 这些配套整个集群直接把工控机的 4GB 内存吃满了。客户运维打开任务管理器看到 java.exe 的进程列表占了一整屏当场就打电话问我“你们这软件是不是病毒怎么开了十几个窗口”其次是启动时间。JVM 冷启动加上 Spring 容器初始化每次重启要等 40 秒到 1 分钟。产线停电恢复之后工控机开机要两分钟我们的服务又要等一分钟采集数据中间断档十几分钟。客户车间主任根本接受不了这种“系统不稳定”的印象天天找项目经理投诉。最要命的是资源占用导致的其他程序异常。采集服务一跑起来CPU 占用率长期在 30% 到 50% 浮动组态软件偶尔卡顿现场的操作员工直接骂上了。运维没办法只能建议“换一台 i7 处理器、16GB 内存的新机器”。客户老板看到这个建议第一句话就是“你们写的软件凭什么要我换机器”项目验收自然就卡住了。我接手做技术重写的时候目标非常明确不换硬件、不装额外运行环境、不改变客户的使用习惯用同样的工控机把采集服务跑起来内存占用不超过 300MB启动时间不超过 10 秒CPU 占用率控制在 5% 以内。能做到项目就能活做不到这单生意就黄了。2. .NET 在物联网场景里的硬实力到底硬在哪2.1 内存占用同一个采集流程差出一个数量级用 .NET 重写之后同样是采集 50 台设备的实时数据、做协议解析和边缘计算内存占用从 Java 版本的 800MB 以上降到了 160MB 到 220MB。这不是我优化得多厉害而是运行时的“底子”就不一样。Java 的 JVM 是一个重量级虚拟机内置了完整的垃圾回收器、JIT 编译器、线程管理、类加载器光 JVM 自身的基础开销就有 200MB 左右这是省不掉的。加上 Spring Boot 框架的反射注入、动态代理、组件扫描以及 Netty 的多个线程池堆外内存和元空间还会持续膨胀。在一个 4GB 内存的老工控机上这 800MB 往往就是压死骆驼的最后一根稻草。.NET 这边.NET 5 之后的 CoreCLR 做了大量精简垃圾回收器的分代策略更激进启动时不加载的模块就不加载服务器 GCServer GC和工作站 GCWorkstation GC可以按场景灵活切换。尤其在只做数据采集和传输的边缘节点上.NET 的内存占用比 Java 低了 60% 到 70%这是我在多个项目中反复验证过的结论。不过我也得说句公道话如果只是比“高峰期 10 万并发下的内存表现”Java 和 .NET 的差距并不大甚至 JVM 在大堆场景下表现更稳定。但物联网边缘侧恰恰不是大并发场景而是“小并发 7×24 小时长跑 硬件资源吃紧”的场景这时候 .NET 低基座的资源优势就被无限放大了。2.2 启动速度和容器镜像边缘设备等不起Java 版本启动 40 秒以上.NET 版本冷启动 3 到 5 秒热启动 1 秒以内。这个差异在生产环境里可不是“体验好坏”的问题而是“系统可不可用”的问题。举个真实场景产线突然停电市电恢复后PLC、组态软件、采集服务全部要重新启动。如果采集服务要等一分钟才能恢复这条产线在这一分钟里失去数据监控按客户的质量追溯要求这段时间的产品全部要标记为“无监控数据”甚至要隔离复查。多停一分钟产线就多损失几十件产品的追溯信息。另外如果你们打算用容器化部署镜像大小的差异也很关键。Java 的 JRE 基础镜像动辄 200MB 以上加上 Spring Boot 的胖 JAR 包最终镜像轻松突破 500MB。而 .NET 的 ASP.NET Core 镜像基础版只有 80MB 左右SDK 镜像也才 200MB 上下如果做自包含单文件发布PublishSingleFile一个采集网关的整个程序就是一个 50 到 80MB 的可执行文件拷贝到哪都能跑。在边缘侧很多设备是 ARM 架构磁盘空间本来就紧张网络带宽也有限。Java 那套“大而全”的运行时在这个场景下就是负担。2.3 单机并发与性能.NET 的异步模型更“吃”得起资源有人可能会反驳Java 的 Netty 在并发处理上是很强的凭什么说 .NET 更合适这个观点没错Netty 确实成熟但问题在于 Netty 的上层往往要搭配 Spring Boot 的线程池、消息队列的消费线程这一套组合拳在资源受限的机器上反而是瓶颈。.NET 原生的 Kestrel 服务器和 System.Threading.Channels 异步管道配合 async/await 模型在单机上处理几万路 TCP 长连接是非常轻松的而且线程占用极少。我在这个产线项目里用的是 .NET 的 IHostedService Channel MQTTnet一个采集服务进程同时接入 Modbus TCP 和 OPC UA 通信1000 多个点位的数据流CPU 占用率始终控制在 8% 以内。另外.NET 的 AOT 编译Native AOT是 Java 目前没有对应等价物的方案。如果用 Native AOT 发布程序直接编译为原生机器码不依赖任何运行时启动时间缩短到毫秒级内存占用再砍一截这在嵌入式 Linux 设备上简直是杀手锏。不过 AOT 也有坑后面实操部分细说。2.4 部署与运维一条命令和一个文件夹的区别回到客户运维的视角这是最实际的一环。Java 部署一套服务要装 JDK、配环境变量、可能需要 Nginx 转发、要建多个目录、写启停脚本。中间任何一步出了问题对非专职运维的人来说都是灾难。.NET 我一般推荐两种部署方式都能让运维门槛降到零第一种是框架依赖部署。在工控机上装一个 .NET Desktop Runtime 或 ASP.NET Core Runtime装完就一劳永逸后面发版只需要替换文件夹里的文件。.NET 运行时的安装包就是一个 exe双击下一步没有任何额外配置比装个 QQ 还简单。第二种更绝自包含单文件部署。用 dotnet publish 命令直接把程序、依赖、运行时全部打成一个 exe 文件拷到目标机器双击就能跑。客户现场不需要安装任何运行时也不需要连外网这对很多保密要求高的军工、能源、汽车企业来说简直是刚需。发布命令就一条我给你贴在下面dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFiletrue -p:IncludeNativeLibrariesForSelfExtracttrue -o ./publish跑完这条命令发布的文件夹里就只有一个 MyGateway.exe。拷到客户那台 Win7 老工控机上双击启动日志打到当前目录的 logs 文件夹就这么简单。Java 项目要做到这个程度且不说 GraalVM Native Image 的兼容性坑光是构建流程就复杂得多。3. 我为什么敢在产线项目里押注 .NET3.1 技术选型的本质是风险管理很多人把技术选型当成“技术偏好之争”其实技术选型本质上是风险管理和成本管理。你要评估的不是哪个语言更好而是哪个方案在“这个项目、这个客户、这个运行环境、这个团队能力”的组合下交付风险最低、维护成本最可控。在物联网/产线数字化这个细分领域风险点从来不在并发量而在下面这几个维度硬件环境不可控你不知道客户的机器有多老、系统有多旧、磁盘有多满。运维能力薄弱现场的人能接受“双击运行”接受不了“命令行 配置 依赖管理”。交付节奏快物联网项目往往边施工边改需求服务要频繁更新部署方式必须轻量。设备离线场景多网络不稳定、断电断网是常态程序必须有很强的健壮性不能一崩就要“重装系统”。把这四个风险点摆出来再回头看 Java 生态它的核心优势庞大的第三方库、成熟的企业级中间件、海量社区问答在这个场景里基本派不上用场。反而那些重框架、重依赖的解题思路会变成现场的负担。.NET 的优势正好踩在这几个风险点上自包含部署解决环境不可控单 exe 双击运行解决运维薄弱轻量低内存解决老机器带不动异步 IO 和高性能解决小马拉大车。所以选 .NET不是因为我喜欢微软而是这个场景的数学期望算下来就是它。3.2 .NET 在物联网方向的真实生态积累有人对 .NET 的印象还停留在“.NET Framework Windows Only”这是一个很大的误区。.NET Core 出来这么多年跨平台能力早就不是问题了。我在树莓派上跑过采集程序在飞腾 ARM 国产化设备上跑过服务在麒麟系统上部署过边缘网关全部没有问题。工业物联网领域常用的几个库.NET 生态里都有成熟方案MQTT 通信MQTTnet可能是目前市面上性能最好的 MQTT 客户端/服务端库之一比 Java 的 Eclipse Paho 好用得多。Modbus 协议NModbus 和 ModbusTCP稳定维护多年工业界用得非常多。OPC UAOPCFoundation 官方有 .NET Standard 实现兼容性最好Java 那边反而经常要自己封装。PLC 通信HslCommunication工业通信库、S7netplus西门子 S7 协议都是国内工控圈大量使用的库基本开箱即用。时序数据InfluxDB .NET Client、TDengine 的 .NET 连接器都有官方维护。数据采集类项目里80% 以上的底层协议栈在 .NET 这边都有现成的、经过验证的库可以调。而且这些库是面向工业场景设计的稳定性经过了大量现场验证相比自己拿 Netty 去解析 Modbus 报文不知道稳到哪里去了。3.3 “现场友好型”体现在哪些细节这一小节说的都是我自己现场踩出来的一些小细节也是为什么我说 .NET 更“现场友好”。第一微软的官方运行时安装包对老系统兼容性极好。.NET 4.8如果你还在用 Framework可以在 Win7 SP1 上直接装而 .NET 6/8 的 Windows 桌面运行时也支持 Win10 LTSB 这类精简系统。Java 的 JDK 17 对老版 Windows 的兼容性则经常出问题网上搜“JDK 在 Win7 上安装失败”的一大堆。第二.NET 的配置文件是 JSON 和 XML不是 YAML。客户运维要改个 IP 地址、端口号用记事本打开 appsettings.json 就能改无需理解缩进语法。YAML 那种严格空格对齐的格式对非专业运维来说就是灾难。第三.NET 的日志默认就是纯文本文件不需要额外接 ELK。Java 项目标配 logback 控制台 日志文件看起来功能一样但要配置得好、轮转不出问题需要一定经验。.NET 的 ILogger 默认行为在中小项目里够用Serilog 接一行就能用日志文件按日期切割运维能直接看懂。第四也是我最看重的.NET 程序出问题之后错误信息里往往直接告诉你缺哪个库、哪个版本不匹配绝大多数情况下程序员能一眼定位。JVM 的堆栈和类加载错误尤其是遇到 jar 包冲突时排查起来是真的会怀疑人生。4. 选型决策框架什么时候选 .NET什么时候认怂选 Java4.1 一个简单的五维打分法虽然我在这篇文章里力挺 .NET但我并不建议你像追星一样无脑选 .NET。选型决策尤其是技术负责人拍板的技术栈必须有一套可以量化的评估方法。我自己平时用的是一套五维打分法每个维度按 1 到 5 分打分总分高者胜出。团队技能栈权重最高团队里谁会谁就能把项目风险降到最低。如果团队全是十年 Java 老兵硬切 .NET 的阵痛期可能让项目延期那 Java 反而更稳。部署环境约束客户现场有没有内网、能不能联网装依赖、硬件资源紧不紧张、有没有专职运维。资源紧、运维弱.NET 加分反之 Java 也能接受。性能与资源需求单机并发规模、内存预算、响应时间要求。边缘侧采集、实时控制类.NET / C / Rust 更合适大型分布式后端Java / Go 并没有劣势。生态与第三方库要接入的硬件设备协议、工业中间件是否已有成熟客户端。Modbus、OPC UA、西门子 PLC 通信.NET 生态都是老牌强项如果是金融支付、电商中台那套Java 生态更丰富。长期维护与招聘客户后期会不会自建团队维护市场上哪个方向的人好招。二三线城市 .NET 从业者其实不少一线互联网则 Java 更多。要结合项目所在地来判断。拿我手里的产线采集项目打分部署环境约束 5 分、性能资源 5 分、生态 5 分、团队 Java 略弱、长期维护 4 分。总分 .NET 明显胜出。反之如果是一个互联网公司的用户行为分析平台不用打分我就知道该选 Java 或者 Go。4.2 团队技能储备的权重必须调到最大我在评审很多物联网项目的时候发现一个普遍问题甲方招标文件写了“必须使用 Java”乙方为了中标硬着头皮用 Java 做结果现场跑不起来或者技术负责人是 Java 出身升职之后盲目把团队整体迁移到 Java结果交付一塌糊涂。我的观点很明确在物联网这个领域团队技能储备在选型决策里的权重应该占到一半以上。不是说 Java 不行而是你用不熟的那个技术栈一定会以 Bug 和延期的方式把成本还回来。如果你团队已经熟练掌握了 .NET就不要为了“Java 市场占有率更高”这种空泛的理由去切技术栈。.NET 的跨平台能力、生态完整度、性能表现在今天已经完全不虚 Java除非有非用 Java 不可的强绑定比如甲方硬性要求源码交付且指定语言否则团队会什么是第一优先级。4.3 客户现场环境的判断清单每次做物联网项目售前评估我都会让销售或者实施去现场拍一组照片、填一份环境清单。你可以把这张清单当成技术选型的“体检表”工控机的 CPU 型号和内存大小低于 4GB 内存直接排除 JVM 系。操作系统版本Win7 还是 Win10Linux 内核版本多少是不是国产化系统。现场是否允许安装运行时/驱动老军工企业往往不允许任何非授权软件安装。有没有专职 IT 运维没有选型要偏向自包含、双击运行。是否处于内网隔离环境不能联网拉 NuGet 包和 Maven 依赖部署包必须全内置。采集设备的通信协议Modbus、OPC UA、Profinet、自定义 TCP 协议提前确认有没有现成库。单设备点位规模和采集频率每秒采一次和每毫秒采一次技术难度差两个量级。断电重启恢复时间要求客户要求 5 分钟内恢复你的服务启动超 1 分钟就废了。这套清单跑完之后90% 的产线物联网项目会指向同一个结论边缘侧采集节点用 .NET 或者更底层的语言中心侧管理平台用什么都可以甚至可以同时混用。5. 实操用 .NET 搭建一个产线边缘采集网关的关键步骤5.1 硬件与系统环境准备我不是纯理论派下面给你一套我已经在项目里跑通的边缘采集网关搭建方案。硬件就用客户现场那种“被嫌弃”的配置赛扬 J1900、4GB 内存、Win10 或 Linux 都行。操作系统方面Windows 环境用 .NET 6 或 .NET 8 的 Windows 服务托管方式Linux 用 systemd 托管。开发机上需要安装 .NET SDK 8.0然后用以下命令确认版本dotnet --version # 输出类似 8.0.100 说明 SDK 环境正常项目结构我会这么建EdgeGateway/ ├── EdgeGateway.csproj ├── Program.cs # 程序入口负责宿主构建和配置 ├── appsettings.json # 运行配置连接信息、采集点位 ├── Workers/ │ ├── ModbusCollectWorker.cs # Modbus 数据采集后台任务 │ ├── PlcCollectWorker.cs # PLC 通信采集后台任务 │ └── DataPublishWorker.cs # 数据上报/转发后台任务 ├── Services/ │ ├── ModbusService.cs # Modbus TCP 通信封装 │ └── PlcService.cs # S7 协议通信封装 └── Models/ ├── DevicePoint.cs # 点位模型 └── DeviceData.cs # 采集数据模型这个结构是“优雅且不过度设计”的典型每个采集任务一个 Worker通过 Channel 解耦生产者和消费者数据流一目了然。新手也容易理解老手维护起来也不难受。5.2 核心代码骨架Worker 与 Channel 的组合Program.cs 里的宿主构建是 .NET 后台服务的标准姿势我把关键配置贴出来using EdgeGateway.Workers; var builder Host.CreateApplicationBuilder(args); builder.Services.AddHostedServiceModbusCollectWorker(); builder.Services.AddHostedServicePlcCollectWorker(); builder.Services.AddHostedServiceDataPublishWorker(); builder.Services.AddSingletonChannelDeviceData(_ Channel.CreateBoundedDeviceData(new BoundedChannelOptions(1024) { FullMode BoundedChannelFullMode.Wait })); var host builder.Build(); await host.RunAsync();这段代码核心就干了两件事注册后台任务注册一个容量为 1024 的有界 Channel 作为数据总线。有界 Channel 加 Wait 模式的意思是如果下游处理不过来上游采集不会被拉崩而是等待下游消费这是边缘节点做缓冲的常见做法。采集 Worker 的骨架大概是这样的public class ModbusCollectWorker : BackgroundService { private readonly ChannelDeviceData _channel; private readonly ILoggerModbusCollectWorker _logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var config _configuration.GetSection(Modbus).GetModbusConfig(); using var modbus new ModbusTcpClient(config.Ip, config.Port); while (!stoppingToken.IsCancellationRequested) { var stopwatch Stopwatch.StartNew(); foreach (var point in config.Points) { // 读取寄存器值映射到点位模型 var value await modbus.ReadHoldingRegistersAsync(point.Address, 1); var data new DeviceData { PointId point.Id, Value value, Timestamp DateTime.UtcNow }; await _channel.Writer.WriteAsync(data, stoppingToken); } stopwatch.Stop(); // 动态计算剩余等待时间保证采集周期稳定 var delay TimeSpan.FromMilliseconds(config.IntervalMs) - stopwatch.Elapsed; if (delay TimeSpan.Zero) await Task.Delay(delay, stoppingToken); } } }用 Stopwatch 计算实际采集耗时再用总周期减去耗时作为延迟时间这是保证采集周期不漂移的常用手段。整点采集的时候特别有用否则时间一长采集周期会逐渐偏移导致数据时间戳不齐。5.3 性能调优与内存控制采集服务最怕两件事内存增长失控和 GC 停顿导致采集抖动。针对这两点我有几个亲测有效的优化项。第一把 GC 模式调整为 Server GC 或 Workstation GC。如果采集点位少于 500 个推荐 Workstation GC延迟更低如果点位很多且需要吞吐Server GC 在高配机器上更好。生产环境的 csproj 里可以直接配置PropertyGroup ServerGarbageCollectiontrue/ServerGarbageCollection ConcurrentGarbageCollectiontrue/ConcurrentGarbageCollection /PropertyGroup第二避免在采集热路径里频繁分配大对象。比如读取 Modbus 寄存器时预先分配缓冲区重复使用而不是每次循环 new 一个 byte[]。我曾经看到一个同事的代码每采一个点位就 new 一个 byte[1024] 数组采集 3000 个点位GC 每秒钟要被触发好几次CPU 白白浪费了 20%。优化后的写法是复用缓冲区var buffer new byte[1024]; // 在循环外创建一次 // 循环内直接填充 buffer而不是重新分配第三序列化慎用反射。如果你的数据要转 JSON 上报在没有特殊要求的情况下用 System.Text.Json 就够了它是微软专门为高性能场景设计的比 Newtonsoft.Json 快不少还不用反射。但如果点位模型是动态的那 Newtonsoft 的 JObject 反而更方便这个根据场景取舍。我用 .NET 8 做的边缘网关在赛扬 J1900 上跑 1200 个点位、每秒采集一轮内存稳定在 170MB 左右CPU 峰值 12%。这个表现客户运维完全没话说了。5.4 部署为 Windows 服务与 Linux systemd程序开发完部署方式决定运维体验。Windows 环境最简单的方式是用微软官方推荐的 Windows 服务扩展。项目里安装 NuGet 包 Microsoft.Extensions.Hosting.WindowsServices然后在 Program.cs 加一行builder.Services.AddWindowsService(options { options.ServiceName EdgeGateway; });发布后以管理员身份运行sc create EdgeGateway binPath C:\EdgeGateway\EdgeGateway.exe start auto sc start EdgeGateway以后客户重启机器服务会自动启动不需要登录系统不需要手动双击也不用塞启动文件夹。这才是产线级该有的体验。Linux 环境发布成 systemd 服务更简单。先发布dotnet publish -c Release -r linux-x64 --self-contained true -o ./publish然后在 /etc/systemd/system/edgegateway.service 写服务配置[Unit] DescriptionEdge Gateway Service Afternetwork.target [Service] Typenotify WorkingDirectory/opt/edgegateway ExecStart/opt/edgegateway/EdgeGateway Restartalways RestartSec5 [Install] WantedBymulti-user.target注意 Typenotify 是因为 .NET 宿主默认使用 systemd 通知机制来标记服务就绪比 Typesimple 更可靠服务启动完成前不会被误判为“已经崩了”。然后执行sudo systemctl daemon-reload sudo systemctl enable --now edgegateway这几条命令下来客户现场的 Linux 设备也能做到开机自启、崩溃自动拉起、日志由 journald 统一接管。这个部署模型对一个边缘网关项目来说已经算得上“优雅”二字了。5.5 监控与日志的落地姿势采集服务 7×24 小时跑日志和监控必须从一开始就做好否则出了事连定位的依据都没有。日志我用 Serilog配置很简单写文件按日期滚动保留 30 天。关键一点日志级别要默认 Debug但生产环境用 Information避免日志量爆炸。尤其是 Modbus 读取失败这种高频错误必须做“指数退避重试”防止错误日志刷爆磁盘。监控方面如果项目规模不大不建议一开始上一套 Prometheus Grafana。轻量做法是程序内部统计每个采集任务最近 1 分钟的采集成功率、平均耗时、最大耗时每 10 秒写一次内存缓存通过一个 HTTP 接口 /health 对外暴露。客户运维只需要打开 http://工控机IP:5000/health 看 JSON 状态{ status: Healthy, uptime: 3d 04:22:11, lastCycleMs: 980, failedPoints: 0, memoryMb: 175 }这个健康检查接口还能给 PLC 或者组态软件做心跳检测一旦服务异常现场能立刻声光报警。真正做到了“不靠人去盯靠系统自己告警”这也让客户对系统的信任度上了一个台阶。6. 踩过的坑和排查经验6.1 第一个坑GC 延迟导致的采集抖动第一次把采集频率调到 200ms 一轮的时候我发现 Modbus 读取的响应时间偶尔会突然飙到 800ms 以上而且很有规律地每隔几秒出现一次。排查下来罪魁祸首是工作站 GC 在后台触发了 Gen2 回收导致线程停顿几十到几百毫秒。对于 200ms 的采集周期来说这个停顿直接造成了数据空洞。解决办法很简单在 csproj 里启用 Server GC并配合 ConcurrentGC让回收线程和业务线程尽量并发执行。改完之后那种周期性的尖刺就消失了。如果你遇到类似问题第一步先采集 GC 日志dotnet-counters 或者 application insights确认是不是 GC 停顿不要一上来就怀疑协议栈。6.2 第二个坑Docker 镜像在 ARM 设备上的兼容问题后来有一个项目要部署到 ARM 嵌入式设备上用 Docker 跑 .NET 服务。我一开始发布的是 linux-arm64 镜像结果在设备上报 Exec format error。排查到最后发现是构建机的问题构建机的 Docker 开了 buildx但默认的 QEMU 模拟器版本太老导致生成的 ARM 镜像在真机上无法执行。解决方案是升级构建机的 buildx 组件并且明确指定构建平台docker buildx build --platform linux/arm64 -t edgegateway:arm64 .另外一个 ARM 上的常见坑是如果你用了 Native AOT必须确认目标设备是 ARMv7 还是 ARMv8AOT 是“一次编译、只跑一类 CPU”的不像托管代码可以在同架构的机器上兼容。发布前一定先用uname -m看清楚架构。6.3 第三个坑客户杀毒软件误杀这是个很现实的问题。自包含单文件发布出来的 exe因为被打包了大量原生代码杀毒软件很容易误报为木马尤其是有位客户现场装了 360 和火绒双杀软。第一次部署的时候程序直接被隔离删除了服务起不来客户运维还以为中了病毒。做法是让客户在杀毒软件里加白名单。经验是把自包含单文件改回“框架依赖 文件夹发布”反而能降低误报率因为整体文件形态更接近普通程序。如果项目允许安装 .NET Runtime尽量用框架依赖别图省事用单文件。我后来在这个客户现场就换成了框架依赖发布一次就过了。6.4 常见问题速查表我把这几年在物联网项目里遇到的高频问题整理成一张速查表遇到问题先来查一查比搜索引擎快得多。现象可能原因排查/解决思路服务启动后一两秒自动退出依赖的服务没就绪如数据库、Kafka查看系统日志确认连接字符串、确认依赖服务状态内存不断增长几小时涨到 1GB热路径里频繁分配大对象/数组用 dotnet-dump 抓内存快照分析对象分布数据采集频率不准周期漂移未考虑单轮采集耗时直接 Task.Delay(间隔)用 Stopwatch 计算实际耗时动态计算剩余延迟Modbus 读取偶发超时工控机与 PLC 之间的网络闪断或设备响应慢增加超时重试重试次数不超过 3 次记录日志日志文件疯狂变大错误循环打印 没做日志级别控制错误率做指数退避日志级别调到 Information部署后端口被占用上一次残留进程未退出用 netstat -ano 查看端口占用kill 遗留进程服务在断电恢复后不自启Windows 服务没设为 Automatic或 systemd enable 没执行重新执行 sc config 或 systemctl enable程序在 Win7 上提示缺少 DLL使用了 .NET 8但 Win7 不支持换 .NET 6支持到 Win7 SP1或换 Win10 机器MQTT 连接不稳定、频繁掉线未设置心跳和重连策略启用 MQTT 协议的 KeepAlive实现断线自动重连这张表里的问题几乎每一个都是我实际踩过或者帮人排查过的没有一条是理论推演。做物联网项目就是这样80% 的时间不是在写新功能而是在跟现场的奇葩环境死磕。而 .NET 的优势恰恰在于出问题时你能更快定位、更快修复、用更少的时间和客户解释这就是“不被赶下台”的底气来源。最后再分享一个我做项目的习惯不管最终选了什么技术栈我都会在开工之前用客户现场的真实硬件把最小原型跑一遍验证内存、启动时间、稳定性这三个硬指标。技术选型的争论放一边实测数据才是最有力的说服工具。如果你也面临物联网项目的技术栈选择不妨先拿一台配置最差的工控机把两套方案都跑一跑答案自然会浮出水面。