О синхронизации данных в Linux

Когда дома есть в наличии большой компьютер и маленький, может возникнуть необходимость держать некоторые данные и там, и там. Причем, держать синхронизированно. Лично я, например, регулярно пользуюсь мега-программой для заметок Basket, и конечно мне хотелось бы, чтобы все заметки были доступны и на нотбуке, и на десктопе.

В Линуксе для решения задачи синхронизации есть все необходимое. В принципе, есть и готовые программы для синхронизации, но я считаю, что для моего случая достаточно rsync, md5sum и небольшого скрипта на bash. Ну и сетевого раздела NFS — чтобы через него синхронизироваться. Далее о том, как все делается.

Синхронизацию я провожу через сетевой раздел NFS, открытый на десктопе. В openSUSE, которая у меня установлена везде, поднять раздел NFS — дело трех кликов в yast2. Стоит однако заметить, что если на нотбуке используется Network Manager, имеет смысл немного изменить запись в /etc/fstab. Дело в том, что Network Manager подключает WiFi только после логина и загрузки оконного менеджера (KDE, в моем случае), а монтирование происходит раньше. Естественно, ввиду отсутствия доступа к сети, раздел NFS смонтирован не будет, да к тому же загрузка замедлится на несколько минут в ожидании ответа от сервера. Поэтому строчку, отвечающую за нужный раздел в /etc/fstab лучше подредактировать, добавив ключи noauto и users:

:<...>/Share  <...>/Share      nfs     users,noauto,rw 0 0

Ключ noauto отключает автоматическое монтирование раздела при загрузке, а users — разрешает простым смертным (а не только root-у) монтировать этот раздел по желанию. После этого достаточно (для KDE) в ~/.kde4/Autostart/ добавить шелл-скрипт:

#!/bin/sh
mount PATH/TO/Share

и выставить ему права на исполнение. Тогда при загрузке NFS-раздел будет монтироваться уже после старта KDE.

Теперь обозначу основные особенности рассматриваемого случая. Basket хранит каждую заметку в отдельном файле, корзины с заметками — это директории, причем вложенные корзины — это вложенные директории. Синхронизировать нужно, соответственно:

  • группу файлов;
  • с соблюдением вложенности директорий;
  • с поддержкой удаления файлов;
  • с пересылкой только изменившейся информации (файлов может быть более 9000!).

Для этого очень хорошо подходит консольное приложение rsync. Строчка

rsync -arz --progress --delete $LOCAL_DIR $SRV_DIR >> $LOG_FILE

проводит синхронизацию из директории $LOCAL_DIR в $SRV_DIR с удалением файлов и записью информации о ходе пересылки в файл $LOG_FILE. В принципе на этом можно было бы и остановиться, но к сожалению эта строчка сама по себе едва ли годится для автоматической синхронизации. Дело в том, что команда хотя и проводит синхронизацию как надо, но при этом не подразумевает никакого решения вопроса «что новее — источник или приемник». То есть пользователь должен решить это сам, что конечно исключает автоматизм.

То есть нужно автоматически принимать решение, где данные новее — на сервере, или в локальной копии — и уже на основании этого вывода запускать rsync. Алгоритм придумывается простой, на основе хэша синхронизируемых данных:

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

Под сервером здесь подразумевается, конечно, раздел NFS.

Для вычисления хэша синхронизируемой директории используются команды:

cd $LOCAL_DIR
find ./ -type f -print0 | sort -z | xargs -0 md5sum | md5sum

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

Это, в общем-то, все. Имеет смысл отметить недостатки получившейся системы. Их я вижу два:

  1. Синхронизация очевидно не имеет средств разрешения конфликтов (когда данные изменились и локально, и на сервере). Но на это она и не рассчитана, я ведь один и не буду скакать по компьютерам и менять все подряд.
  2. Из-за сложности вычисления хэша, синхронизация будет очень долго работать по директориям с большими объемами данных (фото, видео, аудио). Это проблема, ага.

Финальный скрипт:

#!/bin/sh
#файл для лога синхронизации
LOG_FILE="$HOME/logs/sync.log"
#путь к серверной копии данных
SRV_DIR="$HOME/Share/baskets/"
#путь к локальной копии данных
LOCAL_DIR="$HOME/.kde4/share/apps/basket/baskets/"
#файл с хэшем сервера
SRV_HASH_FILE="$HOME/Share/.basket_hash"
#файл с локальной копией хэша сервера
LOCAL_SRV_HASH_FILE="$HOME/.basket_hash_local"
#файлы с хэшем должны быть _вне_ синхронизируемых директорий!
#читаем хэши
SRV_HASH=$(cat $SRV_HASH_FILE)
SRV_LOCAL_HASH=$(cat $LOCAL_SRV_HASH_FILE)
echo 'Начало синхронизации' > $LOG_FILE
echo "серверный хэш = $SRV_HASH" >> $LOG_FILE
echo "локальная копия серверного хэша = $SRV_LOCAL_HASH" >> $LOG_FILE
# сравниваем хэши
if [ "$SRV_HASH" == "$SRV_LOCAL_HASH" ]; then
  echo "хэш на сервере и его локальная копия совпадают" >> $LOG_FILE
  #сравниваем хэши клиента и сервера
  cd $LOCAL_DIR
  LOCAL_HASH=$(find ./ -type f -print0 | sort -z | xargs -0 md5sum | md5sum);
  echo "хэш клиента = $LOCAL_HASH" >> $LOG_FILE
  if [ "$LOCAL_HASH" != "$SRV_HASH" ]; then
    echo "хэш клиента не совпадает с сервером, засылаем данные" >> $LOG_FILE
    rsync -arz --progress --delete $LOCAL_DIR $SRV_DIR >> $LOG_FILE
    #обновляем серверный хэш
    cd $SRV_DIR
    find ./ -type f -print0 | sort -z | xargs -0 md5sum | md5sum > $SRV_HASH_FILE
    cp $SRV_HASH_FILE $LOCAL_SRV_HASH_FILE
    echo "новый серверный хэш = $(cat $SRV_HASH_FILE)" >> $LOG_FILE
  fi
else
  echo "хэш на сервере не совпал с локальной копией, скачиваем данные" >> $LOG_FILE
  rsync -arz --progress --delete $SRV_DIR $LOCAL_DIR >> $LOG_FILE
  # скачиваем серверный хэш
  cp $SRV_HASH_FILE $LOCAL_SRV_HASH_FILE
  #выводим хэши
  echo "копия серверного хэша = $(cat $LOCAL_SRV_HASH_FILE)" >> $LOG_FILE
  #для отладки выводим новый клиентский хэш
  cd $LOCAL_DIR
  echo "новый хэш клиента = $(find ./ -type f -print0 | sort -z | xargs -0 md5sum | md5sum)" >> $LOG_FILE
fi
echo 'Синхронизация завершена!' >> $LOG_FILE

Просто отредактируйте пути в начале скрипта, положите его в ~/bin, выдайте права на запуск и добавьте, например, в cron на всех машинах, участвующих в синхронизации. И все.

Кстати. Перед установкой обязательно бэкапните синхронизируемые данные. Мало ли что.

Комментарии