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

18 мая 2023
Модульный монолит

В нашем майском Go-дайджесте мы задумались о низкоуровневых задачах и собственном Git-сервере с блэкджеком и хуками. А ещё изучили подходы к структурированию проектов на Go и нашли слово moduliths в словаре Google.

Пишем бутлоадер

Иногда мы слышим, что на Go пишут хипстеры, а чтобы создать что-то значимое, нужен язык C. Аргументируют так: Windows, Linux и macOS написаны на C, а ваш этот новомодный Go годится лишь для высокоуровневых вещей. Сегодня постараемся опровергнуть это.

Любые персональные компьютеры имеют BIOS или UEFI в качестве базовой микропрограммы. они взаимодействуют с железом на самом низком уровне, а также передают управление загрузчику операционной системы. Сложность заключается в том, что обе этих системы не совместимы друг с другом. Благо, существует универсальный загрузчик GRUB, который совместим и с BIOS, и c UEFI.

Портировать его на Go будет сложно по множеству причин. Первое, что приходит на ум — стандарт мультизагрузки требует размещения специальной структуры в первых 4 Kб бинарника. Но у Go бинарник даже в пустом виде будет весить 5 Kб. Альтернативой GRUB могут быть BOOTBOOT и Limine. Они не поддерживают старые 32-битные ядра, но с современными 64-битными никаких сложностей нет. При этом оба умеют запускать бинарники в формате ELF c нативной поддержкой go build (через настройку GOOS=linux).

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

func _start(){
// Code goes here
for { /* loop forever */ }
}

Вроде бы всё просто и с помощью флага -E можно добиться желаемого. Но сразу же появляется проблема. Что Limine, что BOOTBOOT требуют специального формата ELF-бинарника. Этот формат создаётся с помощью скриптов компоновщика… которые компоновщик Go не поддерживает.

Придётся прибегнуть к хитрости и скрестить ежа с ужом, то есть стандартный Go-компилятор со сторонним компоновщиком (например, из GCC). Опустим процесс установки GCC для разных архитектур и операционных систем, сосредоточившись на подключении компоновщика:

-ldflags="-linkmode external \
    -extld x86_64-elf-ld \
    -extldflags '-nostdlib -n -v -static -m elf_x86_64 -T link.ld'"

Это лишь начало сложного и долгого пути. Если вам интересно узнать, как разобраться с cgo, что такое прагмы и какой недокументированный флаг надо передать при сборке, то все ответы вы найдёте в статье Writing an OS in Go: The Bootloader.

Self-hosted GIT

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

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

Но давайте всё-таки о том, как без особых усилий такой локальный Git создать. На помощь приходят те же ребята, которые создали BubbleTea и Wish. Мы рассказывали об этих инструментах в одном из прошлых дайджестов. Их Git-сервер называется Soft Serve и прекрасно работает прямо из командной строки:

Soft Serve

Весь Git-сервер представляет собой один бинарник, который после запуска создаёт директорию data, где будет хранить репозитории, ключи и базу данных. При первом запуске потребуется определить переменную окружения SOFT_SERVE_INITIAL_ADMIN_KEYS. Любой ключ, добавленный в эту переменную, будет считаться ключом администратора и иметь полный набор разрешений для выполнения любых действий.

Используя эту переменную, Soft Serve создаст пользователя admin. Потом его можно будет переименовать или изменить настройки профиля. Также при первом запуске внутри директории data будет создан конфигурационный файл config.yaml с настройками сервера. Там можно кастомизировать базовые настройки, такие как используемые порты. Ну а управление репозиториями осуществляется из консоли с помощью команды repo.

Soft Serve имеет поддержку стандартных хуков git и кастомных хуков. Они могут работать глобально для всех репозиториев (удобно для выстраивания цепочек CI/CD) или быть заданы для конкретных репозиториев. По нашему мнению, это отличный инструмент, который достоин быть в арсенале разработчиков и DevOps-инженеров. А если хотите пощупать своими руками его текстовый UI, то просто подключитесь к демонстрационной версии по SSH:

ssh git.charm.sh -t soft-serve

В поисках лучшей структуры

Если о своём опыте рассказывают международные компании, такие как HUMAN Security, обычно это попадает в best practices. Недавно в блоге израильского software-инженера Авива Карми (Aviv Carmi) были выложены две части статьи, посвящённых поиску лучшей структуры для проекта на Go. Это вторая серьёзная попытка выбрать подход к структурированию кода. Первая была сделана 4 года назад и изложена в статье OK Let’s Go: Three Approaches for Structuring Go Code.

Если времени читать нет, то суть сводится к тому, что есть три основных подхода:

  1. Single Package — вся кодовая база лежит в одной директории, никаких отдельных пакетов не создаётся.
  2. Coupled Packages — разбиваем код на пакеты, единолично отвечающие за некоторое определённое поведение. Пакеты взаимодействуют друг с другом и знают друг о друге. Гарантируется, что каждая часть логики полностью реализована в отдельном пакете.
  3. Independent Packages — полностью независимые пакеты, которые, как и в предыдущем подходе, взаимодействуют друг с другом, но ничего не знают друг о друге. Для каждого сервиса пакет объявляет собственный интерфейс.

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

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

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

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

В HUMAN Security решили остановиться на Coupled Packages. Спустя несколько лет написания проектов на основе этого подхода провели ретроспективный анализ и осознали, что это было хорошее, но не лучшее решение. Детали вы найдёте в первой и второй частях оригинальной статьи.

Фреймворк Service Weaver

Забавную попытку усидеть на двух стулья сразу сделала Google, выпустив фреймворк Service Weaver. Представьте, что вы разрабатываете приложение так, словно оно монолит, но при этом у вас есть магическая кнопка, позволяющая распределить его по кластеру микросервисов. Они даже термин соответствующий придумали — moduliths, сокращение от слов modular monoliths. По замыслу авторов такая штука заполнит пробел между монолитом и микросервисами.

На бумаге всё выглядит замечательно. Фреймворк оперирует компонентами, которые могут взаимодействовать друг с другом, но при этом очень мало знают друг о друге. Чем-то напоминает отношения покупателей и продавцов на Авито. Обычный интерфейс Go становится контрактом компонента, например: 

type Reverser interface {  
	Reverse(context.Context, string) (string, error)  
}

При этом у каждого компонента есть одна или несколько реализаций:

//go:generate go run github.com/ServiceWeaver/weaver/cmd/weaver@latest generate
type reverser struct {
	weaver.Implements[Reverser]
}

// Reverse is a concrete implementation of the Reverser interface 
func (r *reverser) Reverse(ctx context.Context, s string) (string, error) {
	// concrete implementation
}

Здесь видно встраивание weaver.Implements[Reverser] в reverser. Может показаться, что нарушены все принципы интерфейсов, гласящие, что интерфейсы не должны знать друг о друге. Не всё так просто. В Service Weaver это служит триггером для генератора кода, который сделает основную магию. При запуске приложения (словно это монолит) он сделает локальную заглушку и привяжет Reverser к reverser конкретной реализации. Затем перенаправит все вызовы методов.

Когда мы захотим раскидать компоненты на микросервисы, вызов метода Reverse(...) из Reverser фактически превратится в цепочку:

  • сериализация ввода;
  • выполнение сетевого вызова;
  • десериализация на другой стороне;
  • выполнение действия;
  • возврат ответа по этой же цепочке.

Также будут инициализированы зависимости компонентов, то есть определённые локальные или сетевые прокси в зависимости от способа развёртывания. Таким образом, фреймворк Service Weaver — это гибрид из генератора кода, сетевого прокси и контейнера внедрения зависимостей (DI). Хотите узнать детали? Тогда советуем прочитать статью Преслава Рачева (Preslav Rachev) — Digging into Service Weaver: Dependency Injection.

Митапы

Онлайн

Go meetup

14 июня 2023

Совсем скоро мы встретимся на Go Meetup. Программа мероприятия формируется, но регистрация уже открыта. Кстати, вы уже можете подать доклад прямо в режиме онлайн. Заявки на участие спикера принимаются до 25 мая.

Интересуетесь нашими мероприятиями? В Telegram-канале Evrone meetups мы выкладываем анонсы с подробными описаниями докладов, а также студийные записи прошедших митапов. Тем для кого выступать в новинку, мы оказываем всяческую поддержку и помогаем оформить экспертизу в яркое выступление. Подписывайтесь и пишите @andrew_aquariuss, чтобы узнать подробности.

Регистрация

Вакансии

Удаленка / Офис

Evrone 

Мы открыты для новых Go-разработчиков. В Evrone можно работать удалённо с первого дня, мы поддерживаем и оплачиваем участие в Open-source проектах и выступления на конференциях, а расти в грейдах можно с помощью честной системы проверки навыков и менторства.

Подробнее

 

Подписаться
на Digest →
Важные новости и мероприятия без спама
Технологии которыми вы владеете и которые вам интересны
Ваш адрес электронной почты в безопасности - вот наша политика конфиденциальности.