Pull to refresh

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

Reading time 6 min
Views 1.7K
Original author: Nick Johnson
Этим топиком я хочу открыть серию переводов блога Ника Джонсона. Ник публикует крайне полезные статьи по 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?
Tags:
Hubs:
+43
Comments 6
Comments Comments 6

Articles