节点的深度:JS:探索I/Assoncion
#node #linux #async #libuv

介绍

我最近研究了node.js.

的执行

我最终从有关como o Event Loop funciona的文章中进行了很多学习(和写作),©twitter上的线程解释了quem espera a requisição http terminar

如果您愿意,也可以通过单击aqui

来访问我在编写此帖子之前创建的心理图。

现在,让我们去开展业务!

节点如何治疗牛

无节点:

  • 所有JavaScript均在主线程上执行。
  • 图书馆 libuv >
  • 按模式,libuv提供4个 worker threads node.js
    • 仅在突击操作阻止时才使用这些线程,在这种情况下,将libuv线程之一(是操作系统线程)从主线程(节点执行)中锁定。
  • 有阻塞和阻止操作,当前大多数突击行动都在阻止。

Operaind Assancronas:您£o?

通常,在异步操作方面存在混乱。

许多人认为这意味着背景中,同时或其他线程发生了某些事情。

实际上,突击操作是现在不会返回的操作,而是。

它们取决于与外部代理的通信,这些代理可能不会立即对其请求做出响应。

我们正在谈论I/O操作(输入/输出)。

示例:

  • 文件读数:磁盘的数据数据在应用程序中的 enter
  • 文件中写的:data explying 输入磁盘。
  • Operações de Rede
    • http申请,例如。
    • 应用程序将 a 申请http 发送到某些服务器,接收数据。

Node chama libuv, libuv chama syscalls, event loop roda na thread principal

阻止可分配的操作没有阻止

在现代世界中,人们不会说话大多数被掩盖的操作都不会阻止。

但是,这意味着:

  • libuv提供4个线程( by pattest )。
  • 他们“照顾” I/O操作阻止
  • 绝大多数行动。

似乎有点不眠吗?

Libuv worker threads ops assíncronas bloqueantes

考虑到这个质疑,我决定进行一些实验。

comfunã§封锁器

首先,我测试了CPU的强烈CPU使用,CPU是罕见函数之一,阻止了node的分配

使用的方式如下:

// index.js
// index.js
import { pbkdf2 } from "crypto";

const TEN_MILLIONS = 1e7;

// Função assíncrona de uso intenso de CPU
// Objetivo: Bloquear uma worker thread
// Objetivo original: Gerar uma palavra-chave
// O terceiro parâmetro é o número de iterações
// Nesse exemplo, estamos passando 10 milhões
function runSlowCryptoFunction(callback) {
  pbkdf2("secret", "salt", TEN_MILLIONS, 64, "sha512", callback);
}

// Aqui queremos saber quantas workers threads a libuv vai usar
console.log(`Thread pool size is ${process.env.UV_THREADPOOL_SIZE}`);

const runAsyncBlockingOperations = () => {
  const startDate = new Date();
  const runAsyncBlockingOperation = (runIndex) => {
    runSlowCryptoFunction(() => {
      const ms = new Date() - startDate;
      console.log(`Finished run ${runIndex} in ${ms/1000}s`);
    });
  }
  runAsyncBlockingOperation(1);
  runAsyncBlockingOperation(2);
};

runAsyncBlockingOperations();

要验证操作,我包围了命令:

UV_THREADPOOL_SIZE=1 node index.js

重要:

  • uv_threadpool_size:â确定libuv节点的 worker线程的差异。

结果是:

Thread pool size is 1
Finished run 1 in 3.063s
Finished run 2 in 6.094s

也就是说,使用1stnica螺纹,每个执行时间耗时约6秒,并且它们顺序发生。一个ap。

现在,我决定进行以下测试:

UV_THREADPOOL_SIZE=2 node index.js

结果如下:

Thread pool size is 2
Finished run 2 in 3.225s
Finished run 1 in 3.243s

这样,证明了libuv的 worker线程,在Node.js中处理阻止Asastial操作。

但是阻止 呢?如果没有人在等他们,它们如何工作?

我决定编写另一个功能进行测试。

具有非块功能的实验

futch futch (节点本地)执行网络Ashnial操作,它是一个块。

以以下方式,我数据金进行了第一个实验的测试:

//non-blocking.js
// Aqui queremos saber quantas workers threads a libuv vai usar
console.log(`Thread pool size is ${process.env.UV_THREADPOOL_SIZE}`);

const startDate = new Date();
fetch("https://www.google.com").then(() => {
  const ms = new Date() - startDate;
  console.log(`Fetch 1 retornou em ${ms / 1000}s`);
});

fetch("https://www.google.com").then(() => {
  const ms = new Date() - startDate;
  console.log(`Fetch 2 retornou em ${ms / 1000}s`);
});

并使用以下命令执行脚本:

UV_THREADPOOL_SIZE=1 node non-blocking.js

结果如下:

Thread pool size is 1
Fetch 1 retornou em 0.391s
Fetch 2 retornou em 0.396s

入口,我决定使用两个线程进行测试,以查看是否发生了变化:

UV_THREADPOOL_SIZE=2 node non-blocking.js

和入口:

Thread pool size is 2
Fetch 2 retornou em 0.402s
Fetch 1 retornou em 0.407s

这样,我可以看到:

在libuv中运行更多线程无助于执行非块分配操作。

但是,我再次想知道,如果没有Libuv线程“等待”返回的请求,它是如何工作的?

我的朋友,这是我在一项巨大的研究和有关操作的知识中

的知识。

攻击行动不阻止

多年来,操作系统已经发展了很多,以便能够以非封锁方式处理I/O操作,这是通过 syscalls ,sa o es进行的:

  • 选择/民意调查:这些是处理I/O封锁的传统方式,通常被认为效率较低。
  • iocp :在Windows中用于Asanchronous操作。 Kqueue:Macos和BSD的母亲
  • epoll :有效且用于Linux。相反,他不受FD的数量的限制。
  • io_uring :epoll进化,带来了绩效的改进和基于队列的方法。

要更好地理解,我们需要深入研究I/O不封锁的操作细节。

ENTENDONO文件描述符

为了解释I/O不封锁,我需要快速解释文件描述符(SDS)的概念。

什么是FD?

â内核维护的许多表,其中每个记录都有:

  • 功能类型(例如文件,套接字,设备)。
  • 文件指针的当前位置。
  • 权限和标志,定义诸如阅读或写作之类的模式。
  • coferentiaã资源数据结构中的内核。

它们对于I/O管理是基础的。

FD和I/O不封锁

通过启动I/O操作不阻止,Linux将FD链接到它而不会中断(阻止)过程的执行。

例如:

让我们想象您要读取包含一个非常大的文件的内容。

阻止方法

  • process调用函数读取文件
  • 流程正在等待阅读文件的控制
    • 只要结束结束,该过程就会被阻止

方法不阻止

  • 流程请求阅读 assoncrona
  • 因此,您开始阅读遏制并将FD返回到该过程中。
  • 过程没有锁定,可以做其他事情。
  • 时不时地调用 syscall 知道阅读是否结束。

定义阅读方式的是过程,该过程是通过fcntl函数使用flag o_nonblock ,但是目前是次要的。

用Syscall监视FD

为了有效地观察母亲FD,SOS具有一些 syscalls

理解选择:

  • 收到FDS列表。
  • 阻止了一个或多个FD为指定操作准备的过程(阅读,书面,例外)。
  • SYSCALL返回后,该程序可以在SD上的项目中识别为I/O。
  • 准备的程序。
  • 使用(n)的搜索算法。
    • 效率低下,缓慢,疲倦了许多FD

epoll

这是 select < /em>的演变,它使用自平衡裁决来存储FD,使访问时间实际上是constance,(1)。 /p>

太别致!

如何工作:

  • 是通过epoll_create创建的。
  • 将FDS与epoll_ctl相关联。
  • 使用epoll_wait等待任何FDS。
  • 它具有超时表。
    • Libuv的事件环!

Comparação de tempo entre select e epoll

io_uring

这个家伙来这里踢帐篷棒。

尽管 epoll 已经发展(很多!)搜索性能和seizure fds ,io_uring逐渐重新考虑了我操作的所有性质/o。

所以,在理解它的工作原理之后,我想知道没有人在以前想知道它!

回顾:

  • 选择:接收一个FDS列表,顺序存储(作为数组),然后检查1到1(复杂性O(n))以查看谁有变化或活动。li> li>
  • epoll :接收一个FDS列表,使用自脚的self stolach储存它们,看不到1到1,更有效,并且与选择相同,只是具有复杂性o(1)

从历史上看,该过程负责迭代返回的FD,以找出谁结束了。

  • io_uring :怎么样?返回列表?进行投票?愚蠢?您听说过队列

它使用两条主要线路起作用,以矮的形式©is(戒指,名称io- ring )。

  • 1提交任务
  • 1 tarefas完成

simples否?

启动I/O操作时的过程, lump 使用 ion_urn_uring

的操作

aã,在呼叫 select epoll 的发明中,并且在返回的fds中,它是在每个人上的零件中,可以选择通知该过程当某些I/O操作结束时。

包括£o

这样,现在我确切地知道节点通过哪种方式执行突击操作。

正在阻止:

  • 使用libuv
  • 执行ASANCRONE操作
  • 添加到libuv 工作线程
  • a 工作线程被阻止,等待完成操作。
  • 最后,该线程负责将结果放在事件循环
  • 在主线程上执行回调

没有阻止:

  • 使用libuv
  • 执行ASANCRONE操作
  • libuv执行I/O的I/O的A syscall 不阻止
  • do 用FDS解析(epoll)
  • 投票
  • 来自版本20.3.0使用io_uring
    • 顺从行/完成操作的方法
  • 收到完成的操作事件后
    • Libuv负责执行或回调NA线程Main