4 July 2011

16 практических советов по работе с CouchDB

NoSQL
Где-то год назад при разработке нашего проекта мы дошли до некой точки развития, когда или начинается кропотливая настройка и оптимизация MySQL-сервера, или начинается опять же кропотливое изучение запросов, которые идут в БД. Так получилось, что именно тогда был бум статей про MongoDB, CouchDB и прочие NoSQL базы данных и соблазн попробовать их на живом проекте был крайне велик.

При выборе главную роль сыграла фраза «CouchDB предназначен именно для веба», а также то, что для доступа не требовались никакие прослойки — доступ осуществляется по любимому мной REST, а API выглядит очень простым и изящным. Вдобавок к этому CouchDB имеет крайне удобный веб-интерфейс для администрирования Futon, чего на тот момент не было у MongoDB, а также железную устойчивость к падениям.

Забегая вперед скажу, что выбор полностью себя оправдал — мы избавились от огромного количества проблем при разработке и проектировании БД, код проекта сильно упростился и стал гораздо лучше структурирован, но самое главное — тот самый поворот в сознании, который нам дал CouchDB. За это время я лично набил множество шишек при разработке и хотел бы поделиться опытом с Хабрасообществом. Эти советы не для начинающих — это советы по использованию CouchDB на живом production.


Используйте больше баз данных


Во многих пособиях для начинающих (в том числе CouchDB: The Definitive Guide) примеры выглядят очень красиво, но совершенно не сочетаются с жизнью. Суть в том, что как только количество ваших документов перерастает сколько-нибудь реальные масштабы, скажем 100000 документов в БД, разработка temporary views становится практически нереальной, поскольку серверу теперь уже нужно прошерстить все ваши документы на предмет соответствия map-функции. Плюс к этому, каждый map будет содержать что-то в таком роде:
function(doc) {
if (doc.type == 'photo') {
...
}
}
что напоминает небольшой велосипед.

Логика CouchDB такова, что при обновлении одного документа в БД это обновление «затрагивает» все выборки. То есть абсолютно все выборки обновят свои ETag при обновлении всего лишь одного документа. Это еще один минус использования множества документов с различными полями type в одной БД. В то же время обновление одного документа не затрагивает ETag, который будут отдавать другие документы этой БД, поскольку ETag'ами для документов служат их последние ревизии.

Репликация должна происходить в одной локальной сети


Именно репликация считается одним из конкурентных преимуществ CouchDB. Она запускается POST-запросом и может работает в фоне. На живом сервере выснилось, что процесс репликации успешно проходит только в локальной сети. Как только ваши сервера начинают находиться далеко друг от друга, то начинаются совершенно неотлавливаемые глюки, как например: обрыв соединения, невозможность получения изменений и прочее-прочее. При всем этом реплика может выдать сообщение в лог и спокойно делать вид, что все хорошо. Потому совет: реплицируйте данные только в одной локальной сети.

Используйте нативные reduce-функции на Erlang


Не изобретайте велосипедов. В документации в качестве примеров reduce-функций часто используются такие вещи:
function (key, values, rereduce) {
return sum(values);
}
Старайтесь избегать их и используйте нативные reduce, написанные на Erlang: "_count" и "_sum", которые к тому же работают горадо быстрее своих Javascript-аналогов.

Трижды подумайте перед тем как использовать сложные reduce-функции


Этот момент не освещен в документации, однако в ней говорится о том, что если вы не используете reduce-функции, то вполне возможно, вы очень много теряете. Reduce может вызывать саму себя при слишком большой выборке, порождая rereduce. Но в жизни все это теряет смысл как только ваша выборка становится чуть более сложной.

В нашем проекте мы имеем базу comments, в которой храним комментарии. Каждый комментарий находится в отдельном документе, также в этом документе хранится город комментария (у нас российский портал, несколько городов), а также его т.н. принадлежность — поле belongs. Задача заключается в том, чтобы вывести N последних обсуждений. Если говорить языком MySQL, задача сводится к чему-то в таком духе:
SELECT * FROM comments GROUP BY (belongs, city) ORDER BY timestamp
Основная проблема выборки в CouchDB заключается в том, что она сортируется по ключу, а вперед нам необходимо выводить самые новые ветки обсуждений. Значит, группировку через group / group_level мы применить уже не сможем. Вот в этом месте мы и обратились к (re)reduce. Функция усечения выборки в конце выглядела так:
function(key, values, rereduce) {
if (rereduce) {
var data = [], meta = [], record, tmp, index, total = [];

for (var i in values) {
for (j=0; j<values[i].length; j++) {
record = values[i][j];

tmp = record[2] + '_' + record[3];
index = meta.indexOf(tmp);

if (index === -1) {
meta.push(tmp);
data.push(record);
} else {
data[index][1] = Math.max(data[index][1], record[1]);
}
}
}

data.sort(function(a, b) {
if (a[1] === b[1]) {
return 0;
}

return (a[1] > b[1])
? -1
: 1;
});

return data.slice(0, 7);
} else {
var output = [];
for (var i in values) {
output.push([values[i]._id, values[i].ts, values[i].belongs, values[i].city]);
}

return output;
}
}
И все работало хорошо, но возникла проблема скорости обновления выборки. После занесения одного комментария, обновление этой выборки занимало 2 секунды на сервере с 4Гб памяти и процессором Athlon 64 X2 5600+ (ссылка). А при постоянном потоке комментариев постоянное провисание БД было недопустимо. Сейчас количество документов в БД — 22,000, в выборке — 258,000. Отсюда вывод: используйте мощные reduce-функции только при мощном сервере. В противном случае вся идея становится бессмысленной.

Кэшируйте данные через ETag


Получение данных через связку «If-Modified-Since / ETag» действительно быстрее простого получения данных примерно в 3 раза (синтетические тесты). Не забывайте о том, что когда вы используете заголовки If-None-Match, при статусе ответа 304 тело ответа всегда пустое, поскольку сервер подразумевает, что вы храните данные на своей в стороне. В нашем проекте для этих целей мы используем Memcached с небольшой простой оболочкой для работы с CouchDB (ссылка)

Думайте в стиле CouchDB


Thinking in CouchDB — отдельная, требующая вникания вещь. Мало написать парсер из %СУБД% в CouchDB, важно действительно привыкнуть мыслить в стиле CouchDB, и тогда все задачи будут восприниматься вами с совершенно иной точки зрения.

Приведу простой пример. Есть события, которые проходят в какой-то промежуток времени. Если нам надо узнать в MySQL, какие события проходят именно сегодня, то мы пишем такой запрос:
SELECT * FROM table_name WHERE UNIX_TIMESTAMP() BETWEEN start_timestamp AND finish_timestamp
А теперь вернумся к CouchDB и вспомним о том, что в выборках нет такого понятия как текущее время. Все выборки формируются 1 раз, а в дальнейшем при изменении/создании/удалении документов в них они только обновляются. Соответственно мы имеем только документы и ничего больше. Важно понять, что вы должны составить выборку так, чтобы можно было передать в качестве ключа к ней какой-либо параметр. То есть вы можете только ограничить выборку по ключу. Решением этой задачи является составление выборки, в которой для каждого документа в выборку в качестве ключа попадут все дни, в которые проходит событие. А в дальнейшем чтобы получить все события, проходящие в этот день, вам будет достаточно обратиться к виду с ключом "?key=текущий_день"

Почти все, что вы делаете на SQL, реализовывается на CouchDB гораздо проще и красивее


Я лишь разбавлю этот псевдожелтый заголовок своим наблюдением 3-летней давности. В свое время мне довелось работать младшим программистом в фирме, штампующей сателлиты. На этих сайтах посещаемость была не больше 30 человек в день, но под каждым из них стоял мощный движок с преобразователем XSL-шаблонов на серверной стороне. Я даже не хочу объяснять, почему это глупо. Общая идея — вы всегда должны выбирать именно то средство, которое наиболее хорошо подходит к решению проблем. В случае саттелитов это простые html-страницы, которые может генерировать ваша CMS; в случае мощных порталов с большой посещаемостью это ни в коем случае не может быть бесплатная Joomla.

Вернемся к заголовку. Не все программисты понимают как работает их код, а особенно как происходит взаимодействие с БД. Зачастую встречаются запросы, в которых есть огромное количество JOIN'ов для выборки простых данных, и даже EXPLAIN не поможет этому человеку определить, какая часть его запроса тормозит, поскольку весь запрос составлен без применения головы. Более того, на живом проекте все сводится к простым выборкам по PRIMARY KEY, все остальные запросы становятся обузой, а знания по составлению сложных SQL-запросов становятся бесполезными.

В настоящий момент я глубоко убежден, что CouchDB позволяет начинающим программистам включать голову и не городить мощнейшие запросы только ради того, чтобы они работали. Удобство reduce-функций позволяет не писать глупые усечения данных, выбрасывая memory overflow. Почти все вещи, которые используются при работе простых сайтов с посещаемостью до 5,000 человек в день, гораздо красивей и проще реализовываются на CouchDB: получение страниц по URL, получение списка новостей, работа с гостевой книгой, фотогалереи и прочее. В то же время единственно возможная используемая кодировка UTF-8 избавит вас от множества вещей, о которых при разработке думать не нужно.

Используйте утилиты для просмотра текущих действий


Все текущие действия в CouchDB можно просмотреть. В MacOS утилита для работы с CouchDB называется CouchDBX. Похожая утилита есть и для Windows. Они запускают CouchDB-сервер на порту 5984 и позволяют смотреть текущие запросы к серверу в реальном времени. В Linux достаточно запустить сервер не в режиме демона (за это отвечает параметр -d в /usr/bin/couchdb) и все запросы будут выводиться в консоль.

Также все текущие действия можно смотреть во вкладке «Status» в «Futon».

Не используйте CouchDB для часто обновляемых данных


У каждой вещи есть свои наиболее лучшие методы применения. У CouchDB к этим сторонам точно не относится работа с часто обновляемыми данными. Выборка же данных в CouchDB — это идеал. Почему так происходит? Когда обновляется один документ в БД, то ETag сбрасываются для всех выборок в БД. Это означает, что все они становятся невалидными, устаревшими. Для выборок это означает обновление и обновление их ETag при следующем вызове (т.е. min +1 запрос для всех выборок в БД). На уровне сервера это означает разрастание БД в размерах, с чем придется бороться с помощью операции Compaction.

Не забывайте про Compaction


Каждое обновление документа ведет к созданию его более новой ревизии. Также это ведет к регенерации выборок, в которых участвует этот документ (на регенерацию также влияют операции добавления и удаления документов) при их следующем вызове. Все старые ревизии сохраняются, и далеко не всегда вам необходимо иметь доступ к 600 ревизии документа, в то время как его нынешняя — тысячная. Размер БД растет, а место на сервере не всегда резиновое, поэтому не забывайте выполнять операцию compaction для видов и документов. Это сэкономит массу свободного места на дисках.

Регенерация видов. Stale=update_after


До релиза CouchDB версии 1.10 небольшой проблемой была выборка данных из несгенерированных видов. Для этого предлагалось использовать параметр «stale=ok» при выборе вида, а саму регенерацию видов повесить, допустим, в crontab. Начиная с версии 1.10 появился параметр «stale=update_after», который действует так же, как и «stale=ok», однако вызывает обновление вида после его получения. Вместе с простым получением данных вида, мы имеем все возможности для быстрой работы с даже сложными design-документами.

Добавление или изменение вида на продакшн сервере влияет на соседние виды design-документа


При добавлении вида в design-документ происходит его сборка. Это значит, что все время пока будет собираться вид (скажем, _design/list/_view/by_name), соседние с ним виды (скажем, _design/list/_view/by_age) будут недоступны. Не забывайте про это, когда добавляете вид на production-сервере.

Устанавливайте из исходных кодов. Обновляйтесь чаще.


Как многие уже привыкли, мэйнтейнеры Ubuntu/Debian не торопятся обновлять пакеты в репозиториях. Это значит, что в Ubuntu Maverick CouchDB имеет версию 1.0.1, а в Lucid так вообще 0.10, в то время как CouchDB давно включен в список приоритетных проектов Apache и все время развивается. Последняя версия на данный момент (1.10) содержит следующие вещи:
  • Нативная поддержка SSL
  • Список баз данных наконец-то отсортирован по алфавиту
  • Более точная поддержка ETag для видов (в обсуждении говорят, что выборки не будут менять свои ETag при обновлении документов, не входящих в них);
  • Поддержка CommonJS модулей в map-функциях (вспоминаем про NodeJS)
  • Опция «stale=update_after», которая действует так же, как и «stale=ok», однако вызывает обновление вида после его получения

Полнотекстовый поиск


Я говорил о том, что CouchDB подходит для многих задач, но не всех. Полнотекстовый поиск как раз попадает под это исключение. Поскольку мы не можем передать какой-либо параметр прямиком в вид, мы не можем искать что-то точно в БД. Поэтому вы не сможете организовать поиск на сайте, используя CouchDB. Есть различные решения этих проблем, но все это — велосипеды. Скажу честно — это не всегда так плохо: зачастую это позволяет вам понять, что захотят искать ваши посетители. Есть еще один важный момент: на малопосещаемом сайте поиск почти не нужен. А на большом портале поиск должен быть достаточно релевантным, что не позволит вам обойтись простыми LIKE/LOCATE-запросами.

Простым решением данной проблемы является использование Поиска по сайту от Яндекса или Custom Search Engine от Google.

Более сложным и цельным решением этой проблемы может стать использование отдельного поискового сервера. Это может быть Sphinx, Apache Solr, Lucene (есть связка couchdb-lucene, упомянутая в документации). По сути это тема для отдельной статьи, поэтому сейчас заострять внимание на этом не буду.

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

Геопоиск


Еще одной проблемой в CouchDB является геопоиск, например нахождение всех объектов в радиусе N метров. В SQL-подобных БД данная задача реализуется с помощью небольшой функции, которая позволяет по широте и долготе определить расстояние между двумя точками. В CouchDB мы имеем только одну шкалу сортировки в ключах, поэтому найти все точки, попадающие в квадрат — почти невыполнимо. Однако автор CouchDB в твиттере упомянул, что вы можете реализовать геопоиск точно так же, как он реализован в MongoDB, а именно с использованием идеи Geohash. заключается она в том, что любые координаты могут быть представлены в виде численно-буквенного хэша. При этом чем точнее указаны координаты, тем больше будет длина хэша. Таким образом, вы можете передавать геохэш в качестве ключа и варьировать его длину в параметрах startkey/endkey для уточнения радиуса поиска (конечно, это не совсем радиус). Реализаций geohash существует великое множество, вы всегда можете ознакомиться с ними или же написать свою.

Бэкапы данных


Бэкапы данных — одна из вещей в CouchDB, за который его стоит любить. Бэкапы делаются простым копированием файлов БД из директории /var/lib/couchdb. Помните, что копировать файлы можно только при выключенном сервере CouchDB, иначе все ваши файлы БД будут побитыми. Таким образом, общий порядок действий таков:
  1. выключаем CouchDB-сервер
  2. копируем нужные нам БД
  3. включаем CouchDB-сервер
Репликация же осуществляется при работающем сервере. Файлы с расширением *.couch содержат все документы соответствующих баз данных. Директории .%database_name%_design содержат сгенерированные виды сооответствующих баз данных. Если вы не скопируете директории с видами, ничего страшного не будет: при первом запросе к видам, они будут сгенерированы на вашем компьютере.

Не забывайте о том, что все файлы БД и видов должны принадлежать соответствующему пользователю CouchDB, поэтому проверяйте права файлов при копировании и устанавливайте их при необходимости через утилиту chown.



Пост написан хабраюзером 1999 и опубликован по его просьбе.
Tags:CouchDBMySQLSphinxApache SolrLuceneUbuntuDebian
Hubs: NoSQL
+54
14.6k 134
Comments 45
Top of the last 24 hours