TL;DR

Реализация динамических типов на GraphQL выглядит довольно естественно. Однако, есть определенные сложности.

Intro

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

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

Что такое динамические типы?

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

Допустим, мы разрабатываем энциклопедию Властелина Колец. Изначально мы предоставили пользователям небольшой набор рас: эльфы, гномы, хоббиты и люди. Также, мы создали несколько героев: Леголас (эльф), Гимли (гном), Фродо (хоббит) и Арагорн (человек).

Таким образом, расы — это типы данных, а герои — объекты этих типов.

Затем администратор логинится в систему и говорит: «Эй, а где Гендальф?» И он сначала добавляет расу Майар, а потом создает героя Гендальф.

Но как это можно реализовать?

Динамические типы с GraphQL

На одном из проектов в Okko мы делаем это с помощью GraphQL. Если в двух словах, то gateway предоставляет API для создания типов и объектов. После того, как создан новый тип, схема на gateway сервисе обновляется, и новый тип становится доступен для запросов и мутаций.

А теперь посмотрим детально, как это работает.

Есть три основных сервиса на бэкенде: gateway, settings и data‑access‑layer.

Gateway — это API gateway, который предоставляет GraphQL API всех бэкенд сервисов при помощи мержа их схем. И он просто перенаправляет API запросы.

Settings сервис отвечает за управление типами и атрибутами типов.

Data‑access‑layer — это главная абстракция над базой данных. Он предоставляет GraphQL API с CRUD операциями над объектами.

Итак, процесс начинается, когда фронтенд отправляет запрос на gateway на создание нового типа (1). Запрос проксируется в settings сервис, который создает новый тип в базе данных (2).

Затем settings отправляет «refresh schema» сообщение в RabbitMQ (3), которое обрабатывается data‑access‑layer сервисом.

Data‑access‑layer в свою очередь отправляет запрос в settings (4) для получения всех типов (5) и атрибутов этих типов (6) из базы данных. Как только data‑access‑layer сервис получает результат (7), он делает две вещи:

  • обновляет свою собственную GraphQL схему, чтобы появились CRUD операции для нового типа;

  • отправляет «refresh schema» сообщение в RabbitMQ (8).

Когда gateway получает «refresh schema» сообщение, он фетчит обновленные схемы со всех сервисов (9), мержит их и предоставляет GraphQL API, которое затем может быть использовано для создания объектов нового типа (10).

Почему так сложно? ????

Окей, я признаю, что выглядит все не очень просто. И при виде этой схемы в голове возникает много вопросов. Попробую ответить хотя бы на несколько.

Фетч обновленной схемы и инкрементальный мерж

На шаге (3) settings отправляет сообщение в data‑access‑layer, который в свою очередь вызывает settings (4), чтобы получить GraphQL схему со всеми типами и их атрибутами (7). Так почему бы просто не отправить инкрементальный апдейт (новый тип) в сообщении? Тогда нам вообще не нужны шаги (4)‑(7).

Когда data‑access‑layer получает «refresh schema» сообщение, он должен обновить свою GraphQL схему — добавить CRUD операции для нового типа. И этот инкрементальный мерж схемы — нетривиальная задача. Поэтому пока что мы просто достаем все типы (4)‑(7) и собираем из них новую GraphQL схему.

По той же причине на шаге (9) gateway фетчит схему со всех сервисов, несмотря на то, что схема изменилась только в data‑access‑layer сервисе.

Так что инкрементальный мерж — это наш технический долг и одно из мест, где мы можем сделать систему лучше.

Масштабирование

Теперь давайте посмотрим, что произойдет с бэкенд‑сервисами, когда они будут горизонтально масштабироваться.

Если у нас есть несколько gateway сервисов, тогда каждый из них должен получать «refresh schema» сообщение (8), чтобы обновить свою схему. И это именно то, что происходит, так что тут все хорошо.

Если у нас есть несколько data‑access‑layer сервисов, тогда нам надо быть уверенными, что

  • каждый инстанс получает «refresh schema» сообщение (3);

  • когда gateway отправляет «get schema» запрос (9), схема уже обновлена на том data‑access‑layer инстансе, который получил этот запрос.

Очевидно, первое выглядит аналогично предыдущему случаю с шагом (8), но как решается второе?

Элементарно, мой дорогой Ватсон! Мы просто не масштабируем data‑access‑layer сервис. Он всегда запускается как один инстанс, представляя собой единую точку отказа в системе.

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

Заключение

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

Комментарии (2)


  1. dimstream
    00.00.0000 00:00

    Спасибо за статью. Какие инструменты для работы с GraphQL используете?


    1. pfilaretov42 Автор
      00.00.0000 00:00

      Для генерации схемы и эндпоинтов - DGS Framework + самописный код по работе со схемами.
      Для тестирования запросов - Insomnia.