«Только со смертью догмы начинается наука.»
// Галилео Галилей
«Я начал завидовать рабам. Они всё знают заранее. У них твёрдые убеждения.»
// х/ф Марка Захарова «Убить дракона» по мотивам пьесы Евгения Шварца
Уже пару лет и дня не проходит, чтобы я не услышал (или не прочитал) от людей, начинающих новые проекты, фразу типа «Возьмем серверный движок для REST API и MVC, и погнали». Сначала я думал, что у этих слов есть один источник, может книжку какую завезли во все магазины или где-то в топе поисковиков лежит статья, зомбирующая разработчиков. Если же выяснять у них, что они понимают под REST и MVC, то можно повредиться умом. Ну с MVC уже все ясно, об этом я уже давно писал, ничего не изменилось, только усугубилось, стоит набрать в Google Images «mvc» и мы увидим страшное, стрелочки в любые стороны. Ну а про REST отвечают следующее: ну как же, нам нужно из браузерного GUI и мобильного приложения вызывать серверные методы, например: setUserCity(userId, cityId) или calculateMatrix(data) или startVideoConverter(options, source, destination) а потом мы столкнемся с большой нагрузкой и архитектура REST все решит. Дальше я задаю вопросы, от которых глаза округляются уже у тех, кто недавно еще горел праведной верой, рвался в бой и точно знал, что к чему в этом мире. Теперь можно перейти к рассмотрению терминологической катастрофы, в эпицентре которой мы с вами пребываем.
Вопросы по MVC
Первый вопрос: нарисуйте мне схему «что такое MVC?». Что я получаю:
Для MVP таких пониманий и схем тоже достаточно, даже не сомневайтесь.
Второй вопрос: а как это паттерн, предназначенный для пользовательских интерфейсов может быть применен на сервере?
Я не могу перечислить тут все возможные варианты ответов. Но правильный встречается очень редко и он такой: на сервере MVC может быть только если у нас пользовательский интерфейс полностью реализован на сервере, т.е. нет браузерного кода, интерфейс генерируется на сервере, а браузер просто показывает его.
Третий вопрос: как вам удалось совместить MVC с REST?
Не удержусь, опубликую распространенные варианты:
- Ну как же, у нас MVC реализован при помощи REST
- Ну как же, у нас REST реализован при помощи MVC
Не могу это комментировать, тут совершенно ясно, что мы имеем дело с терминологической катастрофой, но об этом дальше.
Вопросы по REST
Первый вопрос: ну, дорогие мои, а как эти ваши setUserCity, calculateMatrix и startVideoConverter могут быть реализованы на REST?
Мой вариант ответа: весь смысл REST в том, что он оперирует файлами, каждый из которых имеет свой уникальный URL и над этим файлом можно производить только HTTP методы: GET, PUT, POST, DELETE. Поэтому, все эти свои setUserCity будьте любезны заменить на HTTP POST /user/id и отправлять туда нужно целиком сериализованного пользователя. Или другой вариант, нужно для каждого параметра пользователя иметь отдельный URL. Очевидно, что CRUD методов (create, read, update, delete), вызванных через HTTP методы, хватит для манипуляции файлами или объектами в БД. Например, для ввода форм и бланков, сохранения их в таблицы с дальнейшим удалением и редактированием. Но любое современное приложение не может же быть сведено к таким примитивным вызовам. И разработчики, чаще всего, понимают под REST именно его противоположность — RPC (вызов удаленных процедур), когда мы можем сделать сколько угодно методов, а не только CRUD, и когда один URL может отдавать совершенно разные ответы, в зависимости от того, какой пользователь его вызвал, с какими параметрами и в какой последовательности. А это уже совсем не REST, например, Хабр выдает по адресу http://habrahabr.ru/tracker/ разный список для каждого из нас и такое повсеместно, в Google, Facebook, Github и т.д. Так что, уберите слово REST, вводящее в заблуждение, делайте API и все.
Второй вопрос: ладно, а что такое REST именно для вас?
Разброс мнений тут не так широк, как для MVC, но терминологическая катастрофа вот в чем: как я показал выше, под REST понимают совсем не REST, а чтобы сделать все по науке идут и изучают догматическую литературу по REST, подыскивают фреймворки с поддержкой REST, а в результате, выходит гибрид RPC и REST. И хуже всего, что все это происходит неосознанно.
Третий вопрос: вы можете объяснить, как именно REST спасет вас от больших нагрузок?
Мой вариант ответа: он STATEless, но что это значит и зачем нам отказываться от состояния на сервере — обсудим ниже.
В чем же смысл STATEless
При масштабировании современные веб-сервера порождают отдельные процессы для каждого запроса. Если эти процессы будут STATEless, то нет ни какой разницы, на одном сервере работает система или на сотне серверов. Каждый процесс получает данные запроса по протоколу CGI и должен выделить память, развернуть в ней свои структуры данных, если нужно, то создать соединения с БД, прочитать файлы или сделать вызовы к внешним модулям. Потом процесс выполнит свою логику и все это хозяйство погибает. Такие процедуры задерживают выполнение HTTP запросов, поэтому есть FastСGI, который предусматривает несколько всегда запущенных процессов и экономит время на их порождении, первичном выделении памяти, ее освобождении и завершении процессов. Еще FastСGI позволяет повторно использовать подключения к БД и другие дескрипторы. Но структуры памяти, необходимые для обработки каждого HTTP запроса, все равно живут не долго, они погибают и создаются, а при этом повторно выполняются запросы в БД, обращения к файлам и к внешним модулям. Но и в этом случае необходимо придерживаться принципа STATEless, потому, что неизвестно, в какой процесс попадает следующий HTTP запрос. А разные запросы могут относиться к разным сайтам приложениям, и даже в одном приложении могут относиться к разным сессиям или предусматривать развертывание в памяти очень отличающихся структур данных.
Настало счастье программисту из верхнего палеолита
В то время, когда складывалось мое программистское мировоззрение (1997-2002 годы), мэинстримом была трехзвенная архитектура приложений (оконный клиент, сервер приложений, СУБД). И клиенты взаимодействовали с серверами приложений по RPC. Все было STATEful, но тогда и задач то таких не было, которые бы требовали масштабирования сервера приложений на много машин. И вот, долгие годы, с 2002 по 2012 я терпел REST и STATEless, мечтая в душе про полноценное STATEful программирование и RPC для высоконагруженных систем. Теперь же, когда большинство языков и платформ имеют свои Event-Loop решения, как Node.js, Python's Twisted и Ruby Event Machine, я чувствую себя совершенно счастливым человеком и больше не знаю, зачем нужен STATEless и откуда берутся в наше время фразы типа «Возьмем серверный движок для REST API и MVC, и погнали».
С тех пор, как появилась возможность делать высоконагруженные сервера приложений с состоянием, процессы опять стали жить долго, все структуры данных можно оставлять в памяти, остаются и соединения с БД, кеши, таймеры, можно развернуть сложную модель в памяти и она будет там сидеть месяц. И с этой моделью может взаимодействовать клиентская часть через API. А теперь немного арифметики, представьте, что состояние пользователя 32Кб, а у нас есть 16Гб памяти, этого хватит на 524288 (более полумиллиона) пользователей.
Единственное, что осталось решить, это как направлять в один и тот же процесс все запросы, относящиеся к одной сессии. То есть, нужно «приклеивать» сессию к серверу и к процессу (процесс может быть на одном из множества серверов). Для этого у нас есть аппаратные и программные балансировщики, реализующие прилипание по IP (ip-sticky) и по кукизу (cookie-sticky). Конечно, каждый процесс должен маркировать кукизом со своим идентификатором все запросы, которые не имеют такого идентификатора. Если идентификатор есть, то работает прилипание, а если нет, то round robin или более сложный алгоритм балансировки.
А с MVC мы еще не закончили
MVC научил всех разделять модель, представление и контроллер, при чем бездумно, где нужно и где не нужно. Но общего понимания о каждом компоненте так и не выработалось. Кто-то оставляет в модели только параметры, а методы выносит в контроллер, а у кого-то модели умеют себя сами сохранять и восстанавливать из БД, другие же — яростные противники такого подхода и выделяют еще data access layer для доступа к БД. В любом случае, с терминологической катастрофой нужно что-то делать.
Предлагаю использовать слово «модель», как его принято использовать в науке и технике, а именно — это упрощенное представление реальной системы и/или процесса. Из этого следует, что модель может содержать параметры и методы. Но методы эти должны подходить под определение. Например, методы CRUD не могут быть частью модели объекта реального мира, потому, что ни один реальный объект не умеет себя создавать или удалять. CRUD — это не есть часть модели, а часть API слоя хранения данных, как методы отрисовки — часть API графического слоя. Модель же не должна знать ничего о своем хранении, протоколах передачи по сети, особенностях отрисовки в браузере и т.д. Хороший пример метода для модели: SteppingMotor.setSpeed(х), когда модель является драйвером физического устройства или используется для моделирования физического устройства. Другой хороший пример, это математические модели, например, Equation.calculateRoots(). И третий пример, это информационные модели, например: Patient.assignBed(bedId).
Слова же «view» и «controller» по возможности избегаю, лишь потому, что в сознании отдельных разработчиков у них могут быть совершенно непредсказуемые и странные смыслы. Вместо них лучше использовать более конкретные понятия «template», «control» (или «user interface control»), «request router» и т.д.
Советы по разработке приложений на технологиях верхнего палеолита
Из событийно-ориентированных решений я лично использую Node.js, но думаю, что они могут носить универсальный характер:
1. Разделяйте клиент и сервер (браузерное приложение и сетевое API) по принципам RPC и STATEful с приклеиванием сессий к процессам.
2. Используйте оперативную память, не лазьте в базу данных постоянно. STATEful — это великолепная возможность писать быстрые приложения, и даже не из-за того, что Event Loop фреймворки предполагают неблокирующий ввод/вывод, а из-за правильного использования памяти. Большинство операций ввода-вывода не нужно даже делать в во время обработки запросов, чтение можно делать упреждающими и параллельным, а запись ленивой (lazy). Разворачивайте данные в память приложения, стройте хеши, объекты, массивы, которые проживут долгую и счастливую жизнь в STATEful процессе.
3. Между разными процессами взаимодействуйте через ZeroMQ (и другие MQ), TCP, HTTP, IPC и еще что-угодно. Таким образом, данные разных процессов, в зависимости от того, что это за данные, могут или дублироваться в памяти (кешироваться, если это общие данные) или быть разделены на «прилепленные» сессии или синхронизироваться между собой через межпроцессовое взаимодействие.
Заключение
Каждый раз переосмысливайте шаблоны проектирования. не берите чужие решения только из-за их авторитета, они могут быть не плохими, но не для вашего случая. Брать решения можно и нужно, но только при полном их переваривании и понимании.
Всегда сверяйте терминологию со своими коллегами еще перед началом проекта и уточняйте ее вплоть до создания словарика в общем доступе. С той путаницей понятий, которая накопилась в наше время, невозможно быть уверенными, что говорите об одном и том же, даже если используете одни и те же слова.
Автор: MarcusAurelius