Быстрый старт для новых проектов

Часто разработчики проводят годы и десятки лет разрабатывая и поддерживая один проект. Когда разрабатываешь большой проект, на протяжении длительного срока, очень важно придерживаться правильных инженерных практик. Писать тесты. Вовремя избавляться от технического долга. Обновлять архитектуру. Но иногда мы начинаем новые проекты. И то, как мы начинаем их разработку оказывает чудовищное влияние на дальнейшую его поддержку.

Чаще всего программисты выбирают новый framework и начинают с его tutorial'а. Это прекрасно, если вы учитесь программировать. Возможно это не менее хорошо, если вы просто выбрали новый для себя framework и делаете прототип чтобы показать инвестору. Но это не очень хороший подход, если вы создаете стартап или сервис который очень быстро получит в десять раз больше разработчиков, вырастет в тысячу раз в количестве строк кода, и получит на 4-6 порядков больше пользователей.

Да вы не знаете вырастет он или нет. Но инженерные практики, описанные здесь, полезны, уже если вы знаете что на разработку проекта потратите больше одного-двух человеко-месяцев. Врядли ведь у вас проект закончится быстрее, правда?

Что нужно сделать при старте проекта, это начать с чистого листа. Создать, продумать, нарисовать, описать архитектуру приложения. Мы должны спроектировать систему так, чтобы когда она вырастет в 10 раз после первого прототипа, она всё еще была быстрой, стройной и понятной. Дальше если у вас получится использовать какой-то существующий framework в соответствующем узле новой архитектуры, вы быстро поймете что его нужно использовать. Если вам удастся использовать какой-то существующий код -- еще лучше. Но только после того как вы узнали как вы собираетесь устроить всё. К архитектуре нужно подходить с чистым ясным сознанием не думая о готовых решениях.

Итак, в этой статье я не смогу рассказать как вам делать ваш следующий проект. Только вы это можете. Но я попытаюсь представить примерный ход мыслей архитектора в 2016-м году. Я являюсь сторонником микросервисов, и конечно же они тут будут. Возможно вы пишете по-другому, и конкретные решения описанные в статье вам не пригодятся.

Вводные

Я занимаюсь разработкой веб-проектов уже 12 лет, из них почти 9 пишу на Python'е. С другой стороны мне всегда нравилось писать системное ПО. Что чаще
всего означает что нужно писать на С. А как бы вы писали сайт на C? Если вы попытаетесь писать его так же как на питоне, это займёт у вас целую вечность. На самом деле нужно просто отдавать немного статических файлов, и сделать JSON API для всех нужных динамических данных. Правда звучит очевидно в 2016-м?
Раньше, 5-10 лет назад это было не так.

Static files and Json Api

Естественно сегодня не только приложения на C так пишут. А с появлением разных видов Virtual DOM и подобных техник, другого способа писать приложения в общем-то и не остается. Правда? Хм, а ведь пока я вам не сказал "давайте начнём с чистого листа" вы собирались брать Django Flask с шаблонами, блек-джеком и моделями.

Первый шаг

Так вот, значит мы хотим одностраничное приложение с JSON API, но его нужно как-то отдавать поисковикам. Да и не только поисковикам, пользователи тоже хотят его побыстрее увидеть. Кажется, уже давно не нужно объяснять как это делается:

Virtual Dom Renderer

Не забываем, что API нужно отдавать и напрямую, зачем тратить время на пересылку данных:

Virtual Dom and JS API

Масштабирование

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

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

Итак скорее всего мы будем больше отдавать страниц на чтение, чем обрабатывать записей. Также скорее всего записи будут намного более длительными. Например, чтение может занимать 5 миллисекунд, тогда как запись способна превысить 500 мс. Разница в 100 раз. 5 миллисекунд это скорее всего без учёта рендеринга virtual dom'а. Но последнее, во-первых, происходит не каждый раз, а тольно при
начальной загрузке страницы. А во-вторых -- мы можем масштабировать рендеринг горизонтально без больших проблем. А если машин станет сильно много -- перепишем на более производительном языке. Переписать эту часть потом -- просто.

Для начала сделаем некоторые предположения:

  • Предположим что у нас 10 процессов на одном сервере
  • У нас соотношение чтений и записей 10 к 1
  • Получается мы можем обработать пиковую нагрузку в примерно 18 запросов в секунду на процесс, или 180 запросов в секунду на сервер

И вот мы так живём с 200 запросами в секунду в среднем, и 300 запросами в секунду при пиковых нагрузках с 2мя серверами и всё хорошо. Но в один прекрасный момент приходит 450. Всего на треть больше.

В этот момент всё ложится. Почему спросите вы? Всё очень просто: у вас приходит 45 медленных запросов в секунду, а вы можете обработать 40. Это значит, что
на каждом сервере есть как минимум один медленный запрос. Что означает, что любой запрос, который раньше выполнялся 5 мс, сейчас ждёт как минимум 500 мс. В 100 раз больше. Более того, поскольку общей пропускной способности не достаточно, то очереди начинают накапливаться. И все, даже быстрые, запросы теперь начинают отваливаться по таймауту потому что перед ними еще несколько медленных запросов на запись.

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

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

  • Во первых при операциях записи пользователь вполне может подождать. Как правило разница между 0.5 сек, и 2 секундами не очень заметна
  • Более того чаще всего запись происходит Ajax'ом, и пользователь собственно и не ждёт её завершения
  • Тогда как начальная скорость загрузки страницы очень важна

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

И так, я думаю уже понятно к чему я веду:

Split GET and POST Request

Конечно же, рендеринг Virtual DOM'а никуда не девается. Я просто убрал его из схемы для простоты. Оставляю за вами составление полной схемы.

Также, очень часто, изменяемая часть сайта лучше отделяется с помощью пути или поддомена, а не HTTP метода. С другой стороны, медленная часть сайта не обязательно эквивалентна "изменяемой" или POST-запросам. Но достаточно важно разделить медленную часть и быструю. Тогда быстрая часть будет всегда достаточно шустрой даже если у вас получаются всплески нагрузки. Вспомните цифры выше. Вы вполне могли бы пережить 400-кратный всплеск нагрузки для данных только для чтения, а при смешивании этого траффика система не может пережить даже рост в 30%.

В цепочку очень удобно добавить и базу данных:

Split with A DB

Это имеет много преимуществ. Среди которых:

  1. В приложении только одна БД, сложно перепутать
  2. Читабельная часть сайта работает даже если главная база лежит
  3. Легко сделать реплику читабельной части в другом дата центре

Авторизация

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

Auth microservice

Выделение авторизации в микросервис имеет следующие преимущества:

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

Сложность

Сложно ли это? Разделить давно работающий сайт -- да, сложно. Когда вы пишете проект с нуля, для вас нет никакой разницы -- писать обработчик этого вызова в приложении X или приложении Y. На старте вы можете вполне писать все сервисы в одном и том же git-репозитории. Даже можете импортировать общие модули.

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

На этом создание архитектуры сферического сайта в вакууме закончено. Я надеюсь что ход мыслей в целом понятен. Тут есть немного советов чего еще многие из нас часто упускают на ранних этапах проектирования.

Использование БД

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

Ну например, если вы делаете новостной сайт, то новости вы храните в базе данных. Но если вы делаете корпоративный сайт, где новости будут появляться раз в неделю -- не держите его в базе данных. Положите новости в git. К этому всему вы можете сделать админку, но тем не менее, вы не будете думать о масштабировании такого решения. О транзакциях. Backup'ах. И многом другом. В большинстве случаев и об админке вам тоже не нужно думать. А о быстром и частом deployment'е, в современном проекте, вам нужно задуматься не только по-этому.

Обратный случай, был у меня, когда мы делали игровой проект. У нас были бои между персонажами почти в реальном времени. Это вроде бы как и пользовательский контент, и данные нужно хранить на протяжении больше чем одного запроса. Вместо того, чтобы класть их в базу данных, мы сделали маршрутизирование запросов так, чтобы запрос к конкретной боёвке попадал в конкретный python'ий процесс, и держали состояние боя в памяти. Длительность боя была в среднем до 10ти минут. Таким образом мы избавили себя от множества проблем: удаление старых боев, конфликтные транзакции, тормоза при работе с диском, и т.д.

Так же важно понимать, что базы данных и кеши это тоже микросервисы. Вы можете использовать несколько баз данных одного или разных классов. Например, если часть ваших данных имеет разницу между записями и чтениями 1 к 3, а другие данные имеют соотношение 1 к 100. То вам стоит их разделить. При старте проекта вы можете их положить в один сервер, разделив их по имени базы. Но вскоре вы можете, для практически только читаемой части, повысить фактор репликации в несколько раз, и получить заметный выигрышь в производительности и надёжности фактически бесплатно.

Еще один сценарий это базы данных поддерживающие мульти-мастер репликацию. Часто разработчики боятся их использовать. И это оправдано -- как правило не все данные легко склеить, если записи были с обеих сторон. Но как только вы воспринимаете базу данных как микросервис, у вас быстро появляются и такие применения. Например, наиболее известный пример такого использования, это Amazon, который хранит корзины пользователей в DynamoDB (тогда как хранить,
например, профили пользователей в такой базе довольно опасно).

Микросервисы

Насколько дробить приложение на микро-сервисы тоже зачастую не понятно.

Есть вредные рекомендации:

  • Держать в каждом сервисе меньше 1000 строк
  • Делить по REST-ресурсам
  • Делить по Python'им модулям или папкам

Так вы быстро наклепаете в вашем приложении 10-20-50 сервисов, и собирать их воедино будет еще сложнее чем управляться с монолитом.

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

Например: для новостного сайта, админка -- это часть где cоотношения записей к чтениям 2 к 1. Тогда как в публичной части 1000 к 1. Так же в админке данные должны быть всегда консистентны. Тогда как на публичной части могут оставать на десятки секунд, а может и на минуты без проблем. Значит, мы выделяем админку отдельным сервисом.

Но на публичной части есть еще и комментарии. Во-первых, комментарии это тоже много записей, тогда как в публичной части их в общем то и нет. Во-вторых, если комментарии не доступны, ничего трагического не случится. По-этому мы их загружаем ajax'ом. И делаем отдельным микросервисом c собственной БД.

Другой пример: типичный сайт интернет магазина. Он по большей части только для чтения. Но там есть 2 важных вещи которые часто обновляются:

  1. Наличие товара на складе
  2. Корзины, заказы товаров

Корзина товаров, как указано выше, хорошо бы чтобы работала в режиме мультимастера. Т.е. всегда доступна. Наличие товара на сладе, с одной стороны, требует консистентности, с другой стороны, если есть оставание данных в 99% случаев, этого никто не заметит, а в остальных -- можно решить с клиентом в индивидуальном порядке. В любом случае это дешевле чем простой сервиса. Т.е. мы должны спроектировать c систему минимальной задержкой, но дать возможность ей отвечать даже если центральный источник правды не доступен. В зависимости от конкретных препочтений, вы можете либо ajax'ом загружать данные о наличии товара на складе позже, либо держать master-slave базу данных. Последняя не должна содержать данные о товарах, а только пары "товар-количество" которые могут быстро реплицироваться в разные дата-центры.

Есть еще много вариантов реализации. Я не пытаюсь рассказать все, а лишь дать толчок для размышлений.

Заключение

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

Paul Colomiets

Read more posts by this author.