Обратите внимание, что новости можно получать по RSS.
X
-

Информационные технологии, LiveJournal cr_it - архив

13 августа 2009, 16:12 (3353 дня назад, №8772)Пример разработки под Google App Engine (погода)
На примере приложения показывающего прогноз погоды, захотелось попробовать, что представляет собой Google App Engine (а заодно - Python). Клиентская часть (реализация на Flex) достаточно примитивна - при выборе города посылается запрос к серверной части и та возвращает XML пакет с прогнозом на несколько дней, который показывается в виде текста и картинок. Текущий город запоминается у пользователя в SharedObject.

Более интересно поговорить о серверной части.

Задача для неё заключается в следующем:

Два раза в сутки мы должны забирать XML файл размером примерно 300 кб (прогноз по сотням городов на 4 дня), парсить его и помещать в базу данных. После чего отдавать клиенту данные для нужного города/даты. Достаточно важный момент - прогноз постоянно уточняется, поэтому если мы получаем данные за дату для которой они уже есть в базе, нужно их обновить.

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

Первая и главная трудность, с которой пришлось столкнуться - так называемые "квоты" GAE на все, что только можно. На процессорное время, на число и длительность HTTP запросов, на объем передаваемых и принимаемых по HTTP данных, на число запросов к хранилищу и т.п. Причем, есть суточные квоты, которых при правильном программировании в принципе хватает и бесплатных (их можно увеличить, включив биллинг) и есть "по-минутные" квоты, с которыми все серьезнее. Основной смысл по-минутных квот - не позволять приложению потреблять ресурсы в ущерб другим приложениям, которые работают в данный момент на том же сервере. Квоты эти чрезвычайно жёсткие, из-за чего подход к разработке принципиально меняется. Нужно постоянно думать, как уместиться в ограничения, помнить какие операции считаются дешевыми, а какие дорогими (в прямом и переносном смысле). Процесс живо напомнил мне программирование на ассемблере для восьмибитного MOS6510 (56 команд, три регистра) на Commodore 64 :)




Возьмём, к примеру, простую, казалось бы, операцию - получение XML файла, его парсинг (сделал через minidom.xml, хотя эффективнее, видимо, было бы через elementstree) и помещение данных в хранилище. На PHP/MySQL и традиционном хостинге мы могли бы просто в цикле вставлять получаемые данные INSERT'ом в базу. Здесь, увы, халява не пройдёт - уже через пару десятков таких вставок будет превышена по-минутная квота на api_calls к хранилищу и приложение будет прибито с exception'ом "DeadlineExceed".

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

К счастью, совсем недавно появилось нормальное решение - сервис TaskQueue. Идея в том, что можно запускать кусок кода так, что он будет выполняться в фоне. В цикле создаём кучу задач (по одной на каждый город) и каждая из этих задач независимо от других занимается записью данных по погоде в этом городе в datastore. GAE сам решает, на каких серверах какая задача выполняется. Можно зафиксировать минимальный промежуток времени между запуском каждой задачи и таким образом растянуть процесс, избегая превышения по-минутных квот по CPU time.

for cnode in cnodes:

...
dbentry.tt = entry['tt']
dbentry.w = entry['w']
...
taskqueue.add(url='/putentry2db', params={'dbentry': pickle.dumps(dbentry)})

class PutEntry2DB(webapp.RequestHandler):
def post(self):
d = self.request.get('dbentry')
dbe = pickle.loads(str(d))
...
res = dbe.put()

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

API Calls CPU означает в данном случае именно время затраченное на обращение к datastore ( там get() и put() ) и обращение к taskqueue.add()




Итак, допустим, данные в хранилище мы поместили и они там лежат в виде:

class DBEntry(db.Model):
dtf = db.DateProperty(indexed=True) # на какую дату прогноз
c = db.IntegerProperty(indexed=True) # city id
t = db.StringProperty() # Название города
...
tf = db.ListProperty(long,indexed=False) # Температура от
tt = db.ListProperty(long,indexed=False) # Температура до
...
dtc = db.DateTimeProperty(auto_now_add=True,indexed=True) # когда сделана запись

Теперь задача номер два - надо отдавать требуемые данные Flex клиенту. По-правильному, вероятно, надо было бы делать это в виде бинарного AMF3 (PyAMF вполне работает с GAE), но я на этот момент уже обжёгся на квотах (на кодирование в AMF будет уходить ощутимый CPU time, полагаю) и решил отдавать обычный XML.

Для начала попробовал просто читать из datastore:

query = db.GqlQuery("SELECT * FROM DBEntry WHERE c = :1 AND dtf >= :2 AND dtf <= :3",c,date_msk_since,date_msk_till)
...
results = query.fetch(10)

но эти чтения оказались весьма дорогими. Пришлось делать по-правильному, кэшируя запросы для одинаковых городов и дат через memcache.

Разница получилась очень ощутимая:

Из datastore:

575ms 1983cpu_ms 1620api_cpu_ms
401ms 378cpu_ms 210api_cpu_ms
300ms 345cpu_ms 210api_cpu_ms
505ms 1927cpu_ms 1620api_cpu_ms


Из кэша:

24ms 31cpu_ms
52ms 22cpu_ms
20ms 25cpu_ms
23ms 26cpu_ms


(во втором случае api_cpu нет, т.к. нет обращения к datastore)

Основная функциональность в итоге достигнута. Но захотелось какой-то минимальной статистики. И это - слабая сторона GAE. Дело в том, что datastore - нереляционная база данных. Невозможно выбирать данные из нескольких "таблиц" связанных по какому-то свойству. Не существует агрегатных функций (вплоть до того, что невозможно узнать сколько записей в хранилище - разве что выбрав их все).

Если нужна любая статистика, её надо считать не тогда, когда данные уже есть, а в процессе записи данных в datastore. Т.е. если хочется узнать число запросов прогноза, нужно сделать счетчик, и при каждом обращении увеличивать его значение в datastore. Но это в теории. На практике такая операция (постоянное чтение-запись одной и той же записи в datastore) приведёт все туда же - к превышению квот. Выход из этого положения в использовании т.н. sharded counters. Идея в том, чтобы не мучать одну и ту же запись, а создать их, скажем, десяток и каждый раз увеличивать случайную запись из этого десятка. Тогда GAE сможет распараллелить процесс. Само собой, чтобы узнать значение счётчика, придется вытащить все эти десять значений и сложить. Но это дешевая операция, к тому же редкая.

Вообще, специфика datastore создаёт множество препятствий. К примеру, нет способа удалить все данные (если решили начать заново или, скажем, изменить модель данных). Придется удалять по одной записи, со всеми вышеописанными неприятностями (т.е. в цикле это делать не выйдет - квоты сразу превысим). Даже если у вас накопилось всего каких-нибудь 10 тысяч записей - это уже серьезная проблема.

Нельзя просмотреть более 1000 записей подряд (в терминах SQL: нельзя указать для LIMIT больше 1000). Собственно понятно, что это все те же квоты для равномерного распределения нагрузки, только в завуалированном виде. В последнем roadmap'е вот грозятся ввести курсоры, чтобы как-то жить с этим ограничением.

Язык запросов (GQL) только на очень поверхностный взгляд напоминает SQL. На самом деле там куча ограничений, в WHERE далеко не все операции можно сочетать. Выбираются всегда все свойства записи (" SELECT * " указывается скорее для красоты).

Помимо этих проблем, обусловленных архитектурой системы (её распределенностью, в первую очередь) есть и просто недоработки. Казалось бы - сервис Google'a, уж наверное там с поиском должно все быть просто, удобно и мощно. Увы. Пока нормального решения просто нет. Также как нет, например, сервиса для хранения больших файлов (хотя это уже вот-вот обещают).

Также, что касается админки - порядочно сделано для того, чтобы можно было следить за нагрузкой, но почти ничего для работы с тем же datastore.

Отдельно надо отметить нестабильность системы. Речь даже не столько о периодических отказах сервисов (они не так уж часты и вполне допустимы, учитывая статус GAE), сколько о совершенно непредсказуемой реакции системы на совершенно одинаковые действия. Один и тот же скрипт, ежедневно забирающий практически одинаковые файлы и помещающий в datastore одинаковое количество очень похожих данных может то работать без ошибок, создавая небольшую нагрузку, то вдруг превышать допустимый CPU time, выдавать различного рода TransientError, MemoryError, Timeout и прочее, которые в группе поддержки GAE, как правило, никак не комментируются (т.е. люди часто спрашивают, но конкретно по таким вопросам ответов не получают).

Общий рекомендуемый подход, насколько я смог его понять из отдельных писем разработчиков GAE, состоит в том, что необходимо обрабатывать все теоретически возможные exception'ы во всех теоретически возможных местах их появления и в случае чего (сбой при чтении или записи в datastore, к примеру) повторять попытку.

Стоит сказать пару слов про SDK. SDK существует под Windows, MacOS, Linux и эмулирует datastore, доступные в GAE библиотеки, веб сервер. Кроме него надо еще поставить Python 2.5 и (уже по вкусу) - Eclipse с плагином PyDev (настройка всего этого дела - процесс не совсем тривиальный. Кто будет ставить, это прочтите). SDK, в принципе, достаточно близко повторяет функциональность GAE, но админка там другая (и ведёт себя иначе), а, скажем, функциональность связанная с TaskQueue вообще реализована лишь частично (созданные задачи надо запускать руками, нажимая кнопку Submit. Особенно радует, когда это надо сделать, скажем, 300 раз подряд :)

Всё это ограничивает круг разработчиков под GAE откровенными маньяками. Вывод я бы сделал такой: перспективно (и всё равно всё к этому придёт) но пока слишком сыро.

P.S. По-русски про GAE можно почитать здесь и здесь (многое уже устарело, но для начала полезно).



Опубликовано: Пётр Соболев

Случайная заметка

7304 дня назад, 00:0019 октября 1998 (Андрей Винокуров, 19/Окт/98) < К оглавлению Человек - существо социальное, вот уже много тысячелетий он живет в обществе себе подобных. И вполне естественно, что одной из важнейших его способностей является умение общаться с другими людьми - передавать им сведения о том, что происходит в окружающем их ...далее

Избранное

531 день назад, 01:575 мая 2017 Часть 1: От четырёх до восьми Я люблю читать воспоминания людей, заставших первые шаги вычислительной техники в их стране. В них всегда есть какая-то романтика, причём какого она рода — сильно зависит от того, с каких компьютеров люди начали. Обычно это определяется обстоятельствами — местом работы, учёбы, а иногда и вовсе — ...далее

43 дня назад, 20:305 сентября 2018 "Finally, we come to the instruction we've all been waiting for – SEX!" / из статьи про микропроцессор CDP1802 / В начале 1970-х в США были весьма популярны простые электронные игры типа Pong (в СССР их аналоги появились в продаже через 5-10 лет). Как правило, такие игры не имели микропроцессора и памяти в современном понимании этих слов, а строились на жёсткой ...далее