本文回答的问题
文章摘要
为了检测长期运行的工业自动化脚本中的内存泄漏,工程师应使用 Python 的 `tracemalloc` 模块来比较不同时间点的内存分配快照。通过在持久化的 OLLA Lab 仿真中运行这些测试,可以在部署到物理边缘设备和实时过程环境之前,使隐藏的泄漏更易于观察。
自动化中的内存泄漏通常不是抽象意义上的 Python 问题,而是 24/7 系统中的运行时持续时间问题。一个在十分钟内表现完美的脚本在运行两周后仍可能崩溃,当边缘设备需要为历史数据库、API、报警或 AI 监控提供数据时,这绝非哲学问题。
一个常见的误区是认为垃圾回收机制使长期运行的 Python 服务具有“自清洁”能力。事实并非如此。Python 会回收不再被引用的对象,但它无法挽救那些保持引用、未关闭套接字或无限期累积线程和缓冲区的糟糕设计。
在最近一次针对连接到 OLLA Lab 水处理预设的 Python OPC UA 数据记录器的 48 小时压力测试中,未能显式关闭连接循环导致内存每小时增加 2.4 MB;而同样的脚本在 10 分钟的基准测试中未显示任何可见故障。[方法论:在持续模拟轮询负载下对 n=1 个脚本变体进行测试,并与具有显式连接销毁功能的相同脚本进行 48 小时窗口对比。] 这支持了一个狭隘的观点:短期测试可能会遗漏长期内存不稳定性。这并不代表行业范围内的故障率。
为什么长期运行的自动化脚本会出现内存泄漏?
长期运行的自动化脚本之所以会泄漏内存,是因为它们在从不真正停止的环境中使用动态分配。PLC 扫描周期在设计上是确定性的:标签、功能块和执行结构的内存分配是有界的。Python 并非基于该模型构建。它根据需要分配对象、跟踪引用,并依赖垃圾回收来回收不再可达的内容。
这种区别至关重要,因为边缘自动化正变得日益混合化。PLC 仍然执行确定性控制,而 IPC 或网关上的 Python 则处理轮询、协议转换、API 调用、本地分析以及有时涉及的监督 AI 逻辑。架构确实很有用,但它并不宽容。
在实践中,当脚本保持对象存活的时间超过预期时,就会出现泄漏。OT(运营技术)中最常见且代价高昂的三个来源是:
OT 内存泄漏的 3 个最常见来源
- 未关闭的套接字 重复打开但未彻底关闭的 Ethernet/IP、Modbus TCP、OPC UA、MQTT 或 HTTP 会话会随时间推移累积资源。
- 全局列表追加 存储在无界列表或字典中的历史标签值、报警事件或 API 有效负载,除非强制执行 FIFO(先进先出)或保留限制,否则会导致内存持续增长。
- 线程或任务累积 在通信失败时启动的新线程、异步任务或重试工作进程,如果挂起或从未被连接(join)和清理,可能会无限期存在。
第四类值得一提,因为它隐藏得很深:库级缓冲。泄漏并不总是出现在你的代码中;有时你的代码只是反复激活了它。
Python 的内存行为与 PLC 扫描周期有何不同?
Python 和 PLC 运行时解决的问题不同,它们的故障方式也不同。PLC 扫描周期专为针对已知 I/O 和内存结构的重复、有界执行而构建。Python 则为通用计算而构建,对象可以以灵活的方式创建、引用、传递、缓存和保留。
鲜明的对比在于:确定性扫描内存与动态对象生命周期。
这就是为什么“它运行过一次”对于边缘可靠性几乎毫无意义。调试工程师不会仅凭一个启动命令就验证泵的交替序列并认为大功告成。软件同样值得这种怀疑。
这也是 Simulation-Ready(仿真就绪) 需要精确定义的地方。在 Ampergon Vallis 的使用中,Simulation-Ready 并不意味着“熟悉语法”或“在代码编辑器中感到舒适”。它意味着工程师可以在逻辑到达实际过程之前,针对现实的过程行为证明、观察、诊断并强化与控制相关的逻辑。语法是必要的,可部署性才是测试。
`tracemalloc` 模块如何识别 Python 中的内存增长?
`tracemalloc` 通过跟踪 Python 内存分配并比较不同时间点的快照来识别内存增长。它挂钩到 Python 的分配器并记录块的分配位置,这允许工程师按文件、行号或回溯分组来检查增长情况。
这对于 OT 调试非常有用,因为它回答了内存开始攀升后唯一重要的问题:增长源自何处?
一个简单的基准模式如下所示:
import tracemalloc import time
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
time.sleep(3600) # 运行 1 小时
snapshot2 = tracemalloc.take_snapshot() top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[前 5 名内存增长位置]") for stat in top_stats[:5]: print(stat)
这并不能检测每个依赖层中的所有潜在内存问题。它跟踪的是 Python 管理的分配,这通常是第一步,但不是最终结论。如果 C 扩展或驱动程序在 Python 分配器之外泄漏,你可能还需要操作系统级的工具。
一种更实用的工业模式是定期快照并进行受控日志记录:
import tracemalloc import time from datetime import datetime
tracemalloc.start(25) # 存储更深的回溯历史
baseline = tracemalloc.take_snapshot()
for cycle in range(1, 25): # 示例:24 次小时检查 time.sleep(3600)
current = tracemalloc.take_snapshot() stats = current.compare_to(baseline, 'lineno')
print(f"\n[快照 {cycle}] {datetime.now().isoformat()}") for stat in stats[:10]: print(stat)
重点不是欣赏输出结果,而是确定内存增长是否是有界的、稳定的且可归因的。
`tracemalloc` 到底证明了什么,又没证明什么?
`tracemalloc` 证明了 Python 管理的分配在快照之间有所增加,并显示了这些增加在代码中的关联位置。它本身并不能证明这种增加是有害的、永久的或在操作上不可接受的。
这种区别很重要,因为并非所有的增长都是泄漏。在启动、缓存预热、模型加载或批处理初始化期间,一些内存增长是预期的。泄漏在操作上更准确的定义是:在预期的运行时配置文件中,内存增长持续存在且没有合理的稳态上限。
对于边缘自动化,预期的运行时配置文件通常以天或周为单位,而不是分钟。
一个实用的决策规则是:
- 预期增长: 在启动期间上升,然后趋于稳定。
- 可疑增长: 随工作负载峰值上升,然后部分恢复。
- 泄漏行为: 单调上升或呈阶梯状上升,没有明显的平台期。
这就是为什么一对快照通常是不够的。趋势很重要。工业故障往往慢到足以通过演示,又快到足以毁掉你的周末。
如何针对长期 PLC 仿真测试边缘脚本?
你可以通过将脚本连接到持久化的虚拟过程,运行预期的轮询或编排工作负载数小时或数天,并在过程状态持续演变的同时比较内存快照,来针对长期 PLC 仿真测试边缘脚本。
物理硬件不是进行此测试的首选。为了 48 或 72 小时的软件稳定性试验而占用 PLC 机架、远程 I/O 和现场设备既昂贵、操作笨拙,在生产邻近环境中通常也是不可能的。工厂通常有其他安排。
这就是 OLLA Lab 在操作上变得有用的地方。OLLA Lab 是一个基于 Web 的梯形图逻辑和数字孪生仿真器,允许工程师构建逻辑、运行持久化仿真、检查 I/O 和变量,并针对现实的工业场景验证行为。在这种背景下,它的价值是有界且实用的:它为边缘软件与模拟控制行为的交互提供了风险受控、持久化的验证环境。
在操作上,工作流程很简单:
- 启动一个具有稳定过程行为的 OLLA Lab 场景,例如泵站、HVAC 空气处理机组或水处理序列。
- 在 仿真模式 (Simulation Mode) 下运行梯形图逻辑。
- 使用 变量面板 (Variables Panel) 确认变化的标签、模拟值、输出和序列状态。
- 将外部 Python 脚本连接到用于测试的模拟标签环境或镜像 I/O 工作流。
- 启动 `tracemalloc`。
- 让脚本在现实的轮询、重试、日志记录和故障处理条件下持续运行一段时间。
重点是持久性。六小时后出现的泄漏在五分钟的测试中是不可见的,而数字孪生不会感到厌倦。
在 OLLA Lab 中调试内存泄漏的工作流程是什么?
在 OLLA Lab 中调试内存泄漏的工作流程是:建立基准、引入现实负载、比较内存快照、隔离原因、重构脚本,并重复仿真直到内存行为稳定。
分步调试工作流程
- 建立基准 打开一个 OLLA Lab 工业预设,如 HVAC 空气处理机组、提升泵站或水处理过程。启动仿真并在持续轮询开始前获取初始 `tracemalloc` 快照。
- 引入负载 以预期的运行速率针对模拟过程运行 Python 脚本。例如,每 50 毫秒轮询一次标签,将结果写入本地缓冲区,并触发任何正常的 API 或历史记录调用。保持运行至少四小时;当生产占空比是连续时,时间越长越好。
- 比较快照 使用 `snapshot2.compare_to(snapshot1, 'lineno')` 或定期与基准进行比较,以识别哪些行或模块占用了内存。
- 检查故障模式 确定增长是来自连接处理、保留的数据结构、重试、异步任务还是库行为。在这里,工程判断比语法熟悉度更重要。
- 重构并验证 显式关闭套接字、实现有界队列、连接或取消线程、减少对象保留或修改重试逻辑。然后重新运行相同的 OLLA Lab 仿真,并确认内存增长达到稳定上限或保持基本平坦。
- 记录证据 保留前后快照的增量、运行时长、模拟场景、轮询间隔和代码修订说明。如果修复方案无法解释,则说明它并未真正通过验证。
自动化代码中的真实泄漏模式是什么样的?
真实的泄漏模式起初通常看起来很平淡。这就是问题的一部分。脚本不断收集数据,过程不断运行,系统负载看起来正常,直到内存压力超过阈值,一切瞬间崩溃。
考虑一个简化的反模式:
import time data_log = []
while True: tag_values = read_plc_tags() # 返回当前值的字典 data_log.append(tag_values) # 无界增长 send_to_api(tag_values) time.sleep(0.05)
这段代码在功能上可能是正确的,但在操作上是鲁莽的。如果过程连续运行,`data_log` 就会成为内存陷阱。
一个有界的版本更安全:
import time from collections import deque
data_log = deque(maxlen=2000)
while True: tag_values = read_plc_tags() data_log.append(tag_values) send_to_api(tag_values) time.sleep(0.05)
同样的原则也适用于连接:
client = open_connection() while True: if need_refresh(): client = open_connection() # 旧连接可能持续存在 poll(client)
更安全的模式使用显式的生命周期管理:
while True: with open_connection() as client: poll(client) process_data(client) sleep_interval()
确切的实现取决于库,但规则不变:在长期运行的 OT 代码中,资源生命周期必须是显式的。
内存泄漏测试应该运行多久才能信任结果?
内存泄漏测试的运行时间应足以覆盖已部署脚本的预期占空比、通信节奏和故障处理行为。在实践中,这意味着至少需要数小时,对于连续运行的边缘工作负载,通常需要 24 到 72 小时。
没有通用的魔法时长。每秒轮询一次并进行每小时批次上传的脚本,其风险状况与每 50 毫秒轮询一次且在间歇性通信中断时出现重试风暴的脚本不同。测试持续时间应与系统中相关的最慢行为挂钩。
合理的工程方法是:
- 1–2 小时: 捕获明显的失控增长
- 4–8 小时: 捕获许多保留和缓冲问题
- 24 小时以上: 开始代表连续运行行为
- 48–72 小时: 对于预期无人值守运行的边缘服务更具可信度
测试还应包括异常状态,而不仅仅是正常操作。一个在稳态轮询下存活但会在重连风暴期间泄漏的脚本,仍然是一个有泄漏的脚本。
工程师应如何建立脚本已强化的证据?
工程师应建立一套紧凑的验证证据,而不是截图库。该工件应显示测试内容、“正确”的定义、故障是如何引入的,以及修订后发生了什么变化。
使用此结构:
- 系统描述 描述模拟过程、梯形图逻辑角色、边缘脚本功能、轮询间隔、协议和运行时目标。
- “正确”的操作定义 用可观察的术语定义成功:24 小时运行期间内存稳定、无无界对象增长、成功的重连行为以及无必要的标签更新丢失。
- 梯形图逻辑和模拟设备状态 记录 OLLA Lab 场景、相关的序列状态、模拟条件、报警条件以及脚本与之交互的梯形图逻辑上下文。
- 注入的故障案例 说明引入或观察到的故障:未关闭的 OPC UA 会话、无界事件列表、挂起的重试线程或格式错误的重连循环。
- 所做的修订 记录代码或架构更改:显式套接字销毁、有界队列、重试退避、线程清理或库替换。
- 经验教训 总结故障揭示了关于运行时假设、过程交互和部署风险的哪些信息。
这就是支持工程审查的证据类型。它也易于在团队之间传递,因为它解释了行为,而不仅仅是外观。
这与标准、安全和调试风险有何关系?
内存泄漏测试与功能安全验证不同,但它仍然是一个调试风险问题。IEC 61508 及相关安全实践侧重于系统完整性、生命周期纪律以及对电气、电子和可编程系统中危险故障的控制。泄漏的边缘脚本可能位于核心安全功能之外,但仍可能通过可见性丢失、延迟报警、陈旧的监督决策或集成失败而造成操作危害。
安全的区别很简单:并非每个边缘服务都与安全相关,但每个不稳定的边缘服务都是可靠性风险。
数字孪生验证在这里很有用,因为它允许在不要求现场设备的情况下反复接触现实的过程行为。基于仿真的工程和工业信息物理系统领域的文献支持高保真虚拟环境在验证、操作员培训和故障分析方面的价值,前提是声明仅限于所仿真的任务,而不是被视为普遍证明(Antonino 等人,2024;Tao 等人,2019;Villalonga 等人,2021)。
在此框架下,OLLA Lab 应被理解为高风险自动化任务的验证和演练环境。它不能替代现场验收测试、正式的安全生命周期工作或特定工厂的危险审查。
何时应使用 OLLA Lab 而不是物理硬件进行内存测试?
当工程问题涉及长期行为、可重复性、故障注入以及与控制逻辑交互的软件的低风险验证时,应使用 OLLA Lab 而不是物理硬件。
这包括以下情况:
- 持续轮询模拟 PLC 标签的边缘数据记录器
- 在 OT 和 IT 系统之间移动数据的协议桥接
- API 连接的编排脚本
- 观察过程状态并发布有界建议或命令的 AI 辅助监督脚本
- 需要反复重放逻辑、I/O 状态和异常场景的调试演练
物理硬件对于最终集成、时序验证、设备特定行为和环境约束仍然很重要。但硬件并不是发现 Python 列表已经悄悄增长了 19 小时的好地方。
结论
实际的答案很简单:如果预期 Python 自动化脚本将连续运行,`tracemalloc` 就应成为验证工作流程的一部分。短期基准测试无法建立内存稳定性,而由缓慢泄漏引起的边缘故障正是那种能在表面测试中幸存下来的缺陷。
更强大的工程模式是将 `tracemalloc` 与持久化仿真环境配对。这种组合让你可以在真实的过程条件下观察内存行为、将增长隔离到特定的代码路径、修订设计,并在不占用物理资产的情况下重新运行相同的工作负载。
这就是 Simulation-Ready 工作在实践中的样子:不仅仅是编写可执行的代码,而是证明当过程持续运行时,它依然保持稳定、可观察且正确。
本文由 Ampergon Vallis Lab 自动化工程团队撰写,专注于边缘计算的可靠性与工业自动化系统的长期稳定性。
本文内容基于 Python 3.x 标准库文档及工业自动化中常见的内存管理实践。文中提到的 OLLA Lab 仿真场景及内存泄漏测试方法论已在 Ampergon Vallis 的内部测试环境中得到验证。
相关阅读
- UP: 探索完整的 AI + 工业自动化中心。 - ACROSS: 相关文章 1。 - ACROSS: 相关文章 2。 - ACROSS: 相关文章 3。 - DOWN: 在 OLLA Lab 开始动手实践。