PostgreSQL-04-Storage-存储引擎

模块概览

Storage 模块负责 PostgreSQL 的底层数据存储与管理,包括缓冲区管理(Buffer Manager)、磁盘 I/O、页面组织、共享内存管理等核心功能。

核心能力

  1. 缓冲区管理(Buffer Manager):管理共享内存中的数据页缓存
  2. 磁盘 I/O:异步/同步读写磁盘页面
  3. 页面组织:8KB 页面格式、空闲空间管理
  4. 共享内存:进程间数据共享、锁管理
  5. 文件管理:虚拟文件描述符(VFD)管理

架构图

flowchart TB
    subgraph Storage["Storage 存储引擎"]
        BufMgr[Buffer Manager<br/>缓冲区管理] --> BufPool[Buffer Pool<br/>共享缓冲区]
        BufMgr --> BufTable[Buffer Mapping Table<br/>页面查找哈希表]
        BufMgr --> FreeList[Free List<br/>空闲缓冲区链表]
        
        BufMgr --> DiskIO[磁盘 I/O]
        DiskIO --> SMGR[SMGR<br/>存储管理器接口]
        SMGR --> MD[MD<br/>磁盘文件管理]
        
        BufMgr --> LockMgr[Lock Manager<br/>Buffer锁管理]
        
        subgraph Page["页面结构"]
            PageHeader[Page Header<br/>页头]
            ItemIds[Item Pointers<br/>行指针数组]
            Tuples[Tuples<br/>元组数据]
            Special[Special Space<br/>索引特殊空间]
        end
    end
    
    subgraph Access["访问层"]
        HeapAM[Heap AM<br/>堆表访问]
        IndexAM[Index AM<br/>索引访问]
    end
    
    Access --> BufMgr
    MD --> Disk[(磁盘文件)]

核心数据结构

1. BufferDesc(缓冲区描述符)

/* src/include/storage/buf_internals.h */
typedef struct BufferDesc
{
    BufferTag   tag;            /* 页面标识(关系OID + 块号) */
    int         buf_id;         /* 缓冲区编号(0-NBuffers-1) */
    
    pg_atomic_uint32 state;     /* 状态标志位(脏页、有效、锁等) */
    int         wait_backend_pgprocno;  /* 等待此缓冲区的进程 */
    
    int         freeNext;       /* 空闲链表下一个 */
    
    LWLock      content_lock;   /* 内容锁(读写锁) */
    LWLock      io_in_progress_lock;  /* I/O 进行中锁 */
} BufferDesc;

typedef struct BufferTag
{
    Oid         spcOid;         /* 表空间 OID */
    Oid         dbOid;          /* 数据库 OID */
    RelFileNumber relNumber;    /* 关系文件号 */
    ForkNumber  forkNum;        /* Fork 类型(主数据/FSM/VM) */
    BlockNumber blockNum;       /* 块号 */
} BufferTag;

状态标志位

#define BM_DIRTY        (1U << 0)   /* 脏页标志 */
#define BM_VALID        (1U << 1)   /* 有效页标志 */
#define BM_TAG_VALID    (1U << 2)   /* Tag 有效 */
#define BM_IO_IN_PROGRESS (1U << 3) /* I/O 进行中 */
#define BM_LOCKED       (1U << 4)   /* 内容锁定 */
#define BM_PERMANENT    (1U << 5)   /* 永久缓冲区(系统表) */

2. 页面结构(Page Layout)

/* 8KB 页面布局 */
typedef struct PageHeaderData
{
    PageXLogRecPtr pd_lsn;      /* WAL 日志序列号(8 字节) */
    uint16      pd_checksum;    /* 页面校验和 */
    uint16      pd_flags;       /* 标志位 */
    LocationIndex pd_lower;     /* 空闲空间起始位置 */
    LocationIndex pd_upper;     /* 空闲空间结束位置 */
    LocationIndex pd_special;   /* 特殊空间起始位置 */
    uint16      pd_pagesize_version; /* 页面大小和版本 */
    TransactionId pd_prune_xid; /* 最早需要清理的事务ID */
    ItemIdData  pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* 行指针数组 */
} PageHeaderData;

/* 行指针(4 字节) */
typedef struct ItemIdData
{
    unsigned    lp_off:15;      /* 元组偏移(字节) */
    unsigned    lp_flags:2;     /* 标志(未使用/正常/重定向/死亡) */
    unsigned    lp_len:15;      /* 元组长度(字节) */
} ItemIdData;

页面布局示意

+------------------+ 0
| Page Header      | 24 字节
+------------------+
| Item Pointers    | pd_lower
| (growing down)   |
+------------------+
|   Free Space     |
+------------------+
| Tuples           | pd_upper
| (growing up)     |
+------------------+
| Special Space    | pd_special
+------------------+ 8192

核心功能实现

功能 1:缓冲区查找与分配

ReadBuffer() - 读取页面

/* src/backend/storage/buffer/bufmgr.c */
Buffer
ReadBuffer(Relation reln, BlockNumber blockNum)
{
    return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum,
                              RBM_NORMAL, NULL);
}

Buffer
ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
                   ReadBufferMode mode, BufferAccessStrategy strategy)
{
    BufferDesc *bufHdr;
    Block       bufBlock;
    bool        found;
    
    /* 1. 构造 BufferTag */
    BufferTag newTag;
    INIT_BUFFERTAG(newTag, reln->rd_smgr->smgr_rlocator.locator,
                   forkNum, blockNum);
    
    /* 2. 在缓冲池中查找页面 */
    bufHdr = BufferAlloc(reln->rd_smgr, reln->rd_rel->relpersistence,
                         forkNum, blockNum, strategy, &found, NULL);
    
    if (found)
    {
        /* 缓存命中,直接返回 */
        return BufferDescriptorGetBuffer(bufHdr);
    }
    
    /* 3. 缓存未命中,从磁盘读取 */
    bufBlock = BufHdrGetBlock(bufHdr);
    
    smgrread(reln->rd_smgr, forkNum, blockNum, bufBlock);
    
    /* 4. 标记为有效 */
    pg_atomic_fetch_or_u32(&bufHdr->state, BM_VALID);
    
    return BufferDescriptorGetBuffer(bufHdr);
}

BufferAlloc() - 分配缓冲区

static BufferDesc *
BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
            BlockNumber blockNum, BufferAccessStrategy strategy,
            bool *foundPtr, IOContext io_context)
{
    BufferTag newTag;
    uint32 newHash;
    int buf_id;
    BufferDesc *buf;
    bool found;
    
    /* 1. 计算哈希值 */
    INIT_BUFFERTAG(newTag, smgr->smgr_rlocator.locator, forkNum, blockNum);
    newHash = BufTableHashCode(&newTag);
    
    /* 2. 在哈希表中查找 */
    LWLockAcquire(BufMappingPartitionLock(newHash), LW_EXCLUSIVE);
    
    buf_id = BufTableLookup(&newTag, newHash);
    if (buf_id >= 0)
    {
        /* 找到,增加引用计数 */
        buf = GetBufferDescriptor(buf_id);
        PinBuffer(buf);
        *foundPtr = true;
        LWLockRelease(BufMappingPartitionLock(newHash));
        return buf;
    }
    
    /* 3. 未找到,从空闲链表获取缓冲区 */
    buf = GetFreeBuffer(strategy);
    
    /* 4. 如果缓冲区是脏页,先写回磁盘 */
    if (pg_atomic_read_u32(&buf->state) & BM_DIRTY)
    {
        FlushBuffer(buf, NULL, IOOBJECT_RELATION, io_context);
    }
    
    /* 5. 插入哈希表 */
    buf->tag = newTag;
    BufTableInsert(&newTag, newHash, buf->buf_id);
    
    *foundPtr = false;
    LWLockRelease(BufMappingPartitionLock(newHash));
    
    return buf;
}

时钟扫描算法(Clock-Sweep)

PostgreSQL 使用时钟扫描算法选择牺牲页:

static BufferDesc *
StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state,
                  bool *from_ring)
{
    BufferDesc *buf;
    int trycounter;
    uint32 local_buf_state;
    
    /* 时钟指针,全局共享 */
    static pg_atomic_uint32 StrategyControl_nextVictimBuffer;
    
    for (trycounter = NBuffers; trycounter > 0; trycounter--)
    {
        /* 获取下一个缓冲区(时钟前进) */
        buf = GetBufferDescriptor(pg_atomic_fetch_add_u32(
            &StrategyControl_nextVictimBuffer, 1) % NBuffers);
        
        local_buf_state = LockBufHdr(buf);
        
        /* 检查是否可替换 */
        if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0)
        {
            if (BUF_STATE_GET_USAGECOUNT(local_buf_state) > 0)
            {
                /* 使用计数 > 0,递减后继续 */
                local_buf_state -= BUF_USAGECOUNT_ONE;
                UnlockBufHdr(buf, local_buf_state);
            }
            else
            {
                /* 找到牺牲页 */
                *buf_state = local_buf_state;
                return buf;
            }
        }
        else
        {
            /* 被 Pin,不能替换 */
            UnlockBufHdr(buf, local_buf_state);
        }
    }
    
    /* 未找到可用缓冲区 */
    elog(ERROR, "no unpinned buffers available");
    return NULL;
}

时钟算法原理

  • 使用计数(Usage Count):每次访问页面时增加(最大 5)
  • 时钟扫描:从当前位置顺序扫描缓冲区
    • 使用计数 > 0:递减,继续扫描
    • 使用计数 = 0 且未被 Pin:选为牺牲页
  • 优势:近似 LRU,开销低(O(1)),无需维护链表

功能 2:脏页管理

MarkBufferDirty() - 标记脏页

void
MarkBufferDirty(Buffer buffer)
{
    BufferDesc *bufHdr;
    uint32 old_buf_state;
    
    if (!BufferIsValid(buffer))
        elog(ERROR, "bad buffer ID");
    
    if (BufferIsLocal(buffer))
    {
        /* 本地缓冲区(临时表) */
        MarkLocalBufferDirty(buffer);
        return;
    }
    
    bufHdr = GetBufferDescriptor(buffer - 1);
    
    /* 设置脏页标志 */
    old_buf_state = pg_atomic_fetch_or_u32(&bufHdr->state, BM_DIRTY);
    
    /* 如果是第一次标记为脏,更新统计 */
    if (!(old_buf_state & BM_DIRTY))
        pgstat_count_buffer_write(bufHdr);
}

FlushBuffer() - 刷脏页

static void
FlushBuffer(BufferDesc *buf, SMgrRelation reln,
            IOObject io_object, IOContext io_context)
{
    XLogRecPtr recptr;
    ErrorContextCallback errcallback;
    Block bufBlock;
    
    /* 1. 获取页面 LSN */
    bufBlock = BufHdrGetBlock(buf);
    recptr = BufferGetLSN(buf);
    
    /* 2. 确保 WAL 已刷盘(Write-Ahead Logging) */
    XLogFlush(recptr);
    
    /* 3. 写入磁盘 */
    smgrwrite(reln,
              BufTagGetForkNum(&buf->tag),
              buf->tag.blockNum,
              bufBlock,
              false);
    
    /* 4. 清除脏页标志 */
    pg_atomic_fetch_and_u32(&buf->state, ~BM_DIRTY);
    
    /* 5. 更新统计 */
    pgstat_count_io_op(IOOBJECT_RELATION, IOCONTEXT_NORMAL, IOOP_WRITE);
}

WAL-Logging 规则

  • 规则:页面的 LSN ≤ WAL 刷盘位置
  • 保证:崩溃恢复时,WAL 包含所有已落盘页面的修改
  • 实现:刷脏页前调用 XLogFlush(page_lsn)

功能 3:页面组织与管理

PageAddItem() - 插入行指针

OffsetNumber
PageAddItem(Page page, Item item, Size size, OffsetNumber offsetNumber,
            bool overwrite, bool is_heap)
{
    PageHeader phdr = (PageHeader) page;
    Size alignedSize;
    int lower;
    int upper;
    ItemId itemId;
    OffsetNumber limit;
    
    /* 1. 对齐大小(8 字节对齐) */
    alignedSize = MAXALIGN(size);
    
    /* 2. 检查空闲空间 */
    lower = phdr->pd_lower;
    upper = phdr->pd_upper;
    
    if (offsetNumber == InvalidOffsetNumber)
    {
        /* 追加模式:分配新行指针 */
        limit = OffsetNumberNext(PageGetMaxOffsetNumber(page));
        if (limit > MaxHeapTuplesPerPage)
            return InvalidOffsetNumber;
        offsetNumber = limit;
    }
    
    /* 计算需要的空间 */
    Size needSpace = alignedSize + sizeof(ItemIdData);
    if (upper < lower + needSpace)
        return InvalidOffsetNumber;  /* 空间不足 */
    
    /* 3. 写入元组数据(从上往下) */
    upper -= alignedSize;
    memcpy((char *) page + upper, item, size);
    
    /* 4. 设置行指针(从下往上) */
    itemId = PageGetItemId(page, offsetNumber);
    itemId->lp_off = upper;
    itemId->lp_len = size;
    itemId->lp_flags = LP_NORMAL;
    
    /* 5. 更新页头 */
    phdr->pd_lower = lower + sizeof(ItemIdData);
    phdr->pd_upper = upper;
    
    return offsetNumber;
}

性能优化

1. shared_buffers 调优

# postgresql.conf
shared_buffers = 16GB          # 推荐系统内存的 25%

监控缓存命中率

SELECT 
    sum(heap_blks_hit) / nullif(sum(heap_blks_hit + heap_blks_read), 0) AS cache_hit_ratio
FROM pg_statio_user_tables;

-- 结果应 > 0.99(99% 命中率)

2. effective_io_concurrency

# SSD 环境
effective_io_concurrency = 200  # 并发 I/O 能力

3. 检查点调优

checkpoint_timeout = 15min      # 检查点间隔
max_wal_size = 10GB            # 触发检查点的 WAL 量
checkpoint_completion_target = 0.9  # 平滑 I/O

总结

Storage 模块是 PostgreSQL 的基础设施,通过高效的缓冲区管理、页面组织和 I/O 调度,支撑上层的事务处理与查询执行。关键设计包括:

  1. 时钟扫描算法:高效的页面替换策略
  2. WAL-Logging:保证数据持久性
  3. 页面结构:灵活的行指针数组,支持就地更新
  4. 共享内存:进程间高效数据共享