Press "Enter" to skip to content

管理大数据应用程序的云存储费用

使用云存储时降低费用的建议

JOSHUA COLEMAN在Unsplash上的照片

随着对数据需求的不断增加,现代公司对高容量和高可扩展性的数据存储解决方案的依赖程度比以往任何时候都更高。对于许多公司来说,这种解决方案采用云存储服务,例如Amazon S3、Google Cloud Storage和Azure Blob Storage,每种服务都配备了丰富的API和功能(例如,多层存储),支持各种各样的数据存储设计。当然,云存储服务也有相关的成本。这些成本通常由许多因素组成,包括您使用的存储空间的总大小,以及将数据转移至云存储内、云存储间或云存储外等活动。例如,Amazon S3的价格包括(截至本文撰写时)六个成本组成部分,每个都需要考虑到。很容易看出,管理云存储成本可以变得复杂,因此已经开发出专用计算器(例如,在这里)来协助处理这些成本。

在最近的一篇文章中,我们扩展了设计数据和数据使用的重要性,以减少与数据存储相关的成本。我们的重点是使用数据压缩作为减少数据总大小的方法。在本文中,我们将重点放在云存储的一个有时被忽视的成本组成部分上——针对云存储桶和数据对象进行API请求的成本。我们将通过示例演示,为什么这个成本组成部分经常被低估,以及如果不妥善管理,它可能成为您的大数据应用程序成本的重要组成部分。然后,我们将讨论几种简单的方法来控制这个成本。

免责声明

尽管我们的演示将使用Amazon S3,但本文的内容同样适用于任何其他云存储服务。请不要将我们选择Amazon S3或任何其他工具、服务或库解释为它们的使用是得到认可的。对于您来说,最好的选择将取决于您自己项目的独特细节。此外,请记住,关于如何存储和使用数据的任何设计选择都会有其利弊,应根据自己项目的细节进行权衡。

本文将包括在Amazon EC2 c5.4xlarge实例上运行的许多实验(具有16个虚拟CPU和“高达10 Gbps”的网络带宽)。我们将分享它们的输出作为您可能会看到的比较结果的示例。请记住,输出可能会因实验运行的环境而大大不同。请不要依赖于此处呈现的结果来进行自己的设计决策。我们强烈建议您在决定自己项目的最佳方案之前运行这些以及其他实验。

一个简单的思维实验

假设你有一个数据转换应用程序,它从S3中操作1 MB的数据样本,并产生1 MB的数据输出,这些输出被上传到S3。假设你的任务是通过在适当的Amazon EC2实例上运行你的应用程序(在与你的S3存储桶相同的区域内,以避免数据传输成本)来转换10亿个数据样本。现在让我们假设Amazon S3每1000个GET操作收取0.0004美元,每1000个PUT操作收取0.005美元(截至本文撰写时)。乍一看,这些成本可能看起来如此之低,以至于与数据转换相关的其他成本相比可以忽略不计。然而,一个简单的计算表明,我们的Amazon S3 API调用单独将计算出一笔5400美元的账单!!这很容易成为您项目中最主要的成本因素,甚至比计算实例的成本还要高。我们将在本文末返回这个思维实验。

将数据分批成大文件

降低API调用成本的显而易见的方法是将样本分组成较大的文件,并将转换运行在批处理样本上。将我们的批处理大小表示为 N ,假设不使用多部分文件传输,这种策略可能将我们的成本降低了N倍(请参见下文)。这种技术不仅可以节省PUT和GET调用的费用,还可以节省Amazon S3的所有成本组成部分,这些成本组成部分取决于对象文件的数量而不是数据的总大小(例如,生命周期转换请求)。

将样本分组在一起存在许多缺点。例如,当您单独存储样本时,可以自由地随意访问其中任何一个。但是当样本被组合在一起时,这变得更加具有挑战性。(有关将样本分批放入大文件中的优缺点,请参见此文章。)如果您选择将样本分组在一起,则最大的问题是如何选择大小N。更大的N可以降低存储成本,但可能会引入延迟,增加计算时间,并且通过扩展,增加计算成本。找到最佳数量可能需要一些考虑这些和其他因素的实验。

但是,让我们不要自欺欺人。进行这种更改并不容易。您的数据可能有许多消费者(包括人类和人工),每个消费者都有自己特定的需求和限制。将样本存储在单独的文件中可以使满足每个人的要求更容易。找到一个能够满足每个人的批处理策略将是困难的。

可能的妥协:批处理放置,单独获取

您可以考虑的一种妥协是上传包含分组样本的大文件,同时允许访问单个样本。一种方法是使用维护每个样本位置的索引文件(其中它被分组的文件,起始偏移量和结束偏移量),并向每个消费者公开一个薄的API层,使他们能够自由地下载单个样本。 API将使用索引文件和S3 API来实现,该API可提取对象文件的特定范围(例如,Boto3的get_object函数)。虽然这种解决方案在GET调用上不会节省任何费用(因为我们仍然拉取相同数量的单个样本),但更昂贵的PUT调用将减少,因为我们将上传较少的较大文件。请注意,这种解决方案对我们用于与S3交互的库提出了一些限制,因为它依赖于允许从大文件对象中提取部分块的API。在先前的帖子中(例如,在此处),我们讨论了与S3交互的不同方式,其中许多不支持此功能。

下面的代码块演示了如何使用PyTorch版本1.13实现一个简单的PyTorch数据集,该数据集使用Boto3 get_object API从分组样本的大文件中提取单个1 MB样本。我们将在此方式下迭代数据的速度与迭代存储在单独文件中的样本进行比较。

import os, boto3, time, numpy as npimport torchfrom torch.utils.data import Datasetfrom statistics import mean, varianceKB = 1024MB = KB * KBGB = KB ** 3sample_size = MBnum_samples = 100000# modify to vary the size of the filessamples_per_file = 2000 # for 2GB filesnum_files = num_samples//samples_per_filebucket = '<s3 bucket>'single_sample_path = '<path in s3>'large_file_path = '<path in s3>'class SingleSampleDataset(Dataset):    def __init__(self):        super().__init__()        self.bucket = bucket        self.path = single_sample_path        self.client = boto3.client("s3")    def __len__(self):        return num_samples    def get_bytes(self, key):        response = self.client.get_object(            Bucket=self.bucket,            Key=key        )        return response['Body'].read()    def __getitem__(self, index: int):        key = f'{self.path}/{index}.image'        image = np.frombuffer(self.get_bytes(key),np.uint8)        return {"image": image}class LargeFileDataset(Dataset):    def __init__(self):        super().__init__()        self.bucket = bucket        self.path = large_file_path        self.client = boto3.client("s3")            def __len__(self):        return num_samples    def get_bytes(self, file_index, sample_index):        response = self.client.get_object(            Bucket=self.bucket,            Key=f'{self.path}/{file_index}.bin',            Range=f'bytes={sample_index*MB}-{(sample_index+1)*MB-1}'        )        return response['Body'].read()    def __getitem__(self, index: int):        file_index = index // num_files        sample_index = index % samples_per_file        image = np.frombuffer(self.get_bytes(file_index, sample_index),                              np.uint8)        return {"image": image}# toggle between single sample files and large filesuse_grouped_samples = Trueif use_grouped_samples:    dataset = LargeFileDataset()else:    dataset = SingleSampleDataset()# set the number of parallel workers according to the number of vCPUsdl = torch.utils.data.DataLoader(dataset, shuffle=True,                                 batch_size=4, num_workers=16)stats_lst = []t0 = time.perf_counter()for batch_idx, batch in enumerate(dl, start=1):    if batch_idx % 100 == 0:        t = time.perf_counter() - t0        stats_lst.append(t)        t0 = time.perf_counter()mean_calc = mean(stats_lst)var_calc = variance(stats_lst)print(f'mean {mean_calc} variance {var_calc}')

下表总结了使用不同样本分组大小 N 时数据遍历速度。

不同分组策略对数据遍历时间的影响(作者)

需要注意的是,虽然这些结果强烈暗示将样本分组成大文件对单独提取样本的性能影响相对较小,但我们发现比较结果因样本大小、文件大小、文件偏移值的大小、从同一文件中并发读取的数量等因素而异。虽然我们不知道 Amazon S3 服务的内部运作情况,但内存大小、内存对齐和限流等考虑因素影响性能并不足为奇。找到您数据的最佳配置可能需要进行一些实验。

影响省钱分组策略的一个重要因素是使用多部分下载和上传,我们将在下一节中讨论。

使用可以控制多部分数据传输的工具

许多云存储服务提供商支持多部分上传和下载对象文件的选项。在多部分数据传输中,大于某个阈值的文件被分成多个部分并同时传输。如果要加速大文件的数据传输,这是一个关键特性。AWS 建议对于大于 100 MB 的文件使用多部分上传。在以下简单的示例中,我们将比较设置了不同值的多部分阈值和块大小的 2 GB 文件的下载时间:

import boto3, timeKB = 1024MB = KB * KBGB = KB ** 3s3 = boto3.client('s3')bucket = '<bucket name>'key = '<key of 2 GB file>'local_path = '/tmp/2GB.bin'num_trials = 10for size in [8*MB, 100*MB, 500*MB, 2*GB]:    print(f'multi-part size: {size}')    stats = []    for i in range(num_trials):        config = boto3.s3.transfer.TransferConfig(multipart_threshold=size,                                              multipart_chunksize=size)        t0 = time.time()        s3.download_file(bucket, key, local_path, Config=config)        stats.append(time.time()-t0)    print(f'multi-part size {size} mean {mean(stats)}')

实验结果总结在下表中:

多部分块大小对下载时间的影响(作者)

需要注意的是,相对比较将大大取决于测试环境,特别是实例与 S3 存储桶之间通信的速度和带宽。我们的实验在实例和存储桶位于同一区域的情况下进行。然而,随着距离的增加,使用多部分下载的影响也会随之增加。

关于我们讨论的主题,需要注意多部分数据传输的成本影响。具体而言,当您使用多部分数据传输时,您将为每个文件部分的 API 操作收费。因此,使用多部分上传/下载将限制批处理数据样本成大文件的成本节省潜力。

许多 API 默认情况下使用多部分下载。这是很好的,如果您主要关心减少与 S3 的交互延迟。但是,如果您关心限制成本,此默认行为不利于您。例如,Boto3 是一种流行的 Python API,用于从 S3 上传和下载文件。如果没有指定,boto3 S3 API(例如 upload_file 和 download_file)将使用默认的 TransferConfig,对于任何大于 8 MB 的文件,均应用多部分上传/下载,块大小为 8 MB。如果您负责控制组织中的云成本,您可能会不高兴地发现这些 API 正在广泛使用其默认设置。在许多情况下,您可能会发现这些设置是不合理的,并且增加多部分阈值和块大小的值或完全禁用多部分数据传输对您的应用程序性能几乎没有影响。

示例——多部分文件传输大小对速度和成本的影响

在下面的代码块中,我们创建了一个简单的多进程转换函数,并测量了多部分块大小对其性能和成本的影响:

import os, boto3, time, mathfrom multiprocessing import Poolfrom statistics import mean, varianceKB = 1024MB = KB * KBsample_size = MBnum_files = 64samples_per_file = 500file_size = sample_size*samples_per_filenum_processes = 16bucket = '<s3 bucket>'large_file_path = '<path in s3>'local_path = '/tmp'num_trials = 5cost_per_get = 4e-7cost_per_put = 5e-6for multipart_chunksize in [1*MB, 8*MB, 100*MB, 200*MB, 500*MB]:    def empty_transform(file_index):        s3 = boto3.client('s3')        config = boto3.s3.transfer.TransferConfig(                             multipart_threshold=multipart_chunksize,                             multipart_chunksize=multipart_chunksize                             )        s3.download_file(bucket,                          f'{large_file_path}/{file_index}.bin',                          f'{local_path}/{file_index}.bin',                          Config=config)        s3.upload_file(f'{local_path}/{file_index}.bin',                       bucket,                       f'{large_file_path}/{file_index}.out.bin',                       Config=config)    stats = []    for i in range(num_trials):        with Pool(processes=num_processes) as pool:            t0 = time.perf_counter()            pool.map(empty_transform, range(num_files))            transform_time = time.perf_counter() - t0            stats.append(transform_time)    num_chunks = math.ceil(file_size/multipart_chunksize)    num_operations = num_files*num_chunks    transform_cost = num_operations * (cost_per_get + cost_per_put)    if num_chunks > 1:        # 如果使用多部分,还需添加CreateMultipartUpload和CompleteMultipartUpload API调用的成本        transform_cost += 2 * num_files * cost_per_put    print(f'chunk size {multipart_chunksize}')    print(f'transform time {mean(stats)} variance {variance(stats)}    print(f'cost of API calls {transform_cost}')

在这个例子中,我们将文件大小固定为500 MB,并将相同的多部分设置应用于下载和上传。更全面的分析将会变化数据文件的大小和多部分设置。

在下面的表格中,我们总结了实验的结果。

Impact of Multi-part Chunk Size on Data Transformation Speed and Cost (by Author)

结果表明,在多部分块大小不超过500 MB(我们文件的大小)时,数据转换时间的影响最小。另一方面,在与使用Boto3的默认块大小(8MB)相比较时,云存储API成本的潜在节省是显著的,高达98.4%。这个例子不仅展示了通过将样本分组的成本效益,而且还暗示了通过适当配置多部分数据传输设置的额外节省机会。

结论

让我们将我们上一个例子的结果应用于我们在本文开头引入的思考实验中。我们展示了对10亿个数据样本应用简单转换将花费5400美元,如果将样本存储在单独的文件中。如果我们将样本分组为具有500个样本的200万个文件,并在没有多部分数据传输的情况下应用转换(与上面的示例的最后一次试验相同),则API调用的成本将降低到10.8美元!同时,在假设相同的测试环境的情况下,我们预期的整体运行时间的影响将相对较低。我认为这是一个相当不错的交易。你呢?

总结

在开发基于云的大数据应用程序时,了解我们活动的所有细节至关重要。在本文中,我们专注于Amazon S3定价策略的“请求和数据检索”组件。我们演示了此组件如何成为大数据应用程序整体成本的主要组成部分。我们讨论了影响此成本的两个因素:将数据样本组合在一起的方式和使用多部分数据传输的方式。

自然地,仅优化一个成本组成部分很可能会以一种增加其他组成部分的方式提高总成本。适当的数据存储设计需要考虑所有潜在的成本因素并且极大程度上取决于您特定的数据需求和使用模式

像往常一样,请随时提出您的评论和更正意见。

Leave a Reply

Your email address will not be published. Required fields are marked *