Как сделать серверную часть сайта
Руководство для начинающих по серверной веб-разработке с Node.js
Большую часть своей веб-карьеры я работал исключительно на стороне клиента. Проектирование адаптивных макетов, создание визуализаций из больших объемов данных, создание инструментальных панелей приложений и т. Д. Но мне никогда не приходилось иметь дело с маршрутизацией или HTTP-запросами напрямую. До не давнего времени.
Этот пост представляет собой описание того, как я узнал больше о веб-разработке на стороне сервера с помощью Node.js, и краткое сравнение написания простого HTTP-сервера с использованием 3 разных сред, Express, Koa.js и Hapi.js.
Примечание: если вы опытный разработчик Node.js, вы, вероятно, подумаете о том, что это все элементарно/просто. ¯\_(ツ)_/¯.
Некоторые основы сети
Когда я начал работать в веб-индустрии пару лет назад, я наткнулся на курс по компьютерным сетям профессора Дэвида Ветерала на Coursera. К сожалению, он больше не доступен, но лекции по-прежнему доступны на веб-сайте Pearson.
Мне очень понравился этот курс, потому что он объяснял, что происходило под капотом, в понятной форме, поэтому, если вы можете взять в руки учебник «Компьютерные сети», прочитайте все подробности о чудесах сети.
Здесь, однако, я собираюсь лишь кратко рассказать о контексте. HTTP (Hypertext Transfer Protocol) — это протокол связи, используемый в компьютерных сетях. В Интернете их много, таких как SMTP (простой протокол передачи почты), FTP (протокол передачи файлов), POP3 (протокол почтового отделения 3) и так далее.
Эти протоколы позволяют устройствам с совершенно разным аппаратным / программным обеспечением связываться друг с другом, поскольку они предоставляют четко определенные форматы сообщений, правила, синтаксис и семантику и т.д. Это означает, что, пока устройство поддерживает определенный протокол, оно может связываться с любым другим устройством. в сети.
От TCP / IP против OSI: в чем разница между двумя моделями?
Операционные системы обычно поставляются с поддержкой сетевых протоколов, таких как HTTP, из коробки, что объясняет, почему нам не нужно явно устанавливать какое-либо дополнительное программное обеспечение для доступа в Интернет. Большинство сетевых протоколов поддерживают открытое соединение между двумя устройствами, что позволяет им передавать данные туда и обратно.
HTTP, на котором работает сеть, отличается. Он известен как протокол без установления соединения, потому что он основан на режиме работы запрос / ответ. Веб-браузеры отправляют на сервер запросы на изображения, шрифты, контент и т.д., но после выполнения запроса соединение между браузером и сервером разрывается.
Servers and Clients
Термин сервер может слегка сбивать с толку людей, впервые знакомых с отраслью, поскольку он может относиться как к аппаратному обеспечению (физические компьютеры, на которых размещены все файлы и программное обеспечение, требуемое веб-сайтами), так и к программному обеспечению (программе, которая позволяет пользователям получать доступ к этим файлам в Интернете).
Сегодня мы поговорим о программной стороне вещей. Но сначала несколько определений. URL обозначает Universal Resource Locator и состоит из 3 частей: протокола, сервера и запрашиваемого файла.
Структура URL адреса
Протокол HTTP определяет несколько методов, которые браузер может использовать, чтобы попросить сервер выполнить кучу различных действий, наиболее распространенными из которых являются GET и POST. Когда пользователь щелкает ссылку или вводит URL-адрес в адресную строку, браузер отправляет GET-запрос на сервер для получения ресурса, определенного в URL-адресе.
Сервер должен знать, как обрабатывать этот HTTP-запрос, чтобы получить правильный файл, а затем отправить его обратно браузеру, который его запросил. Наиболее популярное программное обеспечение веб-сервера, которое обрабатывает это Apache и NGINX.
Веб-серверы обрабатывают входящие запросы и отвечают на них соответственно
Оба представляют собой полнофункциональные пакеты программного обеспечения с открытым исходным кодом, которые включают в себя такие функции, как схемы аутентификации, перезапись URL-адресов, ведение журнала и проксирование, и это лишь некоторые из них. Apache и NGINX написаны на C. Технически, вы можете написать веб-сервер на любом языке. Python, golang.org/pkg/net/http, Ruby, этот список может продолжаться довольно долго. Просто некоторые языки лучше выполняют определенные вещи, чем другие.
Создание HTTP сервера с Node.js
Node.js — это среда выполнения Javascript, построенная на движке Chrome V8 Javascript. Он поставляется с модулем http, который предоставляет набор функций и классов для построения HTTP-сервера.
Для этого базового HTTP-сервера мы также будем использовать файловую систему, путь и URL-адрес, которые являются собственными модулями Node.js.
Начните с импорта необходимых модулей.
Мы также создадим словарь типов MIME, чтобы мы могли назначить соответствующий тип MIME запрашиваемому ресурсу на основе его расширения. Полный список типов MIME можно найти в Internet Assigned Numbers Authority (интернет-центре назначенных номеров).
Мы передадим функцию-обработчик запроса в createServer() с объектами запроса и ответа. Эта функция вызывается один раз каждый раз, когда к серверу поступает HTTP-запрос.
Объект request является экземпляром IncomingMessage и позволяет нам получать доступ ко всей информации о запросе, такой как статус ответа, заголовки и данные.
Объект response является экземпляром ServerResponse, который является записываемым потоком и предоставляет множество методов для отправки данных обратно клиенту.
В обработчике запросов мы хотим сделать следующее:
Весь код размещен на Glitch, и вы можете сделать ремикс на проект, если хотите.
Создание HTTP-сервера с фреймворками Node.js
Фреймворки Node.js, такие как Express, Koa.js и Hapi.js, поставляются с различными полезными функциями промежуточного программного обеспечения, в дополнение к множеству других удобных функций, которые избавляют разработчиков от необходимости писать самим.
Лично я чувствую, что лучше сначала изучать основы без фреймворков, просто для понимания того, что происходит под капотом, а затем после этого сходить с ума с любым фреймворком, который вам нравится.
В Express имеется собственный встроенный плагин для обслуживания статических файлов, поэтому код, необходимый для выполнения тех же действий, что и в собственном Node.js, значительно короче.
У каждой из этих платформ есть свои плюсы и минусы, и они будут более очевидными для более крупных приложений, а не просто для обслуживания одной HTML-страницы. Выбор структуры будет сильно зависеть от реальных требований проекта, над которым вы работаете.
Серверное программирование веб-сайтов
Тема Динамические веб-сайты – серверное программирование состоит из ряда модулей, рассматривающих создание динамических веб-сайтов; сайтов, которые доставляют персонализированную информацию в ответ на HTTP запрос. Этот модуль предоставляет общее введение в серверное программирование, наряду со специальными руководствами начального уровня о том, как использовать Django (Python) и Express (Node.js/JavaScript) веб-фреймворки для создания простых приложений.
В современном мире веб-разработки крайне рекомендуется изучить разработку на стороне сервера.
Программа обучения
Начинать с серверного программирования обычно легче, чем с разработки на стороне клиента, поскольку динамические веб-сайты склонны производить множество однообразных операций (извлекать данные из базы данных и помещать их на странице, подтверждать пользовательский ввод и сохранять его в базе данных, проверять пользовательские права и выполнение входа, и.т.д.) и сконструированы с использованием веб-фреймворков, которые выполняют эти и другие привычные веб-серверу операции с лёгкостью.
Вам потребуется понимать «как работает веб». Мы рекомендуем вам сначала ознакомиться с темами:
С этим базовым набором знаний вы будете готовы освоить модули в этой секции.
Модули
Эта тема состоит из следующих модулей. Начинайте с самого первого модуля, а затем переходите на выбор к любому из двух следующих, рассматривающих работу с парой популярных серверных языков с использованием соответствующих веб-фреймворков.
Как сделать свой сервер для сайта
Наверное, я бы не стал писать на эту тему ни одной статьи, если бы не слишком частые просьбы рассказать, как сделать свой сервер для сайта. То есть превратить обычный домашний компьютер в сервер, на котором можно размещать свои сайты и на которые смогут зайти люди из любой точки мира. Задача это очень сложная, но постараюсь кратко описать порядок действий.
Прежде чем задумываться о своём сервере, нужно понять, удовлевторяет ли Ваш компьютер минимальным требованиям? Вот их список:
Из этих двух требования становится понятно, что дешевле и проще будет арендовать физический сервер. О чём я всегда и пишу всем тем, кто хочет создать свой сервер для сайта.
Но для тех, кто не хочет доверять обслуживание сервера другим лицам и у кого выполняются оба требования, для тех я напишу, что необходимо сделать:
Если Вы хотите, чтобы сайт был доступен не только по IP, но и по домену, то тогда нужно поднимать DNS.
Также можно установить ещё PHP и MySQL, а также PHPMyAdmin. Если планируете иметь доступ с другого компьютера, то потребуется и FTP-сервер.
И, напоследок, не забудьте, что компьютер должен быть всегда включённым, иначе Ваши сайты будут недоступны.
Копирование материалов разрешается только с указанием автора (Михаил Русаков) и индексируемой прямой ссылкой на сайт (http://myrusakov.ru)!
Добавляйтесь ко мне в друзья ВКонтакте: http://vk.com/myrusakov.
Если Вы хотите дать оценку мне и моей работе, то напишите её в моей группе: http://vk.com/rusakovmy.
Если Вы не хотите пропустить новые материалы на сайте,
то Вы можете подписаться на обновления: Подписаться на обновления
Если у Вас остались какие-либо вопросы, либо у Вас есть желание высказаться по поводу этой статьи, то Вы можете оставить свой комментарий внизу страницы.
Порекомендуйте эту статью друзьям:
Если Вам понравился сайт, то разместите ссылку на него (у себя на сайте, на форуме, в контакте):
Комментарии ( 23 ):
добрый вечер. на самом деле ничего сложного в этом нет. но вот если у сервера не дай бог что нибудь испортиться и у вас не будет возможности починить, вы потеряете всех своих клиентов. лучше хостинг покупайте. свой сервер не самый лучший вариант
На хостинге тоже могут быть проблемы. Надо просто делать резервную копию и всё
Я бы с большим удовольствием себе сделал, но я ума не приложу как я буду пользоватся юниксом, один раз порылся и комп завис с ошибкой перегрузки ОС. Не дай бог кому нибудь иметь с ней дело
Да, выделенный сервер нужен для больших нагрузок.
А ваш сайт на каком сервере?Сколько посетителей выдержит
Сегодня ночью переехал на другой сервер, но пока ничего говорить не буду, расскажу о результатах через пару недель. А сколько выдержит посетителей, зависит от множества факторов.
Не пишите теги заглавными буквами. И используйте такой doctype:
А после установки apache, можно воспользоваться компьютерам в других целях. (У меня просто Windows 10)
Здраствуйте, Михаил! Как можно сделать постраничную навигацию, чтобы на экране появились название, картинки видеороликов, и потом внутри картинки были код видеороликов.Как это реализовать, какой запрос нужно написать?
Совершенно не ясен вопрос.
Я хотель сказать как в сайте ютубе или других видеохостингах есть же внизу сайта постраничная навигация, они как это делают, если много видео на сайте.
Все данные видео хранятся в базе данных, а дальше вот это: http://myrusakov.ru/php-page-navigation.html
Вы обещали кинуть видео где расскажете о вашем новом сервере.
Здравствуйте Кирилл, если и обещалось, то сделано будет, времени на все не хватает, ожидайте.
Случайно нашел эту статью, по-этому могу не в тему написать и неверно поставить вопрос, но все же) У меня такой вопрос, может кто-то подсказать (вопрос может быть не корректным с профессиональной точки зрения) Цель: обеспечить комфортное нахождение на сайте 50 тыс пользователей единовременно Вопрос: как организавать серверное оборудование и какое оно должно быть, сколько, чтобы обеспечить данную цель? P.S. Серверы мы будем покупать собственные, и размещать в дата центре
Здравствуйте. Такая вот проблема. Если я сделал свой веб сервер, как создать свой домен DNS?
Решила создать свой сайт, но не была уверена, на какой платформе лучше всего его делать. Думала сначала про разные конструкторы, но наткнулась на эту статью https://ifish2.ru/sozdat-rabochij-sajt/ и поняла, почему все-таки лучше сделать это сразу на WordPress. В статье в принципе описаны оба способа, и каждый может решить для себя сам, какой ему подходит больше)
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.
Copyright © 2010-2021 Русаков Михаил Юрьевич. Все права защищены.
Как устроен наш код. Серверная архитектура одного проекта
Так сложилось, что к тридцати годам я менял работу лишь единожды и не имел возможности на собственном опыте изучить, как в различных компаниях устроены веб-проекты, расчитанные на высокую скорость отклика и большое количество пользователей. Так что, дорогой хабраюзер, попавший в поле моего зрения в оффлайне, увидев меня, лучше беги, пока я не начал докучать тебе вопросами на тему обработки ошибок, логирования и процесса обновления на рабочих серверах</irony>. Мне интересен не столько набор используемых технологий, сколько принципы, на которых построена кодовая база. Как код разбит на классы, как классы распределены по слоям, как бизнес-логика взаимодействует с инфраструктурой, каковы критерии по которым оценивается качество кода и как организован процесс разработки нового функционала. К сожалению, подобную информацию найти непросто, в лучшем случае всё ограничивается перечислением технологий и кратким описанием разработанных велосипедов, а хочется, конечно, более детализированной картинки. В этом топике я попытаюсь как можно более подробно описать, как устроен код в компании, где работаю я. Этот подход — мой суммарный опыт полученный за 10 лет разработки в разных компаниях.
Заранее извиняюсь, если мой тон кому-то покажется наставническим — я не имею амбиций обучать кого-либо, максимум на что претендует этот пост — это рассказ об архитектуре серверной части одного реального проекта. В моем не самом развитом с точки зрения разработки ПО городе я не один и не два раза встречал разработчиков, которым был очень интересен мой опыт построения серверной части веб-приложений, так что, ребята, я пишу этот пост во многом из-за вас, и я искренне надеюсь, что у меня получится удовлетворить ваш интерес.
В статье я не буду демонстрировать листинги рабочего проекта, но рассказывать о том как устроена кодовая база без листингов будет все-таки тоже неправильно. Поэтому я решил “придумать” абстрактный продукт и на его примере пройти весь процесс разработки серверной части: от получения программистами ТЗ до реализации сервисов хранения, используя при этом принятые в нашей команде практики и проводя параллели с тем, как устроен наш реальный проект.
MoneyFlow. Постановка задачи
Контракт взаимодействия между клиентской и серверной частями приложения
В нашей команде разработка нового функционала серверной и клиентской частей сервиса ведется параллельно. После ознакомления с ТЗ мы первым делом определяем интерфейс, по которому строится взаимодействие фронтенда с бэкендом. Так сложилось, что наше API построено как RPC, а не REST, и при его (API) определении мы в первую очередь руководствуемся принципом необходимости и достаточности передаваемых данных. Попробуем по макетам прикинуть, какая информация может потребоваться клиентскому приложению от серверной части.
В макете списка у нас всего три столбца — сумма, описание и день, когда была совершена операция. Кроме этого клиентскому приложению для отображения списка потребуются категория, к которой относится расходная операция, и Id, чтобы можно было кликом из списка перейти к экрану редактирования.
Пример результата вызова серверного метода для страницы со списком совершенных расходных операций.
Теперь посмотрим на макет страницы редактирования.
Для отображения страницы редактирования клиентскому приложению потребуется метод, возвращающий следующую JSON строку.
По сравнению с моделью операции, запрашиваемой в списке, на странице редактирования в модель добавилась информация об истории создания/изменения записи. Как я уже писал, мы руководствуемся принципом необходимости и достаточности и не создаем единую общую модель данных для каждой сущности, которая могла бы использоваться в API в каждом связанном с ней методе. Да, в приложении MoneyFlow разница в моделях списка и страницы редактирования всего в одно поле, но в реальности такие простые модели бывают только в книгах, наподобие “Как за 7 дней научиться программировать на C++ на уровне эксперта”, с недобро улыбающимся упитанным мужиком в свитере на обложке. Настоящие проекты устроены намного сложнее, разница в модели для экрана редактирования по сравнению с запрашиваемой в списке моделью в реальном проекте у нас может достигать двузначного количества полей и, если мы будем пытаться везде пользоваться одной и тоже моделью, мы неоправданно увеличим время генерации списка и впустую будем нагружать наши сервера.
JSON объекты, отдаваемые сервером, это конечно же сериализованные DTO объекты, поэтому в серверном коде у нас будут классы, определяющие модели для каждого метода из API. В соответствии с принципом DRY эти классы, если это возможно, построены в иерархию.
Классы, определяющие контракт API MoneyFlow для экранов списка, создания и редактирования операций.
После того как контракт определен, занимающиеся фронтендом разработчики создают моки (mocks) на еще несуществующие серверные методы и идут творить Angular JS магию. Да, у нас очень толстый и очень замечательный клиент, руку к которому приложили уважаемые хабраюзеры iKbaht, Houston и еще несколько не менее замечательных хабра-анонимов. И было бы совсем некрасиво со стороны серверных разработчиков, если бы наше красивое, мощное JS приложение работало бы на медленном API. Поэтому на бэкенде мы стараемся разрабатывать наше API быстрым настолько, насколько это вообще возможно, выполняя большую часть работы асинхронно и строя модель хранения данных максимально удобной для чтения.
Разбиваем систему на модули
Если рассматривать приложение MoneyFlow с точки зрения функционала, можно выделить в нем два различных модуля — это модуль по работе с расходами и модуль отчетности. Если бы мы были уверены, что число пользователей у нашего приложения будет невелико, мы бы строили модуль отчетности прямо поверх модуля внесения расходов. В этом случае у нас была бы, к примеру, хранимая процедура, которая бы строила для пользователя отчет за год непосредственно по базе внесенных расходов. Такая схема очень удобна с точки зрения консистентности данных, но, к сожалению, у нее есть и недостаток. При достаточно большом количестве пользователей таблица с данными расходов станет слишком большой, чтобы наша хранимая процедура отрабатывала быстро, рассчитывая “на лету”, к примеру, сколько средств было потрачено пользователем на транспорт за год. Поэтому, чтобы пользователю не приходилось ждать отчета долго, нам придется генерировать отчеты заранее, обновляя их содержимое по мере появления в системе учета расходов новых данных.
Разработка ПО — процесс итеративный. Если считать схему с единой моделью данных для расходов и отчетов первой итерацией, то вынесение отчетов в отдельную денормализованную БД уже итерация номер два. Если теоретизировать дальше, то можно вполне нащупать направление и для следующих улучшений. К примеру, как часто нам придется обновлять отчеты за прошлый месяц или год? Наверное, в первых числах января пользователи будут вносить в систему информацию о покупках, совершенных в конце декабря, но большая часть приходящих в систему данных будет относиться к текущему календарному месяцу. То же справедливо и для отчетности, пользователей гораздо чаще будут интересовать отчеты за текущий месяц. Поэтому в рамках третьей итерации, если эффект от использования отдельного хранилища для отчетов будет нивелирован увеличением количества пользователей, можно оптимизировать систему хранения перенесом данных по текущему периоду в более быстрое хранилище, расположенное, к примеру, на отдельном сервере. Или использованием хранилища вроде Redis, хранящего свои данные в оперативной памяти. Если проводить аналогию с нашим реальным проектом, то мы находимся на итерации 2.5. Cтолкнувшись с падением производительности, мы оптимизировали нашу систему хранения, перенеся данные каждого модуля в независимые базы данных, а также мы перенесли часть часто используемых данных в Redis.
По легенде проект MoneyFlow только готовится к запуску, поэтому мы оставим его на итерации номер два, оставив себе простор для последующих улучшений.
Синхронный и асинхронный стеки выполнения
Кроме увеличения производительности за счет хранения данных в максимально удобном для чтения виде мы стараемся большую часть работы производить незаметно для пользователя в асинхронном стеке выполнения. Каждый раз, когда со стороны клиентского приложения к нам приходит запрос, мы смотрим, какую часть работы необходимо выполнить до того, как ответ будет возвращен клиенту. Очень часто мы возвращаем ответ почти сразу, выполнив лишь необходимую часть вроде проверки валидности пришедших данных, переложив большую часть работы на асинхронную обработку. Работает это следующим образом.
Теперь сам код. Для каждого пришедшего с клиентской части запроса мы создаем объект, ответственный за его корректную обработку. Объект, обрабатывающий запрос на создание новой расходной операции, выглядел бы у нас так.
Объект ChargeOpsCreator проверил корректность входных данных и добавил совершенную операцию в модуль учета расходов, после чего клиентскому приложению возвратился Id созданной записи. Процесс обновления отчетов у нас производится в фоновом процессе, для этого мы отправили на сервер очередей сообщение ChargeOpCreated, обработчик которого и обновит отчет для пользователя. Сообщения, отправляемые в сервисную шину, это простые DTO объекты. Вот так выглядит класс ChargeOpCreated, который мы только что отправили в сервисную шину.
Разбиение приложения на слои
Оба стека выполнения (синхронный и асинхронный) на уровне сборок у нас разбиты на три слоя — слой приложения (контекст исполнения), слой бизнес-логики и сервисы хранения данных. У каждого слоя строго определена зона его ответственности.
Синхронный стек. Слой приложения
В синхронном стеке контекстом исполнения является ASP.NET приложение. Его зона отвественности, кроме обычных для любого веб-сервера действий вроде приема запросов и сериализации/десериализации данных, у нас невелика. Это:
Слой приложения в нашей системе очень прост и легковесен, мы за день можем поменять контекст выполнения нашей системы с ASP.NET MVC (так у нас исторически сложилось) на ASP.NET WebAPI или Katana.
Синхронный стек. Слой бизнес-логики
Слой бизнес-логики в синхронном стеке у нас состоит из множества маленьких объектов, обрабатывающих входящие запросы пользователей. Класс ChargeOpsCreator, листинг которого я приводил выше, это как раз пример такого объекта. Создание подобных классов хорошо состыкуется с принципом единственной ответственности и каждый подобный класс может быть полноценно протестирован модульными тестами благодаря тому, что все его зависимости инжектируются в конструктор.
В реальном проекте объекты слоя бизнес-логики у нас отделены от контроллеров, где они инстанцируются, интерфейсами. Но особой практической пользы это отделение нам не принесло, а только усложнило секцию IoC контейнера в конфигурационном файле.
Асинхронный стек
В асинхронном стеке приложением является служба, занимающаяся разбором поступающих на сервер очередей сообщений. Сама по себе служба не знает, к каким очередям она должна подключиться, сообщения каких типов и как должна обработать, и сколько потоков ей можно выделить на обработку сообщений той или иной очереди. Вся эта информация содержится в конфигурационном файле, загружаемом службой при запуске.
Пример конфига службы (псевдокод).
Служба при старте считывает конфигурационный файл, проверяет, что на сервере сообщений существует очередь с названием ReportBuilder (если нет — то создает ее), проверяет существование роутинга, отправляющего сообщения типа ChargeOpCreated в эту очередь (если нет — настроит роутинг сама), и начинает обрабатывать сообщения, попадающие в очередь ReportBuilder, запуская для них соответствующие обработчики. В нашем случае — это единственный обработчик типа ChargeOpCreatedHandler (об объектах обработчиках чуть ниже). Также служба поймет из конфигурационного файла, что на разбор сообщений очереди “ReportBuilder” она может выделить до 10 потоков, что в случае возникновения ошибки в работе объекта ChargeOpCreatedHandler сообщение должно вернуться в очередь с таймаутом в 200мс, а при повторном падении обработчика сообщение должно попасть в лог с пометкой “CriticalError” и еще несколько подобных параметров. Это дает нам замечательную возможность ”на лету”, не внося изменений в код, масштабировать разборщики очередей, запуская на резервных серверах в случае накопления сообщений в какой-нибудь очереди дополнительную службу, указав ей в конфиге, какую именно очередь она должна разбирать, что очень, очень удобно.
Сервис разбора очередей — это обертка над библиотекой MassTransit (сам проект, статья на хабре), реализующий паттерн DataBus над сервером очередей RabbitMQ. Но программист, пишущий в слое бизнес-логики, ничего об этом знать не должен, вся инфраструктура, которой он касается (сервера очередей, key/value хранилища, СУБД и т.д.), скрыта от него слоем абстракции. Наверное, многие видели пост “Как два программиста хлеб пекли” о Борисе и Маркусе, использующих диаметрально противоположные подходы к написанию кода. Нам бы подошли оба: Маркус разрабатывал бы бизнес-логику и слой, работающий с данными, а Борис бы занимался у нас работой над инфраструктурой, разработку которой мы стараемся вести на высоком уровне абстракции (иногда мне кажется, что даже Борис бы наш код одобрил). При разработке же бизнес-логики, мы не пытаемся выстроить объекты в длинную иерархию, создавая большое количество интерфейсов, мы скорее стараемся соответствовать принципу KISS, оставляя наш код максимально простым. Вот так, например, в MoneyFlow будет выглядеть обработчик сообщения ChargeOpCreated, который мы уже заботливо прописали в конфиг занимающейся разбором очереди ReportBuilder службы.
Все объекты-обработчики являются наследниками абстрактного класса MessageHandler, где T — тип разбираемого сообщения, с единственным абстрактным методом HandleMessage, перегруженном в наследниках.
После получения сообщения с сервера очередей, служба создает нужный объект-обработчик с помощью IoC контейнера и вызывает метод HandleMessage, передавая в качестве параметра полученное сообщение. Чтобы оставить возможность тестировать поведение обработчика в отрыве от его зависимостей, все внешние зависимости, а у ChargeOpCreatedHandler это только сервис хранения отчетов, инжектируются в конструктор.
Как я уже писал, мы не занимаемся проверкой корректности входных данных при обработке сообщений — этим должна заниматься бизнес-логика синхронного стека и не обрабатываем ошибки — это ответственность службы, в которой был запущен обработчик.
Обработка ошибок в асинхронном стеке выполнения
Модуль отчетов у нас больше подходит под определение согласованности в конечном счете (eventual consistency), чем под определение сильной согласованности (strong consistency), но это все равно гарантирует конечную согласованность данных в модуле отчетов при любых возможных сбоях системы. Представим, что сервер с базой данных, хранящей отчеты, упал. Понятно, что в случае бинарной кластеризации, когда каждый инстанс базы данных продублирован на отдельном сервере, подобная ситуация практически исключена, но все-таки представим, что это произошло. Клиенты продолжают вносить свои расходы, сообщения о них появляются в сервере очередей, но разборщик, ответственный за обновление отчетов, не может получить доступ к серверу БД и падает с ошибкой. Согласно конфигу службы, приведенному выше, после падения на сообщении ChargeOpCreated это же сообщение вернется обратно на сервер очередей через 200мс, после второй попытки (тоже неудачной) сообщение будет сериализовано и занесено в специальное хранилище упавших сообщений, которое в нашем проекте объединено с логами. После того, как сервер БД поднимется, мы можем взять все упавшие в процессе обработки сообщения из логов и отправить их на сервер очередей обратно (у нас это делается вручную), приведя тем самым данные модуля отчетов в согласованное состояние. Но все это накладывает на программистов обязательство писать код в объектах-обработчиках сообщений очереди по принципу атомарности. Обработчик должен либо полностью сработать, либо сразу упасть. Как вариант, он также может быть “идемпотентным”, то есть выполнив часть работы и упав, он должен при повторной обработке сообщения понять, какую работу он уже выполнил, и не пытаться сделать ее повторно.
Слой хранения данных
Слой хранения данных у нас общий для асинронного и синхронного стеков выполнения. Для разработчика бизнес-логики сервис хранения — это просто интерфейс с методами для получения и изменения данных. Под интерфейсом скрывается сервис, полностью инкапсулирующий в себе доступ к данным определенного модуля. При проектировании интерфейсов сервисов мы пытаемся, если это возможно, следовать концепции CQRS — каждый метод у нас является либо командой, выполняющей какое-то действие, либо запросом, возвращающим данные в виде DTO объектов, но не одновременно. Делаем мы это не для разбиения системы хранения на две независимые структуры для чтения и для записи, а скорее порядка ради.
Как бы мы не снижали время отклика, выполняя большую часть работы асинхронно, неудачно спроектированная система хранения может перечеркнуть всю проделанную работу. Описание того, как устроен слой хранения в нашем проекте, я не случайно оставил в самом конце. Мы взяли за правило разрабатывать сервисы-хранилища только после того, как завершена реализация объектов слоя бизнес-логики, чтобы при проектировании таблиц БД точно понимать, как данные этих таблиц будут использованы. Если при разработке бизнес-логики нам нужно получить какую-то информацию из слоя хранения, мы добавляем в интерфейс, скрывающий реализацию сервиса, новый метод, отдающий данные в удобном для бизнес-логики виде. У нас именно бизнес-логика определяет интерфейс хранилища, но никак не наоборот.
Вот пример интерфейса сервиса хранения отчетов, который был определен при разработке бизнес-логики приложения MoneyFlow.
Для хранения данных мы используем реляционную базу данных Postgresql. Но это конечно же не означает, что данные мы храним в реляционном виде. Мы закладываем возможность масштабирования шардингом и проектируем таблицы и запросы к ним по специфичным для шардинга канонам: не используем join-ы, строим запросы по первичным ключам и т.д. При построении хранилища отчетов MoneyFlow мы тоже оставим возможность перенести часть отчетов на другой сервер, если вдруг это потребуется впоследствии, не перестраивая при этом структуру таблиц. Как мы будем делать шардинг — с помощью встроенного механизма физического разделения таблиц (partitioning) или добавлением в сервис хранения отчетов менеджера шард — мы будем решать тогда, когда в шардинге появится необходимость. Пока же нам стоит сконцентрироваться на проектировании структуры таблицы, которая бы впоследствии не препятствовала шардингу.
В Postgresql есть замечательные NoSQL типы данных, такие как json и менее известный, но не менее замечательный hstore. Отчет, который нужен клиентскому приложению, должен представлять собой json строку. Поэтому нам было бы логично использовать для хранения отчетов встроенный тип json и отдавать его на клиент как есть, не тратя ресуры на цепочку сериализаций DB Tables->DTO->json. Но, чтобы еще раз попиарить hstore, я буду делать то же самое с одной лишь разницей, что внутри БД отчет будет лежать в виде ассоциативного массива, для хранения которых и предназначен тип hstore.
Для хранения отчетов нам будет достаточно одной таблицы с четырьмя полями:
Поле | Что означает |
id | идентификатор пользователя |
year | отчетный год |
month | отчетный месяц |
report | хеш-таблица с данными отчета |
Первичный ключ таблицы у нас будет составным по полям id year month. В качестве ключей ассоциативного массива report мы будем использовать категории расходов, а в качестве значений — сумму, потраченную на соответствующую категорию.
Пример отчета в базе данных.
По этой строке ясно, что пользователь с id «d717b8e4-1f0f-4094-bceb-d8a8bbd6a673» потратил в январе 2015 года 500р на транспорт и 2500р на развлечения.
Если реализация метода GetMonthReport() не вызывает вопросов, сформировать json строку отчета из ассоциативного массива несложно встроенными средствами postgresql, то для корректной релизации обновляющего месячный отчет метода UpdateMonthReport() придется повозиться чуть побольше. Во-первых, нам надо убедиться, что отчет за этот месяц уже существует в БД и создать его, если это не так. Во-вторых, нам надо исключить состояние гонки (race condition) — попытки создания/обновления этого же отчета паралельным потоком. Пример получился довольно большим, но связано это не со сложностью типа hstore, а с необходимостью производить операцию UpSert, состоящую из двух запросов и следующую из этого необходимость исключения состояния гонки. 99% методов в сервисах хранения у нас устроены гораздо проще, я и сам не ожидал, что придется писать так много кода. Но нет худа без добра, этот пример отлично демонстрирует, почему именно слой бизнес-логики у нас определяет интерфейс сервиса хранения, а не наоборот. Если бы мы начали работу над проектом с создания хранилища отчетов, мы наверняка сделали бы классический репозиторий с методами AddReport(), GetReport(), UpdateReport() и невольно переложили бы тем самым необходимость обеспечения потокобезопасного доступа на клиентов этого репозитория. То есть на слой бизнес-логики. Именно поэтому, отношения объектов слоя бизнес-логики с сервисами хранения мы строим, руководствуясь принципами большого начальника, которые гласят следующее: ни один крупный руководитель не будет пытаться выполнять работу, с которой его подчиненный в состоянии справиться самостоятельно, и тем более он не будет подстраиваться под своего подчиненного.
Код сервиса-хранилища отчетов.
В конструкторе сервиса ReportStorage у нас две зависимости — это IDbMapper и IDistributedLockFactory. IDbMapper — это фасад над легковесным ORM фреймворком BLToolkit.
Для генерации запросов было бы вполне допустимо использовать NHibernate или еще какой-либо ORM, но мы решили писать запросы руками и только маппинг результатов выполнения в DTO объекты переложить на фреймворк, с чем BLToolkit справляется просто прекрасно.
IDistributedLockFactory в свою очередь — это фасад над механизмом распределенных блокировок, наподобие встроенного в ServiceStack механизма RedisLocks.
Я уже писал, что мы используем подход: абстрактная инфраструктура — “чисто конкретная” бизнес-логика, именно поэтому мы стараемся оборачивать сторонние библиотеки врапперами и фасадами, чтобы всегда иметь возможность заменять элементы инфраструктуры, не переписывая бизнес-логику проекта.
“Смешанная” концепция разбиения на модули
Есть определенная доля лукавства в словах, что мы выделили систему отчетности приложения MoneyFlow в отдельный, независимый модуль. Да, мы храним данные отчетов отдельно от данных системы учета, но в нашем приложении бизнес-логика и части инфраструктуры, такие как сервисная шина или например веб-сервер, являются общими ресурсами для всех модулей приложения. Для нашей небольшой компании, в которой для пересчета программистов вполне достаточно пальцев одной руки фрезеровщика, подобный подход более чем оправдан. В крупных же компаниях, где над разными модулями работа может вестись разными командами, принято заботиться о минимизации использования общих ресурсов и об отстутствии единых точек отказа. Так, если бы приложение MoneyFlow разрабатывалось бы в крупной компании, его архитектура бы представляла собой классическое SOA.
Отказ от идеи сделать систему на основе полностью независимых модулей, общающихся друг с другом на основе одного простого протокола, дался нам непросто. Изначально при проектировании мы планировали делать настоящее SOA решение, но в последний момент, взвесив все за и против в рамках нашей компактной (не в смысле замкнутой и ограниченной, а просто очень небольшой) команды решили использовать “смешанную” концепцию разбиения на модули: общая инфраструктура и бизнес-логика — независимые сервисы хранения. Сейчас я понимаю, что это решение было верным. Время и силы, не потраченные на полное дублирование инфраструктуры модулей, мы смогли направить на улучшение других аспектов приложения.