Разработка ПО и Backend систем.

Позднее Ctrl + ↑

Как я на HL сгонял

карикатурный прогромист

Побывал на HighLoad 2-3 декабря в Москве. Я остался доволен, так что мне есть, что рассказать.

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

Но у ребят все круто продумано:

  • ✔️На бейдже присутствует карта помещений, так что найти сундук с сокровищами шансы есть;
  • ✔️Сами помещения понятно обозначены, но тут скорее особенность здания Школы Управления Сколково. Через часа 3 ты уже спокойно ориентируешься, хотя порой и тупишь;
  • ✔️Между докладами перекусы и кофе в шаговой доступности;
  • ✔️Обед по талонам(во время выдачи на стойке регистрации я невольно засмеялся :), без них был бы Ад и Израиль;
    3 декабря в здании было около 2 тысяч человек(или даже больше, @r0oxy, поправь меня), но места хватало всем.

Доклады и спикеры

  • ✔️Доклады были подготовлены сильно лучше, чем на других конференциях, которые я посещал;
  • ✔️Были спикеры, которые терялись от вопросов после доклада, но сами доклады ребята рассказывали хорошо. Слушать было приятно, запинок было мало.

Сами темы докладов сильно сложнее и комплекснее в среднем(привет докладу с темой «Почему Go лучше Python» с содержанием, пересказывающим статью на Хабре 5 летней давности, которое можно сократить до слова «Потому.») Аномалии Postgres под нагрузкой, устройство файлового хранилища ВК, их же система распознавания отрезков видео, замеры работы разных ДБ на больших RPS — лишь небольшая часть из тем, что были рассказаны.

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

CQRS (3)

Когда полезно использовать

— В сложных доменных областях. Использование подхода позволит снизить сложность каждого конкретного контекста программы.
— Если в системе сильно различается частота запросов на чтение и запросов на БЛ составляющие системы. Например можно разместить Command & Queries в разных процессах, что позволит горизонтально масштабировать Queries часть системы, так как обычно запросов на чтение сильно больше запросов на запись.
— Если ваша система спроектирована по принципу EDA(Событийно-ориентированная архитектура). Событию системы соотносится конкретная модель данных. Позволяет БЛ не вытекать за пределы обработчика события.
— Когда вы собираетесь разделить базу данных на БД для записи и БД для чтения. Однако следует учитывать, что при разделение БД так же встает вопрос о конечной согласованности данных в базах.

Когда использование не даст преимуществ

— Модели Command & Queries совпадают. В таких случаях лучше использовать совместную(единую) модель.
— В случае, если доменная область неверно выделена в системе, то использование CQRS только запутает и без того слабо структурированный код.

CQRS (2)

Вернемся к основной теме

В проектах часто вижу, что ORM модель сущности используется во всех контекстах(доменных областях, которые по сути есть в программе, но по факту никто их явно не выделял) системы:
— как выходное отображение для ответа API;
— как DTO между разными модулями;
— для модуля работы с БД(orm модель по логике должно существовать только в рамках этого модуля);
— как модель для формирования тела сообщения в очередь сообщений.

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

CQRS предоставляет разделение жирной модели на Command model(модель для работы БЛ: создание, обновление, связывания с моделями других сущностей, и т. д.) & Query Model(отображаемая пользователю в запрашиваемом домене).

Названия украдены из концепции Command Query Separation (https://martinfowler.com/bliki/CommandQuerySeparation.html), ключевая идея которой в том, что требуется разделять действия над моделью на две категории:
— Queries — возвращает результат, но не меняет состояние системы(методы без сторонних эффектов)
— Command — Изменяет состояние системы, но не возвращает состояние системы.

Для каждой категории действий над сущностями приложения мы имеем конкретные модели. Если меняется логика создания сущностей — мы актуализируем модель для Command категории. Если меняется логика отображения пользователю — мы актуализируем модель для Query.

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

CQRS (1)

Или же Command Query Responsibility Segregation

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

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

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

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

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

Модульный монолит (5)

Хочу подвести итоги темы модульного монолита.

1) Монолит — система, состоящая из одной единицы развертывания.
2) Монолит по умолчанию не комок грязи, в его определение такого нет в принципе
3) Модульный монолит — монолит, спроектированный с учетом модульности.
4) Модульный компонент — независим, автономен и имеет четкий интерфейс

Стоит помнить особенности разработки и эксплуатации монолитных систем:
1) Более легкий порог вхождения для разработки — думаю, тут все понятно.
2) Запуская проект для локальной разработки — вы запускаете ВЕСЬ проект. Был я на проекте, где с учетом размера кодовой базы и мощностей рабочего железа процесс локального развертывания мог занимать полчаса. Удовольствие очень сомнительное, скажу я вам. Желательно в таком случае иметь тестовые стенды, в которых вы можете подкинуть свои изменения и проверить выполненную работу на них.
3) Деплой монолита — это деплой всех его модулей. Опять же, с ростом кодовой базы процесс становится дольше, отсюда у разных компаний могут появляться свои системы сборки, с точечной оптимизацией процесса развертывания, свои отделы выпусков, которые будут следить за окончанием работ над каждым модулем и соблюдением режима тишины. Буду надеятся, что внутрь подобных систем сборки залить вам не придется :)
4) Иногда разработчикам двух разных модулей приходится ждать друг друга — ведь чтобы запустить сборку изменение второго разработчика, нужно будет дождаться сборки изменений первого. Обычно это касается работы в крупном энтерпрайзе.
5) Масштабируемость. Возможны ситуации, когда определенные модули потребляют ресурсов больше, чем все остальные. К сожалению, нельзя добавить мощностей только для нагруженных модулей, вернее вертикальное масштабирование сервера приложения позволит это сделать, но ВМ конечно. А при горизонтальном масштабировании, добавляя новый экземпляр приложения, мы так же закладываем ресурсы для модулей, которые не нуждаются в дополнительных ресурсах. Как итог — теряем деньги на лишние вычислительные мощности. Не факт, что подобные потери будут велики, но стоит это учитывать. (тут должна быть шутка про то, как AWS выставляет чек на крупную сумму, так как ты не отключил автомасштабирование облака).
6) Сломана нога — умирает весь человек. Если один из модулей падает с критической ошибкой, все приложение падает на пол, вся команда разрабов сидит и чинит прод на выходных, либо же просто сидит отдыхает, пока виновник торжества исправляет ситуацию, отбиваясь от дубликатов ошибок и нависшего над душой менеджера.
7) Использование сторонних технологий. В рамках монолита нельзя использовать разные ЯП(исключения есть, но не о них сейчас). Поэтому резко начать впихивать rust у вас не получится, как использовали php, так и используйте :)

Модульный монолит (4)

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

Для понимания того, как модульность выглядит на уровне приложения обратимся к схеме:

Оранжевый и сиреневый квадраты — это модули Бизнес Логики(БЛ).
Зеленые линии — это тоже модули, но модули технические.

В контексте данной темы технические модули называют «слоями», а модули БЛ «модулями», так как чаще всего изменения и разработка касаются бизнес логики — добавляются новые фичи, изменяется поведение старых и тому подобное.

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

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

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

Модульный монолит(3)

Продолжим по модульный монолит.

Если монолитная архитектура почти всегда скатывается в «комок грязи», а модульность должна нас от этого комка спасти. Давайте поймем, что такое «модуль».

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

Модулем как правило называют модуль бизнес-логики приложения(модуль почтовой рассылки), но так же есть «инфраструктурные» модули(модуль для работы с БД, очередью и так далее).

Модуль должен обладать следующими свойствами:
1) Независимостью — зависимости модуля сведены к минимуму и частота изменений «родительских» модулей минимальна. Так же снижаем сцепленность и увеличиваем связность.
2) SRP — модуль решает четко определенную задачу.
3) имеет четко определенный интерфейс — контракт для взаимодействия с другими модулями. Хороший контракт — непротиворечивый и минимальный. Интерфейс модуля должен быть стабильным(не подвергаться частому изменению).

Сравнение кода. Опрос.

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

Первый вариант:

def _write_steps(
    self,
    steps: List[SoftwareProfileStep],
    profile_dir_path: Path,
) -> None:
    """
    Записать шаги профиля в файловое хранилище

    Args:
        steps: Список шагов профиля управления ПО.
        profile_dir_path: Путь к директории профиля в файловом хранилище
    """
    steps_j2 = []
    for step in steps:
        if step.type == SoftwareProfileStepType.PACKAGE:
            step_var_uid = self._write_step_vars(
                step, 
                profile_dir_path,
            )
            steps_j2.append(
                self._step_package_template.format(
                    action=step.package_action,
                    step_vars=step_var_uid,
                )
            )
        elif step.type == SoftwareProfileStepType.REPOSITORY:
            steps_j2.append(
                self._step_repo_template.format(
                    step_uid=step.uid,
                )
            )

    steps_list = list(map(str, steps_j2))
    steps_str = self.__to_jinja_statement(
        self._steps_template.format(
            steps=steps_list,
        )
    )

    with Path(profile_dir_path / "steps.j2").open("w") as steps_file:
        steps_file.write(steps_str)

Второй вариант:

def _write_steps(
    self,
    steps: List[SoftwareProfileStep],
    profile_dir_path: Path,
) -> None:
    """
    Записать шаги профиля в файловое хранилище

    Args:
        steps: Список шагов профиля управления ПО.
        profile_dir_path: Путь к директории профиля в файловом хранилище
    """
    steps_j2 = [
        self.__fill_step_template(step)
        for step in steps
    ]

    steps_list = list(map(str, steps_j2))
    steps_str = self.__to_jinja_statement(
        self._steps_template.format(
            steps=steps_list,
        )
    )

    with Path(profile_dir_path / "steps.j2").open("w") as steps_file:
        steps_file.write(steps_str)

@staticmethod
def __fill_step_template(step: SoftwareProfileStep):
    def _package_template(step: SoftwareProfileStep):
        step_var_uid = self._write_step_vars(
            step,
            profile_dir_path,
        )
        return self._step_package_template.format(
            action=step.package_action,
            step_vars=step_var_uid,
        )
    def _repo_template(step: SoftwareProfileStep):
        return self._step_repo_template.format(
            step_uid=step.uid,
        )

    actions = {
        SoftwareProfileStepType.PACKAGE: _packege_template,
        SoftwareProfileStepType.REPOSITORY: _repo_template,
    }
    return actions.get(step.type)(step)

Кто я такой?

Senior Python Developer.
Разрабатываю систему ACM в AstraLinux. Руковожу командой из 8 разработчиков, отвечаю за функциональность установки/конфигурации ПО и выполнения заданных команд/скриптов на целевых компьютерах(контур из 4 микросервисов), управления инфраструктуры(контур из 3 микросервисов), а так же за отказоустойчивость отведенной мне подсистемы. Развиваю проект с MVP версии, спас несколько релизов и провел(и буду проводить дальше) методы повышения качества кода и DevEx от согласования до внедрения.

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

Развиваюсь в направлении архитектуры систем/кода, методологий разработки, технологиях создания качественных Backend-систем.
Вкатываюсь в fullstack-историю, оказываю консультации(в том числе трудоустройство с нуля) и разрабатываю ПО для коммерческих проектов.
Блог в телеграмме — https://t.me/SmotrovDev

Связь со мной:

  1. Консультационные услуги — https://teletype.in/@dsmotrov/y6bjePDDihw
  2. Для сотрудничества — SmotrovDM@yandex.com
Ранее Ctrl + ↓