Вы слышали про NoSQL? Модный такой тренд, всякие новые хранилища данных в противопоставление старым добрым надежным и, что немаловажно - тормозным реляционным БД. Я пробовал одну - MongoDB. Клевая штука в общем-то. И функциональность немаленькая, и летает быстро, куда быстрее чем многие базы на реляционках. И схемы делать не надо.

Но за все надо платить. Чем заплатили в Монге? Размером. Ну сами подумайте - если к каждой записи-документу хранить всю его структуру, типы, названия ключей текстом… Толстые записи получаются, а? А уж индексы… Вот в индексах и проблема. Индексы - это такая штука, в которой очень важен рандомный доступ, но диски-то в наших с вами компьютерах вращаются, и чтобы прочитать-записать какой-то блок данных, надо до него сначала повернуться, а это ох как не быстро. Да, я в курсе про SSD, но они во-первых дорогие, а во-вторых подозреваются на быстрый износ. Кеширование данных в оперативке средствами операционки конечно спасает, но когда эти самые индексы разрастаются в раз этак шесть больше объема доступной RAM, дискового доступа на каждую операцию становится не избежать.

Вот есть у меня один проект, логи хранит. Причем по этим логам надо искать как по содержимому, так и по источнику записей. Без индексов никуда.

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

Вернемся к определению задачи: хранилище логов с поиском по содержимому и источнику, а так же фильтрация по временному промежутку. Содержимое может повторяться по тысяче раз, а может быть совершенно уникальным. Источников несколько тысяч. Событий порядка сорока миллионов. Размер сырых логов (при получении они пишутся в обычные файлы дозаписью, и только по крону закидываются в БД) в чуть-чуть сжатом виде за полгода не превышает гигабайта. На сервере два гига оперативки.

В первой реинкарнации была построена очень нормализованная БД на mysql - отдельно справочник источников, отдельно справочники составляющих частей лог-записи, составной справочник готовых записей, и таблица самих событий навроде id_источника, id_записи, время. Когда эта схема себя изжила - записей было в районе пяти-десяти миллионов. Утыкаться она стала в процессор: сначала надо было найти все компоненты из справочников, потом сджойнить для получения id записи, и только потом записать событие. Записывалось в эту базу со скоростью где-то три-пять событий в секунду.

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

Стал я думать думу великую и додумался: вернуться можно на старые добрые реляционки. Но! Реляционная база совершенно не означает, что её пользовать нужно только реляционно. Я взял за основу прошлую схему и несколько денормализовал её: а именно не собирал теперь джойном из кучи словарей данные для определения id записи, а просто хранил изначально-текстовое представление этой записи вдобавок к нормальному представлению для быстрого поиска. Вдобавок перешел с мускуля на постгрес, а в последнем есть такая замечательная вещь как поля, содержащие массив примитивов - строк или чисел. Эта штука позволила мне избавится от одного отношения многие-ко-многим с толстой промежуточной таблицей, которую надо было джойнить, но при этом не потеряв возможность запросить все записи, в которых присутствует конкретные элементы. А заодно заново переписал конвертер из сырых данных в базу, заставив его делать множество инсертов одной большой операцией. В итоге у меня постоянная скорость вставок на уровне нескольких тысяч событий в секунду, и занимаемое место вместе с индексами в раз пять меньше монговского, а значит и чтений будет куда меньше. Хотя все равно утыкается в процессор. ;-) Но для продакшна еще рано, надо попробовать еще несколько оптимизаций, и разобраться с профилированием SQL-запросов в постгресе, чтобы найти и устранить текущий затык.

Пытливые личности могут докопаться: что за логи-то такие?

Ну представьте себе обычный access-лог обычного веб-сервера:

1.2.3.4 - 1 Apr 2012 100500 200 GET /foo/bar.php?a=bla&c=baz

Мне вот надо выбирать и по ip, и по урлу, и по get-параметрам (всем вместе, комбинацией или по отдельности), и по размеру. Grep это круто, но чтобы последовательно прогрепать полгода логов - несколько часов ждать надо.