網(wǎng)站banner效果自己建立網(wǎng)站步驟
文章目錄
- 共享緩沖區(qū)基礎(chǔ)知識(shí)
- 邏輯讀和物理讀
- LRU算法和CLOCK時(shí)鐘算法
- 共享緩沖區(qū)管理器結(jié)構(gòu)
- 共享緩沖表層
- 共享緩沖區(qū)描述符層
- 共享緩沖頁層
- 共享緩沖區(qū)管理器工作流程
- 初始化緩沖區(qū)
- 讀緩沖區(qū)
- 淘汰策略
- 共享緩沖區(qū)鎖
共享緩沖區(qū)基礎(chǔ)知識(shí)
通常數(shù)據(jù)庫系統(tǒng)都會(huì)在內(nèi)存中預(yù)留buffer緩沖空間用于提升讀寫效率,因?yàn)榕c內(nèi)存讀寫交互效率遠(yuǎn)大于與磁盤的讀寫效率,而緩沖區(qū)管理器管理著緩沖區(qū)內(nèi)存空間,將熱數(shù)據(jù)加載到內(nèi)存中以減少直接的磁盤讀寫,并維持這部分?jǐn)?shù)據(jù)在緩沖池中的狀態(tài)/鎖管理,充當(dāng)著讀寫進(jìn)程和操作系統(tǒng)之間的協(xié)同者角色;
共享緩沖區(qū)管理器在設(shè)計(jì)上需要考慮的核心問題是:緩沖區(qū)大小設(shè)計(jì)+提升緩沖區(qū)命中率+制定合理緩沖區(qū)回收策略+保證多并發(fā)下的一致性問題。
在開始需要先了解一些基礎(chǔ)知識(shí)。
邏輯讀和物理讀
邏輯讀和物理讀的區(qū)別在pg后臺(tái)進(jìn)程獲取數(shù)據(jù)的過程中是否涉及到磁盤讀,邏輯讀過程中pg后臺(tái)進(jìn)程直接讀從共享緩沖區(qū)獲取到的數(shù)據(jù)頁,物理讀需要從借助操作系統(tǒng)從磁盤讀取數(shù)據(jù)并加載到內(nèi)存中再返回給讀寫程序。
做個(gè)實(shí)驗(yàn)先創(chuàng)建表并插入數(shù)據(jù),通過expain analyze分析看一下物理讀和邏輯讀的差異。
postgres=# create table yzg(a int ,b varchar);
postgres=# insert into yzg (1,'a');
postgres=# insert into yzg select * from yzg;
INSERT 0 1
...
postgres=# insert into yzg select * from yzg;
INSERT 0 524288
構(gòu)造查詢語句SELECT * FROM yzg,發(fā)現(xiàn)命中了緩沖池中的4640個(gè)頁(這里的yzg整個(gè)表大小也是4640個(gè)頁,因?yàn)樾陆ǖ膒g環(huán)境沒有其他并行連接和查詢),這里就是邏輯讀的過程;
postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM yzg;QUERY PLAN
---------------------------------------------------------------------------------------------------------------Seq Scan on yzg (cost=0.00..15125.76 rows=1048576 width=6) (actual time=0.015..140.675 rows=1048576 loops=1)Buffers: shared hit=4640Planning Time: 0.051 msExecution Time: 222.198 ms
(4 rows)postgres=# SELECT
postgres-# nspname AS schema_name,
postgres-# relname AS table_name,
postgres-# pg_total_relation_size(C.oid) AS total_size, -- 包括表、索引、toast表等的總大小
postgres-# pg_relation_size(C.oid) AS heap_size, -- 表的堆大小
postgres-# (pg_relation_size(C.oid) / (1024 * 8))::bigint AS pages -- 表占用的頁面數(shù)(假設(shè)頁面大小為8KB)
postgres-# FROM g_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE nspname || '.' || relname = 'public.yzg';schema_name | table_name | total_size | heap_size | pages
-------------+------------+------------+-----------+-------public | yzg | 38060032 | 38010880 | 4640
(1 row)
如果我們把數(shù)據(jù)庫down掉后再啟動(dòng)數(shù)據(jù)庫,緩沖區(qū)數(shù)據(jù)就會(huì)被清理,再看執(zhí)行計(jì)劃產(chǎn)生了物理讀;
postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM yzg;QUERY PLAN
---------------------------------------------------------------------------------------------------------------Seq Scan on yzg (cost=0.00..15125.76 rows=1048576 width=6) (actual time=0.032..161.956 rows=1048576 loops=1)Buffers: shared read=4640Planning:Buffers: shared hit=15 read=8Planning Time: 2.666 msExecution Time: 243.906 ms
(6 rows)
LRU算法和CLOCK時(shí)鐘算法
將磁盤數(shù)據(jù)頁block加載到內(nèi)存以提升查詢效率的必要性已經(jīng)不言而喻了,但是通常內(nèi)存肯定沒有磁盤空間充足,其能夠緩存的數(shù)據(jù)空間有限,需要一定策略來決定當(dāng)內(nèi)存滿時(shí)應(yīng)該替換掉哪些頁面,也就是緩存置換算法。
LRU(Least Recently Used)算法是一種常用的決定當(dāng)內(nèi)存滿時(shí)應(yīng)該替換掉哪個(gè)頁面的置換算法,其核心思想是優(yōu)先淘汰最近最少使用的頁面,假設(shè)如果一個(gè)頁面最近被訪問過那么它很可能很快會(huì)被再次訪問,反之如果一個(gè)頁面很久沒被訪問那么它很可能在未來也不會(huì)被訪問。
CLOCK時(shí)鐘算法是另一種在數(shù)據(jù)庫緩沖池中使用的更節(jié)省資源的LRU替代方案,它試圖在訪問歷史和內(nèi)存中維持一個(gè)平衡,其再內(nèi)存頁面上維護(hù)一個(gè)“引用位”(reference bit)來判斷頁面是否最近被訪問過,不需要維護(hù)完整的訪問歷史鏈表,而是通過簡單的位標(biāo)志來判斷頁面的使用情況,在實(shí)現(xiàn)上更加簡單消耗的資源也更少但可能不如 LRU 算法精確。
兩者都是緩存置換算法,但CLOCK算法在實(shí)現(xiàn)上更簡單,并且可以減少內(nèi)存訪問次數(shù),但LRU算法在實(shí)現(xiàn)上更復(fù)雜,pg使用時(shí)鐘算法,oracle使用LRU算法。
共享緩沖區(qū)管理器結(jié)構(gòu)
共享內(nèi)存緩沖區(qū)可以被數(shù)據(jù)庫多個(gè)子進(jìn)程共享訪問,緩沖區(qū)管理器維護(hù)著這塊緩沖區(qū)的數(shù)據(jù)一致性并返回真實(shí)的數(shù)據(jù)頁page,按照《PostgreSQL指南》將緩沖區(qū)管理器分三層:緩沖表、緩沖區(qū)描述符、緩沖頁,但這里的層次劃分是為了便于理解而進(jìn)行的邏輯劃分,如下圖。
在代碼上共享內(nèi)存緩沖區(qū)的目錄地址是src/backend/storage/buffer/,總代碼量6000行+,其中:
1.buf_init.c 是緩沖區(qū)管理器的初始化入口,在啟動(dòng)數(shù)據(jù)庫進(jìn)程時(shí)候會(huì)初始化并分配1個(gè)共享內(nèi)存緩沖區(qū),并初始化緩沖表、緩沖區(qū)描述符和緩沖頁。
2.buf_table.c 是緩沖表管理器,維護(hù)著緩沖區(qū)管理器中緩沖區(qū)的索引,緩沖區(qū)管理器通過緩沖表獲取緩沖區(qū)描述符的索引項(xiàng)。
3.bufmgr.c 是緩沖區(qū)管理器核心代碼,處理緩沖區(qū)管理器中緩沖區(qū)的訪問并返回真實(shí)的數(shù)據(jù)頁page。
4.freelist.c 是緩沖區(qū)管理器中緩沖區(qū)的淘汰策略,緩沖區(qū)不夠用時(shí)調(diào)用淘汰策略來淘汰緩沖區(qū)。
5.localbuf.c 是本地緩沖區(qū)管理器,用于管理本地緩沖區(qū),本地緩沖區(qū)是進(jìn)程私有的緩沖區(qū),用于緩存進(jìn)程自己訪問的數(shù)據(jù).。
共享緩沖表層
共享緩沖表層代碼實(shí)現(xiàn)在buf_table.c文件中,其函數(shù)都是針對(duì)hash表(dynahash.c)的查/增/刪操作,這個(gè)hash表是全局共享的在InitBufferPool()中創(chuàng)建。
其中BufTableHashCode函數(shù)將在hash表中找到入?yún)uffer_tags對(duì)應(yīng)的buffer_id:
uint32 BufTableHashCode(BufferTag *tagPtr)
{return get_hash_value(SharedBufHash, (void *) tagPtr);
}
buffer_tag數(shù)據(jù)庫子進(jìn)程對(duì)于共享緩沖區(qū)的輸入,能夠唯一標(biāo)識(shí)請(qǐng)求的page頁,其中RelFileNode的屬性分別是表對(duì)象oid/數(shù)據(jù)塊oid/表空間oid,頁面的forknumber(分別為0、1、2)(0=表和索引塊,1=fsm,2=vm)頁面number(頁面屬于哪個(gè)塊)。例如{(16888, 16389, 39920), 0, 8}標(biāo)簽表示在某個(gè)表空間(oid=16888)某個(gè)數(shù)據(jù)庫(oid=16389)的某表(oid=39920)的0號(hào)分支( 0代表關(guān)系表本體)的第8號(hào)頁面。
// 映射關(guān)系
typedef struct
{BufferTag key; /* Tag of a disk page */int id; /* Associated buffer ID */
} BufferLookupEnt;
// BufferTag結(jié)構(gòu)定義在buf_internals.h中,可以唯一標(biāo)識(shí)數(shù)據(jù)頁
typedef struct buftag
{RelFileNode rnode; /* physical relation identifier */ForkNumber forkNum;BlockNumber blockNum; /* blknum relative to begin of reln */
} BufferTag;
// 關(guān)系定義包含表空間id+dbid+表id
typedef struct RelFileNode
{Oid spcNode; /* tablespace */Oid dbNode; /* database */Oid relNode; /* relation */
} RelFileNode;
共享緩沖區(qū)描述符層
共享緩沖區(qū)描述符層也維護(hù)了1個(gè)BufferDesc元素構(gòu)成的BufferDescriptors數(shù)組,該數(shù)組是所有數(shù)據(jù)庫進(jìn)程共享的,數(shù)組創(chuàng)建后由BufferStrategyControl負(fù)責(zé)管理,每個(gè)BufferDesc包含BufferTag和buf_id信息以及freeNext。
其中有個(gè)概念是freelist數(shù)組用于緩存空閑的BufferDesc,是BufferDescriptors數(shù)組中某些描述符的引用,當(dāng)緩沖池管理器需要一個(gè)空閑的緩沖區(qū)時(shí)會(huì)首先檢查freelist并從中取出空閑緩沖區(qū)的引用,這個(gè)描述符將從freelist中移除并被標(biāo)記為已分配狀態(tài),當(dāng)一個(gè)緩沖區(qū)不再需要時(shí)它的描述符會(huì)被放回freelist中。
共享緩沖頁層
共享緩沖頁層即buffers[]數(shù)組,存儲(chǔ)真實(shí)的數(shù)據(jù)頁page,并與共享描述符一一對(duì)應(yīng)。
共享緩沖區(qū)管理器工作流程
初始化緩沖區(qū)
shared_buffers參數(shù)是基于系統(tǒng)內(nèi)存大小動(dòng)態(tài)計(jì)算的,對(duì)于<8GB內(nèi)存的系統(tǒng)默認(rèn)值為 128MB,對(duì)于>8GB,<16GB內(nèi)存的系統(tǒng)默認(rèn)值為系統(tǒng)內(nèi)存的 1/4,對(duì)于>16GB內(nèi)存的系統(tǒng)默認(rèn)值為 4GB。
postgres=# show shared_buffers ;shared_buffers
----------------128MB
初始化流程如下:
main(int argc, char *argv[])
--> PostmasterMain(argc, argv);--> reset_shared(); // Set up shared memory and semaphores.--> CreateSharedMemoryAndSemaphores(); // Creates and initializes shared memory and semaphores.--> CalculateShmemSize(&numSemas); // Calculates the amount of shared memory and number of semaphores needed.--> add_size(size, BufferShmemSize());--> PGSharedMemoryCreate(size, &shim);--> InitShmemAccess(seghdr);--> InitBufferPool(); // 初始化緩沖池// 1. 初始化buffer descriptor--> ShmemInitStruct("Buffer Descriptors",NBuffers * sizeof(BufferDescPadded),&foundDescs); // 2. 初始化buffer pool--> ShmemInitStruct("Buffer Blocks", NBuffers * (Size) BLCKSZ, &foundBufs);--> StrategyInitialize(!foundDescs); // 3. 初始化 buffer table--> InitBufTable(NBuffers + NUM_BUFFER_PARTITIONS);--> ShmemInitHash("Shared Buffer Lookup Table", size, size, &info, HASH_ELEM | HASH_BLOBS | HASH_PARTITION);--> hash_create(name, init_size, infoP, hash_flags);
讀緩沖區(qū)
讀取緩沖區(qū)的流程比較簡單,緩沖表根據(jù)輸入(buftag)輸出緩沖區(qū)描述符buf_id并找到緩沖區(qū)描述符,若buf_id值大于0表示命中,命中后pin住該頁使之不能被淘汰掉,修改BufferDesc->state值,refcount+1,usage+1,沒有命中根據(jù)淘汰策略淘汰頁再從磁盤讀取頁到緩沖區(qū)槽位,然后根據(jù)緩存區(qū)槽位返回對(duì)應(yīng)的buf_id。
數(shù)據(jù)庫子進(jìn)程調(diào)用ReadBufferExtended ->ReadBuffer_common函數(shù)讀取buffer,其中ReadBuffer_common會(huì)調(diào)用核心函數(shù)BufferAlloc來真正意義上進(jìn)行緩沖區(qū)掃描與加載,其流程圖如下:
函數(shù)梳理:
Buffer ReadBuffer(Relation reln, BlockNumber blockNum)
--> ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);--> ReadBuffer_common(RelationGetSmgr(reln), reln->rd_rel->relpersistence,forkNum, blockNum, mode, strategy, &hit);--> BufferAlloc(smgr, relpersistence, forkNum, blockNum, strategy, &found);--> INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); /* create a tag so we can lookup the buffer */--> BufTableHashCode(&newTag); // 哈希函數(shù)輸入buftag,輸出哈希值--> get_hash_value(SharedBufHash, (void *) tagPtr);--> buf_id = BufTableLookup(&newTag, newHash); // 根據(jù)buftag,查找緩沖表,獲得bug_id,如果命中的話,如果找不到,返回-1--> hash_search_with_hash_value(SharedBufHash,(void *) tagPtr,hashcode,HASH_FIND,NULL);// 如果命中,返回,如果沒有命中,繼續(xù)執(zhí)行--> StrategyGetBuffer(strategy, &buf_state); // 獲取一個(gè)空閑可用的buffer,返回bufferdesc, 默認(rèn)策略是NULL--> GetBufferFromRing(strategy, buf_state);--> BufTableInsert(&newTag, newHash, buf->buf_id); // 將新獲取的buf_id,插入到緩沖表中--> smgrread(smgr, forkNum, blockNum, (char *) bufBlock); // 從磁盤讀到buffer
pinbuffer函數(shù)用于“pin”一個(gè)緩沖區(qū),當(dāng)緩沖區(qū)被pin時(shí)意味著它正被一個(gè)或多個(gè)事務(wù)或查詢使用,因此不能被緩沖池管理器淘汰或替換:
startBufferIo函數(shù)用于啟動(dòng)一個(gè)緩沖區(qū)I/O操作,當(dāng)緩沖區(qū)中的數(shù)據(jù)需要從磁盤讀取或?qū)懭氪疟P時(shí)StartBufferIO()會(huì)被用來發(fā)起從磁盤讀取數(shù)據(jù)的 I/O 請(qǐng)求,同樣當(dāng)緩沖區(qū)中的數(shù)據(jù)被修改并且需要持久化到磁盤時(shí)也會(huì)調(diào)用此函數(shù)來執(zhí)行寫操作:
淘汰策略
strategygetbuffer函數(shù)是緩沖區(qū)管理策略的一部分用于獲取一個(gè)緩沖區(qū),決定從緩沖池中獲取哪個(gè)緩沖區(qū),如果緩沖區(qū)不存在或已被淘汰那么它會(huì)根據(jù)當(dāng)前的策略選擇一個(gè)空閑或可替換的緩沖區(qū),并可能觸發(fā) I/O 操作以加載或?qū)懭霐?shù)據(jù)。
共享緩沖區(qū)鎖
待續(xù)。