Вопрос или проблема
Как ускорить повторную синхронизацию большого файла (>3 ТБ), в котором изменилось несколько его блоков (< 1 ГБ) с помощью rsync.
Насколько мне известно, rsync будет выполнять сравнение контрольных сумм между источником и назначением блоков, чтобы найти различия, а затем синхронизировать их.
Существует ли способ увеличить параллелизм на этапе сравнения контрольных сумм, чтобы ускорить синхронизацию?
На самом деле нет!
rsync
, как вы, вероятно, заметили, действительно выполняет хеширование блоков, всё самостоятельно.
Он может использовать различные хеши, но на любой достаточно современной комбинации версий сервера и клиента rsync это будет xxh128
, xxh3
или классический xxhash
(xxh64
). Бенчмарки показывают, что они работают со скоростью примерно 30 ГБ/с на достаточно современном процессоре. Это быстрее, чем 8× линии PCIe 5.0 (и мне не известно о каком-либо не дата-центровом решении, которое могло бы достичь такой скорости).
Другими словами, проверка контрольных сумм блоков не имеет смысла, чтобы делать более параллельной – получение данных работает быстрее, если вы просто читаете последовательно из вашего хранилища (или массива хранилищ), поскольку именно это предзагрузка будет оптимизировать, в любом случае. Ваш CPU достаточно быстрее, чем ваше хранилище в этом аспекте.
Вы упоминаете сравнение контрольных сумм как узкое место, это происходит в исходном коде здесь, с основной частью вычислений, вероятно, происходящей в hash_search
функции.
Там нет ничего настраиваемого, насколько я могу судить, кроме updating_basis_file
(что является параметром --inplace
, упомянутым в комментариях), и размера блока; я думаю, это немного замедляет сравнения, но я не уверен. Поиск относительно прост:
- вы вычисляете контрольную сумму для каждого блока (например, используя
xxh128
), - вы вычисляете MD4-хеш контрольной суммы, чтобы поддерживать размер хеш-таблицы под контролем
- вы проверяете, видели ли вы тот же хеш раньше
- если да, вы на самом деле возвращаетесь и проверяете (с гораздо более низкой вероятностью коллизии) оригинальные контрольные суммы (вместо того, чтобы просто их хеши). Если они совпадают, это блок, который вам не нужно передавать!
Итак, на самом деле не так много, что вы могли бы сделать в кратчайшие сроки, чтобы увеличить скорость в этом аспекте; вы должны в основном сталкиваться с ограничениями пропускной способности оперативной памяти!
Но, безусловно, экспериментируйте и увеличьте размер блока (--block-size
, это в байтах) до чего-то большего, чем 217 B (128 кБ) по умолчанию для больших файлов. Вы можете передавать больше, чем вам нужно – но это ускорит поиск.
Следующая идея по решению проблемы.
В настоящее время на каждой синхронизации:
- сканирование полной контрольной суммы источника
- сканирование полной контрольной суммы цели
- передача различий
Таким образом, в зависимости от обсуждения ввода/вывода, если ввод/вывод 1) быстрее, чем ввод/вывод 2) Может возникнуть необходимость попробовать избежать сканирования (2)), сохраняя метаданные контрольных сумм для следующего выполнения.
Что вам нужно в этом случае, так это контрольная сумма (возможно, написанная самостоятельно), которая выполняет на одном потоке ввода-вывода контрольное суммирование блоками и полное, например:
- 3TB хеш
- a. 1.5Tb хеш
- b. 1.5TB хеш
- a. 750GB хеш
- b. 750GB хеш
- c. 750GB хеш
- d. 750GB хеш
…
до приемлемого окна изменения, например, 8k.
- a. 8k хеш
- b. 8k хеш..
- aaaaa. 8k хеш
который вы будете использовать для сравнения в древовидной структуре с последним запуском (целью) и, следовательно, знать, какие ветви в дереве вам нужно следовать до самого маленького окна изменения.
Этот список вы бы скопировали и использовали метаданные хешей в качестве источника для следующей синхронизации.
Ещё лучше было бы, если бы вы знали записи в источнике, так что повторное сканирование было бы ограничено этими элементами ветви, если это вообще будет нужно.
Альтернатива
Это вы бы получили бесплатно, если бы изменили систему хранения на что-то вроде zfs, создавая снимки и затем выполняя блочные инкрементные отправки и получения изображений, которые на самом деле представляют только измененные диапазоны в источнике.
Извините, что это лучшее, что приходит мне в голову.
Обновление:
Поскольку я только начинаю изучать python, я попробовал и узнал, что если объединить сканер и сравнение хешей, нет необходимости в древовидной структуре.
time python3 ./filehasher.py /export/disk-1/zfs.cache.raw
Namespace(inputfile="/export/disk-1/zfs.cache.raw", min_chunk_size=8192)
os.stat_result(st_mode=33188, st_ino=12, st_dev=2065, st_nlink=1, st_uid=0, st_gid=0, st_size=2147479552, st_atime=1728674135, st_mtime=1728674159, st_ctime=1728674159)
Файл размером в байтах составляет 2147479552
Файл размером в байтах составляет 31 exp 2147483648 min_chunk_exp 13
- загружаем старые хеши...
Нам нужно 1048574 хеша для этого файла.
кусок 512 из 0.20%
различие 13:512 o 124a33e4488e3b10a5434f24789173fad96014a4cc46856289b2b7f04aa41538 != n ea42ae1df2ce719035f0c51e3369ebe4e6b8b9dd9c44e2b60c3084a0efeedbcc
кусок 145991 из 55.69%
различие 13:145991 o a9198b4af91db0d5eeb4ad53d1c41302952071c3926713f74558c83d0e901f1e != n 0abfad615307b69f406d089104c0c12c05d197c3e3d9b072894780b9b1c93f88
кусок 145992 из 55.69%
различие 13:145992 o c89ae8a8c82edc9d2045eaad344887ae00878f6066fa3893cd42e7f9fa02c5b4 != n 4a9586bca78224d2c7343abcccd0e2138e2a416d806be72788cef3cdd84e1a2f
кусок 145993 из 55.69%
различие 13:145993 o f882619b014bc284101769ab9d2dc3b017f3fa9873a352a566f03fcb805aee1b != n 97f1aeb56c68be8b524aa480636d6987a75c88cc93f941b8cbe76043d108eabe
кусок 145994 из 55.69%
...сокращено...
различие 13:146274 o 10b911c4ccc35dfc91f5025eee0b17fd96e9ea7a8c38896e2bda824ec3a6d618 != n 1946f829b181b95cd95c9071f70a300700f8d3e25ac6e00758f7779e99e1bdde
кусок 146275 из 55.80%
различие 13:146275 o c201a9773aba93789a7c0191901b5a0cb04a4eafb90ef6bd225bb89ebc0d0294 != n f7bc8527c8585f680a32940c72ff0d300a95e2d6ddf0341d5892133c225592b8
кусок 146276 из 55.80%
различие 13:146276 o c311d18dfe474cb60aaaf63ecb20b70a7a8a4959a30e986661b79b2ceab8d50c != n d823513af052881feae29e5706c687051b3f54fa894a3b97b94e67eac2594adc
кусок 262143 из 100.00%
запись хешей...
реально 0m25.236s
пользователь 0m14.316s
системный 0m4.296s
Код выглядит так: (пожалуйста, учтите, что поддержки нет 🙂 )
import argparse
parser = argparse.ArgumentParser("filehasher")
parser.add_argument("inputfile", help="файл, который должен быть захеширован.", type=str)
parser.add_argument("--min-chunk-size", help="самый маленький кусок для хеширования.", type=int, default=8192)
args = parser.parse_args()
print (args)
import os
file_stats = os.stat(args.inputfile)
print(file_stats)
print(f'Файл размером в байтах составляет {file_stats.st_size}')
import math
two_exp=math.log(file_stats.st_size,2)
if two_exp-int(two_exp) > 0:
two_exp=int(two_exp)+1
else:
two_exp=int(two_exp)
two_exp_mchung=int(math.log(args.min_chunk_size,2))
print(f'Файл размером в байтах составляет {two_exp} exp {2**two_exp} min_chunk_exp {two_exp_mchung}')
sum_hashes=0
multi=2
hash_obj={}
hash_max_size={}
hash_max_current={}
hash_obj_idx={}
import pickle
#args.inputfile+".pickle"
pickle_filename="./hash.pickle."+str(args.min_chunk_size)
old_hash=False
if os.path.isfile(pickle_filename):
print("- загружаем старые хеши...")
old_hash=True
with open(pickle_filename, 'rb') as handle:
old_hash_obj = pickle.load(handle)
import hashlib
for idx in range(two_exp,two_exp_mchung - 1,-1):
# print(f"{idx} needs {multi} hashes")
hash_obj_idx[idx]=0
hash_obj[idx]={}
hash_obj[idx][hash_obj_idx[idx]]=hashlib.sha256(b"")
hash_max_size[idx]=2**idx
hash_max_current[idx]=2**idx
sum_hashes=sum_hashes+multi
multi=multi*2
print(f"Нам нужно {sum_hashes} хешей для этого файла.")
def read_in_chunks(file_object, chunk_size=1024):
"""Ленивая функция (генератор) для чтения файла кусками.
Размер куска по умолчанию: 1k."""
while True:
data = file_object.read(chunk_size)
if not data:
break
#print(f"чтение {len(data)}")
yield bytes(data)
def hashing_tree(data):
data_len=len(data)
#print(f"хеширование {data_len}")
for idx in range(two_exp,two_exp_mchung - 1,-1):
hash_obj[idx][hash_obj_idx[idx]].update(data)
hash_max_current[idx]=hash_max_current[idx]-data_len
#print(f"обновление хеша exp = {idx}...{hash_max_size[idx]} - остаток в idx {hash_max_current[idx]}")
if hash_max_current[idx] == 0:
#print(f"индекс цикл {idx} от {hash_obj_idx[idx]} до {(hash_obj_idx[idx]+1)}")
# цикл индекса
# 1. сброс до максимума
hash_max_current[idx]=hash_max_size[idx]
# 2. увеличение индекса
hash_obj_idx[idx]=hash_obj_idx[idx]+1
hash_obj[idx][hash_obj_idx[idx]]=hashlib.sha256(b"")
def hashing_flat(data):
data_len=len(data)
#print(f"хеширование {data_len}")
idx=two_exp_mchung
hash_obj[idx][hash_obj_idx[idx]].update(data)
hash_max_current[idx]=hash_max_current[idx]-data_len
#print(f"обновление хеша exp = {idx}...{hash_max_size[idx]} - остаток в idx {hash_max_current[idx]}")
if hash_max_current[idx] == 0:
#print(f"индекс цикл {idx} от {hash_obj_idx[idx]} до {(hash_obj_idx[idx]+1)}")
# цикл индекса
# 1. сброс до максимума
hash_max_current[idx]=hash_max_size[idx]
# 2. увеличение индекса
# 2.1 преобразование объекта в строку для сериализации
hash_obj[idx][hash_obj_idx[idx]]=hash_obj[idx][hash_obj_idx[idx]].hexdigest()
if old_hash:
if old_hash_obj[idx][hash_obj_idx[idx]] == hash_obj[idx][hash_obj_idx[idx]]:
pass
#print("")
#print(f"хорошо {idx}:{hash_obj_idx[idx]}")
else:
print("")
print(f"различие {idx}:{hash_obj_idx[idx]} o {old_hash_obj[idx][hash_obj_idx[idx]]} != n {hash_obj[idx][hash_obj_idx[idx]]}")
# 2.2 продолжаем...
hash_obj_idx[idx]=hash_obj_idx[idx]+1
hash_obj[idx][hash_obj_idx[idx]]=hashlib.sha256(b"")
chk=int(0)
max_chk=int(file_stats.st_size/args.min_chunk_size)
with open(args.inputfile,"rb") as f:
for piece in read_in_chunks(f,args.min_chunk_size):
print(f"кусок {chk} из {(chk*100/max_chk):3.2f}% ", end="\r")
hashing_flat(piece)
chk=chk+1
#break
# process_data(piece)
# преобразование последнего элемента
for idx in range(two_exp,two_exp_mchung - 1,-1):
hash_obj[idx][hash_obj_idx[idx]]=hash_obj[idx][hash_obj_idx[idx]].hexdigest()
#import json
#print(json.dumps(hash_obj))
#exit(0)
print("")
print("запись хешей...")
with open(pickle_filename, 'wb') as handle:
pickle.dump(hash_obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
#print(a == b)
.
Ответ или решение
Чтобы ускорить повторную синхронизацию больших файлов (более 3 ТБ) с использованием инструмента rsync
, необходимо рассмотреть несколько ключевых факторов, связанных с архитектурой и алгоритмом работы rsync
. В данной статье будут изучены оптимизации, которые можно рассмотреть для быстрого выполнения этой задачи, а также проблемы, с которыми может столкнуться пользователь.
Проблема повторной синхронизации
При использовании rsync
для синхронизации больших файлов, которые претерпели изменения в пределах нескольких блоков (менее 1 ГБ), rsync
должен сначала выполнить сравнение контрольных сумм (checksums) блоков, чтобы выявить различия между исходным и целевым файлами. Алгоритм rsync
разбивает файл на блоки и использует контрольные суммы для определения тех блоков, которые нуждаются в передаче.
В процессе проверки контрольных сумм система выполняет следующие этапы:
- Полное сканирование контрольной суммы исходного файла.
- Полное сканирование контрольной суммы целевого файла.
- Передача изменённых блоков.
Таким образом, производительность может серьёзно зависеть от скорости выполнения операций ввода-вывода и вычислений для криптографических хешей, таких как xxHash
, которые rsync
использует для назначения контрольных сумм.
Оптимизации
Увеличение размера блока
Одним из простых способов увеличить скорость обработки является изменение размера блока, используемого rsync
, с помощью параметра --block-size
. Установка более крупного размера блока может уменьшить количество необходимых контрольных сумм, а следовательно, также снизить затраты на вычисления. Рекомендуется попробовать увеличить размер блока до значений, превышающих 128 КБ (например, 1 МБ или более).
rsync --block-size=1048576 source_file target_file
Хранение контрольных сумм
Для ускорения повторной синхронизации можно рассмотреть возможность хранения метаданных о контрольных суммах между запусками синхронизации. Это может быть выполнено с помощью скрипта, который будет выполнять послепередачную проверку контрольных сумм, а затем сохранять их в файле. При следующей синхронизации не нужно будет повторно вычислять контрольные суммы для целевого файла.
Параллельная обработка
Хотя rsync
не поддерживает параллельное вычисление контрольных сумм, можно использовать сторонние инструменты или собственный скрипт для предварительного хеширования файла в несколько потоков, а затем передавать только изменённые куски, основываясь на вычисленных хешах.
Пример реализации на Python может помочь с таким подходом, как показано в коде выше, где с помощью метода hashing_tree
можно выполнить предварительное хеширование, сохраняя результаты для дальнейших сравнений.
Альтернативные решения
Если вышеуказанные методы не обеспечивают необходимой производительности, можно рассмотреть использование более современных решений для хранения, например, файловых систем, таких как ZFS, которые поддерживают снимки и позволяют выполнять инкрементальные передачи. При использовании ZFS можно воспользоваться командами zfs send
и zfs receive
для передачи инкрементальных изменений, которые эффективно работают с изменёнными блоками без необходимости полного перекрёстного сравнения.
Заключение
При работе с большими файлами в rsync
важно оптимизировать процесс для максимальной эффективности. Увеличение размера блока, сохранение контрольных сумм и использование современных файловых систем — все это составные части успешной стратегии синхронизации. Если вы столкнулись с проблемами производительности, рекомендуется протестировать каждое из предложенных решений, чтобы найти наиболее подходящий вам вариант.