Вадим Мартынов @Vadimyan
Программист
Information
- Rating
- 57-th
- Location
- Ростов-на-Дону, Ростовская обл., Россия
- Works in
- Date of birth
- Registered
- Activity
Specialization
Backend Developer, Community manager
Lead
C#
.NET Core
Database
High-loaded systems
Designing application architecture
Database design
Привет. Рассказывал про доклад про метрики доступности, вышла запись, поэтому доношу сюда в комменты.
Про аппаратную часть, сетевую инфраструктуру и комплексность замечания всё ещё в ней не рассмотрены, но про это я уже ниже писал ответ про уровни абстракции и зоны ответственности :)
О, правильные и действительно безопасные деградации — это отдельная весёлая тема. Они чаще всего опираются на конкретный домен, так что мне сложно будет привести хорошие примеры. Если с цветом машины решение несложное, то есть ситуации, когда действительно сложно подобрать правильный способ замены. Как пользователь я бы скорее хотел чтобы марка-номер не отображались, но машина приехала, мы там в чате или по телефону договоримся. Но у меня есть ещё пару любимых из личного опыта.
Доставки-агрегаторы из магазинов часто не могут хорошо синхронизироваться по наличию товара с магазинами, а если и могут, то это очень ненадёжная синхронизация. И решили проблему довольно просто — заказывай, что хочешь, сборщик тебе позвонит и уточнит детали, или по галочке выберет аналог.
Не совсем деградация, но некоторый компромисс в ситуации, когда на складе у тебя 100 товаров, а пользователи за 1 секунду заказывают 500. Ситуация выглядит нереалистично, но вот рассказ про что-то подобное.
Цвет машины — не обязательно сервис. Это может метод внутри крупного сервиса, который умеет фейлится, и вопрос выбора фоллбека — это то, как обрабатывать ошибку вызова этого метода. Тут уровень абстракции у каждого свой.
Постарался показать пример с коэффициентом готовности как раз на примере с теневой репликой, но в остальном, конечно, эта часть тут довольно сильно обрезана. У меня есть немного расширенная версия в виде задачи, где можно как раз это пощупать руками и обжечься. С вот таким примером, где сервис A зависит от данных из двух других сервисов, поэтому может работать, но не быть в работоспособном состоянии :)
Вообще да, но когда дело доходит до систем, где есть хотя бы несколько сотен сервисов и столько же БД, всё это над несколькими уровнями инфраструктуры и с зависимыми элементами, то все переходы между состояниями обычно становится нереалистично описать, а считать это целиком может быть избыточно.
Я именно поэтому постарался сделать дисклеймер о том, что эта модель не точно отражает реальность. Но она может подсвечивать критичные места в достаточной степени для принятия решений.
Да, это довольно обсуждаемые в последнее время статьи, про них стоит говорить отдельно. Идея того, что ЦПТ не всегда хорошо — хорошая. Среднее нам действительно не важно. И фактически в примере с устойчивостью и временем восстановления вы чаще всего будете видеть большую дисперсию и скопление большинства точек "слева". Правильнее тут говорить про процентили (то есть, избавление от длинного хвоста). И качественные исследования хорошо делать как раз на выбросах. А ещё про waste при координации у меня есть вот такой доклад, который как раз про "Hidden Costs of Coordination" из статьи.
Вообще, этот и предыдущий ваш комментарий хорошо ложатся в близкую тему про то, как быть с SLO, и про эти статьи, и про посервисность, и про работу с зависимостями, которые влияют на доступность, я встречал хорошие обсуждения в телеге в ALLSLO - все про SLO (из SRE) — там довольно концентрированно именно об SLO обычно.
Это крутой вопрос, я постараюсь ответить компактно.
Когда я писал этот текст, то представлял себе именно сервис в понимании разработчика — потому что это обычно тот уровень абстракции, с которыми чаще всего взаимодействуют команды разработки.
Здесь же вся статья про модель, то есть упрощение. И мне кажется, что будет хорошо применять это упрощение так, как удобно для наилучшего результата. Мне модель с "услугой" кажется удобной с точки зрения клиентского SLA, но избыточно высокоуровневой для применения при взаимодействии команд разработки. Но "сервис" это тоже не совсем удобная единица работы — потому что чаще всего недоступность сервиса != недоступности какого-то сценария. И в этом плане я бы скорее говорил не в терминах услуг, а в отдельных "функциональностях" — поиск, страница товара, корзина. Так будет достаточно детализированно чтобы локализовать проблему и определить действия. Такие подсчёты требуют дополнительной разметки или дополнительных индикаторов, и разметка по функциональности (связь между функциональностью и метриками + их связь на домены и сервисы) требует дополнительных усилий. То есть, это хорошо, но довольно дорого. Поэтому в базовом варианте можно смотреть именно на сервис. но стремиться к функциональностям.
Такую разметку функциональности может хорошо помочь сделать DOMA — в ней как раз деление по доменам проходит часто по функциональности и у каждого домена есть свои специфичные метрики, которые и будут доступностью этой функциональности.
С инфрой вообще всё интересно и многогранно, там можно и компоненты копать, и конфигурацию внутри ДЦ, а потом оказывается, что нам важна связность между ДЦ, каналы, а там могут быть спецэффекты в виде шумных соседей, которые заливают канал. И это только небольшой кусок, виртуализация тоже влияет.
И тут, с одной стороны, важно разделить уровни и зоны ответственности, я поэтому явно и говорю, что формулы на самом деле работают для независимых компонент, которыми сервисы не являются (а сам доклад сделан в backend секции). А с другой (и про это отчасти пункт о изменениях в платформе) — что мы можем или учитывать или влиять на эти «не наши» уровни. Делать load shedding, local-dc и прочие политики маршрутизации, делать системы, которые не боятся вылета шарда и все эти стандартные уже продуманные вещи для устойчивости.
Всё верно, тут речь скорее про индикаторы availability, которые тоже можно определить по-разному в зависти от того, что мы вообще хотим называть доступностью, я про это даже сделал продолжение в виде доклада “Математика SLO” — потому что доступность как раз с ним сильно связана. Думаю, что скоро получу запись.
Но стоит признать, что в литературе по теме определение надежности из статьи (и похожие определения вокруг вероятности безотказной работы) часто встречаются в количественных показателях надёжности, или вовсе отождествляются ей.
Тут хотелось именно показать из чего складывается эта цифра доли отказов, что на неё влияет и как это можно оценить.
Тут вы абсолютно правы, я раскрываю эту мысль в детальном примере с фолбеками:
И в примере до этого с кэшем тоже описана проблема того, что, кроме надёжности самого внешнего кэша, есть ещё и логика, которая его наливает и инвалидирует.
Но глобально даже заглушку нельзя считать на 100% надёжной потому что это код, который подвержен ошибкам.
Я как-то очень по-разному могу понять этот вопрос.
Использовать готовые методы для авторизации или документации не должно быть проблемой.
Если вопрос к общим регистрациям, то исходная статья говорит об этом вот так: "Настройка общей инфраструктуры (например, логирование, аутентификация, мидлвары, swagger, …) приложения также остается в
Program.cs
, потому что она используется во всех модулях."Но я при этом понимаю, что общая инфра как бы усложнит большие рефакторинги типа вынесения модулей в отдельные сервисы, это как-то необходимое зло, кажется.
Если речь о кастомных атрибутах (методах, если мы говорим о Minimal APIs и смотреть на статью, то как раз об этом автор оригинала в конце говорит "Следуя этой архитектуре, вы по-прежнему можете выделить слой Core/Domain и слой Infrastructure в разные проекты. Будете вы это делать или нет? Зависит от размера проекта, и, как мне кажется, было бы неплохо поговорить об этом, чтобы быть одной волне. Лично у меня нет однозначного мнения на этот счет."
В моём представлении это всё — не доменный слой, а значит не обязательно думать об изоляции этого кода внутри модуля.
Согласен, концепция встречалась раньше много раз. Тут как будто сложно найти самый ранний источник. Потому что не совсем понятно, что именно считать началом.
Про feature folders в asp.net mvc писали ещё в 2013ом.
Статья "Vertical slices in ASP.NET MVC" из 2016. И тоже без изоляции фич/модулей.
Сложная настройка поиска вьюх по папкам в статье Feature Slices for ASP.NET Core MVC из MSDN Magazine. Тоже 2016 год.
В "Организации по модулям, а не слоям" на мой взгляд не хватает изоляции и самодостаточности модулей, которая бы включала отделении регистрации доменнозависимых зависимостей внутри модуля.
Мне кажется, что идея как раз в том, чтобы применить идею модулей к Mininal APIs и минималистичному подходу к конфигурации приложения, который пришёл на смену
Startup.cs
. Если раньше модули приходилось реализовывать, прикладывая дополнительные усилия, то в современном .net это делать легче, чем соблюдать "традиционный" подход.Мне кажется, что это вопрос организации кода и реализации. А код из статьи это просто пример.
Можно держать модули в проектах сервисов или внешних проектах, а сервисы в одном репозитории/солюшене.
В зависимости от способа публикации (будут ли попадать "лишние" сборки в папку приложения) тут может быть разный подход, но при реализации нужно будет помнить, что есть шанс выстрелить себе в ногу.
А при локальном дебаге поведение может отличаться, если в этом случае все сборки складываются в одну папку.
Если делить модули на проекты и сервисы на репозитории, то искать по всем сборкам логичнее и этот код авторегистрации нужно переписать.
Эта же проблема всё равно была в случае контроллеров — доменный слой в разных bounded context не должен перемешиваться. Просто контроллеры не подсвечивали явно разные контексты и могли наоборот запутать. Роут
/users/{id}/orders/
же просится подумать, что User тут тот же самый, что и в/users/{id}
, хотя в первом случае у нас статус покупателя, размер скидки и прочие атрибуты клиента, а во втором учетные данные и всякие статусы рассылки.Но я согласен, что держать рамку DDD без дополнительных ограничений и/или проверок не всегда очень просто при развитии проекта.
Мне не кажется, что тут есть однозначный ответ.
Можно смотреть на то, что говорит DDD по этому поводу. Между несколькими bounded context могут быть "одинаковые" сущности, которые отражают разные данные и разное представление об одном объекте реального мира, но могут иметь одинаковые свойства. В доменах они будут представлены разными типами и даже храниться в разных таблицах на уровне БД. В этом случае ответ понятный — разным доменам могут соответствовать разные контексты.
При хранении в одной таблице на самом деле тоже можно разделять контексты (EF Core умеет маппить в двух контекстах одну таблицу с разным набором свойств). Другое дело, что при последующем возможном разделении сервисов, когда один из модулей будет переноситься в отдельный сервис, мы получим не микросервисы, а распределенный монолит, который ковыряется в одной базе, ещё и в одной таблице разными сервисами.
Наверное, самый подходящий ответ — зависит. Потому что вопрос тут в сценарии развития. В самой статье есть фраза "Настройка общей инфраструктуры (например, логирование, аутентификация, мидлвары, swagger, …) приложения также остается в
Program.cs
, потому что она используется во всех модулях". В этом смысле DbContext может быть общим в рамках всего приложения (и это даже не будет противоречить DDD потому что контекст БД это не слой домена).И, собственно, тоже самое можно сказать и про другие разделяемые между модулями зависимостями - если это не протекание доменного слоя, то вполне нормально шарить такие вещи.
На самом деле безконтроллерность же у нас была и раньше в разных проявлениях, а не только в "Minimal APIs"-like виде.
Уже, кажется, не поддерживаемый Nancy, который мне кажется довольно похожим на FastEndpoints.
Тот же Carter, на который есть ссылка в статье.
Довольно необычный ServiceStack, в котором
[Route]
вообще вешается на модели.FastEndpoints после Minimal APIs выглядит самым привычным из всех. При этом он всё ещё смешивает маршрутизацию и обратку в одном месте, а в каком-то идеализированном варианте я вижу их разделенными. Условно, это
app.MapGet<TRequest, TResponse>("myRoute")
, который умеет находить хендлер по переданным generic-параметрам. Но в таком случае мы опять возвращаемся к проблемам навигации, как с MediatR.И .NET 7 на мой взгляд расширил эту концепцию с помощью тех же Route groups (и общего развития этого типа приложений). Но в разговорах о применимости minimal APIs я всё ещё довольно часто встречаю подход "Minimal APIs для экспериментов / микропроектов, для всего крупнее используйте контроллеры".
Идея изначальной статьи, кажется, в том, что стоит посмотреть на Minimal APIs как полноценную замену контроллеров. И это ещё и облегчит реализацию domain-driven подхода. В общем случае это касается в разной степени всех MVC-фреймворков.
Да, тут вышла путаница у меня с терминами из-за того, что в общем случае термин heapify применяется к процедуре, которая для конкретного элемента ставит его на нужное место чтобы куча стала в согласованное состояние:
> This function takes in an element index
index
, and maintains the min heap property, by swapping with the smallest element of its immediate sub-tree.И как раз heapify имеет сложность в зависимости от высоты. Только вот в внутренней реализации .net метод
Heapify
производит операцию над каждым элементом, поэтому в итоге мы всё равно получим сложность равную сложности построения кучи.А ещё
Heapify
в случаеEnqueueRange
будет работать только для пустой очереди, если там уже были элементы, то последовательно вызовется многоEnqueue
. В общем, метод получается скорее сахарный, чем оптимизирующий, таковым он будет только для сценария пустой кучи.Спасибо за дополнение, действтительно очередь с приоритетами не гарантирует сохранение порядка для элементов с одинаковым приоритетом, об этом даже явно сказано в документации, поэтому поправил статью.
> Note that the type does not guarantee first-in-first-out semantics for elements of equal priority.
Да, мапперы это история скорее про организацию кода. И иногда про валидацию, например, благодаря
AssertConfigurationIsValid
. То есть, ручная конвертация будет в любом случае быстрее (или сопоставима по скорости в случае с кодогенерируемым маппингом).Я для себя вижу такие потенциальные недостатки или опасности ручного маппинга:
В сложных моделях, когда метод конвертации зависим от пары других методов конвертации могут начаться проблемы с организацией кода и тем, где он лежит. Чаще всего с хорошей организацией моделек таких проблем не случается, но это место, где может появиться вторая реализация маппинга потому что использовали не метод, а написали свою конвертацию подмодельки.
(В немикросервисах) через пару лет поддержки можно обнаружить 3-4 метода конвертации одной и той же модельки в разных местах прото от того, что разработчик не нашёл существующий метод конвертвции.
Сценарий "добавил свойство, не протащил в конвертер" — это как раз то, что ловится в библиотеках маппинга валидацией.
Все эти риски — явная проблема качества и организации кода, так что можно просто никогда не ошибаться и этих проблем не будет.
На самом деле в стороннем бенчмарке из статьи есть сравнение как раз с ручным маппингом в методе. Мне, правда, это сравнение кажется не совсем честным — например, там повсеместно используется LINQ, что очевидно будет замедлять код. Собственно, ручной маппинг там на 2-3 месте обычно. Без LINQ и на моих модельках результаты немного другие, Mapster вдвое медленнее на комплексных типах и чуть медленнее на простых.
У Mapster есть аналогичная ProjectTo поддержка EF какр раз через вскольз упомянутый ProjectToType, который работает с
IQueryable
. Можно попробовать сравнить. Тут вопрос, будет ли, например, хорошим сравнением использованием InMemory или хочется именно данных на реальной "железной" БД? И нужно, конечно, сравнивать не "очевидные" сценарии, где маппинг явный.