Представленный ниже шаблон сервера на Golang был подготовлен для передачи знаний внутри нашей команды. Основная цель шаблона, кроме обучения — это снизить время на прототипирование небольших серверных задач на Go.  Первая часть шаблона посвящена HTTP серверу: Вторая часть шаблона посвящена прототипированию REST API.
Ссылка на репозиторий проекта осталась прежней. Третья часть посвящена развертыванию шаблона в Docker, Docker Compose, Kubernetes (kustomize). Пятая часть посвящена оптимизации Worker pool и особенностям его работы в составе микросервиса, развернутого в Kubernetes.  В ходе тестирования шаблона на стенде были получены следующие результаты. Шаблон представлен в виде нескольких изолированных слоев. Наличие нескольких слоев позволяет адаптировать шаблон с класического REST API под другие варианты архитектуры: FaaS, очередь, gRPC. Для целей демонстрации, в шаблон включены два объекта "Department" и "Employee". Реализация этих объектов сделана в реляционной БД.
На уровне модели данных определяются теги для работы с БД, формирования JOSN и правила валиации.
Имена структур для модели данных выбираются на основании уникальных кратких имен сущностей (бизнес-объектов). Все методы работы с моделью сгруппированы в несколько интерфейсов.
Это позволяет изолировать выше лежащие слои обработки от слоя хранения модели в конкретной реализации (можно относительно легко переключиться с одной БД на другую).
Количество интерфейсов, обычно, соответствует количеству бизнес-сущностей (объектов, которые могут существовать и обрабатываться независимо). Объекты с типом отношений "композиция" нет смысла разносить по различным интерфейсам.
Так как пример в шаблоне ориентирован на REST API над реляционной БД, то и методы в него включены соответствующие: Get, Create, Update.
Все методы первым параметром принимают context.Context — это позволяет реализовать "отмену запросов".
Передача параметров осуществляется через указатели, причем при реализации интерфейсов, по возможности, не должны создаваться новые объекты, необходимо использовать те объекты, которые были переданы. Такой подход позволяет эффективно использовать механизм sync.Pool. Пример модели для работы с очередью IBM MQ. Пример методов модели для работы с очередью IBM MQ. Я привык классифицировать проверки, по принадлежности проверяемых атрибутов: Проверки 1, 2 и 3 уровней могут быть выполнены на основании входных данных. Проверки 4, 5, 6 уровней, обычно, требуют извлечения данных из системы хранения. Подход к выполнению проверок сильно зависит от специфики приложения и организации хранения данных.
В случае если данные хранятся в жестко структурированных нормализованных реляционных таблицах, то может быть, например, использован следующих подход. Еще один подход, который отлично работает с реляционным БД — использование промежуточных интерфейсных таблиц для DML операций. Данные сначала помещаются в интерфейсную таблицу (в БД Oracle есть возможность создавать временные сессионные таблицы). Потом запускается процедура из табличного API, которая выполняет проверки и переносит данные в основные таблицы.
Этот подход особенно хорошо подходит для массовой загрузки данных. Если хранение данных нереляционное, то проще реализовать валидацию 1, 2 и 3 уровней через проверку JSON схемы. В качестве примера в 3 части шаблона будут включены проверки 1, 2 и 3 уровней на основе библиотеки gopkg.in/go-playground/validator.
Проверки 4 уровня в шаблоне реализованы в части контроля уникальности по натуральным и суррогатным ключам (PK, UK).
Для проверок 5 и 6 уровня в шаблоне предусмотрены места для встраивания. Приведенная в шаблоне схема построения REST API (JSON->Model->DB) не требует длительного хранения объектов (структур). Объекты создаются, считываются из БД, парсятся в JSON и выбрасываются. При большом количестве запросов к API, это приводит к существенной нагрузке на сборщик мусора (GC). Это проявляется как периодическое резкое падение производительности (до завершения работы GC). Один вариантов снижения нагрузки на GC — использование стандартного sync.Pool. Принцип использования очень простой. Когда объект не нужен — кладем его в sync.Pool.
Вместо того, чтобы создавать новый объект — обращаемся к sync.Pool, если там уже есть свободный объект, то получите на него указатель, если нет, то будет создан новый объект. Для реализации пула объектов достаточно объявить переменную sync.Pool и реализовать несколько методов: Ниже пример для пула объектов Dept При реализации пула срезов указателей нужно не забывать обнулять все значения среза Замеры показали, что при интенсивной нагрузке, на 1000000 Get запросов, было создано не более 500 новых объектов. Все остальные брались из пула.
Нужно иметь в виду, что обслуживание sync.Pool требует затрат CPU, поэтому для небольших и редко используемых структур использование sync.Pool избыточно.
Структуры dept и emp из примера шаблона очень компактные и точно не стоят того, чтобы делать ради них отдельный пул.  Стандартный пакет encoding/json не славится высокой производительностью.
В качестве альтернативы рассматривал две библиотеки: На страницах авторов есть сравнения. Производительность в целом сопоставимая.
Из отличительных особенностей: В текущем шаблоне используется github.com/mailru/easyjson. После определения структур модели данных, нужно указать рядом с каждой структурой, которую нужно кодировать/декодировать в JSON комментарий для генератора. Для того, чтобы easyjson корректно обрабатывал NULL поля из БД, используется библиотека gopkg.in/guregu/null.v4. Поля структуры, которые могут быть NULL в БД определяются с использованием этой библиотеки, например, "Loc null.String" вместо привычных "Loc sql.NullString". Собственно генератор запускается командой. Использование Marshal / Unmarshal описано в разделе, посвященном JSON. Служебный слой работы с БД SQLXX в шаблоне отвечает за: Если вы используете привязку к особенностям конкретной БД, то этот уровень абстракции вам вряд ли нужен.
Я его включил в шаблон, в основном, для обработки ошибок и паник и изоляции работы с SQL от следующих слоев. В пакете шаблона httpserver/sqlxx обернуты некоторые типы и методы из пакета jmoiron/sqlx. Такая обертка позволит, при необходимости, с меньшими затратами переключиться с пакета jmoiron/sqlx, например, на пакет jackc/pgx. Пакет jmoiron/sqlx напичкан паниками, поэтому в шаблоне обернуты все ключевые методы: Beginx, Rollback, Commit, Select, Get, Exec.
Возврат ошибки после обработки паники осуществляется через именованную переменную.
Первичное логирование ошибок происходит в месте возникновения в режиме INFO без trace. В этот момент, обычно, не известно, является ли это ошибкой, или она будет успешно обработана на уровне выше. Все параметры на этом слое передаются через interface{}, поэтому нужно проверять, не только что интерфейс не пустой, но и что указатель, который в нем передается также не пустой. Операция reflect.ValueOf(in).IsNil() может привести к панике, поэтому во всех методах этого слоя предусмотрена обработка паники и возврат ошибки через именованную переменную. На этом же слое определяются основные настройки БД и параметры подключения. Конфигурация считывается из общего конфиг файла. Слой SQLXX работает с предварительно подготовленными именованными SQL командами и interface{}.
Он не имеет представления ни о модели данных, ни о параметрах SQL команд. В слое SQLXX для хранения SQL предусмотрены отдельная структура и хэш таблица. Можно хранить SQL и во внешнем файле, но большого преимущества я не вижу, а риск ошибок возрастает (любое изменение SQL требует перетестирования). Слой реализации DB в шаблоне отвечает за: Хорошая практика, иметь в реляционной БД для каждой таблицы (бизнес-сущности) следующие ключи: В примерах таблиц dept и emp, которые включены в шаблон, первичный ключ совпадает с натуральным — в промышленных решениях так делать не рекомендуется. При инициализации сервиса DB создается и наполняется хэш таблица с SQL командами.
Вся дальнейшая работа с SQL идет только по ключам хэш таблицы: "GetDept", "GetDeptUK", "DeptExists" и т.д. Это сделано намеренно, чтобы снизить риск ошибок в SQL. Для каждого объекта в модели, создается несколько SQL запросов: Если объект собирается из нескольких таблиц БД, то лучше сделать отдельный слой View API. Заниматься отладкой SQL запросов на уровне REST API очень дорогостоящее занятие. Лучше перенести это на уровень БД. После создания подключения, SQL запросы предварительно "парсятся". Реализация интерфейсов модели данных разделена на два слоя: Шаблон внутреннего слоя для Get метода крайне простой. Для простоты, часть проверок и обработка ошибок в тексте статьи убрана. Внешний слой для Get метода еще проще. Шаблон внутреннего слоя для Create метода включает шаги: Внешний слой для Create метода включает шаги: Шаблон внутреннего слоя для Update метода включает шаги: Внешний слой для Update метода включает шаги: Кэширование в памяти имеет смысл применять, если решение преимущественно ориентировано на чтение данных из БД и текущую производительность БД не получается улучшить более простыми и дешевыми способами. К основному недостатку кэширования в памяти можно отнести необходимость отслеживания изменений и менять данные в кэше. В случае, если у вас один backend узел, то отслеживать изменения достаточно легко. Но если предполагается распределенное / кластерное решение, то придется придумывать дополнительные механизмы. В 3 части шаблона будут описано кэширование в памяти на основе библиотеки dgraph-io/ristretto. Отслеживания изменений будет осуществляться через БД PostgreSQL используя механизм listen/notify. Слой JSON в шаблоне отвечает за: JSON слой работает только с интерфейсом модели данных. Он полностью отвязан от конкретной реализации модели в какой-либо БД (реляционной или нет).  Точно также слой JSON отвязан от специфики HTTP. Он не работает запросами и ответами, заголовками и телом. Он оперирует толь моделью данных и буферами []byte, которые содержат JSON. Для encode в JSON используется возможность пакета github.com/mailru/easyjson передавать результат через внешний буфер. Например, так выглядит encode в JSON для объекта Dept: Decode из JSON еще проще: Есть только одна хитрость. После генерации методов для mailru/easyjson необходимо руками поправить методы отвечающие за decode в местах, где выделяется память для вложенных подобъектов и срезов. Вместо явных операций make и new, нужно указать "правильные" методы для работы с пулом объектов модели.
Например: Сценарий Get крайне простой: Сценарий Create немногим сложнее: Сценарий Update: Кэширование в памяти имеет несколько недостатков: В 3 части шаблона будут описано кэширование JSON в go.etcd.io/bbolt. Отслеживания изменений будет осуществляться так же через БД PostgreSQL используя механизм listen/notify. Слой HTTP в шаблоне отвечает за: Большая часть этих операций отлично типизируется. Подходы к типизации написания HTTP обработчиков были описаны в моей статье Упрощаем написание HTTP обработчиков на Golang. Идея, заложенная в предлагаемый подход, достаточно простая: Количество типовых обработчиков зависит от специфики приложения. В шаблоне включен один такой обработчик process, который включает большинство типовых задач для работы с REST API. Далее будет описаны только отличия от ранее опубликованной статьи и код собственно функциональной обработки (на схеме зеленым цветом).  После формирования JSON, []byte записывается в http.ResponseWriter и выбрасывается. В последствии он убирается сборщиком мусора GC. Есть два подхода как снизить нагрузку на GC в этой ситуации. В шаблоне используется второй подход. Методы для работы с буферным пулом аналогичны для любого пула, построенного с использование стандартного sync.Pool: В конфигурационный файл добавлены параметры настройки буферного пула: Выделение буфера из пула осуществляется в типовом process перед вызовом функционального обработчика. Если выделенный буфер будет недостаточного размера, то он не будет использован, и при encode в JSON будет создан новый буфер.
Это нормальная ситуация, результирующий буфер с JSON проверяется на максимальный размер и отправляется в буферный пул.
Постепенно в буферном пуле наберутся []byte подходящего размера. Понять вернулся ли нам тот же самый буфер или был создан новый, можно сравнением указателей. Метод Pointer() возвращает указатель на первый элемент в []byte. Так как вся типовая обработка вынесена в process, то собственно в обработчике остаются задачи: POST обработчик еще проще: PUT обработчик немногим сложнее: Для упрощения регистрации новых обработчиков, в httpserver/httpservice создана простая хэш таблица Handlers. Наполнение хэш таблицы происходит в методе httpservice.New. Если обработчик нужно обернуть для обработки паники, то используется service.recoverWrap(). Тестовый стенд был собран в облаке Oracle на 3-х идентичных серверах:  Тестировались следующие сценарии: Исходные результаты тестирования доступны в репозитории.
Таблица с агрегированными данными доступна по ссылке График зависимости Requests per second от Concurrency.  График зависимости Time per req. [ms] от Concurrency (логарифмическая шкала).  График зависимости 99% percentile [ms] от Concurrency.  График зависимости 99% percentile [ms] от Concurrency (логарифмическая шкала).  Так выглядит загрузка backend сервера при 100 000 [#/sec] и кэшировании данных в BBolt (Concurrency 128). Средняя задержка ввода / вывода на NVMe диск 0,02 [ms] (500 000 IOPS).  IT Архитектор