GraphQL — невероятная технология, привлёкшая много внимания с тех пор, когда я начал в 2018 году использовать её в продакшене. Вам не придётся долго листать мой блог, чтобы увидеть, как я раньше продвигал её. После создания множества React SPA поверх путаницы нетипизированных JSON REST API технология GraphQL показалась мне глотком свежего воздуха. Я искренне поддерживал хайп вокруг GraphQL.

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

В статье для примеров я буду использовать код на Ruby с превосходной библиотекой graphql-ruby, но я уверен, что многие из перечисленных проблем не зависят от выбора языка/библиотеки GraphQL.

Если вы знаете более качественные решения или способы, напишите мне комментарий.

Поверхность атаки

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

Авторизация

Наверно, это самое широко признанная угроза GraphQL, так что не буду вдаваться в подробности. TLDR: если вы раскрываете полностью самодокументируемый API запросов всем клиентам, то нужно быть чертовски уверенным в том, что каждое поле авторизуется для текущего пользователя согласно контексту, в котором запрашивается это поле. Изначально казалось, что достаточно авторизации объектов, но этого быстро перестало хватать. Допустим, у нас есть Twitter X API:

query {
  user(id: 321) {
    handle # ✅ мне разрешено просматривать публичную информацию пользователей
    email # ? я не должен иметь возможности просматривать их личную информацию только потому, что могу просматривать пользователя
  }
  user(id: 123) {
    blockedUsers {
      # ? а иногда я даже не должен иметь возможности просматривать их публичную информацию,
      # потому что важен контекст!
      handle
    }
  }
}

Можно только догадываться, насколько GraphQL ответственен за то, что Broken Access Control взбирается на вершину OWASP Top 10. Устранить проблему можно, сделав ваш API защищённым по умолчанию при помощи интеграции с фреймворком авторизации вашей библиотеки GraphQL. При возврате каждого объекта и/или ресолвинге каждого поля вызывается ваша система авторизации, чтобы подтвердить наличие у текущего пользователя доступа.

Сравните это с миром REST, где в общем случае авторизуется каждая конечная точка — гораздо более мелкая задача.

Ограничение частоты

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

query {
  __schema{
    types{
      __typename
      interfaces {
        possibleTypes {
          interfaces {
            possibleTypes {
              name
            }
          }
        }
      }
    }
  }
}

Я только что протестировал на эту атаку GraphQL API explorer очень популярного веб-сайта, и получил спустя 10 секунд ответ 500. Вот так легко я потратил 10 секунд времени чьего-то CPU на выполнение этого (с удалённым пробелом) 128-байтного запроса, а от меня даже не потребовали залогиниться.

Стандартная защита1 от этой атаки такова:

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

  2. Фиксировать настоящую сложность исполняемого запроса и выкидывать его из бакета бюджета, который через какой-то интервал обнуляется.

[1] Защитой от этой и многих других атак могут стать хранимые запросы (persisted queries), но если вы хотите раскрыть предназначенный для пользователей GraphQL API, то вариант с persisted queries использовать невозможно.

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

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

type Article {
  title: String
  tags: [Tag]
}
type Tag {
  name: String
  relatedTags: [Tag]
}

При оценке сложности Tag.relatedTags можно предположить, что у статьи не может быть больше пяти тегов, поэтому вы присваиваете этим полям сложность 5 (или 5 * сложность их дочерних элементов). Проблема здесь в том, что Article.relatedTags может быть собственным дочерним элементом, так что неточность вашей оценки может накапливаться экспоненциально по формуле N^5 * 1. Поэтому при таком запросе:

query {
  tag(name: "security") {
    relatedTags {
      relatedTags {
        relatedTags {
          relatedTags {
            relatedTags { name }
          }
        }
      }
    }
  }
}

Следует ожидать сложности 5^5 = 3125. Если нападающий сможет найти статью с десятью тегами, то будет способен отправить запрос с «истинной» сложностью 10^5 = 100000, в 32 раза больше нашей приблизительной оценки.

Частично эту проблему можно решить, предотвращая запросы с глубокой вложенностью. Однако показанный выше пример показывает, что это не особо защищает, потому что мы взяли не какой-то необычно глубокий запрос. По умолчанию максимальная глубина в GraphQL Ruby равна 13, а здесь всего лишь 7.

Сравните это с ограничением частоты конечной точки REST, обычно имеющей примерно такое же время ответа. В этом случае достаточно ограничителя частоты на основе бакета, позволяющего пользователю, допустим, сделать не более 200 запросов в минуту суммарно ко всем конечным точкам. Если у вас есть более медленные конечные точки (допустим CSV-отчёт или генератор PDF), то можно задать для них более агрессивные ограничения частоты. Это элементарно делается при помощи какого-нибудь HTTP middleware:

Rack::Attack.throttle('API v1', limit: 200, period: 60) do |req|
  if req.path =~ '/api/v1/'
    req.env['rack.session']['session_id']
  end
end

Парсинг запросов

Перед исполнением запроса он парсится. Однажды мы получили отчёт пентеста, гласящий, что можно создать невалидную строку запроса, приводящую сервер к OOM (Out of memory). Например:

query {
  __typename @a @b @c @d @e ... # представьте, что здесь ещё тысяча таких директив
}

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

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

Производительность

Когда заходит разговор о производительности, пользователи GraphQL часто говорят о его несовместимости с кэшированием HTTP. Лично для меня это не стало проблемой. В SaaS-приложениях данные обычно сильно индивидуальны для конкретных пользователей, а отправка устаревших данных неприемлема, так что у меня не возникало проблемы с отсутствующими кэшами ответов (и с вызываемыми ими багами недействительности кэша…).

Самыми серьёзными проблемами производительности, с которыми столкнулся я, были…

Получение данных и проблема N+1

Мне кажется, сегодня эта проблема осознаётся многими. TLDR: если ресолвер полей обращается к внешнему источнику данных, например, к базе данных или к HTTP API, и они вложены в список, содержащий N элементов, то ресолвер выполнит эти вызовы N раз.

Эта проблема не уникальна для GraphQL, а строгий алгоритм ресолвинга GraphQL позволил большинству библиотек реализовать общее решение: шаблон Dataloader. Однако для GraphQL уникально то, что поскольку это язык запросов, подобное может стать проблемой при отсутствии изменений в бэкенде, когда клиент модифицирует запрос. В результате приходится заранее защищаться, добавляя везде абстракцию Dataloader просто на случай, если когда-то в будущем клиент запроси поле в контексте списка. При этом нужно писать и поддерживать большой объём бойлерплейта.

В REST же можно в общем случае поднять вложенные запросы N+1 к контроллеру; мне кажется, освоить такой шаблон гораздо легче:

class BlogsController < ApplicationController
  def index
    @latest_blogs = Blog.limit(25).includes(:author, :tags)
    render json: BlogSerializer.render(@latest_blogs)
  end

  def show
    # Предварительная выборка здесь не требуется, поскольку N=1
    @blog = Blog.find(params[:id])
    render json: BlogSerializer.render(@blog)
  end
endclass BlogsController < ApplicationController
  def index
    @latest_blogs = Blog.limit(25).includes(:author, :tags)
    render json: BlogSerializer.render(@latest_blogs)
  end
def show
# Предварительная выборка здесь не требуется, поскольку N=1
@blog = Blog.find(params[:id])
render json: BlogSerializer.render(@blog)
end
end

Авторизация и проблема N+1

Но постойте, есть и другие N+1! Если вы прислушались к приведённому выше совету об интеграции с фреймворком авторизации вашей библиотеки, то вам придётся справляться с совершенно новой категорией проблем N+1. Продолжим наш пример с X API:

class UserType < GraphQL::BaseObject
  field :handle, String
  field :birthday, authorize_with: :view_pii
end

class UserPolicy < ApplicationPolicy
  def view_pii?
    # О нет, я обратился к базе данных, чтобы получить друзей пользователя
    user.friends_with?(record)
  end
end
query {
  me {
    friends { # возвращает N пользователей
      handle
      birthday # выполняет UserPolicy#view_pii? N раз
    }
  }
}

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

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

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

Связывание

По моему опыту, в достаточно зрелой кодовой базе GraphQL бизнес-логика выдавливается на транспортный слой. Это происходит при помощи множества механизмов; о некоторых из них я уже говорил:

  • Солвинг авторизации данных приводит к разнесению правил авторизации по типам GraphQL

  • Солвинг авторизации мутации/аргументов приводит к разнесению правил авторизации по аргументам GraphQL

  • Солвинг получения ресолвером данных N+1 приводит к перемещению этой логики в отдельные dataloader GraphQL

  • Использование удобного шаблона Relay Connection приводит к перемещению логики получения данных в специфические custom connection object GraphQL

В конечном итоге из-за этого для качественного тестирования приложения вы обязаны выполнять тщательное тестирование на слое интеграции, например, выполнением запросов GraphQL. Это очень мучительный опыт. Все возникающие проблемы перехватываются фреймворком, что заставляет выполнять «весёлую» задачу по чтению трассировок стека в сообщениях об ошибках JSON GraphQL. Так как многое, что связано с авторизацией и Dataloader, происходит внутри фреймворка, отладка часто оказывается намного сложнее, потому что нужная вам точка останова находится не в коде приложения.

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

Сложность

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

И другое…

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

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

  • В инструментарии постоянно используются коды ответов HTTP, поэтому сильно раздражает, когда 200 может означать что угодно, от «всё в порядке» до «всё лежит».

  • Получение всех данных в одном запросе в эпоху HTTP 2 и старше часто плохо влияет на время ответа; ситуация становится ещё хуже, если сервер не распараллелен.

Альтернативы

Ну, закончим с нытьём. Что я могу порекомендовать вместо GraphQL? Откровенно говоря, я определённо нахожусь на раннем этапе цикла хайпа, но пока считаю, что можно сделать следующее:

  1. Контролировать всех своих клиентов

  2. Иметь трёх клиентов или меньше

  3. Написать клиент на языке со статической типизацией

  4. Использовать больше одного языка на сервере и в клиентах2

[2] В противном случае может больше подойти решение для конкретного языка, например, tRPC.

Вероятно, лучше раскрыть JSON REST API, совместимый с OpenAPI 3.0+. Если, как в моём случае, больше всего вашим фронтенд-разработчикам нравится в GraphQL его самодокументируемая типобезопасная природа, то я считаю, что вам подойдёт это решение. Инструментарий сильно улучшился с момента появления GraphQL; существует много способов генерации типизированного клиентского кода, вплоть до библиотек получения данных под конкретные фреймворки. Пока мой опыт можно описать так: лучшие части того, для чего я использовал GraphQL, без сложности, требующейся Facebook.

Что касается GraphQL, то существует пара подходов к реализации…

Инструментарий с упором на реализацию генерирует спецификации OpenAPI из типизированного сервера/сервера с подсказками типов. Хорошими примерами такого подхода являются FastAPI в Python и tsoa в TypeScript3. С этим подходом я работал больше всего и мне кажется, он вполне рабочий.

[3] В Ruby нет эквивалентного подхода, вероятно, из-за непопулярности подсказок типов. Вместо него у нас есть rswag, генерирующий спецификации OpenAPI из спецификаций запросов. Было бы здорово, если бы мы могли собирать спецификации OpenAPI из типизированных конечных точек Sorbet/RBS!

Инструментарий с упором на спецификацию эквивалентен подходу «главное — это схема» в GraphQL. Такой инструментарий генерирует код из написанной вручную спецификации. Не могу сказать, что когда-то смотрел на файл YAML-файл OpenAPI и думал «хотелось бы мне написать его самому», но недавний релиз TypeSpec всё изменил. Благодаря нему можно создать достаточно изящный подход на основании схемы:

  1. Написать краткую человекочитаемую схему TypeSpec

  2. Сгенерировать из неё YAML-спецификацию OpenAPI

  3. Сгенерировать клиент API со статической типизацией для выбранного вами фронтенд-языка (например, для TypeScript)

  4. Сгенерировать серверные обработчики со статической типизацией для бэкенд-языка и серверного фреймворка (например, TypeScript + ExpressPython + FastAPIGo + Echo)

  5. Написать компилируемую реализацию для этого обработчика, точно зная, что он будет типобезопасен

Этот подход ещё не проверен опытом, но я считаю, что он перспективен.

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

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


  1. softaria
    01.06.2024 11:59
    +1

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


    1. mentin
      01.06.2024 11:59

      Отдельно полноценный язык не страшен. Скажем, SaaS базы данных предоставляют SQL, такой же или более полноценный язык. Многие ещё расширения на JavaScript или Питоне позволяют. Но там строго определено к каким данным должен быть доступ, к каким нет, и это не зависит от пути по графу. И как брать деньги за запрос известно. А вот описанная смесь полноценного языка и сложной системы авторизации, и сложной системы квот, это действительно катастрофа.


      1. dph
        01.06.2024 11:59

        SaaS базы данных предоставляют сами базы данных как сервис, понятно что они отдают SQL.
        Но GQL, по сути, возвращает нас во времена двузвенных приложений, только с ухудшенной реализацией, распределенностью под капотом, но и со всеми остальными проблемами этого стиля. Причина, по которой хоть кто-то пользуется gql - не понятна.

        Но, можно использовать gql как способ описания отдельных entrypoint. Т.е. на каждую конкретную ручку - своя схема в gql, позволяющая только указывать число полей и с жестким ограничением возможностей поиска и, тем более, мутаций. Этот подход работает, позволяет уйти от кривости RESTful и использовать стандартные протоколы. Но нужна зрелая команда.


  1. MonkAlex
    01.06.2024 11:59
    +8

    Про N+1 я спрашивал ещё в первые годы хайпа. Мне говорили что провайдер магически решает вопросы. Видать не всегда =)


  1. LeVoN_CCCP
    01.06.2024 11:59
    +5

    Ой как мне хорошо помнится время когда оно считалось "передовой" технологией, что всем срочно на него надо переходить. Благо тогда костьми лёг, но не дали нашим проектам использовать его.

    В то время СДЭК как раз был на graphql (возможно и счас), это те времена когда дважды сливалась его база. Прекрасно помню по слухам конечно же, что немного магии вебреквестов и можно получить было доступ к любой переписке саппорта, к любым заказам и данным любого клиента (телефоны, адреса и т.п.) просто тупо перебирая номера отправления (а с ними сложностей никаких, если знаешь логику назначения).

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


    1. opusmode
      01.06.2024 11:59
      +1

      А GraphQL и не перестал считаться передовой технологией.

      И ваши аргументы просто глупы и нелепы. Как и ваши выводы.

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

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

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


      1. nin-jin
        01.06.2024 11:59
        +15

        Вы, видимо, считаете себя очень умным, но это определённо не так, если не видите противоречия между следующими отличительными свойствами GQL:

        • Рекурсивные выборки

        • Денормализованная выдача

        Только полнейший профан мог такое спроектировать. И только бездумные овощи делают вид, будто этой проблемы нет.

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


        1. revhead
          01.06.2024 11:59
          +1

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

          Если graphql так сильно распространен и применяется, но спроектирован полнейшими профанами, то и применяющие его, получается, безумны, просто сумасшедшие? А может быть все не совсем так и только бездумные овощи не понимают причин, по которым эта альтернатива rest'у стала так популярна. Не обошлось, конечно, без хайпа, но есть и рациональные причины. О чем все эти компании из списка adopters в graphql landscape думают (а там далеко не все перечислены, вероятно), странно все это.

          Вот тут в видео по следам этой статьи человек описывает ситуации, в которых считает оправданным применение graphql и я с ним согласен: https://www.youtube.com/watch?v=dNoVg9SOjPk

          P.S. Наименование "бездумный овощ" я у вас позаимствовал, как и категоричность в целом, но их применение не одобряю и считаю проблемой! В it есть куча других проблем, но эти просто вопиющие, ибо не допустить их было бы проще простого :) Это грубо, мне кажется.


          1. nin-jin
            01.06.2024 11:59

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

            Хотите показать себя умным? Нет ничего проще - обоснуйте грамотность решения неограниченно дублировать данные в денормализованной выдаче по глубокому запросу.


            1. revhead
              01.06.2024 11:59

              Забавно, что человек свою точку зрения считает более значимой, чем мнение довольно крупного круга лиц.

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


              1. iEstale
                01.06.2024 11:59
                +1

                Господа, снимите уже комнату или давайте по делу.


      1. LeVoN_CCCP
        01.06.2024 11:59
        +2

        Окей, будем знать, что он ещё передовой.

        Можете сказать какой(ие) аргумент(ы) вы считаете глупыми и нелепыми? Я не приводил никаких аргументов, по крайней мере, перечитав раз 5 ещё раз своё первое сообщение, не смог найти их.

        Есть факт в том, что многие программисты (особенно те, кто недавно в области) предпочитают поиспользовать ВСЁ новое в текущих проектах. Это не хорошо или плохо, это факт. Хорошо когда они изучают технологию и смотрят 1. её все плюсы и минусы 2. можно ли её применить у нас 3. насколько сильно это поменяет зоопарк текущих технологий и насколько валидно перестать использовать зоопарк перейдя на одну эту технологию (по крайней мере в области технологии). А вот плохо это когда они это делают на работающих проектах. Вот это уже аргумент, можете назвать его глупым и нелепым теперь.

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

        А я и не забываю о ГКЛ, хотя бы по причинам описанным выше со СДЭКом. И моя позиция (конкретно по тому что вы попытались притянуть за уши прочитав каким-то образом в моём первом сообщении) не в том что "не нужон этот ваш интернет", а в том, что описано тут. И у меня например так же нет супер-знаний о какахах, я видел как они выглядят, я знаю как они пахнут, я не хочу их пробовать на вкус и разбирать биологические составляющие, хотя при этом понимаю, что их через годик прекрасно можно использовать в качестве удобрений.


  1. Politura
    01.06.2024 11:59
    +3

    GraphQL хорош в качестве api gateway, когда в нем нет бизнеслогики и он просто ресолвит запрос на разные сервисы, те же самые rest api.

    В таком виде проблема авторизации просто не встает, ибо авторизация происходит на уровне бакенда.

    Проблему n+1 у себя мы решили без даталоадеров, просто в редких случаях, где она возможна, ресолвер проверяет предыдущий уровень, есть ли там фильтр по идентификатору, то есть запрашивается одна запись, или список? Если список, то запрос к бакенду для ресолвинга определенного поля не делается вообще, всегда возвращается пустой массив.

    Проблема с генерацией миллиона ошибок видимо как-то решена была до меня.

    Ну а больше в статье никаких проблем не осталось. Связывание я пропустил, ибо там про бизнес-логику, которой в гейтвее нет.


    1. olku
      01.06.2024 11:59

      Любопытное решение вместо ститчинга. Можно почитать про такое побольше?


      1. ddruganov
        01.06.2024 11:59

        Разрешаю


  1. KuKuNyaka
    01.06.2024 11:59

    Обзор написан профаном, который из бека смог в ИБ, но не смог в фуллстек. Вместо тысячи саг и простыни редьюсеров, которые нужны при использовании фастапи - графкуэль на фронте позволяет сделать из коробки внутренний кеш с хранилищем состояний и прозрачные компоненты API. Плюс сквозная автоматизированная кодогенарация в TS на основе схемы с бека - это суперкрутая фича. Плюс подписки с вебсокетами из коробки. То что на REST должны делать 10 аналитиков, 5 бекендеров и 4 фронта - на графкуэль могут сделать 2 фуллстека. Это просто тупо быстрее. Все описанные проблемы с безопасностью на беке - решаемы, и объем и сложность кода для их решения - намного меньше чем поддержка реста и обвязки на него. Аргументы в стиле "на авторизацию нужно вешать глобальную миддлварю", "резолвер словит переполнение на жопошную схему" и "сложна писать много сложного кода на беке, хочу фастапи и чилить", "криво сделанный резолвер позволяет делать мегаджойны и рекурсивный N+1" - это аргументы для бедных. Если бек+ИБ и ты не осиливаешь писать много сложного кода на беке, с последующей оптимизацией и ограничением всевозможных проблем, запросов, отладкой и перепиливанием бекового ядра используемого фреймворка графкуэль - возможно просто именно для тебя это слишком сложно. На самом деле написать много сложного кода на беке для исправления озвученных в обзоре проблем - в разы проще, быстрее и аккуратнее, чем поддерживать рест на фронте. А вообще графкуэль это не про супербезопасность, а про скорость вывода новых фич в продакшен с последующей монетизацией в условиях ограниченного финансирования.


    1. MonkAlex
      01.06.2024 11:59
      +3

      Я никогда не писал на графкуэле и не видел готовых решений - всё таки что делается с N+1 то?

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


      1. wert_lex
        01.06.2024 11:59

        del


      1. ermadmi78
        01.06.2024 11:59
        +1

        Я никогда не писал на графкуэле и не видел готовых решений - всё таки что делается с N+1 то?

        В GraphQL от N+1 защищаются точно так же, как и в REST. Просто в публичном API проектируют схему так, что выполнить N+1 невозможно. Иногда, когда очень нужно опубликовать "богатый" API, ограничивают глубину запроса на бекенде (во всяком случае в Java все инструменты для этого есть)

        Очень помогает в этом патерн проектирования API Gateway. Во "внутренних" микросервисах можно публиковать всё, что угодно - в том числе возможность выполнить N+1. Тут проблема решается постановкой нормального процесса разработки - Code Review плюс автоматизированное и/или мануальное тестирование. К проектированию "внешнего" API нужно относится вдумчиво и серьёзно. В том числе продумывать защиту от потенциальных DDoS атак.

        PS

        Я вообще против противопоставления REST и GraphQL. GraphQL - это тот же самый REST, но на стеройдах. И минусы у обеих технологий абсолютно одинаковые. Все проблемы, описанные в этой статье, можно запросто схлопотать и на REST. В том числе и N+1 (если постараться). GraphQL просто гораздо более мощный инструмент, чем REST. Поэтому с помощью него гораздо проще выстрелить себе в коленную чашечку. Но это не значит, что от него надо отказываться. Вы же не отказываетесь от езды на автомобиле только потому, что на нём можно развить большую скорость и разбиться? Вы продолжаете ездить на автомобиле. Просто соблюдаете ПДД и проявляете разумную осторожность.


        1. MonkAlex
          01.06.2024 11:59
          +1

          Я может слишком привык к формату разработки вида "фронт-бэк-субд". GraphQL же в моей голове даёт практически доступ с фронта в субд, что выглядит сразу подозрительно и странно.

          При трехзвенке есть проработка данных - какие, кому отдавать, в каком объеме, для каких целей. При использовании GraphQL я не очень представляю - остаются ли эти решения? Или фронт сам решит, что запросить, сам определит необходимый объем и всё? Нужен ли нам фуллстек для такой разработки или разработчик фронта может и не представлять, как там бэк и субд обработают запрос?


          1. ermadmi78
            01.06.2024 11:59

            GraphQL же в моей голове даёт практически доступ с фронта в субд

            Это стандартное заблуждение новичков в GraphQL. И в этом вы, поверьте, не одиноки :)

            GraphQL не имеет никакого отношения ни к СУБД ни к SQL. GraphQL - это инструмент, позволяющий осуществлять коммуникацию (синхронную и асинхронную) между двумя сервисами по сети.

            По способу реализации (коммуникация поверх HTTP и сериализация в JSON) GraphQL очень похож на REST (почти что копия).

            Но идеологически GraphQL ближе к gRPC. И тот и другой инструмент построены в парадигме "API First" (есть схема, формально декларирующая API). Оба работают поверх HTTP. Различаются они только механизмом сериализации данных. У GraphQL это JSON, у gRPC это Google Protobuf. Если сравнивать два эти инструмента, то у каждого есть свои плюсы и минусы. У GraphQL гораздо более богатый синтаксис описания схемы. Плюс GraphQL, в отличии от gRPC, может осуществлять не только синхронную, но и асинхронную коммуникацию. При асинхронной коммуникации с помощью подписок GraphQL чем то похож на брокеры сообщений. У gRPC гораздо более эффективный протокол сериализации - Google Protobuf. Грубо говоря, для злого highload я бы взял gRPC. А для построения сложного API коммуникации двух сервисов взял бы GraphQL.


            1. MonkAlex
              01.06.2024 11:59

              Эм, а как тогда работают вещи типа того что в ссылке ниже - https://graphql.org/conf/2023/sessions/09bc04c42310bfe14024455bce46d781/ описаны? Данные берутся не из БД? А если из БД, то мой вопрос остается актуальным и ответа на него в вашем сообщении я не вижу.


              1. ermadmi78
                01.06.2024 11:59

                Данные берутся либо из СУБД либо из других микросервисов. В этом GraphQL ничем не отличеется ни от REST ни от gRPC.


              1. ermadmi78
                01.06.2024 11:59

                Постараюсь выразиться понятнее. С помощью GraphQL нельзя написать и выполнить SQL запрос к реляционной СУБД. Для того, чтобы получить по GraphQL данные из СУБД нужен бекенд, который по вызову GraphQL ендпоинта получит данные из СУБД, преобразует их в JSON, и отправит в качестве ответа по сети. Реализация этого бекенда практически ничем не отличается от реализации бекенда для REST.


            1. dph
              01.06.2024 11:59

              Хм, REST не про коммуникацию поверх HTTP и не про сериализацию в JSON. REST - про реализацию гипертекстовых систем на основе идеологии ресурсов и операций с ними.
              И в этом смысле GQL никак не связан с RESTом, это совсем другая парадигма, гораздо более близкая к двузвенным архитектурам прошлого тысячелетия и повторяющая все те же ошибки, что были совершены тогда.
              И нет, grpc не эффективнее gziped json-over-http на реальных задачах )


            1. mentin
              01.06.2024 11:59

              Насчёт сравнения GraphQL и gRPC. По-моему разница не в протоколах сериализации, gRPC тот же JSON тоже умеет. Асинхронная коммуникация там тоже есть, см bidirectional streaming. Но в gRPC я описываю методы, возвращающие конкретные данные, известные и описанные заранее для каждого метода. GraphQL это query, где клиент говорит, что и как глубоко он хочет - отчего и возникают всякие проблемы описанные в статье, вроде неожиданных для разработчика N+1, или не планируемого возврата лишних полей. Можно конечно GraphQL использовать как gRPC, по отдельной схеме и реализации на каждый запрос, но зачем, когда есть gRPC?


      1. ermadmi78
        01.06.2024 11:59
        +2

        Еще один способ "защиты" от N+1 - это Dataloader. Но называть это "защитой" не совсем корректно. Это скорее способ уменьшить ущерб от N+1. При грамотном использовании ущерб можно уменьшить до нуля.


      1. KuKuNyaka
        01.06.2024 11:59
        +1

        Как оно сделает реальный вызов из БД - знает разработчик, реализовавший резолвер на беке. Если бек-разработчик не провел оптимизацию, не оптимизировал запросы и префетчи в более корневом резолвере, который порождает проблему N+1 или просто генерирует супертяжелый запрос - то ему пора пойти и поправить это, правится оно совсем не сложно. Если вдруг по каким то причинам это правится сложно - надо пойти, и поправить фреймворк бекового графкуэля который это не позволяет делать. Вообще, если не использовать автогенераторы схемы БД/фильтров/relay (а их использовать очень удобно), то все резолверы всей иерархии дерева пишутся руками (за исключением листьев - на них резолверы не нужны, т.к. объект-ответ заполняется из резолвера уровнем выше). При такой разработке - у разработчика есть полный контроль над тем, что резолвится ниже по иерархии. Соответственно N+1 можно либо джойнить, либо префетчить, либо ограничть по количеству N, либо закешировать, либо оптимизировать запрос. Как и в обычном ресте. В случае кривого автогенератора - придется форкать фреймворк автогенератора и переделывать его до таких возможностей (что сложно, но вполне реализуемо и существенно легче, чем поддерживать руками на фронте всё то, что делает GraphQL)


        1. MonkAlex
          01.06.2024 11:59

          Стало понятнее, спасибо.


        1. dph
          01.06.2024 11:59

          Ну да. GQL заставляет разработчика бэка реализовывать полноценную распределенную СУБД уровня YDB, только на коленке. С соответствующим результатом )


  1. Adrinalin4ik
    01.06.2024 11:59

    Проблему 1 и 2 полностью решает библиотека nestjs-graphql-tools https://github.com/Adrinalin4ik/Nestjs-Graphql-Tools. Что касается авторизации полей это нормально проверять что можно отдавать авторизованному пользователю, а что нет. В ресте этого нет, т.к. концепт другой, однако рест может отдавать много лишнего и отсутствует гибкость (в этом его минус), особенно чувствуется, когда тебе от юзера надо одно имя, а ты тянешь его полностью, либо городишь специальный путь на запрос пары полей из модели. Это же можно отнести к большому бойлерплейту. С кешированием есть проблемы, однако это не везде применимо, а где применимо они либо частично, либо полностью решены. Эффективность graphql точно не ниже чем рест. Любой запрос можно решить по разному. Если у тебя жирная кверя и супер глубокая вложенность, то никто не запрещает тебе сделать сделать специальный резолвер, который соберёт все и вернёт структурированные данные, грубо говоря рест в graphql. Парсинг перед исполнением дело страшное, согласен. Запрос на интроспекцию весьма тяжёлый. С тем же парсингом, он не должен парсить до конца, он должен парсить до лимитов, и это то, что он делает. Если пугает, то сделай лимит на вложенность 2-5 и он глубже не полезет, а сразу выкинет ошибку. При нахождении левой директивы в запросе или дубликата, можно тоже сразу выкидывать ошибку.



    1. dph
      01.06.2024 11:59

      А в чем проблема отдать лишние поля? Когда это вообще стало проблемой?


      1. nin-jin
        01.06.2024 11:59
        +1

        По ссылке выше есть яркий пример этой проблемы.


        1. dph
          01.06.2024 11:59

          Не смог там найти никаких реальных проблем.


          1. nin-jin
            01.06.2024 11:59

            И правда, грузить мегабайт вместо килобайта - это ж разве проблема.


            1. dph
              01.06.2024 11:59

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


              1. nin-jin
                01.06.2024 11:59
                +1

                А вы по ссылкам принципиально не ходите?


                1. dph
                  01.06.2024 11:59

                  По ссылке нет ни одного реального кейса.


  1. titan_pc
    01.06.2024 11:59

    Протобаф вместо 1000 графкуклей


  1. ermadmi78
    01.06.2024 11:59

    Если из ружья выстрелить себе прямо в коленную чашечку - то прострелишь себе коленную чашечку (здравствуй, Капитан Очевидность). Это не повод отказываться от ружья. Это повод не стрелять себе в коленную чашечку - вот и всё.

    Все проблемы, описанные в статье - это проблемы безграмотного и бездарного проектирования API (или скорее проблемы отсутствия этапа проектирования API - еще раз здравствуй Капитан Очевидность). На REST при желании можно получить абсолютно те же самые проблемы.


  1. bykostya
    01.06.2024 11:59

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


  1. mitya_k
    01.06.2024 11:59

    Согласен с автором. GraphQL актуален для задач, когда есть очень «толстое» api с сотнями полей в запросе, и клиенту нужна небольшая их часть. Но это редкий кейс, как правило больших компаний, аля Facebook.

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


  1. andrey_sunday
    01.06.2024 11:59

    Чтобы дискуссия носила конструктивный характер, необходим некоторый одинаково понятный для всех оппонентов уровень абстракции без углубления в специфику. Например, для себя я объясняю принципиальную разницу между GraphQL и REST, следующим образом. REST это набор элементов, которые описывают предметную область и вы свободно комбинируете с этими элементами для получения нужного результата, а GraphQL это уже готовые комбинации элементов из предметной области под специфическую задачу. Из этого вытекают все плюсы и минусы обеих технологий. Что касается проектирования API, то если вы применяете REST, то его не надо проектировать, REST API это логическое продолжение схемы БД, которая должна отражать все характеристики и взаимосвязи предметной области (к сожалению, это далеко не всегда так, довольно часто реляционные БД используют как чулан, куда в разной степени беспорядочности сваливают данные). Чтобы спроектировать GraphQL API нужно учесть всю специфику конкретного клиента (беда в том, что эта специфика меняется, а клиентов может быть много и у каждого своя специфика). Если самыми общими словами, то вы ищете компромисс что лучше свести к минимуму количество запросов для получения максимально подготовленных для клиента данных или сделать минимальную связанность с клиентом, но при этом клиенту надо приложить некоторые усилия чтобы как-то подготовить унифицированные данные под свою специфику.


    1. dph
      01.06.2024 11:59

      Советую все-таки почитать про REST, хотя бы диссертацию Филдинга, где вводится это понятие.
      И нет, REST никогда не отражение СУБД, он ни в коем случае не должен быть таковым.
      Ну и в реальности мало кто использует REST, чаще реально используется какой-то RPC с json-over-http, впрочем, это и правильно. Но там не будет никакого отражения СУБД )


      1. andrey_sunday
        01.06.2024 11:59

        Очень хочется верить что вы сами прочитали диссертацию Филдинга и не хотя бы, а внимательно и в оригинале. Как вы думаете, что в подавляющем большинстве случаев на практике, подразумевается под абстрактным понятием ресурс? Это запись в таблице БД (или кортеж - элемент отношения, чтобы не заставлять вас советовать мне почитать еще чью-нибудь диссертацию).

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

        Итак, пререходим непосредственно к сути. Например, нашим приложением является маркетплейс. Не будем умничать с кортежами и отношениями. У нас есть записи (они же ресурсы) о продуктах, тегах и категориях каждые из которых занесены в свою таблицу products, tags,categories. Более того, ресурсы связаны друг с другом отношениями (это уже не про те отношения, которые с кортежами), ну вы понимаете, продукт с тегами многие-ко-многим, продукты с категориями многие-ко-одному, вы не находите, что БД это есть отражение некой предметной области? А теперь, внимание, каждый ресурс должен иметь уникальный и постоянный идентификатор, а ресурс у нас что? Правильно запись в таблице БД, а это значит что? REST API есть логичное продолжение схемы БД, которая отражает что?. Предметную область приложения. Обновляем, добавляем ресурсы - записи в таблицу БД с помощью представлений (чаще всего json). Сообщение самодостаточное потому что всю необходимую информацию содержит в себе.

        Очень интересная ситуация, у нас есть набор таблиц с записями - ресурсами, которые описывают нашу предметную область, каждый ресурс имеет уникальный и постоянный идентификатор, чаще всего это URI. Каждый ресурс мы можем читать как по отдельности, так и коллекциями, можем создавать новый, обновлять и удалять существующие. Что еще нужно проектировать?. Какие-то ресурсы могут быть производными или вычисляемыми, конечно, но они определены бизнес-логикой приложения.

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

        По поводу того, что чаще используется. В диссертации Филдинга об этом ни слова, вы откуда это взяли?))


        1. dph
          01.06.2024 11:59

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

          Мне не очень интересно рассматривать REST, он так же мало применим к реализации API (что, кстати, у Филдинга прямо написано), как и GQL. Тем более не интересно обсуждать какие-то фантазии на тему REST.


          1. andrey_sunday
            01.06.2024 11:59

            А зачем обсуждаете? Это похоже на "Я гналась за вами 3 дня чтобы сказать как вы мне безразличны". )) Ну и раз уж вы здесь, позвольте тоже макну вас - неинтересно пишется слитно.