Как REST-архитектура влияет на скорость и надежность работы сайта

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

Унифицированный программный интерфейс


Ключевой момент: совместимость с HTTP-методами в плане безопасности и идемпотентности.

Безопасный запрос — это запрос, который не меняет состояние приложения.

Идемпотентный запрос — это запрос, эффект которого от многократного выполнения равен эффекту от однократного выполнения.

Таблица соответствия HTTP-метода безопасности и идемпотентности:
HTTP-Метод Безопасный Идемпотентый
GET Да Да
HEAD Да Да
OPTIONS Да Да
PUT Нет Да
DELETE Нет Да
POST Нет Нет
Что мы видим? GET-запрос не должен менять состояние ресурса, к которому применяется. PUT и DELETE запросы могут менять состояние ресурса, но их можно спокойно повторять, если нет уверенности, что предыдущий запрос выполнился. В принципе, это логично: если многократно повторять запрос удаления или замены определенного ресурса, то результатом будет удаление или замена ресурса. Но POST запрос, как мы видим из таблицы, небезопасный и неидемпотентный. То есть мало того, что он меняет состояние ресурса, так и многократное его повторение будет иметь эффект, зависимый от количества повторений. Ему по смыслу соответствует операция добавления новых элементов в коллекцию: выполнили запрос Х раз, и в коллекцию добавилось Х элементов.

Опираясь на понятия безопасности и идемпотентности легче понять, какие именно методы соответствуют операциям в терминах CRUD:
HTTP-метод Операция
POST Create
GET Read
PUT Update
DELETE Delete
Что из этого следует? При проектировании REST интерфейса надо в первую очередь думать, не о том, как будет выглядеть структура URL, а соответствует ли суть выполняемых операций безопасности и идемпотентности выбранного HTTP метода.

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

Отказ хранить состояние клиента и кэширование

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

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

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

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

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

Не совсем очевидно, но кэшировать можно не только успешные HTTP-ответы (200 OK), но и другие:

  • 301 (Moved Permanently). Ресурс сменил адрес. Мы можем выставить здесь кэширующий заголовок и пользователь запомнит, куда этот ресурс переехал, и не будет его больше запрашивать
  • 400 (Bad Request). Пользователь прислал неверные данные. Если он в следующий раз пришлет те же данные, результат будет той же самой ошибкой. Можно выставить ему кэширующий заголовок, и запрос повторяться не будет.
  • 404 (Not Found). Ресурс отсутствует. Если он еще долго не появится по этому адресу можно выставить кэширующий заголовок, чтобы предотвратить повторные запросы от этого клиента.
  • 405 (Method Not Allowed). HTTP метод не поддерживается. Можно выставить кэширующий заголовок, если его поддержка в ближайшее время не планируется
  • 410 (Gone). Ситуация такая же, как и с 404.

Надеюсь, у меня получилось заинтересовать вас на более глубокое изучение HTTP-протокола и REST-архитектуры. Есть хорошие книги, например, «RESTful Web Services Cookbook». Удачи вам и спасибо за потраченное на статью время!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 31

    0
    Срасибо. Вы предлагаете собирать веб страницу на стороне клиента или все же на стороне сервера?
      0
      Страницу можно собрать на сервере, не включая туда данные, зависящие от состояния текущего клиента. Если необходимо отрисовать элементы, зависящие от состояния клиента, то клиент или берет их из своего хранилища, или запрашивает на сервере.
        +1
        Отлично. И вместо одного GET запроса будет 100500 запросов к апи по любому чиху. За такое проектирование я бы палкой по рукам бил.
        Отказ от сессий невозможен. Так или иначе, но сессии будут «изобретены»
          +2
          Заместо одного GET-запроса — два. Причем один из них с большой вероятностью берется из кэша, а второй в идеальном случае может не выполняться, если нет необходимости забирать с сервера пользовательские данные. От сессий отказываться не обязательно.
            +1
            Нет, не два.
            Навскидку:

            1) userinfo
            2) комментарии к товарам/записям
            3) отображение самих товаров в зависимости от настроек приватности
            4) непрочтенные сообщения
            5) еще какая-нить хня — чатик там
            и т.п.

            Так что нет.
              0
              Если есть необходимость оптимизировать по количеству запросов, то для пользовательских данных гипотетически можно использовать один композитный ресурс и забрать их одним GET-запросом.
                0
                Тут прекрасная абстракция REST и потекла…
        0
        Насколько я понимаю, тут всё сильно завязано на AJAX. Окончательная «сборка страницы» происходит в браузере.
        +1
        REST-архитектура решает задачи сохранения данных, но практически всегда стоит вопрос об изменении данных на сервере и получения обновлений на клиенте. Именно поэтому в REST часто пролазят всякие адреса описывающие действие, а не состояние и костыли в виде перезапросов изменения состояния по таймауту. Но по сути это отдельные сущности в рамках одной системы:
        — хранение данных (REST)
        — уведомление об изменениях (Pull/Push)
        — модификация данных (серверная или клиентская логика)
        Отсутствие формального описания всей системы в целом создает неправильное представление о REST, как о неполной архитектуре.
        Но это не значит что REST сломана! REST это не silver bullet.

        REST чаще всего описывается поверх HTTP, расширив модель можно добавить RestEvents поверх Server-Sent Events, WebSocket и т.д.
        А в расширение RestActions можно добавить блокировки, транзакционность и прочее.
          0
          Нельзя сводить все к REST, там, где мы оперируем цельными ресурсами и файлами, там можно использовать Stateless подход, а где явно есть множество глаголов, то нужен RPC. Основное преимущество REST — ответ на вопрос «зачем?» это масштабирование. Потому, что RPC обычно имеет состояние, более того, состояние прямо в памяти сервера, да еще и долговременное соединение (вебсокеты, SSE, long polling). Но это не значит, что RPC не может масштабироваться, ему просто нужен другой балансировщие, не раундробин, а приклеивание по IP или по Cookie, а если RPC происходит через двунаправленный и долгосрочно открытый вебсокет, то и прилипание не нужно, потому, что сам вебсокет и есть состояние, к сокету состояние на сервере или сессия и приклеивается, и сервер не меняется пока не разорвется связь.

          Вполне хороший подход, когда вся статика работает через REST, это же файлы их так и обрабатывает с отдельного домена, а вся динамика работает через RPC и вебсокеты (с другого домена), а урлы идентифицируют не ресурсы, а методы (глаголы). Более того, когда на клиенте развивается направление байндинга данных и GUI (тот же React), то байндить же можно и дальше сквозь сокет прямо на сервер и это прекрасно. При чем, синхронизацию состояний можно оптимизировать, чтобы не передавать все изменения мгновенно, а накапливать буфер или за малый временной промежуток и пересылать пачкой. В базу или на диск сервер может читать/писать исключительно в отложенном режиме, состояние нужно конечно персистать, но это же не нужно делать прямо во время запроса, можно отдать ответ и уже неспешно сохранить, да еще и склеить изменения за определенное время.

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

          Тут подробнее http://habrahabr.ru/post/204958/
            0
            Целью статьи не было все свести к REST, здесь изложена классификация HTTP-методов в плане безопасности и идемпотентности, а так же сделан акцент на совместимости и компоновке веб-приложения, которая облегчила бы прозрачное кэширование значительной части контента на клиенте и промежуточными узлами. Понятно, что окружающий мир многообразен, и редко какая методология, архитектура или модель охватывает все его нюансы.

            REST вполне неплохо работает и с композитными ресурсами, и с вычислениями, и с асинхронными задачами. Выкручивать руки себе не надо, согласен, надо брать лучшее и осмысленно это использовать.
              0
              Не могу выразить, насколько этого понимания не хватает широкой общественности.
              0
              Как-то я не понял, почему вы напрямую связали RPC и stateful. Не вижу проблемы сделать RPC stateless, потому как в самом RPC-подходе нет упоминания о том, как должно храниться или не храниться состояние. Более того, если использовать RPC в концепции «чистой функции», тогда подход становится поистине stateless и вся масштабируемость REST-подхода никуда не пропадает.

              Единственное что действительно концептуально отличает RPC от REST — это стандартизация и унификация интерфейсов. Ну и то, что в основе RPC лежат «действия», а в REST — «ресурсы».
              0
              Кэширование 404-х ответов — это жесть… Никогда не думал, что всерьёз применяется.
                0
                Да ладно, 301 разве хуже?
                +1
                В реальных проектах с пользователем, который прошел аутентификацию, без сохранения состояния на сервере не обойтись. Иногда умиляют молодые программисты, которые гордо отказываются от сессии и вводят, например, токен. Хотя известно, что все это приводит в конечном результате к одному и тому же — передаче сгенерированного случайного числа в куках. И неважно что оно означает. Сессию, токен, или еще что-нибудь. И сервер по прежнему по этому числу будет восстанавливать состояние, предварительно сохраненное.
                  0
                  Надо разделять понятия: хранить состояние клиента (как субъекта отношений клиент-север) и хранить пользовательские данные. Например, флаг «держать меню свернутым» — это скорее состояние клиента, о нем вообще сервер не должен ничего знать, а тем более хранить у себя. Но список заказов конкретного пользователя — это пользовательские данные. И их, конечно, надо показывать авторизованному пользователю, если он их хозяин. Факт авторизации, конечно, можно хранить в сессии. Но если прорисовываем нейтральную по отношению к пользователю информацию (например, страницу товара), почему основной контент страницы не сделать свободным от состояния клиента и, возможно, от сессий вообще, особенно, если большинство пользователей этой страницы — не авторизованные.
                    0
                    Например, флаг «держать меню свернутым» — это скорее состояние клиента, о нем вообще сервер не должен ничего знать, а тем более хранить у себя.

                    Пользователь может мигрировать между клиентами, нынче это обычное дело.
                      +1
                      Семантика конкретных данных в приложении зависит от этого самого конкретного приложения. Если пользователь мигрировал на другой клиент и есть необходимость вместе с миграцией перенести данные, то эти данные — пользовательские и их надо хранить на сервере.

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

                      Данные конкретного пользователя, который подключается через разные клиенты, хранить надо. Но что считать пользовательскими данными, а что считать состоянием клиента, решает разработчик приложения.
                        0
                        Я вообщем-то к этому и веду. Если взять любой реальный проект, чуть сложнее сайта-визитки, то в нем, с вероятностью 100%, потребуется хранить состояние на сервере. А это означает нарушение принципов REST. Тогда возникает вопрос — зачем усложнять приложение и вводить клиентский кэш?

                        Для себя я выбрал следующую формулу. Кэш на клиенте используется в редких случаях. В основном, в страницах, требующих повышенной производительности. По умолчанию, данные пользователя хранятся в сессии.
                          +1
                          Хранение пользовательских данных на сервере не нарушает принципов REST. Нарушает принципы REST хранение состояния клиента. Надо смотреть конкретные примеры, конкретные приложения, конкретные данные, чтобы разговор был предметный.

                          Ввести клиентский кэш — не самоцель. Во многих случаях публичный клиентский кэш вообще нельзя использовать по соображениям безопасности.
                  +1
                  Всё хочу спросить у аппологетов REST-а: как соотносятся PUT и DELETE запросы (небезопасные но идемпотентные) с состоянием гонки?

                  Что должно быть, если два клиента, например, будут выполнять PUT к одному ресурсу по очереди:

                  1: PUT
                  2: PUT
                  1: PUT < — должен ли этот PUT изменять ресурс?
                    0
                    Правильная практика примерно следующая: PUT возвращает в заголовках ревизию ресурса, повторная перезапись производится, только если передана правильная актуальная ревизия.

                    P.S. Апологет.
                      0
                      Все PUT запросы могут быть отправлены до получения какого-либо ответа.
                        0
                        Тогда 2 и 3 просто отлупятся с 409
                      +1
                      Для этого предусмотрены HTTP-заголовки: If-Match и If-Unmodified-Since.
                        0
                        1: PUT (известна дата изменения — именуем D1, возвращает ETag — именуем E1)
                        2: PUT (может, конечно, отправить запрос без предварительных условий, но это не правильно)
                        1: PUT (задаем два условия: If-Match: E1, If-Unmodified-Since: D1)
                      0
                      PUT предполагает, что ресурс по заданному урлу (пере)задаётся полностью. Обновлять записи через PUT неудобно — нельзя обновить запись частично. Для частичного изменения ресурса предназначен метод PATCH:

                      tools.ietf.org/html/rfc5789

                      Правильное соответствие REST — CRUD выглядит примерно так:

                      Create — PUT
                      Read — GET
                      Update — PATCH дпя частичного обновления, PUT для перезаписи
                      Delete — DELETE

                      А что же POST? Теоретически, POST-ом можно создавать ресурсы, но это не совсем REST-way.
                      POST изменяет состояние ресурса таким образом, что это не может быть отражено прямым URL
                      Например, если пользователь имеет право выбирать id своей записи, то он делает PUT /records/{id} — кладёт новый ресурс, доступный по URL. Операции PUT и GET в этом смысле симметричны — что положено по урлу PUT-ом, то можно получить по этому же урлу GET-ом.
                      Но вот если id генерируется сервером, и пользователь его наперёд знать не может — тогда делается POST /records и в ответе возвращается id.
                      Другое очевидное применение POST — это всевозможные служебные запросы не имеющие прямых урлов. Ну, например, если нужно заапдейтить много записей сразу, делается ручка /updater, на которую POST-ом передаются параметры операции.
                        +1
                        «Как REST-архитектура влияет на скорость и надежность работы сайта» — так же, как наличие домашней собаки влияет на долголетие ее хозяина. То есть, напрямую собака на это никак не влияет, но ее приходится регулярно выгуливать, укрепляя таким образом свое здоровье.
                        Исходя из главного принципа REST — stateless — очевидно, что сервер, таким образом, будет разгружен, но станет ли от этого быстрее сайт в целом — еще вопрос.
                          0
                          инициировать загрузку сессионных данных из хранилища, сохранение сессионных данных в хранилище. Все это выливается в активное чтение и запись на диск или в БД


                          Почему вы не рассматриваете вариант сохранения данных сессии в памяти?
                            0
                            Можно сессию и в памяти хранить, если обеспечить, чтобы данные не удалялись при недостатке памяти. Но сам факт переноса сессии с диска или БД, например, в память, говорит о том, что с хранением её на диске или БД возникли сложности. Таким образом можно любые часто используемые данные, с которыми у нас проблемы, перенести в память.

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

                          Only users with full accounts can post comments. Log in, please.