26 November 2008

Горизонтальное масштабирование PostgreSQL с помощью PL/Proxy.

PostgreSQL
Очень тяжело начать писать статью. Т.е очень тяжело придумать вступительное слово. Хочется рассказать обо всём и сразу :) Но нет. Будем последовательны.
Начну с того что совсем недавно проходил Highload++ 2008 на котором мне удалось побывать.
Скажу сразу — мероприятие было проведено по высшему клаcсу, докладов было много и все были очень интересными.
Одной из самых запомнившихся презентаций была лекция Аско Ойя об инфраструктуре серверов баз данных в Skype. Лекция в большей степени касалась различных средств с помощью которых достигается такая производительность серверов.
По словам Аско, база данных Skype выдержит даже если все жители Земли захотят подключится к скайп в один момент.

Приехав домой очень захотелось это всё попробовать в живую. О чём я сейчас и расскажу. Сразу оговорюсь — структура базы данных для теста, взята из примера на сайте самих разработчиков и естественно не имеет ничего общего с реальной загрузкой.
В статье будет описано что распределением нагрузки надо заниматься после того как уже припекло и база падает, но это не совсем так. С помощью данной статьи я как раз хочу подготовить начинающих и не опытных разработчиков и заодно заставить их задуматься о том, что предусматривать возможность распределения нагрузки между серверами надо ещё при проектировании системы. И это не будет считаться той самой «преждевременной оптимизацией» о которой так много пишут и которой так боятся.

UPD: Как правильно заметил хабраюзер descentspb в статье присутствует досаднейшая ошибка. В следствие своей невнимательности я подумал что PgBouncer надо устанавливать между прокси и клиентом. Но, как оказалось, та проблема которую я решал с помощью PgBouncer не решится если установить его именно так. Правильнее надо устанавливать боунсер между нодами и прокси. Мало того, именно так и рекомендуется делать в оффициальном мануале на сайте PL/Proxy.
В любом случае использование PgBouncer так как указано на моей схеме также даст прирост производительности. (Разгрузит Proxy).




1.Кто виноват?


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

— Оптимизируем код.
— Наращиваем мощность сервера.
— Кеширование (ищем по тегам статьи о memcache).
— Распределяем нагрузку между серверами.

Остановимся на последнем пункте.

2.Что делать?


Итак код оптимизирован, сервера круче некуда вся база лежит в кеше и тем не менее падает от одного запроса. Пришло время заняться горизонтальным масштабированием.
Ах да, я до сих пор не упомянул что статья о PostgreSQL. А вы что до сих пор пользуетесь MySQL? Тогда мы идём к вам :)
По моему скромному мнению если проект действительно серёзный то и база должна быть немного по серьёзней чем MySQL. Тем более что для Postgres'a существуют такие замечательные средства для масштабирования. (Может быть и для MySQL есть? Жду ответную статью :) ).

3. А с чем это едят?


PL/Proxy представляет из себя язык для удалённого вызова функций на серверах баз данных PostgreSQL, а также для партицирования данных.
Схема работы показана на картинке. О PgBouncer я расскажу ниже.
image

Обычно ваше приложения просто делает запрос к базе данных. В нашем случае приложение тоже делает обычный запрос к базе данных. Только вызывает оно не чистый SQL-код, а заранее написанную функцию.
Далее база данных определяет на каком из нодов расположены требуемые данные.
И перенаправляет запрос на нужный сервер.
Запрос выполняется и возвращается на главный сервер после чего данные возвращаются в приложение.

Все вроде бы хорошо но при большом количестве запросов, PL/Proxy создаёт большое
количество соединений к нодам, а это создаёт новый процесс Postgres (fork) что не очень хорошо влияет на производительность. Что бы решить эту проблему и нужен PgBouncer.
PgBouncer является… мм… как бы это сказать так что бы не налажать… Мультиплексором соединений. Он выглядит как обычный процесс Postgres, но внутри он управляет очередями запросов что позволяет в разы ускорить работу сервера. Из тысяч запросов поступивших к PgBouncer до базы данных дойдет всего несколько десятков.
Что бы оценить бонус от использования этого замечательного средства достаточно взглянуть на график загрузки сервера базы данных на двух сайтах до и после включения PgBouncer. Картинка взята из презентации Николая Самохвалова «Производительность Postgres».
image

4.Дайте же и мне этих мягких французских булок


4.1.Установка PgBouncer

Процесс установки нисколько не оригинален:
Качаем пакет (на момент написания статьи последняя версия была 1.2.3)
pgfoundry.org/frs/?group_id=1000258
Распаковываем:
#tar -xzvf pgbouncer-1.2.3.tgz
Компилим и ставим:
#cd pgbouncer-1.2.3
#./configure
#make
#make install


Создаём конфигурационный файл:
/etc/pgbouncer/pgbouncer.ini

[databases]
testdb = host=localhost port=5432 dbname=testdb

[pgbouncer]
listen_port = 6543
listen_addr = 127.0.0.1
auth_type = md5
auth_file = users.txt
logfile = /var/log/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid
pool_mode = statement #Если вы не планируете использовать PL/Proxy эту строчку указывать не надо
admin_users = root

Создаём файл с аутентификацией.
/etc/pgbouncer/users.txt
"testdb_user" "testdb_user_password"

Запускаем:
/usr/local/bin/pgbouncer -d /etc/pgbouncer/pgbouncer.ini -u postgres
Ключ -d указывает на то что надо запускать в режиме демона, а ключ -u указывает от чьего имени надо запускать процесс pgbouncer.

Для пользователей gentoo приятный сюрприз в виде стартового скрипта:
/etc/init.d/pgbouncer
#!/sbin/runscript

depend() {
need postgresql
use pgsql
}
start() {
ebegin "Starting Pgbouncer"
start-stop-daemon --start --background --exec /usr/local/bin/pgbouncer --chdir /etc/pgbouncer/ -- -d pgbouncer.ini -u postgres
eend $? "Failed to start Pgbouncer"
}
stop() {
ebegin "Stopping Pgbouncer"
start-stop-daemon --pidfile /var/run/pgbouncer/pgbouncer.pid --stop
eend $? "Failed to stop Pgbouncer"
}


Теперь в качестве DSN в своём приложении надо будет поменять только порт на который подключаться к базе данных с 5432 на 6543 и начать сравнивать загруженность сервера до и после.

4.2 Установка Pl/Proxy

Для проведения этого эксперимента нам понадобится 3 сервера Postgres.
Один из них, назовём его proxy, будет проксировать запросы на два других.
Назовём их node1 и node2.
Для корректной работы pl/proxy рекомендуется использовать количество нод равное степеням двойки.
Предполагаю что сам Postgres у вас уже установлен.
Устанавливаем PL/Proxy на сервере proxy.
Качаем свежую версию pl/proxy: pgfoundry.org/frs/?group_id=1000207
Как обычно:
#./configure
#make
#make install
Вот тут надо бы перезапустить сам Postgres.
А теперь начинается самое интересное.

Для теста создадим на каждой из нод новую базу данных proxytest:
CREATE DATABASE proxytest
WITH OWNER = postgres
ENCODING = 'UTF8';


И а внутри этой базы создадим Schema c названием plproxy. В официальной инструкции этого пункта не было но у меня почему-то все вызываемые функции пытались вызываться именно так: plproxy.functioname().
CREATE SCHEMA plproxy
AUTHORIZATION postgres;
GRANT ALL ON SCHEMA plproxy TO postgres;
GRANT ALL ON SCHEMA plproxy TO public;



И добавим в неё одну табличку:
CREATE TABLE plproxy.users
(
user_id bigint NOT NULL DEFAULT nextval('plproxy.user_id_seq'::regclass),
username character varying(255),
email character varying(255),
CONSTRAINT users_pkey PRIMARY KEY (user_id)
)
WITH (OIDS=FALSE);
ALTER TABLE plproxy.users OWNER TO postgres;



Теперь создадим функцию для добавления данных в эти таблицы:
CREATE OR REPLACE FUNCTION plproxy.insert_user(i_username text, i_emailaddress text)
RETURNS integer AS
$BODY$
INSERT INTO plproxy.users (username, email) VALUES ($1,$2);
SELECT 1;
$BODY$
LANGUAGE 'sql' VOLATILE;
ALTER FUNCTION plproxy.insert_user(text, text) OWNER TO postgres;



С нодами покончено. Приступим к настройке сервера.
Как и на всех нодах, на главном сервере (proxy) должна присутствовать база данных:
CREATE DATABASE proxytest
WITH OWNER = postgres
ENCODING = 'UTF8';



И соответсвующая schema:
CREATE SCHEMA plproxy
AUTHORIZATION postgres;
GRANT ALL ON SCHEMA plproxy TO postgres;
GRANT ALL ON SCHEMA plproxy TO public;



Теперь надо укзать серверу что эта база данных управляется с помощьюpl/proxy:
CREATE OR REPLACE FUNCTION plproxy.plproxy_call_handler()
RETURNS language_handler AS
'$libdir/plproxy', 'plproxy_call_handler'
LANGUAGE 'c' VOLATILE
COST 1;
ALTER FUNCTION plproxy.plproxy_call_handler() OWNER TO postgres;
-- language
CREATE LANGUAGE plproxy HANDLER plproxy_call_handler;



Также, для того что бы сервер знал где и какие ноды него есть надо создать 3 сервисные функции которые pl/proxy будет использовать в своей работе:
CREATE OR REPLACE FUNCTION plproxy.get_cluster_config(IN cluster_name text, OUT "key" text, OUT val text)
RETURNS SETOF record AS
$BODY$
BEGIN
-- lets use same config for all clusters
key := 'connection_lifetime';
val := 30*60; -- 30m
RETURN NEXT;
RETURN;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE
COST 100
ROWS 1000;
ALTER FUNCTION plproxy.get_cluster_config(text) OWNER TO postgres;



Важная функция код которой надо будет подправить. В ней надо будет указать DSN нод:
REATE OR REPLACE FUNCTION plproxy.get_cluster_partitions(cluster_name text)
RETURNS SETOF text AS
$BODY$
BEGIN
IF cluster_name = 'clustertest' THEN
RETURN NEXT 'dbname=proxytest host=node1 user=postgres';
RETURN NEXT 'dbname=proxytest host=node2 user=postgres';
RETURN;
END IF;
RAISE EXCEPTION 'Unknown cluster';
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE
COST 100
ROWS 1000;
ALTER FUNCTION plproxy.get_cluster_partitions(text) OWNER TO postgres;



И последняя:
CREATE OR REPLACE FUNCTION plproxy.get_cluster_version(cluster_name text)
RETURNS integer AS
$BODY$
BEGIN
IF cluster_name = 'clustertest' THEN
RETURN 1;
END IF;
RAISE EXCEPTION 'Unknown cluster';
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE
COST 100;
ALTER FUNCTION plproxy.get_cluster_version(text) OWNER TO postgres;



Ну и собственно самая главная функция которая будет вызываться уже непосредственно в приложении:
CREATE OR REPLACE FUNCTION plproxy.insert_user(i_username text, i_emailaddress text)
RETURNS integer AS
$BODY$
CLUSTER 'clustertest';
RUN ON hashtext(i_username);
$BODY$
LANGUAGE 'plproxy' VOLATILE
COST 100;
ALTER FUNCTION plproxy.insert_user(text, text) OWNER TO postgres;



Вопросы по коду функций принимаются в комментах, однако примите во внимание что я не гуру Postgres, а всего лишь ученик.

А теперь тестрируем! :)

Подключаемся к серверу proxy на порт 6543 (будем сразу работать через PgBouncer).
И заносим данные в базу:
SELECT insert_user('Sven','sven@somewhere.com');
SELECT insert_user('Marko', 'marko@somewhere.com');
SELECT insert_user('Steve','steve@somewhere.com');



Теперь можно подключится на каждую из нод и если вы всё сделали правильно и без ошибок то первые две записи будут на ноде node1, а третья запись на ноде node2.

Пробуем извлечь данные.
Для этого напишем новую серверную функцию:
CREATE OR REPLACE FUNCTION plproxy.get_user_email(i_username text)
RETURNS SETOF text AS
$BODY$
CLUSTER 'clustertest';
RUN ON hashtext(i_username) ;
SELECT email FROM plproxy.users WHERE username = i_username;
$BODY$
LANGUAGE 'plproxy' VOLATILE
COST 100
ROWS 1000;
ALTER FUNCTION plproxy.get_user_email(text) OWNER TO postgres;



И попробуем её вызвать:
select plproxy.get_user_email('Steve');


Вопщем то, у меня всё получилось.

5.А почему ты такое бедный раз такой умный?


Как видно на тестовом примере ничего сложного в работе с pl/proxy нет. Но, я думаю все кто смог дочитать до этой строчки уже поняли что в реальной жизни все не так просто.
Представьте что у вас 16 нод. Это же надо как-то синхронизировать код функций. А что если ошибка закрадётся — как её оперативно исправлять?
Этот вопрос был задан и на конференции, на что Аско ответил что соответствующие средства уже реализованы внутри самого Skype, но ещё не достаточно готовы для того что бы отдавать их на суд сообществе opensource.
Второй проблема которая не дай бог коснётся вас при разработке такого рода системы, это проблема перераспределения данных в тот момент когда нам захочется добавить ещё нод в кластер.
Планировать эту масштабную операцию прийдётся очень тщательно, подготовив все сервера заранее, занеся данные и потом в один момент подменив код функции get_cluster_partitions.

6.Дополнитеьлные материалы


Проекты PlProxy и PgBouncer на сайте разработчиков Skype.

Презентация Аско на Highload++
Производительность Postgres Николай Самохвалов (Постгресмен)

7. Бонус для внимательных


Уже после того как я опубликовал статью я обнаружил в ней одну ошибку и один недочёт.
Опишу тут так как править уже написанную статью тяжело.
1) В таблицах используюет sequence с именем user_id_seq. Но SQL кода для неё нигде не приведено. Соответственно если кто то будет просто копипастить код — у него ничего не выйдет. Исправляюсь:
CREATE SEQUENCE plproxy.user_id_seq
INCREMENT 1
MINVALUE 0
MAXVALUE 9223372036854775807
START 1
CACHE 1;
ALTER TABLE plproxy.user_id_seq OWNER TO postgres;


2) Во время вставки данных в базу генерируется последовательность для поля user_id. Однако этих последовательностей две. И каждая работает на своей ноде. Что неминуемо приведёт к тому что два разных пользователя будут иметь одинаковые user_id.
Соответственно функцию insert_user надо исправить таким образом, что бы новый user_id брался из последовательности размещённой на сервере proxy а не на нодах. Таким образом можно избежать дублирование в поле user_id.

З.Ы: Весь SQL-код подсвечен в Source Code Highliter
Tags:postgresqlpostgresplproxyplpgsqlpgbouncerвысоконагруженные проектыраспределенные системы
Hubs: PostgreSQL
+46
22.5k 125
Comments 46
Top of the last 24 hours