Pull to refresh

Comments 118

Думал уже о нем. Отдельно, позже проведу анализ. Протокол заслуживает отдельного внимания.
Будете делать анализ, сравните его ещё с Twirp. Не просто же так в Twitch сделали это ответвление от gRPC.
смысл их сравнивать с рестом, их и прочие rpc-протоколы надо сравнивать между собой
Все верно. Статья отсылка к той. Я даже явную ссылку дал в конце статьи, а название зеркальное отражение ;) Ну и чтобы не было какого-то странного восприятия, это не попытка дезавуировать ту статью. Лишь обратная сторона одной медали.
Мне ваша подача более близка и понятна.
REST действительно более гибок в плане использования различных неявно задействованных, но реально существующих штуковин. (Http headers http statuses, cdns, load balancers etc). Но он таки требует от разработчика бОльшего понимания и протоколов и сети в целом и архитектуры.
В то время как RPC более строг и поэтому понятен как «вещь в себе». И разработчику незачем знать обо всем остальном. В этом тоже есть своя прелесть. И ваша статья эту разницу хорошо раскрывает.
UFO just landed and posted this here
Для этого есть механизмы инвалидации. Если кратко, смотрите в сторону ETag. Тут не рассматривается, просто потому, что отдельная тема.
И всё-таки, если можно, опишите в двух словах: как веб-кеш на балансировщике узнает, что данные в БД изменились?
Если об этом балансировщику никто не скажет, никак.
Поэтому обычно инвалидируют по времени, либо балансировщик делают чуть-чуть умным.
И как его сделать более умным? Наградить его связями с другими слоями системы и отправить в свободное плавание? И все это ради только использования концепции rest?

Да понятно, про то и речь. Автор сравнивает совершенно разные вещи и делает из этого выводы. Где-то показал сферическое преимущество, где-то умолчал о нетривиальности реализации...


Ps: в целом статья очень даже хорошая.

ETag конечно классно. Но он сожалению не разгружает самый ценный (с точки зрения затрат) ресурс — бэкэнд приложения. Все что он делает это говорит клиенту что запрос не изменился и можно не тратить время на его пересылку клиенту. Никто не говорит что это плохо. Особенно если ресурс массивный. Но никакой магии не произойдет. Ресурсы бэкенда исчерпываются как правило раньше чем ресурсы простого http-сервера или сети.


Поскажите, может быть я ошибаюсь. Но с тем чтобы пробить кэш и взять данные с сервера у современных реализаций http-серверов есть проблема. Например, я установил кэш для url на топ 10 новостей на 10 минут для публичных клиентов.
Параллельно у меня есть API для контент-менеджера который добавил новость. Я как бы должен ему или сделать отдельное API которое позволит GET-запросом получить данные не кэшируемые или сделать кэш чувствительным к какому-то заголовку или cookie (что фактически также заголовок) или к параметру. Проще сделать это даже в отдельном API, т.к. в случае с заголовками придется согласовывать признаки по каоторым происходит кэширование на стороне nginx с бэкэндом, который должен устанавливать какие-то cookie или с фронтендом который должен добавлять в Ajax-запросы дополнительные заголовки.
Но вот проблема. После того как я уже решил вопрос с GET-запросом для контент менеджера без кэша — простые публичные пользователи будут продолжать получать устаревшие закэшированные данные. Если бы я разрабатывал систему на том же JSON-RPC — я смог бы управлять этим параметром более гибко.

Писал уже ниже, но повторюсь. Управление кэшем непростая задача. Это отдельный слой web-сервиса. Иногда, когда с ним возникают проблемы, некоторые проекты идут радикальным путем — исключаю его.

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

Дам ссылку на толковую статью по этой части. Сам, пока в полемику встревать не буду.

m.habr.com/ru/post/428127

Да это все мне известно. И отчасти вопрос (скорее всего) решила бы фича с purge_cache но она доступна только по коммерческой подписке
image
Это как бы уже не проблема протокола http а проблема коробочной реализации этого протокола в различных серверах.

Коммерчески состоятельные проекты тратят большие средства на ФОТ. На ФОТ грамотных, талантливых специалистов. Потратить деньги на решение, которое сделано грамотными, талантливыми специалистами другой компании, которое обоснованно собственными специалистами и окупается бизнесом — вполне логичный шаг. Более того, (ИМХО) это как раз верный шаг в сравнении с воспитанием «незаменимых» специалистов с их «уникальными» решениями на бесплатном софте. Возможно это непопулярное мнение, но он мое.

Но все же, это не единственное решение. Можно реализовать хитрые «ручки», которые доступны изнутри сети на кэшироующих серверах. И микросервисы управления кэшем. Еще раз повторюсь — нет единой пилюли для всего. Это действительно непростая часть проекта и делают ее для непростых проектов. Т.е. решение сопоставимо задаче.

Делать велосипед что на REST что на JSON-RPC неперспективно. Что касается JSON-RPC то нет ничего кроме велосипедов не только по кэшированию но и вообще по теме.


Что касается задачи — то это самый распространенный случай. Есть контент кэшируемый и обновляемый. Решается обычно влоб — есть GET api/content кэшируемый и есть GET api/admin/content не кэшируемый. То что обычный клиент получит контент с допустимым лагом вобщем-то никого особенно не волнует. Если приложение конечно самое обычное — то есть 100500 сайт-визитка или интернет-магазин

И да и нет. «Хитрые» роуты решают вопрос в случае, когда ты уверен, что не будет атаки на инфраструктуру с целью ее перегрузить.

Запросы за авторизацией, не часто нужно кэшировать. Просто потому, что действия пользователя чаще всего разнообразные, а возвращаемый контент уникальный. Т.е. кэш тут попросту ничего не дает. Но иногда и их нужно. Например, когда пользователь получает доступ к статическим файлам (скажем медиабиблиотеке) просто потому, что он залогинился.
UFO just landed and posted this here
Он, возможно, прочитает не то описание и увидит не ту цену. При стечении негативных обстоятельств. Но заплатить не ту сумму у него не выйдет. Т.к. транзакция оплаты будет валидироваться по актуальным данным. Тут-то все и встанет на места.

Ни один клиент при этом не пострадает ;)

А вот над оперативностью обновления можно думать, но не загоняться. Потому, что так можно предполажить, что клиент открыл страницу… смотрит на нее 2 часа и потом наживает кнопку купить. А за эти два часа товар поменял описание и цену. Результат тот же. Но кэш тут в общем-то не виноват. Точнее это уже кэш пользователя зафейлил информацию.

Кстати один из вариантов почему не REST — такие вещи как зависшие страницы нужно обновлять по изменению цен проталкиванием с сервера. Если делать по REST каркас приложения сильно на REST ориентируется и проталкивание это еще один каркас. Поэтому частенько разработчики махлюют и делают опросы по таймеру. Если все делать на единой технологии (типа WAMP) то не возникает сопротивления каркаса ориентированного на REST необходимости проталкивать сообщения с сервера. Все как бы работает в одном ключе.

Как вариант, это решается отправкой одиночных реквестов с кастомным заголовком. Придется сделать некий свой «велосипед» со своими правилами, но в более менее больших проектах это оправдано, если кроме этой фичи, от бесплатного nginx больше ничего не надо.
UFO just landed and posted this here
UFO just landed and posted this here
Да нет. Не получается так. Т.к. сначала создается пользователь. А уж далее выполняются несвязанные действия в его контексте. Т.е. обогащение и верификация через СМС. Ну и если на одной ноде обогащение не прошло, оно уйдет на вторую, как и в случае с СМС. А уж если и там обвалится — ну это уже даунтайм.
UFO just landed and posted this here
Реализация, которую я изложил относительно типовая. Сначала создается какой-то юзер. Но находится в состоянии «draft». Пока не пройдет его активация через SMS. Обогащение, как я понимаю, вопросов не вызывает? Остановимся на SMS. Алгоритм такой:

1. Я открываю транзакцию в СУБД
2. Инсертю секрет
3. Дергаю SMS шлюз
4. Если все удалось, комичу, если нет, ролбэчу и падаю с ошибкой 503.

Управление ушло на другую ноду.

Вы хотите сказать, что в боевом сервисе все будет не так работать? Ну так да. Дергать напрямую из бэка никто не будет гейт. Для этого SMS поставят в очередь в каконить RabbitMQ. И она уйдет когда будет возможность. Не забывайте, что цель статьи наглядность. Если я начну тут расписывать все тонкости реальных инфраструктур… основная мысль будет утеряна. Упрощение допустимое «зло» в этом случае.

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

Упрощение автора не искажает общую картину.
Представте, что у вас кролик на той ноде слег и вы не можете отправить сообщение в очередь…

Чтобы продолжать функционировать в случае неработающей очереди, при этом сохранив гарантию отправки сообщения, существует паттерн guaranteed delivery (https://www.enterpriseintegrationpatterns.com/patterns/messaging/GuaranteedMessaging.html).


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


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


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

Так в этом и суть транзакции, чтобы откатить все сразу, если что-то отказало. Если это не требуется, то не надо исполнять логику в виде транзакции — и тогда какая разница, это один батч, некоторые части которого отказали, или несколько REST-запросов, некоторые из которых отказали?
При этом зеркальный вариант — когда транзакция все-таки необходима — несколькими REST-запросами реализовать затруднительно. И это тоже надо учитывать, выбирая между REST и RPC.

На самом деле я противник того, чтобы уходить в одну крайность.
Надо RPC? Легко, только не нужно отказываться от фич HTTP. Нужен REST? Так же легко, только на надо упарываться с запросами на каждый чих когда можно сделать пачку.
Сохранение сообщения происходит в той же транзакции, что и сохранение данных, поэтому если сохранение произошло, то сообщение уже не потеряется (разве что «протухнет»).


С этим могут быть проблемы, если очередь хранится в «кролике» или «кафке» а данные хранятся в postgress/cassandra/ydb/…

Не имеет значения, в какой СУБД хранятся данные, если она поддерживает транзакции. Вот тут вариант реализации расписан более детально (http://www.kamilgrzybek.com/design/the-outbox-pattern/). Но мы все дальше уходим в оффтопик.

Транзакции в БД придумали для того чтобы гарантировать атомарность изменений данных в БД. Ваша идея держать транзакцию открытой пока вы ходите в SMS шлюз так себе решение.

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

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

В-третьих, вы все равно не можете гарантировать 100% достижение результата: даже если SMS шлюз корректно ответил, пользователь все еще может не получить SMS по ряду причин.

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

По-этому нужно коммитить секрет еще до запроса в шлюз, и только после этого отправлять SMS. Если шлюз вернул ошибку – дергать еще раз, на усмотрение бекенда. И в любом случае предусмотреть дергание шлюза еще раз по запросу пользователя, но не раньше чем через время, определенное в БД рядом с секретом и не больше чем то количество раз, которое предусмотрено конфигурацией.

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

Не знаю, откуда у вас взялись такие тезисы, но давайте разберем.
RPC запросы быстрее и эффективнее, потому, что позволяют делать batch-запросы.
Кол-во batch запросов всегда ограничивается в рамках разумного, например не больше 5.
На схеме у вас master-master репликация? Или вы будете роутить только GET запросы на реплику со всеми известными последствиями.
Использование кеша: на уровне сервера приложений (но в PHP у вас его нет по умолчанию), тут можно отдельную статью писать.
RPC запросы надежнее, потому, что могут выполнять batch-запросы в рамках одной транзакции
В документации написано, что batch запросы могут исполнятся параллельно. Поэтому с транзакциями тут вообще нет никакой связи.
The Server MAY process a batch rpc call as a set of concurrent tasks, processing them in any order and with any width of parallelism.

Весь смысл в batch-запросах: запустить их выполнение параллельно на сервере и сделать это в одном HTTP запросе, если возможно.
А смысл в RPC: это проектирование API. Даже gRPC пытаются сделать grpc-gateway, чтобы на бэкенде использовать gRPC, а фронтенд там как-нибудь сам справится.
Не скрою, ждал этого поста :))) Вопросы правильные.

Кол-во batch запросов всегда ограничивается в рамках разумного, например не больше 5.

Какой тогда смысл в этом? Какие-то условности. В протоколе есть ограничения?

В документации написано, что batch запросы могут исполнятся параллельно.

Я сразу указал, что пишу о стереотипах, которые слышу от коллег по «цеху». То, что в документации есть иное — очевидно. Как и то, что в документации по HTTP все написано. Но… не все читают. Тем более, что «может» и «нужно» неравнозначны. Все как всегда зависит от конкретной реализации. Но если вы предлагаете получать batch, парсить его и раскидывать внутри по нодам, а потом собирать в кучу… так по мне это дикий оверхед. Все уже придумано, это REST.

А смысл в RPC: это проектирование API.

Ну такой себе аргумент. Все же мы в проектирование API и на REST трудностей не испытываем. OpenAPI/Swagger в помощь.
Смысл в том, что когда у пользователя есть 5 различных сущностей (например, получить профиль, последние сообщения, счетчик новых откликов и т.д.), то проще упаковать это в batch, чем делать 5 HTTP запросов. А в этот момент на сервере они могут выполниться параллельно.
Вы можете мне возразить, что будет HTTP/2, одно соединение, и там дальше запросы полетят на разные ноды/инстансы приложения. Но если мы говорим о классическом PHP-FPM (не в daemon, типа roadrunner.dev), то конечный RPS будет у вас гораздо меньше из-за оверхеда на обработку каждого запроса на PHP.
Тем более, что «может» и «нужно» неравнозначны
Поэтому половину вашей статьи можно убрать, так как она не соответствует спецификации. Может — это как с проверкой на null: Если в 99% не null, но в 1% null, то вряд ли не нужно проверять на null, иначе все упадет.
получать batch парсить его и раскидывать внутри по нодам
graphql передает привет :)
мы в проектирование API и на REST трудностей не испытываем.
Рад за вас.
если мы говорим о классическом PHP-FPM

Вообще-то PHP далеко не единственный язык на котором можно писать RESTfull API ;-)
Там хаб в статье PHP, поэтому в примерах речь про PHP
Тут я взял хабы из упоминаемой статьи. В общем, так сложилось :)
то проще упаковать это в batch, чем делать 5 HTTP запросов

Я никогда не понимал слово «проще». Проще кому? Программисту? Инфраструктуре? Логам? Мониторингу? Выберите пожалуйста из этого списка — кому проще? Может я кого-то забыл?

А в этот момент на сервере они могут выполниться параллельно

Могут выполняться. А могут нет. Это же от вашего кода зависит? От вашего умения? А в REST они могут выполняться на разных серверах, в разных регионах мира, причем сейчас одним кодом, а через секунду другим. Или даже одновременно разными. И все параллельно. И это никак не будет зависит от вашего умения параллелить batch запросы. Т.е. ценность этого скила, скажем так, становится невысока.

Поэтому половину вашей статьи можно убрать, так как она не соответствует спецификации.

Ну уж… давайте дадим и читателям сделать свое заключение. Может не все так пессимистично?

graphql передает привет

Зачем? Он передает привет? Там своих граблей мало? У каждой технологии есть свои сферы реализации. Надеюсь, вы не нашли, что я где-то говорю, что RPC это плохо? Ведь это не так. Я даже ссылку привел на статью, которая обратное говорит. Всему свое время и место.
Тут был длинный текст, но habr его съел другой кнопкой ответить, к сожалению…

GraphQL был упомянут в контексте github.com/graphql/dataloader
По поводу проще: думайте о пользователе, которому не важно, что у вас там внутри в архитектуре, а который хочет получить результат за минимальное время. Поэтому тема взаимодействия «frontend» и «backend» на схеме вообще не раскрыта, а от этого зависит все остальное обсуждение. Кто такой фронтенд — браузер в регионах ИЛИ сервис, отдающий готовый HTML? Или и то, и другое (next.js), и еще мобильные приложения?
Network latency, время обработки ответа, легкость разработки/поддержки API, его версионирование, вот это важно. А все остальное — это уже способы достижения этих целей.
Зачем обсуждать протоколы API, если нет контекста? Берите gRPC! :)
Я планирую статью о gRPC, и, кстати, о том почему REST хорош (удобен) для всех, включая пользователя. И про оперативность там будет.

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

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

Зато все параллельно.

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

Yep.
Нет. Система не должна быть спроектирована сложнее, чем это необходимо для выполнения требований. Возможность выполнять любой запрос на любом сервере по отдельности – сомнительное требование. Достоинства такого подхода не показаны, зато недостатки – на лицо.

Запросы должны идти в разные регионы если это нужно, а не потому что так можно. Никто не мешает сделать два RPC запроса вместо одного.
Система должна быть спроектирована хорошо.

Никто не мешает сделать два RPC запроса вместо одного.

Здравый смысл мешает. Это создание дополнительных требований к протоколу. И попытка получить от RPC несвойтсвенное ему поведение. Конечно, никто это запретить делать не может. Но для этого уже есть REST. Протокол JSON-RPC не ограничивает число запросов в batch. На том и стоим. Ручное ограничение это уже не JSON-RPC.
«Хорошо» – это никудышняя формулировка требований к дизайну.
Здравый смысл мешает.
Чем именно вам мешает здравый смысл не пытаться батчить два запроса на разные домены?
Это создание дополнительных требований к протоколу. И попытка получить от RPC несвойтсвенное ему поведение. Конечно, никто это запретить делать не может. Но для этого уже есть REST. Протокол JSON-RPC не ограничивает число запросов в batch. На том и стоим. Ручное ограничение это уже не JSON-RPC.
Никаких дополнительных требований к протоколу как раз не возникает – есть некоторый набор атомарных запросов, каждый из которых должен вернуть консистентный результат, но не гарантировать консистентность между собой (если иное не оговорено явно).

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

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

Мне кажется конструктив закончился. Спасибо за комментарии!

Хорошая статья и хорошая тема. Кстати в последнее время уже не поднимает холивара.
Но некоторые моменты вызывают у меня вопросы в статье.


На приведенной схеме эта мощь реализуется через метод запроса — GET.
У HTTP методов, помимо прочего, есть стратегии кеширования. Познакомиться с ними можно в документации на HTTP. Для RPC используются POST запросы, которые не считаются идемпотентными

Это не так. Спецификация JSON-RPC не определяет не только метод но и даже транспортный протокол. Хотите в качестве транспорта использовать http-POST — пожалуйста, http-GET — никто не запрещает главное в количество символов URL уложиться. А еще можно на вебсокетах, или AMQP, MQTT и т.п.


Хотите реализовать правила кэширования — все же реализуемо. Можно кэшировать как на клиенте так и на любом из серверов. Выражаясь Вашими словами все в руках свободного художника.


Развитая инфраструктура включает в себя CDN. Часто он же решает вопрос противодействия DDoS и DoS атакам… Тут для RPC все становится совсем плачевно. RPC просто не в состоянии делегировать работу с нагрузкой CDN. Остается надеяться только на системы противодействия атакам.

CDN это совсем другая песня. Это же контент, статика (медиа, ассетики и т.п.) к удаленному вызову процедур вообще прямого отношения не имеют.


RPC запросы надежнее, потому, что могут выполнять batch-запросы в рамках одной транзакции (далее Вы показываете что это не так)

batch-запросы используют немного не для этого. Просто некоторые считают что при одном запросе будет легче структурировать приложение на фронтэнде. Эту же фичу предлагает graphql, но поскольку ни graphql, ни json-rpc широко не используются этот момент остается под вопросом.
Сами по себе транзакции внутри одной базы данных имеют смысл только если вся транзакция выполнятся единым обращением к базе данных (несколько операторов но обращение одно). Если действительно из нескольких батч-процедур вызывать пред началом bеgin, потом несколкьо обращений в базу данных и в заключении commit — то это так называемая https://en.wikipedia.org/wiki/Long-lived_transaction. Это плохо. Так никто не делает. Хотя к сожалению некоторые библиотеки на этом построены (например orm sequelize для node.js)


Порог входа в REST ниже
Я думаю, что выше проведенный анализ, развенчивающий устоявшиеся стереотипы о RPC наглядно показал, что порог входа в REST несомненно выше, чем в RPC

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


Ну и накоенец, почему я пишу на REST. Потому что есть готовые специалисты и готовые средства для разработки. Повизавшись на любую другую технологию я просто рискую растерять всех сотрудников на проекте. Когда у меня появляются проекты где я делаю и фронт и бэк я пытаюсь сделать что-то что мне больше по душе. Например https://habr.com/ru/post/459978/ на веб-сокетах + socket.io

Эту же фичу предлагает graphql

И JSON:API, правда не так удобно, как GraphQL, но зато со схожестью с REST

Начну с конца.

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

Вам, видимо, сильно везет. Я пишу статьи на Хабр потому, что в своей работе постоянно встречаюсь с необходимостью обсуждать с коллегами технологический стек, который мы будем использовать. Мы даже внедрили специальный документ — порядок принятие ключевых решений. Он требует проведение предварительного анализа любого элемента стека перед внедрением. Так вот… примерно в 70% случаев начинается диалог со слов — а давайте завезем RPC. Или — а давайте завезем GraphQL. Или gRPC.

Эта статья, по сути, часть такого анализа. Я решил, что писать сухой внутренний документ не единственный верный путь. Уверен, она мне еще не раз пригодится.

batch-запросы используют немного не для этого. Просто некоторые считают что при одном запросе будет легче структурировать приложение на фронтэнде. Эту же фичу предлагает graphql, но поскольку ни graphql, ни json-rpc широко не используются этот момент остается под вопросом.

Опять же вам реально «везет». У меня независимые коллеги только и делают, что хвастаются как они завезли GraphQL. Подчеркиваю, это не плохо, плохо то, что я часто на слышу аргументации этому.

CDN это совсем другая песня. Это же контент, статика (медиа, ассетики и т.п.) к удаленному вызову процедур вообще прямого отношения не имеют.

Вопрос CDN был поднят как средство кэширования. С этим прекрасно справляется, например CloudFlare. Для RPC он не имеет смысла в этом вопросе.

Это не так. Спецификация JSON-RPC не определяет не только метод но и даже транспортный протокол. Хотите в качестве транспорта использовать http-POST — пожалуйста, http-GET — никто не запрещает главное в количество символов URL уложиться. А еще можно на вебсокетах, или AMQP, MQTT и т.п.

Вынужден с вами согласиться. Более того, всячески поддержать — для RPC HTTP просто транспорт. И в целом, действительно можно использовать любой метод из предложенных. Но используется POST потому, что… в GET метод можно напихать и процедуры модификации и процедуры получения данных. При этом, такой запрос перестанет считаться идемпотентным. Высока вероятность, что очередной запрос просто не дойдет до сервера, потому, что будет закеширован.

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

Я еще могу заметить, что GET метод, где параметры будут PRC уже будет сильно смахивать на URI… Т.ч. все же POST.

Что же касается синхронных транспортов (типа WEBSocket), то PRC прям хорошо ложится. Как пример, мы сейчас пилим шину на WEBSocket именно с RPC вызовами.

Хорошая статья и хорошая тема. Кстати в последнее время уже не поднимает холивара.

Спасибо! Статья не ради холивара. Этого добра хватает и без нее :)
Мне кажется или у автора регулярно подгорает на теме rpc vs rest?
Речь идет о том, что в RPC можно в одном запросе выполнить вызов сразу нескольких процедур. Например, создать пользователя, добавить ему аватар и в том же запросе подписать его на какие-то топики. Всего один запрос, а сколько пользы!

Вообще, именно этот кейс прокрутить нельзя.


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

Можно. Для этого достаточно использовать uuid как идентефикатор пользователя. uuid сгенерируется еще на клиенте. В этом случае, порядок выполнения не имеет значения. Запрос может упасть в момент валидации на любой процедуре. И либо все откатится, либо все применится.
Пожалуйста, только не на клиенте! Не делайте нам больно
У… клиент должен страдать! :)))

Из комментом, я вынес для себя то, что есть еще одна интересная тема — управление распределенным хранилищем. Думаю, стоит ее затронуть. И клиент там будет очень важной и нагруженной частью. А uuid, ключевой штукой, которая позволит клиенту частично получать сервис даже в offline.
Кто в системе главный — тот и «папа». Если это например какая-нибудь большая учетная система, со своими тараканами, то и UUID будем делать тот, который она требует. Вплоть до того, что только она и имеет право его сгенерить и ее придется сначала спросить. Были и такие кейсы.
Не… GUID делался как раз для того, чтобы любой мог его генерировать и использовать без синхронизации с другим (не спрашивая). А UUID это конкретная реализация класса GUID.

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

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

Придумать «плохой» UUID так же сложно как «хороший»;)
А чем тогда контроль отличается от "… вплоть до того, что только она и имеет право его сгенерить и ее придется сначала спросить"?
Тем, что система доверяет, но проверяет. А не не доверяет и сама все делает.

В первом случае ты волен делать что угодно, но в конце результат будет проверен. Во втором случае, ты ничего не можешь делать пока тебе не дадут то, без чего ты не можешь начать.
Я конечно тот еще специалист по безопасности, но тут tools.ietf.org/html/rfc4122#section-6 явно указано, что не стоит предполагать, что UUID сложно угадать. Я ни в коем случае не утверждаю, что генерировать UUID на клиенте нельзя никогда, но есть случаи, когда стоит задуматься, особенно если речь идет об авторизации/аутентификации.
Получить доступ по UUID (угадать) и создать объект с UUID принципиально разные действия. Думать стоит всегда. Это точно.
Так там на верху ветки шла речь об одном запросе, который:
1) Создает пользователя
2) На что-то его подписывает
Причем запрос вызывает именно несколько разных процедур, а не одну с кучей параметров.
Отсюда можно сделать вывод, что зная UUID пользователя, который можно угадать — его можно и подписать на что-нибудь не нужное…
Получить доступ по UUID (угадать) и создать объект с UUID принципиально разные действия.

Здесь безусловно согласен.
Создать пользователя


В этом суть. Создание роисходит с уже указанным UUID, который сгенерирован на стороне клиента. Т.е. он о нем уже знает. А не угадывает существующего пользователя.

Это как раз аргумент в пользу клиентской генерации. Когда UUID генерируется сервером — злоумышленник ещё может "поймать" какую-то закономерность и что-то с этим сделать. Но когда идентификаторы генерируются кучей клиентов независимо — никакой закономерности просто нет.

Почему закономерности нет? Алгоритм же есть и он на клиенте, его можно вызвать много раз для анализа входа-выхода. Мне кажется, что так даже проще. Но я никогда чем-то подобным не занимался, поэтому могу быть не прав — и мне интересно в чем.
Один из этапов взлома — выяснение алгоритма. В случае генерации на клиенте, мы просто дарим алгоритм «бэзвазмэздно» (с) Сова из Винни-Пуха

Потому что у каждого клиента свой ГСЧ с независимым внутренним состоянием.

Для этого достаточно использовать uuid как идентефикатор пользователя. uuid сгенерируется еще на клиенте. В этом случае, порядок выполнения не имеет значения.
Отличный план.

Как гарантировать что клиент не подставил в качестве ID значение своего соседа?
Ключ в БД? И если будет коллизия, транзакция откатится. А если нет — проблемы соседа, что он не был первым.
Чем тогда UUID отличается от любого другого уникального пользовательского идентификатора (username, phone, email)? По-моему достоинств никаких, только добавится лишний шаг на конвертацию из логина в uuid на этапе авторизации.

Если же мы говорим о каких-либо объектах, не являющихся пользователями, тогда у нас наверняка будет необходимость авторизовывать запросы по UUID. А как их авторизовывать, если запись о самом объекте возникнуть позже, чем нужно будет произвести запись дополнительной информации по этому объекту?
Тем, что UUID формализванный стандарт придуманный умными людьми для таких целей и хранится в СУБД под отдельным типом. Что сильно помогает.
UUID не придумывался для того чтобы генерировать ключи на стороне пользователя.

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

Если вам все равно нужно гарантировать уникальность человеко-запоминаемого идентификатора, использование размерности UUID теряет смысл. Если проверки на уникальность человеко-запоминаемого идентификатора не избежать, можно вполне обойтись 64 или даже 32 битным ключем, генерируемым последовательно, что позволит сэкономить в 2 или 4 раза на размере ключа.

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

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

Остальное решается простым правилом — доверяй, но проверяй. Валидация на сервере решит эту проблему при синхронизации с клиентом.

Окей. Клиент сгенерил уже существующий uuid. Первый запрос возвращает ошибку конфликта, последующие обновляют существующего пользователя.

Ok… Забьем на авторизацию… и то, что batch выполняется в транзакции… и или все или ничего.

И какой он? Первый. Напишите тут плз его uuid.

JsonRPC по духу (но не по его спеке)

Ох и на тяжёлые дебаты вы подписались)
Все подходы по своему хороши. Мне вот нравится юзать RPC в вебсокетах.


Имхо, кэширование данных (изменяемых!) на клиенте — сомнительное решение. А кэширование на редисе во многом можно заменить кэшированием на вебсервере, который прекрасно кэширует POST запросы.

Дебаты обычные :) Я бы так сказал. Это не первый мой заход на тему REST. В прошлый раз разговор был о кодах ответа. Кирпичей тоже было знатно накладено. Ниче… Это всем полезно. Это развивает критическую точку зрения.

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

На WEBSocket мы также завозим RPC ;)
Речь идет о том, что в RPC можно в одном запросе выполнить вызов сразу нескольких процедур. Например, создать пользователя, добавить ему аватар и в том же запросе подписать его на какие-то топики. Всего один запрос, а сколько пользы!

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

Вот в этом тезисе, от которого строятся дальнейшие размышления (которые вообще верные), видится небольшая манипуляция. REST не для вызова процедур, но его мерят этой линейкой. Если отталкиваться от этого примера, то если в сервисе есть кейс, где нужно «создать пользователя, установить аватарку, подписать его на что-то», то нужно задуматься о возможности в момент создания пользователя сразу передать все требуемые данные. По сути это Resource Expansion работающий для создания ресурса. Как экстремальное развитие идеи Resource Expansion можно рассматривать GraphQL. Вы же не сравниваете RPC с GraphQL по линейке количества вызовов процедур?

Под вызовом нескольких процедур одним запросом обычно подразумевается немного другой случай. Когда на сервере реализовано api из N процедур и заранее неизвестно в каком сочетании из будет вызывать фронтенд. Например если это Хабр то при переходе на страницу со статьей возможно на фронтенде удобно будет получить список топ "что сейчас обсуждают" или "самое читаемое". Это отдается на откуп фронтенду.


Что касается REST и манипуляций это с Вами соглашусь. В процессе разных дискуссий REST понимается то в узком смысле (REST-API/RESTFull-API) то как REST в самом общем смысле как это было описано в диссертации Roy Fielding как обоснование уже давно на тот момент используемого протокола http

Интересная статья, подумаю в одном месте заюзать РПЦ, там как раз надо просто подергивать метод, передавая sql и параметры его. Джейсонрпц для этого в самый раз.


А рэст это не просто хттп и он "передача состояния"… это вообще не рпц.

UFO just landed and posted this here
Т.е. REST это не протокол, а концепция… у REST нет стандарта, есть некоторые рекомендации…


REST это архитектурный стиль описанный Роем Филдингом 20 лет назад как попытка стандартизировать тот ещё молодой WEB, где гипертекст и браузеры только развивались.
У него есть 7 конкретных ограничений из которых 6 обязательных.

Советую хотя бы посмотреть маленькое интервью с ним — youtube. В нём он как раз описал причины появления REST.

Оригинальная публикация в которой был введён термин — тут

То что вы называете REST это всё тот же RPC поверх http. http verbs, resource — это все уровень http и описано в rfc по http(rfc7231), это не про REST, и вы не найдёте этого в оригинальном описании REST. Зато найдёте такую вот строчку:
Глава 5.2 диссертации
REST ignores the details of component
implementation and protocol syntax in order to focus on the roles of components, the
constraints upon their interaction with other components, and their interpretation of
significant data elements.


P.S. И да, Рой уже не раз сам писал о своём негодовании по поводу людей который зовут термином REST то, что им не явлется. Вот, например, его статья, с хорошим объяснением.
А вот забавный твит-ответ из последнего. Слово стало модным и консультанты начали его продавать.
У меня один вопрос — вы сами то требования читали? Что из требований изложенных в REST нарушено в статье?

Советую хорошо все перепроверить перед ответом.
Что из требований изложенных в REST нарушено в статье?

Один из важнейших принципов — Unified Interface, и HATEOAS(Hypertext as the engine of application state) в частности, как минимум. Это не про случай когда клиент с сервером структурками данных общается.
Вы просто сравниваете несравнимое. И цель REST — никак не в производительности.

Обратите внимание, что первый запрос в случае с REST должен вернуть идентификатор пользователя

Архитектурный стиль сам по себе ничего не возвращает)
А сервер в REST-архитектуре должен возвращать гипертекст, клиент не должен полагаться на конкретные струтуры данных.
И переходы состояния на клиенте контроллируются сервером через гипертекст, клиент сам не знает куда ему делать запросы.
А что из них вообще присутствует?)

А что нет?

А сервер в REST-архитектуре должен возвращать гипертекст, клиент не должен полагаться на конкретные струтуры данных.

Идентификатор пользователя в парадигме REST это URI на пользователя. По которому, затем выполняются методы обогащения.

Мне кажется, что вы все же не подумали перед ответом. Жаль.
Все, что нужно знать о REST — он неотличим от HTTP.

Как это вообще понимать? Архитектурный стиль REST развивался параллельно с HTTP для того, чтобы выделить принципы, которые закладывались в основу HTTP. Это разные понятия.
Вдобавок, сам Филдинг пишет так:


A REST API should not contain any changes to the communication protocols aside from filling-out or fixing the details of underspecified bits of standard protocols, such as HTTP’s PATCH method or Link header field. Workarounds for broken implementations (such as those browsers stupid enough to believe that HTML defines HTTP’s method set) should be defined separately, or at least in appendices, with an expectation that the workaround will eventually be obsolete. [Failure here implies that the resource interfaces are object-specific, not generic.]
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
Я написал ровно то, что написал. Не ищите в этом больше. Отличить REST от простого HTTP запроса невозможно. Не зная, что это REST запрос. Это позволяет смотреть на REST проще, не загоняясь во все эти казуистические дебри — кто, что сказал в каком году про REST. А рассматривать мощь REST ввиду полного использования им «магии» HTTP.

Рой Филдинг является автором REST и соавтором HTTP. Это не "кто-то там", а авторитетный первоисточник по этому вопросу. Главный документ по REST уже написан, вам лишь нужно правильно ссылаться на него.

Спасибо. Теперь я знаю. Мой мир более не будет прежним.

Не нужно воспринимать мои посты как личностный выпад. Просто ваша статья противопоставляет RPC с HTTP и не содержит вещей, специфичных для REST.

Отличить REST от простого HTTP запроса невозможно. Не зная, что это REST запрос.

Потому что не существует такого понятия как REST-запрос, так же как не существует такой вещи, как RESTful URI.
Если я напишу приложение в рамках HTTP, широко используя его возможности и не нарушая его ограничений, это будет достаточным условием для того, чтобы назвать свое приложение RESTful?

Обязательных ограничений пять, а шестое — код по требованию — является опциональным. А в остальном совершенно солидарен с вами. Занятно, что сам Филдинг в своей диссертации указывал на любовь на любовь компьютерной индустрии к баззвордам:


Design-by-buzzword is a common occurrence. At least some of this behavior within the software industry is due to a lack of understanding of why a given set of architectural constraints is useful.
Обязательных ограничений пять, а шестое — код по требованию — является опциональным

Да, спасибо за уточнение, вот это я выдал под вечер.

Отправка SMS с временным паролем прямо из frontend'а это прям эпично.
Раз надо отправлять sms, то это какой-то публичный сайт… у вас наружу висит API "отправить любой текст любому абоненту"? Да вас спаммеры за такой интерфейс расцелуют,… а коллекторы (после того, как вы проиграете суд с смс-провайдером, через которого под вашим аккаунтом разошлют сообщений на дофига денег) снимут последние штаны.


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


Отдельно хочу добавить пять копеек по поводу "кеширование на уровне HTTP" — всен хорошо до тех пор, пока вам не попадётся корпоративная прокси с настройкой на агрессивное кеширование. Тут-то и окажется, что периодически HTTP POST — лучшее решение для API.

Уникальный request_id для каждой формы решит вопрос
UFO just landed and posted this here
Не заметил, чтобы это уже писали выше, поэтому напишу сам: использование RPC может быть обусловлено тем, что нужно делать вызов неких бизнес-методов, а не неких действий с ресурсами (давайте отложим в сторону холивар про то, есть или нет вообще ресурсы в REST). И это просто более прозрачно воспринимается, если у нас есть некий микросервис «договоры», который должен позволять сделать операцию «аннулирование договора».

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

А все то, что описано в статье и обсуждается в комментариях, на самом деле может быть решено так или иначе в обоих подходах.
давайте отложим в сторону холивар про то, есть или нет вообще ресурсы в REST

Они есть как таковые, но спецификации не ограничивают сферу того, что может быть отнесено к ресурсам. Многие почему-то воспринимают термин "ресурс" просто как объект данных, отсюда и миф о CRUD в REST.

Например, спецификации URI (Uniform Resource Identifiers):


A resource can be anything that has identity. Familiar examples include an electronic document, an image, a service [...]
RFC2396


Обновленный RFC (текущий стандарт) содержит более явную формулировку:


This specification does not limit the scope of what might be a resource; rather, the term "resource" is used in a general sense for whatever might be identified by a URI.
RFC3986


Рой Филдинг в своем блоге дополнительно пишет о ресурсах в контексте REST:


Resources are not storage items (or, at least, they aren’t always equivalent to some storage item on the back-end). [...] Likewise, a single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items.
Fielding

Почему в статье рассмотрено использования для RCP метода POST, а для REST метода GET? Ведь все зависит от реализации. Можно ведь и перевернуть примеры статьи — в REST использовать POST, а в RCP GET. И тогда все рассуждения в статье "посыпятся".

Нельзя. Даже если представить, что в параметры можно запихнуть RPC как параметры… Метод GET только для получения. Он не предусматривает модификацию на стороне сервера. Более того, он еще и кэшируется.

batch запросы обречены на метод POST. Собственно этому посвящен отдельный абзац. И тут в комментариях я это уже раскрывал — habr.com/ru/post/476576/#comment_20905682

P.S. Да и в REST используется и GET, и POST, и PUT, и DELETE…

Единственное различие между GET и POST это объем передаваемых серверу данных — GET имеет ограничение в 4К. Всё! А уж что "спрятано" на стороне сервера — это уже зависит от реализации. И GET-ом можно модифицировать состояние сервера.

Если для Вас и это не ограничения, тем более не понятны ваши рассуждения в статье. Сравните POST в обоих случаях.

Втащили в рассуждение "левое ПО" (кеш), которое не имеет какого либо отношения
к рассмотренным протоколом и реагирующее на содержание HTTP заголовка. Естественно, разная реакция этого кеша на разное содержание HTTP заголовка Вам дает разный результат в независимости от протокола. Ваши выводы не корректны. Да еще и минусы ставите, нехорошо.

Sign up to leave a comment.

Articles