Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 128, АВГУСТ 2009 г.

Да пошел ты, SQL! Как отказать от SQL баз данных и выиграть

Александ Лозовюк (aleks.raiden@gmail.com)

Но как же без него? Если не привычный базы данных SQL, то что? Вот, что я тебе скажу. Первым кинь выиграть камень в того, кто скажет, что в большом проекте без, SQL не обойтись. Обойтись можно. И при этом - некисло выиграть!

Скажи честно, тебе ведь интересно, как устроены изнутри те суперпроекты, на которых висишь сутками ты и еще сотни миллионов пользователей сразу? Google, Amazon, eBay, Twitter тот же Facebook или наш Вконтакте? Они совсем не похожи на большинство обычных веб-сайтов, написанных на PHP+mySQL. База данных в них - все. Там и новости, и информация о товарах в интернет-магазине, и статьи с комментариями в блоге, и самое вкусное - логины и пароли.

База данных – вширь и ввысь

Очень многие разработчики и архитекторы также думали, что без базы не обойтись, продолжая создавать все более мощные сайты. Но вскоре столкнулись с тем, что сколько ни тужься, какие только хаки и умные штуки ни придумывай - но при нагрузке в сто миллионов пользователей, базы данных все равно мрут как мухи. Ребята тоже слышали о кластерах, и о распределенных системах, и даже об облачных вычислениях (подробнее читай статью "Заоблачные вычисления" в #125 номере ][). Если надо, чтобы больше людей скачало новый порно-ролик с Берковой, достаточно поставить еще пару серверов и скопировать на них файлы.

А вот базы данных так просто не работают. Вот тут-то и нарисовалась проблема масштабирования. Каждый решает ее по-своему.

Сначала ставят второй сервер: с него приложение только читает данные, записывая только на первый, а он уже сам, в фоновом режиме, переносит новые данные. Такая архитектура называется master-slave, но ничего связанного с BDSM здесь нет!

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

Светлые умы подумали и решили, а что, если все базы данных будут сразу главными? То есть, у тебя есть три сервера, и на каждом из них есть вся информация, а приложение случайно или определенному алгоритму выбирает, с каким сервером работать.

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

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

Скажи твердое "Нет" SQL-у!

А тем временем сайты растут, пользователей становится все больше, счет идет уже на десятки миллионов. Что же делать? А вот что - просто отказаться от обычной базы данных! Ведь что такое база данных? Это специальное хранилище данных (обычно, это просто файлы, но с собственной структурой и кешем в памяти) с движком, который принимает от тебя команды в виде языка SQL (например, на выборку данных) и выполняет их.
Особенно достают кривые руки разработчика или админа, когда для самого простого запроса «а сколько юзеров у меня на странице?» приходится тупо перебирать весь список пользователей и проверять, у кого статус он-лайн. Ведь юзеров может быть реально много, а если ты еще не озаботишься правильными индексами, то на каждый такой запрос придется серверу доставать всю табличку с данными (а это может и гигабайт быть) и считать снова и снова. А если в этот момент Вася скинул своим френдам в ЖЖ ссылку на твой суперпроект и пришла еще тысяча юзеров, каждого из которых надо записать в базу? Все - капут серверу!

Все потому, что и базы данных и язык SQL, которым эти данные выбираются, достаточно плохо приспособлен к масштабированию.
То есть, пока один-два сервера, все будет окей. Но как только больше - начинаются проблемы. Нельзя добавить еще машинку и гарантированно заставить работать все быстрее. В Гугле это давно поняли и изобрели свое решение, полностью отказавшись от применения таких обычных баз данных. Но это гугл со своими ноу-хау, а что делают остальные? Остальные используют key-value database!

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

Интерфейс доступа к такой базе также максимально прост – обычно это простейшие команды типа get (получить данные по ключу), set (записать данные с ключем), delete (удаляет ключ и его данные), update (обновляет уже существующие данные).

Самым главным преимуществом является то, что если правильно все сделать, сложность таких операций (то есть, время вычисления результата) будет заранее известным и не зависит от объема данных или количества серверов. Более того, операции обычно атомарные (в обычных SQL базах данных это называется транзакциями). Т.е. задавая команду, ты можешь быть уверенным, что она либо успешно отработает, либо сразу вернет тебе ошибку - при этом другие пользователи не помешают тебе, даже если будут пытаться сделать то же самое.

Это самый обычный тип key-value баз данных. Подобных проектов существует много, но отличаются они как правила типами данных, возможных для хранения. Например, кроме строк, можно хранить числа или двоичные объекты (BLOB-ы), а также количеством команд-операций. Понятно, что описанные выше четыре операции самые простые, обычно поддерживается еще инкременент/декремент (счетчик в памяти), особо продвинутые могут хранить массивы и списки.

На низком уровне такие базы строятся на базе хеш-таблиц и их разновидности – распределенной хеш-таблицы (DHT). Это просто обычная, хоть и большущая таблица, которая может автоматически распределяться на любое количество компьютеров и поддерживает поиск и получение данных баз знания, где они конкретно (такой принцип в частности используется для бессерверного обмена пирами во время скачки файла через torrent). И хотя обычно для быстрой работы данные хранятся в оперативной памяти, некоторые сервера обеспечивают хранения на диске и бекап, так что после выключения такого сервера все данные сохраняются.

Светлые и темные стороны Силы

Сильной стороной таких решений – масштабируемость и скорость. Свойства DHT такие, что можно присоединять новые сервера постоянно, и такая база будет расти и расти. Столько - сколько надо. При этом в самих приложениях ничего менять не надо, все делается автоматически! Скорость также очень и очень высокая, так как практически все такие базы работают обычно в памяти, а на диск пишется лишь бекап (при этом, он может быть постоянным - в таком случае в него записывается только новая инфа). Показатели в сотни тысяч запросов в секунду на одном дохленьком сервере – это обычное дело для таких баз.

Но несмотря на восторги, есть и сложности. Первая – это скудность возможностей работы с данными. Ага, вот и расплата за скорость и расширяемость. Сервер знает только ключ и данные, которые с ним ассоциированы, а вот, что это за информация - номер кредитной карточки или дата регистрации - уже не ведает. Этим должно заниматься само приложение! Поэтому просто взять и, например, написать один SQL запрос, чтобы выбрать всех пользователей, которые регистрировались год назад и совершили больше одного платежа за это время, уже не получится.

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

Увы, автоматически трансляторов SQL в key-value запросы пока нет, но работы в этом направлении ведутся. Еще одним недостатком таких баз является требовательность к параметрам сервера и в особенности оперативной памяти, которой, как известно, много не бывает. Прожорливость удается удовлетворить за счет хранения не используемых данных на диске. Подобным образом поступили разработчики MemcacheDB, где скрестили популярный сервер memcache и базу данных BerkleyDB, используемое как постоянное хранилище данных. В более молодом, но очень сильном проекте проекте - Redis - используется асинхронная запись в фоновом режиме на диск. Другие также не брезгуют использовать традиционные базы данных для хранения, ведь их совсем не видно за фасадом сервера и они работают локально, поэтому на скорость работы почти не влияют.

Сервера - подходи, выбирай!

Довольно теории! Давай посмотрим, какие есть базы и чем они отличаются.

Memcached/MemcacheDB (memcachedb.org) – наверное самый известный представитель семейства key-value DB. Многие используют его как кеширующую систему, что, по большому счету, то же самое. Проект хранит данные в оперативной памяти, занимает места столько, сколько ему выделили и может объединяться с другими серверами, чтобы распределить данные между собратьями.

Доступ к данным идет через UDP-порт и сокеты, что очень быстро, а с выходом последней версии, 1.4, добавлен и экономичный бинарный протокол. Хотя, в Facebook считают иначе и ускоряют, как могут, добиваясь нескольких сотен тысяч одновременных подключений! Кстати именно эта социальная сеть имеет самую большую инсталляцию Memcached-севреров – в архитектуре участвует более тысячи серваков! Недостаток мемкеша в том, что он, хранит все в памяти. По этой причине в местах, где необходима сохранность данных, придется использовать MemcacheDB, который использует обычную базу данных как постоянное хранилище данных. Другие недостатки – ограниченность на данные, которые понимает сервер (это только числа и строки), а также сложности выборки одним запросом множества ключей.

Project Voldemort (project-voldemort.com) – такой же мощный, как и Темный Лорд, только в царстве баз данных. Штука написана на Java и изначально нацелена на распределённость. Добавлять новые сервера можно без остановки - данные по ним "расползутся" без посторонней помощи. Кроме обычного сетевого доступа Project Voldemort, поддерживает JavaAPI и различные сетевые протоколы, например, Google ProtoBuf или Thrift, что сильно экономит трафик и повышает скорость. Данные хранятся как в памяти, так и на диске (можно использовать и обычные базы данных), так что сбои питания никак не нарушат целостности.

Сильной стороной является поддержка версионности, то есть каждая единица данных имеет историю версий и изменений, поэтому можно откатываться назад, если что-то записали не то или возникли ошибки. Быстродействие также на высоте: в среднем 10 – 20 тысяч операций в секунду, так что такой гигант, как соцсеть LinkedIn не прогадал, используя кластер из таких серверов для своей работы.

Apache CouchDB (couchdb.apache.org) - это уже тяжелое оружие из будущего! Шутка, CouchDB это представитель отдельного семейства баз данных, называемых документно-ориентированными. То есть, в этой штуке хранят документы, представляющий собой некоторую группу данных, которые вместе составляют один объект-документ. Например, статья (текст), краткая аннотация, имя автора, дата публикации и статус. По отдельности, это просто отдельные значения, а вот документная база позволяет их сгруппировать как один объект и производить над ним операции. Apache CouchDB написана на Erlang (просто замечательная платформа, если речь идет о расширяемости) и имеет HTTP REST-интерфейс или JSON API, так что можно получать данные сразу напрямую из JavaScripta-а на веб-странице! Кстати, она имеет встроенный язык запросов, какой ты думаешь? Да, JavaScript вместо традиционного SQL. Справедливости ради стоит сказать, что о промышленном применении базы пока не слышно. Уж сильно уж экспериментальная разработка, хотя и чрезвычайно перспективная.

Redis (code.google.com/p/redis) - проект хотя и молодой и в идее достаточно простой, но по возможностям мощнее всех предыдущих вместе взятых! Почему? Да взять хотя бы производительность. Более 100 запросов в секунду на простеньком сервере или мощном лаптопе. Знакомься, Redis или, как он сам себя называет, сервер структурированных данных. Проект позволяет хранить не только обычные ключи и значения, но и списки, наборы данных (то есть, группы пар ключ-значение), а также производить всего одной командой (и с гарантированным временем выполнения!) сложнейшие операции над такими списками.

Там, где для memcached надо писать вручную две, три или десяток команд и еще вычислять что-то в самом приложении, при использовании Redis-а можно обойтись одной! Поддерживается даже сортировка, что является самой сложной и практически не выполнимой командой для всех key-value баз (в отличии от SQL, где это самая тривиальная операция). Написанный на ANSI C, сервер умещается в паре десятков Кб исходных текстов (по лицензии BSD), работает на любой системе и сотворит с твоими данными чудеса. Команды посылаются по TCP или напрямую через telnet. Помимо есть и API или модули на любой вкус и язык. Не буду скрывать, что сам являюсь автором класса-интерфейса для PHP, расширяющего возможности сервера еще сильнее! :)

А давай замутим… свой Twitter?!

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

Шаг 0

Определимся, что мы делаем. Простой твиттер наш должен уметь хранить аккаунты пользователей (и пускать тех, кто знает пароль), уметь хранить твои записи и выводить их, позволять добавлять и удалять друзей (фолловеров) и показывать их список, а также отображать полную ленту сообщений (как твоих, так и всех твоих друзей).

Шаг 1

Аккаунты будем хранить в виде отдельных пар ключ-значение, где ключём будет логин пользователя, а значением – сериализированный массив (язык не имеет значения, например, РНР), в котором уже все о юзере, его имя, пол, дата регистрации и остальные данные. Вместо сериализации даже лучше использовать JSON – тогда мы вообще не будем зависеть от языка приложения, ведь JSON умеет обрабатывать любой современный язык программирования.

Команда SET admin “{name:’supervasya’,age:21,sex:’m’,registered:’27.07.2009’} ” – записывает нового юзера с логином admin. Теперь выполнив запрос GET admin мы получим JSON-строку с данными.

Для авторизации мы используем отдельное значение: SET admin_pass “md5(password)” – ключем здесь также логин, но с добавлением строки «_pass», а значение – md5 хеш пароля.

Авторизация будет в два шага (для надежности, но можно и в один). Сначала проверим, существует ли логин: EXISTS admin, если все ок (значит в базе есть такой юзер), извлекаем его хеш пароля для проверки: GET admin_pass. Саму проверку и сравнение хешей придется делать уже в приложении.

Не забудем счётчик всех юзеров (а то ведь SELECT COUNT() здесь нету): INCR count_user - увеличит счетчик юзеров на 1.
Если тебе захочется иметь весь список юзеров, придется раскошелиться на еще одну переменную, например, загнав все логины в набор (set): SADD all_user_list admin. Таким образом, в all_user_list у нас будет хранится список всех логинов, по которым можно извлечь профайл аккаунта.

Шаг 2

Теперь будем хранить все твои сообщения. Ключем в данном случае будет метка времени, потому что ключ должен быть уникальным, да и вряд ли ты будешь постить что-то чаще раза в секунду (нефиг спамить!). Можем просто создать ключ, используя логин и метку времени, например, admin_11232142135, и хранить его как отдельное значение вместе с сообщением: SET admin_11232142135 “{author:’admin’,text:’моя супер статья!’,time: 11232142135,title:’статья!’} ”

Но чтобы облегчить себе жизнь, мы сделаем еще список, где будут хранится данные о времени постов каждого автора. Вот так: RPUSH admin_msgs 11232142135. Команда добавит в конец списка admin_msgs новое значение – метку времени твоего поста. Зачем? Для облегчения потом получения всех постов за определенное время или просто указанного количества, например, для постраничного вывода. Внутри списка даты уже отсортированные по времени, поэтому дополнительной сортировки ненужно.

Шаг 3

Если ты хочешь зафолловить (читать) Васю, необходимо сохранить логин Васи в твоем списке фоловверов. Для этого также применим списки, создав для каждого юзера список фолловеров: RPUSH admin_follow vasja. В списке admin_follow теперь будут хранится логины всех юзеров, которых хочет читать admin. Аналогично, если Вася хочет читать, что же про него пишет админ: RPUSH vasja _follow admin

Шаг 4

Выводим полную ленту сообщений. Выше мы уже умеем хранить все сообщения одного пользователя и хранить список тех, за кем он следит. Теперь выводим ленту сообщений, в которой будут как собственные сообщения юзера, так и все сообщения тех, за кем он следит. При этом, все сообщения должны идти в хронологическом порядке. Допустим, мы будем показывать только сообщения за последний час.

Здесь уже немного сложнее. Сначала выберем список всех пользователей, которых надо показать. Для этого сначала получим количество наших фолловеров (длину списка): LLEN admin_follow. Допустим, мы получили 2 (админ отслеживает двух юзеров): LRANGE admin_follow 0 1 – получаем в виде массива логины юзеров. Не забываем, что надо прибавить сюда и свой логин, так как наши сообщения также должны быть видны. Это уже придется делать самому приложению.

Далее, имея список логинов, нам надо выбрать все списки сообщений каждого юзера. К сожалению, для этого надо N раз вызвать команду LRANGE, указав ей каждый раз другой список (комбинацию логин игрока + _msgs). Конечно, в этом нет особо страшного ничего, ведь скорость работы Redis-а очень высокая, но этот момент может нуждаться в оптимизации. Например, есть команда KEYS которая ищет по паттерну все ключи и возвращает сразу список. Поэтому можно попробовать задать ей такое выражение, чтобы сразу получить все ключи сообщений (ведь они формируются через логин и метку времени, значит можно отфильтровать). Но это уже тебе как домашнее задание (на самом деле эта задача имеет несколько решений и не факт, что каждое из них самое лучшее).

Мы пока сделаем по-старинке, получив список сообщений для каждого юзера, программно сформируем из него список заранее подготовленных ключей для извлечения сообщений. Так как все сообщения идут по времени, достаточно полученный массив преобразовать из JSON-а в родной для твоего языка программирования и отбросить все значения, меньшие за текущее время минус 3600 (мы ведь за последний час выбираем). Если брать не за час, а просто последние 100, то задача еще более упростится.

Далее простым циклом формируем ассоциативный массив из комбинации логин + метка времени, где ключом будет метка времени (число, для обеспечения правильной сортировки), а значение – строка вида login_time (то есть так, как хранится у нас в Redis-e), а потом просто объединяем эти массивы. Язык сам позаботится о правильной последовательности, например, РНР так и сделает, используя команду array_merge и, если надо array_sort.

Эту часть нам пришлось вынести из базы и обработать в приложении, хотя при обычной архитектуре эта нагрузка легла бы на SQL-движок. Но это расплата за масштабируемость, поэтому не стоит переживать за нагрузку на сервере.

Последний штрих – сформируем команду к Redis-у на извлечение всех сообщений, ключи которых мы уже подготовили. У сервера есть волшебная команда, которой так не хватает другим популярным системам вроде memcached (там пытаются приспособить для этого теги) – MGET список_ключей, то есть одной командой получаем все ключи, имена которых передали. Остается только превратить наш массив в строку, разделителем служит символ пробела и все, мы сразу получим массив JSON-строк с сообщениями. Его даже сразу можно передать на веб-страницу, ведь с JSON умеет работать любой AJAX-фреймворк. На счет производительности не стоит переживать – операция декодирования JSON в родной для языка массив везде очень и очень быстрая, даже если речь идет о сотнях или тысячах преобразованиях.

Аналогично можно отобразить список всех фолловеров – ведь мы храним их в списке admin_follow, в котором хранятся логины, а значит, используя потом MGET команду, мы сразу достанем профайлы всех юзеров, за которыми следит админ.

Я ничего не сказал об удалении данных – ведь вдруг Вася окажется занудным типом или спаммером и ты захочешь отписаться от него. Для этого надо просто удалить из списка admin_follow его логин, что делает команда LREM, которой стоит передать только логин и все, нет больше Васи.

Что в результате?

Сейчас реляционные базы данных (SQL СУБД) уже не правят миром, особенно если речь идет о высоконагруженных проектах или сайтах, где надо обслуживать клиентов без задержки. Если раньше все проблемы пытались решить кешированием, то сегодня мы видим, как гиганты индустрии просто уперлись в ограничения баз данных и в поисках выхода попробовали посмотреть на традиционные кеши с другой стороны. И получилось! Добавив чуточку смекалки и пару новых команд теперь можно делать почти все, что раньше требовало сложных SQL-запросов, используя всего пять-шесть команд. При этом не важно, один сервер, десять или тысяча, мы вообще никак не ограничены в масштабировании! Конечно, не стоит сразу бросать любимый мускул и переписывать под Redis или MemcachedDB, но если ты делаешь сайт, где надо что-то делать быстро, очень быстро, как можно быстрее (ну типа чата, твиттера или онлайн игры, а то и биржевой системы) – попробуй посмотреть на мир key-value баз данных!
Может это то, что надо! А SQL-базам оставим нудные дела вроде построения аналитики и анализа данных.

WWW

Содержание
ttfb: 10.64395904541 ms