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

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

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

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.

AWS Cloud Practitioner

На днях пополнил коллекцию сертификатов еще одним — AWS Cloud Practitioner.

К AWS присматривался давно, на текущий момент это самое популярное облако. C Нового года я работаю на новом проекте, который активно использует AWS. Это стало дополнительным мотиватором, чтобы разобраться с тем как всё устроено.

В качестве своего рода чекпоинта решил получить сертификат.

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

Источники для подготовки:

Сам экзамен проходили в офлайне, что, как по мне, намного удобнее, чем в онлайне.

 Нет комментариев    119   7 мес  

Балансировка нагрузки

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

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

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

Обнаружение служб

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

  • Файлы конфигурации.
  • DNS
  • Zookeper, Consul и т. д.

Проверка работоспособности

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

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

В активном режиме балансировщик периодически делает запросы на специальный эндпоинт, который проверяет состояние приложения. Их также можно разделить на несколько типов: liveness и readiness

Алгоритмы балансировки нагрузки

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

  • DNS
  • Sticky Session
  • Round Robin
  • Weighted Round Robin
  • Least Connection
  • IP Hash

DNS
Самый простой способ распределить запросы это использовать DNS, он позволяет работать клиентам с несколькими серверами и повысить их доступность.  Для этого достаточно зарегистрировать несколько серверов на одно доменное имя. Когда клиент запрашивает IP адрес, DNS возвращает список адресов серверов, который каждый раз начинается с другого адреса. Такой подход похож на работу алгоритма Round Robin.

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

Round Robin
Самый простой алгоритм. Балансировщик держит обычную очередь из серверов. Первый сервер в очереди обрабатывает запрос и помещается в конец очереди и так по кругу. Таким образом сервера равномерно нагружены.

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

Weighted Round Robin
Тот же round robin, но имеет дополнительное свойство — вес сервера. С его помощью мы можем указать балансировщику сколько трафика отправлять на тот или иной сервер. Так сервера помощнее будут иметь больший вес и соответственно обрабатывать больше запросов чем другие сервера.

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

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

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

Примеры софтверных балансировщиков

  • HAProxy
  • nginx
  • AWS Route 53
  • AWS Elastic Load Balancer

Дополнительная информация

  • The power of two random choices. Автор предлагает отказаться от централизованной балансировки нагрузки и использовать балансировку на стороне клиента. Для этого информации и загруженности серверов сохраняется в каком-то кэше, который время от времени обновляется. Сами же клиенты будут случайным образом выбирать два сервера и отправлять запрос на менее загруженных из двух. Такой подход позволяет оптимально распределять нагрузку и избавиться от центрального балансировщика.
  • Introduction to modern network load balancing and proxying. Хорошая статья в которой описаны способы использования балансировщиков нагрузки и в чем разница между L4 и L7 балансировкой. Перевод на русский.

Небольшое объявление

Привет всем кто меня читает 👋🏻

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

В ближайшее время планирую сделать несколько заметок по AWS (EC2, Lamdba, API Gateway). Обновить или полностью переписать старые статьи по архитектуре.

Stay tuned 🚀

 Нет комментариев    158   8 мес  
Ранее Ctrl + ↓