সরাসরি পর্বে যান
পর্ব ৪৮ · ৬৭-এর মধ্যে · মডিউল ৭

Recovery — log, WAL, checkpoint আর ARIES

Crash Recovery — Logging, Write-Ahead Logging, Checkpoints and ARIES

Taught in Bangla, this lesson covers crash recovery: failure and storage types, how the buffer pool forces steal and no-force decisions, shadow paging and SQLite's rollback journal, write-ahead logging with log records and LSNs, deferred versus immediate update and when to redo or undo, checkpoints, the ARIES analysis, redo and undo phases, media recovery, and fsync and synchronous commit in real systems.

এই পর্বে যা শিখবেন

  • কোন ব্যর্থতায় কী হারায়, আর “stable storage” বলতে কী বোঝায়
  • Steal/no-steal আর force/no-force — কোন জোড়ায় UNDO, কোনটিতে REDO লাগে, আর কেন
  • Shadow paging, SQLite-এর rollback journal আর write-ahead logging-এর নিয়ম ও প্রমাণ
  • Log record, LSN, pageLSN, group commit আর checkpoint — লাইভ SQL-এ log টেবিল থেকে undo/redo-র তালিকা বের করা
  • ARIES-এর পুরো উদাহরণ, আর PostgreSQL, InnoDB ও SQLite আসলে কী করে — fsync-এর ফাঁদসহ

১ · কী ভাঙতে পারে — ব্যর্থতা আর স্টোরেজের ধরন

পর্ব ৪৪-এ ACID-এর দুটি প্রতিশ্রুতি দেখেছেন: atomicity (হয় পুরো কাজ, নয় কিছুই না) আর durability (commit হলে আর হারাবে না)। এই দুই প্রতিশ্রুতি রাখার দায়িত্ব DBMS-এর recovery manager-এর। আর সে কাজটি করে এমন সময়ে, যখন সবকিছু ভেঙে পড়েছে।

bKash-এ রহিম করিমকে ৫০০ টাকা পাঠালেন। ডেটাবেসকে দুটি কাজ করতে হবে: রহিমের ব্যালান্স কমানো, করিমেরটা বাড়ানো। ঠিক মাঝখানে সার্ভার রুমে বিদ্যুৎ চলে গেল। আবার চালু হলে দুটি জিনিস নিশ্চিত হতেই হবে: টাকা যেন অর্ধেক পথে উধাও না হয়, আর রহিমকে “সফল” বার্তা পাঠানো হয়ে থাকলে টাকা যেন সত্যিই করিমের কাছে থাকে।

Transaction failure

একটি transaction নিজে ব্যর্থ: CHECK ভাঙল, ব্যালান্স কম, deadlock-এ বাতিল (পর্ব ৪৬), বা অ্যাপ ROLLBACK দিল। সিস্টেম ঠিক আছে। হারায়: কিছুই না — শুধু এই transaction-এর আধা কাজ উল্টাতে হয়।

System crash

বিদ্যুৎ যাওয়া, OS বা DBMS-এর crash. হারায়: সব মেমোরি — buffer pool-এর dirty page, log buffer, lock table. ডিস্ক অক্ষত।

Media failure

ডিস্ক নষ্ট, ফাইল মুছে গেল, ভুল কমান্ডে ডেটা ধ্বংস। হারায়: ডিস্কের ডেটাও। প্রতিকার একমাত্র অন্য জায়গায় রাখা কপি — backup আর archived log.

এখান থেকেই স্টোরেজের তিন ধারণা। Volatile storage (RAM, CPU cache) বিদ্যুৎ গেলে মুছে যায়। Non-volatile storage (SSD, HDD) crash-এ টেকে, কিন্তু ডিস্ক নিজেও মরতে পারে। Stable storage একটি আদর্শ: যা কখনো হারায় না। বাস্তবে একে কাছাকাছি আনা হয় একাধিক স্বাধীন কপি দিয়ে — RAID-এর mirror, অন্য ভবন বা অন্য শহরে replica. বইয়ের যুক্তিতে ধরা হয়, log থাকে stable storage-এ; তাই log-এর নিরাপত্তাই পুরো recovery-র ভিত্তি।

২ · Buffer pool কেন কাজটা কঠিন করে — steal আর force

পর্ব ৩৯-এ দেখেছেন, DBMS page বদলায় মেমোরির buffer pool-এ, আর dirty page পরে সুবিধামতো ডিস্কে লেখে। সমস্যা এখানেই: crash-এর মুহূর্তে ডিস্কে কিছু page নতুন, কিছু পুরোনো, আর কোনটি কোন transaction-এর, ডিস্ক নিজে জানে না। buffer manager-এর দুটি সিদ্ধান্ত ঠিক করে recovery-কে কী কী পারতে হবে। এই দুটি নাম দেন Theo Härder আর Andreas Reuter, ১৯৮৩ সালের প্রবন্ধে — যেখানে ACID শব্দটিও প্রথম আসে:

  • Steal: commit হয়নি এমন transaction-এর dirty page কি ডিস্কে লেখা যায় (frame “চুরি” করে অন্য page-কে দিতে)? হ্যাঁ হলে steal, না হলে no-steal.
  • Force: commit-এর আগে কি transaction-এর সব বদলানো page ডিস্কে লিখতেই হবে? হ্যাঁ হলে force, না হলে no-force.
নীতি × crash-এর পর যা লাগবে✓ = লাগে

            
এই জোড়ায়
অসমাপ্ত কাজের page ডিস্কে যায়?
commit-এ সব page লেখা?
crash-এর পর UNDO
crash-এর পর REDO
গতি
  1. No-steal + force. ডিস্কে কখনো অসমাপ্ত কাজ যায় না, আর commit-এর আগেই সব লেখা — তাই crash-এর পর কিছুই করার নেই। কিন্তু দাম ভয়ংকর: প্রতিটি commit এখানে-ওখানে ছড়ানো page-এর random লেখার জন্য অপেক্ষা করে, আর বড় transaction-এর সব page RAM-এ আটকে থাকে। তার ওপর একাধিক page লেখার মাঝে crash হলে অর্ধেক লেখা থেকে যায় — তাই খাঁটি রূপে এটি বাস্তবে টেকে না।
  2. Steal + force. buffer ভরে গেলে অসমাপ্ত page ডিস্কে যেতে পারে। crash হলে সেগুলোর পুরোনো মান ফেরাতে হবে — UNDO. commit-এ সব লেখা, তাই REDO লাগে না। commit তবু ধীর।
  3. No-steal + no-force. commit হয় দ্রুত (page পরে লেখা হবে), কিন্তু crash হলে commit হওয়া কাজ ডিস্কে না-ও থাকতে পারে — REDO লাগে। অসমাপ্ত কাজ কখনো ডিস্কে যায় না বলে UNDO নেই, কিন্তু বড় transaction-এর সব dirty page RAM-এ আটকে রাখতে হয়।
  4. Steal + no-force. buffer manager পুরো স্বাধীন: যখন খুশি যেকোনো page লেখে, commit-এ শুধু log লেখে। এটিই দ্রুততম, আর প্রায় সব বড় DBMS-এর পছন্দ (PostgreSQL-ও, যদিও MVCC-র কারণে তার আলাদা undo pass লাগে না — অংশ ৮)। দাম: recovery-কে UNDO আর REDO দুটোই পারতে হয়। ARIES ঠিক এই জোড়ার জন্য বানানো।
  5. এক লাইনে নিয়ম। steal মানে ডিস্কে অসমাপ্ত কাজ থাকতে পারে, তাই undo-র তথ্য (before image) রাখতে হবে। no-force মানে commit হওয়া কাজ ডিস্কে না-ও থাকতে পারে, তাই redo-র তথ্য (after image) রাখতে হবে। দুটো তথ্যই থাকে log-এ।

৩ · Shadow paging আর SQLite-এর rollback journal

log ছাড়াও একটি পথ আছে: shadow paging. ডেটাবেসের সব page-এর ঠিকানা থাকে একটি page table-এ। transaction কোনো page বদলালে পুরোনোটি ছোঁয় না, নতুন জায়গায় কপি লিখে নিজের page table-এর কপিতে ঠিকানা বদলায়। commit মানে একটিমাত্র পারমাণবিক কাজ: ডিস্কের “বর্তমান page table কোনটি” লেখা ছোট pointer-টি নতুন table-এ ঘোরানো। crash হলে পুরোনো pointer পুরোনো table-কেই দেখায় — undo বা redo কিছুই লাগে না।

শুনতে চমৎকার, কিন্তু IBM-এর System R (১৯৭০-এর দশক) এটি ব্যবহার করে সমস্যাগুলো টের পায়: page ডিস্কে এলোমেলো ছড়িয়ে পড়ে, পাশাপাশি থাকা ডেটা আর পাশাপাশি থাকে না; প্রতিটি commit-এ বদলানো page আর page table লিখতে হয় (মূলত force); পুরোনো page ফেরত তোলার কাজ লাগে; আর একসাথে অনেক transaction চালানো কঠিন। তাই বড় DBMS-গুলো log-এর দিকে চলে যায়। তবে ধারণাটি মরেনি — LMDB, আর ZFS ও Btrfs-এর মতো copy-on-write ফাইল সিস্টেম এর আত্মীয়।

SQLite-এর default rollback journal আরেকটি ঘনিষ্ঠ আত্মীয়। কোনো page প্রথমবার বদলানোর আগে SQLite তার মূল কপি -journal ফাইলে লেখে আর fsync করে; তারপর ডেটাবেস ফাইলেই বদলায়। commit-এ ডেটাবেস ফাইল fsync করে journal মুছে দেয় — journal মোছাই commit-এর মুহূর্ত। crash-এর পর ডেটাবেস খুললে SQLite একটি “hot journal” পায় আর মূল page-গুলো ফিরিয়ে লেখে। অর্থাৎ এটি undo-only, force নীতি। নিচের এডিটর দোকানের নমুনা ডেটাবেসে (db-shop) চলে: customers, categories, products (product_id, name, category_id, price, stock — ১০টি পণ্য), orders আর order_items. এই পাতার SQLite-ও (sql.js) ব্রাউজারের মেমোরিতে একটি ফাইল বানিয়ে এই journal ব্যবহার করে। দেখুন undo কীভাবে কাজ করে — পুরো transaction, আর savepoint পর্যন্ত আংশিকও:

PRAGMA journal_mode;
BEGIN;
UPDATE products SET stock = stock - 5 WHERE product_id = 101;
SAVEPOINT second_item;
UPDATE products SET stock = stock - 10 WHERE product_id = 102;
ROLLBACK TO second_item;
COMMIT;
SELECT product_id, stock FROM products WHERE product_id IN (101, 102);
BEGIN;
UPDATE products SET stock = 0;
ROLLBACK;
SELECT SUM(stock) AS total_stock FROM products;

প্রথম সারি delete — journal-এর default মোড (commit-এ journal মুছে ফেলা হয়, তাই নাম)। তারপর 101 | 7 আর 102 | 40: ROLLBACK TO second_item শুধু savepoint-এর পরের কাজ (102-এর −১০) উল্টেছে, আগেরটি (101-এর −৫) রেখে transaction চলেছে আর commit হয়েছে। শেষে 290: দশটি পণ্যের মজুদ শূন্য করার UPDATE (“১০টি সারি বদলেছে”) ROLLBACK পুরো উল্টে দিয়েছে — ২৯৫ থেকে শুধু commit হওয়া ৫ কমেছে। এই আংশিক rollback-ও recovery-র যন্ত্রেরই কাজ: ARIES-এর নামের “partial rollback” ঠিক এটি, CLR লিখে savepoint-এর LSN পর্যন্ত পেছনে যাওয়া। শিক্ষা: atomicity কোনো জাদু নয় — উল্টানোর তথ্য (এখানে journal-এর মূল page) কোথাও লেখা থাকতেই হবে।

এই ব্রাউজারের ফাইলে WAL মোডও চালু করা যায়। স্বয়ংক্রিয় checkpoint বন্ধ করে ১,০০০ সারি ঢুকিয়ে দেখি WAL-এ কতগুলো page (frame) জমল:

PRAGMA journal_mode = WAL;
PRAGMA synchronous;
PRAGMA wal_autocheckpoint = 0;
CREATE TABLE sales (id INTEGER PRIMARY KEY, amount INTEGER);
WITH RECURSIVE s(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM s WHERE x < 1000)
INSERT INTO sales SELECT x, x * 10 FROM s;
PRAGMA wal_checkpoint;
INSERT INTO sales VALUES (1001, 50);
PRAGMA wal_checkpoint;
PRAGMA wal_checkpoint(TRUNCATE);

ফলগুলো: wal, 2, 0, তারপর তিনবার checkpoint-এর (busy, log, checkpointed): 0 | 7 | 7, 0 | 1 | 1, 0 | 0 | 0. synchronous = 2 মানে FULL — প্রতিটি জরুরি মুহূর্তে fsync. CREATE আর বড় INSERT-এর দুটি commit মিলে WAL-এ ৭টি frame (প্রতিটি একটি বদলানো page-এর নতুন কপি), আর checkpoint সাতটিই মূল ফাইলে কপি করেছে। পরের একটি সারির INSERT-এ log আবার ১ থেকে শুরু: পুরো WAL checkpoint হয়ে গেলে SQLite পরের লেখা WAL-এর শুরু থেকে করে। TRUNCATE ফাইলটিকে শূন্যে নামায়। আসল ডিস্কের ফাইলে বাইট গুনে একই জিনিস দেখবেন অংশ ৮-এর Python প্রোগ্রামে।

৪ · Write-ahead logging — নিয়ম, log record আর LSN

Write-ahead log (WAL) একটি শুধু-শেষে-যোগ-করা ফাইল। প্রতিটি পরিবর্তনের একটি বিবরণ — log record — আগে এখানে লেখা হয়, তারপর আসল page বদলায়। ব্যাংকের ক্যাশিয়ারের খাতার মতো: টাকা গোনার আগে খাতায় লেখা, আর খাতার কোনো লাইন কখনো মোছা হয় না। log record-এর প্রধান ধরন:

ধরনকী থাকেকাজে লাগে
BEGINtransaction শুরু (অনেক বইয়ে <T1 start>)ARIES-এ আলাদা দরকার নেই — প্রথম record-ই শুরু বোঝায়
UPDATEtransaction, page, before image (পুরোনো মান), after image (নতুন মান), prevLSNbefore image দিয়ে UNDO, after image দিয়ে REDO
COMMITtransaction সফলএই record ডিস্কে পৌঁছানোই commit-এর মুহূর্ত
ABORTtransaction বাতিলের শুরুএরপর উল্টানোর কাজ চলে
CLRএকটি UPDATE উল্টানোর বিবরণ আর undoNextLSNশুধু REDO হয়, কখনো UNDO নয়
ENDcommit বা abort-এর সব কাজ শেষtransaction table থেকে সরানো

প্রতিটি record-এর একটি ক্রমবর্ধমান নম্বর: LSN (log sequence number) — বাস্তবে প্রায়ই log ফাইলের ভেতরের বাইট-অফসেট। একই transaction-এর আগের record-এর LSN থাকে prevLSN-এ, ফলে প্রতিটি transaction-এর record-গুলো পেছনের দিকে একটি শিকল। আর প্রতিটি data page-এর হেডারে লেখা থাকে সেই page-কে শেষবার বদলানো record-এর LSN: pageLSN. DBMS মেমোরিতে মনে রাখে log কতদূর ডিস্কে পৌঁছেছে: flushedLSN. এবার দুই নিয়ম:

WAL-এর দুই নিয়ম

(১) Undo নিয়ম: page P ডিস্কে লেখার আগে pageLSN(P) ≤ flushedLSN হতে হবে — অর্থাৎ P-এর সব পরিবর্তনের log আগে ডিস্কে। (২) Redo নিয়ম: transaction-এর COMMIT record পর্যন্ত log ডিস্কে (fsync) পৌঁছানোর পরেই “commit সফল” জানানো যাবে। log বাফার থেকে ক্রম মেনে লেখা হয় বলে commit record ডিস্কে মানে ওই transaction-এর সব record ডিস্কে।

T1: A (রহিম, page P5) −১০০, B (করিম, page P9) +১০০* = dirty, বন্ধনীতে pageLSN

            
RAM আর ডিস্ক
log buffer (RAM)
log (ডিস্ক)
flushedLSN
P5 (RAM)
P5 (ডিস্ক)
P9 (RAM)
P9 (ডিস্ক)
  1. প্রথম পরিবর্তন। log record 101 (before 500, after 400) তৈরি হয় মেমোরির log buffer-এ, আর P5 বদলায় buffer pool-এ। P5-এর pageLSN এখন ১০১. ডিস্কে কিছুই যায়নি।
  2. দ্বিতীয় পরিবর্তন। record 102, P9-এর pageLSN ১০২. দুটি page-ই dirty.
  3. Steal — নিয়ম ১ কাজ করছে। অন্য page-এর জায়গা লাগবে, P5-কে সরাতে হবে, অথচ T1 এখনো commit হয়নি। buffer manager দেখে pageLSN ১০১ > flushedLSN ০, তাই আগে log ফ্লাশ করে, তারপর P5 লেখে। উল্টো ক্রমে হলে কী হতো? ডিস্কে A=400, কিন্তু log-এ পুরোনো মান ৫০০-র কোনো চিহ্ন নেই — এখন crash হলে T1-কে আর উল্টানো যেত না, রহিমের ১০০ টাকা উধাও।
  4. Commit — নিয়ম ২. COMMIT record 103 ডিস্কে fsync হওয়ার পরেই রহিমের ফোনে “সফল” বার্তা। লক্ষ করুন, P9 এখনো ডিস্কে লেখা হয়নি — no-force. commit-এর খরচ শুধু একটি একটানা log লেখা।
  5. Crash. RAM-এর সব গেল, P9-এর নতুন কপিও। ডিস্কে অবস্থা মিশ্র: P5 নতুন, P9 পুরোনো। কিন্তু log-এ সব আছে।
  6. Recovery. log-এ T1-এর COMMIT আছে, তাই T1 বিজয়ী — তার কাজ redo করতে হবে, undo নয়। pageLSN বলে দেয় কোনটি লাগবে: P5 ইতিমধ্যে ১০১ পর্যন্ত হালনাগাদ, বাদ; P9-এর pageLSN ৪০, তাই ১০২ আবার প্রয়োগ। দুজনের হিসাব ঠিক, আর করিমকে বলা “টাকা এসেছে” কথাটি সত্য থাকল।

Group commit — একটি fsync, অনেক commit

নিয়ম ২-এর দাম একটি fsync, যা SSD-তে কয়েক দশ µs থেকে ms, আর HDD-তে কয়েক ms. প্রতিটি commit যদি নিজের fsync-এর জন্য লাইনে দাঁড়ায়, তবে HDD-তে সেকেন্ডে কয়েকশো commit-এর বেশি সম্ভব নয়। সমাধান group commit: একই মুহূর্তে অপেক্ষারত অনেক transaction-এর COMMIT record একসাথে log-এ লিখে একটিমাত্র fsync. বাসস্ট্যান্ডে প্রতিটি যাত্রীর জন্য আলাদা বাস না ছেড়ে, ভরলে একটি বাস ছাড়ার মতো। প্রত্যেকের latency সামান্য বাড়তে পারে, কিন্তু throughput বহু গুণ বাড়ে। PostgreSQL, InnoDB — সবাই এটি করে।

এবার আমাদের উদাহরণের পুরো log দেখুন। এটি একটি সাধারণ টেবিল log: lsn, prev_lsn, txn, type, page, item (কোন মান), before_val, after_val আর CLR-এর জন্য undo_next. তিনটি transaction — T1, T2, T3 — চারটি page-এ ছয়টি মান (A–F) বদলেছে; LSN ১২০-এর পর crash. পাশে আরও তিনটি টেবিল আছে, যা অংশ ৭-এ লাগবে: ckpt_att, ckpt_dpt আর disk.

SELECT lsn, prev_lsn, txn, type, page, item,
       before_val, after_val, undo_next
FROM log ORDER BY lsn;

১২টি সারি। পড়ার কৌশল: T1-এর শিকল পেছন থেকে — ১২০ (CLR) → prev ১১০ (ABORT) → ৯০ → ৫০ → ১০ → NULL. T2: ২০ UPDATE, ৬০ COMMIT, ৮০ END — পুরো শেষ। T3: ৭০, ১০০ — কোনো COMMIT নেই। ৩০ আর ৪০ checkpoint-এর শুরু আর শেষ (অংশ ৬)। ১২০ নম্বর CLR বলছে: T1 abort করে E-কে ১-এ ফিরিয়েছে (৯০-এর উল্টো), আর এরপর উল্টাতে হবে undo_next = 50 থেকে।

৫ · Deferred বনাম immediate update — কে redo, কে undo

বইয়ে দুটি সরল recovery কৌশল আগে শেখানো হয়, ARIES-এর ভিত্তি হিসেবে:

Deferred update

commit-এর আগে ডেটাবেসে কিছুই লেখা হয় না (no-steal); সব পরিবর্তন log-এ জমা। তাই log-এ শুধু after image লাগে। crash-এর পর: COMMIT আছে এমন transaction-কে redo, বাকিদের উপেক্ষা — undo কখনো নয়।

Immediate update

commit-এর আগেই page ডিস্কে যেতে পারে (steal). log-এ before আর after দুটোই। crash-এর পর: COMMIT আছে এমন transaction redo (log-এর সামনে থেকে), আর COMMIT নেই এমন সবাইকে undo (পেছন থেকে)।

যাদের COMMIT log-এ আছে তারা বিজয়ী (winner); যাদের নেই তারা পরাজিত — loser. log টেবিল থেকে এই ভাগ একটি GROUP BY-তেই:

SELECT txn,
       group_concat(type, ' → ') AS records,
       CASE WHEN SUM(type = 'COMMIT') > 0 THEN 'winner: REDO'
            ELSE 'loser: UNDO' END AS fate
FROM log
WHERE txn IS NOT NULL
GROUP BY txn;

তিনটি সারি: T1 | UPDATE → UPDATE → UPDATE → ABORT → CLR | loser: UNDO, T2 | UPDATE → COMMIT → END | winner: REDO, T3 | UPDATE → UPDATE | loser: UNDO. type = 'COMMIT' SQLite-এ ১ বা ০ দেয়, তাই SUM দিয়ে গোনা যায় (অন্য DBMS-এ CASE WHEN … THEN 1 END লিখুন)। T1 নিজেই abort করছিল আর শেষ করতে পারেনি — সেও loser. checkpoint-এর record-এ txn NULL, তাই বাদ।

এবার immediate update-এর দুই তালিকা: undo পেছন থেকে (বড় LSN আগে), redo সামনে থেকে:

SELECT lsn, txn, item, after_val AS now_val, before_val AS restore_to
FROM log
WHERE type = 'UPDATE'
  AND txn NOT IN (SELECT txn FROM log WHERE type = 'COMMIT')
ORDER BY lsn DESC;
SELECT lsn, txn, item, after_val
FROM log
WHERE type = 'UPDATE'
  AND txn IN (SELECT txn FROM log WHERE type = 'COMMIT')
ORDER BY lsn;

undo তালিকা পাঁচ সারি: 100 T3 F 8→7, 90 T1 E 2→1, 70 T3 D 9→5, 50 T1 C 35→30, 10 T1 A 80→100. redo তালিকা এক সারি: 20 | T2 | B | 70. পেছন থেকে undo কেন? একই মান দুবার বদলালে (ধরুন X: ১→২→৩) সামনে থেকে উল্টালে শেষে ২ থাকত, পেছন থেকে উল্টালে ঠিক ১. আর একটি ফাঁক চোখে পড়ছে? LSN ৯০ তালিকায় আছে, অথচ T1 আগেই CLR ১২০ দিয়ে সেটি উল্টে ফেলেছে। এখানে মান ফেরানো (E = ১) দুবার করলেও ক্ষতি নেই, কিন্তু “stock থেকে ৫ বিয়োগ”-এর মতো যৌক্তিক কাজ দুবার উল্টালে ভুল হয়। ARIES-এর CLR ঠিক এই সমস্যার উত্তর (অংশ ৭)।

কেন এটি কাজ করে — একটি ছোট প্রমাণ

দাবি: WAL-এর দুই নিয়ম মানলে crash-এর পর redo (বিজয়ীদের) আর undo (পরাজিতদের, উল্টো ক্রমে) শেষে ডেটাবেসে ঠিক বিজয়ীদের কাজ থাকে, পরাজিতদের কিছুই না।

  1. পরাজিতের কোনো পরিবর্তন ডিস্কে থাকলে তার before image log-এ আছে। পরিবর্তনটি ডিস্কে গেছে page লেখার মাধ্যমে, আর নিয়ম ১ বলে তার আগে সেই record ডিস্কে গেছে। তাই undo সম্ভব।
  2. বিজয়ীর সব পরিবর্তনের after image log-এ আছে। নিয়ম ২ অনুযায়ী COMMIT record ডিস্কে, আর log ক্রম মেনে লেখা হয় বলে তার আগের সব record-ও। page না লেখা হলেও redo সম্ভব।
  3. বারবার করলেও ফল একই। redo-তে LSN ≤ pageLSN হলে record বাদ যায়, তাই কোনো record একটি page-এ দুবার বসে না। recovery-র মাঝে আবার crash হলে আবার শুরু করলেই চলে — এই গুণকে বলে idempotency.

strict 2PL (পর্ব ৪৬) ধরে নেওয়া হয়েছে: পরাজিতের লেখা কোনো মান বিজয়ী পড়েনি বা ওপরে লেখেনি। নইলে পরাজিতকে উল্টালে বিজয়ীর কাজও নষ্ট হতো।

৬ · Checkpoint — recovery কতক্ষণ লাগবে, তার সীমা

checkpoint ছাড়া recovery-কে log-এর একেবারে শুরু থেকে পড়তে হতো — এক বছর চলা সার্ভারে এক বছরের log! Checkpoint মাঝে মাঝে log-এ একটি চিহ্ন বসায়: “এর আগের কাজ নিয়ে আর ভাবতে হবে না” বা অন্তত “ঠিক কোথা থেকে ভাবতে হবে”।

সরল (consistent) checkpoint

নতুন transaction থামানো, চলমানগুলো শেষ হতে দেওয়া, সব dirty page ডিস্কে লেখা, log-এ CHECKPOINT লেখা। recovery শুরু এখান থেকে। কিন্তু থামার সময়টুকু পুরো সিস্টেম জমে থাকে — ব্যস্ত দোকানে হিসাব মেলাতে শাটার নামানোর মতো।

Fuzzy checkpoint

কাজ থামে না। log-এ BEGIN_CKPT, তারপর মেমোরির দুটি টেবিলের কপি — চলমান transaction-এর transaction table আর dirty page table — দিয়ে END_CKPT. কোনো page জোর করে লেখা হয় না। ডিস্কের একটি নির্দিষ্ট জায়গায় (master record) শেষ BEGIN_CKPT-এর LSN রাখা হয়।

Dirty page table-এ প্রতিটি dirty page-এর পাশে থাকে recLSN: যে record প্রথম page-টিকে dirty করেছিল তার LSN — অর্থাৎ ডিস্কের কপিতে এর আগের সব কিছু আছে, এর পর থেকে হয়তো নেই। তাই redo শুরু হয় সব recLSN-এর ক্ষুদ্রতমটি থেকে, যা checkpoint-এর আগেও হতে পারে। আমাদের উদাহরণে checkpoint ৩০-এ, কিন্তু P1 ১০ থেকে dirty, তাই redo শুরু ১০ থেকে।

recovery-র সময় তাহলে বাঁধা থাকে দুটি জিনিসে: শেষ checkpoint থেকে কত log (analysis), আর সবচেয়ে পুরোনো dirty page কত আগের (redo). এজন্যই সিস্টেমগুলো পেছনে ক্রমাগত dirty page লেখে — পুরোনো recLSN বিদায় নেয়, redo-র শুরু সামনে আসে। PostgreSQL-এ checkpoint_timeout-এর default ৫ মিনিট আর max_wal_size ১ GB; দুটির যেটি আগে আসে। ঘন checkpoint মানে দ্রুত recovery, কিন্তু বেশি লেখা (আর PostgreSQL-এ full page write বেশি, অংশ ৮); বিরল checkpoint উল্টো।

৭ · ARIES — analysis, redo আর undo

ARIES (Algorithms for Recovery and Isolation Exploiting Semantics) প্রকাশ করেন IBM-এর C. Mohan, Don Haderle, Bruce Lindsay, Hamid Pirahesh আর Peter Schwarz, ১৯৯২ সালে ACM TODS জার্নালে। steal + no-force, fine-grained (সারি-স্তরের) lock আর আংশিক rollback — সবকিছু একসাথে সামলানো এর লক্ষ্য। IBM DB2 আর Microsoft SQL Server-এর recovery ARIES-এর ধারণায় দাঁড়ানো বলে ব্যাপকভাবে পরিচিত, আর প্রায় সব পাঠ্যবই এটিকেই আদর্শ ধরে। তিনটি মূলনীতি: WAL; repeating history — redo-তে crash-এর মুহূর্তের অবস্থা হুবহু আবার গড়া, পরাজিতদের কাজসহ; আর undo-কেও log করা CLR দিয়ে।

restart():
  ATT, DPT ← END_CKPT থেকে (master record দেখিয়ে দেয়)
  ANALYSIS: checkpoint থেকে log-এর শেষ পর্যন্ত, সামনে
     update/CLR: ATT[T].lastLSN ← lsn; page DPT-তে না থাকলে DPT[page] ← lsn
     COMMIT: ATT[T].status ← C        END: ATT থেকে T বাদ
  REDO: min(DPT.recLSN) থেকে, সামনে, প্রতিটি update/CLR r:
     if r.page ∈ DPT and r.lsn ≥ DPT[r.page].recLSN and pageLSN(r.page) < r.lsn:
        r প্রয়োগ; pageLSN ← r.lsn
  UNDO: ToUndo ← {ATT-এর বাকি (loser) সবার lastLSN}
     while ToUndo খালি নয়: r ← সবচেয়ে বড় LSN
        CLR হলে: ToUndo += r.undoNextLSN
        update হলে: CLR লেখো (undoNextLSN = r.prevLSN), উল্টাও; ToUndo += r.prevLSN
        পরেরটি NULL হলে: সেই transaction-এর END লেখো

এবার আমাদের log-এ পুরো হিসাব। শুরুর মান ছিল A=100, B=50, C=30, D=5, E=1, F=7. crash-এর মুহূর্তে ডিস্কের অবস্থা (disk টেবিল): P1 একবার লেখা হয়েছিল LSN ৭০-এর পর (pageLSN ৭০, A=80, D=9); P3 লেখা হয়েছিল ৫০-এর পর (pageLSN ৫০, C=35); P2 আর P4 কখনো লেখা হয়নি (pageLSN ০)। লক্ষ করুন, P1 আর P3-এ অসমাপ্ত কাজ ডিস্কে — steal.

LSN prev txn type    page  বিবরণ
10   –   T1  UPDATE  P1    A 100→80
20   –   T2  UPDATE  P2    B 50→70
30           BEGIN_CKPT
40           END_CKPT  ATT{T1:10,T2:20}
                       DPT{P1:10,P2:20}
50   10  T1  UPDATE  P3    C 30→35
60   20  T2  COMMIT
70   –   T3  UPDATE  P1    D 5→9
80   60  T2  END
90   50  T1  UPDATE  P4    E 1→2
100  70  T3  UPDATE  P2    F 7→8
110  90  T1  ABORT
120  110 T1  CLR     P4    E→1, undoNext 50
     ✗ crash

ধাপ ১ — Analysis

এখন পড়ছিU = অসমাপ্ত, C = committed

            
ATT (lastLSN) আর DPT (recLSN)
T1
T2
T3
P1
P2
P3
P4
  1. Checkpoint থেকে শুরু। master record বলে শেষ checkpoint ৩০-এ শুরু। END_CKPT ৪০ থেকে ATT আর DPT মেমোরিতে তোলা হয়: T1 আর T2 চলমান ছিল, P1 আর P2 dirty ছিল।
  2. LSN ৫০. T1-এর নতুন record, তাই T1.lastLSN = ৫০. P3 DPT-তে নেই — অর্থাৎ ৫০-ই হয়তো প্রথম পরিবর্তন যা ডিস্কে নেই — recLSN ৫০ দিয়ে যোগ।
  3. LSN ৬০ — T2-এর COMMIT. status C. T2 বিজয়ী, কিন্তু END আসা পর্যন্ত টেবিলে থাকে।
  4. LSN ৭০. T3 টেবিলে নতুন। P1 আগেই আছে; recLSN বদলায় না, কারণ recLSN মানে প্রথম dirty করা record.
  5. LSN ৮০ — T2-এর END. T2-এর আর কিছু করার নেই, টেবিল থেকে বাদ।
  6. LSN ৯০. T1.lastLSN = ৯০, আর P4 DPT-তে recLSN ৯০ নিয়ে।
  7. LSN ১০০. T3.lastLSN = ১০০. P2 আগেই DPT-তে।
  8. LSN ১১০ — T1-এর ABORT. T1 নিজে উল্টানো শুরু করেছিল, শেষ করেনি — এখনো অসমাপ্ত।
  9. LSN ১২০ — log-এর শেষ। ATT-তে বাকি T1 (১২০) আর T3 (১০০) — এরাই loser. DPT-তে চারটি page; P1 আর P3 আসলে ডিস্কে লেখা হয়েছিল, কিন্তু analysis তা জানে না — page লেখা log হয় না। DPT তাই একটু বড় হতে পারে, ছোট নয়; redo-র pageLSN পরীক্ষা বাড়তিটুকু ধরে ফেলে। redo শুরু হবে ক্ষুদ্রতম recLSN ১০ থেকে।

analysis-এর দুই টেবিল SQL দিয়েও বানানো যায়: checkpoint-এর কপি নিয়ে তার পরের record-গুলো জুড়ে দেওয়া।

WITH after_ckpt AS (
  SELECT * FROM log WHERE lsn > (SELECT lsn FROM log WHERE type = 'BEGIN_CKPT'))
SELECT txn, MAX(last_lsn) AS last_lsn
FROM (SELECT txn, last_lsn FROM ckpt_att
      UNION ALL SELECT txn, lsn FROM after_ckpt WHERE txn IS NOT NULL)
WHERE txn NOT IN (SELECT txn FROM after_ckpt WHERE type = 'END')
GROUP BY txn;
WITH after_ckpt AS (
  SELECT * FROM log WHERE lsn > (SELECT lsn FROM log WHERE type = 'BEGIN_CKPT'))
SELECT page, MIN(rec_lsn) AS rec_lsn
FROM (SELECT page, rec_lsn FROM ckpt_dpt
      UNION ALL SELECT page, lsn FROM after_ckpt WHERE type IN ('UPDATE', 'CLR'))
GROUP BY page;

ATT: T1 | 120, T3 | 100. DPT: P1 | 10, P2 | 20, P3 | 50, P4 | 90 — stepper-এর শেষ ধাপের সাথে হুবহু মিল। MAX দেয় শেষ LSN, MIN দেয় প্রথম dirty করা LSN; checkpoint-এর recLSN সবসময় পরের যেকোনো LSN-এর চেয়ে ছোট, তাই MIN ঠিক কাজ করে। আসল DBMS এক পাসে, মেমোরির hash table-এ এই কাজটিই করে।

ধাপ ২ — Redo: ইতিহাস হুবহু আবার

শর্ত: page ∈ DPT, LSN ≥ recLSN, pageLSN < LSNবন্ধনীতে pageLSN

            
Page-এর মান (pageLSN)
P1
P2
P3
P4
সিদ্ধান্ত
  1. শুরু LSN ১০ থেকে। checkpoint ৩০-এ হলেও P1 ১০ থেকে dirty, তাই redo তার আগে থেকে। ডিস্কের অবস্থা: P1-এ A=80, D=9 (৭০ পর্যন্ত হালনাগাদ), P3-এ C=35, P2 আর P4 পুরোনো।
  2. LSN ১০ — বাদ। P1 DPT-তে আর ১০ ≥ recLSN, তাই page আনা হলো; কিন্তু pageLSN ৭০ বলছে এই পরিবর্তন আগেই ডিস্কে আছে।
  3. LSN ২০ — REDO. P2-এর pageLSN ০, অর্থাৎ T2-এর committed কাজ ডিস্কে পৌঁছায়নি। B=70, pageLSN ২০.
  4. LSN ৫০ — বাদ। P3 ৫০-এর পর লেখা হয়েছিল। এটি T1-এর (loser) কাজ, তবু redo নিয়ে প্রশ্ন শুধু pageLSN-এর — কে জিতল তা redo দেখে না।
  5. LSN ৭০ — বাদ। P1-এর pageLSN ঠিক ৭০, তাই এটিও ডিস্কে আছে।
  6. LSN ৯০ — REDO. পরাজিত T1-এর কাজ হলেও আবার প্রয়োগ: E=2. এটিই repeating history — কে জিতল তা নিয়ে ভাবা undo-র কাজ।
  7. LSN ১০০ — REDO. P2-এর pageLSN এখন ২০ < ১০০, তাই F=8.
  8. LSN ১২০ (CLR) — REDO. CLR-ও redo হয়: E আবার ১, pageLSN ১২০. এখন ডেটাবেস ঠিক crash-এর মুহূর্তের মতো: A=80, B=70, C=35, D=9, E=1, F=8 — বিজয়ী আর পরাজিত সবার কাজসহ।
WITH dpt(page, rec_lsn) AS (VALUES ('P1', 10), ('P2', 20), ('P3', 50), ('P4', 90))
SELECT l.lsn, l.type, l.page, d.rec_lsn, k.page_lsn AS disk_lsn,
       CASE WHEN l.lsn < d.rec_lsn THEN 'skip: recLSN'
            WHEN k.page_lsn >= l.lsn THEN 'skip: pageLSN'
            ELSE 'REDO' END AS action
FROM log l JOIN dpt d USING (page) JOIN disk k USING (page)
WHERE l.type IN ('UPDATE', 'CLR')
  AND l.lsn >= (SELECT MIN(rec_lsn) FROM dpt)
ORDER BY l.lsn;

সাতটি সারি, action কলামে: ১০ skip: pageLSN, ২০ REDO, ৫০ skip: pageLSN, ৭০ skip: pageLSN, ৯০, ১০০ আর ১২০ REDO — stepper-এর সাথে মিল। এখানে ডিস্কের পুরোনো pageLSN দিয়েই তুলনা চলে, কারণ redo-তে LSN কেবল বাড়ে: একটি page-এ নতুন pageLSN বসলেও পরের record-এর LSN তার চেয়ে বড়। skip: recLSN এখানে আসেনি; ধরুন P3-এর recLSN ৬০ হতো (৫০-এর পর লেখা, তারপর আবার dirty) — তখন ৫০ page না এনেই বাদ যেত, একটি I/O বাঁচত।

ধাপ ৩ — Undo: CLR লিখে লিখে উল্টানো

undo-তে loser-দের সব record একসাথে, বড় LSN আগে — একেক transaction আলাদা নয়, কারণ log-এ পেছনে একবার হাঁটাই সবচেয়ে কম I/O. প্রতিটি UPDATE উল্টানোর সময় একটি CLR লেখা হয়, যার undoNextLSN = সেই UPDATE-এর prevLSN — অর্থাৎ “এরপর এখান থেকে উল্টানো শুরু হবে”। আর CLR নিজে কখনো উল্টানো হয় না; তাকে দেখলে শুধু তার undoNextLSN-এ লাফ।

ToUndo থেকে সবসময় সবচেয়ে বড় LSNনতুন record LSN 130 থেকে

            
ToUndo আর নতুন log
ToUndo
নতুন record
A B C
D E F
  1. শুরু। analysis-এর loser: T1-এর lastLSN ১২০, T3-এর ১০০. redo-র পর মানগুলো crash-এর মুহূর্তের।
  2. ১২০ একটি CLR. crash-এর আগে T1 নিজেই ৯০ উল্টেছিল। CLR-এর undoNextLSN ৫০ বলে দেয়, এরপর ৫০ থেকে — ৯০ আর দ্বিতীয়বার উল্টানো হবে না।
  3. ১০০ উল্টানো। আগে CLR ১৩০ log-এ (F=7, undoNext = ১০০-এর prevLSN ৭০), তারপর page-এ F=7. CLR-টি নিজেও WAL মানে।
  4. ৭০ উল্টানো, T3 শেষ। CLR ১৪০ (D=5). ৭০-এর prevLSN নেই, তাই undoNext ফাঁকা — T3-এর END ১৫০.
  5. ৫০ উল্টানো। CLR ১৬০ (C=30), undoNext ১০.
  6. ১০ উল্টানো, T1 শেষ। CLR ১৭০ (A=100), END ১৮০. শেষ মান A=100, B=70, C=30, D=5, E=1, F=7 — শুধু বিজয়ী T2-এর কাজ (B = ৫০ → ৭০) টিকে আছে।
  7. Undo-র মাঝে আবার crash? ধরুন CLR ১৪০ ডিস্কে পৌঁছানোর পরেই বিদ্যুৎ গেল। পরের restart-এ redo CLR ১৩০ আর ১৪০ আবার বসায় (pageLSN দেখে)। analysis-এ T3-এর lastLSN ১৪০, যার undoNext ফাঁকা — তাই শুধু END লেখা। T1-এর lastLSN ১২০ (CLR ১৩০ T3-এর) — আবার ৫০-এ লাফ। CLR-এর কারণে প্রতিটি UPDATE সর্বোচ্চ একবার উল্টানো হয়, তাই বারবার crash হলেও log সীমাহীন বাড়ে না।

undo-র ক্রম SQL-এর recursive CTE (পর্ব ২৫) দিয়েও বের করা যায় — প্রতিটি loser-এর শেষ record থেকে শুরু করে CLR হলে undo_next, নইলে prev_lsn ধরে পেছনে:

WITH RECURSIVE todo(lsn) AS (
  SELECT MAX(lsn) FROM log WHERE txn IN ('T1', 'T3') GROUP BY txn
  UNION ALL
  SELECT CASE WHEN l.type = 'CLR' THEN l.undo_next ELSE l.prev_lsn END
  FROM todo JOIN log l ON l.lsn = todo.lsn
  WHERE (CASE WHEN l.type = 'CLR' THEN l.undo_next ELSE l.prev_lsn END) IS NOT NULL)
SELECT l.lsn, l.txn, l.type, l.item, l.before_val AS restore_to,
       CASE WHEN l.type = 'UPDATE' THEN 'CLR + undo' ELSE 'jump' END AS action
FROM todo JOIN log l USING (lsn)
ORDER BY l.lsn DESC;

পাঁচ সারি: 120 T1 CLR E NULL jump, 100 T3 F 7, 70 T3 D 5, 50 T1 C 30, 10 T1 A 100 — শেষ চারটির action CLR + undo. অংশ ৫-এর সরল তালিকার সাথে তুলনা করুন: ৯০ আর ১১০ এখানে নেই। CLR-এর লাফ ৯০-কে পাশ কাটিয়েছে, আর ABORT record-টি শিকলের পথেই পড়েনি। লক্ষ করুন, সারিগুলো আসে ORDER BY lsn DESC অনুযায়ী — ARIES-এর “সবচেয়ে বড় LSN আগে” নিয়মও ঠিক তা-ই।

৮ · Media recovery আর বাস্তব সিস্টেম

ডিস্কই নষ্ট হলে log-ও হয়তো গেছে। তাই media recovery-র ভিত্তি অন্য জায়গায় রাখা দুটি জিনিস: একটি পূর্ণ backup, আর তার পর থেকে জমা সব log-এর কপি — archived log. backup ফিরিয়ে এনে archived log redo করলে ডেটাবেস যেকোনো মুহূর্তে পৌঁছাতে পারে: point-in-time recovery. বিকেল ৩:০৫-এ কেউ ভুল করে DROP TABLE orders চালালে ৩:০৪:৫৯ পর্যন্ত redo করে থামা যায়। পুরো কৌশল — backup-এর ধরন, replication, high availability — পর্ব ৫২-এ।

PostgreSQL-এর WAL থাকে pg_wal ফোল্ডারে, ১৬ MB-র segment ফাইলে। recovery শুধু redo — কোনো undo pass নেই! কারণ MVCC (পর্ব ৪৭): বাতিল transaction-এর সারির সংস্করণগুলো টেবিলে থেকে যায়, কিন্তু commit log (pg_xact) বলে দেয় সেগুলো বাতিল, তাই কেউ দেখে না; পরে VACUUM সরায়। torn page ঠেকাতে full_page_writes (default on): checkpoint-এর পর কোনো page প্রথমবার বদলালে পুরো page-এর ছবি WAL-এ যায়। archive_command বা pg_basebackup দিয়ে media recovery আর PITR.

InnoDB-তে দুটি log: redo log (WAL, প্রতিটি page-এর ভৌত পরিবর্তন) আর undo log (পুরোনো সংস্করণ — rollback আর MVCC-র পড়া দুটোর জন্যই)। crash-এর পর redo log প্রয়োগ, তারপর অসমাপ্ত transaction rollback — যা বড় হলে পেছনে চলতে চলতে সময় নেয়। torn page ঠেকায় doublewrite buffer: page আগে একটি আলাদা এলাকায়, তারপর আসল জায়গায় লেখা। checkpoint fuzzy, ক্রমাগত। replication আর PITR-এর জন্য আলাদা binary log (binlog) — redo log-এর সাথে গুলিয়ে ফেলবেন না।

default rollback journal (অংশ ৩): undo-only, commit-এ force. ৩.৭.০ সংস্করণ (২০১০) থেকে WAL মোড: নতুন page যোগ হয় -wal ফাইলের শেষে, commit মানে commit-চিহ্নিত frame লেখা; পাঠকেরা পুরোনো ডেটাবেস ফাইল আর WAL মিলিয়ে পড়ে, তাই পাঠক আর লেখক একে অপরকে আটকায় না। checkpoint WAL-এর page-গুলো মূল ফাইলে কপি করে — default-এ WAL ১০০০ page ছাড়ালে নিজে থেকেই। WAL নেটওয়ার্ক ফাইল সিস্টেমে কাজ করে না, কারণ সব প্রক্রিয়াকে একই কম্পিউটারের shared memory ভাগ করতে হয়।

নিচের Python প্রোগ্রাম একটি আসল ফাইলে SQLite চালায় (Python-এর sqlite3 মডিউল)। প্রথমে rollback journal-এর আয়ু দেখায়, তারপর WAL মোডে তিনটি commit-এ -wal ফাইল বাড়তে দেখায়, শেষে checkpoint. স্বয়ংক্রিয় checkpoint বন্ধ রাখা হয়েছে, যাতে হিসাব পরিষ্কার থাকে:

import os, sqlite3, tempfile
path = os.path.join(tempfile.mkdtemp(), "shop.db")
con = sqlite3.connect(path, isolation_level=None)       # we write BEGIN/COMMIT ourselves
size = lambda ext: os.path.getsize(path + ext) if os.path.exists(path + ext) else "-"
con.execute("CREATE TABLE sales (id INTEGER PRIMARY KEY, amount INTEGER)")
print("mode:", con.execute("PRAGMA journal_mode").fetchone()[0])
con.execute("BEGIN")
con.execute("INSERT INTO sales (amount) VALUES (100)")
print("inside txn, -journal bytes:", size("-journal"))
con.execute("COMMIT")
print("after commit, -journal bytes:", size("-journal"))
print("mode:", con.execute("PRAGMA journal_mode = WAL").fetchone()[0])
con.execute("PRAGMA wal_autocheckpoint = 0")              # no automatic checkpoint
for batch in range(1, 4):
    con.execute("BEGIN")
    con.executemany("INSERT INTO sales (amount) VALUES (?)", [(i,) for i in range(400)])
    con.execute("COMMIT")
    print(f"commit {batch}: db={size('')} wal={size('-wal')}")
print("FULL checkpoint (busy, log, done):", con.execute("PRAGMA wal_checkpoint(FULL)").fetchone())
print(f"after FULL: db={size('')} wal={size('-wal')}")
con.execute("PRAGMA wal_checkpoint(TRUNCATE)")
print(f"after TRUNCATE: db={size('')} wal={size('-wal')}")

আউটপুট:

mode: delete
inside txn, -journal bytes: 4616
after commit, -journal bytes: -
mode: wal
commit 1: db=8192 wal=4152
commit 2: db=8192 wal=20632
commit 3: db=8192 wal=37112
FULL checkpoint (busy, log, done): (0, 9, 9)
after FULL: db=20480 wal=37112
after TRUNCATE: db=20480 wal=0

পড়ুন সংখ্যাগুলো। transaction-এর ভেতরে journal ৪৬১৬ বাইট = ৫১২ বাইটের হেডার + একটি page-এর মূল কপি (৪০৯৬ + ৮ বাইট হিসাব); commit-এর পর journal নেই (-) — মুছে ফেলাই commit. WAL-এ প্রতিটি frame = ২৪ বাইট হেডার + ৪০৯৬ বাইটের page, আর ফাইলের শুরুতে ৩২ বাইট: ৩২ + ১ × ৪১২০ = ৪১৫২, তারপর ৫টি frame (২০৬৩২), তারপর ৯টি (৩৭১১২)। এই পুরো সময় মূল ফাইল 8192 বাইটেই আটকে — commit হওয়া ডেটা শুধু WAL-এ! FULL checkpoint ৯টি frame মূল ফাইলে কপি করে (ফাইল ২০৪৮০ বাইট = ৫টি page), কিন্তু WAL ফাইল ছোট করে না, পরের লেখায় শুরু থেকে আবার ব্যবহার করে। TRUNCATE সেটি শূন্যে নামায়।

৯ · fsync, ডিস্কের “মিথ্যা” আর synchronous_commit

WAL-এর পুরো প্রমাণ একটি অনুমানের ওপর: fsync “সফল” বললে log সত্যিই টেকসই। কিন্তু মাঝখানে কয়েকটি স্তর: DBMS → OS-এর page cache → ডিস্কের নিজস্ব RAM cache → আসল flash বা চৌম্বক থালা। অনেক ডিস্কে একটি volatile write cache থাকে, যা লেখা নিজের RAM-এ পেয়েই “হয়ে গেছে” বলতে পারে। fsync-এর পর OS ডিস্ককে cache খালি করার নির্দেশ (flush) পাঠায় — যদি ডিস্ক সেটি সত্যিই মানে। কিছু সস্তা ডিস্ক বা ভুল কনফিগার করা RAID controller flush উপেক্ষা করে বলে বহু বছর ধরে অভিযোগ আর পরীক্ষার রিপোর্ট আছে; আবার ভালো server SSD-তে ক্যাপাসিটর থাকে, যা বিদ্যুৎ গেলেও cache-এর ডেটা লিখে ফেলে (power-loss protection).

২০১৮ সালের fsync সমস্যা। সেই বছর PostgreSQL ডেভেলপাররা (মার্চে মেইলিং লিস্টের একটি আলোচনা থেকে শুরু) লক্ষ করেন: Linux-এ fsync একবার I/O ত্রুটি দিলে, ব্যর্থ page-গুলো কখনো কখনো “পরিষ্কার” চিহ্নিত হয়ে যেতে পারে, আর পরের fsync “সফল” বলে — অথচ লেখা হারিয়ে গেছে। PostgreSQL আবার fsync চেষ্টা করত আর ভাবত সব ঠিক। পরে নীতি বদলায়: fsync ব্যর্থ হলে সার্ভার সরাসরি থেমে (PANIC) WAL থেকে recovery করে। আচরণটি kernel-এর সংস্করণ আর ফাইল সিস্টেম ভেদে আলাদা ছিল, আর Linux-এও ত্রুটি জানানোর ব্যবস্থা উন্নত হয়েছে। শিক্ষা: durability অনেক স্তরের প্রতিশ্রুতির শিকল, আর সবচেয়ে দুর্বল স্তরটিই আসল নিশ্চয়তা।

অন্য দিকে, কিছু অ্যাপ জেনেশুনে একটু durability ছেড়ে গতি নেয়। PostgreSQL-এর synchronous_commit:

সেটিংcommit কখন “সফল”crash-এ ঝুঁকি
PostgreSQL synchronous_commit = on (default)WAL স্থানীয় ডিস্কে fsync-এর পর (synchronous replica থাকলে তারও নিশ্চয়তার পর)কিছুই হারায় না
PostgreSQL synchronous_commit = offWAL buffer-এ লেখার পরেই; WAL writer পরে লেখেশেষ অল্প কিছু commit হারাতে পারে (সর্বোচ্চ wal_writer_delay-এর তিন গুণ, default-এ ~০.৬ সেকেন্ড), কিন্তু ডেটাবেস অসংগত হয় না
PostgreSQL fsync = offfsync কখনোই নয়বিপজ্জনক: crash-এ ডেটাবেস নষ্ট হতে পারে — শুধু পরীক্ষার কাজে
InnoDB innodb_flush_log_at_trx_commit১ (default): প্রতি commit-এ flush; ২: OS-এ লেখা, flush সেকেন্ডে একবার; ০: লেখা আর flush দুটোই সেকেন্ডে একবার২-এ OS crash-এ, ০-তে mysqld crash-এও প্রায় এক সেকেন্ডের commit হারাতে পারে
SQLite PRAGMA synchronousFULL (২), NORMAL (১), OFF (০)WAL মোডে NORMAL-এ ডেটাবেস নষ্ট হয় না, তবে বিদ্যুৎ গেলে শেষ commit-গুলো হারাতে পারে

লক্ষ করুন “হারানো” আর “নষ্ট হওয়া”-র পার্থক্য। synchronous_commit = off-এ WAL-এর ক্রম আর নিয়ম ১ অক্ষত; শুধু commit record একটু দেরিতে ডিস্কে যায়। তাই crash-এর পর ডেটাবেস একটু আগের একটি সংগত অবস্থায় ফেরে। fsync = off-এ নিয়ম ১-ই ভাঙে — page আগে, log পরে পৌঁছাতে পারে — তাই অসংগত, এমনকি পড়া যায় না এমন ডেটাবেস। একটি ওয়েবসাইটের “page view” গোনা off-এ চলতে পারে; bKash-এর লেনদেন কখনোই নয়। PostgreSQL-এ এটি এক সংযোগ বা এক transaction-এর জন্যও বদলানো যায়:

SHOW synchronous_commit;            -- on
SHOW full_page_writes;              -- on
BEGIN;
SET LOCAL synchronous_commit = off; -- শুধু এই transaction-এ
INSERT INTO page_views (url, seen_at) VALUES ('/home', now());
COMMIT;
CHECKPOINT;                          -- হাতে checkpoint (superuser বা pg_checkpoint ভূমিকা)

আরও গভীরে: ARIES-এর log “physiological” — কোন page তা ভৌতভাবে বলে, কিন্তু page-এর ভেতরে পরিবর্তনটি যৌক্তিক (যেমন “slot 3-এ এই সারি ঢোকাও”), যাতে slotted page-এর ভেতর record সরলেও log ঠিক থাকে। B+ tree-এর page split-এর মতো কাঠামোগত কাজ “nested top action” হিসেবে log হয়, যাতে transaction abort হলেও split উল্টানো না লাগে। বড় undo-র অপেক্ষা কমাতে SQL Server ২০১৯ সংস্করণে Accelerated Database Recovery এনেছে, যা সারির পুরোনো সংস্করণ একটি স্থায়ী version store-এ রেখে undo-কে প্রায় তাৎক্ষণিক করে — MVCC আর recovery-র মিলন।

১০ · AI-এর যুগে recovery আর durability-র প্রশ্ন

“ডেটাবেসে লেখা ধীর, দ্রুত করো” — AI প্রায়ই এমন সেটিং সুপারিশ করে যা গতি দেয়, কিন্তু নিঃশব্দে durability কেড়ে নেয়। সাধারণ ভুল:

AI-এর ভুলফলকীভাবে ধরবেন
fsync = off আর synchronous_commit = off-কে একই বলাপ্রথমটিতে ডেটাবেস নষ্ট হতে পারেডকুমেন্টেশনে দুটির সতর্কবার্তা পাশাপাশি পড়ুন
“COMMIT মানে ডেটা ফাইলে লেখা হয়েছে”WAL, checkpoint আর backup নিয়ে ভুল সিদ্ধান্তcommit = log ডিস্কে; data page পরে
SQLite WAL নেটওয়ার্ক ড্রাইভে চালানোlock আর shared memory ভাঙে, ডেটা নষ্টSQLite-এর WAL ডকুমেন্টেশনের সীমাবদ্ধতা দেখুন
InnoDB-র redo log আর binlog গুলিয়ে ফেলাভুল backup বা replication পরিকল্পনাকোন log কোন কাজে, প্রশ্ন করে মিলিয়ে নিন
backup আছে বলে restore কখনো পরীক্ষা না করাবিপদের দিনে backup অচলনিয়মিত অন্য সার্ভারে restore করে দেখুন

AI-কে কাজের ধরন আর কতটা ক্ষতি সহ্য করা যায়, দুটোই বলুন:

আমার অ্যাপ: PostgreSQL 16, একটি মোবাইল পেমেন্টের লেনদেন টেবিল আর একটি "page_views" লগ টেবিল। লেখা ধীর — প্রতি সেকেন্ডে কয়েক হাজার INSERT. ১. synchronous_commit, fsync, full_page_writes আর commit_delay — প্রতিটি কী করে আর crash-এ কী হারায়, আলাদা করে বলো। ২. লেনদেন টেবিলে একটি commit-ও হারানো চলবে না, page_views-এ এক সেকেন্ডের ডেটা হারানো চলবে। কোন সেটিং কোথায় (সার্ভার, সংযোগ না transaction স্তরে) দেবে? ৩. প্রতিটি প্যারামিটারের নাম PostgreSQL 16-এর ডকুমেন্টেশনে আছে কি না নিশ্চিত হয়ে লেখো; অনিশ্চিত হলে বলো। ৪. পরিবর্তনের পর crash-এর পরীক্ষা কীভাবে করব (পরীক্ষামূলক সার্ভারে), সেটিও বলো।

AI-এর পরামর্শ মানার আগে — চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • WAL protocol কী? — data page ডিস্কে যাওয়ার আগে তার log record ডিস্কে, আর commit record ডিস্কে পৌঁছানোর পরেই commit সফল।
  • Steal/no-force-এ কেন UNDO আর REDO দুটোই লাগে? — steal-এ অসমাপ্ত কাজ ডিস্কে থাকতে পারে (UNDO), no-force-এ committed কাজ ডিস্কে না-ও থাকতে পারে (REDO).
  • ARIES-এর তিন ধাপ? — analysis (ATT, DPT), redo (min recLSN থেকে, pageLSN দেখে, সবার কাজ), undo (loser-দের, CLR লিখে)।
  • CLR কেন? — undo-কেও log করা, যাতে recovery-র মাঝে crash হলে কোনো কাজ দুবার উল্টানো না হয়; CLR শুধু redo হয়।
  • Fuzzy checkpoint কী? — কাজ না থামিয়ে ATT আর DPT log-এ লেখা; page জোর করে লেখা হয় না।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Shadow paging-এ undo বা redo কিছুই লাগে না। তবু System R-এর পর বড় DBMS-গুলো log-ভিত্তিক recovery-তে গেল কেন? আর আজ copy-on-write আবার ফিরছে কেন?

IBM-এর System R ১৯৭০-এর দশকে shadow page আর log দুটোই ব্যবহার করত। Jim Gray ও সহকর্মীরা ১৯৮১ সালে “The Recovery Manager of the System R Database Manager” প্রবন্ধে অভিজ্ঞতাটি লেখেন, আর সমস্যাগুলো স্পষ্ট হয়। প্রথম সমস্যা স্থানীয়তা: প্রতিটি হালনাগাদ page নতুন জায়গায় যায়, তাই একটি টেবিলের পাশাপাশি page কিছুদিন পর ডিস্কজুড়ে ছড়িয়ে পড়ে — পর্ব ৩৯-এর sequential I/O-র সুবিধা হারায়। দ্বিতীয় সমস্যা commit-এর খরচ: সব বদলানো page আর page table-এর বদলানো অংশ ডিস্কে লিখতে হয়, যা আসলে force নীতি।

তৃতীয় সমস্যা concurrency: একটি page-এ দুটি transaction-এর সারি থাকলে কার shadow কোনটি? page-স্তরের shadow সারি-স্তরের lock-এর সাথে মেলে না। তাই System R-কেও আলাদা log রাখতে হতো, আর দুই ব্যবস্থার খরচ একসাথে বইতে হতো। এই অভিজ্ঞতা থেকেই WAL আর পরে ARIES-এর পথ — যেখানে page নিজের জায়গায় থাকে (update in place), আর সব জটিলতা যায় একটানা লেখা log-এ।

তবু ধারণাটি বিভিন্ন রূপে টিকে আছে। LMDB পুরো B+ tree copy-on-write করে, আর root-এর pointer বদলেই commit করে; একজন লেখক আর অনেক পাঠকের কাজে এটি সরল আর দ্রুত। ZFS আর Btrfs ফাইল সিস্টেম কখনো জায়গায় লেখে না। SSD-র ভেতরের FTL-ও আসলে নতুন জায়গায় লেখে (পর্ব ৩৯)। অনেক NoSQL ইঞ্জিনের log-structured storage (LSM tree) পুরোনো ডেটা না ছুঁয়ে নতুন ফাইল লেখে। কারণ আজকের ডিস্কে random পড়া আগের চেয়ে সস্তা, আর মেমোরি বেশি — স্থানীয়তার দাম কমেছে।

মূল উপলব্ধি: recovery-র কৌশল বাছাই হয় হার্ডওয়্যারের দাম আর কাজের ধরন দেখে। HDD-যুগে update in place + WAL জিতেছিল; নতুন ডিস্ক আর কাজের ধরনে copy-on-write আবার যুক্তিসংগত হচ্ছে।

প্র ০২ARIES redo-তে পরাজিত transaction-এর কাজও আবার করে, তারপর undo-তে উল্টায়। শুধু বিজয়ীদের redo করলেই তো কম কাজ হতো। “repeating history” কেন?

সরল কৌশলটি শুনতে সাশ্রয়ী: বিজয়ীদের record redo, পরাজিতদের undo, ব্যস। কিন্তু ভাবুন একটি page-এ T2 (বিজয়ী) আর T3 (পরাজিত) দুজনেরই সারি — ARIES সারি-স্তরের lock চায়, তাই এটি স্বাভাবিক। page-এর pageLSN একটিমাত্র সংখ্যা। বিজয়ীদের record বাছাই করে redo করলে page-এ এমন একটি অবস্থা তৈরি হয় যা কখনো ছিল না — LSN ১০০-র পরিবর্তন নেই অথচ ১১০-এর আছে। তখন pageLSN দিয়ে “কতদূর পর্যন্ত page-এ আছে” বলা অসম্ভব, আর redo-র পুরো idempotency ভেঙে পড়ে।

আরও বড় কারণ যৌক্তিক undo. B+ tree-এ পরাজিতের ঢোকানো একটি key উল্টাতে হয়তো ভিন্ন page-এ যেতে হয়, কারণ মাঝে অন্য কেউ page split করেছে। এমন undo শুধু তখনই ঠিক কাজ করে যখন গাছটি crash-এর মুহূর্তের মতো সংগত। repeating history ঠিক সেটি নিশ্চিত করে: redo-র পর ডেটাবেস একটি বাস্তব অবস্থায়, আর undo ঠিক সেই নিয়মে চলে যা স্বাভাবিক abort-এ চলত। ফলে recovery-র undo আর সাধারণ ROLLBACK একই কোড — কম কোড, কম ভুল।

Mohan ও সহকর্মীরা প্রবন্ধে আগের কয়েকটি ব্যবস্থার তুলনা করে দেখান, যেখানে redo বাছাই করা হতো সেখানে CLR আর আংশিক rollback-এর সাথে সূক্ষ্ম ভুল দেখা দিত। খরচের দিক থেকে বাড়তি redo সাধারণত অল্প, কারণ পরাজিত transaction সংখ্যায় কম আর crash-এর ঠিক আগের সময়েরই।

মূল উপলব্ধি: আগে ইতিহাস হুবহু আবার গড়া, তারপর স্বাভাবিক নিয়মে উল্টানো — এই দুই ভাগ আলাদা রাখায় page-স্তরের হিসাব (pageLSN) সরল থাকে, আর সূক্ষ্ম lock ও যৌক্তিক undo নিরাপদ হয়।

প্র ০৩একটি ডেটাবেস সফটওয়্যার নিখুঁত হলেও durability কেন নিশ্চিত নয়? ২০১৮ সালের fsync সমস্যা আর ডিস্কের cache থেকে কী শিখি?

WAL-এর প্রমাণে একটি বাক্য আছে যা ডেটাবেসের হাতে নেই: “fsync ফিরে এলে লেখা টেকসই”। এই প্রতিশ্রুতি আসলে কয়েকজনের: OS-এর ফাইল সিস্টেম, ডিভাইস ড্রাইভার, RAID controller, ডিস্কের firmware. যেকোনো একজন মিথ্যা বললে পুরো শিকল ভাঙে, আর ভাঙাটা দেখা যায় শুধু বিদ্যুৎ যাওয়ার পর — তাই বহু বছর লুকিয়ে থাকতে পারে।

২০১৮ সালে PostgreSQL সম্প্রদায় দেখে, Linux-এ fsync একবার ত্রুটি দিলে পরের fsync সফল বলতে পারে, যদিও আগের লেখাগুলো হারিয়ে গেছে। PostgreSQL প্রায় দুই দশক ধরে ব্যর্থ fsync আবার চেষ্টা করত — ঠিক ভুল কাজটি। FOSDEM ২০১৯-এর মতো সম্মেলনে এ নিয়ে বক্তৃতাও হয়। সমাধান হিসেবে PostgreSQL ব্যর্থ fsync-এ থেমে গিয়ে WAL থেকে recover করে, কারণ WAL-এ সব আছে আর data page নতুন করে গড়া যায়।

গবেষণাও একই ছবি দেখায়। ২০১৪ সালের OSDI সম্মেলনে Wisconsin বিশ্ববিদ্যালয়ের একটি দলের “All File Systems Are Not Created Equal” প্রবন্ধ অনেক জনপ্রিয় অ্যাপ্লিকেশনে crash-এর পর ডেটা অসংগত হওয়ার কয়েক ডজন সুযোগ খুঁজে পায় — ফাইল সিস্টেম লেখার ক্রম যেভাবে রাখে বলে ডেভেলপাররা ধরে নেন, সবসময় সেভাবে রাখে না। ডিস্কের দিকে, consumer SSD-র একাংশে power-loss protection নেই, আর কিছু ডিভাইস flush নির্দেশ ঠিকমতো মানে না বলে বিভিন্ন পরীক্ষায় অভিযোগ উঠেছে।

ব্যবহারিক শিক্ষা: গুরুত্বপূর্ণ ডেটার জন্য power-loss protection-যুক্ত ডিস্ক, battery-backed RAID controller, আর replica রাখুন; DBMS-এর data checksum চালু রাখুন (PostgreSQL-এ initdb --data-checksums); নিয়মিত backup থেকে restore পরীক্ষা করুন।

মূল উপলব্ধি: durability কোনো একটি সফটওয়্যারের গুণ নয়, পুরো স্তূপের। প্রতিটি স্তরকে সন্দেহ করুন, আর একাধিক স্বাধীন কপিকেই আসল “stable storage” ভাবুন।

প্র ০৪একটি ই-কমার্স সাইট synchronous_commit = off দিয়ে লেখার গতি কয়েক গুণ বাড়াল। এটি কি দায়িত্বহীন? কোন কাজে মানা যায়, আর replication এলে হিসাব কেমন বদলায়?

প্রথমে ঝুঁকিটি সঠিকভাবে মাপুন। off-এ commit ফেরে WAL buffer-এ লেখার পর, ডিস্কে নয়; WAL writer সাধারণত কয়েকশো মিলিসেকেন্ডের মধ্যে লিখে ফেলে। PostgreSQL-এর ডকুমেন্টেশন বলে, ঝুঁকির জানালা সর্বোচ্চ wal_writer_delay-এর তিন গুণ। crash হলে সেই জানালার commit-গুলো হারায়, কিন্তু ডেটাবেস সংগত থাকে — কোনো অর্ধেক লেনদেন নয়, কোনো ভাঙা index নয়। এটি fsync = off থেকে মৌলিকভাবে আলাদা।

প্রশ্ন তাহলে ব্যবসার: গ্রাহককে “অর্ডার সফল” বলার পর অর্ডার হারালে কী হয়? টাকা কেটে নেওয়া হয়েছে অথচ অর্ডার নেই — এটি অগ্রহণযোগ্য। কিন্তু “কোন পণ্য কে দেখল”, session log, analytics event — এক সেকেন্ড হারালে কেউ টেরও পাবে না। PostgreSQL তাই সেটিংটি transaction স্তরে দেয়: পেমেন্টের transaction on-এ, দেখার লগ off-এ — একই ডেটাবেসে।

group commit প্রায়ই একটি মাঝামাঝি পথ: অনেক সংযোগ একসাথে commit করলে একটি fsync অনেকের কাজ করে, তাই on রেখেও throughput বাড়ে; commit_delay সেটিং এই দলবদ্ধতা বাড়াতে পারে। batching-ও সাহায্য করে — হাজারটি আলাদা INSERT-এর বদলে একটি transaction-এ হাজার সারি মানে একটি fsync.

replication যোগ হলে “টেকসই” মানে বদলায়। synchronous_commit = remote_write বা remote_apply মানে replica-ও নিশ্চিত করার পর commit — একটি পুরো সার্ভার বা ডেটা সেন্টার হারালেও commit থাকে, কিন্তু প্রতিটি commit-এ নেটওয়ার্কের যাতায়াতের সময় যোগ হয়। asynchronous replica-য় primary মরলে শেষ কিছু commit replica-য় না-ও পৌঁছাতে পারে। পর্ব ৫২ আর ৫৪-এ এই trade-off বিস্তারিত।

মূল উপলব্ধি: durability একটি মাপা যায় এমন সিদ্ধান্ত — কতটা হারানো সহ্য হয়, কতটা দেরি সহ্য হয়। “হারানো” আর “নষ্ট হওয়া” আলাদা করুন, আর সিদ্ধান্ত নিন ডেটার মূল্য দেখে।

প্র ০৫InnoDB-র undo log আছে, PostgreSQL-এর কোনো undo pass নেই। একই সমস্যার এই দুই সমাধান crash-এর পর আর প্রতিদিনের কাজে কী পার্থক্য আনে?

InnoDB সারি জায়গায় বদলায় আর পুরোনো সংস্করণ রাখে undo log-এ। ফলে crash-এর পর redo-র শেষে অসমাপ্ত transaction-গুলোকে undo log ধরে উল্টাতে হয় — ARIES-এর মতো। কয়েক কোটি সারির একটি বড় UPDATE মাঝপথে crash করলে এই rollback অনেক সময় নিতে পারে; InnoDB তাই সার্ভার খোলার পর এটি পেছনে চালায়, কিন্তু সংশ্লিষ্ট সারির lock ততক্ষণ থাকে। প্রতিদিনের কাজে undo log MVCC-র পুরোনো সংস্করণও জোগায়; দীর্ঘ খোলা transaction থাকলে undo জমে বড় হয় (history list length বাড়ে)।

PostgreSQL প্রতিটি UPDATE-এ নতুন সংস্করণ লেখে, পুরোনোটি টেবিলেই থাকে। একটি transaction বাতিল হলে তার লেখা সংস্করণ মুছতে হয় না — commit log-এ “aborted” চিহ্নই যথেষ্ট, কেউ সেগুলো দেখবে না। তাই crash-এর পর শুধু redo, আর বড় transaction-এর abort প্রায় তাৎক্ষণিক। দাম দিতে হয় অন্য জায়গায়: মৃত সংস্করণ সরাতে VACUUM লাগে, টেবিল ফুলে ওঠে (bloat), আর transaction id-র wraparound এড়াতে নিয়মিত freeze লাগে।

দুই দিকের দুর্বলতা মেটাতে পরীক্ষাও হয়েছে। PostgreSQL-এর জন্য “zheap” নামে একটি undo-ভিত্তিক storage-এর কাজ ২০১৮ সালের দিকে আলোচনায় আসে, যদিও মূল ধারায় আসেনি। SQL Server-এর Accelerated Database Recovery উল্টো দিক থেকে এসে সংস্করণ রাখে একটি স্থায়ী store-এ, যাতে বড় undo তাৎক্ষণিক হয়। Oracle বহুদিন ধরে undo segment থেকে পুরোনো সংস্করণ দিয়ে consistent read দেয় — InnoDB-র মতো ধারা।

মূল উপলব্ধি: undo-র কাজ সরিয়ে দেওয়া যায় না, শুধু সময় বদলানো যায় — InnoDB দেয় rollback-এর সময়, PostgreSQL দেয় VACUUM-এর সময়। storage আর recovery-র নকশা আলাদা বিষয় নয়, একই সিদ্ধান্তের দুই পিঠ।

অনুশীলন

একটি কোয়েরি লেখা, একটি হাতে trace, একটি নীতি চেনা, একটি হিসাব।

  1. কোয়েরি লিখুন: অংশ ৪-এর log টেবিল থেকে প্রতিটি page-এর জন্য কয়টি record (UPDATE বা CLR) সেটিকে ছুঁয়েছে আর redo-র পর তার pageLSN কত হবে (সবচেয়ে বড় LSN), বের করুন। (অংশ ৪-এর এডিটরে লিখে চালাতে পারেন।)
    SELECT page, COUNT(*) AS records, MAX(lsn) AS page_lsn_after_redo
    FROM log
    WHERE type IN ('UPDATE', 'CLR')
    GROUP BY page
    ORDER BY page;

    ফল: P1 | 2 | 70, P2 | 2 | 100, P3 | 1 | 50, P4 | 2 | 120 — redo stepper-এর শেষ ধাপের pageLSN-এর সাথে মিল। WHERE page IS NOT NULL লিখলেও একই ফল আসত, কারণ এখানে শুধু এই দুই ধরনের record-এই page আছে।

  2. হাতে trace করুন: immediate update, checkpoint নেই। log: ১ T1 X ১০→২০; ২ T2 Y ৫→৬; ৩ T1 COMMIT; ৪ T2 X ২০→৩০; ৫ T3 Z ১→২; ৬ T3 COMMIT; তারপর crash. ডিস্কে কী আছে জানা নেই। বিজয়ী আর পরাজিত কারা, redo আর undo কোন ক্রমে, আর recovery-র পর X, Y, Z কত?

    বিজয়ী T1, T3 (COMMIT আছে); পরাজিত T2. redo সামনে থেকে: ১ (X=২০), ৫ (Z=২) — ARIES হলে ২ আর ৪-ও redo হতো (Y=৬, X=৩০)। undo পেছন থেকে: ৪ (X=২০), ২ (Y=৫)। ফল X = ২০, Y = ৫, Z = ২. লক্ষ করুন T2 X লিখেছে T1-এর commit-এর পরে — strict 2PL-এ এটিই সম্ভব; T1 commit-এর আগে T2 X ছুঁতে পারত না।

  3. নীতি চিনুন: প্রতিটি ব্যবস্থা steal না no-steal, force না no-force, আর crash-এর পর UNDO, REDO কোনটি লাগে? (ক) SQLite rollback journal মোড; (খ) SQLite WAL মোড, যেখানে checkpoint শুধু committed frame মূল ফাইলে কপি করে; (গ) অংশ ২-এর stepper-এর ব্যবস্থা, যেখানে buffer ভরলে যেকোনো page লেখা হয় আর commit-এ শুধু log.
    • (ক) steal (বড় transaction-এ cache ভরলে commit-এর আগেই ডেটাবেস ফাইলে লেখা যায়, journal-এ মূল কপি রেখে) + force (commit-এ ডেটাবেস ফাইল fsync) → শুধু UNDO (hot journal থেকে মূল page ফেরানো)।
    • (খ) মূল ফাইলের দিক থেকে no-steal (অসমাপ্ত কাজ মূল ফাইলে যায় না) + no-force (commit-এ মূল ফাইল লেখা নয়) → শুধু REDO: পরের খোলায় WAL-এর committed frame পড়া; শেষ commit-চিহ্নের পরের frame উপেক্ষা।
    • (গ) steal + no-force → UNDO আর REDO দুটোই — ARIES-এর ক্ষেত্র।
  4. Checkpoint-এর হিসাব: একটি সার্ভার মিনিটে ২০ MB WAL তৈরি করে। recovery-তে log একটানা ১০০ MB/s হারে পড়া যায়। checkpoint প্রতি ৫ মিনিটে হলে আর প্রতি ৩০ মিনিটে হলে, সবচেয়ে খারাপ ক্ষেত্রে মোটামুটি কত log পড়তে হবে আর তাতে কত সময়? এই হিসাব আসল recovery-র সময়ের চেয়ে কম কেন?

    সবচেয়ে খারাপ ক্ষেত্রে crash ঠিক পরের checkpoint-এর আগে: ৫ মিনিটে ২০ × ৫ = ১০০ MB, পড়তে ~১ সেকেন্ড; ৩০ মিনিটে ৬০০ MB, ~৬ সেকেন্ড. (fuzzy checkpoint-এ redo শুরু min recLSN থেকে, যা checkpoint-এর আগেও হতে পারে — তাই আরও একটু বেশি।)

    আসল সময় বেশি, কারণ redo-তে প্রতিটি record-এর page আনতে হয় — সেগুলো random I/O, একটানা নয়। হাজারো ভিন্ন page হলে random পড়াই সময়ের বড় অংশ। এজন্য সিস্টেমগুলো পেছনে dirty page লিখে DPT ছোট রাখে, আর PostgreSQL-এর মতো কিছু সিস্টেম redo-র আগে page আগাম পড়ে রাখার (prefetch) কৌশল নেয়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমCrash-এর পর ARIES-এর restart-এর কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. master record থেকে শেষ checkpoint-এর অবস্থান পড়া
  2. analysis: checkpoint থেকে log-এর শেষ পর্যন্ত পড়ে ATT আর DPT গড়া
  3. redo: ক্ষুদ্রতম recLSN থেকে, pageLSN দেখে, সব update আর CLR আবার প্রয়োগ
  4. undo: loser-দের record বড় LSN থেকে উল্টানো, প্রতিটির জন্য CLR লিখে
  5. প্রতিটি loser-এর END লেখা আর নতুন কাজ নেওয়া শুরু

Write-ahead logging-এর নিয়মে, buffer manager একটি dirty page P ডিস্কে লেখার আগে কী নিশ্চিত করতে হয়?

  • P-কে বদলানো সব transaction commit হয়েছে
  • P-এর pageLSN পর্যন্ত সব log record ডিস্কে পৌঁছেছে
  • P লেখার আগে একটি checkpoint নেওয়া হয়েছে
  • buffer pool-এর সব dirty page একসাথে লেখা হচ্ছে

একটি DBMS commit-এর আগেও অসমাপ্ত transaction-এর page ডিস্কে লিখতে দেয়, আর commit-এর সময় data page লেখে না, শুধু log. crash-এর পর তার recovery-কে কী কী পারতে হবে?

  • কিছুই না, log থাকাই যথেষ্ট
  • শুধু UNDO
  • শুধু REDO
  • UNDO আর REDO দুটোই

ARIES-এর redo-তে LSN ৯০-এর একটি UPDATE record page P4-এর। P4 dirty page table-এ আছে (recLSN ৬০), আর ডিস্ক থেকে আনা P4-এর pageLSN ১২০. কী হবে?

  • record বাদ — পরিবর্তনটি আগেই page-এ আছে
  • record প্রয়োগ, কারণ P4 DPT-তে আছে
  • transaction committed কি না দেখে ঠিক করা
  • recLSN-এর কারণে page না এনেই বাদ

ARIES-এ undo করার সময় প্রতিটি উল্টানো UPDATE-এর জন্য একটি CLR (compensation log record) লেখা হয়। এর মূল কারণ কী?

  • log ফাইলের আকার কমানো
  • পরে CLR-টিকেও undo করা যায় যাতে
  • undo-র অগ্রগতি log-এ রাখা, যাতে আবার crash হলে কোনো কাজ দুবার উল্টানো না হয়
  • বিজয়ী transaction-এর কাজ চিহ্নিত করা

Fuzzy checkpoint সম্পর্কে কোন কথাটি সঠিক?

  • এটি সব dirty page ডিস্কে লেখে, তারপর নতুন transaction নেয়
  • এটি কাজ না থামিয়ে transaction table আর dirty page table log-এ লেখে, আর redo checkpoint-এর আগে থেকেও শুরু হতে পারে
  • এটি থাকলেও recovery-কে log-এর একেবারে শুরু থেকে পড়তে হয়
  • এটি চলমান সব transaction বাতিল করে

PostgreSQL-এ synchronous_commit = off দিলে crash-এর পর সবচেয়ে সঠিক বর্ণনা কোনটি?

  • শেষ অল্প কিছু commit হারাতে পারে, কিন্তু ডেটাবেস সংগত থাকে
  • ডেটাবেস নষ্ট হয়ে পড়ার অযোগ্য হতে পারে
  • কিছুই হারায় না, শুধু গতি বাড়ে
  • শুধু অসমাপ্ত transaction-গুলো হারায়, committed সব থাকে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ব্যর্থতা তিন রকম: transaction failure (শুধু উল্টানো), system crash (RAM হারায়, log থেকে সারানো), media failure (ডিস্ক হারায়, backup + archived log).
  2. Steal ⇒ UNDO, no-force ⇒ REDO. দ্রুততম জোড়া steal + no-force দুটোই চায়; before আর after image থাকে log-এ।
  3. WAL: page-এর আগে তার log; commit = commit record পর্যন্ত log ডিস্কে। LSN, pageLSN আর flushedLSN এই হিসাব রাখে; group commit একটি fsync-এ অনেক commit.
  4. Checkpoint আর ARIES: fuzzy checkpoint ATT আর DPT রাখে; analysis টেবিল গড়ে, redo min recLSN থেকে ইতিহাস হুবহু আবার করে (pageLSN দেখে), undo loser-দের CLR লিখে উল্টায়।
  5. বাস্তবে: PostgreSQL redo-only (MVCC) + full_page_writes, InnoDB redo + undo + doublewrite, SQLite journal বা WAL. durability নির্ভর করে fsync আর ডিস্কের সততার ওপর; synchronous_commit = off হারায়, fsync = off নষ্ট করে।

পরের পর্বে: মডিউল ৮ শুরু — ডেটাবেসের ভেতর থেকে এবার অ্যাপের দিকে। পর্ব ৪৯-এ driver, prepared statement আর connection pool: আপনার কোড কীভাবে ডেটাবেসের সাথে কথা বলে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান