修改:事实证明,我最初的假设是错误的。我在这里添加了一个冗长的答案,邀请其他人进行压力测试和纠正。
我正在寻找一种以单线程方式利用Boto3 S3 API来模仿线程安全键值存储的方法。简而言之,我想使用调用线程而不是新线程进行上传。
据我所知,Boto3(或.upload_file()
)中.upload_fileobj()
方法的默认行为是将任务启动到新线程并立即返回None
。
来自docs:
这是一个托管传输,如有必要,它将在多个线程中执行分段上传。
(如果我一开始的理解是错误的,那么对此进行更正也会有所帮助。这在Boto3 1.9.134中。)
>>> import io
>>> import boto3
>>> bucket = boto3.resource('s3').Bucket('my-bucket-name')
>>> buf = io.BytesIO(b"test")
>>> res = bucket.upload_fileobj(buf, 'testobj')
>>> res is None
True
现在,我们假设buf
并不是一个短的4字节字符串,而是一个巨大的文本blob,它将花费不小的时间来完全上传。
我还使用此功能来检查是否存在具有给定键的对象:
def key_exists_in_bucket(bucket_obj, key: str) -> bool:
try:
bucket_obj.Object(key).load()
except botocore.exceptions.ClientError:
return False
else:
return True
我的意图是不重写对象的名称。
这里的竞争状况非常明显:异步启动上传,然后使用key_exists_in_bucket()
进行快速检查,如果仍在写入对象,则返回False
,然后再次写入不必要的结果。
是否有办法确保当前线程调用bucket.upload_fileobj()
而不是在该方法范围内创建的新线程? >
我意识到这会减慢速度。在这种情况下,我愿意牺牲速度。
答案 0 :(得分:5)
upload_fileobj采用Config参数。这是一个boto3.s3.transfer.TransferConfig对象,它具有一个名为use_threads
的参数(默认为true)-如果为True,则在执行S3传输时将使用线程。如果为False,则在执行传输时将不使用任何线程:所有逻辑都将在主线程中运行。
希望这对您有用。
答案 1 :(得分:3)
测试该方法是否被阻止:
我自己进行了经验测试。首先,我生成了一个100MB的文件,其中包含:
dd if=/dev/zero of=100mb.txt bs=100M count=1
然后,我尝试以与您相同的方式上传文件,并测量花费的时间:
import boto3
import time
import io
file = open('100mb.txt', 'rb')
buf = io.BytesIO(file.read())
bucket = boto3.resource('s3').Bucket('testbucket')
start = time.time()
print("starting to upload...")
bucket.upload_fileobj(buf, '100mb')
print("finished uploading")
end = time.time()
print("time: {}".format(end-start))
upload_fileobj()方法完成并花费了8秒以上的时间来读取下一条python行(1gb文件需要50秒),因此我认为此方法正在阻止。< / p>
使用线程进行测试:
使用多个线程时,即使使用use_threads = False 选项,我也可以验证该方法同时支持多个传输。我开始上传200mb文件,然后上传100mb文件,然后首先上传100mb文件。这证实了 TransferConfig 中的并发与分段传输有关。
代码:
import boto3
import time
import io
from boto3.s3.transfer import TransferConfig
import threading
config = TransferConfig(use_threads=False)
bucket = boto3.resource('s3').Bucket('testbucket')
def upload(filename):
file = open(filename, 'rb')
buf = io.BytesIO(file.read())
start = time.time()
print("starting to upload file {}".format(filename))
bucket.upload_fileobj(buf,filename,Config=config)
end = time.time()
print("finished uploading file {}. time: {}".format(filename,end-start))
x1 = threading.Thread(target=upload, args=('200mb.txt',))
x2 = threading.Thread(target=upload, args=('100mb.txt',))
x1.start()
time.sleep(2)
x2.start()
输出:
开始上传文件200mb.txt
开始上传文件100mb.txt
完成上传文件100mb.txt。时间:46.35254502296448
完成上传文件200mb.txt。时间:61.70564889907837
使用会话进行测试:
如果希望按调用顺序完成上载方法,这就是您需要的。
代码:
import boto3
import time
import io
from boto3.s3.transfer import TransferConfig
import threading
config = TransferConfig(use_threads=False)
session = boto3.session.Session()
s3 = session.resource('s3')
bucket = s3.Bucket('testbucket')
def upload(filename):
file = open(filename, 'rb')
buf = io.BytesIO(file.read())
start = time.time()
print("starting to upload file {}".format(filename))
bucket.upload_fileobj(buf,filename)
end = time.time()
print("finished uploading file {}. time: {}".format(filename,end-start))
x1 = threading.Thread(target=upload, args=('200mb.txt',))
x2 = threading.Thread(target=upload, args=('100mb.txt',))
x1.start()
time.sleep(2)
x2.start()
输出:
开始上传文件200mb.txt
开始上传文件100mb.txt
完成上传文件200mb.txt。时间:46.62478971481323
完成上传文件100mb.txt。时间:50.515950202941895
我发现了一些资源:
-This是SO中有关该方法正在阻塞或不阻塞的问题。它不是结论性的,但其中可能有相关信息。
-GitHub上有一个开放的issue,允许在boto3中进行异步传输。
-还有aioboto和aiobotocore之类的工具专门用于允许从s3和其他AWS服务进行异步下载和上传。
关于我之前的回答:
您可以阅读here来了解boto3中的文件传输配置。特别是:
传输操作使用线程来实现并发。线程使用 可以通过将use_threads属性设置为False来禁用。
最初,我认为这与并发执行的多重传输有关。但是,在使用 TransferConfig 时,在参数{em> max_concurrency 中读取source code注释会说明,并发不是指多次传输,而是指 “将请求执行传输的线程数” 。因此,它可以用来加快传输速度。 use_threads 属性仅用于允许多部分传输中的并发。
答案 2 :(得分:2)
我认为,由于这个问题的答案和another similar question似乎都存在直接冲突,因此最好直接与pdb
一起寻找源头。
boto3
默认使用多个线程(10)我正在努力解决的一个方面是多个(子线程)不会不是表示顶级方法本身是非阻塞的:如果调用线程开始了向多个子线程的上传,但是随后等待那些线程完成并返回,我敢说那仍然是一个阻塞调用。与此相反,如果方法调用用asyncio
来说是“即发即弃”调用。对于threading
,这实际上取决于是否曾经调用过x.join()
。
这是从Victor Val摘录的用于启动调试器的初始代码:
import io
import pdb
import boto3
# From dd if=/dev/zero of=100mb.txt bs=50M count=1
buf = io.BytesIO(open('100mb.txt', 'rb').read())
bucket = boto3.resource('s3').Bucket('test-threads')
pdb.run("bucket.upload_fileobj(buf, '100mb')")
此堆栈框架来自Boto 1.9.134。
现在跳进pdb
:
.upload_fileobj()
首先调用一个嵌套方法-现在还不多。
(Pdb) s
--Call--
> /home/ubuntu/envs/py372/lib/python3.7/site-packages/boto3/s3/inject.py(542)bucket_upload_fileobj()
-> def bucket_upload_fileobj(self, Fileobj, Key, ExtraArgs=None,
(Pdb) s
(Pdb) l
574
575 :type Config: boto3.s3.transfer.TransferConfig
576 :param Config: The transfer configuration to be used when performing the
577 upload.
578 """
579 -> return self.meta.client.upload_fileobj(
580 Fileobj=Fileobj, Bucket=self.name, Key=Key, ExtraArgs=ExtraArgs,
581 Callback=Callback, Config=Config)
582
583
584
因此顶层方法的确会返回某物,但尚不清楚该东西最终如何变成None
。
所以我们介入其中。
现在,.upload_fileobj()
确实有一个config
参数,默认情况下为None:
(Pdb) l 531
526
527 subscribers = None
528 if Callback is not None:
529 subscribers = [ProgressCallbackInvoker(Callback)]
530
531 config = Config
532 if config is None:
533 config = TransferConfig()
534
535 with create_transfer_manager(self, config) as manager:
536 future = manager.upload(
这意味着config
成为默认的TransferConfig()
:
use_threads
-如果为True,则在执行S3传输时将使用线程。如果为False,则在执行传输时将不使用任何线程:所有逻辑都将在主线程中运行。max_concurrency
-发出请求执行传输的最大线程数。如果use_threads设置为False,则提供的值将被忽略,因为该传输只会使用主线程。哇,他们在这里:
(Pdb) unt 534
> /home/ubuntu/envs/py372/lib/python3.7/site-packages/boto3/s3/inject.py(535)upload_fileobj()
-> with create_transfer_manager(self, config) as manager:
(Pdb) config
<boto3.s3.transfer.TransferConfig object at 0x7f1790dc0cc0>
(Pdb) config.use_threads
True
(Pdb) config.max_concurrency
10
现在,我们向下调用堆栈中的某个级别以使用TransferManager
(上下文管理器)。此时,max_concurrency
已用作类似名称的max_request_concurrency
的参数:
# https://github.com/boto/s3transfer/blob/2aead638c8385d8ae0b1756b2de17e8fad45fffa/s3transfer/manager.py#L223
# The executor responsible for making S3 API transfer requests
self._request_executor = BoundedExecutor(
max_size=self._config.max_request_queue_size,
max_num_threads=self._config.max_request_concurrency,
tag_semaphores={
IN_MEMORY_UPLOAD_TAG: TaskSemaphore(
self._config.max_in_memory_upload_chunks),
IN_MEMORY_DOWNLOAD_TAG: SlidingWindowSemaphore(
self._config.max_in_memory_download_chunks)
},
executor_cls=executor_cls
)
至少在此boto3版本中,该类来自单独的库s3transfer
。
(Pdb) n
> /home/ubuntu/envs/py372/lib/python3.7/site-packages/boto3/s3/inject.py(536)upload_fileobj()
-> future = manager.upload(
(Pdb) manager
<s3transfer.manager.TransferManager object at 0x7f178db437f0>
(Pdb) manager._config
<boto3.s3.transfer.TransferConfig object at 0x7f1790dc0cc0>
(Pdb) manager._config.use_threads
True
(Pdb) manager._config.max_concurrency
10
接下来,让我们进入manager.upload()
。这是该方法的全文:
(Pdb) l 290, 303
290 -> if extra_args is None:
291 extra_args = {}
292 if subscribers is None:
293 subscribers = []
294 self._validate_all_known_args(extra_args, self.ALLOWED_UPLOAD_ARGS)
295 call_args = CallArgs(
296 fileobj=fileobj, bucket=bucket, key=key, extra_args=extra_args,
297 subscribers=subscribers
298 )
299 extra_main_kwargs = {}
300 if self._bandwidth_limiter:
301 extra_main_kwargs['bandwidth_limiter'] = self._bandwidth_limiter
302 return self._submit_transfer(
303 call_args, UploadSubmissionTask, extra_main_kwargs)
(Pdb) unt 301
> /home/ubuntu/envs/py372/lib/python3.7/site-packages/s3transfer/manager.py(302)upload()
-> return self._submit_transfer(
(Pdb) extra_main_kwargs
{}
(Pdb) UploadSubmissionTask
<class 's3transfer.upload.UploadSubmissionTask'>
(Pdb) call_args
<s3transfer.utils.CallArgs object at 0x7f178db5a5f8>
(Pdb) l 300, 5
300 if self._bandwidth_limiter:
301 extra_main_kwargs['bandwidth_limiter'] = self._bandwidth_limiter
302 -> return self._submit_transfer(
303 call_args, UploadSubmissionTask, extra_main_kwargs)
304
305 def download(self, bucket, key, fileobj, extra_args=None,
啊,可爱-因此,我们需要进一步降低至少一级才能查看实际的基础上载。
(Pdb) s
> /home/ubuntu/envs/py372/lib/python3.7/site-packages/s3transfer/manager.py(303)upload()
-> call_args, UploadSubmissionTask, extra_main_kwargs)
(Pdb) s
--Call--
> /home/ubuntu/envs/py372/lib/python3.7/site-packages/s3transfer/manager.py(438)_submit_transfer()
-> def _submit_transfer(self, call_args, submission_task_cls,
(Pdb) s
> /home/ubuntu/envs/py372/lib/python3.7/site-packages/s3transfer/manager.py(440)_submit_transfer()
-> if not extra_main_kwargs:
(Pdb) l 440, 10
440 -> if not extra_main_kwargs:
441 extra_main_kwargs = {}
442
443 # Create a TransferFuture to return back to the user
444 transfer_future, components = self._get_future_with_components(
445 call_args)
446
447 # Add any provided done callbacks to the created transfer future
448 # to be invoked on the transfer future being complete.
449 for callback in get_callbacks(transfer_future, 'done'):
450 components['coordinator'].add_done_callback(callback)
好的,现在我们有了s3transfer/futures.py
中定义的TransferFuture
,尚无确切的证据证明线程已经启动,但是当futures介入时,它肯定听起来像。< / p>
(Pdb) l
444 transfer_future, components = self._get_future_with_components(
445 call_args)
446
447 # Add any provided done callbacks to the created transfer future
448 # to be invoked on the transfer future being complete.
449 -> for callback in get_callbacks(transfer_future, 'done'):
450 components['coordinator'].add_done_callback(callback)
451
452 # Get the main kwargs needed to instantiate the submission task
453 main_kwargs = self._get_submission_task_main_kwargs(
454 transfer_future, extra_main_kwargs)
(Pdb) transfer_future
<s3transfer.futures.TransferFuture object at 0x7f178db5a780>
乍看之下,TransferCoordinator
类的以下最后一行似乎很重要:
class TransferCoordinator(object):
"""A helper class for managing TransferFuture"""
def __init__(self, transfer_id=None):
self.transfer_id = transfer_id
self._status = 'not-started'
self._result = None
self._exception = None
self._associated_futures = set()
self._failure_cleanups = []
self._done_callbacks = []
self._done_event = threading.Event() # < ------ !!!!!!
您通常会看到threading.Event
being used for one thread to signal的事件状态,而其他线程可以等待该事件发生。
TransferCoordinator
是used by TransferFuture.result()
。
好吧,从上方绕过去,我们现在位于s3transfer.futures.BoundedExecutor
及其属性max_num_threads
:
class BoundedExecutor(object):
EXECUTOR_CLS = futures.ThreadPoolExecutor
# ...
def __init__(self, max_size, max_num_threads, tag_semaphores=None,
executor_cls=None):
self._max_num_threads = max_num_threads
if executor_cls is None:
executor_cls = self.EXECUTOR_CLS
self._executor = executor_cls(max_workers=self._max_num_threads)
这基本上是equivalent至:
from concurrent import futures
_executor = futures.ThreadPoolExecutor(max_workers=10)
但是仍然存在一个问题:这是“一劳永逸”,还是该调用实际上是等待线程完成并返回?
似乎是后者。 .result()
呼叫self._done_event.wait(MAXINT)
。
# https://github.com/boto/s3transfer/blob/2aead638c8385d8ae0b1756b2de17e8fad45fffa/s3transfer/futures.py#L249
def result(self):
self._done_event.wait(MAXINT)
# Once done waiting, raise an exception if present or return the
# final result.
if self._exception:
raise self._exception
return self._result
最后,要重新运行Victor Val的测试,这似乎证实了上述条件:
>>> import boto3
>>> import time
>>> import io
>>>
>>> buf = io.BytesIO(open('100mb.txt', 'rb').read())
>>>
>>> bucket = boto3.resource('s3').Bucket('test-threads')
>>> start = time.time()
>>> print("starting to upload...")
starting to upload...
>>> bucket.upload_fileobj(buf, '100mb')
>>> print("finished uploading")
finished uploading
>>> end = time.time()
>>> print("time: {}".format(end-start))
time: 2.6030001640319824
(此示例在网络优化的实例上运行,此执行时间可能会更短。但是2.5秒仍然是相当大的时间,根本不表示线程已启动且未等待。)
最后,这是Callback
的{{1}}的示例。随后是文档中的an example。
首先,一个小帮手可以有效地获取缓冲区的大小:
.upload_fileobj()
课程本身:
def get_bufsize(buf, chunk=1024) -> int:
start = buf.tell()
try:
size = 0
while True:
out = buf.read(chunk)
if out:
size += chunk
else:
break
return size
finally:
buf.seek(start)
示例:
import os
import sys
import threading
import time
class ProgressPercentage(object):
def __init__(self, filename, buf):
self._filename = filename
self._size = float(get_bufsize(buf))
self._seen_so_far = 0
self._lock = threading.Lock()
self.start = None
def __call__(self, bytes_amount):
with self._lock:
if not self.start:
self.start = time.monotonic()
self._seen_so_far += bytes_amount
percentage = (self._seen_so_far / self._size) * 100
sys.stdout.write(
"\r%s %s of %s (%.2f%% done, %.2fs elapsed\n" % (
self._filename, self._seen_so_far, self._size,
percentage, time.monotonic() - self.start))
# Use sys.stdout.flush() to update on one line
# sys.stdout.flush()