Копирование одного файла на несколько удаленных хостов параллельно через SFTP
Я хочу использовать Python для копирования локального файла до нескольких удаленных хостов параллельно. Я пытаюсь сделать это с asyncio
и Paramiko, так как я уже использую эти библиотеки для других целей в своей программе.
Я использую BaseEventLoop.run_in_executor()
по умолчанию ThreadPoolExecutor
, который фактически является новым интерфейсом к старому threading
библиотека, а также функция SFTP Paramiko для копирования.
вот упрощенный пример как.
import sys
import asyncio
import paramiko
import functools
def copy_file_node(
*,
user: str,
host: str,
identity_file: str,
local_path: str,
remote_path: str):
ssh_client = paramiko.client.SSHClient()
ssh_client.load_system_host_keys()
ssh_client.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
ssh_client.connect(
username=user,
hostname=host,
key_filename=identity_file,
timeout=3)
with ssh_client:
with ssh_client.open_sftp() as sftp:
print("[{h}] Copying file...".format(h=host))
sftp.put(localpath=local_path, remotepath=remote_path)
print("[{h}] Copy complete.".format(h=host))
loop = asyncio.get_event_loop()
tasks = []
# NOTE: You'll have to update the values being passed in to
# `functools.partial(copy_file_node, ...)`
# to get this working on on your machine.
for host in ['10.0.0.1', '10.0.0.2']:
task = loop.run_in_executor(
None,
functools.partial(
copy_file_node,
user='user',
host=host,
identity_file='/path/to/identity_file',
local_path='/path/to/local/file',
remote_path='/path/to/remote/file'))
tasks.append(task)
try:
loop.run_until_complete(asyncio.gather(*tasks))
except Exception as e:
print("At least one node raised an error:", e, file=sys.stderr)
sys.exit(1)
loop.close()
проблема, которую я вижу, заключается в том, что файл копируется последовательно на хосты, а не параллельно. Поэтому, если копирование занимает 5 секунд для одного хоста, оно занимает 10 секунд для двух хостов и так далее.
Я пробовал различные другие подходы, включая канаву SFTP и трубопроводы файла в dd
на каждом из удаленных хостов через exec_command()
, но копии всегда происходит последовательно.
Я, вероятно, неправильно понимаю некоторые основные идея здесь. Что мешает различным потокам копировать файл параллельно?
из моего тестирования кажется, что задержка происходит на удаленной записи, а не при чтении локального файла. Но почему это так, поскольку мы пытаемся выполнить сетевой ввод-вывод против независимых удаленных хостов?
2 ответов
нет ничего плохого в использовании ввода-вывода.
чтобы доказать это, давайте попробуем упрощенную версию вашего скрипта-нет парамико, просто чистый Python.
import asyncio, functools, sys, time
START_TIME = time.monotonic()
def log(msg):
print('{:>7.3f} {}'.format(time.monotonic() - START_TIME, msg))
def dummy(thread_id):
log('Thread {} started'.format(thread_id))
time.sleep(1)
log('Thread {} finished'.format(thread_id))
loop = asyncio.get_event_loop()
tasks = []
for i in range(0, int(sys.argv[1])):
task = loop.run_in_executor(None, functools.partial(dummy, thread_id=i))
tasks.append(task)
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
С двумя потоками, это будет печать:
$ python3 async.py 2
0.001 Thread 0 started
0.002 Thread 1 started <-- 2 tasks are executed concurrently
1.003 Thread 0 finished
1.003 Thread 1 finished <-- Total time is 1 second
этот параллелизм масштабируется до 5 потоков:
$ python3 async.py 5
0.001 Thread 0 started
...
0.003 Thread 4 started <-- 5 tasks are executed concurrently
1.002 Thread 0 finished
...
1.005 Thread 4 finished <-- Total time is still 1 second
если мы добавим еще один поток, мы достигнем предела пула потоков:
$ python3 async.py 6
0.001 Thread 0 started
0.001 Thread 1 started
0.002 Thread 2 started
0.003 Thread 3 started
0.003 Thread 4 started <-- 5 tasks are executed concurrently
1.002 Thread 0 finished
1.003 Thread 5 started <-- 6th task is executed after 1 second
1.003 Thread 1 finished
1.004 Thread 2 finished
1.004 Thread 3 finished
1.004 Thread 4 finished <-- 5 task are completed after 1 second
2.005 Thread 5 finished <-- 6th task is completed after 2 seconds
все идет как ожидалось, и общее время растет по 1 секунде на каждые 5 предметов. Магическое число 5 задокументировано в ThreadPoolExecutor docs:
изменено в версии 3.5: If max_workers is
None
или не задано, по умолчанию будет указано количество процессоров на машине, умноженное на5
, предполагая, что ThreadPoolExecutor часто используется для перекрытия ввода-вывода вместо работы процессора, и количество работников должно быть выше, чем количество работников для ProcessPoolExecutor.
как сторонняя библиотека может заблокировать мой ThreadPoolExecutor?
библиотека использует какой-то глобальный замок. Это означает, что библиотека не поддерживает многопоточность. Попробуйте использовать ProcessPoolExecutor, но с осторожностью: библиотека может содержать другие анти-шаблоны, такие как использование того же жесткого временного имени файла.
функция выполняется в течение длительного времени и не освобождает GIL. Это может укажите ошибку в коде расширения C, но самой популярной причиной проведения GIL является выполнение некоторых вычислений с интенсивным процессором. Опять же, вы можете попробовать ProcessPoolExecutor, так как он не зависит от GIL.
ничего из этого не должно произойти с библиотекой, такой как paramiko.
как сторонняя библиотека может заблокировать мой ProcessPoolExecutor?
обычно не может. Ваши задачи выполняются в отдельных процессах. Если вы видите, что две задачи ProcessPoolExecutor занимает в два раза больше времени, подозревая узкое место ресурса (например, потребляя 100% пропускной способности сети).
Я не уверен, что это лучший способ подойти к нему, но это работает для меня
#start
from multiprocessing import Process
#omitted
tasks = []
for host in hosts:
p = Process(
None,
functools.partial(
copy_file_node,
user=user,
host=host,
identity_file=identity_file,
local_path=local_path,
remote_path=remote_path))
tasks.append(p)
[t.start() for t in tasks]
[t.join() for t in tasks]
на основе комментария, добавил метку данных и захватил вывод из многопроцессорной обработки и получил следующее:
2015-10-24 03:06:08.749683[vagrant1] Copying file...
2015-10-24 03:06:08.751826[basement] Copying file...
2015-10-24 03:06:08.757040[upstairs] Copying file...
2015-10-24 03:06:16.222416[vagrant1] Copy complete.
2015-10-24 03:06:18.094373[upstairs] Copy complete.
2015-10-24 03:06:22.478711[basement] Copy complete.