【问题标题】:How do I properly resolve 1 million domain names to IP addresses using NodeJS?如何使用 NodeJS 将 100 万个域名正确解析为 IP 地址?
【发布时间】:2021-03-22 10:44:13
【问题描述】:

如何优化以下 NodeJS 代码以异步方式工作并快速运行?我想堆栈变得太大,导致程序变慢并最终崩溃。有没有更好的方法来解析下面的示例中的大量域名?

const dns = require('dns');
const os = require('os');

var logical_cores = os.cpus().length;
console.log(`Logical cores: ${logical_cores}`);
process.env.UV_THREADPOOL_SIZE=logical_cores;

domainToIp_resolve = (domain) => {
  return new Promise((resolve, reject) => {
    // does not use libuv thread pool; uses os async polling 
        dns.resolve(domain, 'A', (err, records) => {
      if (err) {
        reject(`${String(domain).toUpperCase()} | ERROR CODE: ${err.code} |  ERROR MESSAGE: ${err.message}`);
      } else {
        resolve(`${domain}: records: ${records}`);
      }
    });
  });
}

// simulating getting domain names from a list
function* getDomains(num) {
  let cnt = 0;

  for (let ctr = 1; ctr < num+1; ctr++) {

    // 97 = "a"; 123 = "z"
    for (let charCode = 97; charCode < 123; charCode++) {

      let prefix = '';
      for (let p = 0; p < ctr; p++) {
        prefix += `${String.fromCharCode(charCode)}`;
      }

      if (cnt === num) {
        return null;
      } else {
        cnt += 1;
        let domain = `${prefix}.com`;
        yield domain;
      }
    }
  }
}

function resolveDomain(num) {
  let doms = getDomains(num);

  function resolve(cnt, domain) {
    if (cnt < 1) return;
    if (cnt === num) return;

    domainToIp_resolve(domain)
      .then((data) => {
        console.log(data);
      })
      .catch((err) => {
        console.log(err);
      });

    setImmediate(resolve.bind(null, cnt+1, doms.next().value));
  }

  resolve(1, doms.next().value);
}

resolveDomain(1000000);

【问题讨论】:

  • 仅供参考,我认为在 nodejs 加载后设置 process.env.UV_THREADPOOL_SIZE=logical_cores 没有任何好处。我认为在启动时读取环境变量,然后构建线程池。您应该在启动 nodejs 程序之前设置该环境变量。
  • 您的主要问题是您尝试并行运行 1,000,000 次 DNS 查找(一次全部)。当然,您将耗尽内存和/或尝试执行此操作的其他一些系统资源。您一次只需要并行运行 N 个请求,其中 N 可能是一个很小的数字,您可以试验以获得最佳性能。我会尝试从你拥有的核心数量开始到 30 的值,看看性能最佳点在哪里。
  • 我建议你在this answer 中使用类似mapConcurrent() 的东西。

标签: node.js


【解决方案1】:

正如 cmets 中提到的,问题不在于您的程序速度不快。问题是它太快了:你试图并行打开 100 万个套接字。即使您的操作系统允许,您的路由器也不会喜欢它,并且根据下载种子的经验,如果它是消费级家用路由器,您的路由器可能会崩溃。

我们可以通过使用 async/await 来修复它。第一步 - 一次发出一个请求,就像在 PHP 或 Java 等其他语言中一样:

async function resolveDomain(num) { // mark as async so we can use await
  let doms = getDomains(num);

  for (domain of doms) {
    try {
      let data = await domainToIp_resolve(domain);
      console.log(data);
    }
    catch(err) {
      console.log(err);
    }
  }
}

这应该可以,但是会很慢。现在我们可以通过批处理请求来加快速度(我在这里并行处理 20 个,但您可以微调这个数字):

async function resolveDomain(num) { // mark as async so we can use await
  let doms = getDomains(num);
  let count = 0;
  const BATCH_SIZE = 20;

  while (count < num) {
    try {
      let promises = [];
      for (let i=0;i<BATCH_SIZE && count<num;i++,count++) {
        promises.push(domainToIp_resolve(doms.next().value));
      }

      let result = await Promise.all(promises);
      for (let i=0;i<result.length;i++) {
        console.log(result[i]);
      }
    }
    catch(err) {
      console.log(err);
    }
  }
}

使用 async/await 并不是绝对必要的。您可以使用回调重写上述逻辑,但它会变得复杂。对于这样的逻辑,使用 async/await 会更容易。

请注意,如果我们不关心按请求顺序记录结果,我们可以通过在整个批处理完成之前启动新请求来进一步优化代码。我把它作为家庭作业(提示:async-q 库有一个你可以使用的实现,它甚至可以按顺序返回结果)

【讨论】:

  • 你推荐一个while循环吗?我应该合并 setImmediate 以确保非阻塞事件循环吗?
  • await 关键字会将控制权交还给解释器,解释器执行您对setImmediate() 的预期。所以当使用 async/await 时不需要调用setImmediate()。在任何情况下,即使在您的原始代码中 dns.resolve() 也是异步的并且不会阻塞线程。因此,即使使用您的原始代码,也无需调用setImmediate。 DNS 解析在大多数操作系统中是同步的,但节点使用单独的线程进行 DNS 解析,以免阻塞主线程。这是节点在单独的线程中所做的少数事情之一
猜你喜欢
  • 2010-09-05
  • 2019-09-29
  • 1970-01-01
  • 2014-06-18
  • 2011-03-12
  • 2015-03-06
  • 2017-06-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多