2009-07-20

ZF + Горизонтальная масштабируемость

Написал адапnер для ZF предоставляющий функционал горизотального маштабирования данных бд. Основной зарачей горизотального маштабирования является возможно разнесения бд, путем разрезания таблиц с данными на части. Таким образом появляется возможность гибко маштабировать операции записи, выборки из больших таблиц содержащих свыше 10 млн. записей или размером свыше 5 Гб.

Примеры (можно также посмотреть тесты):
Создание и конфигурирование адаптера:



$db = new Core_Db_Adapter_Scale(array(
//список таблиц с которыми работает адаптер
"tables" => array(
"users" => array(
"scale"=> array(
// по какому полю ведется скайлинг
"field" => "id",
// какую стратегию использовать
"strategy" => "Core_Db_Adapter_Scale_Strategy",
),
"primary" => array(
// примари ключ
"field"=>"id",
// включить эмуляцию автоинкремента
"autogenerate" => true,
// стратегия генерации автонреметного ключа
"strategy" => "Core_Db_Adapter_Scale_Primary_Generate_Strategy"
)
),
),
// список шардов (соединений к БД в которых располагаются данные)
"shards" => array(
0 => Zend_Db::factory(
"Pdo_Mysql",
array(
"host" => "127.0.0.1",
"username" => "root",
"password" => "",
"dbname" => "test1",
)
),
1 => Zend_Db::factory(
"Pdo_Mysql",
array(
"host" => "127.0.0.1",
"username" => "root",
"password" => "",
"dbname" => "test2",
)
),
)));


Пример работы с стандартным ОРМ:

class Users extends Zend_Db_Table_Abstract {}

$Users = new Users($db);

$Users->insert(array("name"=>"joseph"));

$UsersRowset = $Users->find(1);
$User = $UsersRowset->current();
$User->name = "peter";
$User->save();

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

Бонусы:
  1. Возможность хранить в таблице неограниченое количество строк милиарды и более
  2. Возможность хранить в таблице неограниченое количесво данных Тб и более
  3. Возможность гибко размазывать нагрузку по всем шардам
  4. Возможность задать неграниченое количество частей разбиения
  5. Адаптер не зависит от БД, в качестве шардом может использоваться любая БД
  6. Возможность задания пользовательского алгоритма выбора шарда
  7. Эмуляция автоикрементного поля
  8. Интерфейс Zend_Db_Adapter

Ограничения:
  1. Во всех запросах требутся использовать поле по которому идет скайлинг.
  2. Не возможность выборки всех данных из таблицы
  3. Не возможность JOIN

Исходные коды: #svn/trunk/Vendor/Core/Db/Adapter

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

13 коментарів:

Макс сказав...

1. А как происходит "Эмуляция автоикрементного поля" ? Для твоего примера - id юзера уникален среди всех шардов ? (ZF-код слабо понимаю)

2. Ну и передавать объекты _всех_ шардов при создании Core_Db_Adapter_Scale - по-моему излишне :)

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

Comma сказав...

Спасибо, как раз вовремя =)

Necromant2005 сказав...

2Макс

Использую эту стратегию.
http://code.google.com/p/zrails/source/browse/trunk/Vendor/Core/Db/Adapter/Scale/Primary/Generate/Strategy.php
Сделано для более низкого старта.
Работает следующим образом:
1. генерится случаное число
2. ищется на шардах
3. если не нашлось возвращается
4. если нашлось - к шагу 1


Да реализация не очень красивая, но обеспечивает, то что требуется. И добратся до того момента когда эта реализация будет не оправдывать себя, еще нужно.

Necromant2005 сказав...

2Макс

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


>Вообще шардинг - очень специфическая штука, очень сильно зависит от приложения.
>И сложно сделать его универсальным и эффективным на уровне фреймворка. ИМХО.
Не совсем много приложений нуждаются в этом не не могут себе этого позволить. Вообще отлично шардятся любые простые запросы(без JOIN) главное только подумать предварительно как лучше разбивать на части.
Касательно сложности, не сказал бы. Да есть некоторые ограничения . которые нужно помнить, но они не так критичны по сравнению с тем что получаешь. А вообще тот же Hibernate horizontal scale-up (http://www.hivedb.org/)Дело просто в пониманиии, того как оно работает и реализации базовых инструментов. Инструменты обеспецивают низкий порог входа.

Макс сказав...

насчет псевдо-авто-инкремента.
С этой стратегии потом сложно слезть, потому что используемые id-шники размазаны по всему пространству int.
Например у нас для этих целей используется центральная база с таблицей с автоинкрементным полем (вобщем-то для этого можно было счетчик в memcachedb / redis использовать - у них inc атомарный)
Такая стратегия с центральынм счетчиком дает мне, знание что id от нуля до текущего значения была использована, а все что выше свободно.
И с такой стратегии на твою можно легко перепрыгнуть - в mt_rand($текущее_значение_счетчика+1, PHP_MAX_INT).
А вот с твоей стратегии на стратегию с центральынм счетчиком - не получится, потому что у тебя занятые id-ки размазаны случайным образом.
Вобщем-то ничего ужасного в ней нет, но ИМХО стратегия с отдельным сервером для автоинкремента более управляема.

> Касательно сложности, не сказал бы. Да есть некоторые ограничения . которые нужно помнить, но они не так критичны по сравнению с тем что получаешь.
Для реальной работы с шардами нужно написать немало системых утилит :
- которые бы умели создавать/удалять/альтерить таблицы на всех шардах
- умели считать нужную статистку по всем шардам
- умели переносить часть данных с одного шарда на другой (и очень желательно чтобы пользователи почти ничего не заметили)
Кроме того программист обязан следить за коннектами, которые открываются с веб-скриптов на шарды (более 2-х реальных коннектов к разным DB-серверам из веб-скрипта - имхо нонсенс).
А еще юзеры с разных DB-серверов могут как-то взаимодействовать на сайте и нужно писать на обе базы и обеспечить, чтобы обе транзакции прошли (ну здесь не столько шардинг, сколько
понимание что есть 2 транзакции которые должны пройти на разные сервера - но суть шардинга именно в понимании, что у нас много DB-серверов)

Я все это к тому, что чтобы шардинг заработал эффективно, он должен быть частью ядра системы, а не маленькой незаметной надстройкой над классом DB.

Necromant2005 сказав...

2Макс
А зачем вообще автоинрементное поле ?
Ты же не собирашься перебирать пользователей по id, или собирать какую-то статистику с этого автоинкремента. Он абсолютно - бесполезен. Основная задача получить уникальй UIN ключ для записи, вот и все. А как релизовать это можно например посмотреть доклад fisher-a на последнем highload.

Вообще с моей стретегии предполагатеся переходить к буквенно-цифровым ключам :) к примеру вот к такому sdfs-0909-sdf9-sdff-4353

Кастельно соединений к БД. Все зендовский конекторы поддреживают стратегию lazy-load. Тоесть непосредственно конект осущуетсяляется перед запросом. Если нет обращения к серверу - нет и соединения с сервером БД.

> что есть 2 транзакции которые должны пройти на разные сервера
Для этого может лучше использовать стандарные механизмы Replication. Хотя мне сложно судить о таком странном архитектурном решении.

>Для реальной работы с шардами нужно написать немало системых утилит
Никто и не спорит что когда-то это понадобится. Но все эти утилиты уже будт зависит непостредственно от архитектуры Вашего приложения. Следовательно реализовать их в какм бы то нибыло самом общем случае - просто глупо.

>Кроме того программист обязан следить за коннектами
Да, конечно. Но это зависит уже от того кто создает архитектуру. Продумать как сложить данные по шардам таким образом, чтоб в 99% случаев не приходилось бегать и искать данные по всем доступным шардам.

Макс сказав...

auto-increment имхо один из самых простых способ получить уникальные id-шки. Взяв новое значение автоинкремента я гарантировано (если правильно запрограммировать и настроить) получаю значение которое еще не использовалось в системе.
А в варианте с mt_rand() надо сгенерить уникальное число, проверить не занято ли оно, попытаться сохранить юзера с этим id. При этом теоретически 2 процесса могут получить одну id-шку, а значит если вставка не прошла из duplicate-entry именно для id, нужно перегенерить id-шку и снова попытаться вставить
Вобщем с авто-инкрементом код проще :)


А в чем преимущество буквенно-циферных ? больше диапазон и меньше вероятность попасть на занятую id-ку ? Все таки потеряшь немного на занимаемой памяти и производительности

Necromant2005 сказав...

> auto-increment имхо один из самых простых способ получить уникальные id-шки
С одной стороны простой. С другой ты получаешь точку отказа. И точку которая не поддается маштабированию вообще. Такми же образом между тем как ты получил значение и сделал автоинкремент может вклинится другой процесс и ты получишь проблему когда разные данные имеют один идентификатор. Но в отличие от рандома, растояние между 2 числами = 1 следовательно вероятность такой проблемы выше. Рамдом обеспечивает маштабируемость, и азмазывает равномерно все числа по всему доступному пространству.

> А в чем преимущество буквенно-циферных ?
Отсутсвие ограничения в 2^32 для поля типа int.
И да уже при превышении 1 милиарда записей, вероятность попать случано сгенерированым числом в заняую обсать - значительно возрастает.

Макс сказав...

> С другой ты получаешь точку отказа
Да, с этим согласен, но масштабирование придумать можно. Например поднять 2 сервиса на разных машинах. На одном сделать четные значения, на втором - нечетные. Выбирать случайным образом и инкрементить на 2. Хотя это я конечно уже выкручиваюсь :)

А вот с вклиниванием другого процесса в инкремент - ты не прав (как минимум для mysql, memcachedb и readis)
Memcachedb и readis - имеют атомарный инкремент. То есть ты просто шлешь на сервер
INC some_key 1
и тебе возвращается новое значение. Тебе не надо делать get, вручную добавлять единицу и потом set.

Для mysql есть как минимум 2 варианта (фактически 2 вариации с функцией last_insert_id()):
Создать какую-то таблицу с auto_increment полем, делать туда insert, и получать id вставленной записи через mysql_insert_id() или SELECT LAST_INSERT_ID().
Они возвращают последний id записи вставленной _в_этом_конкретном_mysql_соединении_
Второй вариант :
см
http://dev.mysql.com/doc/refman/5.0/en/information-functions.html#function_last-insert-id
на тему :
CREATE TABLE sequence (id INT NOT NULL);
INSERT INTO sequence VALUES (0);
UPDATE sequence SET id=LAST_INSERT_ID(id+1);
SELECT LAST_INSERT_ID();

Насчет других БД - не знаю, но наверняка что-то есть (им же надо как-то с mysql конкурировать на рынке веб-приложений)

Макс сказав...

не readis - а redis (рука на автомате - привыкла слово "read" писать :))

Necromant2005 сказав...

mecached - ты наверное имел ввиду memcounter, так как memcache поддерживает только GET/SET/REPLACE
(http://www.danga.com/memcached/)

>Насчет других БД
Autoincrement - Это изобретение Mysql. Можно легко проверить посмотрев исходный код адаптеров Oracle, Postgres, DB2

Макс сказав...

> mecached - ты наверное имел ввиду memcounter, так как memcache поддерживает только GET/SET/REPLACE

http://ru2.php.net/memcache_increment
http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt (раздел Increment/Decrement)
и я писал именно про memcacheDB - который данные на диске хранит.


> Можно легко проверить посмотрев исходный код адаптеров Oracle, Postgres, DB2
А может адаптерам не повезло с авторами ? ;)
вот для постгрес нашел вот это:
http://www.postgresql.org/docs/8.1/static/functions-sequence.html
то есть там nextval() может заменить mysql-ный last_insert_id().
похожее есть и для oracle : http://blog.aggregatedintelligence.com/2008/12/oracle-sequences-and-concurrency.html

Necromant2005 сказав...

>А может адаптерам не повезло
Из всего можно выкрутится. Виртуальные таблицы содержащие последовательность, тригеры опять с теми же последовательностями.
Просто везде за исключением Mysql єто появилось позднее и выглядит как прикрученое сбоку 5 колесо.