Skip to content

面试题:如何在最短时间内找出 RTT 最低的 IP 地址

核心问题

从一个包含多个 IP 地址的列表中,以最快的方式找出发起请求到收到响应时间(Round Trip Time, RTT)最短的那个 IP 地址。

约束条件: 存在一个最大并发请求数(例如:10),意味着不能同时向所有 IP 地址发送请求。


解法演进与分析

方案一:串行请求 (最笨的办法)

最简单直接的方法,逐个处理 IP 地址,完全不考虑并发。

  • 思路:
    1. 从列表中取出第一个 IP。
    2. 发送请求,等待响应返回。
    3. 记录其 RTT。
    4. 对列表中的下一个 IP 重复以上步骤,直到所有 IP 都测试完毕。
    5. 比较所有记录的 RTT,找出最小值。
  • 缺点:
    • 效率极低,总耗时约等于所有 IP 的 RTT 之和。
    • 完全没有利用“最大并发数”这一关键条件。

方案二:分批并行 (简单的并发)

利用最大并发数,将 IP 列表分批处理。

  • 思路:
    1. 将 IP 列表按最大并发数(假设为 N)进行分组。
    2. 取第一批 N 个 IP,同时发起请求。
    3. 等待这一批次的所有请求都完成
    4. 记录下这一批次中的最小 RTT。
    5. 取下一批 N 个 IP,重复以上步骤。
    6. 最后,比较每一批次找出的最小 RTT,得到全局最小值。
  • 缺点:
    • 虽然利用了并发,但效率依然不高。
    • 在每一批次中,必须等待该批次最慢的那个请求完成后才能开始下一批,造成了不必要的等待。例如,批次内 RTT 分别为 3s, 4s, 5s,你必须等待 5s 才能开始下一轮。

方案三:批内竞速 & 提前结束 (重要优化)

在方案二的基础上,优化每个批次内部的等待策略,当批次内的“冠军”产生后,立刻结束本批次。

  • 思路:
    1. 取第一批 N 个 IP,同时发起请求。
    2. 在这一批次中,一旦有任何一个请求率先完成(例如 3s 的那个请求返回了),就立即认为它是本批次的优胜者。
    3. 立即取消该批次内其他所有尚未完成的请求(例如 4s5s 的请求)。
    4. 记录下优胜者的 RTT,然后马上开始下一批次。
  • 优点:
    • 显著减少了每个批次内部的等待时间,不再需要等待最慢的请求。总时间从 sum(max(T_batch_i)) 优化为 sum(min(T_batch_i))
  • 缺点:
    • 这仍然不是全局最优解。整个过程的总时间依然是所有批次“局部最优时间”的总和,并发资源在批次切换的间隙存在浪费。

方案四:全局竞速 & 动态超时 (最优解)

这是对整个过程的终极优化,引入一个全局最短时间的“擂主”,后续所有请求都与这个“擂主”进行比较,从而将无效等待时间压缩到极限。

  • 思路:
    1. 维护一个全局最短 RTT 变量 global_min_rtt,初始值为无穷大。
    2. 启动第一批 N 个并发请求。
    3. 当这批请求中第一个响应返回时(假设其 RTT 为 T1 = 3s),它就是当前的“擂主”。立即更新 global_min_rtt = 3s
    4. 不取消第一批中其他请求,而是让它们继续。同时,从 IP 列表中取出新的 IP 立即发起请求,填补刚刚空出的并发位置,始终让并发数保持饱和。
    5. 对于后续任何一个新返回的请求,都和当前的 global_min_rtt 比较:
      • 如果一个新请求返回,其 RTT 为 T_new
      • T_new < global_min_rtt,则更新 global_min_rtt = T_new,诞生了新“擂主”。
    6. 核心优化点:引入动态超时。当下一次发起新请求时,我们可以设置一个超时时间,这个超时时间就是当前的 global_min_rtt
      • 如果一个后续批次的请求(例如 RTTs 为 6s, 7s, 9s)在 3s 内(即当前 global_min_rtt)都没有任何一个返回,则说明这一整批请求的 RTT 都大于 3s
      • 因此,我们无需等待它们真实返回,可以在 3s 这个时间点直接判定它们全部失败,并取消这些请求,继续测试后面的 IP。
  • 优点:
    • 时间利用率最大化:并发数始终保持饱和,一个请求完成,下一个马上补上,没有空闲时段。
    • 无效等待最小化:通过动态超时,任何不可能成为最优解的“慢”请求都会被尽早放弃,避免了垃圾时间的等待。这是理论上能达到的最快速度。

代码实现思路 (JavaScript)

以最优解(方案四)为例,可以使用 Promise.raceAbortController 来高效实现。

javascript
/**
 * 模拟一个带取消功能的网络请求
 * @param {string} ip - 目标 IP
 * @param {number} rtt - 模拟的 RTT (ms)
 * @param {AbortSignal} signal - 用于中止请求的信号
 * @returns {Promise<{ip: string, rtt: number}>}
 */
function mockFetch(ip, rtt, signal) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      resolve({ ip, rtt });
    }, rtt);

    signal.addEventListener('abort', () => {
      clearTimeout(timer);
      reject(new DOMException('Aborted', 'AbortError'));
    }, { once: true });
  });
}

/**
 * 在 IP 列表中找出 RTT 最低的 IP
 * @param {string[]} ips - IP 地址数组
 * @param {number} maxConcurrency - 最大并发数
 */
async function findFastestIP(ips, maxConcurrency) {
  const ipPool = [...ips];
  let globalMinRtt = Infinity;
  let fastestIp = null;
  let activeRequests = 0;

  console.log(`开始查找... 总共 ${ips.length} 个 IP, 最大并发: ${maxConcurrency}`);

  // 使用一个 Promise 来等待所有任务完成
  await new Promise(resolve => {
    function runNext() {
      // 如果 IP 池已空且没有活跃请求,则任务完成
      if (ipPool.length === 0 && activeRequests === 0) {
        resolve();
        return;
      }

      // 循环直到达到并发上限或 IP 池耗尽
      while (activeRequests < maxConcurrency && ipPool.length > 0) {
        const ip = ipPool.shift();
        const rtt = Math.floor(Math.random() * 2000) + 50; // 模拟 50-2050ms 的 RTT
        activeRequests++;

        const controller = new AbortController();

        // 设定动态超时
        // 如果已经有了一个最快记录,就用它作为超时时间
        const timeout = globalMinRtt !== Infinity ? globalMinRtt : rtt + 100; // 初始时给一个宽裕的时间

        const timeoutId = setTimeout(() => {
          console.log(`[超时] 请求 ${ip} 未能在 ${timeout}ms 内响应,提前中止。`);
          controller.abort();
        }, timeout);
        
        console.log(`[发起] 请求 ${ip} (模拟 RTT: ${rtt}ms), 当前最快: ${globalMinRtt}ms`);

        mockFetch(ip, rtt, controller.signal)
          .then(result => {
            console.log(`[成功] ${result.ip} 响应, RTT: ${result.rtt}ms`);
            // 只有当返回的 rtt 小于当前全局最小时,才更新
            if (result.rtt < globalMinRtt) {
              globalMinRtt = result.rtt;
              fastestIp = result.ip;
              console.log(`%c[更新] 新冠军! IP: ${fastestIp}, RTT: ${globalMinRtt}ms`, 'color: green; font-weight: bold;');
            }
          })
          .catch(error => {
            if (error.name !== 'AbortError') {
              console.error(`请求 ${ip} 发生错误:`, error);
            }
          })
          .finally(() => {
            clearTimeout(timeoutId); // 清除超时定时器
            activeRequests--;
            // 立即尝试发起下一个请求
            runNext();
          });
      }
    }
    
    // 初始启动
    runNext();
  });

  console.log(`\n🎉 [查找结束] 最快的 IP 是 ${fastestIp},RTT 为 ${globalMinRtt}ms。`);
  return { ip: fastestIp, rtt: globalMinRtt };
}

// 示例:
// const ips = Array.from({ length: 100 }, (_, i) => `192.168.1.${i + 1}`);
// findFastestIP(ips, 10);

延伸与探讨

这道题是优秀的场景题,能引申出更多深入的技术问题:

  1. 如何取消一个网络请求?

    • 浏览器 fetch API: 使用 AbortController。创建一个 controller 实例,将其 signal 传入 fetch 的选项中,在需要时调用 controller.abort()。这是现代 Web 开发的标准做法。
    • XMLHttpRequest: 使用 xhr.abort() 方法。
    • Node.js (axios): 支持 AbortController 或其自己的取消令牌(Cancel Token)机制。
  2. 取消请求的底层原理是什么?

    • 调用 abort() 本质上是通知浏览器或运行时环境放弃对该 HTTP 事务的处理。
    • 在操作系统层面,这通常会导致关闭底层的 TCP Socket 连接,从而停止发送和接收数据,并释放相关的系统资源。这可以有效节省不必要的网络流量和 CPU/内存消耗。
  3. 考察的核心知识点:

    • 异步编程模型(Promise, async/await
    • 并发与节流控制
    • 算法优化思想(从暴力求解到动态规划/贪心)
    • 网络基础(RTT, HTTP 请求生命周期)
    • 具体 API 的熟练使用 (AbortController)