пятница, 3 июля 2009 г.

Google App Engine Datastore

Это пост-памятка — попытка собрать в одном месте факты о хранилище данных, которое предоставляет Google для разработчиков GAE.

Они, конечно везде кричат что это, мол, bigtable, но конкретной технической информации на самом деле кот наплакал, в основе всего лежит две презентации.

Итак, что мы знаем о лисе:

  1. Разработчик GAE получает кусок дискового пространства для хранения данных в Datastore, которое называется the entities table.
  2. Эта, гм, “таблица” условно делится на две “колонки”: ключ (entity key) и сущность (serialized entity). Т.е. это и близко не реляционное хранилище, это скорее persistence storage.
  3. Формат данных внутри этой таблицы называется protocol buffers. Он же используется в низкоуровневом API.
  4. Ключ имеет определенную структуру. Полный ключ каждой сущности имеет название (kind), значение и может ссылаться на родительскую сущность (parent/ancestor entity). Так что если мы храним иерархию из объектов, то ключи могут выглядеть так:
    /Client:Bob
    /Client:Bob/Contract:1
    /Client:Bob/Contract:2
    /Client:Alice/
    /Client:Alice/Contract:1
  5. Данные в the entities table не лежат просто так. Их физическая укладка отсортирована по ключу: сущность без родителя (root entity), её дочерний объект (child), другая сущность без родителя и т.д. Это дает возможность сгруппировать обрабатываемые данные рядом и сильно экономить время чтения.
  6. Всё что относится к одной корневой сущности называется entity group. В нашем примере их две: Bob и Alice.
  7. Запись с хранилищем осуществляется только в транзакции (txn), зато можно читать без транзакций. И что ещё интереснее, обычные транзакции (local txns)осуществляются именно в пределах entity group, а если нужно обновить сущности в нескольких entity group, то это уже издевательски именуется гугловцами как distributed transaction. О, честь им и хвала!
  8. Транзакции двухфазные. После первой фазы обновлены все сущности, а после второй обновлены все индексы. Причем блокировок по сути нет - после первой фазы новые данные становятся доступны для чтения. Что дает разработчику уникальную возможность запросить одни данные, а прочитать совсем другие!
  9. В случае физических сбоев самого хранилища транзакции накатываются (rolled forvard)! Очень странное решение. Очень. Остается надеяться что этого на практике не бывает.
  10. Локальная транзакция вообще работает на журнальном принципе: каждая сущность хранит дату своей записи, а root entity хранит дату последнего обновления всей entity group. Обновление сущности просто делает копию этой сущности с измененными параметрами и новым временем записи, а первая фаза транзакции обновляет время в root entity. При перестроении индекса (вторая фаза) просто игнорируются сущности со старым временем.

По хранилищу автоматически и вручную строятся индексы:

  1. Само собой есть индекс по полному ключу. И везде подчеркивается что такой доступ самый быстрый.
  2. Есть индекс по kind. Именно на него опирается эмуляция запросов вида select * from Client.
  3. Есть индексы по одному свойству сущности (single-property index). Такие индексы исходно двунаправлены и могут обрабатывать операции сравнения: больше, меньше и равно. На этих же индексах делается сортировка order by. Такие индексы автоматически строятся для простых типов данных строк и чисел.
  4. А вот составные индексы (composite index) нужно определять самостоятельно. Такие индексы могут включать несколько свойств и ссылку на предка (ancestor).
  5. Движок умеет делать слияние (merge join). Т.е. может использовать несколько single-property index и пересечь результаты отбора.

Проблемы хранилища:

  1. Бывают таймауты. Причины непонятны, отрапортовано давно, толку ноль. Т.е. приложение должно уметь обработать таймаут в любой момент.
  2. Иногда бывает техническое обслуживание: доступ только для чтения, отключение доступа. Т.е. приложение должно уметь обработать ограничения доступа в любой момент.
  3. И самое главное — непонятно как это обрабатывать на Яве, т.к. нужные исключения существуют только для питона. Т.е. API для java в принципе не содержит средств для обработки вышеописанных ситуаций. Update: в последних SDK нужные исключения появились.
  4. Update: Блокировки. При записи даже одного объекта блокируется запись во весь entity group. Так что entity groups должны быть маленькие. И желательно не иметь одних и тех же постоянно обновляемых мест. Именно поэтому разработчики упирают на технику называемую sharded counters.

Разные замечания:

  1. Update: О скорости: на практике Native Datastore API на минимум порядок быстрее JDO/JPA. 46ms на одну запись одного объекта в хранилище.
  2. Долгая инициализация хранилища: у меня первый запрос занимает 2 секунды.
  3. Есть ряд приколов в работе с индексами. Например попадание в диапазон.