使用异步 i/o 编写数据库

Writing a database with async i/o

我最近遇到了 libuv,让 nodejs 发挥其异步魔力的低级库。这让我开始思考。我想按照这些思路进行澄清:

  1. Nodejs 有异步 i/o 调用。但是,如果我将 API 调用到(远程)数据库,则实际 read/write 进入数据库将是同步的,但节点不必等待它。有可能使数据库本身以异步方式写入磁盘吗?是否有任何数据库使用 libuv 进行实际异步 i/o?

  2. Javascript 是著名的单线程。我知道 nodejs 运行时不需要 - 如果我有 4 cpu 个核心,我可以启动 4 个实例。但是,如果我使用 libuv 以支持线程的语言编写另一个 Web 框架,它是否具有异步 i/o 和多线程的所有优点?类似的东西已经存在了吗?

你混淆了两个概念。事实上,您在对服务进行查询时可以异步等待(通过 epoll/kpoll/libuv...),但这并不意味着您的查询在另一端是非阻塞的,反之亦然。这也不意味着,在你的事件循环中,事情 "feel" 异步,它们确实是。

让我们回到什么是事件循环以及 nodeJS 如何发挥它的魔力。我觉得这是一个很好的故事开始。

事件循环的可见部分是代码编写方式的变化——从大部分同步到大部分异步。不可见的部分是,此异步代码尽可能多地抛出在事件循环中,事件循环在后台检查要执行的操作 - IO、计时器等。这不是一个新想法,它完成了它的工作(提供并发)真的很好。

libuv 的文档实际上对此有很好的描述。 Over there 是对他们所做的设计选择的描述,从那里得出了这个流程图:

请注意,他们没有在任何地方声明他们已经使任何东西真正异步 - 因为他们没有。底层系统调用保持同步。它只是 感觉 好像不是。这是关键要点。

关于数据库上的磁盘I/O,我前一阵子在海牙就此发表了演讲,坦率地说,大多数关键的I/O 都是阻塞的。例如,你不能去 "Hey, I'll update the disk snapshot and append-only txlog at the same time!" - 因为,如果其中一个失败,你就会遇到一个非常严重的回滚问题,并且可能是未知状态。

关于问题2,我会给出代码示例,但我不确定您熟悉哪些语言。底线是,当某些东西越过线程边界时,事情就变得很糟糕。一个非常天真的例子是这样的——假设你的事件循环有两个定时器如下:

  • 定时器 1,每 0.5 秒触发一次,递增给定的状态变量 A
  • 定时器 2,每当有人提供用户输入时触发,将状态变量除以 2。

假设您运行正在单线程上运行。尽管你的事件循环感觉是异步的,但它是完全顺序的——定时器 1 将 never 运行 而定时器 2 是 运行ning.

现在添加第二个线程,从中创建计时器 2 运行。如果没有警卫,很有可能某处某处会出现严重错误。

为了能够以简单的方式将某些东西除以 2(不利用 CPU 专门用于此类东西的指令),必须检索变量,将其除以 2,然后将其放回内存中。

同理,递增也是一个三阶段的过程(再次采用幼稚的方法)。

一旦这两个计时器发生冲突,您就会得到一些疯狂的竞争条件,如下所示:

THREAD 1          | THREAD 2
   <- A=1         |
 Local:A=1+1=2    |  <- A=1
                  |  Local: A=1*2=2
     A=2 ->       |  A=2 ->

线程 2 在线程 1 的计算中途开始 运行ning,检索到错误的状态变量值(因为线程 1 尚未更新变量),并将其乘以 2。你应该有 3,但实际上你最终得到了 2.

为了防止这种情况,有一大堆方法和工具。现在大多数处理器架构都有原子指令(例如 Intel),开发人员可以利用这些 如果他们知道他们在哪里需要它们 。您可以在这些工具之上使用一大堆工具 - 互斥锁、read/write 锁、信号量等...以减少或消除这些问题,需要付出代价,并且你知道哪里你会需要它们。

不用说,概括这一点绝非易事。