SQLSERVER 阻塞之 PFS 頁到底是什麼?

一線碼農發表於2023-02-06

一:背景

1. 講故事

在 SQLSERVER 的眾多阻塞場景中,有不小的一部分是由於 PFS 頁上的 閂鎖 等待造成的,畢竟寫頁操作一定是要序列化的,在面對 閂鎖(PAGELATCH_X) 等待問題上,一定要搞明白 PFS 頁到底是什麼? 這篇就來好好聊一聊。

二:PFS 詳解

1. 什麼是 PFS 頁

我們知道資料庫是由海量的 資料頁 組成,表記錄會寫入到 資料頁 上,那海量的資料頁如何管理呢? SQLSERVER 想到了一個辦法,從海量的資料頁中按一定規則擇取一些作為 管理頁 使用,比如:

  • GAM 跟蹤區分配情況
  • SGAM 跟蹤共享區分配情況
  • PFS 跟蹤資料頁的空間使用情況

這裡我簡述一下吧,GAM資料頁中一個 bit 跟蹤一個 64k 的空間(一個區),所以一個 8k 的GAM頁 可以跟蹤大約 (8 * 8192) * 64k = 4G 的空間,而 PFS 資料頁用一個 byte 跟蹤一個 8k 的資料頁,理論上可以管理 8192 * 8k = 64M 的資料。

接下來的問題是這 1byte 是如何標記一個資料頁的使用情況呢? 簡單的畫個圖吧。

從圖中看,這一個跟蹤 byte 差不多都給塞滿了,有了這些基礎之後,接下來用一個案例來演示一下。

2. 案例演示

建立一個 MyTestDB 資料庫,新建一個 post 表,參考sql如下:


CREATE DATABASE MyTestDB
GO
USE MyTestDB
GO
CREATE TABLE post (id INT IDENTITY, content CHAR(1000))

建立好之後來觀察下 MyTestDB 的 mdf 檔案中的 PFS 資料頁,可以使用 DBCC PAGE 命令,但這個命令需要獲取 fileid, pageid 這兩個引數,那如何提取呢?

  • 如何提取 fileid

可以查詢 sys.database_files 系統表提取。


SELECT file_id,name,physical_name FROM sys.database_files;

  • 如何提取 pageid

剛才說過了,這種管理頁都是有規律的,比如 PFS 頁是 64M 一個,在 file_id=1 的檔案中,第一個區的第1號資料頁就是 PFS 頁,即 MyTestDB.mdf 檔案偏移 8192byte 的位置,這裡稍微補充一下,mdf 檔案的第一個區中的 8 個 page 都是有特殊用途的,畫個簡圖如下:

接下來觀察下這個 PFS 資料頁,它的 type=11,後續我們還會對它不斷的觀察。


DBCC TRACEON(3604)
DBCC PAGE(MyTestDB,1,1,3)

------ output -----
Page @0x000001DB52882000

m_pageId = (1:1)                    m_headerVersion = 1                 m_type = 11
m_typeFlagBits = 0x0                m_level = 0                         m_flagBits = 0x0
m_objId (AllocUnitId.idObj) = 99    m_indexId (AllocUnitId.idInd) = 0   Metadata: AllocUnitId = 6488064
Metadata: PartitionId = 0           Metadata: IndexId = 0               Metadata: ObjectId = 99
m_prevPage = (0:0)                  m_nextPage = (0:0)                  pminlen = 0
m_slotCnt = 1                       m_freeCnt = 2                       m_freeData = 8188
m_reservedCnt = 0                   m_lsn = (37:192:1)                  m_xactReserved = 0
m_xdesId = (0:0)                    m_ghostRecCnt = 0                   m_tornBits = 651443756
DB Frag ID = 1     

好了,繼續測試吧,插入一條資料,觀察 PFS 頁對應的位數是否有變化? 比如空間使用率,參考sql如下:


INSERT INTO post(content) VALUES ('aaaaa')
DBCC TRACEON(3604)
DBCC IND(MyTestDB,post,-1)

從圖中可以看到,插入的 aaaaa 記錄寫到了 240 號資料頁,跟蹤這個資料頁的byte 理論上是在 PFS 頁偏移 0n240 byte 的位置,那具體是哪一個位置的 byte 跟蹤的呢?我們用 windbg 觀察下 PFS 資料頁的記憶體頁地址即可,首先用 DBCC PAGE(MyTestDB,1,1,2) 找到 Page 頁在記憶體的首地址。


Memory Dump @0x000000C8177F8000

000000C8177F8000:   010b0000 00000000 00000000 00000000 00000000  ....................
000000C8177F8014:   00000100 63000000 0200fc1f 01000000 01000000  ....c...............
000000C8177F8028:   25000000 e0000000 1c000000 00000000 00000000  %...................
000000C8177F803C:   2c3ed426 01000000 00000000 00000000 00000000  ,>.&................
000000C8177F8050:   00000000 00000000 00000000 00000000 00009c1f  ....................
000000C8177F8064:   44444444 00004444 60647060 74706070 60607060  DDDD..DD`dp`tp`p``p`
000000C8177F8078:   60707060 40404040 40404040 61706070 60606070  `pp`@@@@@@@@ap`p```p
000000C8177F808C:   60646060 60706060 60706060 60606070 40404040  `d```p```p`````p@@@@

接下來用 WinDbg 附加 SqlServer,由於前 96byte 是資料頁頭,所以理論位置應該是 000000C8177F8000+0x60 + 0n240 的位置,截圖如下:

哈哈,終於給找到了,那 0x41 是什麼意思呢?可以用 windbg 的 .formats 命令觀察一下。


0:118> .formats 41
Evaluate expression:
  Hex:     00000000`00000041
  Decimal: 65
  Decimal (unsigned) : 65
  Octal:   0000000000000000000101
  Binary:  00000000 00000000 00000000 00000000 00000000 00000000 00000000 01000001

對應 byte 中位解讀,可以知道當前 240 號資料頁的空間使用率為 1 ~50%,同時 資料頁已被分配

這裡我們順帶觀察下 PageID=80 號這個 IAM 頁,看看 PFS 中如何表示它的,在記憶體中計算位置應該是 000000C8177F8000+0x60 + 0n80,輸出如下:


0:118> dp 000000C8177F8000+0x60 + 0n80 L2
000000c8`177f80b0  70607070`60606070 60302070`60606070
0:118> .formats 70
Evaluate expression:
  Hex:     00000000`00000070
  Decimal: 112
  Decimal (unsigned) : 112
  Octal:   0000000000000000000160
  Binary:  00000000 00000000 00000000 00000000 00000000 00000000 00000000 01110000

再對應 byte 位解讀,可以知道當前的 80號資料頁 的四點資訊:

  • 當前資料頁已分配
  • 當前資料頁在混合區
  • 當前資料頁為 IAM 頁
  • 當前為特殊管理頁

接下來再插入 4條 資料,觀察下 空間使用率 是否有變化 ?


INSERT INTO post(content) VALUES ('bbbbb')
INSERT INTO post(content) VALUES ('ccccc')
INSERT INTO post(content) VALUES ('ddddd')
INSERT INTO post(content) VALUES ('eeeee')

DBCC PAGE 觀察記憶體頁首地址之後,再次用 WinDBG 附加觀察,輸出如下:

0:126> .formats 42
Evaluate expression:
  Hex:     00000000`00000042
  Decimal: 66
  Decimal (unsigned) : 66
  Octal:   0000000000000000000102
  Binary:  00000000 00000000 00000000 00000000 00000000 00000000 00000000 01000010

從輸出可以看到已經由原來的 41 變成了 42,其中的 010 剛好表示當前資料頁為 51~80% 滿,作為校驗的話,可以直接觀察 240號 頁頭上的 PFS (1:1) = 0x42 ALLOCATED 80_PCT_FULL 描述。


DBCC TRACEON(3604)
DBCC IND(MyTestDB,post,-1)
DBCC PAGE(MyTestDB,1,240,2)

-----  output -----

PAGE: (1:240)
...

PAGE HEADER:

Page @0x000001DB58F92000
...           
Allocation Status

GAM (1:2) = ALLOCATED               SGAM (1:3) = NOT ALLOCATED          PFS (1:1) = 0x42 ALLOCATED  80_PCT_FULL
DIFF (1:6) = CHANGED                ML (1:7) = NOT MIN_LOGGED           
...

三:總結

總的來說 PFS頁 瓶頸主要來自於使用者建立的 臨時表表變數,常常在高併發的場景下由於高頻的建立和刪除臨時表以及對臨時表的插入,SQLSERVER 不得不高頻的修改 PFS頁 造成這一塊的瓶頸,如果大家在阻塞中看到大量的PageLatch_x 等待資源,記得一定要將 Tempdb 中的 ndf 劃分為多個來分攤寫入壓力。

相關文章