Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ২/১৯কঠিন৫০ মিনিট

Buffer Pool — disk যখন memory-তে ভাড়া থাকে

Buffer Pool and Replacement Policies

Page disk-এ থাকে, কাজ হয় RAM-এ — buffer manager কীভাবে frame ভাড়া দেয়, LRU-র ভিলেন-দৃশ্য থেকে CLOCK আর PostgreSQL-র ring buffer পর্যন্ত replacement policy-র রাজনীতি, hit-rate math আর mmap debate।

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

  • Double buffering সমস্যাটা ব্যাখ্যা করে বলতে পারবেন database কেন OS pagecache-কে bypass (O_DIRECT) বা mirror করে নিজের cache চালায়
  • Buffer manager-এর pin/unpin contract, dirty flag-এর trade-off — কখন flush হয় কখন হয় না — বর্ণনা করতে পারবেন
  • LRU-র full-scan pollution সমস্যা দেখিয়ে CLOCK-এর second chance আর PostgreSQL ring buffer scan-resistance-এর পার্থক্য simulation-এ প্রমাণ করতে পারবেন
  • Hit-rate formula দিয়ে cold-vs-warm query-র order-of-magnitude ফারাক গণিত করতে পারবেন
  • mmap-as-buffer-pool debate-এর দুই পক্ষের মূল যুক্তি তুলনা করে কোন workload-এ কোনটা যুক্তিসংগত — বলতে পারবেন

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

আগে এটা বুঝি

গত লেসনে দেখলাম table = ৮ KB page-দের file। হিসাবটা একবার করুন: HDD random read ~১০ ms, SSD ~১০০ µs, RAM ~১০০ ns — হাজার–লাখ গুণ ফারাক। প্রতি query- তে disk ছুঁলে database মরে যায়। সমাধান cache — কিন্তু কে? OS-ই তো pagecache চালায়! এখানেই বড় সিদ্ধান্ত: প্রায় সব serious engine নিজের buffer pool চালায় — fixed frames-এ ভাড়া বসে ৮ KB page, নিজস্ব replacement policy; বিরূপণী O_DIRECT পর্যন্ত।

আজকের প্রশ্নগুলো:

  • Double buffering — OS pagecache থাকতেও নিজের pool কেন
  • Buffer manager API: pin/unpin + dirty flag, WAL-ordered flush-এর ইঙ্গিত
  • Policy-রাজনীতি: LRU-র পতন, CLOCK-এর second chance, PG-র ring buffer
  • Hit-rate math — cold vs warm ১০০× ফারাকের হিসাব; শেষে mmap war

মূল ধারণা

Double buffering — একই page দুইবার ভাড়া

OS pagecache খারাপ না — readahead দারুণ। সমস্যা: engine page পড়ল copy #১ kernel-cache-এ, আবার pool-এও রাখল copy #২ — একই ৮ KB দুই জায়গায়, দুই eviction-pressure, কেউ কারো খবর রাখে না।

RAM duplicate হয়, eviction decision দুই স্তরে সংঘর্ষে যায়, আর dirty page কবে disk-এ যাবে — সেই control engine-এর হাতে থাকা চাই (WAL ordering!)। উত্তর দুই দলে: PostgreSQL/InnoDB দ্বৈত খরচ মেনে চলে; Oracle/DB2-style O_DIRECT bypass; SQLite-র মতো embedded-রা উল্টো OS-কেই বিশ্বাস।

Buffer manager-এর contract

API ছোট, কিন্তু শৃঙ্খলা কঠোর:

pin(page_id) → frame      page-টা pool-এ আনো; কেউ ফেলে দিতে পারবে না
unpin(frame, dirty?)      কাজ শেষ; dirty=true মানে বদলানো হয়েছে
  • Pin count — কয়টা active user; zero হলেই কেবল eviction-এর যোগ্য
  • Dirty flag — frame ভাড়াটিয়া বদলেছে কি না; clean victim মানে eviction = শুধু ভুলে যাওয়া (disk write নেই!), dirty victim = আগে লিখতেই হবে
  • Pool full + সব frame pinned → query অপেক্ষা করে — pin leak মানে production deadlock-এর এক classic কারণ

Replacement policy — কাকে বিদায়

Pool ভরা, নতুন page দরকার — কাকে বের করব? ভবিষ্যৎ অজানা, অতীত proxy:

Policyভাবনাবাস্তব সমস্যা
FIFOসবচেয়ে পুরনো যাবেঘন-ঘন ব্যবহৃত পুরনো page-ও যায়
LRUসবচেয়ে আগে-অব্যবহৃতFull-scan pollution — নিচে
MRUসবচেয়ে নতুনটাLoop query-তে অদ্ভুত ভালো — কৌতূহলের খোরাক
CLOCKLRU-র সস্তা approximationBare form-এ pollution ঠেকায় না

LRU-র পতন — এক scan-এ পুরো pool ধ্বংস

SELECT sum(x) FROM events — ৫০ GB scan। LRU-র চোখে প্রতিটা scanned page ‘সদ্য-ব্যবহৃত’; pool ভরে যায় one-hit-wonder-দের দিয়ে, OLTP app-এর গোটা hot set বেরিয়ে যায়। Scan শেষে app ঠান্ডা pool-এ — প্রতিটা touch miss, সকালের SLA শেষ। নাম scan pollution; প্রতিষেধ scan resistance。

CLOCK — LRU-র গরিব চাচা

Exact LRU-তে প্রতি access-এ list move — lock contention। CLOCK: সব frame বৃত্তে, প্রতিটায় reference bit, এক hand। Access → bit=1 (list move নেই); victim খুঁজতে hand ঘোরে, bit 1 → 0 করে এগোয় (second chance), bit 0 পেলে victim। Sweep-এ সবার bit শেষ হয়ে যায় — CLOCK ≈ ‘আনুমানিক LRU’, ভাতা-মাসিতে। কিন্তু ৩× pool- এর scan-ও দুই lap-এ সব গিলে ফেলে — bare CLOCK pollution ঠেকায় না, দেরি করায়।

PostgreSQL-এর জবাব — ring buffer

PG-র strategy: scan-কে চিনে isolate করা। বড় sequential scan/vacuum ছোট বেসরকারি ring buffer-এ (~২৫৬ KB) ঘোরে, মূল pool ছোঁয় না; scan page-রা নিজেরাই evict হয়, hot set অক্ষত। Scan resistance = policy নয়, isolation। Build section-এ এই তিন জনের লড়াই নিজেই চালাবেন।

Hit rate — সবকিছুর হিসাব

Average read time: T = h × t_mem + (1−h) × t_disk (h = hit rate)। সংখ্যায়: t_mem = ১০০ ns, t_disk (SSD) = ১০০ µs:

hTঅনুভূতি
99%~1.1 µsস্বপ্ন
90%~10 µsঠিক আছে
50%~50 µsSSD-র দাস
0%100 µspure disk

লক্ষ্য করুন: h=99% আর h=90%-এর ফারাক ১০× — hit rate linearly পড়ে, latency exponentially বাড়ে। আর read amplification = এক logical read-এর পেছনে physical read কয়টা — index+heap fetch, miss-chain, evict-then-re-read সব মিলিয়ে।

ভেতরে কী ঘটছে

CLOCK-এর এক lap, হাতে

Pool ৪ frame, সব ref=1, hand → A দিকে:

frames:   [A=1] [B=1] [C=1] [D=1]     hand→A
victim খোঁজা (page X load):
  A: 1→0 advance   B: 1→0 advance   C: 1→0 advance   D: 1→0 advance
  A: already 0 → EVICT A, X বসে ref=1, hand→B
ফল: পুরনোদের সবাই এক chance পেল, X-ও বাঁচল — exact-LRU-র আনুমানিক নকল

InnoDB-র ভিন্ন ঔষধ — midpoint insertion

InnoDB-র ওষুধ: নতুন page MRU-end-এ না ঢুকে pool-এর মাঝখানে (new/old subchain সীমানায়) ঢোকে; old-zone-এ টিকে থাকলে তবেই পদোন্নতি — one-shot scan old-এই মরে। PG-ring-এর cousin: scan-কে hot-এলাকা থেকে দূরে রাখা।

mmap debate — এক screen-এ পুরো ঝগড়া

ভাবনা: OS pagecache-ই তো cache — file mmap করলেই তো pool লেখা লাগবে না! পক্ষে: কোড প্রায় শূন্য, memory management OS-এর; LMDB বাঁচিয়ে-বুঝিয়ে চলে। বিপক্ষে: eviction control নেই (kernel pin জানে না), write-back ordering WAL- এর সাথে খাপায় না, disk-খারাপ হলে SIGBUS, TLB-shootdown cost — আর double buffering তবু যায় না। PostgreSQL mmap ছেড়ে dynahash pool-এ গেছে; SQLite-ও mmap mode-এ caveat লেখা রাখে। মোদ্দা: correctness জড়ালে control > convenience; embedded/readonly-তে mmap এখনো রাজা。

উদাহরণ

Cold vs warm — একই query, দুই জগৎ

ধরুন orders table ২ GB, index-সহ point lookup। প্রথমবার (cold):

Index Scan using orders_pkey ...
  Buffers: shared hit=3 read=4        ← ৪ page disk থেকে (cold)
  Execution Time: 12.4 ms             ← SSD random reads dominate

দ্বিতীয়বার (warm — pool-এ সব আছে):

Index Scan using orders_pkey ...
  Buffers: shared hit=7 read=0        ← সব pool-এ!
  Execution Time: 0.08 ms             ← ১৫৫× দ্রুত

Buffers line-টাই আজকের লেসনের scoreboard: hit = buffer pool- এ পাওয়া, read = OS/disk থেকে আনা। Production tuning-এর প্রথম প্রশ্নই — গরম query-র জন্য read কি ০-এর কাছে?

pg_buffercache — pool-এর ভেতরটা live

CREATE EXTENSION pg_buffercache;
SELECT c.relname, count(*) AS buffers,
       round(100.0*count(*)/sum(count(*)) OVER (), 1) AS pct
FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c)
GROUP BY c.relname ORDER BY buffers DESC LIMIT 6;
   relname    | buffers | pct
--------------+---------+------
 events       |   61140 | 46.7
 orders       |   24388 | 18.6
 users_pkey   |     921 |  0.7
 idx_events_ts|   15302 | 11.7

এক নজরে দেখা যায় কে pool দখল করে বসেছে — আর vacuum-এর পরে কোন table-এর page-রা ‘ভুলে যাওয়া’ উচিত ছিল।

ছবিতে

                    ┌────────────────────────────────────────┐
   SELECT ──► parse/plan ──► "page 42 দাও" ──► BUFFER MANAGER
                                                  │
                       hit ◄── hash lookup: 42 আছে? ──► miss
                        │                              │
                        ▼                              ▼
                  return frame                   pin_count[42]==0 frames?
                                                       │ yes          │ no
                                                       ▼              ▼
                                              pick victim (policy)  wait/queue
                                                       │
                                        clean? ──yes──► load page 42 into frame
                                           │no                (OS read → memcpy)
                                           ▼
                                     flush dirty to disk first
                                           └──────► load, return
একটা read-এর পথের map — hit এক তীর, miss পুরো যাত্রা:
LRU (list):        newest ⇄ [E][D][C][B][A] ⇄ oldest     ← hit = move to newest
CLOCK (circle):        ┌─►[A r=1]─[B r=0]◄─ victim!
                     [F r=1]            [C r=1]           ← hit = set bit
                       └──[E r=0]──[D r=1]──┘             hand ঘোরেই খোঁজে
PG ring (scan):    main pool: [hot hot hot hot ...]
                   ring(৩২):  [s1 s2 ... s32] ↺   ← scan page রা নিজেদের মধ্যেই মরে
Replacement-দের মোড়-ফেরার নকশা — LRU list, CLOCK বৃত্ত, PG-র আলাদা রিং:

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

EXPERIMENT

pg_buffercache দেখা, pg_prewarm করা, আর cold/warm timing-এর বিশাল ফারাক

PostgreSQL 14+ (psql; Docker caveat নিচে)· ২০ মিনিট

ধাপ ০ — server লাগবে: docker run --rm -d --name pg-lab -e POSTGRES_PASSWORD=lab -p 5432:5432 postgres:16, তারপর docker exec -it pg-lab psql -U postgres।

ধাপ ১ — মাংসের dataset:

CREATE TABLE events AS
SELECT g AS id, (g % 10000)::int AS uid, md5(g::text) AS payload,
       g::timestamp + interval '1 hour' AS ts
FROM generate_series(1, 2000000) g;
CREATE INDEX ON events (uid);
ANALYZE events;

ধাপ ২ — cold ছোঁয়া: pool থেকে table বের করতে restart সবচেয়ে পরিষ্কার: docker restart pg-lab (অথবা অন্য বড় table scan করে pool ধুয়ে ফেলুন)। তারপর:

\timing on
SELECT count(*) FROM events WHERE uid = 42;    -- প্রথমবার: cold
SELECT count(*) FROM events WHERE uid = 42;    -- দ্বিতীয়বার: warm

প্রত্যাশিত: প্রথমবার ~৫–১৫ ms, দ্বিতীয়বার ~০.১–০.৫ ms — ৩০–১০০×।

ধাপ ৩ — counter চোখে:

EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM events WHERE uid = 42;

Buffers: shared hit=N read=M — warm run-এ M≈0, cold run-এ M>0। এই M-ই ধাপ ২-এর latency-র পুরো ব্যাখ্যা।

ধাপ ৪ — pool-এ কে বসে আছে + ইচ্ছে করে গরম করা:

CREATE EXTENSION IF NOT EXISTS pg_buffercache;
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
SELECT c.relname, count(*) FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c)
GROUP BY 1 ORDER BY 2 DESC LIMIT 5;
SELECT pg_prewarm('events');        -- পুরো table pool-এ ঢোকানোর নির্দেশ

Restart-এর পরে pg_prewarm('events') চালিয়ে আবার uid query — cold penalty আর নেই। Startup-এ critical table prewarm-ই production-এর classic trick।

দেখা গেল কী: performance-এর পার্থক্য CPU-র না — পুরোটাই ‘page RAM-এ ছিল কি না’-র গল্প। Hit-rate-এর এক-দশমিক পয়েন্টই p99 বদলে দেয়।

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

একই query warm pool-এ শতগুণ দ্রুত — Buffers hit/read counter আর \timing output-এ hit-rate math-এর ভবিষ্যদ্বাণী হুবহু মিলে যায়।

নিজে বানান

BUILD IT

BufferPool simulator — LRU vs CLOCK vs PG-style ring, scan-pollution প্রমাণসহ

Python · ●●●○○
  1. fixed frames array + page→frame dict — আসল manager-এর কাঠামো
  2. LRU: OrderedDict; CLOCK: ref bits + ঘোরতে থাকা hand
  3. workload: hot phase → ৩× pool-এর isolated scan → resume — চক্রে ৫ বার
  4. resume-পরবর্তী প্রথম ৩০০০ hot read-এ miss গুনুন — pollution-এর জ্বালাময়ী প্রমাণ
  5. clock+ring variant: scan page-রা ছোট private ring-এ, মূল pool অস্পৃশ্য

লক্ষ্য: policy-দের মতভেদ মুখস্থ না করে দেখা — একই workload-এ তিন manager চালিয়ে hit-rate আর resume-storm পাশাপাশি:

import random
from collections import OrderedDict

HOT_KEYS = 1_500            # app-এর hot working set


def workload(pool_size, cycles=5, hot_ops=15_000, seed=7):
    """app চলছে (hot random) → nightly-batch style একান্ত বিশাল scan (৩× pool)
    → app আবার শুরু — বাস্তব production-এর ছন্দ"""
    rng = random.Random(seed)
    for _ in range(cycles):
        for _ in range(hot_ops):
            yield ('hot', rng.randrange(HOT_KEYS))
        for j in range(pool_size * 3):
            yield ('scan', j)


class BufferPool:
    def __init__(self, nframes, policy, ring=0):
        self.n, self.policy = nframes, policy   # 'lru' | 'clock'
        self.ring_cap = ring                    # >0 = scan-bypass ring (PG-style)
        self.ring_buf, self.ring_i = [], 0
        self.page_of = [None] * nframes         # frame -> page
        self.slot = {}                          # page -> frame
        self.lru = OrderedDict()
        self.ref = [0] * nframes                # CLOCK reference bits
        self.hand = 0
        self.hits = self.misses = self.storm = 0
        self.storming = False

    def access(self, page, kind='hot'):
        if self.ring_cap and kind == 'scan':
            self._ring_access(page)             # sweep মূল pool ছোঁয় না!
            return
        if page in self.slot:
            self.hits += 1
            f = self.slot[page]
            if self.policy == 'lru':
                self.lru.move_to_end(page)
            else:
                self.ref[f] = 1
        else:
            self.misses += 1
            if self.storming:
                self.storm += 1
            self._load(page)

    def _ring_access(self, page):
        if page in self.slot or page in self.ring_buf:
            self.hits += 1
            return
        self.misses += 1
        if len(self.ring_buf) < self.ring_cap:
            self.ring_buf.append(page)
        else:
            self.ring_buf[self.ring_i] = page   # round-robin overwrite
            self.ring_i = (self.ring_i + 1) % self.ring_cap

    def _load(self, page):
        f = next((i for i in range(self.n) if self.page_of[i] is None), None)
        if f is None:
            f = self._evict()
        self.page_of[f] = page
        self.slot[page] = f
        self.ref[f] = 1                         # নতুন page ref-bit-সহ জন্মায়
        if self.policy == 'lru':
            self.lru[page] = None

    def _evict(self):
        if self.policy == 'lru':
            victim, _ = self.lru.popitem(last=False)
            f = self.slot.pop(victim)
            self.page_of[f] = None
            return f
        while True:                             # CLOCK sweep
            p = self.page_of[self.hand]
            if self.ref[self.hand]:
                self.ref[self.hand] = 0         # second chance!
                self.hand = (self.hand + 1) % self.n
            else:
                f = self.hand
                self.slot.pop(p)
                self.page_of[f] = None
                self.hand = (self.hand + 1) % self.n
                return f

    def rate(self):
        return self.hits / max(1, self.hits + self.misses) * 100


def run(policy, ring=0, pool_size=2_000):
    pool = BufferPool(pool_size, policy, ring)
    storm_left, just_scanned, warm = 0, False, False
    for kind, key in workload(pool_size):
        if kind == 'scan':
            just_scanned = True
        else:
            if just_scanned:                    # scan সবে শেষ → app resume
                just_scanned = False
                if warm:                        # প্রথম cycle warm-up বাদ
                    storm_left = 3_000
            if storm_left > 0:
                pool.storming = True
                storm_left -= 1
                if storm_left == 0:
                    pool.storming = False
            warm = True
        pool.access(key, kind)
    return pool


if __name__ == '__main__':
    POOL = 2_000
    print(f'{POOL} frames; 5 cycles: 15k hot reads -> scan {POOL*3} pages')
    print('policy       overall   resume-storm')
    for label, args in (('lru       ', ('lru', 0)),
                        ('clock     ', ('clock', 0)),
                        ('clock+ring', ('clock', 32))):
        p = run(*args)
        print(f'{label}  {p.rate():5.1f}%    {p.storm}')

প্রত্যাশিত output:

2000 frames; 5 cycles: 15k hot reads -> scan 6000 pages
policy       overall   resume-storm
lru          71.4%    5185
clock        71.4%    5185
clock+ring   77.1%    0

পড়ার নিয়ম তিনটা লাইনে: (১) LRU আর bare CLOCK হুবহু একই ভেঙে পড়ে — ৩× pool-এর scan second-chance-কেও গিলে ফেলে; (২) resume-storm ৫১৮৫ মানে প্রতিটা scan-এর পরে hot set (~১৫০০ page × ৪ cycle) পুরো আবার-পড়া; (৩) ring-এ storm শূন্য — scan-কে isolate করাই আসল scan resistance, policy নিজে নয়। PostgreSQL-এর সিদ্ধান্তের নকল।

নিজে বাড়ান: (১) pin/unpin + dirty flag যোগ করে victim-flush cost মডেল করুন (write-back হিসাব); (২) InnoDB midpoint insertion variant — নতুন page-কে half-way-তে ঢোকান; (৩) hot_ratio আর scan_len sweep করে কোথায় boundary তা heat-map করুন।

বাস্তব সিস্টেমে

Production-এ buffer pool

  • PostgreSQL: shared_buffers (default 128 MB; RAM ~২৫% common পরামর্শ); bulk ops নিজে থেকেই ring নেয় (vacuum_buffer_usage_limit); pg_buffercache/pg_prewarm built-in।
  • InnoDB: innodb_buffer_pool_size = tuning-এর প্রথম dial; old-zone ratio innodb_old_blocks_pct; restart-time pool dump/load = pg_prewarm-এর ভাই।
  • Oracle: KEEP/RECYCLE/DEFAULT pools — segment-per-pool = isolation-এর manual মোড।
  • SQL Server: buffer pool + plan cache এক জায়গা ভাগ করে — pressure-রাজনীতি অন্য মাত্রায়।

Incident pattern: রাতের batch-র forgotten full scan সকালে app-কে ঠান্ডা pool রেখে যায় — ৯টার p99 spike-এর অনেক postmortem এক গল্প। সমাধান এক: batch isolation (ring/separate pool) বা off-peak।

যে ভুলগুলো সবাই করে

“OS pagecache আছে তো — database-এর নিজের cache টাকার অপচয়।”

খরচ সত্যি, তবু উত্তর ‘বাদ’ নয়: eviction timing, pinning, WAL-ordered write-back — এই control generic pagecache দেয় না; O_DIRECT-বাড়িরাও নিজের pool-ই চালায়। mmap-only পথ readonly/embedded-তেই সীমাবদ্ধ。

“LRU-ই সেরা policy — textbook-এ সবাই তাই পড়ায়।”

Textbook theory, production scan-flood: ৩× pool scan LRU-কে গা-ঢাকসা করে। বলেই কোনো বড় engine exact-LRU চালায় না — CLOCK, midpoint, multi-pool, ring — সবাই recency ভাঙে।

“Dirty page মানে এখনই disk-এ লিখতে হবে, নইলে data হারাবে।”

Crash-এ হারাবে ঠিক, কিন্তু রক্ষা করে WAL: log record durable হলেই committed; data-page flush দেরিতেও নিরাপদ। Background writer ইচ্ছেমতো দেরি করে — তাড়াহুড়ো flush উল্টো throughput খায়। (পূর্ণ গল্প WAL লেসনে।)

“Buffer pool যত বড় তত ভালো — সব RAM দিয়ে দাও।”

Working-set-চেয়ে বড় pool-এর লাভ flat; খরচ বাড়ে — checkpoint flush-storm, লম্বা warm-up, OS-cache-র জায়গা শেষ। Sizing মাপা হয় hit-rate curve-এ, আবেগে নয়।

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

1

Buffer manager-এর দুই-call contract আর দুইটা মূল metadata — নাম করুন।

স্মরণ

pin(page) → frame আর unpin(frame, dirty?); metadata: প্রতি frame-এ pin count (eviction-যোগ্যতা) আর dirty flag (eviction-এ write লাগবে কি না)। Pool full + সব pinned = waiter queue।

2

Double buffering-এর ঠিক কী খরচ? O_DIRECT কোন সমস্যাটা সমাধান করে, কোনটা নয়?

যুক্তি

খরচ: একই page kernel-cache + pool-এ দুইবার — RAM দ্বিগুণ, দুই eviction- system একে অপরের বিরুদ্ধে। O_DIRECT kernel-cache skip করে duplication সরায়; কিন্তু prefetch/admission/eviction সব engine-এর দায় — ওই জটিলতা কোথাও যায় না, ঘরেই থাকে।

3

t_mem = 100 ns, t_SSD = 100 µs। h=99% বনাম h=90% average read time? কেন এই nonlinearity production-এ ভয়ংকর?

প্রয়োগ

h=99%: 0.99×100ns + 0.01×100µs ≈ 1.1 µs; h=90%: 0.9×100ns + 0.1×100µs ≈ 10 µs — ৯% hit-drop = ~৯× latency। Nonlinear কারণ miss-দাম hit-দামের হাজার গুণ; p99 tail-এ miss-cluster জমে আরও খারাপ।

4

Bare CLOCK ৩× pool-এর scan ঠেকাতে পারে না কেন — ref-bit mechanics-এ ধরে ব্যাখ্যা করুন। তাহলে CLOCK লেখাই/তো লাগে কেন?

যুক্তি

Scan page-রাও bit=1 নিয়ে ঢোকে; hand-এর প্রথম pass-এ সবার bit clear হয়, দ্বিতীয় pass-এ eviction — ৩× load মানে ≥২ lap সহজেই, hot page-ও শেষে victim। CLOCK-এর লাভ exact-order maintenance না করে LRU-approximation — কম lock/overhead; scan-resistance আলাদা mechanism (ring/isolation)-এ আসে।

5

PostgreSQL ring buffer আর InnoDB midpoint insertion — দুটোই scan-resistant, কিন্তু কৌশলে কোথায় ভিন্ন? কোনটা কবে বেশি সুবিধা দেয়?

যুক্তি

Ring: scan-কে pool থেকে বাইরে ছোট private space-এ বন্দি — মূল pool zero-touch। Midpoint: scan page pool-এ ঢোকে, কিন্তু old-zone-এ — promote হতে হলে দ্বিতীয়বার দর্শক লাগে। Ring বেশি রক্ষক যখন scan বিশাল ও one-shot; midpoint ভালো যখন scan-data-র পুনর্ব্যবহার সম্ভাবনা থাকে (তাকে new-zone-এ ওঠার সুযোগই দেয়)।

6

আপনার service: OLTP API (working set ২ GB) + রাত ২টায় ৮০ GB reporting scan, RAM ৩২ GB। shared_buffers sizing + scan handling — design করুন।

ডিজাইন

shared_buffers ~৮ GB (২ GB working set-এর ৪× headroom, বাকিটা OS/other- এর জন্য)। Reporting-কে dedicated role/connection-এ চালান — PG নিজেই bulk scan ring-এ পাঠায়; একদম নিশ্চিত হতে রাতের job-এর পরে critical table pg_prewarm, বা আলাদা replica-তে reporting সরান। Monitor: pg_stat_io / pg_buffercache-তে hot tables-এর steady residency।

এরপর কী

এরপর কী

Pool গরম, page ভাড়ায় বসা — কিন্তু lookup-এর উত্তর এখনো হয় পুরো table scan। এবার সেই পুরনো বন্ধু disk-জগতের পোশাকে: B+ tree index। পরের লেসনে:

  • PostgreSQL nbtree-র পেজ-চিড়িয়া: metapage, root, internal, leaf chain
  • High key, deduplication, ctid — index entry আসলে কী ধরে রাখে
  • Index scan vs index-only scan — visibility map-এর গোপন ভূমিকা
  • প্রতিটা INSERT-এর ওপর index-র খাজনা, delete-এ bloat — আর EXPLAIN (ANALYZE, BUFFERS)-এ সব চোখে দেখা

গত লেসনের hit=7 read=0 দেখেছেন? পরের লেসনে সেই ৭ page-এর ভেতরে ঠিক কী ঘটে, step by step।

আরও পড়ুন

  • PostgreSQL Documentation — Buffer Manager / shared_buffers — The PostgreSQL Global Development Group · Ring buffer, bulk-read strategy, pg_buffercache — production-এর প্রামাণ্য বর্ণনা
  • Database Internals — Chapter 8: Buffer Pool (Alex Petrov) — Alex Petrov · Buffer manager API, eviction policy-র taxonomy — এই লেসনের মেরুদণ্ড
  • SQLite mmap debate — '30% faster than mmap' ও related mailing-list threads — SQLite developers · mmap-as-cache-এর বাস্তব পাল্টা-যুক্তি — realworld section-এর উৎস