Девятый бит: Блог кафедры АСОИУ ОмГТУ

RingSync: синхронизируем на полной скорости сети

RingSync demo party

В комментарии к статье про dVHD заморозку системы я намекнул на быстрый способ распространения/синхронизации больших файлов по локальной сети.

Конкретно, в моем случае VHD файл весил примерно 120 GiB, и его нужно было залить на 30 PC. При каждом обновлении VHD он перезаливался. Под катом читайте историю, про то, как с таким большим файлом не справлялся BTSync, и про создание RingSync.

Начну с небольшого QA (Вопрос-Ответа), сравнивая различные схемы обмена данными.

Классика: Server → Client

Q: Что происходит в классическом варианте, когда один сервер раздает файл множеству клиентов?

A: Допустим, что и на сервере и на клиентах установлены обычные HDD (последовательное чтение/запись выполняется быстро, произвольное — медленно).

1 x Server - switch - N x Clients

У нас есть:

  • 1 сервер (файловое хранилище);
  • 4 клиента (загружающие с файлового хранилища один и тот же файл);
  • 1 switch (все порты работают в дуплексном режиме; скорость соединения на всех портах одинаковая).

Еще один важный момент — скорость используемых HDD (при последовательных операциях доступа к данным) выше скорости сетевого подключения.

В этом случае узким местом будет сетевое соединение между сервером и switch’ом. Сервер инициирует 4 потока (по потоку для каждого клиента) передачи данных, которые разделят общую пропускную способность сетевого соединения сервера.

В итоге скорость загрузки упадет в 4 раза, по сравнению со случаем, когда только 1 клиент загружает файл. Чем больше клиентов, тем меньше скорость загрузки у каждого из этих клиентов. Для 30 клиентов и 100 Mbps сети 120 GiB файл будет загружаться минимум 3 дня 13 часов и 54 минуты, т.е. для клиента 100 Mbps превратятся в 3 Mbps :( .

При этом сервер вынужден несколько раз (по количеству клиентов) отправлять одну и ту же часть файла в сеть. Последствия этого можно увидеть, сравнив две ситуации (два граничных условия):

  1. Все клиенты запустили загрузку файла в одно и то же время.
  2. Запуск загрузки не был синхронизирован, например, получился разброс ±10 минут.

Вспомним, что у нас обычные HDD, которым трудно дается произвольный доступ к данным…

В первом случае система сможет кешировать считанную один раз из HDD часть файла (для одного клиента), и будет раздавать оставшимся клиентам эту часть файла уже из кеша (без обращения к HDD). Для HDD это будет выглядеть, как последовательное чтение данных, которое ему легко дается.

Во втором случае клиенты станут обращаться к разным частям, и у системы будет меньше шансов повторно воспользоваться кешированными частями файла. Для HDD это будет выглядеть как произвольный доступ к данным. Например, второй клиент запустил загрузку с 10 минутным отставанием от первого клиента, значит, первый клиент уже успел загрузить 7 GiB, и чтобы система смогла отдавать части файла для второго клиента без повторного обращения к HDD, ей нужен кеш размером в 7 GiB.

В реальности 100% “попадание” в файловый кеш не требуется, например, в кеш можно помещать не только текущую часть файла, но и несколько следующих частей…

P2P: BTSync

Q: В чем заключается польза от использования p2p в локальной сети?

A: Если p2p клиенты используют следующую схему передачи данных:

  1. сид отправляет каждому пиру разную часть файла,
  2. каждый пир пересылает полученную часть файла другому пиру,
  3. сид отправляет каждому пиру разную часть файла, которую сид не отправлял никому ранее,
  4. одновременно с предыдущим пунктом, пиры продолжают обмениваться частями файла,
  5. пункты 3-4 повторяются, пока сид не раздаст все части файла, и все пиры получат эти части

, и все они подключены к одному switch’у, то, теоретически, скорость получения данных пиром не будет уменьшаться с увеличением числа пиров (в классическом варианте Server → Client скорость уменьшалась). Это работает благодаря возможности switch “соединять” любые 2 своих порта, и данные между этими портами смогут передаваться на полной скорости соединения.

Похожий режим работы в BitTorrent клиентах называется Super-seeding.

Q: Какой может быть негативный эффект от использования этой схемы?

A: Так как теперь части файла передаются не последовательно (от начала до конца), а в разброс, то скорость чтения(для сида)/записи(для пира) с/на HDD упадет (последствия произвольного доступа к данным). Поэтому клиенты должны выбирать оптимальный размер части файла, и использовать адаптированный под эту схему работы алгоритм кеширования.

История про BTSync и большой файл

BitTorrent SyncПро BTSync написано уже много статьей ( 1   2   3 ), но самое интересное можно было найти на официальном форуме (он переехал сюда). Вкратце, баги были, и с ними приходилось бороться, например, в некоторых версиях были проблемы с Local Peer Discovery (не всегда работал) — обходилась проблема прописыванием IP одного из пиров в сети. У него есть еще тележка интересных “особенностей”, но сейчас речь пойдет про его работу с большими файлами…

Для развертывания системы на VHD, был собран минимальный WinPE, содержащий:

  • BTSync — запускался через “BTSync.exe /config config.json”;
  • программу ShowWindow, которая используя FindWindow, SendMessage (WinAPI), и переданное ей имя класса окна “BTSync4823DF041B09”, открывает скрытое окно BTSync (он запускается “свернутым в трей”, а трея у нас нет…);
  • драйверы (в основном для NIC);
  • скрипты (startnet.cmd: тюнинг сетевого стека, вызов пред и пост разверточных скриптов).

Всё это происходило в середине 2014 года (или 2013…), и пишется по памяти4.

На сиде BTSync запускался с RW секретом, а на пирах — с RO секретом (с включенной опцией принудительной синхронизации, если файл существует, и отличается от файла у сида).

После трех “разворачиваний” системы по 8-10 PC за раз (скорость сетевого соединения 100 Mbps; PC подключены к одному switch; максимальная скорость HDD была в районе 110 MiBps; размер файла тот же — 120 GiB), собралась следующая “статистика”:

  • после запуска BTSync на всех PC, и начала синхронизации, в районе часа сетевая активность была минимальна, т.е. файл не передавался;
  • на некоторых (1-2) пирах процесс синхронизации прерывался, и приходилась вручную перезапускать BTSync;
  • при подключении через нормальный switch средняя скорость загрузки была 8 MiBps (падала до 6, поднималась до 9) — при отключенном шифровании трафика;
  • при подключении через устройство, очень похожее на ZyXEL ES-116S, средняя скорость загрузки была 3.4 MiBps (иногда поднималась до 6) — шифрование трафика также было отключено.

Эксперимент 1

После этого было решено провести эксперимент: взять 2-3 PC, нормальный  switch, и добавить Process Explorer в WinPE для наблюдения за активностью  BTSync. Судя по описанию проблем на форуме - у каждой версии имелся свой набор “особенностей” (новая версия не значит более лучшая версия), поэтому в эксперименте участвовало несколько версий BTSync (1.0.1341.3.106). После каждого опыта, скаченный пирами файл стирался, и все PC перезагружались.

  • И так, запускаем BTSync на сиде, запускаем на одном пире, и вуаля: сетевая активность появляется сразу же, и файл синхронизируется со скоростью 11 MiBps.
  • Теперь сделаем то же самое, но запустим два пира одновременно: сетевая активность минимальна, у сида наблюдается высокая дисковая активность (BTSync запустил подсчет контрольных сумм частей файла?), и только после уменьшения дисковой активности начинается синхронизация с некоторыми “особенностями” (про них — ниже).
  • А если взять первый вариант, и запустить второго пира посередине синхронизации: синхронизация приостанавливается, и возрастает дисковая активность у сида (вычисляет контрольные суммы?), затем (когда вычислит все контрольные суммы?) синхронизация продолжается (опять же с “особенностями”).

Особенности синхронизации заключались в том, что при каждом новом запуске, схема работы отличалась от предыдущего запуска. Всего получилось 3 схемы работы (сокращения: сид — s, пир1 — p1, пир2 — p2):

1. s --{7 MiBps}--> p1; s --{4.5 MiBps}--> p2; p1 --{7 MiBps}--> p2; p2 --{4.5 MiBps}--> p1; через определенный промежуток времени p1 и p2 меняются местами:
   s --{7 MiBps}--> p2; s --{4.5 MiBps}--> p1; p2 --{7 MiBps}--> p1; p1 --{4.5 MiBps}--> p2; смена происходит с определенным периодом времени;

2. s --{10 MiBps}--> p1; после того, как p1 все загрузит:
   s --{10 MiBps}--> p2;

3. s --{7.5 MiBps}--> p1; s --{3.5 MiBps}--> p2; p1 --{7.5 MiBps}--> p2; p2 --{3.5 MiBps}--> p1; затем:
   s --{4.5 MiBps}--> p2;                        p2 --{4.5 MiBps}--> p1; и возвращение:
   s --{7.5 MiBps}--> p1; s --{3.5 MiBps}--> p2; p1 --{7.5 MiBps}--> p2; p2 --{3.5 MiBps}--> p1; смена происходит с определенным периодом времени.

Причем первая схема работы (самая лучшая) включалась, когда BTSync на обоих пира запускался одновременно. Если же была задержка между запусками, то чаще встречалась схема 3 или 2.

Что касается различий в версиях BTSync, то в опытах с более новыми версиями второй вариант схемы встречался реже. Но это ничего не значит, т.к. испытания каждой версией повторялись всего 4 раза, что очень мало. Версии 1.0.134 и 1.3.106 испытывались большее число раз.

Однако в новых версиях (предположительно начиная с 1.3. x), появилась еще одна “особенность”: когда размер загружаемого файла (можно посмотреть командой dir в консоли) на пире достигает 70% (или около того), рост размера файла приостанавливается на некоторое время.

Эксперимент 2

За основу брался первый эксперимент, с отличиями:

  1. использовалась только последняя версия BTSync;
  2. загруженный пирами файл не удалялся, а изменялось его содержимое.

Эти испытания BTSync провалил — синхронизация с сидом так и не начиналась (опция принудительной синхронизации была включена).

:-( :-(

В повседневном использовании с 1 GiB файлами мне помогала «легкая приостановка» — через контекстное меню “иконки в трее” пира поставить BTSync на паузу, а затем убрать паузу.

RingSync

Во время борьбы с “особенностями” BTSync появились несколько мыслей:

  • большинство проблем с BTSync связаны с теми возможностями, которые в данном случае не нужны;
  • нам не нужно отслеживание изменений в файле, т.к. каждый раз его все равно приходится передавать целиком, и источник всегда один — сид;
  • следовательно, нет необходимости в отдельном этапе подсчитывания контрольных сумм, и определения “кто из пиров обладает более новой версией файла”;
  • у нас всего один файл;
  • HDD попросил сделать последовательное чтение/запись файла 8-O .

С этими мыслями родилась следующая схема:

1 x Seed - switch - N-1 x Peers + 1 x Leech

  1. сид передает часть файла пиру;
  2. пир сохраняет ее на HDD, и параллельно передает следующему пиру (то же самое делают остальные пиры в цепочке);
  3. лич (последний пир) просто сохраняет часть файла на HDD.

Причем это все происходит параллельно (в потоковом режиме) — когда первый пир отправляет часть файла следующему пиру, он уже получает следующую часть файла от сида. А сам файл передается от начала до конца, что очень хорошо для HDD (последовательный доступ к данным).

От switch здесь требуется всего лишь “соединить” Rx одного порта с Tx другого порта на полной скорости (см. схему), с чем он отлично справляется.

Программа на Golang

go, golang, gobot-gopherНа современном языке сделать PoC этой схемы не составит никакого труда. На Golang основную часть программы пира можно записать всего в одну строчку:

io.Copy(io.MultiWriter(outFile, peerConn), seedConn)

, все остальное — обвязка.

Последовательность запуска

Код RingSync написан максимально просто, и при этом в нем предусмотрена возможность запуска сида и пиров в произвольном порядке. Что это означает? Можно одновременно запустить всех пиров и сида, дождаться пока они включатся, а затем запустить лича. Именно лич запускает процесс синхронизации.

Под капотом это выглядит так:

  1. запускаются пиры и лич, и начинают слушать входящие соединения;
  2. запускается лич, и устанавливает соединение с последним пиром;
  3. каждый пир устанавливают соединение с предыдущим пиром (по цепочке);
  4. первый пир устанавливают соединение с сидом, и сид начинает отправку данных.

Если немного пожертвовать простотой, и добавить возможность переподключения при неудачной “установке соединения”, то можно будет и лича, и сида, и пиров запускать в любой последовательности.

Репозиторий

the LabtocatRingSync можно форкнуть здесь.
Там же можно скачать уже собранный для Windows 32bit бинарник.

Q: Почему именно Windows 32bit?
A: Потому, что я его использовал с WinPE 32bit. К тому же, вместе с RingSync лежит специальный launcher, который облегчает конфигурирование после сетевой загрузки. Launcher считывает конфиг из файла (одного и того же для всех PC), и в зависимости от MAC-адреса первого сетевого интерфейса PC, настраивает этот сетевой интерфейс, и запускает RingSync с нужными параметрами. Сейчас launcher завязан на использование netsh, поэтому результат кросс-компиляции под не Windows работать не будет. В отличие от launcher, у самого RingSync таких проблем нет :-)

Опции запуска RingSync приводить не буду — их можно увидеть, просто запустив его без опций. А вот примеры для разных режимов (сид, пир, лич) будут полезны.

Сид (слушает порт 5001, ожидает подключения от лича/пира с IP “192.168.0.2”, рассылает содержимое файла “big_file.vhd”):

RingSync -mode=seed -port=5001 ^
         -leech=192.168.0.2 -if=big_file.vhd

Пир (слушает порт 5001, ожидает подключения от лича/пира с IP “192.168.0.3”, подключается к порту 5001 сида/пира с IP “194.168.0.1”, записывает полученные данные в файл “big_file.vhd”):

RingSync -mode=peer -port=5001 ^
         -leech=194.168.0.3 ^
         -seed=194.168.0.1:5001 -of=big_file.vhd

Лич (подключается к порту 5001 сида/пира с IP “194.168.0.1”, записывает полученные данные в файл “big_file.vhd”):

RingSync -mode=leech ^
         -seed=194.168.0.2:5001 -of=big_file.vhd

Сборку этих же строк можно увидеть в исходниках launcher’а.

TCP Congestion Control

До написания кода RingSync была идея собрать свой TCP-велосипед, в котором данные передавались бы по цепочке от сида -> через пиров -> к личу, а ответ (ACK), подтверждающий получение данных, отправлял бы лич к сиду. Причем, если сида переименовать в “сервер”, лича — в “клиент”, а пиров в “маршрутизаторы”, то получится типичная схема передачи данных в Интернете. Вот только маршрутизаторы не сохраняют переданные через себя данные. В этот TCP-велосипед, можно было бы включить наиболее подходящий для данных условий передачи данных TCP Congestion Avoidance Algorithm ( 1   2   3 ), а не полагаться на системный TCP Congestion Control и его настройки. А в будущем можно было бы получить больший контроль над передачей данных, например, добавив возможность отправки сообщений о “заторах” в сети или HDD.

Но был и более легкий путь — использовать стандартный системный TCP. В этом случае пир, получивший данные, сам отправляет подтверждение (об успешной доставке данных) пиру, отправившему эти данные. И информация о случившимся “заторе” автоматически передается через цепочку пиров к сиду:

  1. вначале ближайший к “затору” пир, не получив вовремя подтверждение, сбрасывает скорость, и его буфер на прием данных (входной буфер) начинает наполняться;
  2. при переполнении буфера, предыдущий в цепочке пир также перестает получать подтверждения вовремя, и начнет сбрасывать скорость;
  3. то же самое делают остальные пиры, вплоть до сида.

Это напоминает движение длинного состава (локомотив-сид тянет спереди) с большим числом вагонов (пиров), и пружиной (буфером) вместо сцепки.

На самом деле второй вариант даже лучше, т.к. для некоторых алгоритмов TCP Congestion Avoidance (косвенно это влияет и на остальных) чем меньше RTT — тем лучше. А в первом варианте мы заведомо создаем более неблагоприятные условия…

В итоге, второй вариант, даже в сети с двумя switch:

switch{1 Gbps}[сид -> пир -> пир]
└─{100 Mbps}—↓
switch{100 Mbps}[пир -> пир -> пир -> лич]

показал стабильные 11.8 MiBps.

PC подключены к разным switch

В сети с несколькими switch узким местом может стать соединение между ними. Например, если строить цепочку пиров в произвольном порядке, то при сети:

switch1{1 Gbps}[сид, пир2, пир4, пир6]
└─{1 Gbps}─┐
switch2{1 Gbps}[пир1, лич, пир3, пир5]

, и цепочке:

сид->пир1->пир2->пир3->пир4->пир5->пир6->лич

, скорость упадет в 4 раза.

Q: Почему скорость падает в 4 раза? Как получилось это число “4”?
A: Если возьмем все потоки данных в направлении от switch1 к switch2, то получим:

  1. сид  -> пир1
  2. пир2 -> пир3
  3. пир4 -> пир5
  4. пир6 -> лич

Теперь посмотрим на потоки данных в обратном направлении (от switch2 к switch1):

  1. пир2 <- пир1
  2. пир4 <- пир3
  3. пир6 <- пир5

В направлении от switch1 к switch2, общий канал в 1 Gbps делили 4 потока, получая максимум 0.25 Gbps на поток. В обратном направлении получилось 3 потока, соответственно максимум 0.33 Gbps на поток. Самым узким местом стал канал от switch1 к switch2 (максимальная скорость потока 0.25 Gbps). В самом узком месте скорость потока в 4 раза меньше, по сравнению с максимальной скорости в “самом широком месте” (1 Gbps).

Q: Как исправить ситуацию?
A: Достаточно строить цепочку примерно так (можно сразу перейти к TL;DR) 8-) :

  1. добавить в цепочку сида;
  2. взять switch, к которому подключен сид, и добавить в цепочку всех пиров (в любом порядке), подключенных к этому switch;
  3. взять следующий, ранее не взятый, switch (к которому подключены пиры), подключенный к предыдущему switch (возможно не прямое подключение, а подключение через несколько промежуточных switch, к которым не подключен ни один пир), и добавить в цепочку всех пиров (в любом порядке), подключенных к этому switch;
  4. повторять предыдущий пункт, пока он будет выполним (“следующий switch” будет существовать);
  5. если можно вернуться к предыдущему switch, то вернуться к нему, и выполнить предыдущий пункт;
  6. последнего пира в цепочке назвать личем.

TL;DR: используем обход графа в глубину, при этом:

  • вершины — это switch;
  • в вершине хранится список пиров, подключенных к этому switch/вершине;
  • начинаем со switch/вершины, к которому подключен сид;
  • при входе в вершину, добавляем в цепочку всех пиров из списка этой вершины.

Для использования этого способа нужна информация о топологии сети на канальном уровне.

Ссылки «в тему»

Как обойтись без копирования большого VHD файла по сети?

На самом деле можно не копировать каждый раз полный VHD при обновлении системы, а распространять только dVHD с обновлением. Такая схема обновления работает следующим образом (система обновляется на одном Master-PC, и копируется на множество Slave-PC):

  • На Master-PC все изменения, в ходе обновления системы, сохраняются в новый dVHD файл.
  • Все Slave-PC должны хранить оригинальный VHD файл с Master-PC, поэтому на них нужно создавать дополнительный dVHD файл, в котором будут сохранены все отличия Slave-PC от Master-PC. Без этого следующий пункт работать не будет.
  • На Slave-PC копируется dVHD файл с обновлением, и, затем, объединяется с VHD файлом (если бы VHD файл на Master-PC и Slave-PC отличался, то объединение завершилось бы с ошибкой).
  • На каждом Slave-PC заново создается тот дополнительный dVHD файл, хранящий отличия каждого Slave-PC от Master-PC.
  • На Master-PC dVHD файл с обновлением системы также объединяется с VHD файлом.

При этом время распространения обновления будет включать в себя время копирования обновления на Slave-PC + время объединения VHD с dVHD. Здесь важно, чтобы объединение выполнялось быстро, иначе суммарное время (копирование dVHD + объединение) может превысить время простого копирования цельного VHD файла. Еще один минус – из-за появления на Slave-PC дополнительного dVHD файла, система будет работать медленней.

   

You must be logged in to post a comment.