پیاده سازی PostgreSQL Distributed Architecture

چند وقت پیش ما یک دیتابیس حجیم در PostgreSQL داشتیم که روزانه چند ده میلیون رکورد رو Insert میکردیم و میخواستیم حجم Insert های ورودی رو باز هم افزایش بدیم. اضافه کردن سخت افزار به سرور فعلی منطقی نبود و چاره‌ای نداشتیم جز این که به صورت افقی سیستم رو Scale کنیم و تعداد سرورها رو افزایش بدیم. تو این مطلب سعی می‌کنم تجربه پیاده سازی و استفاده از این معماری رو توضیح بدم.دیتابیس‌های توزیع شده (Distributed Databases)معماری دیتابیس توزیع شدهدیتابیس‌های توزیع شده دیتابیس هایی هستن که محل ذخیره سازی اونها به یک پردازنده مشترک متصل نشده و از طریق شبکه با هم در ارتباط هستن. ممکنه این سرورها در یک نقطه فیزیکی کنار هم باشند یا از نظر جغرافیایی در نقاط مختلفی قرار گرفته باشند.یک دیتابیس توزیع شده که درست طراحی شده باشه هم از نظر معماری و هم شبکه شفاف یا Transparent هست. به این معنی که استفاده کننده از اون متوجه معماری و ارتباط سرورهای متصل به دیتابیس نمیشه و انگار به یک دیتابیس با یک Node متصل هست.استفاده از این معماری مزایا و معایب خودش رو داره. از مزایای اون میشه به high availability و fault tolerance اشاره کرد و از مهمترین معایب اون پیچیده شدن و سخت‌تر شدن نگهداری و عیب‌یابی سیستم هست.کوئری‌های توزیع شده (Distributed Queries)برای این که روی یک دیتابیس توزیع شده بتونیم کوئری بزنیم باید کارهایی از قبل انجام بدیم. فرض کنید تو یک دیتاسنتر ما چند تا سرور داریم که دیتای این سرورها کپی یا duplicate هم نیست. برای کوئری زدن روی این دیتابیس‌ها مجبور بودیم مستقیم به هر دیتابیس وصل بشیم و دیتای اون رو بخونیم ولی طبیعیتا نیاز داشتیم تا دیتای همه سرورها رو در کنار هم داشته باشیم. با یه سرچ ساده به یک سری راه حل رسیدیم و نیازهای خودمون رو با اونها بررسی کردیم:dblink extensionPostgres Foreign Data Wrapper (fdw) extensionPrestoافزونه dblink:یکی از extension های postgres هست که ساده ترین روش پیاده سازی کوئری‌های توزیع شده روی دیتابیس رو داره ولی روش بهینه‌ای نیست. مدل کار کردن این افزونه به این شکل هست که موقع کوئری زدن، دیتا از روی دیتابیس‌های remote روی یک سرور جمع میشه و دیتای اضافه‌ای مثل آمار و index ها رو نداره در نتیجه برای پردازش دیتای حجیم مشکلات performance ای داره.افزونه fdw:یکی دیگه از افزونه‌های postgres هست که از ورژن ۹.۳ همراه دیتابیس نصب میشه. نسخه ارتقا پیدا کرده همون dblink هست که مراحل پیاده سازی پیچیده تری داره ولی سرعت بالاتری هم داره. روش کارش هم به این صورته که در کنار جدول‌های اصلی یک سری جدول خارجی foreign table از دیتابیس های دیگه رو میتونه نمایش بده و در نتیجه کوئری های تجمیع شده انجام بده. دیتابیس و جدول‌های خارجی که این افزونه نمایش میده مثل یک symlink در کنار دیتابیس اصلی نشون داده میشه.نرم افزار Presto:یک Distributed Query Engine هست که توسط فیسبوک و زبان Java توسعه داده شده. از Presto برای کوئری‌های big data در حد چندین ترابایت در فیسبوک استفاده میشه. به دلیل استفاده از Java خیلی سبک نیست ولی مراحل پیاده سازی و مستندات اون به شدت ساده هست. نکته مثبت دیگه این که فقط مختص Postgres نیست و خیلی دیگه از دیتابیس‌ها رو میشه به عنوان منبع ورودی بهش وصل کرد. از معایب اون هم میشه به client اختصاصی اشاره کرد که برای انجام کوئری باید از اون استفاده کرد و در نتیجه transparency در اون رعایت نمیشه و ممکنه در یکپارچه سازی با سیستم‌های دیگه به مشکل خورد.با توجه به این بررسی‌ها ما سراغ افزونه fdw رفتیم و در ادامه نحوه پیاده سازی اون رو برای یک نمونه توضیح میدم.فرض کنید در دیتاسنتر ما سرور A رو تا الان داشتیم و حالا تصمیم گرفتیم سرور B رو اضافه کنیم. این دیتابیس‌ها قرار هست هر کدوم بخشی از دیتا رو نگه دارن و کپی هم نباشند. سرور A رو به عنوان دیتابیس اصلی و سرور B رو به عنوان دیتابیس فرعی در نظر میگیریم و یک جدول نمونه blog_post رو روی هر دو دیتابیس ایجاد میکنیم. برای پیاده سازی fdw مراحل زیر رو انجام میدیم.روی سرور اصلی (سرور A) دیتابیس و جدول های مورد نیاز رو ایجاد میکنیم:$ create database blog; $ \c blog; $ create table blog_post (id serial primary key, title text, body text);حالا با استفاده از دستور explain تست میکنیم که کوئری روی چه جدول‌هایی اجرا میشه:$ explain select * from blog_post; QUERY PLAN ------------------------------------------------------------- Seq Scan on blog_post (cost=0.00..18.50 rows=850 width=68)همونجور که انتظار داشتیم کوئری فقط روی جدول blog_post اجرا شده.حالا سراغ دیتابیس فرعی (سرور B) میریم و اول تنظیمات رو برای postgres انجام میدیم تا دسترسی از دیتابیس اصلی به این دیتابیس وجود داشته باشه. برای کوتاه شدن مطلب این قسمت رو ننوشتم ولی با یک سرچ ساده میشه این تغییرات رو انجام داد.پس از این که دسترسی بین سرور اصلی و فرعی برقرار شد حالا دیتابیس و جدول‌های مورد نیاز رو روی دیتابیس فرعی ایجاد می‌کنیم:$ create database blog; $ \c blog; $ create table blog_post_b (id serial primary key, title text, body text);دیتابیس و جدول تستی رو سرور فرعی ایجاد شده. نکته مهم این که اسم جدول در دیتابیس فرعی باید با اسم جدول در دیتابیس اصلی متفاوت باشه. برای همین اسم جدول رو blog_post_b گذاشتیم.مجدد به دیتابیس اصلی بر می‌گردیم و با دستورات زیر fdw رو راه اندازی می‌کنیم. ابتدا افزونه fdw رو برای دیتابیس blog فعال می‌کنیم:$ \c blog; $ create extension postgres_fdw;بعد دیتابیس فرعی B رو با استفاده از fdw در دیتابیس اصلی به عنوان یک دیتابیس خارجی ایجاد میکنیم. دیتابیس اصلی دیتابیس فرعی رو به عنوان یک symlink یا shortcut در کنار خودش می‌بینه:$ create server master_server foreign data wrapper postgres_fdw options (host '{ip of server B}', port '5432' , dbname 'blog');با دستور زیر نام کاربری و پسورد دیتابیس فرعی رو جهت اتصال از دیتابیس اصلی وارد می‌کنیم:$ create user mapping for '{local postgres user}' server master_server options (user '{username of postgres on B}', password '{password of postgres on B}');دسترسی دیتابیس خارجی ایجاد شده رو به کاربر فعلی میدیم:$ alter server master_server owner to '{local postgres user}';در نهایت جدول blog_post_b که در دیتابیس فرعی وجود داره رو به عنوان یک جدول خارجی در دیتابیس اصلی وارد میکنیم:$ create foreign table blog_post_b () inherits(blog_post) server master_server;در دستور بالا گفتیم جدول خارجی blog_post_b از جدول اصلی blog_post ارث بری می‌کنه که هم باعث میشه ساختار جدول جدید با جدول اصلی یکی باشه و هم کوئری‌هایی که روی جدول اصلی زده میشه هم در جدول اصلی و هم وارث‌های اون یعنی دیتابیس B انجام بشه.دوباره کوئری رو اجرا میکنیم:$ explain select * from blog_post; QUERY PLAN ---------------------------------------------------------------------------- Append (cost=0.00..136.70 rows=891 width=68 -> Seq Scan on blog_post (cost=0.00..0.00 rows=1 width=68) -> Foreign Scan on blog_post_b (cost=100.00..136.70 rows=890 width=68)همون طور که می‌بینید تنظیمات به درستی انجام شده و هر کوئری که روی جدول blog_post دیتابیس اصلی انجام میشه روی دیتابیس اصلی و دیتابیس فرعی انجام میشه و در نهایت دیتای تجمیع شده به کاربر داده میشه.همچنین کاربر نیازی نداره تا دیتا رو join کنه و متوجه معماری و شبکه دیتابیس توزیع شده نخواهد شد.در ادامه میتونیم trigger هایی برای postgres تعریف کنیم تا دیتا رو بر اساس دیتای ورودی بین دیتابیس‌ها پخش کنه و یک معماری distributed کامل رو توسط fdw پیاده سازی کنیم که با توجه به شرایط پروژه ما نیازی به انجام این کار نبود.