正确的重试逻辑:为可靠系统实施指数退避

在软件开发中,可靠的重试逻辑对处理间歇性故障(如网络问题或临时中断)至关重要。最近,我发现一个代码库中的开发人员使用了一个带有固定时间间隔的for循环来重试失败的操作。虽然这种方法看似简单直接,但缺乏真实应用所需的弹性。这就是指数退避的作用——一种旨在使重试更智能和高效的策略。

在本文中,我们将探讨指数退避的工作原理、其相对于基本重试循环的优势,以及如何实施它以增强系统的可靠性。我还将通过一个使用电子邮件发送模块的实际示例向您展示如何使用指数退避来确保更具弹性的错误处理。

指数退避是一种重试策略,其中在每次失败后重试尝试之间的等待时间呈指数增长。每次重试不是以固定时间间隔重试,而是下一次尝试比上一次等待的时间更长—通常是每次延迟加倍。例如,如果初始延迟为1秒,则下一次重试将在2、4、8秒等时刻发生。这种方法有助于减少系统负担,并在高需求时期最大限度地减少对外部服务的压力。

通过在重试之间提供更多时间,指数退避为临时问题提供解决的机会,从而实现更高效的错误处理和改善应用程序稳定性。

  • 减少系统负载: 通过间隔重试,指数退避最小化了对服务器造成过载的可能性,特别适用于处理速率限制或临时故障。

  • 高效的错误处理: 增加的延迟让临时问题有更多时间自然解决,提高了成功重试的可能性。

  • 改善的稳定性: 特别是对于高流量系统,它防止了一波重试请求,保持应用程序平稳运行而不消耗过多资源。

  • 增加的延迟: 每次重试所需时间逐渐增加,指数退避可能导致延迟,尤其是在成功之前需要多次重试的情况下。

指数退避在系统与外部服务交互或管理大量流量的场景中尤其有用。以下是一些其他常见的使用案例:

  1. 限速API: 一些API有速率限制,限制在特定时间内的请求。指数退避有助于避免立即重试导致超过限制,给予限制恢复的时间。

  2. 网络不稳定: 在临时网络故障或超时的情况下,指数退避通过在尝试之间等待更长时间来帮助网络稳定。

  3. 数据库连接: 在重负载下连接数据库时,指数退避有助于通过延迟重试来防止进一步过载,给予数据库恢复的时间。

  4. 队列系统: 在消息队列系统中,如果由于错误导致消息失败,使用指数退避进行重试可以防止快速重新处理,并允许临时问题得到解决。

为了演示指数退避,我们将构建一个基本的电子邮件发送器,如果发生错误,则重试发送电子邮件。此示例展示了与简单的 for 循环相比,指数退避如何改进重试过程。

import nodemailer from "nodemailer";
import { config } from "../common/config";
import SMTPTransport from "nodemailer/lib/smtp-transport";

const emailSender = async (
  subject: string,
  recipient: string,
  body: string
): Promise<boolean> => {
  const transport = nodemailer.createTransport({
    host: config.EMAIL_HOST,
    port: config.EMAIL_PORT,
    secure: true,
    auth: { user: config.EMAIL_SENDER, pass: config.EMAIL_PASSWORD },
  } as SMTPTransport.Options);

  const mailOptions: any = {
    from: config.EMAIL_SENDER,
    to: recipient,
    subject: subject,
  };

  const maxRetries = 5; // maximum number of retries before giving up
  let retryCount = 0;
  let delay = 1000; // initial delay of 1 second

  while (retryCount < maxRetries) {
    try {
      // send email
      await transport.sendMail(mailOptions);
      return true;
    } catch (error) {
      // Exponential backoff strategy
      retryCount++;
      if (retryCount < maxRetries) {
        const jitter = Math.random() * 1000; // random jitter(in seconds) to prevent thundering herd problem
        const delayMultiplier = 2
        const backOffDelay = delay * delayMultiplier ** retryCount + jitter;
        await new Promise((resolve) => setTimeout(resolve, backOffDelay));
      } else {
        // Log error
        console.log(error)
        return false; // maximum number of retries reached
      }
    }
  }
  return false;
};

实施指数退避涉及调整某些参数,以确保重试策略对应用程序的需求起作用良好。以下关键参数影响重试机制中指数退避的行为和性能:

  1. 初始延迟
  • 目的: 设置第一次重试前的等待时间。它应该足够长,以防止立即重试,但又足够短,以避免明显的延迟。

  • 推荐设置:500 毫秒1000 毫秒 的延迟开始。对于关键系统,使用较短的延迟,而较不紧急的操作可以具有较长的延迟。

  1. 延迟倍增器
  • 目的: 控制每次重试后延迟增加的速度。倍增器为2时,延迟翻倍(例如,1秒,2秒,4秒)。

  • 推荐设置: 通常,倍增器在1.52之间可以平衡响应性和稳定性。如果系统能够承受更长的重试延迟,则可以考虑更高的倍增器(例如,3)。

  1. 最大重试次数
  • 目的: 限制重试次数,以防止过度重试导致资源耗尽或系统负载增加。

  • 推荐设置: 通常,3到5次重试 对于大多数应用程序来说是足够的。超出此范围,操作可能需要记录为失败或以其他方式管理,例如通知用户或触发警报。

  1. 抖动(随机化)
  • 目的:为每个延迟添加随机性,以防止重试聚集并引发雷鸣般的效应。

  • 推荐设置:在每次重试间隔中添加0到500毫秒的随机延迟。这种抖动有助于更均匀地分布重试尝试的时间。

通过使用指数退避,您可以为应用程序增加弹性,使其能够处理意外问题。这是一个小改变,但影响却很大,尤其是在应用程序不断增长时。

目前就介绍到这里。请随时留言,如果有任何问题,请提出。祝愿打造更可靠和弹性的应用程序。

编程愉快!👨‍💻❤️

Source:
https://timothy.hashnode.dev/implementing-exponential-backoff-for-reliable-systems