Если нет, то он гонит трафик на Amazon, где продает бутылочки воды за $26, а мы расскажем о сайтах в нашем SaaS-конструкторе

Ни одна площадка не может гарантировать бесперебойную работу в течение, например, года — это данность по целому ряду причин. Значит, надо иметь “план Б” — обеспечить отказоустойчивость на уровне дата-центра и создать резервную площадку, которая чуть что подхватит трафик. Синхронизируют сервера все — и Яндекс, и Гугл, и герои под катом.

Когда мы начинали делать новый конструктор сайтов, в команде было всего 10 человек, вместо такого офиса рабочим местом была квартира (ненадолго, но есть что вспомнить), а текущие 700 000 пользователей, ~50млн инод и ~6Тб данных маячили на далеком горизонте. Это дало нам возможность больше года экспериментировать с синхронизацией серверов — а параллельно выпытывать у коллег из других проектов, как и что устроено у них.

После энного эксперимента и посещения энного сикретне мероприятия, наш программист Максим пришел и сказал: “Кажется, не мы одни такие полоумные”. Короче, мы сделали полностью свою систему синхронизации. И сейчас расскажем, почему.

Пролог: uKit глазами сисадмина


Одной из главных сущностей в нашей системе является “сайт” — это документ в базе, который содержит ссылки на составляющие его “виджеты”, которые как-то расставлены и чем-то наполнены со стороны пользователя. Расставив и заполнив виджеты на свой вкус, пользователь решает, что пора нажать кнопку “опубликовать” — и его сайт должен попасть, так сказать, в продакшн.


Поскольку в качестве основной БД для проекта мы выбрали MongoDB, то особенно не переживали насчет отказоустойчивости: ведь есть асинхронная репликация “из коробки”, с этим всё хорошо.

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

Вот какой путь прошли мы в поисках:

Изобретаем свой “хайлоад”


Какие проблемы могут возникнуть с голым Rsync’ом


Добавим асинхронности? А, нет, не добавим


На распутье: Amazon или свой Лунапарк?


Поворотный момент: или как делают старшие товарищи


“Расческа Вершанского”: как решить проблему параллельности при асинхронности


Не думай о наносекундах свысока: что учесть при тестировании самописной системы


Хэппи-энд: героическое спасение рядовых серверов


Изобретаем свой “хайлоад”


Первой идеей было копировать текущее состояние сайта в отдельную коллекцию. В таком виде задачу и отдали программисту: но повозившись над ней, он предложил просто взять и сдампить всё в файлики, да и положить на диск. У Максима вообще склонность бережно относиться к ресурсам системы.

Поразмыслив, мы постепенно начали проникаться этой идеей: с точки зрения “хайлоада” ничего лучше и надежнее придумать-то, наверное, нельзя: отдать файл с диска Nginx-ом, с его всепобеждающим сендфайлом и тред-пулами. База, какой бы хорошей она ни была, всё равно рано или поздно начинает требовать шардирования, построения новых индексов и прочих регламентных работ. А мы делаем высоконагруженный сервис, мы обязаны думать о таких вещах. В конце концов, так даже проще.

Какие проблемы могут возникнуть с голым Rsync’ом


Итак, у нас есть статические файлы, в основном HTML и картинки в разных форматах. Как синхронизировать не так уж много файлов между двумя серверами? Rsync-ом.

Пока мы были достаточно маленькими, то просто повесили на cron синхронизацию всей директории с сайтами каждую минуту (с lock-файлом). Скрипт отрабатывал за секунду, мы несколько раз переключались между серверами и горя не знали.


Задача маркетологов — сделать так, чтобы сайтов в системе стало побольше. И надо сказать, с задачей они справляются.

Мы начали замечать, что lock-файл в часы пик начинает висеть по 10 и более минут — иерархия директорий подросла, и Rsync уже не успевал обойти всю её быстро. Rsync же сначала обойдет все директории на источнике, построит дерево, получит аналогичное дерево с приемника, сравнит… И только после этого начнет что-то отправлять — что тоже занимает время. Как результат, статика на втором сервере отстает — причем расхождение идет не от какого-то определенного момента времени, что добавляет проблем.

Добавим асинхронности? А, нет, не добавим


Так мы пришли к идее “распределенных файловых систем” — их в мире существует предостаточно: LeoFS, LustreFS, GlusterFS, XtreemFS, WhateverFS. Но на тот момент (может, что и поменялось) все либо были синхронными, либо асинхронный режим у них фактически не работал. Или работал слишком плохо.

Порывшись на Хабре, мы обнаружили проект Lsyncd — работает он посредством Inotify-вотчеров, навешиваемых на каждую директорию, и того-же самого Rsync-а, который срабатывает при событиях создания/изменения/удаления файлов. Мы решили попробовать: на наш миллион директорий оно навешивало свои вотчеры около часа, но в итоге заработало и начало что-то синхронизирновать. Но оказалось, что у Lsyncd проблемы с retry-ями и запоминанием длинного списка изменений:



Да и длительные обрывы связи между серверами Lsyncd переживает плохо.


На распутье: Amazon или свой Лунапарк?


Поиски “грааля” привели нас на любопытное распутье из двух вариантов:

Использовать S3-Like хранилище — например, Amazon S3 или один из множества аналогов. Как вариант, развернуть у себя. Такой подход, с одной стороны, позволит перестать думать о том, где и как хранятся наши файлы и начать жить, а с другой — сулит много новых заморочек.

Во-первых, нам придется переписать весь код работы с файлами, да еще и порефакторить места, которые опираются на то, что файл записывается на диск почти мгновенно и сразу же доступен. Во-вторых, мы потеряем возможность ходить по файлам Midnight Commander-ом, что периодически бывает нужно при отладке.

В-третьих, если файлы при такой схеме лежат у чужих дядек, они могут устроить нам любую пакость — например, внезапно поднять цену в два раза или уронить свой сервис, ошибившись окошком. Да, файлы могут лежать у нас в каком-нибудь Swift/Elliptics/Riak — но это разумно, когда данных в десятки раз больше чем у нас — если ты Яндекс, например. Там же сразу ставится на 5 серверов — три с данными и два управляющих, это минимум.

В общем, сложнее, чем нужно для нашей задачи.

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

Недостаток всего один — это надо делать самим, и успеть до того, как у нас закончится место. Моментом X должен был стать канун Нового 2017-го года.

Поворотный момент: или как делают старшие товарищи


Выбор в пользу самописного решения занял время. При этом мы руководствовались не только своими хотелками и нехотелками, но и опытом коллег по цеху — ловили их на мероприятиях и аккуратно допрашивали.


Уважаемый товарищ, чьи мыcли мы так и не поняли.

Поворотным моментом, пожалуй, стала встреча с ребятами из VK — в кулуарах какой-то конференции мы поинтересовались у них, чем они синхронизируют файлы. Они сказали, что синхронизируют сами, так как ничего хорошего готового тоже не нашли.


Позже мы узнали, что и в Badoo фотки синхронизируются подобным образом.

В общем, Максим засел за написание системы, а я тем временем доставал дисковое пространство из загашников LVM-а и подчищал тяжелые логи.

“Расческа Вершанского”: как решить проблему параллельности при асинхронности


За основу системы мы взяли Rabbitmq (он уже использовался в проекте) и npm-модуль fs — через него проходило большинство операций с файлами. Идея была такой — переопределив модуль fs, мы заставим его записывать все выполняемые действия в очередь и только после этого считать их завершенными (дёргать callback). Рядом с очередью запускается демон, который берет из очереди задание и шлёт его по http в другой демон, который работает на принимающем сервере. Если это создание файла — шлет post-ом этот файл, если удаление — команду delete и так далее.

Но если мы берем все задачи подряд и асинхронно с ними работаем, как по-умолчанию и делает Node.js, то наши действия могут пойти не по порядку. Например, мы будем пытаться записать файл в папку, которой еще нет, или что-нибудь в таком духе. А если выполнять задачи строго по порядку, это будет медленно.

В момент обсуждения этой дилеммы на кухне у доски как-бы-случайно оказался Витя, который всегда как-бы-случайно оказывается рядом, если происходит какое-нибудь интересное обсуждение. Витя начал рисовать вертикальные линии, символизирующие действия CRUD, — линии шли параллельно друг другу и назывались a, b, c, d…


С гитарой — это Витя. Довертеть его идею “расчески” также помогал Паша, который занимается в компании такими вот интересными вещами.

Бинго! Наши файлы хранятся по старой-доброй схеме a/b/c, то есть сайт vasya.ru будет лежать по пути /v/a/s/vasya.ru. Мы решили, что принимающая сторона будет выполнять задания по порядку в пределах одной “верхней” буквы, но за ее пределами — асинхронно. Так получалось и быстро, и надёжно. А поскольку схема Вити на доске напоминала расческу, принцип был назван в честь программиста, оказавшегося в нужное время в нужном месте.

Не думай о наносекундах свысока: что учесть при тестировании самописной системы


Когда уже было что тестировать, мы решили проверять синхронность директорий в двух местах тем же способом, которым мы это делали в далекие времена, — а именно, самим Rsync-ом в режиме dry-run.

С этой опцией на самом деле ничего не происходит, но на экран выводятся действия, которые произошли бы: видно все файлы, которые должны быть скопированы, соответственно, это и есть точки “несинхронности”.



Тут ждала еще одна проблема — ctime файлов наш Node.js выставляет с точностью до миллисекунд (дальше забивает нулями), а Rsync, сравнивая файлы на одинаковость, учитывает еще и наносекунды! И считает файлы разными, когда они на деле одинаковые.

Можно было бы вычислять и сравнивать md5-хэши файлов — но это очень медленно. Поиски готового модуля, способного менять creation time файлов в режиме UltraHD, ни к чему не привели, и пришлось написать свой модуль на C для этих целей. Вот так возвращение к истокам синхронизации вернуло нас к истокам программирования.

Хэппи-энд: героическое спасение двух рядовых серверов


Все шло неплохо и мы начали готовиться к замене дисков на втором сервере на более вместительные — и переезду на него.

В ходе подготовки к переезду написали скрипт “холодной синхронизации”, ласково названный coolSync: чтобы перекачать файлы, которые давно лежат на диске и с ними не производится никаких действий (но какие именно это файлы — мы не знаем). Вариант с обычным рекурсивным Rsync тут не подходил, т.к. эта рекурсия длится вечно, и построенный в итоге список файлов теряет актуальность, не успев достроиться. Наш скрипт генерировал a/b/c пути сам, ходил по ним, обходил папки только “в глубине” и запускал Rsync для каждой отдельной папки на каждой итерации. Всего за какую-то неделю, нам удалось перекачать почти все файлы.


Еще пару недель мы решали, почему “почти”. Когда начали проверять синхронность отдельных “букв” между двумя нашими серверами, обнаружились потерянные файлы: выяснилось, например, что где-то файл перемещается сразу после создания в другую директорию, и передающий демон не может его найти в нужном месте.

Эти проблемы в итоге удалось победить — новый 2017-й год был уже на носу, но как в голливудских блокбастерах, мы справились к часу X. Теперь система работает вот так:



Вместо эпилога


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

Комментарии (23)


  1. neenik
    10.04.2017 15:15
    +1

    Какое количество файлов у вас требовалось синхронизировать?


    1. pys
      10.04.2017 15:20
      +3

      Сейчас там ~50млн. инод и ~6Тб данных.


      1. neenik
        10.04.2017 15:26
        +1

        Хотел вам предложить syncthing, но да, для такого количества слишком много ему нужно будет памяти.


        1. pys
          10.04.2017 15:32
          +2

          Интересно, посмотрим и на него тоже, спасибо)


  1. Markuzzz
    10.04.2017 16:35
    +1

    26 USD стоит 24 бутылки по 0,5 л.


    1. spasibo_kep
      10.04.2017 16:38
      +2

      Спасибо, пасхалка «кто сходит и прочакает проверит» unlocked. Там еще тарификация весьма забавная у него с этими 26-ю баксами и числом бутылок.

      P.S. Вспомнилось:

      Когда Чак Норрис входит в воду, он не становится от нее мокрый — вода становится чаковой.


  1. Openmsk
    10.04.2017 17:04

    я в третьем ряду с низу, третий с права)


  1. lexore
    10.04.2017 18:05

    Уважаемый товарищ, чьи мыcли мы так и не поняли.

    От этого уважаемого товарища в тот же день вышла другая статья, которая кстати, вам очень подходит.
    Краткая её суть такая: если у вас маленькие файлы и вы всегда можете читать/писать их целиком, вы можете использовать key-value хранилище.
    Когда у вас куча мелких файлов, важной проблемой становятся накладные расходы на поиск файла в ФС.
    key-value хранилища эту проблему решают.
    Рекомендую вам посмотреть в эту сторону.


    1. pys
      10.04.2017 18:34
      +1

      Мы смотрели, и написали об этом в той части где описаны проблемы выбора между S3-Like хранилищем и чем-то «своим». Т.е. там подразумевается не только Amazon S3, но и все похожие пути решения, вплоть до Mongodb GridFS.


      1. lexore
        10.04.2017 19:02
        +1

        Боюсь, что здесь в одну кучу смешиваются два вида хранилищ.
        S3, Swift и прочие — это объектные хранилища.
        А GridFS в MongoDB, как написано в доке — это вообще хранилище для больших файлов ("large files").
        Они, конечно, размывают понятие "путь к файлу", но файл у них остается файлом.


        Даниил же рассказывает про кластерную NoSQL СУБД.
        Т.е., если очень упростить, они хранят файлы, как строки в хранилище типа redis.
        Такой трюк можно сделать только с маленькими файлами и там он оправдан.
        Поэтому я, как прочитал вашу статью, сразу вспомнил про статью от него.
        Он упоминает Aerospike и судя по тестам, вашу задачу она решает быстро и надежно.


        1. DmitryKoterov
          11.04.2017 10:14
          +1

          Или даже Redis — у nginx есть модуль для работы с memcached-протоколом, можно прямо URI трансформировать в ключи и отстреливать файлы. Заодно бы шардинг получился и репликация.

          На первый взгляд кажется, что решение использовать файлы по соображениям производительности привело как раз к невозможности обеспечить эту самую производительность легким путем.

          А для mc — написать vfs на питоне занимает 100 строчек. И будет редис на панелях с папками.


          1. lexore
            11.04.2017 11:40
            +1

            Сам redis использовать в таком месте не получится — он все хранит в оперативке и синхронизируется с диском. 6 ТБ данных туда засунуть проблематично. А ещё проблематичней будет эти данные сохранять на диск. Здесь нужен подход традиционных СУБД — хранить данные на диске и по максимуму использовать оперативку под кеширование.


            1. pys
              11.04.2017 12:30
              +1

              Отвечу здесь и на этот, и на предыдущий ваш комментарий. И, возможно, еще на чьи-то.
              Во-первых, спасибо за замечание, видимо я не совсем понятно выразился. Мысль была в том, что у нас было, грубо говоря, 2 пути: засунуть файлы в какую-нибудь базу и не засунуть. Понятно, что баз много, хороших и разных. Но нам бы тогда, повторюсь, пришлось переписывать весь код работы с файлами и менять подход к работе с ними, в том числе к их отдаче. Также напомню, что у нас разные ДЦ, т.е. непредсказуемая latency и весьма условный 1Gbps между нодами. Взвесив все «за» и «против» мы выбрали вариант без баз. Безусловно, у обоих вариантов есть недостатки и преимущества.


          1. pansa
            14.04.2017 02:24

            > написать vfs на питоне занимает 100 строчек
            Вы предлагаете использовать FUSE? Посмотрите, автор привел объемы данных. Представляете накладные расходы на путешествия user — kernel — user на каждый чих?
            FUSE-решения хороши, когда нужно сделать удобно, но о скорости там речи не идёт вообще. GlusterFS это наглядно показал.


            1. DmitryKoterov
              14.04.2017 03:08

              Да какой fuse, у mc есть система плагинов, чтобы входить во всякие там архивы и т.д. на панели. Например, вот для AWS S3: https://github.com/MidnightCommander/mc/blob/master/src/vfs/extfs/helpers/s3%2B.in (я немного контрибутил туда).

              Это не для работы в продакшене, это для отладки. (В статье упоминалось, что один из недостатков иметь данные в key-value storage по сравнению с файлами, — это невозможность ходить по структуре файлов в mc; в принципе, сам по себе аргумент достаточно сильный ИМХО — отладка очень важна.)


              1. pansa
                16.04.2017 13:46

                Если я правильно понял автора, то им всё же нужно в итоге иметь файловое представление. Поэтому mc подойдет только для отладки и всё равно по сути своей будет аналогом FUSE, а значит — медленно.


  1. pansa
    10.04.2017 21:47
    +1

    А не смотрели в сторону zfs -> snapshoot, передавать и применять дифф снапшота? Говорят, должно работать. Хотя куст файлов у вас серьезный, да. Возможно, стоит просто уходить от обычных ФС в сторону чего-нибудь виртуального.


    1. pys
      11.04.2017 12:32
      +1

      Честно говоря, не смотрели. А ZFS под Linux уже production-ready?


      1. pansa
        14.04.2017 02:13

        Да, более чем. У нас несколько БД так крутятся — бэкап удобно снимать — остановил, снапшот сделал, запустил, и потихонечку снапшот вытягиваешь, а БД продолжает работать.
        Но если есть недоверие к пингвину, можете посмотреть на FreeBSD. Есть мнение, что там оно роднее. Хотя, есть и другие мнения =)


        1. pys
          14.04.2017 15:34

          Мы, кстати, тоже так бэкапим БД, только снапшотим средствами LVM. Но я не совсем понял в чем идея. Делать снапшоты одновременно на обоих серверах, как-то diff-ать их по сети и применять изменения на «слэйв-сервере»?


          1. pansa
            16.04.2017 13:41
            +1

            Читать в сторону zfs send -i
            docs.oracle.com/cd/E19253-01/820-0836/gbchx/
            Честно — сами такое ещё не пробовали, но zfs крута и вполне может быть. Если теоретизировать, то механизм CoW должен позволять очень быстро получить набор изменений относительно снапшота. Тогда, если у вас изменения только на условном мастере и их относительно мало, то дифф будет небольшой, быстрый и накатить его на RO слейв не займет времени. Ну и плюс все остальные плюшки ZFS (про минусы тоже надо помнить!).


  1. vasy_pupkin
    11.04.2017 12:07
    +1

    Как я понял Максим это программист. А как известно если ты молоток то вокруг только гвозди. Нельзя программистам давать такие задачи. Все программисты в любых программных продуктах видят изъяны и готовы порвать майку на груди и написать свою еще лучше. Такие задачи должный решать архитекторы или интеграторы. Вы говорите что у вас не крупный проект но при этом берете решение VK (ну они то точно мелкий стартап).
    Как раз небольшым проектам нужно брать готовые решение и для этого есть пару аргументов.
    1. Готовое решение имеет комьюнити и там всегда идет работа над уличением продукта.
    2. Только используя готовое решение долгое время можно понять что действительно вам нужно. Чтоб потом когда вы станете крупным проектом со штатом в сотни программистов написать что-то свое пользуясь опытом других. И этим проектом будет занимать целый отдел а не один программист.
    3. Как часто бывает у подобных самописных решений очень сложная кодовая база, очень сложная инфраструктура и точно у этого всего нет документации. Долгих лет Максиму. Но случись что с ним вы будете смотреть на страдание того кто это возметься поддерживать. А еще вы точно от него услышать что это все нужно переписать так как это чужой программный продукт и там кучу изъянов. И новая итерация самописного $#@ ПО
    ИМХО. Не пишете свои велосипеды там где задачи стары как мир используйте готовое решение. А это задача именованием такая и решена уже сотнями способов под все случаи жизни.


    1. pys
      11.04.2017 13:22
      +4

      Какие именно сотни способов решают эту задачу в нашем случае? Предложите наиболее подходящий с вашей точки зрения, нам очень интересно ваше мнение. Заранее спасибо!