高效处理大文件上传和下载

2024-06-28 10:54:16 浏览数 (1)

在处理大文件传输时。为了提升用户体验感。我们会尽力减少完成传输时间上下文章。一个很简单的道理就是传输时间取决于网路链接速度和文件大小以及并发处理线程数。当然除了这个,其实我们还有断点续传和差异传输更高级的玩法来进一步提升传输体验感。

一、压缩和分块传输

在传输上,我们最直观的想法上看能不能压缩传输文件大小。压缩提高传输信息熵。我们来看看经典传输算法。当然在网络流传输上,有时候我们并不知道传输内容的边界在哪里。于是我们会去认为创造阶段边界,然后进行边压缩边传输。所以压缩有时候会和分片传输结合来用。

1.1. 压缩传输

对于纯压缩算法效率来说,我们用python测试下几种压缩算法的压缩空间效率

运行前需要安装pip install brotli

代码语言:javascript复制
import bz2
import gzip
import lzma
import pickle

import brotli


class SomeObject():

    a = 'some data'
    b = 123
    c = 'more data'

    def __init__(self, i):
        self.i = i


data = [SomeObject(i) for i in range(1, 1000000)]

with open('no_compression.pickle', 'wb') as f:
    pickle.dump(data, f)

with gzip.open("gzip_test.gz", "wb") as f:
    pickle.dump(data, f)

with bz2.BZ2File('bz2_test.pbz2', 'wb') as f:
    pickle.dump(data, f)

with lzma.open("lzma_test.xz", "wb") as f:
    pickle.dump(data, f)

with open('no_compression.pickle', 'rb') as f:
    pdata = f.read()
    with open('brotli_test.bt', 'wb') as b:
        b.write(brotli.compress(pdata))

运行后看这个程序的输出。在压缩空间效率上看,lzma>bz2>brotli>gzip。

1.2、分片传输

将数据切分成多个等大小的数据块,然后启动多个线程并发传输处理。但是也不是说将分块切越小,处理的线程数越多就越好。反而是尽可能避免将传输分成较小的数据块。不分片的好处是在一个数据块中上传全部内容。避免分块消除了查询每个数据块的持久偏移时增加的延迟时间费用和操作费用,并提高了吞吐量。不过,在以下情况中,应考虑分块上传:

  • 系统正在动态生成源数据,并且希望在上传失败时限制缓冲客户端所需的数据量。
  • 与许多浏览器一样,客户端具有请求大小限制。

如果使用 JSON 或 XML API 并且客户端收到错误,可以向服务器查询持久偏移,并从该偏移处继续上传剩余的字节。再来看一下分片传输的例子。我们请求分片大小为4k,然后下载一个文件切割成多份分段请求。

代码语言:python代码运行次数:0复制
import requests
chunk_size = 4096
filename = "logo.png"
document_url = "https://wasi0013.files.wordpress.com/2018/11/my_website_logo_half_circle_green-e1546027650125.png"
with requests.get(document_url, stream=True) as r:
        with open(filename, 'wb') as f:
            for chunk in r.iter_content(chunk_size): 
                if chunk:
                    f.write(chunk)

二、断点续传

可续传上传在上传过程中网络出现故障时,不必从头开始重新上传。

可续传上传的工作原理是发送多个请求,每个请求包含正在上传的对象的一部分。这与简单上传不同,后者在单个请求中包含对象的所有数据,一旦中途失败,则必须从头开始重新上传。

2.1 上传大小注意事项

在选择使用单一请求上传而不是可续传上传或 XML API 多部分上传时,考虑在网络故障需要的时间里,将丢失多少内容需要重启以便从头开始。为了提高连接速度,临界值大小通常可以设置得较大。

例如,假设愿意容忍 30 秒的损失时间:

  • 如果通过平均上传速度为 8 Mbps 的本地系统上传,则可以对大小超过 30 MB 的文件使用单一请求上传。
  • 如果通过平均上传速度为 500 Mbps 的地区内服务上传,则文件的临界值大小差不多为 2 GB。
  • 可续传上传有效期在启动后的一短时间内完成,比如说一周,但可以随时取消。
  • 只有已完成的可续传上传会替换具有相同名称的现有对象。

2.2 未知规模的可续传上传

可续传上传机制支持传输事先不知道大小的文件。对于在上传过程中即时压缩对象等情况而言,这非常有用,因为在传输开始时很难预测压缩文件的确切大小。这可以在中断后恢复的流式传输,

2.3 选择会话区域

可续传上传应该固定在启动上传操作的地区中。例如,如果在美国启动可续传上传,并将会话URL提供给亚洲的客户端,则上传仍会经由美国进行。为了减少跨区域流量并提高性能,应该将可续传上传会话保留在创建它的区域中。

比如说某谷歌云是这样做的。如果使用 Compute Engine 实例启动可续传上传,则该实例应与上传到的 Cloud Storage 存储桶位于同一位置。然后,使用地理位置 IP 服务来选择将客户请求路由到的 Compute Engine 地区,这有助于将流量局限到某个地理位置地区。

2.4 会话 URI

启动可持续上传时,某云是这样做的。Cloud Storage 会返回一个会话 URI,可以在后续请求中使用该 URI 上传实际数据。此会话 URI 充当身份验证令牌,因此使用它的请求不需要签名,并且任何人都可以在不进行任何进一步的身份验证的情况下,使用这些请求将数据上传到目标存储桶。因此,谨慎分享会话 URI,并且仅通过 HTTPS 分享。

会话 URI 的有效期为一周,但在到期之前可以进行取消。如果使用已失效的会话 URI 发出请求,则会收到以下其中一个错误:

  • 410 Gone 状态代码(如果启动上传后不到一周)。
  • 404 Not Found 状态代码(如果启动上传已超过一周)。

在两种情况下,都必须启动新的可续传上传,获得新的会话 URI,然后使用新的会话 URI 从头开始上传。

2.5 完整性检查

建议对最终上传的对象进行完整性检查,以确保其与源文件相同。为此,可以计算源文件的 MD5 digest,并将其添加到 Content-MD5 请求标头中。如果花了很长时间上传大型文件,检查上传文件的完整性尤其重要,这是因为上传操作过程中源文件被修改可能性会增加。

2.6 重试和重新发送数据

对待重新发送重复的数据,可以选择丢弃新请求。因此,在回滚到先前发送的偏移时,不应发送不同的数据。

当 Cloud Storage 在可续传上传操作中保留字节后,这些字节将无法被覆盖,且 Cloud Storage 会忽略尝试这样做。

例如,假设您要上传一个 10 万个字节的对象,并且连接已中断。检测时,会发现已成功上传并保留 50000 个字节。如果尝试在第 40000 个字节处重启上传,则 Cloud Storage 会忽略从 40000 发送到 50000 的字节。Cloud Storage 将开始保留在第 50001 个字节处发送的数据。

三 差分传输

我们来看下一个经典的差分传输算法Rsync。

  • rsync 算法的非数学概述。
  • 该算法在 rsync 实用程序中是如何实现的。
  • 一般来说,rsync 实用程序使用的协议。
  • rsync 进程扮演的可识别角色。

这个方便读者理解

  • 为什么 rsync 的行为如此。
  • rsync 的局限性。
  • 为什么请求的功能不适合代码库。

3.1 流程和角色

客户

角色

客户端启动同步。

服务器

角色

客户端通过本地传输、远程 shell 或网络套接字连接到的远程 rsync 进程或系统。这是一个通用术语,不要与守护进程混淆。

一旦客户端和服务器之间的连接建立,它们之间的区别就会被发送者和接收者角色所取代。

守护进程

角色和流程

等待客户端连接的 Rsync 进程。在某些平台上,这被称为服务。

远程 shell

角色和流程集

提供远程系统上的 Rsync 客户端和 Rsync 服务器之间的连接的一个或多个进程。

发件者

角色和流程

可以访问正在同步的源文件的 Rsync 进程。

接收者

角色和流程

作为角色,接收方是目标系统。作为进程,接收方是接收更新数据并将其写入磁盘的进程。

生成器

过程

生成器进程识别改变的文件并管理文件级逻辑。

3.11 进程启动

当 Rsync 客户端启动时,它将首先与服务器进程建立连接。此连接可以通过管道或网络套接字建立。

当 Rsync 通过远程 shell 与远程非守护程序服务器通信时,启动方法是fork远程 shell,这将在远程系统上启动 Rsync 服务器。Rsync 客户端和服务器都通过远程 shell 通过管道进行通信。就 rsync 进程而言,没有网络。在此模式下,服务器进程的 rsync 选项在用于启动远程 shell 的命令行上传递。

当 Rsync 与守护进程通信时,它直接与网络套接字通信。这是唯一一种可以称为网络感知的 Rsync 通信。在此模式下,必须通过套接字发送 rsync 选项,如下所述。

在客户端和服务器之间通信的一开始,它们各自向对方发送它们支持的最高协议版本。然后,每一方都使用最小值作为传输的协议级别。如果这是守护进程模式连接,则客户端会将 rsync 选项发送到服务器。然后,传输排除列表。从此时起,客户端-服务器关系仅与错误和日志消息传递有关。

本地 Rsync 作业(当源和目标都在本地安装的文件系统上时)的执行方式与推送完全相同。客户端(成为发送方)分叉服务器进程以履行接收方角色。客户端/发送方和服务器/接收方通过管道相互通信。

3.12 文件列表

文件列表不仅包括路径名,还包括所有权、模式、权限、大小和修改时间。如果指定了 --checksum 选项,它还包括文件校验和。

启动完成后,首先发生的事情是发送方将创建文件列表。在创建文件列表时,每个条目都会以网络优化的方式传输到接收方。

完成此操作后,每一方都会根据相对于传输基目录的路径按字典顺序对文件列表进行排序。(确切的排序算法取决于传输所采用的协议版本。)完成此操作后,所有对文件的引用都将通过文件列表中的索引进行。

如果有必要,发送方会按照文件列表提供用户和组的 id→name 表,接收方将使用该表对文件列表中的每个文件进行 id→name→id 的转换。

当文件列表被接收器收到后,它将分叉成为完成管道的生成器和接收器对。

3.1.3 管道

Rsync 是高度流水线化的。这意味着它是一组以(很大程度上)单向方式通信的进程。一旦文件列表被共享,管道的行为如下:

生成器 → 发送者 → 接收者

生成器的输出是发送者的输入,发送者的输出是接收者的输入。每个进程独立运行,只有当管道停滞或等待磁盘 I/O 或 CPU 资源时才会延迟。

3.1.4 生成器

生成器进程将文件列表与其本地目录树进行比较。在开始其主要功能之前,如果已指定 --delete,它将首先识别不在发送方上的本地文件,并在接收方上删除它们。

然后,生成器将开始遍历文件列表。将检查每个文件以查看是否可以跳过。在最常见的操作模式下,如果修改时间或大小不同,则不会跳过文件。如果指定了 --checksum,将创建并比较文件级校验和。不会跳过目录、设备节点和符号链接。将创建缺失的目录。

如果不想跳过某个文件,则接收方的任何现有版本都将成为传输的“基础文件”,并用作数据源,这将有助于消除发送方必须发送的匹配数据。为了实现这种远程数据匹配,将为基础文件创建块校验和,并在文件索引号后立即发送给发送方。如果指定了 --whole-file,则会为新文件发送一个空的块校验和集。

块大小以及后续版本中的块校验和的大小是根据文件的大小逐个计算的。

3.1.5 发件人

发送方进程从生成器中一次读取一个文件索引号以及相关的块校验和集。

对于生成器发送的每个文件 ID,它将存储块校验和并建立它们的哈希索引以便快速查找。

然后读取本地文件,并为从本地文件的第一个字节开始的块生成校验和。在生成器发送的集合中查找此块校验和,如果没有找到匹配项,则将不匹配的字节附加到不匹配的数据中,并从下一个字节开始比较块。这就是所谓的“滚动校验和”

如果发现块校验和匹配,则将其视为匹配块,并且任何累积的不匹配数据将被发送到接收器,然后发送接收器文件中匹配块的偏移量和长度,并且块校验和生成器将前进到匹配块后的下一个字节。

即使块被重新排序或偏移量不同,也可以通过这种方式识别匹配的块。这个过程是 rsync 算法的核心。

这样,发送者将向接收者提供如何将源文件重建为新目标文件的说明。这些说明详细说明了可以从基础文件复制的所有匹配数据(如果存在用于传输的数据),并包括本地不可用的任何原始数据。在每个文件处理结束时,都会发送整个文件的校验和,然后发送者继续处理下一个文件。

生成滚动校验和并在生成器发送的校验和集中搜索匹配项需要大量 CPU 能力。在所有 rsync 进程中,发送方是最耗费 CPU 的。

3.1.6 收件人

接收方将从发送方读取由文件索引号标识的每个文件的数据。它将打开本地文件(称为基础)并创建一个临时文件。

接收方将期望按顺序读取不匹配的数据和/或匹配记录,以获得最终文件内容。读取不匹配的数据时,它将被写入临时文件。收到块匹配记录后,接收方将查找基础文件中的块偏移量,并将该块复制到临时文件。这样,临时文件就从头到尾构建好了。

文件的校验和是在临时文件构建时生成的。在文件末尾,会将此校验和与发送方的文件校验和进行比较。如果文件校验和不匹配,则删除临时文件。如果文件失败一次,则会在第二阶段重新处理,如果失败两次,则会报告错误。

临时文件完成后,设置其所有权和权限以及修改时间。然后将其重命名以替换基础文件。

将数据从基础文件复制到临时文件使接收方成为所有 rsync 进程中磁盘占用最多的进程。小文件可能仍在磁盘缓存中,从而缓解这种情况,但对于大文件,缓存可能会崩溃,因为生成器已转移到其他文件,并且发送方会造成进一步的延迟。由于数据可能随机从一个文件读取并写入另一个文件,如果工作集大于磁盘缓存,则可能发生所谓的寻道风暴,进一步损害性能。

3.1.7 守护进程

守护进程与许多守护进程一样,每次连接都会分叉。启动时,它会解析 rsyncd.conf 文件以确定存在哪些模块并设置全局选项。

当接收到已定义模块的连接时,守护进程会派生一个新的子进程来处理该连接。然后,该子进程读取 rsyncd.conf 文件以设置所请求模块的选项,该选项可能会 chroot 到模块路径,并可能删除进程的 setuid 和 setgid。此后,它将像任何其他采用发送者或接收者角色的 rsync 服务器进程一样运行。

3.2 Rsync 协议

精心设计的通信协议具有许多特点。

  • 所有内容都以明确定义的数据包的形式发送,其中包含标头和可选的主体或数据有效负载。
  • 每个数据包的标头中都指定了一个类型和/或命令。
  • 每个数据包都有一定的长度。

除了这些特点之外,协议还具有不同程度的状态性、数据包间的独立性、人为可读性以及重新建立断开的会话的能力。

Rsync 的协议没有这些优点。数据以不间断的字节流形式传输。除了不匹配的文件数据外,没有长度说明符或计数。相反,每个字节的含义取决于协议级别定义的上下文。

例如,当发送方发送文件列表时,它只是发送每个文件列表条目,并以空字节结束列表。在文件列表条目中,位字段指示预期结构中的哪些字段,而那些可变长度字符串的字段则以空字节结束。发送文件编号和块校验和集的生成器的工作方式相同。

这种通信方法在可靠的连接上效果很好,而且它的数据开销肯定比正式协议要小。不幸的是,这使得协议的文档记录、调试或扩展变得极其困难。协议的每个版本在线路上都会有细微的差异,只有知道确切的协议版本才能预测。

0 人点赞