Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ১৫/১৯অ্যাডভান্সড৫০ মিনিট

Checkpoint ও Vacuum — জমাট বাঁধা housekeeping

Checkpointing and Vacuum

Recovery-র জানালা ছোট রাখতে checkpoint, MVCC-র ময়লা সাফাতে vacuum — দুটোই background housekeeping, দুটোই নিজের নিজের trade-off-এ ভরা।

এই লেসন শেষে আপনি পারবেন

  • Checkpoint কীভাবে recovery time-কে bound করে — flush-all-dirty + WAL marker, আর recovery শুধু শেষ checkpoint থেকে replay — এই যন্ত্রটা ব্যাখ্যা করতে পারবেন
  • Checkpoint কম্পাঙ্কের দুই-দিক trade-off — ঘন হলে IO spike, দূরে হলে লম্বা recovery window — আর PostgreSQL-এর `checkpoint_completion_target` spreading দিয়ে সেই spike কীভাবে ছড়ানো হয় বলতে পারবেন
  • `full_page_writes` অপ্টিমাইজেশনটা — checkpoint-এর পরে প্রতিটা page-এর প্রথম modification-এ পুরো page image log হওয়া, তারপর delta-only — এর কারণ আর খরচ ব্যাখ্যা করতে পারবেন
  • MVCC-জগতে VACUUM-এর তিনটা দায়িত্ব — dead tuple reclaim, xid freeze করে wraparound protection, visibility map maintenance — আলাদা করে চিনতে পারবেন
  • Autovacuum-এর trigger formula আর cost-based delay knob গুলো পড়ে বলতে পারবেন কেন কোনো table ঠিক এই মুহূর্তে vacuum হলো, আর VACUUM FULL কখনো routine tool নয় কেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আগের লেসনের শেষ প্রশ্নটা মনে আছে? 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-এর কাজ সেই জানালাটা নিয়মিত ঘষে ছোট রাখা:

  1. Buffer pool-এর সব dirty page disk-এ flush করা
  2. 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-এ যে FPW flag দেখেছিলেন, এটাই)
  • তার পরে সেই 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_tuples

Default: 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

সাধারণ VACUUMVACUUM 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 অপ্রাসঙ্গিক)
Checkpoint চক্র — প্রতিটা marker পরের recovery-র শুরুর-রেখা; FPW প্রতিটা চক্রের শুরুতে page-image খরচ করে, বাকি চক্র delta-তে চলে।
INSERT            UPDATE             সব old-reader শেষ        VACUUM
   │                 │                     │                    │
   ▼                 ▼                     ▼                    ▼
┌─────────┐   ┌──────────────┐   ┌──────────────────┐   ┌──────────────┐
│ v1 live │──►│ v1: xmax set │──►│ v1 dead, কিন্তু   │──►│ v1-এর জায়গা  │
│         │   │ v2 live      │   │ জায়গা দখল করে    │   │ FSM-এ free   │
└─────────┘   └──────────────┘   │ আছে (bloat)      │   │ নতুন insert  │
                                 └──────────────────┘   │ ওখানেই বসে  │
                                                        └──────────────┘
        "delete" কখনো মুহূর্তে ঘটে না — মৃত্যু-প্রমাণের জন্য
        পুরো ডাটাবেজের সবচেয়ে পুরনো snapshot-এর মৃত্যু লাগে
Tuple-এর জীবন MVCC-তে — death দুই ধাপে: xmax বসা (logical), তারপর vacuum reclaim (physical reuse)।

নিজে চালিয়ে দেখুন

EXPERIMENT

pg_stat_bgwriter, dead-tuple watch, আর autovacuum report decode

macOS / Linux (PostgreSQL)· ১৫ মিনিট

ধাপ ১ — 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 misses
  • pages removed — VACUUM ফাইল shrink করে না (0-ই স্বাভাবিক); জায়গা ভেতরে recycle হয়
  • tuples removed — আসল reclaim
  • nonremovable — যারা এখনো কোনো snapshot-এ দৃশ্যমান (long transaction-এর অস্তিত্বের প্রমাণ)
  • frozen — wraparound-প্রস্তুতি

এই এক line-ই লেসনের hood-section-এর তিন দায়িত্বের receipt।

এটা কী প্রমাণ করে

Checkpointer আর autovacuum-এর কাজ সবই counter আর log-এ জমা থাকে — checkpoint timed-vs-req, dead-tuple উত্থান-পতন, vacuum-এর ভেতরের হিসাব — সব পড়া যায়, অনুমান করতে হয় না।

নিজে বানান

BUILD IT

CheckpointSimulator — interval বনাম replay-cost-এর sweet spot

Python · ●●○○○
  1. Event loop চালান — প্রতি tick-এ commit + buffer dirty
  2. নির্দিষ্ট interval-এ checkpoint: সব dirty flush + marker
  3. Random মুহূর্তে crash sample নিন — replay length = crash_lsn − last_ckpt_lsn
  4. IO খরচও গুনুন — মোট flushed page
  5. 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-এ সেটা ভয়ংকর হতে পারে।

নিজে বাড়ান:

  1. Flush-কে spread করুন — checkpoint-এর খরচ completion_target fraction-এ ভাগ করে per-tick budget; IO-spike column যোগ করুন
  2. Crash-frequency workload-ভেদে বদলান — কম crash-prone সিস্টেমে sweet spot কোন দিকে সরে?
  3. 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।

বুঝেছেন কি না দেখুন

1

Checkpoint-এর দুইটা কাজ আর crash-recovery-তে তার একটাই প্রভাব — কোনটা?

স্মরণ

কাজ: (১) সব dirty page flush, (২) log-এ CHECKPOINT marker। প্রভাব: recovery-র starting point — replay শুরু হয় শেষ completed checkpoint থেকে। তার আগের log আর পড়া লাগে না।

2

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।

3

Table 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।

4

Xid wraparound ঠিক কী ভুল ঘটায়? Frozen mark কীভাবে সমস্যাটা মূল থেকে সরায়?

যুক্তি

Visibility-র নিয়ম: “xmin future ⇒ tuple এখনো দেখা যায় না”। Wrap- এর পরে প্রাচীন tuple-এর xid তুলনায় future-এর মতো দেখায় — তাই বহু বছরের পুরনো data হঠাৎ invisible/overwritable হয়ে যায় — silent data loss। Frozen mark tuple-টাকে “সবসময় past” বলে hardcode করে — আর কোনো xid-তুলনা-ই লাগে না, তাই wrap তাকে ছুঁতে পারে না।

5

Production-এ 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 দৃষ্টিভঙ্গি