Pull to refresh

Comments 173

Спасибо за статью! Я буду благодарен, если вы поделитесь своим мнением по следующему вопросу.

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

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

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

микросервисы — это про горизонтальное масштабирование. Монолит плохо и нерационально масштабируется.

То что описано в статье по вашему монолит?

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

В большинстве случаев декомпозиция в монолите выполнена намного хуже, чем в микросервисах. Типичный пример — у каждого stateful микросервиса обязательно собственная БД. В монолитах использование десятка разных БД (хотя бы на уровне разных логинов и database в одном MySQL) практикуется… практически никогда. Помимо изоляции данных, далеко не каждый язык программирования позволяет действительно надёжно изолировать разные части монолита друг от друга, не на уровне соглашений между разработчиками, а с жёстким контролем средствами языка (напр. в Go поддержка internal пакетов появилась всего 3 года назад).


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


Так что в теории да, можно делать микросервисы внутри монолита (встроенные микросервисы), но на практике это пока редко встречается.

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

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

Разве декомпозиция напрямую зависит от выбора архитектурного паттерна монолит/SOA?
Значит ли это, что мир монолитных приложений погряз в проблемах композиции?

В монолитах использование десятка разных БД (хотя бы на уровне разных логинов и database в одном MySQL) практикуется… практически никогда.

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

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

Поясните, пожалуйста, почему SOLID и GRASP не подходят, а требуются какие-то жесткие средства языка.
Обладают ли этими жесткими средствами C++/C/C#/JAVA?

в Go поддержка internal пакетов появилась всего 3 года назад

Можете пояснить ваш use-case использования internal packages в контексте замены SOA на монолит?

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


Есть какие-то исследования, что все ПО написанное до второго бума SOA «не работает»?
Chromium/V8 тоже не работают?

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


Разве декомпозиция напрямую зависит от выбора архитектурного паттерна монолит/SOA?

В теории — нет. Но на практике эта зависимость однозначно наблюдается. Проблема в том, что если у разработчиков есть возможность (а в монолите — она обычно есть) нарушить архитектуру и сделать что-то неправильно, создать какие-то связи между компонентами, которых быть не должно — они сделают это, причём скорее рано, нежели поздно. Из-за отсутствия квалификации, понимания, или просто времени на то, чтобы сделать "правильно". В SOA намного сложнее и дольше создать такие некорректные связи, потому что для того, чтобы добраться до недоступного через API функционала стороннего сервиса из другого сервиса сначала надо изменить и выкатить первый сервис, добавив в него дополнительное API. Эта дополнительная сложность создаёт препятствие, которое довольно эффективно мешает портить текущую архитектуру. Конечно, при наличии должной степени упёртости можно и это преодолеть, но обычно архитектуру портят не из желания сделать гадость, а из желания сделать проще/быстрее — и вот в SOA делать такие вещи уже ни разу не проще и не быстрее, что очень помогает.


Значит ли это, что мир монолитных приложений погряз в проблемах композиции?

Не то, чтобы буквально погряз… но да, архитектурный паттерн big ball of mud определённо появился в монолитах, и, насколько мне известно, в микросервисах пока не встречался.


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

Лично я прямо сейчас пишу что-то вроде монолита/встроенных микросервисов, и у меня в одном приложении несколько изолированных логинов и database в MySQL. Тем не менее, где либо кроме своих собственных проектов я этого подхода не видел, ни в компаниях, ни в опенсорс-проектах. Так что хотелось бы узнать про Ваш опыт подробнее — где (кроме собственных проектов) Вы такое видели, и как часто это встречается.


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

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


Обладают ли этими жесткими средствами C++/C/C#/JAVA?

Я не эксперт в этих языках, так что с гарантией не скажу. Насколько мне известно — скорее нет.


Можете пояснить ваш use-case использования internal packages в контексте замены SOA на монолит?

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


Есть какие-то исследования, что все ПО написанное до второго бума SOA «не работает»?
Chromium/V8 тоже не работают?

Не надо передёргивать, я лично таких утверждений не делал. Если хотите услышать утверждения, с которым можно всласть поспорить — пожалуйста:


  • до определённой границы сложности монолит писать проще микросервисов, после этой границы проще писать микросервисы
  • микросервисы добавляют заметное количество дополнительной сложности из-за сетевого взаимодействия, но эта сложность константная во всех проектах и не увеличивается с развитием проекта, в отличие от ситуации с большинством монолитов, что и является причиной предыдущего утверждения
  • большинству проектов не нужны возможности писать микросервисы на разных ЯП и запускать их на разных серверах, нужна возможность быстро развивать большой проект, в идеале — монолит… кандидатом для решения этой задачи являются "встроенные микросервисы", которые позволяют получить идентичный микросервисам уровень изоляции между командами разработчиков и кодом внутри монолита, избегая дополнительной сложности из-за сетевого взаимодействия… но чтобы это работало крайне желательно иметь поддержку на уровне инструментов — хоть языка, хоть линтера, хоть pre-commit хуков, но чтобы работало автоматически и мешало из кода одного модуля лазить в данные или код другого модуля мимо его публичного API

По моим наблюдениям, микросервисы требуют заметно большей квалификации от архитектора и меньшей квалификации от разработчиков, чем небольшие монолиты… но большие качественные монолиты, которые не тонут в растущей сложности и не превращаются в большой комок грязи, требуют такой же (а может и большей) квалификации архитектора как микросервисы плюс намного большей квалификации разработчиков, чем микросервисы. Иными словами, без хорошего архитектора к микросервисам подступаться бессмысленно. А с хорошим архитектором и средней командой разработчиков можно осилить микросервисы, но не большой и сложный монолит. Ещё иными словами: нормально написать большой монолит тупо дороже, чем то же самое на микросервисах. Идея со встроенными микросервисами в том, что они должны оказаться заметно дешевле обычных микросервисов, ценой потери незначительных для большинства проектов возможностей (разные ЯП в одном проекте, запуск разных частей проекта на разных серверах).

Не то, чтобы буквально погряз… но да, архитектурный паттерн big ball of mud определённо появился в монолитах, и, насколько мне известно, в микросервисах пока не встречался.

В микросервисах он мигрировал на следующий уровень абстракции :-) и может быть найден в скриптах деплоя и конфигах.

Есть такое. Но там small ball of mud :) — сколько там тех конфигов и скриптов, по сравнению с общим объёмом проекта.

UFO just landed and posted this here
Производительность складывается из latency + throughput.

Какой-бы не был супер-монолит, с передачей всего по ссылкам и прочими ультра-оптимизациями, настанет момент, когда одного сервера перестанет хватать. Но всегда есть функциональность, которую нельзя просто накопипастить по серверам без взаимодействия между ними (например, регистрация аккаунта с проверкой уже занятых логинов), или ведение сессий пользователя. Появится необходимость специализации серверов, и это ведёт к сервисам.
Ну, по сути, более-менее серьёзное веб-приложение состоит по умолчанию в наше время из двух-трёх сервисов типа веб-сервер, апп-сервер (может быть реализован как модуль веб-сервера), сервер СУБД. Причём первые два обычно легко горизонтально масштабируются.
UFO just landed and posted this here
что там может быть такого в обычных веб-приложениях, что нагружало бы целые кластера серверов. Не сталкивался с таким.

А что для вас обычное веб-приложение? Если мы захотим вполне банальный агрегатор отелей или агрегатор авиаперелетов и будем максимально хранить кэши в памяти у нас быстро память закончится, ибо комбинаторный взрыв. Просто вы вероятно не работали в компаниях с достаточно большой аудиторией.
UFO just landed and posted this here
неужели там такие гигантские объёмы

Ну давайте посчитаем, гостиниц в Москве скажем 1 тыс. (учитываем всякие частные гостиницы/аппартменты/хостелы и т.д.), вариантов размещения (общий номер, одноместный с общей ванной, одноместный с собственной ванной, двухместных, люкс и т.д.) до 20 штук (20 тыс. записей), нам нужно хранить цены с завтраком и без (40 тыс.), с возможностью отмены и без (80 тыс), на каждый день в году (30 млн), так же возможны скидки отеля при бронировании нескольких дней подряд (все возможные интервалы в течении года — много, ну скажем еще на 10 умножим (250 млн), на нужны цена так же на разное кол-во взрослых и детей в номере (еще на 10 — 500 млн), цены могут приходить как от самого отеля, так и от систем бронирования (вроде букинга) — скажем еще с 10 партнеров (5 млд. записей), каждая запись кроме цены/валюты еще (как минимум) должна хранить id на сайт партнера и время до которого она валидна. На самом деле, скорее всего параметров будет больше.

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

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

Для авиакомпаний можете сами посчитать, например сколько параметров может быть только для полета из одной Москвы.
В случае Таненбаума и Линуса речь шла совсем не о том же самом что в статье. Точнее эти два спора нельзя сравнивать в принципе из за совершенно разных задач и рисков проектов. То есть риски Ядра операционной системы не всегда и не везде можно сравнивать с рисками веб проекта. Иначе говоря всему свое место.
Спор Линуса и Таненбаума, о том, что Линус говорил, что «сложность» никуда не девается.
Просто «сложность» ядра, переносится в «сложность» взаимодействия между микроядрами.
И он прав.
В микросервисной архитектуре сложность «монолита» переносится на «сложность» взаимодействия между микросервисами.
Всё зависит от того как спроектировано это самое взаимодействие. Я-бы сказал это самая критическая точка в системе.
[Зануда mode]
Тащем-та Танненбаум с Торнвальдсом спорили по поводу микроядро vs монолит относительно разработки операционных систем. Понятия микросервисы, в те достопамятные времена, просто не существовало. Ну а сейчас такой спор просто бессмысленен, с появлением микроядер следующих поколений(L3, L4, QNX Neutrino etc).
[/Зануда mode]
Хм, у нас в команде есть похожая проблема, но там причина не в микросервисах
А в том, что очень многие программистыкодеры не понимают микросервисы, не представляют, что значит распределённая система. У каждого второго что-то «подключиться к localhost:8080» захардкожено. Потом он запускает нативно приложение — всё работает, а в контейнере не работает, и он емучается с этим несколько часов.
Микросервисы применимы не везде. Есть вещи, которые не зависят от уровня программистов. Наибольшим препятствием для использования микросервисов являются CAP-теорема и распределенные транзакции. Поэтому микросервисы применимы лишь там, где эти проблемы допускаются бизнес-логикой.
Или там, где за каждый тип транзакций можно назначить отвественным один микросервис, чтобы избежать распределенности транзакций на уровне сервисов.
Это пока не появится бизнес-требование выполнять транзакции нескольких видов в одном сценарии и обязательно согласованно.
Тут от микросервисов уже мало что зависит по опыту. Если надо добавить запись в базу и файл на диск атомарно, то особой разницы нет в монолите это делать или в микросервисах. Точек отказа больше, но обработка отказов простой не будет и в монолите.
Спасибо что подделились, но, по факту, у вас не было микросервисной архитектуры. Вы ее неправильно поняли. Делить нужно было по ролям, сделать возможность масштабирования конретной роли и вся ваша боль ушла бы. Вы же раскидали по разным воркерам код и назвали это микросервисной архитектурой. От неправильного ее формирования вы и получили проблемы и с саппортом и вот эти
Дополнительная проблема заключается в том, что каждый сервис испытывает разную нагрузку. Некоторые сервисы обрабатывали несколько событий в день, а другие – тысячи в секунду.
Обожаю эти комментарии в стиле «вы просто не умеете их готовить» :) Раз говорим про роли, то давайте критерий (желательно универсальный), по которому эти роли выделять.
Желательно универсальный, сферический, серебряный…
Был монолит, худо бедно справлялся с нагрузкой, потом клиентов стало больше, офисы стали расти как грибы и отдельные операции вешали систему. Хотели переписать, но не срослось. В итоге поделили на микросервисы на основе docker. Нагрузку размазали по серверам, стало гораздо лучше.
По началу на каждый чих создавался микросервис, печать документов — сервис, внешние заявки — сервис, плохие клиенты — сервис. Сейчас их большое количество, не 140 конечно, но около 25, хорошо они хоть друг от друга не так часто зависят.
В общем если сервисов около 20, то админить и тестировать их не сложно, развернуть весь стек приложений тоже, масштабировать и тп., но вот если их больше — то начинается боль, особенно если новый человек хочет разобраться в этом.
— А какой уж там сервис отвечает за X функционал? и какой — за Y?
— Я не мог пол дня настроить окружение, блин они еще друг от друга зависят.

В общем мой совет — не перебарщивать с количеством и делить сервисы на роли.
А как же инфраструктура как программа?! :-)
Т.е. настройка и конфигурация должны настраиваться не в ручную, а быть в виде программного кода. С обязательным покрытием тестами.
Со всеми плюшками DI, тогда зависимости для развертывания dev-окружения можно mock-ировать

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

Как будто в микросервисах говнокода не бывает...


В монолите у меня есть хоткей чтобы увидеть кто еще этот код использует.

Бывает. Но сам по себе микросервис настолько маленький, что переписать его легче. При этом можно быть точно уверенным, что все остальное не упадет. ;-)
А вот это не факт. Может, все остальное и не упадет — но работать в случае ошибки точно не будет, ведь микросервис был для чего-то нужен!

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

А вот хорошая архитектура и монолит, это из области фантастики. :-)
Прекрасная история про то, как чуваки вместо пачки одинаковых библиотек сделали пачку одинаковых микросервисов, закономерно на этом огребли, и говорят про то, как хорошо быть в монолите…
Очередная команда, решившая заюзать архитектуру потому, что она модная, и разочаровавшаяся в ней потому, что не подошла под задачу/кривые руки/не осилили? Ох уж эта хайп-полезнь (

Не надо пожалуйста переводить термины, не сразу понял, что
Полезная нагрузка = payload
Конечная точка = endpoint

Наоборот, везде, где есть адекватный аналог в русском языке, надо использовать русский, а английский оставлять только для плохо переводимых терминов… Вымораживают тексты с постоянными «эндпоинтами» и «пейлоадами»…
Всё от хайпа, согласен. Решили хайпануть и напоролись. В программировании, увы, это сплошь и рядом. У нас сервис пока монилит, до тысячи задач одновременно, насколько я видел. Пока справляется как есть. Изоляция — отдельная проблема, но решаемая. Подумываю насчет разделения на несколько (не микро) сервисов. Инструмент — Delphi, проект порядка миллиона строк почти в одно лицо. Веб (без апача и аналогов), некоторые специфические протоколы, терабайты прокачиваемой через сервис информации.
Вообще странно что ТС напоролись на проблему изменений.
Такое ощущение, что DI к в команде не практикуется. :-)
Заголовок спойлера
Рассказ неосиляторов :-)


Микросервисы требуют большей дисциплины, большей команды и большей компетенции. Если чего-то из этого не хватает, то будут проблемы.
То есть у них по сути были некие модули, выполнявшие одинаковые операции: получить данные от клиента, замапить на новую структуру и отправить другому API. Они каждую отдельно реализовывали, огребая на тестировании всего этого? Вместо того, чтобы создать что-то типа базового сервиса с зависимостями от маппера и способом общения с получателем, развернув в итоге просто несколько копий этого базового сервиса: Мапперы и пересыльщиков можно тестировать отдельно, а DI-контейнеры решат проблему компоновки сервиса для каждого из направлений. Что не так в этой логике?
Думаю, дело в том, что любой серьёзный, динамически развивающийся проект постоянно меняется и может меняться очень сильно. В начале делается прототип для одной концепции, потом концепция меняется, прототип дорабатывается с минимальными затратами по времени, потом ещё, ещё и ещё. В какой-то момент получается «некоторое дерьмо», которое сложно поддерживать и развивать, напрашивается рефакторинг. Это нормальный процесс жизни проекта без чёткого понимания конечной цели.

А самая большая проблема в таких проектах, по моему опыту — это донести до менеджера, что не бывает гибкой разработки проектов (agile) без рефаторинга. Иногда надо остановиться и существенно перетряхнуть внутренности проекта, что может занимать немало времени.
Согласен.
Микросеврисная архитектура и предполагает, что проще переписать, чем изменять. :-)
Я не любитель микросервисной архитектуры, так что люто плюсую автора.

Был опыт развития проекта, построенного на микросервисной архитектуре. Окончательно меня бомбануло, когда разработчик несколько дней дорабатывал API сервиса для того, чтобы сделать подобие LEFT JOIN между двумя сервисами. И работало это на 2-3 порядка медленнее, чем запрос в базу. После было принято решение объдинить сервисы в монолит. Жалею только о том, что не сделал это сразу, как пришёл в проект, много времени было потеряно зря. Но, безусловно, это не проблема концепции, это проблема архитектуры проекта и изменчивость требований заказчика.

Сама по себе микросервисная архитектура не плохая и не хорошая, просто на хайпе её суют куда попало, в каждый «утюг», это же, блин, модно… А каждый инструмент надо использовать с умом, там где он реально помогает решать задачу.

Спасибо автору за то, что поделился опытом. Здорово, что не упёрлись рогом в концепцию, а стали искать другие решения. Может они ещё и вернутся к микросервисной архитектуре, но в другом виде…
UFO just landed and posted this here
Во-первых, просто не любить микросервисы или любить микросервисы, это непрофессионально.

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

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

UFO just landed and posted this here
Потому-что это инструментарий. Это не вопрос личных симпатий и антипатий.
Но почему у меня не может быть любимой лопаты? ))))
Беда в том, что автор оригинальной статьи не только не понимает когда использовать технологию, но и не понимает ее вообще.
Мне кажется, это проблема людей вообще, мы не можем всё знать и совершаем ошибки… Автор поделился своим опытом, сообщество обсудило, а автор, думаю, вынесет из обсуждения что-то полезное для себя.

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

Думается, что чаще бывает наоборот — как раз таки из-за хайпа чаще тянут микросервисы туда, куда не нужно.

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

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


Это случайно не вы Джессике Фразелли недавно объясняли как контейнеры работают?
Э-э-э???
Если нужен был LEFT JOIN и была база, то почему бы не написать микросервис, который это делает?!
Т.е. если можно (и это проще) использовать готовые микросервисы, то надо ими пользоваться.
Если проще написать «с нуля», то почему бы и не написать «с нуля»?!
Тут вопрос к «архитектору», ну или кто отвечал за общую структуру проекта.
Почему он решил, что надо делать так?!
Я так и написал, что это проблема архитектуры:
Но, безусловно, это не проблема концепции, это проблема архитектуры проекта и изменчивость требований заказчика.
При чем тут архитектура.
Как реализовать микросервис, это задача программиста.
Если проще/удобнее напрямую обратиться к БД, то почему бы и да.
Просто «архитектор» мог поставит жесткие условия, что сделать надо так и никак иначе.
ИМХО, «архитектор» должен говорить, «что» будет делать микросервис, а «как» пусть программист сам решит.
Благо микросервис должен быть таким, чтобы он был понятен любому программисту.
Соответственно переписать его может любой программист.

Если, например, вы считаете, что проще/быстрее/удобнее использовать 1 один запрос к БД. То микросервисная архитектура никоим образом это не запрещает :-)
Вполне себе запрещает. Есть микросервис, у него есть своя приватная БД. По идее никто кроме него в эту БД лазать не должен, а все остальные должны обращаться к его публичному апи. Потому что если это разрешить — начнется совсем адский ад, который обычно ставят как пример недостатков монолитов — нарушается изоляция и модульность, и все летит кувырком как только кто-то начинает менять структуру этой бд. Причем если в случае монолита можно хотя бы более-менее просто обнаружить, что в бд лазает какой-то другой участок кода, и поправить его, то в случае микросервисов этот участок кода может лежать в другом репозитории и даже на другом языке программирования.
Честно говоря, еще на заре хайпа по микросервисам, я еще слышал максиму, что каждый микросервис должен работать только с приватной БД.
Но потом, практика показала, что это не максима и вообще хранилище данных это отдельная тема.
Т.к. желательно чтобы микросеврисы были без состояние, то это стоточение где-то должно быть.
Одно из хранилищ состояния может быть, например, БД.
Куда микросервисы обращаются за данными.
Поэтому в публичном АПИ, просто создается микросервис с JOIN-ом, к которому обращаются при надобности :-)
Суть микросервисов именно в «как», в максимальной изоляции их друг от друга, в единственной задаче которые они решают.

Если нужно делать «джойн» данных нескольких микросервисов на уровне запросов к БД, то это, в общем случае, нарушение принципов MSA.
Суть микросервисов в максимальной изоляции бизнес-логики, но не данных!
Т.е. разделения то что не должно меняться (данные) и то что может быть изменено со временем (логика работы с данными).
Почему данные не должны меняться — потому что история. Мы должны знать что у нас было в определенный момент времени.
Поэтому данные не меняются, а добавляются.
Так что общее хранилище данных для микросервисов — это норма.
При чем это может быть не только БД, но например kafka :-)

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


Эта проблема не распространяется на кафку и аналогичные сервисы очередей потому, что данные в них изначально описываются как API — какие у нас топики, какие бывают типы событий, какие по ним даются гарантии, etc. ну и плюс там всё-таки нет свободного RW доступа к этим данным как в обычной БД.


Таким образом, максима про приватную БД у микросервисов это не какая-то глупость времён "зари микросервисного хайпа", а суровая реальность. Конечно, если у Вас данные с БД по своей природе иммутабельные (append-only), плюс формат схемы БД описан и не меняется, то по факту такая БД ничем не отличается от любого другого API — микросервисам доступны только операции которые не могут "сломать" данные в БД (кстати, чтобы это гарантировать, крайне желательно ограничить разрешённые операции для тех аккаунтов, под которыми микросервисы подключаются к БД, исключительно SELECT и INSERT, если речь об SQL) и сохраняется обратная совместимость. В этом случае никакой проблемы чтобы с этой БД работало несколько микросервисов нет. Но, мягко говоря, это не самый типичный сценарий использования БД, поэтому ту "максиму" он никак не отменяет.

Вы приводите в пример, только БД.
При изменении схемы данных, все равно будут меняться микросервисы, которые обращаются к микросервису работающему с БД.
Т.к. контракт скорее всего поменяется.

Если он не поменялся… То зачем изменение :-)

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

P.S. Собственно это одно из свойств сервисов, ради которых их и внедряют.
Норма для микросервисов не общее хранилище операционных данных, а отдельный сервис или сервисы агрегации операционных данных основных сервисов по типу DWH. А история не является обязательным требованием и в целом к SOA или MSA ортогональна.
mad_nazgul, Вы, к сожалению, ошибаетесь. Если интересны подробности, то Sam Newman (коллега Мартина Фаулера по ThoughtWorks) отвечает на этот вопрос в своей книге “Building Microservices. Designing Fine-Grained Systems”.
сделать подобие LEFT JOIN между двумя сервисами
Пусть он почитает книгу «NoSQL Distilled. A Brief Guide to the Emerging World of Polyglot Persistence.» by Pramod J. Sadalage, Martin Fowler. Там описано как решаются такие проблемы в распределенных системах, хотя книга и не про микросервисы.
UFO just landed and posted this here

Это же перевод, так что всё Ваше беспокойство/негодование не к тем обращено

От статьи создается впечатление, что проблема там была не с микросервисами, а как поддерживать 100500 версий сервиса. Для поддержки — это, конечно, ад. Решение было в области отказа от старых версий.

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

ИМХО, в случае, такого большого кол-ва микросервисов лучше не иметь общих библиотек вообще (малейшая ошибка в общей библиотеке и лягут все микросервисы сразу).

Или они должны быть сильно ограничены и сигментированы (грубо говоря, на rest клиента одна библиотека, на работу с xml другая, не обязательно в разных репозиториях, но в разных jar'никах или в чем они делают выдачу).

К своим общим библиотекам лучше относится как к любой другой opensource библиотеки — если она не нужна, не включать, если есть opensource библиотека более подходящая — брать ее, а не свою общую библиотеку.

Эта обшая библиотека по сути делала все микросервисы Монолитом, который слегка кастомизируется и копируется в 120 инстансов. В результате логично, что оказалось, что Монолит для реализации Монолита лучше.
UFO just landed and posted this here
Возможно сидят и тихо работают, не пишут подобные статьи :-)
По моим наблюдениям мотивация рассказать о неудачном опыте сильно превышает таковую в случае успеха.
Профнепригодные архитекторы, профнепригодные менеджеры, профнепригодные девелоперы. Куда деваются люди, со знанием — загадка.


Про менежеров у меня есть теория: хорошо менеджерить IT-проект может только хороший программист, который потрогал всю подноготную ремесла. Который понял, зачем тесты, понял, что такое рефакторинг, деплой, архитектура. В общем, понял это непростое ремесло. После этого, человек имеет возможность менеджерить такие проекты хорошо. Но если он хороший программист — зачем ему менеджерить? Я думаю, что большинство хороших прогеров не хотят перерастать в менеджеров.

Если же менеджер не был программистом, то программистам придется выпрашивать (в буквальном смысле) время на рефакторинг/тесты/песочницы.
А я не соглашусь с вами, менеджер и программист- очень разные активности. Безусловно, менеджер должен понимать тех детали, но это верно для любых профессий.
Судя по зарубежной литературе, хороший менеджер — это мировая проблема :) Вон, старик Брукс аж бестселлер написал про пули и… про то, как он развалил проект ОС/360 :)
А вообще я думаю, что хорошего менеджера практически не должно быть заметно. И это его основная цель: команда должна работать эффективно что с ним, что без. Но чтобы этого достич, нужен хороший менеджер, такая фигня :)
Я немного криво выразился, с моей точки зрения — хороший менеджер получается из хорошего программиста. Но это не означает, что хороший программист станет хорошим менеджером. Быть хорошим прогером — это необходимое условие, но точно, абсолютно точно — не достаточное.
Уверен, что есть исключения, представляющие из собой просто отличных менеджеров, которые ничерта не смыслят в низкоуровневых слоях.

Я думаю, что большинство хороших прогеров не хотят перерастать в менеджеров.

Если же менеджер не был программистом, то программистам придется выпрашивать (в буквальном смысле) время на рефакторинг/тесты/песочницы.

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

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

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

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

Попробуйте обосновать рефакторинг. Например, отделение в бэкенде слоя доступа к данным (DAL)

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


Юнит-тесты должны писаться одновременно с кодом, просто чтобы разработчик был уверен в том, что написанный им код запускается, и делает ровно то, что ожидает разработчик. Это не требует много времени на начальное написание (максимум — удваивает время начальной реализации фичи, т.е. лишний день на среднюю таску), причём это время с лихвой компенсируется экономией времени на последующих багфиксах и доработках ещё до релиза. Единственное исключение, когда затраты на юнит-тесты могут не успеть компенсироваться к моменту релиза — при разработке прототипа. (Ещё одно исключение — когда релиз должен быть к жёсткому дедлайну, напр. для выставки или инвесторов, и его качество не имеет большого значения если проект в принципе запускается и выглядит рабочим в срок, а срыв сроков категорически недопустим и может привести к потере проекта.) В остальных случаях, если написанный код реально должен работать, а не быть рассадником бесконечных багов, "излишние" затраты времени на юнит-тесты реально сводятся к нулю примерно к первому релизу, а дальше просто экономят кучу времени.


Что касается рефакторинга, то в большинстве случаев он должен выполняться в фоне: если трогаешь какой-то кусок кода в рамках текущей задачи — оставь его после себя в чуть лучшем состоянии, чем он был. Тогда время на рефакторинг размазывается незаметным тонким слоем по всем задачам, становится невидимым для менеджера (не в смысле сокрытия от него этих затрат, а в смысле что затраты настолько незначительные, что перестают иметь значение для планирования и управления), и отпадает необходимость выпрашивать под рефакторинг отдельное время. А когда дело доходит до необходимости серьёзных переделок кода, которые требуют заметного времени, то тут речь не столько о рефакторинге, сколько об изменении архитектуры, и обычно эту необходимость можно обосновать в терминах, понятных менеджменту (а если нельзя, то, скорее всего, реальной необходимости в этих изменениях нет).


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

Изменения архитектуры без изменения наблюдаемых эффектов — тот же рефакторинг и/или оптимизация. Сложные изменения, когда часть кода (и тестов) приходится выкидывать — это, прежде всего, изменение функциональных требований.

Изменения архитектуры обычно делаются как раз ради изменения наблюдаемых эффектов, причём наблюдаемых как раз менеджером: появления возможности реализации (нормальной, без диких хаков и тех.долга) каких-то новых фич, увеличения объёма данных поддерживаемого проектом и/или скорости его работы, добавления возможности горизонтального масштабирования, вынесения/изоляции отдельных компонентов чтобы можно было их разработку поручить отдельным командам разработчиков, etc. Все эти причины для серьёзной переработки архитектуры видны и понятны менеджменту, в отличие от рефакторинга, который значительно более низкоуровневый (крайне сложно объяснить среднему менеджеру почему важно тратить время на выбор более подходящего имени для переменной и её переименования).

Я о поведении программной системы для пользователя, а не о поведении системы разработки программной системы для участников разработки. Изменение архитектуры без изменения поведения для пользователя — один из видов рефакторинга и(или) оптимизации, просто рефакторинг архитектуры на самом высоком уровне проводится, в отличии от переименования локальной переменной.
Никакого развивающегося проекта не бывает без рефакторинга. И, кстати, 90% встретившихся мне менеджеров не понимают, что такое Agile/SCRUM и как он работает.
Причины рефакторинга бывают разными. Бывают причины потому что в мастер-бранч мержится некачественный код. Т.е. в команде отсутствует Collaborative Development. В таком случае Agile никак не оправдывает низкое качество кода, и даже, более того, предписывает его не трогать, пока он не мешает и не влияет на экономику разработки. А бывает рефакторинг как элемент реализации Evolutionary Design и YAGNI. Тогда — да, это Agile. Но в таком случае, это не рефакторинг, а Evolutionary Design.
А почему типовое решение для очереди не взяли, какую-нибудь Кафку, например?
Кафка это не совсем очередь, даже совсем не очередь.
Но продукт почти гениальный в простоте своей концепции :-)
Пример для перевода не удачный. Люди не умеют готовить микросервисы

Не понятны два момента:


  1. Чего плохого в общих библиотеках? Они остаются общими, пока могут такими быть. Требования меняются — меняется и код. И не надо пытаться сохранить его, закостылив в "общей" библиотеке ради общности
  2. Вы сначала перешли на отдельные очереди с раздельным управлением, испытывая проблемы с отказоустойчивостью, а потом психанули и перешли на centrifuge, опять создав проблемы с отказоустойчовостью?
    Если я правильно понял, то проблемы у вас были две — микросервисы ради микросервисов и общие библиотеки ради общих библиотек
Это перевод, автор вам не ответит
Общие библиотеки, насколько я понимаю, ради DRY и единообразности, уменьшения порога входа в микросервис.
Они бы создали проблемы с отказоустойчивостью если бы перешли на общую очередь. Но centrifuge, судя по описанию, такой общей очередью не является.

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


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


Проблема с общими библиотеками возникает из-за того, что, поскольку это "библиотеки проекта", они не пишутся настолько аккуратно, как опенсорсные, в них нет такого тщательного соблюдения semver и обратной совместимости API, они не являются настолько библиотеками общего назначения и в них проникает бизнес-логика этого проекта, и в следствие всего этого к ним предъявляется нетипичное требование "все микросервисы обязаны использовать последнюю версию этих библиотек". В результате общие библиотеки создают жёсткую связь между всеми микросервисами и появляется точка, в которой можно все микросервисы поломать, точка, обновление которой вызывает лавину перевыкатов всех микросервисов, точка, которая создаёт больше проблем, чем решает. Вот поэтому общих библиотек проекта у микросервисов быть не должно. А общие опенсорсные — без проблем, причём одна и та же библиотека может в разных микросервисах использоваться разной версии.

Вот поэтому общих библиотек проекта у микросервисов быть не должно.

Как минимум, имеют право на жизнь, библиотеки проекта/продукта, инкапсулирующие конкретный способ использование других микросервисов, например икапсулирующие HTTP-проткол.

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


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

Я кажется понимаю о чём вы, но я о другом. Я о библиотеках, реализующих клиентов к микросервису. Есть, например, сервис работы с пользователями, предоставляющий REST API, которым пользуются все остальные сервисы. Вместе с сервисом разрабытываем библиотеку, которая будет экспортировать классы User и UserRepository и прятать от остальных сервисов факт наличия REST API и вообще факт межпроцессного взаимодействия. Клиентский сервис работает с сервисом пользователей через эту библиотеку даже не подозревая о том, что там идут вызовы по сети. Цикл мажорных и минорных релизов библиотеки совпадает с циклом релизов сервиса пользователей.

Пока использование всеми сервисами этой библиотеки вообще, и только её последней версии в частности, не является обязательным — никаких проблем. Если сервисы предоставляют стабильное и документированное API, а библиотека существует исключительно для удобства разработчиков тех сервисов, которым лень напрямую вызывать REST API — всё в порядке. Как только библиотека начинает использоваться для сокрытия факта нестабильности и/или недокументированности API, что и приводит к требованию её обязательного использования причём только последней версии, вот тогда возникает та проблема, из-за которой "общих библиотек проекта быть не должно".

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

Цель подобных библиотек как раз уменьшения количества зависимого от API сервиса кода, чтобы при изменении API сервиса достаточно было обновить только библиотеку на клиентах (естественно, если API библиотеки не изменился), а не переписывать код самих клиентов. Ну и в целом она сама может являться документацией к сервису.
Вот именно от обратно несовместимого API сервисов и проблемы. Иногда это нужно, но если такое постоянно — значит, микросервисы используются как-то неправильно.
Общие библиотеки у проекта могут быть, но это не должно быть догмой.
Т.е. их использование не должно быть обязательным.
В противном случае — да получаем монолит в библиотеках.
Дублирование кода сильно хуже, чем монолит в библиотеках.
Нету дублирования кода.
Т.к. он разнесен по разным приложениям :-)
И в каждом конкретном приложении он уникален.
А то что есть похожие куски, которые делают почти одно и то же…
То да их можно вынести в библиотеки.
Но проектирование АПИ, не совсем тривиальная вещь. :-)
Как будто от того что это разные приложения стало проще править одну и ту же скопированную ошибку…
Одинаковый код выносится в библиотеки. Если не выносить — то будет дублирование, других вариантов нет.
Например: имеем кастомный парсер текста, использующийся в 10 ти сервисах. И что — делать 10 копий?
Ок. Математический модуль тоже отдельным сервисом делать? :) Я представляю уже производительность.
Микросервисы не про производительность, а про декомпозицию и управление сложностью (и с математической стороны, и со стороны практической поддержки проекта). Если у вас получается критической скорость каналов связи для загрузки математического микросервиса, то что-то у вас спроектировано неоче (возможно, ваш код ещё прост не дорос до того, чтобы ему уже смогло помочь разделение на микросервисы, либо делите на сервисы вашу логику вы не очень корректно).
Если у вас получается критической скорость каналов связи для загрузки математического микросервиса, то что-то у вас спроектировано неоче
Не загрузки, а работы. Если выносить 2+2 в микросервис, то, боюсь, закончится как в статье.
Как по мне, то микросервисы прежде всего про масштабирование. Декомпозицию никто не мешает делать в пределах монолита.
В рамках конкретного сервиса дублирования может и не будет, но в рамках системы — будет. Причём гораздо хуже обнаруживаемое и поддерживаемое чем в случае монолита в общем случае.
Почему хуже?
Тесты же должны быть :-)
Если микросервис работает правильно, то зачем его менять?
Если нашли «ошибку» в одном микросервисе, то не факт, что это же ошибка есть в другом. Не смотря на то что код может быть один и тот же.
Дублированный код сложнее будет обнаружить и поддерживать. А тесты не гарантируют правильность работы.
Зачем его (дублированный код) обнаруживать, если микросервис работает (тесты)?! :-)
Так если дублированный код работает заведомо неверно? Тесты редко покрывают 100% функциональности.
Если дублированный код работает заведомо неверно в нескольких микросервисах… То дублирование кода не самая главная проблема. ;-)
Понятно же, что некоторые ветки кода могут не вызываться достаточно долго в одном микро-сервисе. А в другом они вызываются постоянно, и там уже давно вылизаны до блеска. В нём баг уже поправлен. А в первом — нет, но он там есть. Да, в идеальной системе это всё решается тестами. Но как в реальных — мы все хорошо знаем. Тесты, к слову, тоже придётся дублировать.
В реальном мире тесты используются вместо доказательства правильности алгоритма :-)
1) Повторюсь: Тесты не гарантируют правильность работы. Ошибки могут быть, просто мы о них не знаем до поры до времени.
2) Могут меняться требования
1) Микросервисы как раз для того и нужны, чтобы покрыть полностью код unit-тестами. В силу их «обозримости». Плюс интеграционные и прочие тесты тоже писать чуть легче.
2) Да могут, но не всегда для всех и сразу микросеврисов.
Т.е. «общий» код который уже не правильно работает в одном микросервисе, в другом микросеврисе наоборот работает правильно.
1) Микросервисы нужны не для этого. И в целом не имеет значения покрывать юнит-тестами модуль в составе монолита или микросервиса. И в любом случае для более-менее сложной логики юнит-тестами верифицировать правильность работы модуля невозможно. Даже метод с одним целочисленным параметром потребует написать 2^64 тестов, что практически невозможно.

2) в случае библиотечного кода решается версионированием по-горячему. Там, где ошибка проявилась, ставится новая версия библиотеки с хотфиксом, а остальные её пользователи усилиенно исследуют проявляется ли ошибка со старой и новой версией. В зависимости от результатов или обновляются, или библиотека правится так, чтобы все сервисы работали корректно с последней версией. Если же у нас копипаст, то даже не узнаем, что где-то могут быть ошибки аналогичные тем, которые исправлены в одном сервисе.
UFO just landed and posted this here
Они не гарантируют правильности. например, может быть ошибка в компиляторе/интерпретаторе/процессоре, которая проявляется только на конкретном значении ничем ни примечательным с точки зрения спецификации языка.
Даже метод с одним целочисленным параметром потребует написать 2^64 тестов, что практически невозможно.
Этого недостаточно, т.к. у модуля/класса может быть внутреннее состояние. Чтобы протестировать хорошо, нужно подавать на вход все возможные последовательности int-ов длины, соответствующей ожидаемому времени работы программы, в которую будет включен тестируемый код ))
Полное покрытие юнит тестами в более-менее сложной системе это, увы, фантастика. Кроме того — существуют внешние проблемы, как то: обрывы связи, проблемы с железом, проблемы с операционками, проблемы со сторонним железом, проблемы соответствию стандартам сторонних файлов/протоколов и т.п. И их, увы, никак не потестируешь. А крови они пьют — мама не горюй.
Вышел новый: браузер/операционка/бибиотека/формат/субформат. И часть тестов можно выбрасывать. И так — постоянно.
По собственному почти ежедневному опыту, так сказать.
UFO just landed and posted this here
Понятно. Но системы же разрабатываются не для работы на сферической вируталке в ваккуме :)
UFO just landed and posted this here
Как я уже говорил — всё покрыть тестами, увы, невозможно. Новые вызовы в более-менее сложной системе появляются часто.
UFO just landed and posted this here
1) По ссылке не 100%
2) Даже 100% покрытие не гарантирует правильности работы во всех случаях. Банальный пример: 100% покрытия перебора по массиву, но теста на пустой массив нет, в рамках системы «код-тесты» это является неопределенным поведением, о котором нельзя сказать правильно оно в коде или нет. И даже в требованиях такая ситуация может быть не отражена — нужно уточнять требования.
UFO just landed and posted this here
Не надо меня агитировать за советскую власть тесты. Но гарантий работоспособности при любых входных данных они не дают. Только гарантируют, что поведение кода для конкретных кейсов не изменилось с последнего успешного прогона. Пример — были тесты на функцию сложени\ дефолтных для платформы int, с граничными случаями, с областями эквивалентности и всё работало на 32-бит платформе. Сменили на 64-бит — тесты так же работают и функция работает корректно, пока не приходят числа, которые больше 32-бит. Тесты проходят, а результат неверный.
Никакие тесты не помешали ввести в спутнике Марса число в дюймовой системе вместо метрической, и несколько миллиардов долларов улетело в космос, в прямом смысле. Увы — но жизнь всегда сложнее любых тестов.
Интеграционными и функциональными тестами — да нельзя.
Юнит-тестами — да можно/нужно
Не зная о деталях реализации мы не можем покрыть граничные случаи и все области эквивалентности в принципе. Зная — можем покрыть в общем случае, наверное, но не имеем никаких гарантий, что при изменении реализации граничные случаи и области эквивалентности останутся теми же.
UFO just landed and posted this here
Рефакторинг или оптимизация ресурсов подразумевает замену реализации без изменения теста. Плюс тест не должен падать, если в новой реализации просто добавилась граничная точка, которая для старой такой не была.
UFO just landed and posted this here
Ну а входе рефакторинга/оптимизации граничные случаи и области эквивалентности могут измениться. Тесты зелёные, но модуль иногда возвращает неверный результат.
UFO just landed and posted this here

Ну, была у нас реализация (int a, int b): int => a +b, а стала (int a, int b): int => (a * 2 + b * 2) / 2 (допустим это быстрее гораздо). Требования не изменились, но вот области эквивалентности с предыдущей реализацией не совпадают.

UFO just landed and posted this here
Обычные для целочисленного сложения ограниченной точности. Но после оптимизации перполнение наступает на вдвое меньших значениях, которые были эквивалентны бОльшим, но в новой реализации уже не эквивалентны.
UFO just landed and posted this here
Были зафиксированы. Постановщикам задач с приемочными тесткейсами в голову не могло прийти, что на сложении целых может быть переполнение раньше чем результат слишком большой получится, что переполнение может быть на внутреннем промежуточном результате, который без анализа конкретной существующей реализации тестами поймаешь разве что случайно.

И это я ещё не вспоминал про создание ветвлений в модуле под конкретный тест-кейз специально чтобы тест прошёл, например, если сроки горят, а CI не пускает.
UFO just landed and posted this here
Проблема в том, что на реальных проектах никто в ТЗ не прописывает граничные условия, вроде «сервис авторизации должен хранить не менее 50000 логинов пользователей». Отсюда следует, что разработчик не напишет тесты на граничные условия.
Хорошо, если граничные условия вообще известны. И отлично, если не меняются. Увы, но чаще всего и одно и второе неверно.
UFO just landed and posted this here
У вас на все целочисленные параметры известны диапазоны допустимых значений, и на комбинации параметров написаны тесты, проверяющие, что если вход около этих границ, но не превышает их, нигде не происходит ошибок?
UFO just landed and posted this here
Смысл их я знаю, и задание всех граничных значений необходимо, чтобы постановка задачи была математический корректной, а программа имела возможность формальной проверки.

Мой вопрос в другом. Вам действительно дают в ТЗ все граничные значения всех параметров?

Например, макс. число символов, которое юзер может вписать в любую строку поиска, минимальное и макс. разрешение экрана у юзера, а то положишь координату окна в int32, а у него монитор 248×250 пикселей :)
UFO just landed and posted this here
Довольно редкий случай. Обычно это бывает в отраслях типа космической, где ошибка фатальна и влить 100500 человекочасов в спеки не считается проблемой.
UFO just landed and posted this here
Но, начиная с некоторой степени детализации, спека становится слишком дорогой и может не окупиться.
Ну вот как вы пишите тесты на фактические граничные значения и области эквивалентности, если вы ничего не знаете про реализацию ещё? Ну вот есть в требованиях про область [INT_MIN; INT_MAX] (что уже маловероятно на практике), как вы проверяете что на любом значении из этого диапазона функция хотя бы не падает? А ведь конкретная реализация может споткнуться на любом.
Но новая реализация почему-то плохо работает на INT_MIN/2 или чётных положительных аргументах. фактические области эквивалентности изменились, то тесты это выявить не могут по своей природе — они не тестируют области, они тестируют конкретные кейсы.
UFO just landed and posted this here
Я спорю с утверждением, что тесты гарантируют правильность работы системы на всём диапазоне входных значений. Не гарантируют. В лучше случае гарантируют для конкретных наборов входных значений. Надеяться можно, что если тест отработал для N, то он отработает и для N-176, что при написании.модификации модуля.тестов применялся здравый смысл и в тестах проверяются все фактические области эквивалентности. Но гарантий этого нет.
UFO just landed and posted this here
Проверить всё, что можно жизни не хватит, причём жизни Вселенной :) В большинстве случаев достаточно проверить основной success flow на одном наборе простых данных и имеющиеся в требованиях особые случаи. Дальше покрывать обычно смысла не имеет до выявления багов. Есть баг — локализуем его тестом, исправляем и ждём следующего. При таком подходе мы можем рассматривать систему как в целом работающую, но вероятно имеющую где-то неопределённое поведение незафиксированное в требованиях. Будут требования — зафиксируем требуемое поведение.
Если коротко, то
«My general rule of thumb: don’t violate DRY within a microservice, but be relaxed about violating DRY across all services. The evils of too much coupling between services are far worse than the problems caused by code duplication. There is one specific use case worth exploring further, though.»
Если интересуют подробности этой проблемы, то их полностью раскрывает Sam Newman в «Building Microservices. Designing Fine-Grained Systems». Микросервисы действительно поощряют дублирование кода, но только в тех случаях где это позволяет снизить сопряжение (coupling).
На моей практике организовать работу нескольких команд, которые в итоге выдают общие библиотеки, с semver, обратной совместимостью и прочим плюшками проще, чем решение инфраструктурных проблем с микросервисами. То есть, я рекомендую с библиотек начинать. Если не получается делать нормальные библиотеки, то о нормальных и стабильных интерфейсах между сервисами можно забыть.

Библиотеки не являются полноценной альтернативой микросервисам — в нормальных библиотеках обычно не должно быть своего логирования, своей БД… но вот встроенные микросервисы, особенно если они stateless, от библиотек отличить действительно довольно сложно.

Библиотеки могут реализовывать основную функциональность, а логирование и т. п. можно в них инжектить или делать врапперы вокруг них.
Библиотеки, имхо, вторым этапом должны быть. Первым — просто разбиение на модули/контексты, с жёстким контролем за соблюдение публичного контракта, например с активным использование DIP из SOLID в мейнстрим ООП языках, чтобы в клиентах модулей использовались только интерфейсы или абстрактные классы для взаимодействия с ними.
Наблюдаем уже как минимум третью попытку продавать серебряную пулю архитектуры систем из микрокомпонентов. Об этом в статье «Блеск и нищета микросервисов»
www.arbinada.com/ru/node/1651
Sign up to leave a comment.

Articles