воскресенье, 29 марта 2009 г.

[Перевод] PostgreSQL кластер: партишинг с plproxy (часть 2)

оригинал в блоге Kristo Kaiv

В предыдущем посте я описал как установить plproxy и сделать базовый вариант горизонтального кластера.
Теперь взглянем на другое типичное применение в жизни: создание кластера только-чтение для вашей БД.

Разделение нагрузки на чтение.

Самое простое использование в жизни для plproxy - использование для перенаправления запросов только-чтение на реплики "только-чтение" основной БД. Реплицированые БД могут быть заполнены данными с помощью Londiste из пакета SkyTools. Описание установки может быть найдено тут. Или можно использовать Slony, который является более тяжелым решением и из моего опыта так же более труден в установке и поддержке, хотя и лучше документирован.

Типичный кластер "только-чтение" изображен на схеме

БД со знаком "p" имеют пул соединений. Мы используем PgBouncer но pgpool тоже хорошее решение.

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

В этой установке plproxy-функции определяют базу, к которой нужно перенаправить запрос. Запросы на чтение-запись идут к мастер-базе, а только-чтение к репликам "только-н-чтение".

Настройка репликации проста, как только вы пройдете сложный процесс установки утилит skytools.

Первое - давайте создадим конфигурацию для реплик и мастер-базы:

файл replica1.ini

[londiste]

job_name = londiste_master_to_r1
provider_db = dbname=write
subscriber_db = dbname=ro1
# значение будет использовано как идентификатор, так что не используйте точки и пробелы
pgq_queue_name = londiste.write
pidfile = %(job_name)s.pid
logfile = %(job_name)s.log
use_skylog = 0

Файл replica2.ini практически такой же, только название задания и имя базы нужно изменить. Теперь установим Londiste на провайдере (мастер-базе) и подписчиках (r01, r02) и запусим демонов (службы) репликации:

mbpro:~/temp kristokaiv$ londiste.py replica1.ini provider install
mbpro:~/temp kristokaiv$ londiste.py replica1.ini subscriber install
mbpro:~/temp kristokaiv$ londiste.py replica2.ini subscriber install
mbpro:~/temp kristokaiv$ londiste.py replica1.ini replay -d
mbpro:~/temp kristokaiv$ londiste.py replica2.ini replay -d

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

файл ticker_write.ini

[pgqadm]
job_name = ticker_write
db = dbname=write
# как часто запусткать задачу [минуты]
maint_delay_min = 1
# как часто проверять активность [секунды]
loop_delay = 0.1
logfile = %(job_name)s.log
pidfile = %(job_name)s.pid
use_skylog = 0

Для запуска тикера как демон (служба) выполните:

mbpro:~/temp kristokaiv$ pgqadm.py ticker_write.ini ticker -d

Давайте создадим простую таблицу, которую мы будем реплицировать с мастера на репликантов "только-чтение".

mbpro:~/temp kristokaiv$ psql -c "CREATE TABLE users (username text primary key, password text);" write

mbpro:~/temp kristokaiv$ psql -c "CREATE TABLE users (username text primary key, password text);" ro1

mbpro:~/temp kristokaiv$ psql -c "CREATE TABLE users (username text primary key, password text);" ro2

Добавляем её к репликации

mbpro:~/temp kristokaiv$ londiste.py replica1.ini provider add users
mbpro:~/temp kristokaiv$ londiste.py replica1.ini subscriber add users
mbpro:~/temp kristokaiv$ londiste.py replica2.ini subscriber add users

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

Функции для вставки и выборки из таблицы users:


CREATE OR REPLACE FUNCTION public.add_user(
in i_username text,
in i_password text,
out status_code text
) AS $$
BEGIN
PERFORM 1 FROM users WHERE username = i_username;
IF NOT FOUND THEN
INSERT INTO users (username, password) VALUES (i_username, i_password);
status_code = 'OK';
ELSE
status_code = 'user exists';
END IF;
RETURN;
END; $$ LANGUAGE plpgsql SECURITY DEFINER;

GRANT EXECUTE ON FUNCTION public.add_user(
in i_username text,
in i_password text,
out status_code text
) TO plproxy;

CREATE OR REPLACE FUNCTION login(
in i_username text,
in i_password text,
out status_code text
) AS $$
BEGIN
SELECT 'OK' FROM users u WHERE username = i_username AND password = i_password INTO status_code;
IF NOT FOUND THEN status_code = 'FAILED'; END IF;
RETURN;
END; $$ LANGUAGE plpgsql SECURITY DEFINER;

GRANT EXECUTE ON FUNCTION login(
in i_username text,
in i_password text,
out status_code text
) TO plproxy;


Только для комфорта тех, кто реально попробует повторить эти шаги, привожу конфигурацию прокси-бд:

cluster config:


CREATE OR REPLACE FUNCTION plproxy.get_cluster_partitions (cluster_name text)
RETURNS SETOF text AS $$
BEGIN
IF cluster_name = 'readonly' THEN
RETURN NEXT 'host=127.0.0.1 dbname=ro1';
RETURN NEXT 'host=127.0.0.1 dbname=ro2';
RETURN;
ELSIF cluster_name = 'write' THEN
RETURN NEXT 'host=127.0.0.1 dbname=write';
RETURN;
END IF;
RAISE EXCEPTION 'no such cluster%', cluster_name;
END; $$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE OR REPLACE FUNCTION plproxy.get_cluster_config(
in cluster_name text,
out key text,
out val text)
RETURNS SETOF record AS $$
BEGIN
RETURN;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION plproxy.get_cluster_version(cluster_name text) RETURNS int AS $$
SELECT 1;
$$ LANGUAGE SQL;


Последнее что осталось сделать - создать plproxy-функцию, которая перенаправляет вызов функции логина на бд только-чтение и функцию add_user на бд для записи (мастер).


CREATE OR REPLACE FUNCTION public.login(
in i_username text,
in i_password text,
out status_code text
) AS $$
CLUSTER 'readonly'; RUN ON ANY;
$$ LANGUAGE plproxy;

CREATE OR REPLACE FUNCTION public.add_user(
in i_username text,
in i_password text,
out status_code text
) AS $$
CLUSTER 'write';
$$ LANGUAGE plproxy;


Вот и всё. кластер готов! Учтите, что хотя создание кластера только-чтение выглядит просто и как быстрое решение для ваших проблем с производительностью - это не решение "серебряная пуля".

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

[Перевод] PostgreSQL кластер: партишинг с plproxy (часть 1)

оригинал в блоге Kristo Kaiv

мой перевод на http://translated.by/you

Skype разработал много полезных утилит для создания кластера баз данных и эта серия публикаций прольет свет на ранее недокументированный функционал. В основе - plproxy. Лучшая фраза описать функциональность - "dblink на стероидах". Это короткое пособие объяснит, как установить plproxy, написать простые запросы к БД и установить простое горизонтальное масштабирование.

Партишинг для чайников.
Партишинг позволяет вам распределить загрузку БД и сами данные между несколькими серверами. Принцип прост. Допустим вы имеете одну таблицу, которая содержит данные на пользователей. Но проблема - миллионы пользователей ежедневно входят по своим аккаунтам в систему. Это создает большую загрузку сервера, не говоря о огромных размерах БД. Первое - нужно определить критерий, по которому мы будем выбирать на каком сервере содержатся данные. Мы можем сделать это по первому символу и имени пользователя. Пользователи a-j попадают в первый сервер, а k-z во второй. Это будет работать. Но скорее всего нагрузка на серверы будет не равномерной. Распространенное решение - использовать партишинг основываясь на хэше первичного ключа, в нашем случае - имени пользователя. Использование хэша позволит размещать записи более равномерно между серверами. То, что вам нужно знать про функцию хэша - это то, что она просто вычисляет номер, основанный на любом введённом значении.

Пример:
select hashtext('kristokaiv1') = 1116512480
select hashtext('kristokaiv2') = 1440348351
select hashtext('kristokaiv3') = -219299073

Объяснение, как это работает - выходит за рамки этого пособия.
Итак, допустим мы имеем 2 партиции, так что мы можем получить номер партиции основываясь на хэше имени пользователя.
Пример: partition nr = hashtext($1) & 1

&1 даст нам последний бит номера, который может быть 0 или 1. Это номер, которым мы можем выбрать партицию, на которой будут сохранены данные. Если 0 - данные попадут на партицию 0 и если 1 - на партицию 1.
select hashtext('kristokaiv1') & 1 = 0 -> partition 0
select hashtext('kristokaiv2') & 1 = 1 -> partition 1
select hashtext('kristokaiv3') & 1 = 1 -> partition 1

Как работает plproxy.
Концепция очень проста - plproxy это новый язык для запросов в PostgreSQL БД, который позволяет делать запросы к удаленным БД также как это делает dblink. Синтаксис очень прост - следующий запрос создает новую plproxy-функцию в БД, которая при запуске подключится в БД remotedb, выполнит функцию get_user_email(text) и вернет результаты.

localdb=#
CREATE FUNCTION get_user_email(username text) RETURNS text AS $$
CONNECT 'dbname=remotedb host=123.456.123.21 user=myuser';
$$ LANGUAGE plproxy;

Создадим функцию-пустышку в remotedb, которая будет отвечать на запрос

remotedb=#
create function get_user_email(text)
returns text as $$
select 'me@somewhere.com'::text;
$$ language sql;

При выполнении мы получим те же результаты, которые получаем по запросу на remotedb

localdb=#
select * from get_user_email('tere');
get_user_email
------------------
me@somewhere.com
(1 row)

Конечно это только простой пример и я вернусь к более сложному синтаксу позже. Давайте посмотрим, как установить plproxy-язык.

Установка plproxy.
Загрузить код можно с http://pgfoundry.org/projects/plproxy/ но я советую взять свежую версию из CVS (системы контроля версий).
Должен быть установлен набор разработки PostgreSLQ. Каталог, где расположена утилита конфигурации постгреса (pg_config), должен быть включен в переменную $PATH.
При этих условиях установка сводится к:

$ make
$ make install
$ make installcheck

Если у вас не получается заставить это работать - вы всегда можете попросить помощи. Финальный шаг - установить язык в БД. Это нельзя сделать как с другими языками (пользуясь утилитой createlang). Вы должны выполнить файл plproxy.sql, который создаст обработчик языка.

Файл plproxy.sql должен быть где-то в каталоге contrib.
$ psql -f /usr/local/pgsql/share/contrib/plproxy.sql queries
CREATE FUNCTION
CREATE LANGUAGE

Всё. Можно проверить установку простой plproxy-функцией (см. в синтаксических примерах).

Установка первого кластера.

Давайте создадим простой кластер, который будет состоять из 3 БД (в моем примере они все используют один и тот же вариант PostgreSQL). Одна прокси-БД вызывает запросы и две разделенные БД (партиции) с данными. Горизантальное разделение сделано на основе имени пользователя. Это часто используемый способ, т.к. большинство данных в БД - это связанные с пользователем: логин, заказы, платы, настройки...

Настройка Кластера.

Настройки БД кластеров хранятся внутри plpgsql-функций, которые вызывает плпрокси.

Существует 3 функции, которые вы должны создать для правильной конфигурации. Давайте создадим их в БД прокси.

1) plproxy.get_cluster_version(cluster_name text)
Эта функция вызывается при каждом запросе и она используется для отслеживания изменений конфигурации кластера (если возвращаемый ей номер больше, чем закешированное значение - конфигурация перезагружается). Давайте начнем с первой версии конфигурации. Пример:
CREATE OR REPLACE FUNCTION plproxy.get_cluster_version(cluster_name text) RETURNS int AS $$
BEGIN
IF cluster_name = 'queries' THEN
RETURN 1;
END IF;
END;
$$ LANGUAGE plpgsql;

2) plproxy.get_cluster_partitions(cluster_name text)
Эта функция должна возвращать строки для соединения для всех партиций в правильном порядке.
Из-за необоснованного ограничения общее кол-во должно быть кратно 2. Это необоснованное ограничение можно легко обойти, но это будет объяснено в другой раз.

CREATE OR REPLACE FUNCTION plproxy.get_cluster_partitions(cluster_name text) RETURNS SETOF text AS $$
BEGIN
IF cluster_name = 'queries' THEN
RETURN NEXT 'host=127.0.0.1 dbname=queries_0000';
RETURN NEXT 'host=127.0.0.1 dbname=queries_0001';
RETURN;
END IF;
RAISE EXCEPTION 'no such cluster: %', cluster_name;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Если в строке соединения не указан логин для БД, будет использован текущий (CURRENT_USER). Так как плпрокси не знает ни одного пароля, разделенные БД должны разрешать соединения от БД прокси.

3)plproxy.get_cluster_config(cluster_name text)
Это эквивалент инициализационного файла. Функция должна возвращать параметры как пары ключь-значение. Все они - не обязательные, но нужно определить функцию-пустышку:
CREATE OR REPLACE FUNCTION plproxy.get_cluster_config (cluster_name text, out key text, out val text)
RETURNS SETOF record AS $$
BEGIN
RETURN;
END;
$$ LANGUAGE plpgsql;

Более подробно о параметрах и что они делают - читайте в документации plproxy.

Теперь установка завершена и можно начать "играть" с нашим новым кластером.

Давайте создадим новую таблицу для хранения имен пользователей (логинов) на обеих партициях.

#queries_0000=# CREATE TABLE users (username text PRIMARY KEY);
#queries_0001=# CREATE TABLE users (username text PRIMARY KEY);

Также нужно создать новую функцию, которая будет добавлять новые имена пользователей в таблицу:

CREATE OR REPLACE FUNCTION insert_user(i_username text) RETURNS text AS $$
BEGIN
PERFORM 1 FROM users WHERE username = i_username;
IF NOT FOUND THEN
INSERT INTO users (username) VALUES (i_username);
RETURN 'user created';
ELSE
RETURN 'user already exists';
END IF;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Теперь давайте создадим прокси-функцию в БД прокси, которая будет вызывать запросы на партициях.

queries=#
CREATE OR REPLACE FUNCTION insert_user(i_username text) RETURNS TEXT AS $$
CLUSTER 'queries'; RUN ON hashtext(i_username);
$$ LANGUAGE plproxy;

Заполнение партиций случайными данными:
SELECT insert_user('user_number_'||generate_series::text) FROM generate_series(1,10000);

Теперь, если мы посмотрим во все БД мы увидим, что обе заполнены и заполнение почти равномерное.
queries_0001 count(*) -> 5071
queries_0000 count(*) -> 4930

Продолжение следует...

четверг, 5 марта 2009 г.

MS SQL newid() для PostgreSQL

UPDATE:
Как оказалось есть UUID. но нет функции-генератора.
Так что я поменял тип возвращаемого результата на UUID.


В данный момент работаю над переходом с MSSQL на бесплатный PostgreSQL.
Первое, на что напоролся - GUID`ов НЕТ!
Поковырял функции постгреса и написал замену:


drop function newid();
create or replace function newid() returns uuid
as $$

DECLARE r varchar='';
chars varchar='0123456789abcdef';
i int:=1;
pos int=0;
one_char varchar(1);
begin


while i<=32 LOOP
pos=round(random()*15)+1;
one_char=cast (substr(chars,pos, 1) as varchar);
r=r||one_char;
if (length(r) in (8,13,18,23)) then r=r||'-'; end if;
i=i+1;
end LOOP;

return r;
end
$$
LANGUAGE 'plpgsql';