داستان مهاجرت ما از PostgreSQL 9.6 به 17 با pglogical: هم کمتر خوابیدیم، هم بیشتر یاد گرفتیم!

۱. داستان از کجا شروع شد؟

فکر کنید یک دیتابیس غول‌آسا دارید روی PostgreSQL 9.6 (که دیگه آخرای عمرشه!)، با جدول‌هایی که بعضی‌هاشون ۴۰، ۶۰ گیگ و حتی بیشتر حجم دارن. بعد می‌خواید علاوه بر ارتقا به نسخه‌ی ۱۷، اوبونتوی تازه ۲۴.۰۴ رو هم تست کنید. هدف: ارتقا با کمترین قطعی ممکن (Near-zero downtime)!

  • سرور قدیمی: PostgreSQL 9.6، آی‌پی: 10.10.10.50، دیتابیس: acme_db
  • سرور جدید: اوبونتو ۲۴.۰۴ با PostgreSQL 17، آی‌پی: 10.10.10.60

می‌شد با pg_upgrade سریع ارتقا داد، ولی Downtime داشت، و ما می‌خواستیم دیتابیس تا حد امکان بالا بمونه. همچنین نیاز داشتیم دیتابیس رو روی یه سرور جدید بیاریم. به این نتیجه رسیدیم که از pglogical (Logical Replication) استفاده کنیم تا انتقال داده به صورت زنده انجام بشه و در نهایت با یک لحظه‌ی کوتاه Cutover، دیتابیس رو سوئیچ کنیم.



۲. چرا pglogical؟

  1. کمترین downtime ممکن: نمیخواستیم دیتابیس ساعت ها down باشه و سرویس و business دچار مشکل بشن.
  2. ارتقا بین نسخه‌های دور (9.6 به 17): استریم رپلیکیشن عادی این فاصله رو پشتیبانی نمی‌کنه. pglogical گفت: «من هستم!»
  3. سرور و سیستم‌عامل جدید: قصد داشتیم تمام مراحل روی یک VM تر و تمیز در اوبونتوی ۲۴.۰۴ اجرا بشه.

۳. آماده‌سازی اولیه

  • روی سرور ۹.۶، به‌دلیل پایان عمر، لازم بود pglogical رو از سورس کامپایل کنیم (یا از مخازن قدیمی در دسترس).
  • در postgresql.conf سرور قدیمی (۱۰.۱۰.۱۰.۵۰):
wal_level = logical
max_wal_senders = 10
max_replication_slots = 10
  • در pg_hba.conf هم دسترسی به آی‌پی سرور جدید رو باز گذاشتیم.
  • یک کاربر رپلیکیشن با SUPERUSER ساختیم:
CREATE ROLE repuser WITH LOGIN SUPERUSER PASSWORD 'secret_password';
  • ری‌استارت PostgreSQL 9.6 تا تغییرات اعمال بشه.

روی سرور جدید (۱۰.۱۰.۱۰.۶۰) هم PostgreSQL 17 رو نصب کردیم و ماژول pglogical رو اضافه کردیم. دیتابیسی به نام acme_db ساختیم تا آماده‌ی ایمپورت باشه.


۴. نقشه کلی مهاجرت با pglogical

۱. گرفتن dump فقط اسکیما (schema-only) از سرور قدیمی و ری‌استور روی سرور جدید تا ساختار جداول و ایندکس‌ها یکسان بشه.
۲. کپی دستی جداول خیلی بزرگ (مثلاً ۶۰ گیگ) با --data-only پیش از اینکه pglogical کپی کنه (اصطلاحاً Pre-copy). اینجوری اگر خطایی وسط کار پیش بیاد، مجبور به کپی دوباره‌ی حجم عظیم نخواهیم شد.
۳. بقیه جداول کوچک/متوسط رو می‌گذاریم pglogical خودش سینک کنه.
۴. جداول بزرگ در Replication Set با synchronize_data = false اضافه می‌شن، تا فقط تغییرات جدید رو دریافت کنن، نه کپی اولیه حجیم.
۵. بعد از کامل‌شدن همگام‌سازی، Cutover می‌کنیم: یک لحظه اپلیکیشن رو از سرور قدیمی به سرور جدید وصل می‌کنیم.


۵. مراحل قدم‌به‌قدم

۵.۱. دامپ schema-only و restore

# on old server
pg_dump -h 10.10.10.50 -U repuser -s -d acme_db > acme_db_schema.sql

# on new server
psql -h 10.10.10.60 -U repuser -d acme_db -f acme_db_schema.sql

حالا ساختار جدول‌ها، ایندکس‌ها و کانسترینت‌ها در دو سرور یکسان می‌شود.

۵.۲. جدوال بزرگ رو pre-copy میکنیم

مثلاً جدول orders (~۶۰ گیگ):

# only-data dump
pg_dump -h 10.10.10.50 -U repuser -d acme_db --data-only -t public.orders > orders_data.sql

# restore on new server
psql -h 10.10.10.60 -U repuser -d acme_db -f orders_data.sql

اگر کلیدهای خارجی دارید، حتماً داده‌ی جدول والد زودتر بارگذاری شود تا خطا نگیره. این کار رو برای هر جدول حجیم تکرار کنید.

۵.۳. تنظیم pglogical در سرور قدیمی (Provider)

CREATE EXTENSION pglogical;

SELECT pglogical.create_node(
  node_name := 'acme_provider',
  dsn       := 'host=10.10.10.50 dbname=acme_db user=repuser password=secret_password'
);

۵.۴. انتخاب جداول برای Replication Set

# Add all tables by default
SELECT pglogical.replication_set_add_all_tables(
  'default', ARRAY['public'], true
);

# Remove pre-copied tables
SELECT pglogical.replication_set_remove_table('default','public.orders');
...

# Add gain but with synchronization=false
SELECT pglogical.replication_set_add_table(
 'default',
  'public.orders',
  false
);
...


۵.۵. تنظیم pglogical در سرور جدید (Subscriber)

CREATE EXTENSION pglogical;

SELECT pglogical.create_node(
  node_name := 'acme_subscriber',
 dsn       := 'host=10.10.10.60 dbname=acme_db user=repuser password=secret_password'
);

۵.۶. ساخت Subscription

SELECT pglogical.create_subscription(
 subscription_name := 'acme_sub',
  provider_dsn      := 'host=10.10.10.50 dbname=acme_db user=repuser password=secret_password',
 replication_sets  := ARRAY['default'],
  synchronize_data  := true
);
  • جداول کوچک به طور کامل کپی می‌شوند؛ جداول بزرگ که قبلاً بارگذاری کردید، فقط تغییرات جدید را می‌گیرند.

۵.۷. مانیتور کردن و بررسی

SELECT * FROM pglogical.show_subscription_status();

اگر status شد replicating، عالی است. اگر down شد، لاگ‌ها را ببینید. حواستان به wal_keep_size باشد که کم نباشد (وگرنه وسط کار لاگ پاک می‌شود).

۵.۸. نهایی‌کردن (Cutover)

1. در یک لحظه کوتاه، اپلیکیشن را یا متوقف یا به حالت Maintenance ببرید.

2. اجازه دهید چند ثانیه آخرین WALها منتقل شوند.

3. آدرس اتصال اپلیکیشن را عوض کنید به سرور ۱۰.۱۰.۱۰.۶۰ (نسخه ۱۷).
4. کار تمام است!



۶. مشکلاتی که خوردیم و درس‌هایی که گرفتیم

  1. مشکل Foreign Keyها و خالی ماندن جداول
  • اگر جدول فرزند پیش از جدول والد لود شود، خطای FK می‌دهد و آن جدول فرزند خالی می‌ماند.
  • راهکار: ترتیب صحیح لود داده، یا غیرفعال‌کردن موقت کانسترینت.


۲. مشکل“relation already exists”

  • اگر اسکیما قبلاً وجود داشته باشد و فایل دامپ شما شامل CREATE TABLE باشد، خطا می‌دهد.
  • راهکار: --data-only برای جداولی که اسکیماشان هست.


3. جداول بزرگ و سرعت

  • دامپ متنی تکی ممکن است کند باشد؛ ما از فرمت Directory + pg_restore -j 4 برای کارهای موازی سود بردیم.
  • اگر ایندکس‌های سنگین دارید، می‌توانید اول بدون ایندکس لود کنید و بعد ایندکس را بسازید.


4. مشکل WAL retention

  • همگام‌سازی ممکن است ساعت‌ها طول بکشد؛ بنابراین wal_keep_size و فضای دیسک را کم نگذارید.
  • اگر WAL زود پاک شود، Subscription وسط راه می‌شکند.


5. مشکل Subscription down

  • اغلب به‌خاطر اختلاف اسکیما، FK ناقص، یا اتصال ناکافی است. خطا را برطرف کنید و اگر لازم شد، Subscription را drop/recreate کنید.



۷. مشکلات مربوط به Sequence در حین Cutover

یکی از چالش‌های مهمی که ما تجربه کردیم، نابرابری Sequenceها پس از Cutover بود. در PostgreSQL، Sequence وظیفه‌ی تولید ID (مثلاً برای ستون‌های SERIAL/BIGSERIAL) را دارد. اگر شما با pglogical یا Dump/Restore داده‌ها را منتقل کنید، احتمالاً مقادیر واقعی ID در جداول از مقدار Sequence جلوتر یا عقب‌تر باشد. نتیجه؟

  • ممکن است بعد از Cutover، Insertهای جدید خطای duplicate key بدهند؛ چراکه Sequence عددی را ارائه می‌دهد که قبلاً در جدول وجود دارد.


راه‌حل:

۱. جستجو و شناسایی Sequenceها:

  • می‌توانید با کوئری زیر، همه‌ی جداول و ستون‌هایشان که وابسته به Sequence هستند را پیدا کنید:
SELECT
    t.relname AS table_name,
    a.attname AS column_name,
    s.relname AS sequence_name
    FROM pg_class s
    JOIN pg_depend d  ON d.objid = s.oid
    JOIN pg_class t   ON d.refobjid = t.oid
    JOIN pg_attribute a
    ON a.attrelid = t.oid
    AND d.refobjsubid = a.attnum
    WHERE s.relkind = 'S'
    AND t.relkind IN ('r','p')
    AND d.deptype = 'a'
    ORDER BY t.relname;


2. تنظیم مجدد Sequence (setval):

  • برای هر سکانس، با SELECT max(id) از جدول مرتبطش، مقدار نهایی ID را به‌دست آورده و آن را یک واحد افزایش دهید. مثل:
SELECT setval('some_table_id_seq', (SELECT COALESCE(MAX(id), 0) FROM some_table) + 1, false);
  • اگر جداول زیاد هستند، می‌توانید این را در یک بلوک DO $$ ... $$ پیاده کنید که همه را به‌صورت خودکار انجام دهد.

3. انجام این کار قبل از شروع Insertهای جدید:

  • درست بعد از Cutover و قبل از اینکه اپلیکیشن روی سرور جدید Insert بزند، Sequenceها را یکسان‌سازی کنید.
  • به این ترتیب، Sequence ها به حداکثر مقدار موجود در جدول +۱ تنظیم می‌شوند و دیگر خطای Duplicate نخواهید داشت.



۸. چند توصیه‌ی طلایی (Best Practices)

1. در محیط تست تمرین کنید.

  • با دیتای کوچک، چالش‌های FK و Sequence را در محیط آزمایشی کشف کنید.

2. جداول خیلی بزرگ را جداگانه یا در Subscription های جدا

  • یا کلاً دستی Pre-copy کنید تا اگر مشکلی شود، از صفر مجبور نباشید ۶۰ گیگ داده بکشید.

3. لاگ‌ها را جدی بگیرید

  • اگر Subscription به down رفت، سریع لاگ سرور قدیمی و جدید را بررسی کنید.

4. ایندکس‌ها و Constraints

  • در صورت نیاز، برای سرعت بیشتر ابتدا بدون آن‌ها لود کنید و سپس بسازید/فعال کنید.

5. همگام سازی و Sequence Sync

  • بعد از پایان انتقال داده، Sequenceها را با setval یکسان کنید تا با MAX(id)+1 همخوانی داشته باشند.



۹. جمع‌بندی

به کمک این رویکرد توانستیم دیتابیس ۹.۶ را تقریباً بدون Downtime به نسخه ۱۷ ببریم و همزمان یک سرور Ubuntu 24.04 جدید راه بیندازیم. چالش عمده شامل FKها، WAL retention و مخصوصاً Sequenceها بود. در نهایت با Pre-copy جداول بزرگ، مدیریت درست FKها و تنظیم Sequenceها، مهاجرت با موفقیت و کمترین اختلال انجام شد.

امیدواریم این تجربه که قدم‌ به‌قدم و عملی بیان شد، بتواند برای دیگران هم مفید باشد و در ارتقاهای بعدی، کابوسی به نام “از صفر شروع‌کردن” یا “داون‌تایم طولانی” را از شما دور کند!