Checkpoint ও Vacuum — জমাট বাঁধা housekeeping
Checkpointing and Vacuum
Recovery-র জানালা ছোট রাখতে checkpoint, MVCC-র ময়লা সাফাতে vacuum — দুটোই background housekeeping, দুটোই নিজের নিজের trade-off-এ ভরা।
আগে এটা বুঝি
আগের লেসনের শেষ প্রশ্নটা মনে আছে? Recovery-র খরচ নির্ধারণ করে একটাই সংখ্যা — শেষ checkpoint থেকে crash পর্যন্ত কতটা log। আর আগের-আগের লেসনের MVCC আমাদের দিয়েছে অবিশ্বাস্য concurrency, কিন্তু একটা নীরব দামে: DELETE আসলে কিছুই মুছে না, UPDATE আসলে দুটা version লেখে। সময়ের সাথে টেবিলের ভেতর জমতে থাকে মৃত tuple-দের কবরস্থান।
এই দুটো সমস্যার মিল হলো — দুটোর সমাধানই background housekeeping: কেউ দেখছে না এমন সময়ে ধীরে ধীরে কাজ করা, যাতে মুহূর্ত-বিশেষে (crash, wraparound) বড় বিল না দিতে হয়।
- Checkpoint: recovery-র জানালাটা নিয়মিত ছোট করা — dirty page flush করে log-এ একটা marker লেখা
- VACUUM: মৃত tuple reclaim করা, xid-দের freeze করে wraparound-এর ভবিষ্যৎ দুর্যোগ আগেই নির্নীত করা
দুটোই “কাজের ফাঁকে ঝাড়ু দেওয়া” — কিন্তু ঝাড়ুটা কখন, কতটা জোরে, কেন এভাবে — এই প্রশ্নগুলোর উত্তরই production database tuning-এর এক বড় অংশ।
মূল ধারণা
Checkpoint — recovery-র জানালা ছোট করা
WAL লেসনে দেখেছি recovery মানে শেষ থেকে সব replay। Checkpoint ছাড়া সেই “শেষ” মানে database-এর জন্ম — uptime যত লম্বা, restart তত যুগান্তকালীন। Checkpoint-এর কাজ সেই জানালাটা নিয়মিত ঘষে ছোট রাখা:
- Buffer pool-এর সব dirty page disk-এ flush করা
- Log-এ একটা CHECKPOINT record লেখা — “এই LSN-এর সব data page এখন disk-এ”
Crash-এর পরে recovery শুধু শেষ completed checkpoint-এর CHECKPOINT record থেকে replay শুরু করে — তার আগের history আর দরকার নেই, কারণ সেই প্রভাবগুলো disk-এ already durable।
কম্পাঙ্কের দুই-দিক টান
| Checkpoint ঘন | ভালো দিক | খারাপ দিক |
|---|---|---|
| বেশি ঘন | recovery window ছোট — crash-এ কম replay | প্রতিবার প্রচুর dirty page burst-write |
| কম ঘন | normal operation মসৃণ | crash-এর পরে লম্বা replay |
PostgreSQL-এর সমাধান: spike-টাকেই spread করা।
checkpoint_timeout (default ৫ মিনিট) বা max_wal_size ছুঁলে
checkpoint শুরু হয়, কিন্তু flush-টা এক ঝলকে নয় —
checkpoint_completion_target (default 0.9) অংশ সময় জুড়ে ধীরে
ধীরে ছড়িয়ে দেওয়া হয়। অর্থাৎ পরের checkpoint-এর প্রায় ৯০%
সময় নিয়ে বর্তমানটার ময়লা ফেলা — OS-এর কাছে write-load সর্বদা
প্রায়-সমান, peak নেই।
full_page_writes — torn page-এর বিরুদ্ধে সস্তা ঢাল
WAL লেসনে torn-page দেখেছি — ৮ KB page-এর অর্ধেক sector লেখা হয়ে power গেলে page corrupt। প্রতিটা লেখার আগে পুরো page-এর checksum-safe কপি log করা হলে WAL বিশাল হয়ে যায়। PostgreSQL-এর চতুর সমাধান:
- Checkpoint-এর ঠিক পরে, প্রতিটা page-এর প্রথম
modification-এ পুরো page-এর image WAL-এ লেখা হয় (FPW —
pg_waldump output-এ যে
FPWflag দেখেছিলেন, এটাই) - তার পরে সেই page-এর সব পরিবর্তন শুধু delta — record-ই ছোট
যুক্তিটা: FPW থাকলে torn page-ও নিরাপদ — recovery-তে page-টা আগে পুরো-image দিয়ে restore হয়, তারপর delta-গুলো replay। Checkpoint এখানে চাবিকাঠি: এটা সেই মুহূর্ত, যেখান থেকে “disk- এর সব page সুস্থ” guarantee নতুন করে শুরু। এজন্যই FPW-র হিসাব checkpoint-boundary-তে বসে।
ভেতরে কী ঘটছে
VACUUM — MVCC-র ময়লার ভাগ্যকর্তা
MVCC লেসনের promise মনে করুন: reader আর writer একে অপরকে ব্লক করে না, কারণ পুরনো version-গুলো রয়ে যায় — xmin/xmax visibility-র বিচারে। কিন্তু পুরনো version কখন ফেরত পাওয়া যাবে? যখন প্রমাণ হয় কোনো চলমান transaction-ই ওগুলো আর দেখতে পাবে না। সেই প্রমাণের নাম VACUUM। তিনটা দায়িত্ব:
১. Dead tuple reclaim
DELETE-কৃত বা overwrite-হওয়া tuple যাদের কেউ আর দেখবে না, ওদের জায়গাটা page-এর ভেতরেই free space হিসেবে ফেরত পাওয়া হয় (FSM — free space map-এ নথিভুক্ত)। লক্ষণীয়: এটা ফাইল ছোট করে না, ফাইলের ভেতরে জায়গা recycle করে — ভবিষ্যৎ INSERT-রা নতুন disk block না নিয়ে এই ফাঁক ব্যবহার করবে।
২. Freeze — wraparound-এর রক্ষাকবচ
Transaction ID ৩২ bit — প্রায় ৪২০ কোটি। বাড়তে বাড়তে ঘুরে (wrap) গেলে “অতীত” xid হঠাৎ “ভবিষ্যৎ”-এর চেয়ে বড় দেখাবে, আর visibility-র গণিত উল্টে যায় — পুরনো data হঠাৎ invisible বা mutable! প্রতিরোধ: বহু পুরনো tuple-দের xmin বদলে বিশেষ “frozen” mark দেওয়া হয় — “এরা সবসময় past, আর তুলনা লাগবে না”। Anti-wraparound autovacuum এই কাজটা জোর করে চালায় — এটা skip করা যায় না, আর skip করার চেষ্টা করলে database shutdown পর্যন্ত যেতে পারে। Production incident-report-এ এই শ্রেণির বিপদ বহুল।
৩. Visibility map
প্রতিটা page-এর জন্য দুই bit: “সব tuple সবার কাছে visible” আর “সব frozen”। Index-only scan আর vacuum নিজেই এই map-এর উপর দাঁড়িয়ে — map যত পরিপাটি, index-only scan তত কার্যকর।
Autovacuum — কখন জাগে, কতটা সাবধানে
Trigger formula প্রতিটা table-এর জন্য:
dead_tuples > autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor × live_tuplesDefault: threshold=50, scale_factor=0.2 — অর্থাৎ এক কোটি
row-এর table জাগে যখন প্রায় ২০ লক্ষ dead tuple। বড় table-এ
এই ২০% নিজেই সমস্যা হতে পারে, তাই per-table override সাধারণ
practice।
জাগার পরে সে লোভী হয় না — cost-based throttle: প্রতিটা
buffer-read/page-write-এর একটা imagined-cost, মোট cost যখন
vacuum_cost_limit ছাড়ায়, তখন vacuum_cost_delay (মিলিসেকেন্ড)
ঘুমায়। ফল: vacuum OLTP traffic-এর সাথে coexist করে, কেউ কারো
পায়ে চাপে না।
VACUUM বনাম VACUUM FULL
| সাধারণ VACUUM | VACUUM FULL | |
|---|---|---|
| কাজ | dead-space reclaim (ফাইলের ভেতরে) | পুরো table rewrite, কমপ্যাক্ট |
| Lock | সাধারণ কাজের সাথে চলে | ACCESS EXCLUSIVE — সব ব্লক |
| ফাইল সাইজ | কমে না (বেশিরভাগ ক্ষেত্রে) | কমে |
| খরচ | ধীর কিন্তু অনলাইন | দ্বিগুণ disk + downtime |
তাই VACUUM FULL হলো emergency surgery — routine maintenance নয়। Routine-এর উত্তর সাধারণত: ভালো autovacuum tuning + fillfactor।
উদাহরণ
একটা table-এর জীবন — জন্ম থেকে সমাধি
t0: CREATE TABLE orders(...); INSERT ১০ লক্ষ row
-> ১২২,০০০ page, সব live
t1: UPDATE orders SET status='shipped' WHERE status='paid' (৩ লক্ষ row)
-> MVCC: পুরনো version মরে না — xmax বসে; নতুন ৩ লক্ষ version
নতুন জায়গায়; table-এর ভেতর ৩ লক্ষ dead tuple
-> n_dead_tup ≈ ৩০০,০০০; table size বেড়ে ১৫৮,০০০ page
t2: চলমান long report query পুরনো snapshot-এ এখনও t1-এর আগের
অবস্থা দেখছে — dead tuple-রা এখনও "কারো কাছে দৃশ্যমান"
t3: report শেষ -> xmin horizon এগোল -> autovacuum জাগল
(formula: ৩ লক্ষ > 50 + 0.2×১০ লক্ষ ✓)
-> dead tuple-দের জায়গা FSM-এ ফেরত; ফাইল তবু ১৫৮,০০০ page-ই
-> visibility map: ৬০% page-এ "all-visible" bit বসল
-> পুরনো tuple-দের অনেকেই freeze হলো (wraparound হিসাবের খাতায়)
t4: নতুন INSERT-রা FSM-এর ফাঁক ব্যবহার করে — ফাইল বাড়ছে ধীরেআর checkpoint-side-এ একই সময়ে চলছিল:
12:00 CHECKPOINT done (marker LSN = 0/20000000)
12:00-12:05 হাজার লেখা; P7 page প্রথম ছোঁয়া -> FPW (৮KB image!)
P7-এর পরের ৯৯৯টা পরিবর্তন -> ছোট delta records
12:04:30 max_wal_size ছুঁল -> checkpoint শুরু
12:04:30-12:09:00 dirty pages ছড়িয়ে flush (completion_target=0.9)
12:09:00 CHECKPOINT record @ 0/24000000
=> পরের crash-এ recovery replay শুরু: 0/24000000 থেকে — মাত্র
৫ মিনিটের log, ১২ ঘণ্টার নয়লক্ষ্য করুন দুটো সমান্তরাল গল্পই “মুহূর্তের চাপ ফেলে সময়ে ছড়ানো” — এটাই housekeeping-এর মূলমন্ত্র।
ছবিতে
দুইটা চক্র, পাশাপাশি
LSN ──────────────────────────────────────────────────────────►
│CKPT₁ │CKPT₂ │CKPT₃
▼ ▼ ▼
╔═════════════════════╗ ╔═════════════════════╗
║ FPW: P3,P17,P42... ║ ║ FPW: P3,P9,... ║ প্রতি চক্রের শুরুতে
║ তারপর deltas only ║ ║ তারপর deltas only ║ first-touch = image
╚═════════════════════╝ ╚═════════════════════╝
Crash এখানে ────────► ▲
│
Recovery replay: CKPT₂ থেকে এখানে — জানালা = শুধু এই অংশ
(CKPT₁-এর আগের সব history অপ্রাসঙ্গিক)INSERT UPDATE সব old-reader শেষ VACUUM
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ v1 live │──►│ v1: xmax set │──►│ v1 dead, কিন্তু │──►│ v1-এর জায়গা │
│ │ │ v2 live │ │ জায়গা দখল করে │ │ FSM-এ free │
└─────────┘ └──────────────┘ │ আছে (bloat) │ │ নতুন insert │
└──────────────────┘ │ ওখানেই বসে │
└──────────────┘
"delete" কখনো মুহূর্তে ঘটে না — মৃত্যু-প্রমাণের জন্য
পুরো ডাটাবেজের সবচেয়ে পুরনো snapshot-এর মৃত্যু লাগেনিজে চালিয়ে দেখুন
pg_stat_bgwriter, dead-tuple watch, আর autovacuum report decode
ধাপ ১ — checkpoints: timed না req?
-- PG 15 বা আগে:
SELECT checkpoints_timed, checkpoints_req FROM pg_stat_bgwriter;
-- PG 16+:
SELECT num_timed, num_requested FROM pg_stat_checkpointer;timed = checkpoint_timeout-এ নিজে থেকে হলো; req = কেউ
বাধ্য করেছে (CHECKPOINT command, বা max_wal_size overflow —
PG16-তে সেটা আলাদা গোনা হয়)। Healthy server-এ req ছোট থাকা
কাম্য — ঘন-ঘন requested checkpoint মানে WAL-volume বা sizing
সমস্যা।
ধাপ ২ — dirty checkpoint-এর দাম মাপা:
\timing on
CREATE TABLE big AS
SELECT g, md5(g::text) AS payload FROM generate_series(1, 4000000) g;
CHECKPOINT; -- পরিষ্কার: মিলিসেকেন্ড
UPDATE big SET payload = payload; -- ৪ লক্ষ row rewrite -> সব dirty!
CHECKPOINT; -- dirty: সেকেন্ড-ব্যাপী flush burst
CHECKPOINT; -- আবার পরিষ্কার: প্রায় বিনা মূল্যে
DROP TABLE big;প্রথম আর তৃতীয় CHECKPOINT কার্যত তাৎক্ষণিক, মাঝেরটা লম্বা —
এই পার্থক্যই buffer-flush-এর খরচ, যেটা production-এ
completion-target ছড়িয়ে দেয়।
ধাপ ৩ — dead tuple-এর উত্থান-পতন:
CREATE TABLE churn AS SELECT g, md5(g::text) v
FROM generate_series(1, 200000) g;
ANALYZE churn;
SELECT relname, n_live_tup, n_dead_tup
FROM pg_stat_user_tables WHERE relname='churn';
DELETE FROM churn WHERE g % 2 = 0;
ANALYZE churn; -- estimator টাটকা হলো
SELECT n_live_tup, n_dead_tup
FROM pg_stat_user_tables WHERE relname='churn';
-- n_dead_tup ≈ ১০০,০০০ — ফিজিক্যাল কবরস্থান, এখনও জায়গা দখলে
VACUUM churn;
SELECT n_dead_tup FROM pg_stat_user_tables WHERE relname='churn';
-- 0 — reclaim হয়েছে; লক্ষ্য করুন table size কমেনি!
SELECT pg_total_relation_size('churn');ধাপ ৪ — vacuum-এর অন্দরমহল report:
ALTER SYSTEM SET log_autovacuum_min_duration = 0;
SELECT pg_reload_conf();
VACUUM (VERBOSE, ANALYZE) churn;Log/stderr-এ এই জাতীয় line:
automatic aggressive vacuum of table "db.public.churn": ...
pages: 0 removed, 1306 remain, 0 skipped due to pins
tuples: 99,984 removed, 0 remain are nonremovable
avg read rate: 12.3 MB/s, avg write rate: 4.1 MB/s
frozen: 102 pages, buffer usage: 1187 hits, 214 missespages removed— VACUUM ফাইল shrink করে না (0-ই স্বাভাবিক); জায়গা ভেতরে recycle হয়tuples removed— আসল reclaimnonremovable— যারা এখনো কোনো snapshot-এ দৃশ্যমান (long transaction-এর অস্তিত্বের প্রমাণ)frozen— wraparound-প্রস্তুতি
এই এক line-ই লেসনের hood-section-এর তিন দায়িত্বের receipt।
Checkpointer আর autovacuum-এর কাজ সবই counter আর log-এ জমা থাকে — checkpoint timed-vs-req, dead-tuple উত্থান-পতন, vacuum-এর ভেতরের হিসাব — সব পড়া যায়, অনুমান করতে হয় না।
নিজে বানান
CheckpointSimulator — interval বনাম replay-cost-এর sweet spot
- Event loop চালান — প্রতি tick-এ commit + buffer dirty
- নির্দিষ্ট interval-এ checkpoint: সব dirty flush + marker
- Random মুহূর্তে crash sample নিন — replay length = crash_lsn − last_ckpt_lsn
- IO খরচও গুনুন — মোট flushed page
- interval sweep করে টেবিল ছাপুন — sweet spot নিজের চোখে
লক্ষ্য: লেখায় যে দুই-দিক টানটা বর্ণনা করেছি, সেটা সংখ্যায় প্রমাণ — ছোট interval মানেই সস্তা recovery কিন্তু বিশাল flush-bill।
import random
def simulate(interval, horizon=8000, seed=7):
"""interval tick পরপর checkpoint; ফেরত: replay-lengths, মোট flush"""
rng = random.Random(seed)
dirty: set[int] = set()
last_ckpt = 0
lsn = 0
flushed = 0
replay = []
for tick in range(horizon):
# এক tick = কিছু commit + কিছু page dirty (workload-এর অভিনয়)
for _ in range(rng.randint(2, 6)):
lsn += 1
dirty.add(rng.randint(0, 255))
if tick % interval == 0 and dirty:
flushed += len(dirty)
dirty.clear() # flush-all + CHECKPOINT marker
last_ckpt = lsn
if rng.random() < 0.004: # crash sampling (~৩২টা crash)
replay.append(max(0, lsn - last_ckpt))
last_ckpt = lsn # restart-এ টাটকা checkpoint
return replay, flushed
rows = []
for iv in (10, 20, 40, 80, 160, 320):
rp, fl = simulate(iv)
rows.append((iv, sum(rp) / len(rp), max(rp), fl))
print(f"{'ckpt-interval':>14} {'গড় replay':>12} {'সর্বোচ্চ':>10} "
f"{'মোট flushed':>12}")
for iv, avg, worst, fl in rows:
print(f"{iv:>14} {avg:>12.0f} {worst:>10} {fl:>12}")
best = min(rows, key=lambda r: r[1] * 0.02 + r[3] / 100000)
print(f"\nsweet-spot অনুমান (weighted): interval={best[0]}")প্রত্যাশিত output-এর আকার (seed অনুযায়ী সংখ্যা সামান্য বদলাবে):
ckpt-interval গড় replay সর্বোচ্চ মোট flushed
10 18 62 51200
20 38 121 26100
40 79 236 13400
80 161 458 6900
160 330 921 3600
320 671 1839 1900
sweet-spot অনুমান (weighted): interval=40প্যাটার্ন পড়ুন: interval দ্বিগুণ হলে গড় replay-ও প্রায় দ্বিগুণ
(linear), কিন্তু flush-bill অর্ধেক — দুই curve-এর যোগফলেই মাঝে
গর্ত (sweet spot)। আরও মজার: max replay দেখায় tail-risk —
mean-based SLA-তে যে interval ভালো লাগে, worst-case-এ সেটা
ভয়ংকর হতে পারে।
নিজে বাড়ান:
- Flush-কে spread করুন — checkpoint-এর খরচ
completion_targetfraction-এ ভাগ করে per-tick budget; IO-spike column যোগ করুন - Crash-frequency workload-ভেদে বদলান — কম crash-prone সিস্টেমে sweet spot কোন দিকে সরে?
- WAL-volume-triggered checkpoint যোগ করুন —
lsn - last_ckptএক threshold ছুঁলেই force; বাস্তবmax_wal_size-এর অনুকরণ
বাস্তব সিস্টেমে
বাস্তব জগতে housekeeping
Wraparound-এর বাস্তব দাঁত। PostgreSQL-এর সবচেয়ে বিখ্যাত
outage-class-গুলোর একটা: বহু বছর অবহেলিত database-এ
anti-wraparound autovacuum বিশাল table-এ আটকে যাওয়া, বা xid
সীমায় পৌঁছে forced shutdown। Monitoring-এ age(datfrozenxid)
আর n_dead_tup তাই প্রথম সারির metric।
Bloat horror stories। High-churn table-এ ৯০%-এর বেশি জায়গা
dead tuple-এর — query সবসময় মৃতদের ভিড় পেরিয়ে live-দের খোঁজে,
index-ও ফুলে ওঠে। চিকিৎসা: per-table autovacuum scale factor
কমানো, fillfactor রেখে HOT update-এর সুযোগ, শেষ ভরসা pg_repack-
জাতীয় online-rewrite।
InnoDB-র সমান্তরাল জগত। MySQL-এর fuzzy checkpoint, purge
thread (vacuum-এর সমতুল্য undo-purge), আর history list length —
concept-by-concept মিলে যায়; innodb_max_dirty_pages_pct এখানে
completion-target-এর চাচাতো ভাই।
SQL Server আর Oracle। Lazywriter/checkpoint থ্রেড, indirect checkpoint (SQL Server 2012+) আসলে recovery-window target দিয়ে interval auto-decide করে — এই লেসনের trade-off-টাকেই parameterize করা।
Cloud-managed databases। RDS/Aurora-র dashboard-এ checkpoint duration, replica lag-এর সাথে vacuum stats — একই অন্ত্র, managed UI-তে মোড়া। ভেতরটা না বুঝলে alert-গুলো শুধু সংখ্যা।
যে ভুলগুলো সবাই করে
“VACUUM চালালে table ছোট হয়, disk space ফেরত আসে।”
সাধারণ VACUUM ফাইল কমায় না — ভেতরের dead-space recycle করে,
যাতে ভবিষ্যৎ growth ধীর হয়। ফাইল shrink করতে হলে VACUUM FULL /
pg_repack — যেটা exclusive-lock বা rewrite-এর দাম চায়। Experiment
section-এ আপনি নিজেই দেখেছেন: tuples removed: 99,984, তবু size
অপরিবর্তিত।
“Autovacuum বন্ধ রাখা ভালো — ওটা performance নষ্ট করে।”
বন্ধ autovacuum-এর বিল পরে আসে: bloat বাড়ে (query ধীর), index-only scan মরে (visibility map পুরনো), আর সবচেয়ে ভয়ের কথা wraparound shutdown। Cost-based delay আছে বলেই এটা OLTP-র সাথে মেলে; সমস্যা হলে tuning, disable নয়।
“Checkpoint ঘন করলেই recovery দ্রুত হবে — শুধু লাভ।”
Build section-এর টেবিলটা মনে করুন: ঘন checkpoint মানে নিয়মিত write-storm, shared_buffers-এর hot data বারবার disk-এ ধাক্কা, OLTP latency-tail লম্বা। Spreading (completion_target) spike কমায়, শূন্য করে না। Sweet spot workload-dependent — universal উত্তর নেই।
“Long-running query "free" — ওটা শুধু পড়ছে, ক্ষতি করার কথা না।”
Read-ও দাম দেয় MVCC-জগতে: পুরনো snapshot বাঁচিয়ে রাখা query-র
xmin-horizon আটকে রাখে — vacuum ওই বয়সী dead tuple reclaim করতে
পারে না। ৬ ঘণ্টার report query মানে ৬ ঘণ্টা bloat-growth। এজন্যই
idle_in_transaction_session_timeout আর long-query alerting
production hygiene।
বুঝেছেন কি না দেখুন
1Checkpoint-এর দুইটা কাজ আর crash-recovery-তে তার একটাই প্রভাব —
কোনটা?
স্মরণ
কাজ: (১) সব dirty page flush, (২) log-এ CHECKPOINT marker। প্রভাব: recovery-র starting point — replay শুরু হয় শেষ completed checkpoint থেকে। তার আগের log আর পড়া লাগে না।
2full_page_writes কেন checkpoint-boundary-তে বাঁধা? Checkpoint-
এর পরে page P-এ ৫০০টা পরিবর্তন হলে WAL-এ কী যায় — প্রথমটার
জন্য কী, বাকি ৪৯৯-এর জন্য কী?
যুক্তি
full_page_writes কেন checkpoint-boundary-তে বাঁধা? Checkpoint-
এর পরে page P-এ ৫০০টা পরিবর্তন হলে WAL-এ কী যায় — প্রথমটার
জন্য কী, বাকি ৪৯৯-এর জন্য কী?কারণ FPW-র উদ্দেশ্য torn-page থেকে বাঁচা, আর checkpoint-ই সেই মুহূর্ত যেখান থেকে “disk-এর সব page অক্ষত” guarantee টাটকা হয় — boundary-র আগের FPW তখন অপ্রয়োজনীয়। প্রথম পরিবর্তনে পুরো ৮ KB image WAL-এ (FPW); বাকি ৪৯৯টা শুধু delta record — bytes-এ ছোট। Torn হলে recovery আগে image restore করে, তারপর delta replay।
3Table A: ১০,০০০ live row; Table B: ১ কোটি live row। Default scale
factor 0.2 হলে কোনটা কত dead tuple-এ autovacuum ডাকবে? বড় table-
এ এই default কেন প্রায়ই সমস্যা?
প্রয়োগ
Formula: 50 + 0.2 × live। Table A: 50 + 2000 = 2050; Table B:
50 + ২০ লক্ষ = ২০ লক্ষ। বড় table-এ ২০% dead জমা থাকা মানেই
বিশাল bloat — index scan সেই মৃত-ভিড়ও ছুঁয়ে যায়, cache দূষিত হয়।
তাই বড় churn-table-এ per-table autovacuum_vacuum_scale_factor
(যেমন 0.01) বা absolute threshold বসানো standard practice।
4Xid wraparound ঠিক কী ভুল ঘটায়? Frozen mark কীভাবে সমস্যাটা
মূল থেকে সরায়?
যুক্তি
Visibility-র নিয়ম: “xmin future ⇒ tuple এখনো দেখা যায় না”। Wrap- এর পরে প্রাচীন tuple-এর xid তুলনায় future-এর মতো দেখায় — তাই বহু বছরের পুরনো data হঠাৎ invisible/overwritable হয়ে যায় — silent data loss। Frozen mark tuple-টাকে “সবসময় past” বলে hardcode করে — আর কোনো xid-তুলনা-ই লাগে না, তাই wrap তাকে ছুঁতে পারে না।
5Production-এ nightly batch চলে — ৩ ঘণ্টা ধরে এক transaction-এ
৫০ লক্ষ row delete + reinsert। দিনের বেলা team অভিযোগ: table
bloat, query ধীর। তিন-ধাপে diagnosis আর fix-এর পরিকল্পনা দিন।
ডিজাইন
(১) Diagnosis: n_dead_tup আর log_autovacuum-এর
nonremovable-count দেখুন — সন্দেহ: ৩-ঘণ্টা txn-এর xmin horizon
vacuum-কে আটকে রাখছে। (২) Immediate fix: batch-কে ছোট chunk-
txn-এ ভাগ করুন (যেমন ১০k/batch) — horizon নিয়মিত এগোবে,
autovacuum মাঝপথে কাজ করবে; প্রয়োজনে batch-এর মাঝে
vacuum_cost_delay কম per-table override। (৩) Structural:
churn-pattern অনুযায়ী scale_factor/threshold tune, fillfactor
কমিয়ে HOT-update-র জায়গা, আর extreme ক্ষেত্রে partition-drop
প্যাটার্নে delete-কে metadata-op বানানো (partitioning লেসনের
preview)।
এরপর কী
এরপর কী
WAL এখন শুধু local durability নয় — এটা একটা পাঠযোগ্য stream, আর সেই stream অন্য machine-এ পাঠালেই জন্ম নেয় replica। পরের লেসনে: streaming বনাম logical replication-এর দুই পথ, lag মাপা, replication slot-এর আশীর্বাদ-অভিশাপ, আর failover-এর বাস্তবতা — RPO/RTO, split-brain, fencing। এই লেসনের checkpoint-WAL-সম্পর্ক সেখানে আরও গভীর হবে।
আরও পড়ুন
- PostgreSQL Documentation — Routine Vacuuming — PostgreSQL Global Development Group · Autovacuum trigger, freezing, wraparound — এই লেসনের vacuum-half-এর authoritative উৎস
- PostgreSQL Documentation — Reliability and the Write-Ahead Log — PostgreSQL Global Development Group · Checkpoint, full_page_writes, async commit — wal-half-এর উৎস
- Designing Data-Intensive Applications, Chapter 7 — Transactions — Martin Kleppmann · MVCC garbage collection-এর practitioner দৃষ্টিভঙ্গি