扩展 MinIO 内部连接

Scaling up MinIO Internal Connectivity

MinIO 集群作为一个统一集群运行。这意味着任何请求都必须能够被任何服务器无缝处理。因此,服务器需要彼此之间进行协调。到目前为止,这都是通过传统的 HTTP RPC 请求来处理的,并且这种方法对我们来说非常有效。

每当服务器 A 想要调用服务器 B 时,就会发出一个 HTTP 请求。我们利用 HTTP 保持连接,因此不必为每个请求创建原始连接。

我们尝试过使用 HTTP/2,但由于其对挂起请求处理不可靠,导致连接大量积压和请求无法响应,因此不得不放弃。

HTTP 的扩展性

然而,随着每个集群拥有数百台服务器(这正变得越来越普遍),我们开始看到这种方法在扩展性方面出现一些问题。

当有 N 台服务器时,我们可以预期每台服务器都需要与 N-1 台服务器通信。这意味着在一个拥有 100 台服务器的集群中,任何一台服务器都需要与 99 台服务器通信,这意味着 99 个入站和 99 个出站连接。这本身并不是什么问题,并且可以正常工作。

但是,随着集群负载增加,问题可能会开始出现。并非所有请求都能立即得到服务,并且诸如列出操作之类的操作通常需要一段时间才能完成 - 可能是因为磁盘需要时间来完成请求,或者是在等待客户端吸收数据。

这些长时间运行的请求意味着许多连接将保持打开状态,这意味着新的调用需要打开新的请求。类似的效果也可能出现在突发的大量传入请求中,其中会创建大量连接来服务这些请求。

对于数百台服务器,这种情况在某些情况下会导致不良影响,即系统在高负载下响应不佳。

双向通信

为了防止这个问题在将来成为一个更大的问题,我们决定探索我们的选择,并研究允许 MinIO 无缝扩展的方案。让每台服务器连接到所有其他服务器本身并不是问题 - 问题在于每台服务器对同一台服务器都有**多个**长时间运行的连接。

我们确定了两个我们想要解决的案例。首先是具有少量有效负载的小型请求。这些请求通常对延迟非常敏感,单个请求花费的时间更长通常会导致原始请求花费的时间更长。其次,我们希望具有少量、流式有效负载的长运行请求不会在调用期间保持连接打开状态。

从技术上讲,HTTP 请求是双向的,这意味着您发送请求有效负载并接收响应。但是对于常规的 HTTP 请求,一旦您开始发送响应,您就无法在连接上再接收任何内容。HTTP/2 允许双向通信,但我们不想走这条路,也不想因为上面提到的原因而不得不放弃它。

我们还考虑了常规的 TCP 套接字以及无状态 UDP 数据包。对于新产品来说,这些都是可行的选择,但我们放弃了它们,因为这需要为许多安装更改网络配置,并且不会提供平滑的过渡。

因此,对于通信,我们选择使用 WebSockets,因为它提供了 A) 双向通信 B) 二进制消息 C) 与现有连接的良好集成以及 D) 良好的性能。

由于连接是双向的,因此我们在任何两台服务器之间使用单个连接,并且仅在单个消息中区分服务器和客户端。

特性

在初始实现中,我们支持两种类型的操作。连接在建立时进行身份验证,因此单个请求不需要身份验证。连接会持续监控,因此可以快速识别和重新建立挂起的连接。

请求

首先是简单的请求 -> 响应请求,这些请求将所有数据作为调用的一部分提供,并将所有数据作为单个回复接收。这针对小型但低延迟的往返。

这针对许多小型消息进行了优化,我们的目标是优化消息数量和延迟。没有取消传播,只有调用方指定的超时。如果调用方不再对结果感兴趣,则该响应将被简单地忽略。

流提供完全控制的双向通信。流将包含初始有效负载,并且可以提供数据输入和输出流。流受拥塞控制,并提供远程取消以及超时。

这适用于低吞吐量的流,否则这些流会在请求运行期间保持连接打开状态,而流量很少。流定期检查远程端是否仍在运行,以确保请求不会在等待永远不会出现的响应时挂起。

消息

我们使用此基本结构提供轻量级消息

我们使用 MessagePack 作为轻量级且简单的序列化器。“Op”定义了字段的含义和消息的路由。

消息可以选择具有校验和,我们为没有 TLS 的连接启用校验和。我们允许将多条消息合并到一条消息中,以减少在多条消息排队时单个写入的数量。

性能

虽然性能从来不是这次更改的主要目标,但我们也希望确保它能够表现得足够好,并且在部署时不会给性能带来瓶颈。

对单个请求 -> 响应进行基准测试。512 字节的请求有效负载和响应,以及并发调用者的数量变化。在 16 核/32 线程 CPU 上测试。首先,我们从 2 台服务器开始

服务器

调用者

操作/秒

延迟(毫秒)

2

32

411,991

0.1549

2

256

901,227

0.5649

2

1024

934,868

2.156

使用 2 台“服务器”,我们达到了每台服务器之间单个连接的限制,大约每秒 100 万条 2x512 字节的消息。此时,延迟将持续增加,因为我们已经达到每个连接的限制。这表明我们在任何两台服务器之间都有足够的吞吐量。

作为参考,纯 HTTP rest API 在类似的设置下,没有身份验证或中间件,每秒最大处理约 20,000 个请求。

让我们观察一下多台服务器上的扩展情况。我们使用 32 台虚拟服务器对性能进行基准测试

服务器

调用者

操作/秒

延迟(毫秒)

32

32

5,501,753

0.1851

32

256

5,857,659

1.375

32

1024

6,343,717

5.086

使用单独的服务器,我们可以观察到我们似乎达到了每秒每台服务器 600 万条消息的限制。

让我们也对**流**进行基准测试。我们对最常见的场景进行基准测试,即具有 512 字节有效负载的请求和 10 个分别具有 512 字节的响应。我们将每个请求的缓冲区限制为 1 条消息,因此我们确保也包括由于流量控制导致的延迟。

我们将每个响应计算为一个操作。

服务器

调用者

操作/秒

延迟(毫秒)

2

32

354,138

0.1747

2

256

806,887

0.661

2

1024

849,717

2.442

与单个请求相比,添加的控制流会增加少量开销。但总的来说,即使流更复杂,与单次请求的差异也很小。

服务器

调用者

操作/秒

延迟(毫秒)

32

32

4,925,902

0.202

32

256

8,269,334

0.9762

32

1024

11,854,333

2.691

查看 32 台服务器,我们看到 1 消息缓冲允许获得更好的吞吐量。

结论和未来计划

内部连接的重写已在 RELEASE.2023-12-02T10-51-33Z 中发布,现在用于所有锁定操作以及十几种与存储相关的调用。我们很高兴地看到,到目前为止,尚未报告与这一相当大的更改相关的任何回归。

我们相信这将有助于 MinIO 在未来扩展得更强大,因为我们预先消除了潜在的扩展瓶颈。我们很高兴地看到我们的解决方案提供了几乎高出两个数量级的消息吞吐量。

我们将继续为新的连接添加更多远程调用,只要这样做有意义。我们预计不会迁移所有远程调用,因为对于传输大量数据(如文件流)而言,单独的连接仍然有意义。

如果您计划部署分布式集群,请联系我们 hello@min.io - 或者如果您只是想试用一下,请在 min.io/download 下载 MinIO 服务器。