当前位置: 首页 > news >正文

JVM 内存大对象监控和优化实践

作者:vivo 互联网服务器团队 - Liu Zhen、Ye Wenhao

服务器内存问题是影响应用程序性能和稳定性的重要因素之一,需要及时排查和优化。本文介绍了某核心服务内存问题排查与解决过程。首先在JVM与大对象优化上进行了有效的实践,其次在故障转移与大对象监控上提出了可靠的落地方案。最后,总结了内存优化需要考虑的其他问题。

一、问题描述

音乐业务中,core服务主要提供歌曲、歌手等元数据与用户资产查询。随着元数据与用户资产查询量的增长,一些JVM内存问题也逐渐显露,例如GC频繁、耗时长,在高峰期RPC调用超时等问题,导致业务核心功能受损。

图片

图1 业务异常数量变化

二、分析与解决

通过对日志,机器CPU、内存等监控数据分析发现:

YGC平均每分钟次数12次,峰值为24次,平均每次的耗时在327毫秒。FGC平均每10分钟0.08次,峰值1次,平均耗时30秒。可以看到GC问题较为突出。

在问题期间,机器的CPU并没有明显的变化,但是堆内存出现较大异常。图2,黄色圆圈处,内存使用急速上升,FGC变的频繁,释放的内存越来越少。

图片

图2 老年代内存使用异常

因此,我们认为业务功能异常是机器的内存问题导致的,需要对服务的内存做一次专项优化。

  • 步骤1 JVM优化

以下是默认的JVM参数:

-Xms4096M -Xmx4096M -Xmn1024M -XX:MetaspaceSize=256M -Djava.security.egd=file:/dev/./urandom -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/{runuser}/logs/other

如果不指定垃圾收集器,那么JDK 8默认采用的是Parallel Scavenge(新生代) +Parallel Old(老年代),这种组合在多核CPU上充分利用多线程并行的优势,提高垃圾回收的效率和吞吐量。但是,由于采用多线程并行方式,会造成一定的停顿时间,不适合对响应时间要求较高的应用程序。然而,core这类的服务特点是对象数量多,生命周期短。在系统特点上,吞吐量较低,要求时延低。因此,默认的JVM参数并不适合core服务。

根据业务的特点和多次对照实验,选择了如下参数进行JVM优化(4核8G的机器)。该参数将young区设为原来的1.5倍,减少了进入老年代的对象数量。将垃圾回收器换成ParNew+CMS,可以减少YGC的次数,降低停顿时间。此外还开启了CMSScavengeBeforeRemark,在CMS的重新标记阶段进行一次YGC,以减少重新标记的时间。

-Xms4096M -Xmx4096M -Xmn1536M -XX:MetaspaceSize=256M -XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark -Djava.security.egd=file:/dev/./urandom -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/{runuser}/logs/other

图片

图3 JVM优化前后的堆内存对比

优化后效果如图3,堆内存的使用明显降低,但是Dubbo超时仍然存在。

我们推断,在业务高峰期,该节点出现了大对象晋升到了老年代,导致内存使用迅速上升,并且大对象没有被及时回收。那如何找到这个大对象及其产生的原因呢?为了降低问题排查期间业务的损失,提出了临时的故障转移策略,尽量降低异常数量。

  • 步骤2 故障转移策略

在api服务调用core服务出现异常时,将出现异常的机器ip上报给监控平台。然后利用监控平台的统计与告警能力,配置相应的告警规则与回调函数。当异常触发告警,通过配置的回调函数将告警ip传递给api服务,此时api服务可以将core服务下的该ip对应的机器视为“故障”,进而通过自定义的故障转移策略(实现Dubbo的AbstractLoadBalance抽象类,并且配置在项目),自动将该ip从提供者集群中剔除,从而达到不去调用问题机器。图 4 是整个措施的流程。在该措施上线前,每当有机器内存告警时,将会人工重启该机器。

图片

图4 故障转移策略
  • 步骤3 大对象优化

大对象占用了较多的内存,导致内存空间无法被有效利用,甚至造成OOM(Out Of Memory)异常。在优化过程中,先是查看了异常期间的线程信息,然后对堆内存进行了分析,最终确定了大对象身份以及产生的接口。

(1) Dump Stack 查看线程

从监控平台上Dump Stack文件,发现一定数量的如下线程调用。

Thread 5612: (state = IN_JAVA)- org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.encodeResponse(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, org.apache.dubbo.remoting.exchange.Response) @bci=11, line=282 (Compiled frame; information may be imprecise)- org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.encode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, java.lang.Object) @bci=34, line=73 (Compiled frame)- org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec.encode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, java.lang.Object) @bci=7, line=40 (Compiled frame)- org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter$InternalEncoder.encode(io.netty.channel.ChannelHandlerContext, java.lang.Object, io.netty.buffer.ByteBuf) @bci=51, line=69 (Compiled frame)- io.netty.handler.codec.MessageToByteEncoder.write(io.netty.channel.ChannelHandlerContext, java.lang.Object, io.netty.channel.ChannelPromise) @bci=33, line=107 (Compiled frame)- io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(java.lang.Object, io.netty.channel.ChannelPromise) @bci=10, line=717 (Compiled frame)- io.netty.channel.AbstractChannelHandlerContext.invokeWrite(java.lang.Object, io.netty.channel.ChannelPromise) @bci=10, line=709 (Compiled frame)
...

state = IN_JAVA 表示Java虚拟机正在执行Java程序。从线程调用信息可以看到,Dubbo正在调用Netty,将输出写入到缓冲区。此时的响应可能是一个大对象,因而在对响应进行编码、写缓冲区时,需要耗费较长的时间,导致抓取到的此类线程较多。另外耗时长,也即是大对象存活时间长,导致full gc 释放的内存越来越小,空闲的堆内存变小,这又会加剧full gc 次数。

这一系列的连锁反应与图2相吻合,那么接下来的任务就是找到这个大对象。

(2)Dump Heap 查看内存

对core服务的堆内存进行了多次查看,其中比较有代表性的一次快照的大对象列表如下,

图片

图5 core服务的堆内存快照
整个Netty的taskQueue有258MB。并且从图中绿色方框处可以发现,单个的Response竟达到了9M,红色方框处,显示了调用方的服务名以及URI。

进一步排查,发现该接口会通过core服务查询大量信息,至此基本排查清楚了大对象的身份以及产生原因。

(3)优化结果

在对接口进行优化后,整个core服务也出现了非常明显的改进。YGC全天总次数降低了76.5%,高峰期累计耗时降低了75.5%。FGC三天才会发生一次,并且高峰期累计耗时降低了90.1%。

图片

图6 大对象优化后的core服务GC情况
尽管优化后,因内部异常导致获取核心业务失败的异常请求数显著减少,但是依然存在。为了找到最后这一点异常产生的原因,我们打算对core服务内存中的对象大小进行监控。

图片

图7 系统内部异常导致核心业务失败的异常请求数
  • 步骤4 无侵入式内存对象监控

Debug Dubbo 源码的过程中,发现在网络层,Dubbo通过encodeResponse方法对响应进行编码并写入缓冲区,通过checkPayload方法去检查响应的大小,当超过payload时,会抛出ExceedPayloadLimitException异常。在外层对异常进行了捕获,重置buffer位置,而且如果是ExceedPayloadLimitException异常,重新发送一个空响应,这里需要注意,空响应没有原始的响应结果信息,源码如下。

//org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#encodeResponse
protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {//...省略部分代码try {//1、检查响应大小是否超过 payload,如果超过,则抛出ExceedPayloadLimitException异常checkPayload(channel, len);} catch (Throwable t) {//2、重置bufferbuffer.writerIndex(savedWriteIndex);//3、捕获异常后,生成一个新的空响应Response r = new Response(res.getId(), res.getVersion());r.setStatus(Response.BAD_RESPONSE);//4、ExceedPayloadLimitException异常,将生成的空响应重新发送一遍if (t instanceof ExceedPayloadLimitException) {r.setErrorMessage(t.getMessage());channel.send(r);return;}}
}//org.apache.dubbo.remoting.transport.AbstractCodec#checkPayload
protected static void checkPayload(Channel channel, long size) throws IOException {int payload = getPayload(channel);boolean overPayload = isOverPayload(payload, size);if (overPayload) {ExceedPayloadLimitException e = new ExceedPayloadLimitException("Data length too large: " + size + ", max payload: " + payload + ", channel: " + channel);logger.error(e);throw e;}
}

受此启发,自定义了编解码类(实现org.apache.dubbo.remoting.Codec2接口,并且配置在项目),去监控超出阈值的对象,并打印请求的详细信息,方便排查问题。在具体实现中,如果特意去计算每个对象的大小,那么势必是对服务性能造成影响。经过分析,采取了和checkPayload一样的方式,根据编码前后buffer的writerIndex位置去判断有没有超过设定的阈值。代码如下。

/*** 自定义dubbo编码类**/
public class MusicDubboCountCodec implements Codec2 {/*** 异常响应池:缓存超过payload大小的responseId*/private static Cache<Long, String> EXCEED_PAYLOAD_LIMIT_CACHE = Caffeine.newBuilder()// 缓存总条数.maximumSize(100)// 过期时间.expireAfterWrite(300, TimeUnit.SECONDS)// 将value设置为软引用,在OOM前直接淘汰.softValues().build();@Overridepublic void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException {//1、记录数据编码前的buffer位置int writeBefore = null == buffer ? 0 : buffer.writerIndex();//2、调用原始的编码方法dubboCountCodec.encode(channel, buffer, message);//3、检查&记录超过payload的信息checkOverPayload(message);//4、计算对象长度int writeAfter = null == buffer ? 0 : buffer.writerIndex();    int length = writeAfter - writeBefore;//5、超过告警阈值,进行日志打印处理warningLengthTooLong(length, message);}//校验response是否超过payload,超过了,缓存idprivate void checkOverPayload(Object message){if(!(message instanceof Response)){return;}Response response = (Response) message;//3.1、新的发送过程:通过状态码BAD_RESPONSE与错误信息识别出空响应,并记录响应idif(Response.BAD_RESPONSE == response.getStatus() && StrUtil.contains(response.getErrorMessage(), OVER_PAYLOAD_ERROR_MESSAGE)){          EXCEED_PAYLOAD_LIMIT_CACHE.put(response.getId(), response.getErrorMessage());return;}//3.2、原先的发送过程:通过异常池识别出超过payload的响应,打印有用的信息if(Response.OK == response.getStatus() &&  EXCEED_PAYLOAD_LIMIT_CACHE.getIfPresent(response.getId()) != null){      String responseMessage = getResponseMessage(response);log.warn("dubbo序列化对象大小超过payload,errorMsg is {},response is {}", EXCEED_PAYLOAD_LIMIT_CACHE.getIfPresent(response.getId()),responseMessage);}}}

在上文中提到,当捕获到超过payload的异常时,会重新生成空响应,导致失去了原始的响应结果,此时再去打印日志,是无法获取到调用方法和入参的,但是encodeResponse方法步骤4中,重新发送这个Response,给了我们机会去获取到想要的信息,因为重新发送意味着会再去走一遍自定义的编码类。

假设有一个超出payload的请求,执行到自定编码类encode方法的步骤2(Dubbo源码中的编码方法),在这里会调用encodeResponse方法重置buffer,发送新的空响应。

(1)当这个新的空响应再次进入自定义encode方法,执行 checkOverPayload方法的步骤3.1时,就会记录异常响应的id到本地缓存。由于在encodeResponse中buffer被重置,无法计算对象的大小,所以步骤4、5不会起到实际作用,就此结束新的发送过程。

(2)原先的发送过程回到步骤2 继续执行,到了步骤3.2 时,发现本地缓存的异常池中有当前的响应id,这时就可以打印调用信息了。

综上,对于大小在告警阈值和payload之间的对象,由于响应信息成功写入了buffer,可以直接进行大小判断,并且打印响应中的关键信息;对于超过payload的对象,在重新发送中记录异常响应id到本地,在原始发送过程中访问异常id池识别是否是异常响应,进行关键信息打印。

在监控措施上线后,通过日志很快速的发现了一部分产生大对象的接口,当前也正在根据接口特点做针对性优化。

三、总结

在对服务JVM内存进行调优时,要充分利用日志、监控工具、堆栈信息等,分析与定位问题。尽量降低问题排查期间的业务损失,引入对象监控手段也不能影响现有业务。除此之外,还可以在定时任务、代码重构、缓存等方面进行优化。优化服务内存不仅仅是JVM调参,而是一个全方面的持续过程。

相关文章:

JVM 内存大对象监控和优化实践

作者&#xff1a;vivo 互联网服务器团队 - Liu Zhen、Ye Wenhao 服务器内存问题是影响应用程序性能和稳定性的重要因素之一&#xff0c;需要及时排查和优化。本文介绍了某核心服务内存问题排查与解决过程。首先在JVM与大对象优化上进行了有效的实践&#xff0c;其次在故障转移与…...

vue indexedDB 取指定数据库指定表 全部key用request.onsuccess

1 例子 export async function funcGetKey(dbName, tableName) {return new Promise((resolve, reject) > {// 打开指定的数据库const request indexedDB.open(dbName);request.onerror (event) > {console.error(打开数据库失败: , event.target.error);reject(event…...

Java 数据结构使用学习

Set和List的区别 Set 接口实例存储的是无序的&#xff0c;不重复的数据。List 接口实例存储的是有序的&#xff0c;可以重复的元素。 Set 检索效率低下&#xff0c;删除和插入效率高&#xff0c;插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>。 List 和数…...

monorepo更新组件报错,提示“无法加载文件 C:\Program Files\nodejs\pnpm.ps1,因为在此系统上禁止运行脚本”

解决方法&#xff1a; 第一步&#xff1a;管理员身份运行 window.powershell&#xff0c; win x打开powerShell命令框&#xff0c;进入到对应项目路径。 第二步&#xff1a;执行&#xff1a;get-ExecutionPolicy&#xff0c;显示Restricted&#xff0c;表示状态是禁止的; 第…...

vue中html引入使用<%= BASE_URL %>变量

首先使用src相对路径引入 注意&#xff1a; js 文件放在public文件下 不要放在assets静态资源文件下 否则 可能会报错 GET http://192.168.0.113:8080/src/assets/js/websockets.js net::ERR_ABORTED 500 (Internal Server Error) 正确使用如下&#xff1a;eg // html中引…...

Android全面屏下,默认不会全屏显示,屏幕底部会留黑问题

前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂&#xff0c;风趣幽默"&#xff0c;感觉非常有意思,忍不住分享一下给大家。 &#x1f449;点击跳转到教程 公司以前的老项目&#xff0c;便出现了这种情况&#xff0c;网上搜索了各种资料&#xf…...

5.Redis-string

string 字符串 字符串类型是 Redis 最基础的数据类型&#xff0c;关于字符串需要特别注意&#xff1a; 1.⾸先Redis中所有 key 的类型都是字符串类型&#xff0c;⽽且其他⼏种数据结构也都是在字符串类似基础上构建的&#xff0c;例如 list 和 set 的元素类型是字符串类型。 2…...

docker高级(redis集群三主三从)

1. 新建6个docker容器redis实例 docker run -d --name redis-node-1 --net host --privilegedtrue -v /redis/share/redis-node-1:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6381docker run -d --name redis-node-2 --net host --privilegedtrue -v /…...

linux 设置与命令基础(二)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、系统基本操作 二、命令类型 三、命令语法 四、命令补齐 五、命令帮助 六、系统基本操作命令 总结 前言 这是本人学习Linux的第二天&#xff0c;今天主…...

ubuntu20.04中ros2安装rosbridge及启动方式

ros2 启动rosbridge&#xff1a; 要启动ROS2中的rosbridge&#xff0c;需要先安装ROS2的rosbridge_suite软件包。使用以下命令安装&#xff1a; sudo apt-get update sudo apt-get install ros-<distro>-rosbridge-suite将<distro>替换为正在使用的ROS2发行版的名…...

TCP之超时重传、流量控制和拥塞控制

一、超时重传 TCP超时重传是TCP协议中的一种机制&#xff0c;用于在发生丢包或数据包未及时确认的情况下&#xff0c;重新发送未确认的数据段。 当发送方发送一个数据段后&#xff0c;会启动一个定时器&#xff08;称为超时计时器&#xff09;&#xff0c;等待接收方的确认。…...

git clone 报SSL证书问题

git命令下运行 git config --global http.sslVerify false 然后再进行重新clone代码...

Spring Boot 排除配置类的引用的方法

Spring Boot 提供的自动配置非常强大&#xff0c;某些情况下&#xff0c;自动配置的功能可能不符合我们的需求&#xff0c;需要我们自定义配置&#xff0c;这个时候就需要排除/禁用 Spring Boot 某些类的自动化配置了。 比如&#xff1a;数据源、邮件&#xff0c;这些都是提供…...

代码随想录打卡—day46—【DP】— 8.29 背包END

1 139. 单词拆分 139. 单词拆分 做了很久...估计2h 一开始我的思路卡死了 看题解之后的思路的详解见注释&#xff0c; 我的写法和carl 答案在一些微小的细节上略有不同&#xff0c;我的更好理解&#xff0c;但他的解法更简单。 我写的过程中&#xff0c;需要注意下标和字符…...

lua学习-3 循环和流程控制

这里写目录标题 判断for 循环数值遍历泛型遍历遍历数组遍历对象ipairs 和 pairs的异同 while 循环repeat循环goto基础用法注意事项 判断 for 循环 数值遍历 for exp1,exp2,exp3 do//todoend上述代码是指&#xff1a;从exp1 到exp2 以exp3为步长进行循环并执行todo代码&#…...

3、监测数据采集物联网应用开发步骤(3)

监测数据采集物联网应用开发步骤(2) 系统整体结构搭建 新建项目 输入项目名称&#xff1a;MonitorData 所谓兵马未动粮草先行&#xff0c;按下图创建好对应的模块备用&#xff1a; com.plugins 业务插件模块 com.zxy.adminlog 日志或文本文…...

MySQL用户管理及用户权限

目录 数据库用户管理 新建用户 查看用户 重命名用户rename 删除用户drop 修改用户密码 找回root密码 数据库用户授权 授予权限 查看用户权限 撤销用户权限 数据库用户管理 新建用户 CREATE USER 用户名来源地址 [IDENTIFIED BY [PASSWORD] 密码];用户名&#xff1a…...

Yolov8-pose关键点检测:模型轻量化创新 | PConv结合c2f | CVPR2023 FasterNet

💡💡💡本文解决什么问题:新的partial convolution(PConv),通过同时减少冗余计算和内存访问可以更有效地提取空间特征。 PConv| GFLOPs从9.6降低至8.5,参数量从6482kb降低至6134kb, mAP50从0.921提升至0.925 Yolov8-Pose关键点检测专栏介绍:https://blog.csdn.n…...

聊聊mybatis-plus的SafetyEncryptProcessor

序 本文主要研究一下mybatis-plus的SafetyEncryptProcessor SafetyEncryptProcessor mybatis-plus-boot-starter/src/main/java/com/baomidou/mybatisplus/autoconfigure/SafetyEncryptProcessor.java public class SafetyEncryptProcessor implements EnvironmentPostProc…...

【PCL (Point Cloud Library)可视化点云的工具汇总】

PCL (Point Cloud Library)可视化点云的工具 PCL (Point Cloud Library) 提供了一系列的工具和类用于点云的可视化。以下是其中的一些主要工具和功能: pcl::visualization::CloudViewer: 如前所述,这是一个简单易用的可视化工具,主要用于基本的点云显示。pcl::visualizatio…...

openclaw-route-check:多协议路由诊断工具的原理、安装与实战应用

1. 项目概述与核心价值最近在折腾一些需要跨地域、跨网络环境访问的服务时&#xff0c;路由问题总是最让人头疼的环节。你可能也遇到过类似情况&#xff1a;明明服务部署在A地&#xff0c;从B地访问时延迟高得离谱&#xff0c;或者干脆时通时不通&#xff0c;排查起来像大海捞针…...

别再手动对比了!用Beyond Compare 4在Ubuntu上5分钟搞定文件同步与合并

高效文件管理利器&#xff1a;Beyond Compare 4在Ubuntu中的深度应用指南 在当今快节奏的开发与运维工作中&#xff0c;文件比较与同步已成为日常工作中不可或缺的环节。无论是代码合并、配置同步还是日志分析&#xff0c;传统的手动对比方式不仅效率低下&#xff0c;还容易出错…...

Figma布局守护者:自动化检查与规范维护插件开发指南

1. 项目概述&#xff1a;Figma布局守护者 如果你是一名UI/UX设计师&#xff0c;或者是一名前端开发者&#xff0c;那么你一定对Figma不陌生。这个基于Web的协作设计工具&#xff0c;凭借其强大的实时协作能力和开放的插件生态&#xff0c;几乎成为了现代产品设计流程中的标准配…...

Adafruit M4SK开发板外设接口实战:从I2C到PDM麦克风的嵌入式交互设计

1. 项目概述与核心价值如果你正在寻找一款既能玩转嵌入式图形界面&#xff0c;又能轻松连接各种传感器、执行器&#xff0c;并且自带丰富交互外设的开发板&#xff0c;Adafruit M4SK绝对是一个会让你眼前一亮的选项。它不像传统的单片机开发板那样“光秃秃”&#xff0c;而是将…...

别再用免费版硬扛交付!Pro计划中被低估的“商用素材合规审计工具”如何帮你规避97%版权风险?

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;商用素材合规审计工具的底层逻辑与风险图谱 商用素材合规审计工具并非简单的关键词扫描器&#xff0c;而是融合数字水印识别、元数据溯源、许可证语义解析与跨平台版权数据库比对的复合型决策引擎。其底…...

基于MCP协议构建个人AI助手:本地化读取Mac消息数据库实践

1. 项目概述&#xff1a;一个让AI助手“读懂”你Mac消息的桥梁如果你和我一样&#xff0c;是个重度依赖Mac原生“信息”应用&#xff08;也就是iMessage&#xff09;来沟通的人&#xff0c;同时又希望自己的AI助手&#xff08;比如Claude、Cursor里的AI&#xff09;能更深入地了…...

高性能计算能效优化:从异构架构到混合精度实践

1. 高性能计算能效优化的核心挑战在过去的十年里&#xff0c;高性能计算&#xff08;HPC&#xff09;系统的能耗问题已经从单纯的运营成本问题演变为制约科学发现速度的关键瓶颈。以欧洲核子研究中心&#xff08;CERN&#xff09;的大型强子对撞机&#xff08;LHC&#xff09;为…...

Arm ADI调试接口架构与实战解析

1. Arm Debug Interface (ADI) 架构解析 在嵌入式系统开发领域&#xff0c;调试接口是连接开发环境与目标硬件的关键纽带。作为行业标准制定者&#xff0c;Arm推出的Debug Interface (ADI) 提供了一套完整的芯片级调试解决方案。我曾在多个基于Cortex-M/A系列处理器的项目中深度…...

pico示波器采集软件SSL1000A在功率器件测试的应用

在新能源汽车电控体系里&#xff0c;IGBT、MOSFET 是电机控制器、OBC、DC-DC 等核心模块的 “功率开关”&#xff0c;它们的开关特性、瞬态响应、稳定可靠性直接影响整车效率与安全。功率器件测试看似简单&#xff0c;实则细节要求极高&#xff0c;因为器件在高频开关中产生的尖…...

工业主板选型与集成实战:从核心设计到故障排查

1. 项目概述&#xff1a;从一块主板看工业智能化的基石最近在整理一个老旧产线的智能化改造方案&#xff0c;客户指着产线控制柜里那台屏幕已经发黄、反应迟缓的工控机问我&#xff1a;“这东西还能用吗&#xff1f;换新的要多少钱&#xff1f;”我拆开一看&#xff0c;里面的主…...