App Engine API под капотом

Original author: Nick Johnson
  • Translation
Этим топиком я хочу открыть серию переводов блога Ника Джонсона. Ник публикует крайне полезные статьи по GAE, делится опытом, ставит необычные экспериметы. Надеюсь, эти материалы будут вам полезны.

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

Общий API-интерфейс



В конечном счете, каждый API-вызов проходит через один общий интерфейс с 4-я аргументами: имя службы (например, 'datastore_v3' или 'memcache'), имя метода (например, 'Get' или 'RunQuery'), запрос и ответ. Запрос и ответ являются буферами протоколов — двоичным форматом, широко используемым в Google для обмена структурированными данными между процессами. Конкретный тип запроса и ответа буферов протокола зависит от вызванного метода. Когда происходит вызов API, буфер протокола запроса формируется из данных, отправленных в запросе, а буфер протокола ответа остается пустым и в дальнейшем заполняется данными, возвращенными ответом API-вызова.

Вызовы API осуществляются передачей четырех параметров, описанных выше, функции 'dispatch'. В Питоне эту роль выполняет модуль apiproxy_stub_map. Этот модуль отвечает за поддержку соответствия между именем службы — первым из описанных параметров — и заглушки, ее обрабатывающей. В SDK это соответствие обеспечивается созданием локальных заглушек — модулей, имитирующих поведение API. В продакшене интерфейсы к реальным API передаются этому модулю во время старта приложения, т.е. еще до того, как загрузится код приложения. Программа, которая совершает API-вызовы, никогда не должна заботиться о реализации самих API; она не знает, как обрабатывается вызов: локально или же он был сериализирован и отправлен на другую машину.

Как только функция dispatch нашла соответствующую заглушку для вызванного API, она посылает к ней вызов. То, что происходит в дальнейшем, полностью зависит от API и среды окружения, но в продакшене в целом происходи следующее: запрос буфера протокола сериализируется в двоичные данные, который потом отправляется на сервер(ы), отвечающие за обработку данного API. Наприер, вызовы к хранилищу сериализируются и отправляются к службе хранилища. Эта служба десериализирует запрос, выполняет его, создает объект ответа, сериализирует его и отправляет той заглушке, что совершила вызов. Наконец, заглушка десериализирует ответ в ответ протокола буфера и возвращает значение.

Вы, должно быть, удивлены, почему необходимо обрабатывать ответ протокола буфера в каждом API-вызове. Это потому, что формат буферов протоколов не предоставляет какого-либо способа различить типы передаваемых данных; предполагается, что вы знаете структуру сообщения, которое планируете получить. Поэтому необходимо обеспечить «контейнер», который понимает, как десериализировать полученный ответ.

Рассмотрим на примере, как всё это работает, выполнив низкоуровневый запрос к хранилищу — получние экземпляра сущности по имени ключа:
  1.  
  2. from google.appengine.datastore import datastore_pb
  3. from google.appengine.api import apiproxy_stub_map
  4.  
  5. def do_get():
  6.   request = datastore_pb.GetRequest()
  7.   key = request.add_key()
  8.   key.set_app(os.environ['APPLICATION_ID'])
  9.   pathel = key.mutable_path().add_element()
  10.   pathel.set_type('TestKind')
  11.   pathel.set_name('test')
  12.   response = datastore_pb.GetResponse()
  13.   apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Get', request, response)
  14.   return str(response)
  15.  

Очень досконально, не так ли? Особенно в сравнении с аналогичным высокоуровневым методом — TestKind.get_by_key_name('test')! Вы должны понять всю последовательность действий: формирование запроса и ответа буферов протоколов, заполнение запроса соответствующей информацией (в данном случае — именем сущности и именем ключа), затем вызов apiproxy_stub_map.MakeSyncCall для создания удаленного объекта (RPC). Когда вызов завершается, заполняется ответ, что можно увидеть по его строковому отображению:
  1.  
  2. Entity {
  3.   entity <
  4.     key <
  5.       app: "deferredtest"
  6.       path <
  7.         Element {
  8.           type: "TestKind"
  9.           name: "test"
  10.         }
  11.       >
  12.     >
  13.     entity_group <
  14.       Element {
  15.         type: "TestKind"
  16.         name: "test"
  17.       }
  18.     >
  19.     property <
  20.       name: "test"
  21.       value <
  22.         stringValue: "foo"
  23.       >
  24.       multiple: false
  25.     >
  26.   >
  27. }
  28.  

Каждый удаленный вызов для каждого API использует внутри тот же самый паттерн — различаются только набор параметров в объектах запроса и ответа.

Асинхронные вызовы


Описанный выше процесс относится к синхронному вызову API — то есть мы ждем ответа прежде чем можем делать что-либо дальше. Но платформа App Engine поддерживает асинхронные вызовы API. При асинхронных запросах мы посылаем вызов заглушке, который возвращается мгновенно, без ожидания ответа. Затем мы можем затребовать ответ позже (или подождать его, если нужно) или задать callback-функцию, которая будет автоматически вызвана, когда будет получен ответ.

На момент написания этой статьи только некоторые API поддерживают асинхронные вызовы, в частности, URL fetch API, которые крайне полезены для извлечения нескольких веб-ресурсов параллельно. Принцип действия асинхронных API такой же, как и у обычных — он просто зависит от того, реализованы ли асинхронные вызовы в библиотеке. API вроде urlfetch адаптированы для асинхронных операций, но другие, более сложные API гораздо сложнее заставить работать асинхронно.
Рассмотрим на примере, как преобразовать синхронный вызов в асинхронный. Отличия от предыдущено примера выделены жирным:
  1.  
  2. from google.appengine.datastore import datastore_pb
  3. from google.appengine.api import apiproxy_stub_map
  4. from google.appengine.api import datastore
  5.  
  6. def do_async_get():
  7.   request = datastore_pb.GetRequest()
  8.   key = request.add_key()
  9.   key.set_app(os.environ['APPLICATION_ID'])
  10.   pathel = key.mutable_path().add_element()
  11.   pathel.set_type('TestKind')
  12.   pathel.set_name('test')
  13.   response = datastore_pb.GetResponse()
  14.  
  15.   rpc = datastore.CreateRPC()
  16.   rpc.make_call('Get', request, response)
  17.   return rpc, response


Отличия в том, что мы создаем RPC-объект для одного конкретного обращения к хранилищу и вызываем его метод make_call(), вместо MakeSyncCall(). После чего возвращаем обект и ответ буфера протокола.

Поскольку это асинхронный вызов, он не был завершен, когда мы вернули RPC-объект. Есть несколько способов обработки асинхронного ответа. Например, можно передать callback-функцию в метод CreateRPC() или вызвать метод .check_success() RPC-объекта, чтобы подождать, пока вызов будет завершен. Продемонстрируем последний вариант, так как его легче реализовать. Вот простой пример нашей функции:
  1.  
  2.     TestKind(key_name='test', test='foo').put()
  3.     self.response.headers['Content-Type'] = 'text/plain'
  4.     rpc, response = do_async_get()
  5.     self.response.out.write("RPC status is %s\n" % rpc.state)
  6.     rpc.check_success()
  7.     self.response.out.write("RPC status is %s\n" % rpc.state)
  8.     self.response.out.write(str(response))
  9.  

Выходные данные:
  1.  
  2. RPC status is 1
  3. RPC status is 2
  4. Entity {
  5.   entity <
  6.     key <
  7.       app: "deferredtest"
  8.       path <
  9.         Element {
  10.           type: "TestKind"
  11.           name: "test"
  12.         }
  13.       >
  14.     >
  15.     entity_group <
  16.       Element {
  17.         type: "TestKind"
  18.         name: "test"
  19.       }
  20.     >
  21.     property <
  22.       name: "test"
  23.       value <
  24.         stringValue: "foo"
  25.       >
  26.       multiple: false
  27.     >
  28.   >
  29. }
  30.  

Константы статуса определены в модуле google.appengine.api.apiproxy_rpc — в нашем случае, 1 означает «выполняется», 2 — «закончен», из чего следует, что RPC действительно выполнен асинхронно! Фактический результат этого запроса, конечно, такой же, как и у обычного синхронного.

Теперь, когда вы знаете, как работает RPC на низком уровне и как выполнять асинхронные вызовы, ваши возможности как программиста сильно расширились. Кто первым напишет новый асинхронный интерфейс к API App Engine вроде Twisted?
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 6

    +4
    Могу помочь с переводами остальных постов, если, конечно, эта тема вызовет интерес. Практически все заметки Ника очень полезны, даже не только начинающим работать с AppEngine
      +2
      Отлично, спасибо за интересный материал. Однако, может не стоит переводить именно названия технологий — «буфер протоколов» на русском имеет немного другой смысл (а вернее — без смысла, так как буфер — это для сущностей, а протокол это описание сужностей но не сами они), лучше оставить без перевода.
        0
        Я долго сомневался и нагуглил: code.google.com/intl/ru-RU/apis/protocolbuffers/
        Это не устоявшийся перевод? Советуете так и оставить: «buffer protocols»?
          0
          Да оставить.
          Смотрите сами: «Буферы протоколов – это не зависящий от языка и платформы, расширяемый способ разделят» (оттуда же) — Буферы — это «способ разделять». При этом он сам являеться протоколом. Буфер сообщений — это правильно, буфер как определенная емкость накапливающая что-либо.
        0
        [q]Кто первым напишет новый асинхронный интерфейс к API App Engine вроде Twisted?[/q]
        Гугл?
          0
          Не факт. Как описано в статье, любой API-вызов можно сделать асинхронным, проблема в том, как распутать ответ. Гугловский webapp далеко не айс, и по сути, разработчики пишут на сторонних фреймворках и библиотеках, используя GAE просто как платформу.
          Думаю, вскоре должны появиться первые либы для асинхрона.

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