4. 重试策略基础:固定间隔重试、指数退避、随机退避、线性退避
好,咱们进入重试策略的核心部分。
说实话,我在做OTA升级的早期,对重试策略的理解特别简单——失败了就等一会儿再试呗。结果呢?有一次量产设备大规模升级,服务器直接被冲垮了。嗯,那次教训挺深刻的。
重试策略,说白了就是决定「失败后等多久再试」的一套规则。别小看这个「等多久」,选错了,轻则升级慢,重则把服务器搞崩。
4.1 固定间隔重试
这是最朴素的做法。每次失败后,等待固定的时间,比如30秒,然后重试。
核心逻辑: 等待时间 = 常量
// 固定间隔重试示例
#define RETRY_INTERVAL_MS 30000 // 30秒
int ota_retry_fixed_interval(int max_retries) {
for (int i = 0; i < max_retries; i++) {
int result = ota_download_chunk();
if (result == SUCCESS) {
return SUCCESS;
}
printf("第%d次失败,等待%d秒后重试\n", i+1, RETRY_INTERVAL_MS/1000);
delay_ms(RETRY_INTERVAL_MS);
}
return FAILED;
}
优点: 实现简单,逻辑清晰。适合网络状况相对稳定的场景。
缺点: 太死板了。你想想看,如果网络正在拥堵,你每隔30秒捅一刀,服务器根本缓不过来。
我的经验: 固定间隔我只在内部局域网升级时用过。外网环境?我建议你慎重。我曾经在一个项目里用固定间隔重试,结果网络抖动时,所有设备同时重试,把服务器打出了502。
4.2 指数退避
这才是工业界的主流做法。每次重试的等待时间,按指数增长。
核心逻辑: 等待时间 = 基数 × 2^重试次数
// 指数退避重试示例
#define BASE_DELAY_MS 1000 // 基数1秒
int ota_retry_exponential_backoff(int max_retries) {
int delay = BASE_DELAY_MS;
for (int i = 0; i < max_retries; i++) {
int result = ota_download_chunk();
if (result == SUCCESS) {
return SUCCESS;
}
printf("第%d次失败,等待%dms\n", i+1, delay);
delay_ms(delay);
delay *= 2; // 每次翻倍
if (delay > MAX_DELAY_MS) {
delay = MAX_DELAY_MS; // 设置上限
}
}
return FAILED;
}
举个例子:基数是1秒,第一次失败等1秒,第二次等2秒,第三次等4秒,第四次等8秒……
为什么要这样?你想啊,网络出问题的时候,往往不是单个设备的问题。如果所有设备都按指数退避,越往后重试的设备越少,服务器就有喘息的机会。
注意: 一定要设置最大延迟上限。我见过有人没设上限,结果重试到第10次,等了512秒,用户早把设备砸了。一般建议上限设在30秒到120秒之间。
4.3 随机退避
指数退避虽然好,但有个问题——如果多个设备同时开始重试,它们的重试时间点可能还是撞在一起。
随机退避就是来解决这个问题的。在等待时间上加上一个随机因子。
核心逻辑: 等待时间 = 基础时间 + random(0, 抖动范围)
// 随机退避重试示例
#include <stdlib.h>
int ota_retry_random_backoff(int max_retries) {
srand(get_device_id()); // 用设备ID做种子,避免所有设备随机数一样
for (int i = 0; i < max_retries; i++) {
int result = ota_download_chunk();
if (result == SUCCESS) {
return SUCCESS;
}
int base_delay = 1000 * (1 << i); // 指数部分
int jitter = rand() % 1000; // 0~1秒的随机抖动
int delay = base_delay + jitter;
if (delay > MAX_DELAY_MS) {
delay = MAX_DELAY_MS;
}
printf("第%d次失败,等待%dms\n", i+1, delay);
delay_ms(delay);
}
return FAILED;
}
说白了,就是在指数退避的基础上,加了个「随机数」来打散重试时间点。
避坑指南: 我曾经犯过一个错——直接用rand()不加种子。结果所有设备复位后随机数序列一模一样,等于没加随机。后来我用设备MAC地址的后几位做种子,才真正把时间点打散。
4.4 线性退避
线性退避介于固定间隔和指数退避之间。每次重试,等待时间按固定步长增加。
核心逻辑: 等待时间 = 初始值 + 步长 × 重试次数
// 线性退避重试示例
#define INITIAL_DELAY_MS 5000 // 初始5秒
#define STEP_DELAY_MS 5000 // 每次增加5秒
int ota_retry_linear_backoff(int max_retries) {
int delay = INITIAL_DELAY_MS;
for (int i = 0; i < max_retries; i++) {
int result = ota_download_chunk();
if (result == SUCCESS) {
return SUCCESS;
}
printf("第%d次失败,等待%dms\n", i+1, delay);
delay_ms(delay);
delay += STEP_DELAY_MS;
if (delay > MAX_DELAY_MS) {
delay = MAX_DELAY_MS;
}
}
return FAILED;
}
线性退避的增长速度比指数退避慢,比固定间隔灵活。适合网络问题不会急剧恶化的场景。
4.5 四种策略对比
| 策略 | 等待时间公式 | 增长速度 | 适用场景 | 我个人的推荐度 |
|---|---|---|---|---|
| 固定间隔 | 常量 | 无增长 | 局域网、可控环境 | ★★☆☆☆ |
| 指数退避 | 基数 × 2^n | 快 | 外网、大规模并发 | ★★★★★ |
| 随机退避 | 基础时间 + 随机抖动 | 可控 | 多设备同时重试 | ★★★★☆ |
| 线性退避 | 初始值 + 步长 × n | 中等 | 网络问题温和的场景 | ★★★☆☆ |
4.6 实际项目中的选择建议
我个人习惯,在OTA升级中这样选:
- 首选指数退避 + 随机抖动。这是最稳妥的组合,能应对大部分网络异常。
- 固定间隔? 只在调试阶段用。上线前一定要换掉。
- 线性退避 适合那些「我知道问题大概多久能恢复」的场景。比如服务器定期维护,每次维护5分钟,那线性退避就很合适。
重要提醒: 不管用哪种策略,一定要设置最大重试次数和最大等待时间。我见过一个设备因为网络断了,按指数退避重试了整整三天,把电池耗光了。用户投诉说「你们的设备怎么一夜就没电了」——嗯,那场面挺尴尬的。
好了,这四种策略你心里有数了吧?下一节咱们聊聊更高级的话题——如何根据错误码动态调整重试策略。到时候我会分享一个我踩过的坑,跟HTTP 503状态码有关,挺有意思的。