بمب ساعتیِ شمارهی تراکنش
شمارهی تراکنش (xid) در پستگرس ۳۲ بیتی است: حدود چهار میلیارد مقدار که به صورت دایرهای استفاده میشود؛ هر تراکنش دو میلیارد شمارهی قبل از خودش را «گذشته» و دو میلیارد بعد را «آینده» میبیند. اگر ردیفی خیلی قدیمی شود و فکری به حالش نشود، ناگهان «در آینده» قرار میگیرد و نامرئی میشود. برای جلوگیری، VACUUM ردیفهای قدیمی را freeze میکند: علامتی که میگوید «این ردیف برای همه قابل مشاهده است، شمارهاش را نادیده بگیر».
اگر freeze عقب بیفتد، پستگرس ابتدا در لاگ هشدار میدهد و وقتی فقط چند میلیون شماره باقی مانده باشد، برای حفاظت از داده دیگر هیچ تراکنش نوشتنی را نمیپذیرد. این یکی از معدود حالتهایی است که دیتابیس سالم عملاً از کار میافتد.
-- چقدر به مرز نزدیکیم؟ (حدود 2.1 میلیارد مرز است)
SELECT datname, age(datfrozenxid) AS xid_age,
round(100.0 * age(datfrozenxid) / 2147483647, 1) AS pct
FROM pg_database ORDER BY 2 DESC;
SELECT c.oid::regclass AS table_name, age(c.relfrozenxid) AS xid_age,
pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_class c
WHERE c.relkind IN ('r', 'm', 't')
ORDER BY age(c.relfrozenxid) DESC LIMIT 10;
autovacuum وقتی سن جدول از autovacuum_freeze_max_age (پیشفرض ۲۰۰ میلیون) بگذرد، حتی اگر autovacuum خاموش باشد، یک vacuum «to prevent wraparound» اجرا میکند. در pg_stat_activity آن را با همین عبارت میبینید؛ آن را kill نکنید، دوباره شروع میشود و کار از اول.
چه چیزی جلوی VACUUM را میگیرد؟
VACUUM فقط نسخههایی را پاک یا freeze میکند که از قدیمیترین snapshot فعال در کل کلاستر قدیمیتر باشند. یک چیز کهنه کافی است تا همهچیز گیر کند:
| عامل | کجا ببینیم |
|---|---|
| تراکنش طولانی یا «idle in transaction» | pg_stat_activity، ستونهای xact_start و backend_xmin |
| replication slot رهاشده | pg_replication_slots، ستونهای active و xmin |
| prepared transaction فراموششده | pg_prepared_xacts |
| standby با hot_standby_feedback = on و کوئری طولانی | pg_stat_replication، ستون backend_xmin |
SELECT pid, usename, application_name, state,
now() - xact_start AS xact_age, backend_xmin, left(query, 60) AS last_query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start LIMIT 10;
SELECT pg_terminate_backend(12345); -- با احتیاط، پس از بررسی
«idle in transaction» یعنی برنامه BEGIN زده، کاری کرده و بعد مشغول چیز دیگری شده (مثلاً منتظر پاسخ یک وبسرویس پرداخت یا کاربر). این اتصال قفلهایش را نگه میدارد و جلوی VACUUM کل کلاستر را میگیرد.
timeoutها: کمربند ایمنی
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '60s';
ALTER ROLE app_user SET statement_timeout = '30s';
ALTER ROLE report_user SET statement_timeout = '10min';
ALTER ROLE app_user SET lock_timeout = '5s';
-- نسخهی 17 به بعد: سقف عمر کل تراکنش
ALTER ROLE app_user SET transaction_timeout = '2min';
تنظیم روی role بهتر از تنظیم سراسری است: گزارشگیر شبانه و pg_dump نباید با timeout سیثانیهای برنامه قطع شوند.
نکتههایی که کمتر کسی میداند
- از نسخهی 14، وقتی سن جدول به
vacuum_failsafe_age(۱.۶ میلیارد) برسد، VACUUM حالت اضطراری میگیرد: محدودیت سرعت (cost delay) و پاکسازی ایندکس را رها میکند تا فقط freeze را زودتر تمام کند. - جدولهای TEMP هرگز توسط autovacuum پردازش نمیشوند؛ نشستی که روزها باز است و جدول موقت بزرگی دارد، میتواند در سن xid سهم داشته باشد.
idle_session_timeout(نسخهی 14) اتصالهای بیکار را میبندد؛ آن را پشت PgBouncer یا pool جنگو فعال نکنید، چون pool از بسته شدن بیخبر است و با خطای «connection closed» روبهرو میشود.- statement_timeout را سراسری در
postgresql.confنگذارید: روی migrationها،CREATE INDEXدستی و اسکریپتهای نگهداری هم اثر میکند. autovacuum و pg_dump خودشان آن را صفر میکنند، اما ابزارهای دستی شما نه. SET LOCAL statement_timeout = '2s'فقط تا پایان همان تراکنش اعتبار دارد؛ راهی امن برای محدود کردن یک کوئری خاص در برنامه بدون اثر روی بقیهی اتصالها.