Но как же без него? Если нет привычной базы данных SQL, то что? Вот, что я тебе скажу. Первым кинь камень в того, кто скажет, что в большом проекте без SQL не обойтись. Обойтись можно. И при этом некисло выиграть!
Скажи честно, тебе ведь интересно, как устроены изнутри те суперпроекты, на которых висишь сутками ты и еще сотни миллионов пользователей сразу? Google, Amazon, eBay, Twitter, тот же Facebook или наш ВКонтакте? Они совсем не похожи на большинство обычных веб-сайтов, написанных на PHP+mySQL. База данных в них — все. Там и новости, и информация о товарах в интернет-магазине, и статьи с комментариями в блоге, и самое вкусное — логины и пароли.
Очень многие разработчики и архитекторы также думали, что без базы не обойтись, продолжая создавать все более мощные сайты. Но вскоре столкнулись с тем, что сколько ни тужься, какие только хаки и умные штуки ни придумывай — при нагрузке в сто миллионов пользователей, базы данных все равно мрут, как мухи. Ребята тоже слышали о кластерах, и о распределенных системах и даже об облачных вычислениях (подробнее читай статью «Заоблачные вычисления» в #125 номере зс). Если надо, чтобы больше людей скачали новый порно-ролик с Берковой, достаточно поставить еще пару серверов и скопировать на них файлы. А вот базы данных так просто не работают. Вот тут-то и нарисовалась проблема масштабирования. Каждый решает ее по-своему. Сначала ставят второй сервер: с него приложение читает данные, записывая только на первый, а он уже сам, в фоновом режиме, переносит новые данные. Такая архитектура называется master-slave, но ничего связанного с BDSM здесь нет!
Позже можно доставить еще сервер, и еще, но это уже не поможет, если писать надо много и постоянно. Ведь каналы между серверами рано или поздно будут забиты так, что новые данные будут появляться на подчиненных узлах гораздо позже, чем это допустимо. А кому интересно ждать, пока его комментарий появится на странице (самые нетерпеливые тупо жмут рефреш, чем еще сильнее нагружают систему)? Светлые умы подумали и решили: а что, если все базы данных будут сразу главными? У тебя есть три сервера, и на каждом из них — вся информация, а приложение случайно или по определенному алгоритму выбирает, с каким сервером работать. Изменения на одном сервере сразу передаются на два других (это называется master-master или multi-master репликация), и в любой момент везде есть самые последние данные, при этом писать и читать информацию можно с любого сервера. Но тут одна сложность — у самых популярных баз эта функция появилась только недавно, да и в настройке и поддержке очень уж сложная. И не дай бог, придется восстанавливать данные или потерянные транзакции — тут вообще без пива не разберешься. Ну и, конечно, до бесконечности наращивать количество серверов также нельзя. Все будет неплохо, пока не дойдешь до десятка. А там не оберешься проблем с взаимной связью и трафиком внутри такого хозяйства. А результат тот же — медленно и ненадежно.
А тем временем сайты растут, пользователей становится все больше, счет идет уже на десятки миллионов. Что же делать? А вот что — отказаться от обычной базы данных! Ведь что такое база данных? Это специальное хранилище данных (обычно это просто файлы, но с собственной структурой и кешем в памяти) с движком, который принимает от тебя команды в виде языка 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 тысяч операций в секунду, и такой гигант, как соцсеть Linkedln не прогадал, используя кластер из этих серверов для своей работы. Apache CouchDB (couchdb.apache.org) — это уже тяжелое оружие из будущего! Шутка, CouchDB это представитель отдельного семейства баз данных, называемых документно-ориентированными. В этой штуке хранят документы, представляющие собой некоторую группу данных, которые вместе составляют один объект-документ. Например, статья (текст), краткая аннотация, имя автора, дата публикации и статус. По отдельности, это просто значения, а вот документная база позволяет их сгруппировать как один объект и производить над ним операции. Apache CouchDB написана на Erlang (просто замечательная платформа, если речь идет о расширяемости) и имеет HTTP REST-интерфейс или JSON API. так что можно получать данные сразу напрямую из JavaScripta-a на веб-странице! Кстати, она имеет встроенный язык запросов, и какой ты думаешь? Да, JavaScript вместо традиционного SQL.
Справедливости ради стоит сказать, что о промышленном применении базы пока не слышно. Уж сильно экспериментальная разработка, хотя и чрезвычайно перспективная. Redis (code.google.com/p/redis) — проект молодой и достаточно простой, но по возможностям мощнее всех предыдущих вместе взятых! Почему? Да взять хотя бы производительность. Более 100 запросов в секунду на простеньком сервере или мощном лаптопе. Знакомься, Redis или, как он сам себя называет, сервер структурированных данных. Проект позволяет хранить не только обычные ключи и значения, но и списки, наборы данных (группы пар ключ-значение), а также производить всего одной командой (и с гарантированным временем выполнения!) сложнейшие операции над такими списками. Там, где для memcached надо писать вручную две, три или десяток команд и еще вычислять что-то в самом приложении, при использовании Redis-a можно обойтись одной! Поддерживается даже сортировка, что является самой сложной и практически не выполнимой командой для всех key-value баз (в отличие от SQL, где это самая тривиальная операция). Написанный на ANSI С сервер умещается в паре десятков Кб исходных текстов (по лицензии BSD), работает на любой системе и сотворит чудеса с твоими данными. Команды посылаются no TCP или напрямую через telnet. Помимо этого, есть и API или модули на любой вкус и язык. Не буду скрывать, что сам являюсь автором класса-интерфейса для РНР. расширяющего возможности сервера еще сильнее!
Понимаю, что все, что я выше с таким трудом рассказал, это фигня, и хочется сразу почувствовать мощь новой технологии (ладно, не сильно новой, но все равно интересной). Давай попробуем ее в действии. Известно, что у самого быстрорастущего сервиса в мире [Twitter) долгое время
были проблемы с производительностью. Писать об этом проекте нам уже надоело, поэтому предлагаю забабахать собственную альтернативу. С использованием обычной БД — это вполне тривиальная задача (если не брать в расчет вопрос масштабируемости). Но мы реализуем тот же функционал без привычной БД — используя только сервер Redis. С кодом сложностей не должно возникнуть, HTML-странички ты сверстаешь сам, а вот как использовать такую необычную базу внутри, я тебе сейчас расскажу. ШАГ 0. Определимся, что мы делаем. Наш простой твиттер должен уметь хранить акка-унты пользователей (и пускать тех, кто знает пароль), хранить твои записи и выводить их, позволять добавлять и удалять друзей (фол-ловеров) и показывать их список, а также отображать полную ленту сообщений (как твоих, так и всех твоих друзей).
ШАГ 1. Аккаунты будем хранить в виде отдельных пар ключ-значение, где ключом будет логин пользователя, а значением — сериализированный массив(язык не имеет значения, например, РНР), в котором уже все о юзере, его имя, пол, дата регистрации и остальные данные. Вместо сериализации лучше использовать JS0N — тогда мы вообще не будем зависеть от языка приложения, ведь JS0N умеет обрабатывать любой современный язык программирования. Команда 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_1123214-2135 «{аи1Ьог:^гпт*,1ех1:’моя супер статья!’ .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-a очень высокая, но этот момент может нуждаться в оптимизации. Например, есть команда KEYS, которая ищет по паттерну все ключи и возвращает сразу список. Поэтому можно попробовать задать ей такое выражение, чтобы сразу получить все ключи сообщений (ведь они формируются через логин и метку времени, значит можно отфильтровать). Но это уже тебе как домашнее задание (на самом деле задача имеет несколько решений и не факт, что каждое из них самое лучшее).
Мы пока сделаем по старинке, получив список сообщений для каждого юзера. программно сформируем из него список заранее подготовленных ключей для извлечения сообщений. Так как все сообщения идут по времени, достаточно полученный массив преобразовать из JSON-a в родной для твоего языка программирования и отбросить все значения, меньшие за текущее время минус 3600 (мы ведь за последний час выбираем). Если брать не за час, а просто последние 100, то задача еще более упростится.
Далее простым циклом формируем ассоциативный массив из комбинации логин + метка времени, где ключом будет метка времени (число, для обеспечения правильной сортировки), а значение — строка вида loginjime (то есть так, как хранится у нас в Redis-e), а потом просто объединяем эти массивы. Язык сам позаботится о правильной последовательности, например, РНРтак и сделает, используя команду аггау_тегде и. если надо array_sort.
Эту часть нам пришлось вынести из базы и обработать в приложении, хотя при обычной архитектуре эта нагрузка легла бы на SQL-движок. Но это расплата за масштабируемость, поэтому не стоит переживать за нагрузку на сервере.
Последний штрих — сформируем команду к Redis-y на извлечение всех сообщений, ключи которых мы уже подготовили. У сервера есть волшебная команда, которой так не хватает другим популярным системам вроде memcached (там пытаются приспособить для этого теги) — MGET список^ключей, то есть одной командой получаем все ключи, имена которых передали. Остается только превратить наш массив в строку, разделителем служит символ пробела — и мы сразу получим массив JSON-строк с сообщениями. Его сразу можно передать на веб-страницу, с JSON умеет работать любой AJAX-фреймворк. Насчет производительности не стоит переживать — операция декодирования JS0N в родной для языка массив везде очень и очень быстрая, даже если речь идет о сотнях или тысячах преобразований. Аналогично можно отобразить список всех фолловеров — ведь мы храним их в списке admin_follow. в котором хранятся логины, а значит, используя потом MGET-команду, мы сразу достанем профайлы всех юзеров, за которыми следит админ.
Я ничего не сказал об удалении данных — вдруг Вася окажется занудным типом или спаммером и ты захочешь отписаться от него. Для этого надо просто удалить из списка admin_follow его логин, что
Сейчас реляционные базы данных (SQL СУБД) уже не правят миром, особенно если речь идет о высоконагруженных проектах или сайтах, где надо без задержки обслуживать клиентов. Если раньше все проблемы пытались решить кешированием, то сегодня мы видим, как гиганты индустрии просто уперлись в ограничения баз данных и в поисках выхода попробовали посмотреть на традиционные кеши с другой стороны. И получилось! Добавив чуточку смекалки и пару новых команд, теперь можно делать почти все, что раньше требовало сложных SQL-запросов, используя всего пять-шесть команд. При этом неважно, один сервер, десять или тысяча, мы вообще никак не ограничены в масштабировании! Конечно, не стоит сразу бросать любимый мускул и переписывать под Redis или MemcachedDB, но если ты готовишь сайт, где надо что-то делать быстро, очень быстро, как можно быстрее (ну типа чата, твиттера или онлайн-игры, а то и биржевой системы) — попробуй посмотреть на мир key-value баз данных! Может, это то, что надо! А SQL-базам оставим нудные дела вроде построения аналитики и анализа данных.
Amazon, Apache, BD, BDSM, DataBase, eBay, Facebook, Google, JavaAPI, JavaScript, key-value, Master, Memcached, MemcacheDB, MSSQL, MySQL, Project Voldemort, redis, Slave, SQL, Thrift, Twitter, vKontakte, База данных, СУБД, Язык