Как это начиналось


Я всю жизнь занимался разработкой под Windows. Сначала на С++, затем на C#. В промежутках мелькали VB, Java Script и другая нечисть. Однако некоторое время назад всё изменилось и я впервые столкнулся с миром Linux, Java и Scala. У нас с Денисом, моим другом и соратником по многочисленным идеям, уже был свой проект – набор утилит для Windows, который пользовался широким спросом в узких кругах. В какой-то момент мы оба потеряли интерес к этому проекту и встал вопрос – что же делать дальше. Денис стал инициатором идеи нового проекта – сервис по обмену clipboard между разными устройствами. Этот проект существенно отличался от предыдущего помимо технологий ещё и целевой аудиторией. Этот сервис должен был стать полезен всем. Скопируйте данные в буфер обмена и вставьте из него на любом другом устройстве. Звучит проще некуда, пока не задумаешься над тем сколько сейчас разных устройств, а также как это все будет работать при большом количестве пользователей.
Первый прототип появился через несколько месяцев. Сервер был написан на ASP.NET и хостился на MS IIS. Было написано 2 клиента: на С++ под Windows и на Java под Android.



Тестирование показало, что прототип держит около 500 соединений. Что же делать, если их будет больше, мы ведь расчитываем на сотни тысяч пользователей ;) Как написать сервер, который может работать с большим количеством соединений, который не надо будет выключать во время апгрейда железа или софта и который будет легко масштабироваться (то есть расширяться в случае увелечения количества пользователей).

Распределенная масштабируемая система


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



Итак масштабируемая система должна иметь несколько узлов (nodes), чтобы в случае увеличения нагрузки мы могли добавлять новые узлы и в случае выхода одного узла из строя остальные бы продолжали работать. Узел это железная или виртуальная машинка. На этом и остальных рисунках каждый квадратик это сервис, в соответствии с лучшими традициями SOA (Service-Oriented Architecture). В дальнейшем мы поговорим о том где и как можно располагать эти сервисы.

На чем современные компании пишут свои сервера. Например Twitter использует функциональный язык – Scala и имеет свою собственную библиотеку Finagle ( The twitter stack ). Scala позволяет писать неблокирующий (non blocking), неизменяющий ( immutable) код. Первое важно, чтобы экономить ресурсы сервера, тк поток не ждёт освобождение какого-либо ресурса, например при IO (input output) операциях. Второе позволяет в любой момент времени распараллелить код и выполнять вычисления в разных потоках без дополнительных усилий по синхронизации. Новый сервер мы стали писать именно на Scala и сначала с использованием Finagle но позднее переехали на Play framework. Преимущество второго в том, что он более динамично развивается и к нему постоянно появляется множество плагинов.

Для того, чтобы клиенты быстро получали информацию о добавлении нового клипборда на сервер мы использовали long poll технологию. Клиент обращается к сервису и если у сервиса нет новых данных то он не сразу отвечает клиенту, а держит коннекцию в течении заданного таймаута, например 60 секунд. Если в течении этого таймаута сервер получает новые данные, то он немедленно возвращает их для удерживаемых коннекций. Клиент таким образом постоянно повторяет запросы и ждёт обновленных данных.

При таком механизме сервисы MyService1, MyService2 и тд должны уметь сообщать друг другу о новом буфере. Например если клиент висит и ждёт результата от MyService1 однако буфер был добавлен другим клиентом на MyService2 то MyService1 должен немедленно узнать об этом. Для того чтобы сервисы могли извещать друг друга о таких изменениях мы использовали remote actors из библиотеки Akka. Akka это библиотека которая позволяет изпользовать объекты без необходимости синхронизации доступа к ним. Это достигается тем, что каждому actor посылается сообщение, а не делается прямой вызов и в отдельный момент времени только одно сообщение обрабатывается actor. Кроме этого Akka позволяет вызывать actors из другого сервиса таким же образом как и локальные actors. Механизм remote actors скрывает межсетевое взаимодействие, что существенно облегчает разработку. Таким образом используя Akka MyService1 (и любой другой сервис) может извещать все остальные сервисы если к нему поподают новые данные.

Откуда же MyService1 может узнать IP адреса MyService2 и MyService3. Мы использовали ZooKeeper для того, чтобы хранить конфигурацию системы. Таким образом каждый сервис знает IP адрес ZooKeeper и может зарегестрироваться на нем, а также получить с него информацию о том, какие ещё ноды есть в системе.



Из рисунка видно, что хотя мы создали масштабируемую систему, где можно добавлять или удалять ноды прямо в рантайм, однако база данных одна и она остается слабым местом системы. Каждое обращение к базе это достаточно долгая операция. Для того, чтобы снизить нагрузку на базу и увеличить общую производительность системы мы решили добавить кеш. В качестве кеша решено было использовать Redis. Redis это хранилище данных в памяти в формате ключ-значение (NoSQL key-value data store). Несмотря на то, что звучит не слишком понятно, идея очень простая. Redis позволяет получать значения по ключу и хранит всё это в памяти. Таким образом обращение к Redis очень быстрое. Чтобы использовать Redis наш сервис был соответственно изменен и стал обращаться за новыми данными сначала к Redis а только затем, если данные не найдены, к базе. Соответсвенно, когда приходил новый буфер, то он попадал сразу и в базу и в кеш.



Количество квадратиков в нашей схеме непрерывно прирастает, но зато увеличивается и надежность системы. Уберите MyService2 и запросы продолжат обрабатывать MyService1 и MyService3. Уберите Redis и сервисы будут напрямую получать данные из базы. Уберите… Нет, DB и ZooKeeper убирать пока что не надо, тк это обрушит всю нашу систему :)

Для того, чтобы обеспечить надежность базы данных она должна поддерживать репликации. Мы выбрали NoSQL базу данных MongoDB тк наши сервисы имеют JSON интерфейс и результаты удобнее хранить в JSON формате, который и поддерживается MongoDB. Кроме этого MongoDB прекрасно поддерживает репликации, те мы можем запустить несколько узлов с MongoDB, связать их в одну реплику и в случае выхода из строя какого либо узла все клиенты смогут продолжить работать с другими узлами. Реплики в MongoDB должны состоять не менее чем из 3х узлов: главный (primary) узел, второстепенный (secondary) и арбитр (arbiter) который следит за остальными узлами и в случае если главный узел вышел из строя, то делает второстепенный узел главным.



Теперь мы можем смело выключать один из узлов с MongoDB и наша система этого даже не заметит. Я не буду подробно останавливаться на ZooKeeper, но и он не будет являться ахелесовой пятой нашей системы, тк также как и MongoDB поддерживает репликации.

Дотошный читатель обратит внимание, что я обошел вопросом как распределяются запросы между узлами MyService1, MyService2 и MyService3. В серьёзных системах для этого используется балансировщик нагрузки – load balancer. Однако, если вы уже устали от бесконечного числа вспомогательных сервисов в нашей системе, то можете использовать DNS в качестве простого балансировщика нагрузки. Когда в DNS приходит запрос к сервису api.myservice.com то он возвращает несколько IP адресов. Хитрость в том, что для каждого запроса порядок этих IP адресов меняется. Например для первого запроса api.myservice.com вернулись: 132.111.21.2, 132.111.21.3, 132.111.21.4 а для второго 132.111.21.3, 132.111.21.4, 132.111.21.2 соответственно клиенты всегда пытаются сначала обратиться к первому IP из списка и только в случае ошибки будут использовать 2-й или 3-й.



Развертывание


В результате мы построили по-настоящему масштабируемую (можно добавлять и удалять узлы-сервисы), распределенную (сервисы могут находится на разных узлах и даже в разных data centers) систему.

Давайте проверим удовлетворяет ли наше приложение основным требованиям предъявляемым к подобным системам.

  • Доступность – наша система всегда доступна. Если мы меняем железо для любой ноды, то остальные ноды продолжают работать. Также мы можем по очереди обновлять ПО
  • Производительность – используя более сложных балансировщик нагрузки мы можем напрявлять европейских пользователей на европейский узел, а пользователей из америки на американский узел. Кроме того использование Redis существенно сокращает обращение к базе данных
  • Надежность – при выходе из строя любого узла, остальные узлы продолжают работать
  • Масштабируемость – можно сколько угодно добавлять MyServiceN
  • Управляемость – хммм, здесь всё ещё есть вопросы, не так ли?
  • Стоимость – все технологии и библиотеки, использованные в данном проекте бесплатные и/или open source


Таким образом под вопросом остался только один критерий – управляемость. Как проще всего разворачивать SOA системы, состоящие из десятков сервисов? Мы решили использовать довольно новую, но хорошо себя зарекомендовавшую технологию – Docker. Docker в чем-то схож с широко изспользуемыми виртуальными машинами, однако имеет ряд преимуществ. Используя Docker вы можете создать docker container для каждого сервиса, а затем запустить их все либо на одной либо на разных машинах. Важный момент, что так как Docker использует встроенный в Linux механизм виртуализации, то он не требует дополнительных ресурсов в отличие от виртуальных машин. Итак мы создали контейнеры для всех узлов нашей системы. Это позволяет одинаково легко развернуть тестовое окружение, где все контейнеры запущены на одном узле и рабочее окружение, где каждый контейнер на своём узле.

В какой-то момент мы столкнулись с вопросом где же размещать нашу систему. Рассматривался самый популярный облачный провайдер – Amazon, однако решили сэкономить и разместить наше приложение на более дешевом Digital Ocean.

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


  1. lair
    30.06.2015 18:06

    А чем вам akka.net не подошла, учитывая, что у вас бэкграунд в C#?


    1. Vankir Автор
      30.06.2015 20:23
      +2

      После создания прототипа на IIS & С# решили что гораздо больше бесплатных библиотек и фреймворков для разработки подобных масштабируемых сервисов на Java. Функциональные языки программирования очень хорошо подходят для подобных сервисов, поэтому решили взять Scala. Плюс Scala что она прекрасно работает с Java. Ну и соответсвенно использовалась Akka — тут уже ни о каких .NET речь не могла идти :)


      1. lair
        30.06.2015 21:13

        А зачем вам там какие-то фреймворки кроме акки?


        1. Vankir Автор
          30.06.2015 21:39

          Мы используем Play Framework для самого сервиса. Например, для работы с Mongo используется ReactiveMongo, для работы с Redis — RedisScala. Сам Play предоставляет удобный механиз для написания сервиса, сериализацию в JSON, есть возможность добавлять различные типы аутентификации, использовать кеш, поддержка куков и тд. Для него есть множество плагинов, недавно я добавил механизм бакендов, чтобы можно было постить клипборды в различные бакенды, просто помечая их тегом. Для аутентификации в бакендах используется библиотека Pac4J. Если планируется развитие продукта, то всегда очень важно насколько развиваются фреймворки, использованные в продукте. Play развивается очень динамично, большое комьюнити и тд.


          1. lair
            30.06.2015 22:03

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

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

            Переформулирую вопрос: что такого вам дает Play Framework, что вам не дал бы asp.net WebAPI + SignalR + akka.net?


            1. Vankir Автор
              01.07.2015 00:24

              По поводу фреймворков спорить не буду — тут есть разные мнения :)

              Не уверен, что могу сравнить Play и ASP.NET WebAPI + SignalR + Akka.net, тк никогда не работал с последним.


              1. lair
                01.07.2015 00:25

                Но вы же тем не менее утверждаете, что «гораздо больше бесплатных библиотек и фреймворков для разработки подобных масштабируемых сервисов на Java». Вот и хотелось бы узнать, чего вам не хватило в .net.

                Да, а еще вы пишете, что «использовалась Akka — тут уже ни о каких .NET» — хотя есть akka.net.


                1. Vankir Автор
                  01.07.2015 00:57

                  под последней фразой «использовалась Akka — тут уже ни о каких .NET» имелось в виду, что раз была выбрана Scala + Java, то было бы странно использовать с ними Akka.NET

                  Не готов спорить по поводу фреймворков и приводить какие-то плюсы и минусы .NET и Java (опыт разработки сервисов на .NET у меня небольшой и он касался в основном очень простых сервисов на WCF). Мое субъективное мнение, что выбор инструментов для создания серьёзных сервисов на Java/Scala больше и больше крупных компаний которые строят свои сервисы на Java. В начале разработки мы изучали, к примеру, стек технологий которые использовались в Twitter — я упоминал эту статью


                  1. lair
                    01.07.2015 00:59

                    Повторю свой вопрос: каких именно инструментов вам не хватило в вашем прототипе, что вы решили перейти на Scala?


            1. Don_Eric
              01.07.2015 13:12
              +1

              +1
              Вместо Akka.Net на мой взгляд проще использовать Orleans


              1. lair
                01.07.2015 14:08

                О, спасибо за ссылку на Orleans, я его как-то упустил из внимания.

                У akka.net есть то достоинство, что у оригинальной акки уже есть достаточно большое сообщество с наработанными шаблонами, которые легко переносимы и применимы.


              1. lair
                09.07.2015 12:18

                А где-нибудь есть разумное сравнение akka.net и orleans?

                (статьи я почитать и сам могу, там видно концептуальное различие, но мне интересно, что по этому поводу думают сами разработчики)


  1. azer
    01.07.2015 15:48

    Это очень нужное приложение, которое необходимо по пять раз в день.

    А под линуксом в КДЕ будет работать?


  1. meln1k
    04.07.2015 19:49

    Mongo любит терять данные при нетсплитах (https://aphyr.com/posts/284-call-me-maybe-mongodb). Как предполагается обеспечивать консистентность данных?


  1. MipH
    06.07.2015 09:37

    Какого рода авторизация между компонентами, зачем и почему pac4j (есть же несколько отличных scala-модулей по авторизации в Play)?..