Rose debug info
---------------

Богдан Стефанюк

Заметки о программировании, путешествиях и плёнке.
Обо мне  •  Список заметок  •  Плёнка

Электронная музыка

В этом году получилось осуществить своё давнее желание — научиться писать электронную музыку.

Эта история началась в школьные годы, тогда я очень любил смотреть видео как люди делают музыку. Тогда же я познакомился с FL Studio. Скачал, открыл и непонимающе смотрел на все это. После начал читать какие-то гайды, но все свелось к тому что я просто сидел во всяких синтезаторах, крутил случайные лучки и слушал, что получается. Приблизительно в тоже время я познакомился с программированием, и оно затянуло меня по полной.

Потом я поступил в лицей, где познакомился с моим хорошим другом. Он тоже любил электронную музыку и также интересовался как ее делают. Часто смотрели всякие видосы о музыке, особенно доставляли те на которых ребята играют на «железках». Особенно удивил Launchpad от Novation.

Потом был универ, там я уже по полной влился в программирование, но желание делать музыку не покидало. Так что я как-то раз скачал Ableton и пробовал в нем что-то делать, но все равно далеко я не продвинулся.

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

Помещение школы
Учебный класс, на столе у каждого есть Ableton Push 2

Летом увидел, что школа также проводит публичные джемы, на которых ученики и другие ребята играют совместно музыку. Сходил, послушал, очень проникся атмосферой и уже на 90% был готов записаться, осталось только выбрать когда. Потом сходил на ещё один джем, после которого уже точно решил, что буду у них учиться.

В августе начался курс, длился полтора месяца, 18 занятий три раза в неделю. По воскресеньям после занятий был коворкинг на котором можно поработать над своим музлом и посоветоваться с преподавателями.

Учеба была очень плотной и насыщенной в плане информации. Все обучение происходило в Ableton, основы которого рассказывают в самом начале. Были занятия по базовой теории музыки, истории электронной музыки, синтезу звука и ударным. Также было много лекций по работе с семплами, на одной из которых делали трек из одного семпла.

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

Первое выступление

В конце курса меня ждало выступление на выпускном. Это оказался самый сложный этап для меня, потому что нужно было написать хотя бы 30 минут материала.

Постер выступления

Вначале не мог никак определиться с жанром, в котором буду писать. Начал с техно, не то чтобы часто его слушаю, так что фантазии не хватило написать что-то вменяемое. В итоге где-то две недели я просто ничего не делал и думал, что же написать. В какой-то момент чисто случайно вспомнил про один из любимых жанром: drum & bass.

В итоге получилось написать восемь треков в стиле драм энд бейс и джангл. Прогнав репетицию получил 40 минут выступления.

Стоит немного отойти в сторону и рассказать о живых выступлениях и dj сетах. Обычно живое выступление подразумевает, что музыкант играет свою музыку, часть партий которых играет автоматически, а часть исполняется вживую. Зачастую используют всякие железные синтезаторы, также есть полностью «железные» выступления, в которых не используется компьютер.

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

Итог моего обучения можно послушать на саундклауде в виде записи моего выступления:

Железо

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

В общем, решил начать с малого и купить себе какой-то небольшой синт, в итоге остановился на Korg Volca Bass и не успел моргнуть, как скупил почти всю линейку Volca. И тут я понял, что сделал ошибку. Во-первых, за стоимость этих четырёх коробочек я мог взять один нормальный хороший синтезатор. Во-вторых, на изучение всего этого зоопарка просто не хватало времени и желания. Да и позже оказалось, что они мне не сильно нравятся, потому что неудобные.

Хорошо что я покупал их б/у и продал по той же цене, что и покупал. Решил приобрести одно устройство, над которым можно сконцентрироваться и хорошо изучить. Остановился на коробочках от Elektron. Меня подцепил красивый дизайн, удобство и мощный сэквенсор.

Купил FM синтезатор Elektron Model:Cycles и потом докупил еще Model:Samples. Получился достаточно сбалансированный сэтап, с ним я понял, что хочу делать. Но оказалось, что Model:Cycles не сильно подходит под мои задачи, а вот samples напротив очень понравился.

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

На эту тему есть просто шикарнейший доклад Максима Ильяхова — о демонах начинающих музыкантов и аналоговых синтах. После него я более спокойно стал относиться к железкам и сейчас они играют роль крутых игрушек для взрослых, с которыми приятно поиграть после работы или просто когда хочется отдохнуть от компьютера. Буду ли я покупать себе железо? Скорее всего, да. Если покупать б/у то можно неплохо сэкономить и потом продать за те же деньги. Оно не сделает меня крутым музыкантом, я просто люблю железо.

Семплы

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

Однажды я просто взял и отфильтровал все семплы, после чего большую часть выкинул. И хочу проделать это еще раз. Это позволило не заморачиваться над конкретным звуком, а просто наслаждаться процессом. Да и когда уже есть какой-то набросок то найти более подходящие звуки намного проще.

Так что ограничения рулят. Об этом Сергей Король написал отличный пост: Самоограничения

Интересные доклады

Ну и напоследок поделюсь несколькими видео, которые мне очень сильно понравились и помогли в написании музыки:

Онлайн документация на базе WordPress и IdentityServer

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

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

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

Первым кандидатом был GitBook. У него приятный интерфейс, много опций по редактированию и оформлению текстов. Необходимый нам функционал также был, но доступен только в Enterprise версии. Цена оказалась слишком большой, пришлось искать дальше.

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

Wordpress

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

В итоге решили делать мануалы на базе Wordpress. Вообще, Wordpress это такая штука, с которой при должном умении можно слепить все что угодно. В нашем случае даже код не пришлось писать (почти).

За основу взяли готовую тему и немного допилили под наши нужды с помощью CSS. Правда в одном месте пришлось немного изменить логику отображения с помощью PHP, но это была достаточно тривиальная задача.

Весь функционал реализовали с помощью пачки плагинов. Главным среди них стал OAuth Single Sign On by miniOrange. Он позволяет логиниться в вордпесс с помощью сторонних сервисов (Google, Facebook и т. д.). Бесплатная версия покрывает почти все кейсы. На же пришлось покупать лицензию, потому что нужен был мапинг наших ролей на роли вордпреса.

За работу с пользователями в нашем приложении отвечает IdentityServer. Сделали отдельную страницу логина для SSO чтобы получить нужную куку. Также включили флаг AlwaysIncludeUserClaimsInIdToken в настройках клиента, чтобы клеймы ролей были в ID токене и их смог увидеть Wordpress.

Хостинг

С хостингом тоже все оказалось достаточно просто. Изначально хотели использовать AWS Lightsail, но основная проблема мы в отсутствии возможности перемещать снапшоты диска между аккаунтами. В итоге развернули все на EC2 с помощью готового бесплатного решения из AWS Marketplace.

Итого

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

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

Ну и стоимость порадовала. Все это обошлось в ~570$, в то время как типичный SaaS сервис берез 5$ в месяц за одного пользователя. Хостинг тоже вышел не очень дорогим (~10-15$ в месяц).

Где захостить свое приложение?

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

Я старался подобрать сервисы, которые позволяют максимально просто и дешёво запускать все необходимые для работы приложения ресурсы. В основном большинство приложений состоит из бекенда (в моем случае .NET), фронтенда и базы данных. Поэтому рассмотрим варианты развертывания каждого компонента.

Типы сервисов

Для начала стоит кратко пройтись по моделям предоставления сервисов в облаке. Эти модели описывают зоны ответственности пользователя и облачного провайдера. В разрезе данной статьи нас интересуют следующие модели:

  • IaaS (Infrastructure as a Service) — облачный провайдер предоставляет минимально необходимую инфраструктуру в виде виртуальных машин, сети и хранилища, а также отвечает за работоспособность. Дальнейшая настройка, в том числе и ОС, лежит на стороне пользователя. По сути это основные строительные блоки в облаке с помощью которых можно сделать все что захотим.
  • PaaS (Platform as a Service) представляет из себя готовую инфраструктуру для разработки и запуска приложений. При такой модели мы не думаем про настройку и управление серверами, операционными системами и т. д. Примером такого сервиса являются управляемые БД (managed databases). Облачный провайдер сам отвечает за правильную настройку сервера, ОС, сохранность данных, бекапах и бесперебойную работу. В основном пользователь только платит деньги и пользуется сервисом.
  • Serverless, самая свежая модель, которая очень похожа на PaaS. Только при такой модели облако само выделяет ресурсы на основании текущей нагрузки. Все настройки и планирование ресурсов скрыто от пользователя. Ему остается только загрузить свой код, а все остальное сделает облако. Также в serverless мире обычно оперируют понятием облачная функция это код, который умеет обрабатывать только одни конкретный запрос.
Наглядный пример зон ответственности в разных моделях предоставления сервисов.

Где захостить .NET бекенд?

AWS/Azure/GCP

Три самых крупных облачных провайдера, очень похожи между собой и предоставляют во многом одни и те же сервисы. Так в каждом из них можно арендовать виртуальные машины, создать управляемую БД или работать с облачными функциями. Мне больше всего нравиться AWS, но для .NET приложения Azure будет более интересным, потому что это родная для дотнета среда.

Больше всего нас интересую сервисы:

  • AWS EC2 (IaaS) обычные виртуальные машины. Есть маркетплейс на котором можно найти огромное множество готовых AMI образов с предустановленным софтом и нужными настройками. Например можно в один клик развернуть виртуалку с Wordpress и всем необходимым.
  • Azure App Service (PaaS) самый простой и нативный способ захостить .NET приложения. App Service сам разворачивает приложение из репозитория, настраивает все необходимое для работы и предоставляет полезные метрики.
  • AWS Lightsail (IaaS/PaaS) упрощенная версия AWS для тех кто с ним не знаком. Можно очень дешево арендовать виртуальную машину, поднять докер контейнер, развернуть БД и хранилище для файлов. Все это делается буквально в пару кликов мышкой. Первые три месяца бесплатные.
  • AWS Elastic Beanstalk (PaaS) умеет поднимать необходимые для работы приложения компоненты в AWS. По сути мы можем все это сделать руками, но Beanstalk автоматизирует всю рутину. Под капотом он представляет из себя набор разных CloudFormation скриптов, которые поднимают необходимые сервисы и настраивают их.

DigitalOcean

Самый простой в использовании сервис. Раньше в DO можно было арендовать только виртуальные машины, сейчас же можно создавать управляемы БД, балансировщики и т. д. На текущий момент почти все мои проекты крутяться в DigitalOcean.

Нам интересны следующие сервисы:

  • Droplets — виртуальная машина, есть много готовых образов на маркетплейсе под любой рантайм.
  • App Platform — позволяет нативно запускать приложения написанные на Python, Nodejs, Go, php или любое другое приложение в виде контейнера.

Heroku

Честно говоря я им ни разу не пользовался, но друзья очень часто советуют. Из коробки не поддерживает .NET приложения, но вроде как есть кастомные билд паки. В основном хероку позволяет развернуть приложения на Nodejs, Go, Python и т. д. Из приятных фишек: достаточно запушить код в репозиторий, все остальное сделает Heroku.

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

Где захостить фронтенд?

С фронтендом дела обстоят намного проще, есть огромное количество сервисов, которые позволяют бесплатно захостить фронт.

Мой личный фаворит — Vercel. Бесплатный план включает такие фишки как HTTPS, автоматически деплой с репозитория, собственный домен. Также он умеет разворачивать приложение под каждый PR, что позволяет протестировать изменения до того как влить код в основную ветку.

Самым же популярным сервисом является Github Pages. В нем очень просто поднять статический сайт прямо из репозитория. Бесплатный и работает сразу из коробки. В основном используется для хостинга документации к коду, блогов или резюме.

App Platform от Digital Ocean я уже упоминал выше. Можно поднять три статических сайта бесплатно, последующие за 3$ в месяц. Есть автоматический деплой, бесплатный HTTPS, возможность подключения своего домена. Сейчас в нем крутиться мое онлайн резюме. Сервис прикольный тем что в нем можно запустить и бек и фронт.

Последним хочу отметить AWS S3. По сути это объектное хранилище (хранилище для файлов) в котором есть встроенная поддержка сайтов. Для этого нужно загрузить HTML/CSS/JS файлы и включить соответствующую опцию в настройках бакета. Часто встречает в продакшене связку CloudFront + S3.

Где развернуть базу данных?

С базами данных ситуация сложнее, в основном предлагают развернуть БД за большие деньги. Такой вариант не подходит если мы хотим захостить небольшое приложение или просто его потестировать без установки БД локально. Вот несколько из вариантов:

  • ElephantSQL, можно развернуть PostreSQL базу данных любого размера. Бесплатный план дает базу с 20 Мб и 5 параллельными подключениями.
  • Mongo Atlas подойдет если нужно развернуть кластер MongoDB. Есть достаточно жирный бесплатный план.
  • В DigitalOcean и AWS Lightsail начиная с 15 долларов в месяц можно развернуть достаточно неплохой сервер БД, который не нужно настраивать и работает из коробки. В Lightsail первые 3 месяца бесплатно.

Полезные ссылки

Pentax 6x7

Сегодня расскажу о своей первой среднеформатной камере — Pentax 6x7. Идея попробовать средний формат возникла где-то полтора года назад, с тех пор я время от времени заходил на OLX (сайт с объявлениями) и просматривал что там есть. Изначально я смотрел в сторону советских фотоаппаратов таких как Киев-60 и Киев-88. Однажды я чисто случайно наткнулся на объявление по продаже Pentax. Цена была вполне вменяема да и состояние вроде как хорошее. Попросил у продавца дополнительных фото и оказалось что камера в отличном состоянии. Решил что стоит взять.

Впервые камера была представлена в 1969 компанией Asahi Pentax и производилась до 2000-х. Главная особенность камеры — форм-фактор, по сути это сильно увеличенная 35 мм камера. Но Pentax не является первой камерой в таком форма-факторе. С середины 1950-х годов в Германии выпускались камеры Pentacon Six.

Камеры выпускались в трех модификациях: Pentax 6x7 (мой экземпляр), Pentax 67 с добавленной функцией приподнятия зеркала и Pentax 67II. Последняя версия стала легче за счет добавления пластиковых деталей. Также в нее добавили экран и кнопки, которые позволяют настраивать камеру. Но и цена за нее в среднем в два раза больше предыдущего поколения.

Вторая отличительная черта камеры это размер и вес. Она получилась очень большой и тяжелой. Вместе с моим 150 мм. объективом она весит более 2.8 кг. Чтобы немного улучшить ситуацию вместе с камерой выпускалась деревянная ручка, которая немного спасала ситуацию, она чем-то напоминает рукоять старых станковых пулеметов.

Из других особенностей можно отметить:

  • Камера не является полностью механической. Спуском управляет небольшой механизм, который питается от батареек 4SR44.
  • Камера имеет встроенный в пентапризму экспонометр, который соединен с телом камеры с помощью цепного механизма, который легко повредить. Поэтому при разборе камеры нужно сначала снять объектив и только потом пентапризму.
  • Звук зеркала при спуске очень громкий, иногда люди оглядываются по сторонам.
  • Большое зеркало также порождает большую вибрацию, которая мешает делать кадры на выдержках длиннее 1/60.

Веб аналитика на коленке с помощью AWS

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

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

Это не какая-то уникальная проблема, обычно для таких задач берут Google Tag Manager. Но никто из нас не умел им пользоваться да и желания изучать особо не было.

И тут внезапно пришла идея как это сделать. Можно взять AWS Lambda, набросать на коленке пару строк кода, которые будут получать событие и куда-то их складывать для дальнейшего анализа. Для места хранения метрик выбрал CloudWatch. Он как раз умеет анализировать разные метрики/логи и строить красивые дашборды.

Также хотелось получать письма на почту с информацией про самые важные события. Для этого взяли SNS.

В итоге пользователь заходит на сайт, делает какое-то действие, оно летит в лямбду, которая просто кладет информацию в логи и отправляет сообщение в SNS. Дальше идем в CloudWatch и смотрим на дашборды и анализируем полученную информацию.

Актуальная аналитика

В итоге получилась относительно простая и даже гибкая аналитика, которую можно использовать сразу на нескольких проектах. Сейчас она обходиться в 0$, нам хватает одного миллиона бесплатных вызовов лямбды в месяц.

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

Паттерн «Репозиторий»

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

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

Самая большая проблема этого шаблона — множество вопросов, которые возникают при его реализации и дальнейшей поддержки.

Изначально хотел сам расписать проблемы, но за меня это уже сделали, поэтому настоятельно советую прочитать статью: «Проблемы паттерна Репозиторий».

Если кратно, то:

  • Что делать если репозиториям нужно использовать закрытые методы друг друга?
  • Можно ли использовать один репозиторий на весь проект или делать репозиторий на каждую сущность?
  • Нужно ли дублировать методы репозитория в сервис или мы можем напрямую использовать репозиторий в контроллерах?
  • Нужно ли возвращать IQueryable и как это повлияет на дизайн системы в случае с .NET кодом? Если нет, то как правильно изменять сущности без использования Change Tracking?
  • Как правильно объединить репозиторий и UoW?

Отдельно также хочу отметить доклад: Денис Цветцих «Repository и UnitOfWork в 2020 году, must have или антипаттерн?». В нем также поднимаются проблемы репозитория, способы их решения и варианты замены этот шаблона на другой.

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

Для себя я решил что репозиторий хорошо подходит, когда вся логика приложения вписывается в CRUD модель. Но если логика более сложная или приложение подразумевает Task Base UI, тогда лучше прибегнуть к подходу CQRS. Он позволяет разбить сложную бизнес логику включая репозитории на независимые объекты, каждый из которых выполняет только одну бизнес задачу или use case.

Я кроме все прочего не люблю репозитории за:

  • Ограничение функциональности ORM, большенство специфических операций недоступны. В зависимости от реализации можем потерять Change Tracking.
  • Дополнительный мапинг из доменных объектов в DTO.
  • Дополнительный слой абстракции, который зачастую просто не нужен. Особенно если речь идет о небольших сервисах где логику нужно делать максимально простой и незамысловатой.
  • Работает только в простых CRUD сценариях.

Аренда велосипедов BikeNow

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

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

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

Одним субботним утром решили взять велики и часик покататься по району. В итоге объехали половину Киева, за 5 часов и намотав чуть больше 35 км.

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

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

Официальное приложение BikeNow.

Из минусов, приложение, которое имеет очень большую погрешность при подсчете расстояния. Приложение показывает 95 км, в то время как Apple Watch с модулем GPS показывают 133 км.

Цены очень даже адекватные, особенно если сравнивать с арендой электро-самокатов. Взять велосипед стоит 5 гривен, минута 50 копеек. Есть возможность купить подписку на 150 дней за 2500 грн, которая позволяет кататься неограниченное количество раз длинной до 30 минут.

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

Короче, советую!

Очереди сообщений

При работе с большими системами, которые состоят из множества компонентов, возникает вопрос: «Как интегрировать несколько приложений для работы друг с другом?». Для этого нужен надёжный, асинхронный и быстрый способ передачи данных, который также позволит передавать информацию в разных форматах.

Все эти свойства есть в системе обмена сообщениями. Они полезны когда:

  • Нужно отправить данные из точки А в точку Б.
  • Нужно интегрировать несколько систем.
  • Нужно масштабирование.
  • Нужна возможность мониторить потоки данных.
  • Нужна асинхронная обработка.
  • Нужна буферизация.

Концепт обмена сообщениями

Обмен сообщениями неновое изобретение. Он активно используется внутри операционной системы для обмена информацией между несколькими процессами (inter-process communication) и для обмена между несколькими потоками внутри процесса (inter-thread communication).

Самый простой способ представить очередь сообщений как длинную трубу в которую можно помещать шарики. Мы можем написать сообщение на шаре и закинуть его в трубу и кто-то получит его на другой стороне. Из такой концепции можно понять несколько вещей.

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

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

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

Причины использовать очереди сообщений

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

Масштабирование
Очереди распределяют процессы обработки информации. Это дает возможность гибко реагировать на нагрузку и добавлять или убирать дополнительные обработчики.

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

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

Decoupling and coupling
С помощью очередей можно достичь двух противоположных целей.

  • Decoupling. Если у нас большой монолит, то будет сложно интегрировать новые фичи. В таком случае очередь сообщений позволяет разъединить одно приложения на несколько независимых компонентов и настроить коммуникацию между ними.
  • Coupling. Иногда нужно, чтобы несколько систем работали как одно целое, очередь позволяют создать промежуточный слой для коммуникации между разными элементами системы.

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

Гарантированная доставка

Гарантированная доставка является одной из ключевых характеристик системы обмена сообщений. Всего есть три типа доставки:

  • at least once
  • at most once
  • exactly once

At least once
Самый простой способ доставки при котором обработчик получает одно и то же сообщение до тех пор, пока не удалит его из очереди или не подтвердит получение. Это значит что возможны ситуации, когда приложение обрабатывает одно сообщение несколько раз.

Такая особенность подразумевает, что обработчик будет корректно работать в случае дублирования сообщений. А такое может случаться часто. Например, упала сеть в момент, когда приложение подтверждало получение — оно получит его ещё раз.

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

С другой стороны, такой подход гарантирует 100% получение сообщения. Даже если получатель сообщения упадёт, до того как подтвердит обработку, то он просто ещё раз его обработает после перезапуска.

At most once
Очень редкий сценарий, к нему прибегают когда двойная обработка сообщения может привести к серьезным проблемам. В таких очередях мы предпочтем потерять сообщение чем обработать его дважды.

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

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

Все они происходят из двух утверждений:

  • Отправители и получатели не идеальны
  • Сеть не идеальна.

Что порождает такие проблемы как:

  • Отправитель может забыть отправить сообщение
  • Сеть между отправителем и очередью может упасть
  • Сеть между очередью и получателем может упасть
  • База данных самой очереди может не сохранить сообщение
  • Подтверждение что сообщение обработано может не дойти до очереди и отправителя

Именно поэтому очень сложно гарантировать одноразовую доставку сообщения. Намного проще сделать систему устойчивой к дубликатам сообщений и использовать подход at-least-once.

Компоненты очереди сообщений

Система обмена сообщениями состоит из нескольких компонентов, которые позволяют ей нормально функционировать и выполнять свои задачи.

Сообщения — любые данные, которые нужно передать через очередь конвертируются в сообщение, которые состоят из двух частей:

  • Заголовки (headers) — в них расположена служебная информация, которая используется самой очередью для правильной обработки сообщения.
  • Тело (body) — информация, которую мы передаем с помощью очереди.

Каналы (channels) — логические соединения между приложениями и системой очередей, которые предоставляют изолированную коммуникацию. Каналы позволяют передавать сообщение в одном из двух режимов:

  • point-to-point — протокол, который обеспечивает прямую коммуникацию между двумя приложениями.
  • publish-subscribe — протокол, в котором отправитель сообщения не знает конкретного получателя, а просто отправляет сообщение в очередь, на которую могут быть подписаны потребители.

Маршрутизатор (router) — помещает сообщения из каналов по разным очередям используя ключ маршрутизации из заголовков сообщения.

Очередь (queue) — хранилище для наших сообщений, которое может находиться как в оперативной памяти так и на диске.

Виды очередей

Очереди сообщений можно разделить на несколько видов. Обычно несколько видом могут быть реализованы в одном продукте.

broadcast (fanout) exchange
В таком случае отправляется копия сообщения во все доступные очереди, ключ маршрутизации игнорируется.

direct exchange
Все сообщения имеют свой ключ маршрутизации, который определяет в какую очередь нужно положить сообщение. В дальнейшем сообщения будут переданы по принципу Round Robin подписанным обработчикам. Это значит что только один обработчик получит сообщение.

topic exchange (multicast)
Такие очереди подписаны на получение сообщений чей ключ подпадает под определенный паттерн. Если ключ маршрутизации подходит для нескольких очередей, то каждая получит по своей копии.

Обычно ключи маршрутизации стараются делать в иерархическом виде. Это достигается за счет разделения логических частей (слов) точками. Например вот так:

[region].[availability-zone].[service].[instance]

eu-east.az1.computer.homepc

Сами же паттерны создаются с использованием специальных символов:

  • * — заменитель только для одного слова.
  • # — заменитель для нескольких слов

Что позволяет сделать такие шаблоны:

  • *.*.computer.* — очередь, которая может обработать сообщения только от компьютеров без разницы где они находятся.
  • eu-east.# — очередь, которая может обрабатывать сообщения только из зоны eu-east

Протоколы

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

Спустя какое-то время появились три открытых стандарта, которые сейчас повсеместно используются:

  • Advanced Message Queuing Protocol (AMQP) — бинарный протокол, который проектировался для  взаимодействия между различными вендорами и стал заменой существующих проприетарных протоколов. Основными особенностями AMQP является надежности и совместимость.
  • Streaming Text Oriented Messaging Protocol (STOMP) — простой текстовый протокол обмена сообщениями, который очень похож на HTTP и работает поверх TCP.
  • MQTT (formerly MQ Telemetry Transport) — очень простой и легковесный протокол, который разрабатывался для минимального использования трафика и работы в нестабильной сети. Все эти качества идеально подошли для использования протокола для общения между устройствами.

Материалы

  • Message queues — отличная статья, которая описывает основные концепты работы очередей.
  • The Big Little Guide to Message Queues — большой гайд в котором описано про причины создания очередей их свойства и особенности работы, а также кратный разбор самых популярных реализаций.

Коллекции в C#

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

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

Все коллекции лежат в нескольких пространствах имен:

  • System.Collections — простые необобщенные коллекции.
  • System.Collections.Generic — обобщенные коллекции.
  • System.Collections.Specialized — специальные коллекции.
  • System.Collections.Concurrent — коллекции для работы в многопоточной среде.

Устройство коллекций

Все коллекции, так или иначе, реализую интерфейс ICollection, некоторые реализуют интерфейсы IList и IDictionary (которые внутри наследуют ICollection). Этот интерфейс предоставляет минимальный набор методов, которые позволяют реализовать коллекцию.

В свою очередь, ICollection расширяет интерфейс IEnumerable. Он предоставляет нумератор, который позволяет обходить коллекции элемент за элементом. Именно этот интерфейс позволяет использовать коллекции в цикле foreach.

Вместительность коллекций

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

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

Поэтому коллекции, которые основаны на массивах имеют сложность вставки:

  • O(1) — когда вместительности достаточно.
  • O(n) — когда вместительности недостаточно и нужно копировать данные в массив побольше.

Чтобы избежать уменьшения производительности нужно при создании коллекции указать необходимую нам вместительности. Это позволит уменьшить количество копирований.

Сравнивание и сортировка элементов коллекций

Сравнение
Такие методы как Contains, IndexOf, LastIndexOf, and Remove используют сравнение элементов для свое работы. Если коллекция является обобщенной, то используются два механизма сравнения:

  • Если тип реализует интерфейс IEquatable тогда механизм сравнения использует метод Equals этого интерфейса.
  • Если тип не реализует интерфейс IEquatable тогда для сравнения используется Object.Equals

Некоторые коллекции имеют конструктор, который принимает имплементацию IEqualityComparer который используется для сравнения.

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

Сортировка по умолчанию подразумевает, что типы хранящиеся в коллекции реализуют интерфейс IComparable, чьи методы под капотом используют коллекции для сравнения.

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

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

System.InvalidOperationException: Failed to compare two elements in the array.
	System.ArgumentException: At least one object must implement IComparable.

Алгоритмическая сложность коллекций

Список List

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

var linkedList = new List<string>();
linkedList.Add("A");
linkedList.Add("B");
linkedList.Add("C");

Для своей работы списки используют обычные массивы. Это значит что могут быть проблемы с производительностью из-за частого создания нового массива. Также у списков есть еще два нюанса:

  • Вставка элемента в середину списка приведет к созданию нового массива и копирование данных, что негативно влияет на производительность.
  • Списки не могут хранить экстремально огромное количество элементов из-за фрагментации адресного пространства.

Если эти проблемы существенны для вас, то стоит присмотреться к LinkedList или ImmutableList

Связанный список LinkedList

Класс LinkedList реализует простой двухсвязный список, каждый элемент которого имеет ссылка на предыдущий и следующий элемент.

Каждый элемент списка оборачивается в специальный класс LinkedListNode, который имеет ссылку на следующий элемент (Next), на предыдущий элемент (Previous) и само значение (Value).

var linkedList = new LinkedList<string>();
linkedList.AddFirst("A");
linkedList.AddLast("B");
linkedList.AddLast("C");
		
Console.WriteLine(linkedList.First.Previous == null); // True
Console.WriteLine(linkedList.Last.Next == null);   // True

Связанный список позволяет вставлять и удалять элементы со сложностью O (1). Также мы можем удалить элемент и заново вставить в тот же или другой список без дополнительного выделения памяти.

Словарь Dictionary

Словари хранят данные в виде ключ-значение. Каждый элемент словаря представляет из себя объект структуры KeyValuePair.

В качестве ключа можно использовать любой объект. Ключи должны быть уникальными в рамках коллекции.

var linkedList = new Dictionary<string, string>();
linkedList.Add("key1", "A");
linkedList.Add("key2", "B");
linkedList.Add("key3", "C");

Внутри словари построены на базе хеш-таблицы, что позволяет очень быстро вставлять элементы и получать по ключу (сложность O (1)). Сами же хеш-таблицы, в свою очередь, реализованы с помощью массивов.

Для адресации значений внутри коллекции используются хеш коды ключей. Это значит что объект ключа не должен изменяться, потому что это приведет к изменению хеш кода, что в свою очередь приведет к потере данных.

Исходники Dictionary

Стек Stack и Очередь Queue

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

Стеки Stack — реализуют подход LIFO (last in — first out).

var stack = new Stack<int>();
stack.Push(1); // stack = [1]
stack.Push(2); // stack = [1,2]
var item = stack.Pop(); // stack = [1], item = 2

Очереди Queue — реализуют подход (first in — first out).

var queue = new Queue<int>();
queue.Enqueue(1); // queue = [1]
queue.Enqueue(2); // queue = [1,2]
item = queue.Dequeue(); // queue = [2], item = 1

Внутри они реализованы с помощью обычных массивов.

Множества HashSet и SortedSet

Эти классы реализуют математические множества. По своей природе множество — набор уникальных элементов с общими характеристиками, в нашем случае одного типа.

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

Внутренняя реализация этих классов отличается:

  • HashSet — множество, построенное на базе хеш-таблицы.
  • SortedSet — отсортированное множество, построенное на базе красно-черного дерева.


ISet<int> set = new HashSet<int> { 1, 2, 3, 4, 5 };
 
set.UnionWith(new[] { 5, 6 });              // set = { 1, 2, 3, 4, 5, 6 }
set.IntersectWith(new[] { 3, 4, 5, 6, 7 }); // set = { 3, 4, 5, 6 }
set.ExceptWith(new[] { 6, 7 });             // set = { 3, 4, 5 }
set.SymmetricExceptWith(new[] { 4, 5, 6 }); // set = { 3, 6 }

Можно заметить что LINQ предоставляет несколько похожих операций (Distinct, Union, Intersect, Except), которые можно выполнить с любой коллекцией. Но HastSet предоставляет намного больший набор операций с множествами.

Основная разница в том что методы множеств изменяют текущую коллекцию, в то время как LINQ методы всегда создают новый экземпляр коллекции.

KeyedCollection

KeyedCollection это абстрактный класс, который позволяет построить собственную коллекцию.

Эта коллекция является гибридом между списками (IList) и словарями (IDictionary). От списков ей досталась возможность получать элементы по индексу, а от словарей возможность получать элемент по ключу.

В отличие от словарей, элемент KeyedCollection коллекции не является парой ключ-значение, вместо этого весь элемент является значением, а в качестве ключа используется его поле, свойство или любое другое значение. Для получения ключа используется абстрактный метод, который является обязательным для реализации. GetKeyForItem

var keyedCollection = new UserCollection();
keyedCollection.Add(new User {
	Id = 1,
	Name = "A"
});
keyedCollection.Add(new User {
	Id = 2,
	Name = "B"
});
		
Console.WriteLine(keyedCollection[2].Name); // B
	
public class UserCollection: KeyedCollection<int, User>
{
	protected override int GetKeyForItem(User user) => user.Id;
}
	
public class User
{
	public int Id {get;set;}
	public string Name {get;set;}
}

Внутри KeyedCollection построен на базе двух структур данных. Для быстрого получения данных по ключу используется словарь, а для получение элемента по индексу используется массив.

NameValueCollection

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

Особенной эту коллекцию делает то что однин ключ может содержать несколько эллементов.

var namedCollection = new NameValueCollection();
namedCollection.Add("key1", "value1");
namedCollection.Add("key2", "value2");
namedCollection.Add("key1", "value3");

Console.WriteLine(namedCollection.Count);   // 2
Console.WriteLine(namedCollection["key1"]); // value1,value3

Иммутабельные коллекции

Иммутабельные коллекции не входят в стандартную библиотеку классов (BCL). Для их использования нужно установить System.Collections.Immutable NuGet пакет.

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

Сами же коллекции можно поделить на несколько видов:

  • Mutable — обычные коллекции которые поддерживают изменения.
  • Immutable — коллекции, которые полностью запрещают изменения. Хотя на самом деле любое изменение иммутабельной коллекции приводит к созданию новой.
  • ReadOnly — обертки над стандартными коллекциями, которые не дают поменять данные. Из-за того что это всего лишь обертка мы можем поменять данные в оригинальной коллекции и ead only коллекция подтянет изменения.

Детальнее можно ознакомиться в статье: Read only, frozen, and immutable collections.

Иммутабельные стеки ImmutableStack и очереди ImmutableQueue

Ничем не отличаются от обычных стеков и очередей, кроме того, что являются не изменяемыми.

Для реализации иммутабельных стеков/очередей массивы не подойдут. Причина заключается в том, что на каждое изменение придётся делать полную копию массива что очень неэффективно.

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

Иммутабельные списки ImmutableList

Под капотом используют сбалансированное бинарное дерево вместо массива или связанного списка.

Массивы не подходят из-за накладных расходов на их использование. Связанный список тоже не подойдет, потому что ImmutableList поддерживает обращение по индексу из-за чего нужно долго перебирать связанные элементы. Поэтому для нормальной работы иммутабельных списков используют сбалансированное бинарное дерево.

Иммутабельные массивы ImmutableArray

По сути, это небольшая прослойка над обычными массивами и все. Любые мутабельные операции приводят к копированию массива. Из-за этого скорость добавления элементов равна O (n), но в то же время получение элемента по индексу занимает O (1).

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

Иммутабельные словари ImmutableDictionary

Неизменяемые словари внутри работают на базе сбалансированного дерева, но с одной особенностью. Каждый элемент внутри коллекции представлен в виде отдельного дерева (ImmutableList>). Так что по своей сути иммутабельные словари — это деревья деревьев.

Из-за своей особенности иммутабельные словари потребляют очень много памяти и долго работают. Поэтому стоит аккуратно их использовать.

Особенности использования

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

var immutableList = new[] { 1, 2, 3 }.ToImmutableList();
immutableList = immutableList.Add(4);

По идее чтобы было проще, мы можем объединить все изменения в цепочку:

immutableList = immutableList
    .Add(5)
    .Add(6)
    .Add(7);

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

Чтобы минимизировать проблему стоит использовать методы, которые могут выполнить нужные изменения за один вызов. Например:

immutableList = immutableList.AddRange(new[] { 5, 6, 7 });

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

immutableList = immutableList
    .Add(6)
    .Remove(2);

Для решения этой проблемы иммутабельные коллекции предоставляют билдеры (builders).

var builder = immutableList.ToBuilder();
builder.Add(6);
builder.Remove(2);
immutableList = builder.ToImmutable();

Внутри себя билдеры используют соответствующую мутабельную коллекцию, что позволяет выполнить все операции над одним экземпляром коллекции. Только после вызова метода ToImmutable экземпляр снова будет неизменяемым. Таким образом можно сократить объем работы уборщика мусора.

Источники и доп. материалы

  • How to Choose the Right .NET Collection Class? Отличная статья про то какую коллекцию выбрать в .NET. И вообще хорошо хоть и поверхностно описаны конкурентные и неизменяемые коллекции.
  • .NET Collections: comparing performance and memory usage. Сравнивались коллекции-словари и среди них лучше всего отработал Dictionary, SortedList в свою очередь в среднем потреблял в два раза меньше памяти чем обычный словарь. Хуже всего себя показал отсортированный словарь SortedDictionary.
  • Collections and Data Structures. Отличное описание коллекций на MSDN.
Ранее Ctrl + ↓