Статьи

Эмуляция длительного процесса (и планировщика) в Google App Engine

Как описано выше , Google App Engine (GAE) не поддерживает долго выполняющиеся процессы. Каждый процесс живет в контексте обработчика HTTP-запроса и должен завершиться в течение нескольких секунд. Если вы пытаетесь получить дополнительные циклы ЦП для какой-либо задачи, то Amazon EC2, а не GAE, является подходящим инструментом (включая возможность получения экземпляров с высоким ЦП для задач, интенсивно использующих ЦП).

Еще более удивительным является тот факт, что GAE не предлагает планировщик. Ваше приложение может быть вызвано только тогда, когда кто-то отправляет ему HTTP-запрос, и вы не можете попросить GAE генерировать постоянный запрос так часто (в стиле crontab). Это кажется как ограничивающим, так и произвольным. На самом деле, я был бы удивлен, если бы GAE не добавила поддержку в ближайшее время.

Тем временем, вам лучше всего создать учетную запись на отдельном сервере, который позволит вам планировать задания, и в этот момент вы сможете управлять своим приложением GAE из этого внешнего планировщика (через HTTP-запросы к вашему приложению GAE). Но только для интеллектуальных упражнений, как можно было бы удовлетворить потребность, оставаясь полностью в рамках предоставляемой Google инфраструктуры?

  • Наиболее очевидный вариант — использовать HTTP-запросы от ваших посетителей. Но:

    • это предполагает, что вы постоянно получаете посетителей с частотой, превышающей интервал вашего планировщика,
    • поскольку вы не можете запускать подпроцессы в GAE, это задерживает ваши ответы для посетителя,
    • Что еще более тревожно, если запланированное задание занимает более нескольких секунд, это означает, что GAE может прервать ваше приложение до того, как вы ответите посетителю, что приведет к неудачному запросу с его точки зрения.
  • Вы можете попытаться немного улучшить это, выполнив эту обработку не как часть основного запроса вашего посетителя, а добавив в ответ HTML-код JavaScript, который будет асинхронно отправлять вам HTTP-запросы в фоновом режиме (как правило, невидимые для пользователя ). Таким образом, данный посетитель будет повторять вызовы, пока страница открыта в браузере. И вы можете установить интервал вызова. Вы даже можете создать некоторую управляемую сервером автоматическую модуляцию интервала (увеличивая его по мере увеличения числа одновременных посетителей), чтобы вы не использовали все свои выделенные Google квоты входящего HTTP с этими вызовами XMLHttpRequest. Это, вероятно, будет очень практичным способом сделать это на практике, даже если:

    • оно работает только в том случае, если в вашем приложении есть посетители, использующие веб-браузеры, а не в том случае, если оно используется только программами (например, через RSS-каналы или другой формат XML),
    • это накладывает бремя на ваших посетителей, которые могут или не могут оценить это, предполагая, что они понимают, что это происходит (как бы вы себя чувствовали, если бы ваш агент по недвижимости заимствовал ваш мобильный телефон, чтобы организовать домашние визиты для вас и их других клиентов?).
  • Хотя GAE не предлагает планировщик, другой сервис Google, Google Reader, предлагает один из видов. Если вы зарегистрируете канал там, Google FeedReader будет извлекать его один раз (на основе моих журналов это происходит примерно каждый час для каждого из двух каналов этого блога). Вы можете создать несколько URL-адресов, которые отображаются на один и тот же обработчик и возвращают несколько фиктивных RSS. Если вы зарегистрируете эти каналы в Google Reader, они будут извлекаться время от времени. Конечно, нет никакой гарантии, что выгрузка различных каналов будет хорошо распределена, но если вы зарегистрируете их достаточно, вам удастся запустить их с частотой, совместимой с частотой вашего планировщика.

Это все хорошо, но не полностью в приложении GAE. Это зависит от посетителей или Google Reader. Можем ли мы сделать это полностью в рамках GAE?

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

  • Когда HTTP-запрос отменяется на стороне клиента, выполнение запроса на сервере может продолжаться (до тех пор, пока он не вернется или GAE не убьет его из-за слишком долгого выполнения).
  • Когда GAE убивает запрос из-за слишком долгого выполнения, он делает это через исключение, которое у вас есть шанс обработать (по крайней мере, на несколько секунд, пока вас не убьют навсегда), то есть когда вы инициируете запрос HTTP, который порождает преемник процесса.

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

На практике все, что вам нужно сделать, это структурировать вашу долгосрочную задачу следующим образом:

class StartHandler(webapp.RequestHandler):
def get(self):
if (StopExec.all().count() == 0):
try:
id = int(self.request.get("id"))
logging.debug("Request " + str(id) + " is starting its work.")
# This is where you do your work
finally:
logging.debug("Request " + str(id) + " has been stopped.")
# Save state to the datastore as needed
logging.debug("Launching successor request with id=" + str(id+1))
res = urlfetch.fetch("http://myGaeApp.appspot.com/start?id=" + str(id+1))

После развертывания этого приложения просто укажите в браузере http://myGaeApp.appspot.com/start?id=0 (при условии, конечно, что ваше приложение GAE называется «myGaeApp»), и начнется длительный процесс. Вы можете нажать кнопку «Стоп» в вашем браузере и выключить компьютер, процесс (или, точнее, последовательность процессов) живет полностью в рамках инфраструктуры GAE.

Утверждение «if (StopExec.all (). Count () == 0)» — это мой способ контролировать зверя (если бы только доктор Франкенштейн имел такое же предвидение). StopExec — это тип объекта в хранилище данных для моего приложения. Если я хочу уничтожить этот самореплицирующийся процесс, мне просто нужно создать объект этого типа, и процесс прекратит репликацию. Без этого единственный способ остановить это — удалить все приложение через панель инструментов GAE. В общем, использование хранилища данных в качестве разделяемой памяти — это способ связи с этой эмуляцией длительного процесса.

A scheduler is an obvious example of a long-running process that could be implemented that way. But there are other examples. The only constraint is that your long-running process should expect to be interrupted (approximately every 9 seconds based on what I have seen so far). It will then re-start as part of a new instance of the same request handler class. You can communicate state between one instance and its successor either via the request parameters (like the “id” integer that I pass in the URL) or by writing to the datastore (in the “finally” clause) and reading from it (at the beginning of your task execution).

By the way, you can’t really test such a system using the toolkit Google provides for local testing, because that toolkit behaves very differently from the real GAE infrastructure in the way it controls long-running processes. You have to run it in the real GAE environment.

Does it work? For a while. The first time I launched it, it worked for almost 30 minutes (that’s a lot of 9 second-long processes). But I started to notice these worrisome warnings in the logs: “This request used a high amount of CPU, and was roughly 21.7 times over the average request CPU limit. High CPU requests have a small quota, and if you exceed this quota, your app will be temporarily disabled.”

And indeed, after 30 minutes of happiness my app was disabled for a bit.

My quota figures on the dashboard actually looked pretty good. This was not a very busy application.

CPU Used 175.81 of 199608.00 Gigacycles (0%)
Data Sent 0.00 of 2048.00 Megabytes (0%)
Data Received 0.00 of 2048.00 Megabytes (0%)
Emails Sent 0.00 of 2000.00 Emails (0%)
Megabytes Stored 0.05 of 500.00 Megabytes (0%)

But the warning in the logs points to some other restriction. Google doesn’t mind if you use a given number of CPU cycles through a lot of small requests, but it complains if you use the same number of cycles through a few longer requests. Which is not really captured in the “understanding application quotas” page. I also question whether my long requests actually consume more CPU than normal (shorter) requests. I stripped the application down to the point where the “this is where you do your work” part was doing nothing. The only actual work, in the “finally” clause, was to opens an HTTP connection and wait for it to return (which never happens) until the GAE runtime kills the request completely. Hard to see how this would actually use much CPU. Yet, same warning. The warning text is probably not very reflective of the actual algorithm that flags my request as a hog.

What this means is that no matter how small and slim the task is, the last line (with the urlfetch.fetch() call) by itself is enough to get my request identified as a hog. Which means that eventually the app is going to get disabled. Which is silly really because by that the time fetch() gets called nothing useful is happening in this request (the work has transitioned to the successor request) and I’d be happy to have it killed as soon as the successor has been spawned. But GAE doesn’t give you a way to set client-side timeout on outgoing HTTP requests. Neither can you configure the GAE cop to kill you early so that you don’t enter the territory of “this request used a high amount of CPU”.

I am pretty confident that the ability to set client-side HTTP timeout will be added to the urlfetch API. Even Google’s documentation acknowledges this limitation: “Note: Since your application must respond to the user’s request within several seconds, a URL fetch action to a slow remote server may cause your application to return a server error to the user. There is currently no way to specify a time limit to the URL fetch action.” Of course, by the time they fix this they may also have added a real scheduler…

In the meantime, this was a fun exploration of the GAE environment. It makes it clear to me that this environment is still a toy. But a very interesting and promising one.