4、重试策略入门:固定间隔重试、指数退避重试、随机退避重试
说到Modbus通讯,重试策略是个绕不开的话题。
我刚开始做工业通讯那会儿,觉得重试嘛,就是失败了再发一次呗。后来被现实狠狠教育了一顿——不合理的重试策略,轻则把从机搞死机,重则把整个总线拖垮。今天咱们就来聊聊三种最基础的重试策略。
4.1 为什么需要重试策略?
先说说背景。Modbus通讯不是100%可靠的,尤其是走RS485这种物理层。干扰、从机繁忙、线路老化,随便哪个原因都能让一帧报文石沉大海。
我遇到过最典型的情况:一个车间里几十台变频器,同时响应主站查询。偶尔有一两台没反应,你重试一次就好了。但如果你不加控制地疯狂重试,那台从机可能正在处理紧急任务,被你连续打断,直接罢工。
所以,重试策略要解决三个问题:
- 什么时候重试?——间隔多久
- 重试多少次?——次数上限
- 重试节奏怎么变?——固定还是变化
核心原则:重试是为了提高成功率,不是给系统添乱。好的重试策略,应该像老司机开车——该快则快,该慢则慢。
4.2 固定间隔重试
这是最朴素的做法。每次重试之间,等待相同的时间。
比如第一次失败,等200ms再试;第二次失败,还是等200ms。简单粗暴,容易实现。
import time
def read_holding_registers_fixed(client, address, count, retries=3, interval=0.2):
for attempt in range(retries):
result = client.read_holding_registers(address, count)
if result.isError():
print(f"第{attempt+1}次失败,{interval}秒后重试...")
time.sleep(interval)
else:
return result
raise Exception("重试耗尽,通讯失败")
这个策略的优点很明显:实现简单,逻辑清晰。你一眼就能看出重试间隔是多少。
但缺点也很致命——如果从机正在忙,你固定间隔去骚扰它,它永远没机会喘口气。我在一个项目里见过,主站每50ms重试一次,从机是台老式PLC,处理一次请求要100ms。结果就是主站疯狂重试,从机永远处理不完,死循环了。
适用场景:从机响应时间稳定、网络状况良好的环境。比如同一个机柜内的Modbus通讯。
4.3 指数退避重试
这个策略聪明多了。每次重试的间隔,按指数增长。
第一次失败等200ms,第二次等400ms,第三次等800ms,第四次等1.6秒……你想想看,这就像两个人吵架,第一次你大声点,第二次更大声,第三次对方还不理你,你就该冷静一下了。
import time
def read_holding_registers_exponential(client, address, count, retries=5, base_interval=0.2):
for attempt in range(retries):
result = client.read_holding_registers(address, count)
if result.isError():
interval = base_interval * (2 ** attempt)
print(f"第{attempt+1}次失败,等待{interval:.2f}秒...")
time.sleep(interval)
else:
return result
raise Exception("指数退避重试耗尽")
我个人习惯把基础间隔设在100ms到500ms之间。为什么?因为Modbus的典型响应时间在10ms到100ms之间,基础间隔太短等于没退避,太长又影响效率。
我曾经在一个分布式IO项目中用过指数退避。现场有30多个从站,偶尔会有几个同时掉线。如果用固定间隔,主站CPU占用率直接飙到80%。换成指数退避后,CPU占用率降到15%,而且从站恢复后能很快被重新发现。
注意:指数增长要设上限。我见过有人设了10次重试,最后一次等了51.2秒。整个系统卡了一分钟。建议最大间隔不超过5秒。
4.4 随机退避重试
指数退避有个问题——如果多个主站同时重试,它们的节奏会撞在一起。就像一群人同时抢着说话,越抢越乱。
随机退避就是解决这个问题的。在指数退避的基础上,加上一个随机抖动。
import time
import random
def read_holding_registers_jitter(client, address, count, retries=5, base_interval=0.2):
for attempt in range(retries):
result = client.read_holding_registers(address, count)
if result.isError():
# 基础间隔 * 2^attempt + 随机0~100ms
interval = base_interval * (2 ** attempt) + random.uniform(0, 0.1)
print(f"第{attempt+1}次失败,等待{interval:.3f}秒...")
time.sleep(interval)
else:
return result
raise Exception("随机退避重试耗尽")
说白了,就是在指数退避的间隔上,加一个随机数。这个随机数通常取0到基础间隔之间的值。
我做过一个测试:两个主站同时查询同一个从站,用指数退避时,碰撞率大概在15%左右。加上随机抖动后,碰撞率降到了2%以下。效果立竿见影。
最佳实践:随机退避 = 指数退避 + 随机抖动。这是工业通讯中公认的黄金组合。
4.5 三种策略对比
| 策略 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 固定间隔 | 实现简单,行为可预测 | 容易造成总线拥堵 | 单主站、稳定环境 |
| 指数退避 | 减少从机压力,自适应 | 多主站时可能碰撞 | 从机负载波动大 |
| 随机退避 | 避免碰撞,鲁棒性最强 | 实现稍复杂 | 多主站、高干扰环境 |
4.6 我的建议
如果你刚开始做Modbus开发,先用固定间隔。等系统跑起来后,观察日志,看看重试频率。如果发现重试次数太多,再换成指数退避。
至于随机退避,我建议在以下情况使用:
- 总线上有多个主站
- 无线通讯环境(干扰大)
- 从站数量超过20个
嗯,这里要注意一点:不管用哪种策略,重试次数一定要设上限。我一般设3到5次。超过这个次数,说明问题不是重试能解决的,该报警就报警。
避坑指南:我曾经在一个项目里把重试次数设成了10次,结果从机掉线后,主站花了整整8秒才放弃。操作员以为系统死机了。后来改成3次重试+2秒超时,用户体验好多了。
好了,三种基础重试策略就讲到这里。下一章咱们聊聊更高级的——自适应重试策略,根据历史成功率动态调整间隔。那个更有意思。