「
前言
我一個在大廠當主管的朋友,跟我說:
「現在招性能測試太難了,當然不是說沒人干,一開招聘信息就能收到一大把簡歷,其中不乏學歷亮眼、背景出色、簡歷里各種高並發、大流量的項目經驗的人才。
問題在於,當你提出講一個項目中遇到的性能問題,以及如何分析定位時,卻發現絕大多數根本沒有遇到過性能問題。
甚至面試了幾個高級性能測試工程師,還是發現一旦涉及到性能分析調優,就開始左顧右盼、答非所問。」
「問題的結症在於,大部分面試者參與的性能測試項目就不多,企業又希望招有經驗的測試,這本身就存在矛盾!」
我說,你說的情況誰不知道?你不如講點實際的,講點經驗,性能調優你說怎麼弄?
朋友思考了一會說道:「那不如正好聊聊我手上的這個項目,聊聊sqlserver數據頁!」
「什麼是數據頁 」
一般來說,對大塊資源或者數據進行高效管理都會按照一定粒度來劃分的,比如說 Windows 對內存的管理就是按照 內存頁 (4k) 來進行劃分,言外之意就是 SQLSERVER 對 mdf 的管理也是按照 數據頁 (8k) 來劃分的,畫個圖大概就是這樣的。
那如何來驗證這個結論呢?剛才也說了數據都在數據頁上,我們弄點數據然後在指定的數據頁上提取出來就好了,這裡用的是 SQLServer 2019
CREATE DATABASE MyTestDB
GO
USE MyTestDB;
GO
IF OBJECT_ID('person') IS NOT NULL
DROP TABLE person;CREATE TABLE person
(
id INT IDENTITY,
name CHAR(5)
);
GOINSERT INTO dbo.person( name ) VALUES ('john');INSERT INTO dbo.person( name ) VALUES ('mary');
「尋找數據所在的數據頁」
一般來說,對大塊資源或者數據進行高效管理都會按照一定粒度來劃分剛才圖中也說了 mdf 是由無數個 數據頁 拼出來的,那如何找到 person 表所在的數據頁呢?其實微軟提供了一個 dbcc ind 命令可以幫我們洞察出來,同時記得開始3604標記,讓輸出顯示在控制台上,而不是默認的錯誤日誌中,這個命令具體怎麼用,大家可以查看官方文檔。
DBCC TRACEON(3604)
DBCC IND(MyTestDB,person, -1)
從輸出看有兩條記錄,第一個是 PagePID=41 是 IAM 數據頁,而PagePID=280 就是我們 person 表記錄所在的數據頁編號,也就是說 person 表的記錄在 mdf 文件偏移為 0n280 * 0n8192 的位置,用 windbg 算一下就是 0x00230000 。
0:090> ? 0n280 * 0n8192
Evaluate expression: 2293760 = 00000000`00230000
那是不是呢?可以用 WinHex 驗證一下,為了不出現進程占用,先把 MyTestDB 下線了,最後記得再上線即可。
ALTER DATABASE MyTestDB SET OFFLINE
ALTER DATABASE MyTestDB SET ONLINE
從WinHex 上看,果然是在偏移為 0x00230000 這個數據頁上。
「如何從內存中看到數據頁」
剛才我們看到的數據頁只是物理硬碟上的,但數據頁和數據頁之間的邏輯關係肯定是在內存中用一定的數據結構來承載的,接下來看下這個 數據頁 映射到 SQLSERVER 進程內存的哪裡呢?微軟提供了 DBCC PAGE 命令可以查看指定 數據頁 的詳細信息。
DBCC TRACEON(3604)
DBCC PAGE(MyTestDB,1,280,2)
輸出結果如下:
DBCC 執行完畢。如果 DBCC 輸出了錯誤信息,請與系統管理員聯繫。
PAGE: (1:280)
BUFFER:
BUF @0x000002B41104F480bpage = 0x000002B3F0632000 bPmmpage = 0x0000000000000000 bsort_r_nextbP = 0x000002B41104F3D0bsort_r_prevbP = 0x0000000000000000 bhash = 0x0000000000000000 bpageno = (1:280)
bpart = 1 ckptGen = 0x0000000000000000 bDirtyRefCount = 0bstat = 0x9 breferences = 0 berrcode = 0bUse1 = 12454 bstat2 = 0x0 blog = 0x15ab215absampleCount = 0 bIoCount = 0 resPoolId = 0bcputicks = 0 bReadMicroSec = 182 bDirtyContext = 0x0000000000000000bDbPageBroker = 0x0000000000000000 bdbid = 10 bpru = 0x000002B3FA708040PAGE header:
Page @0x000002B3F0632000m_pageId = (1:280) m_headerVersion = 1 m_type = 1m_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0x8200m_objId (AllocUnitId.idObj) = 179 m_indexId (AllocUnitId.idInd) = 256 Metadata: AllocUnitId = 72057594049658880 Metadata: PartitionId = 72057594043170816 Metadata: IndexId = 0Metadata: ObjectId = 581577110 m_prevPage = (0:0) m_nextPage = (0:0)
pminlen = 13 m_slotCnt = 2 m_freeCnt = 8060m_freeData = 128 m_reservedCnt = 0 m_lsn = (37:584:3)
m_xactReserved = 0 m_xdesId = (0:0) m_ghostRecCnt = 0m_tornBits = -116446693 DB Frag ID = 1 Allocation Status
GAM (1:2) = ALLOCATED SGAM (1:3) = NOT ALLOCATED PFS (1:1) = 0x41 ALLOCATED 50_PCT_FULL
DIFF (1:6) = CHANGED ML (1:7) = NOT MIN_LOGGED
DATA:
Memory Dump @0x000000F840DF8000000000F840DF8000: 01010000 00820001 00000000 00000d00 00000000 ....................000000F840DF8014: 00000200 b3000000 7c1f8000 18010000 01000000 ........|...........000000F840DF8028: 25000000 48020000 03000000 00000000 00000000 %...H...............000000F840DF803C: 1b2a0ff9 00000000 00000000 00000000 00000000 .*..................000000F840DF8050: 00000000 00000000 00000000 00000000 10000d00 ....................000000F840DF8064: 01000000 6a6f686e 20020000 10000d00 02000000 ....john ...........000000F840DF8078: 6d617279 20020000 00002121 21212121 21212121 mary .....!!!!!!!!!!000000F840DF808C: 21212121 21212121 21212121 21212121 21212121 !!!!!!!!!!!!!!!!!!!!000000F840DF80A0: 21212121 21212121 21212121 21212121 21212121 !!!!!!!!!!!!!!!!!!!!
...000000F840DF9FF4: 21212121 21212121 70006000 !!!!!!!!p.`.
OFFSET TABLE:
Row - Offset 1 (0x1) - 112 (0x70) 0 (0x0) - 96 (0x60)
DBCC 執行完畢。如果 DBCC 輸出了錯誤信息,請與系統管理員聯繫。
Completion time: 2022-12-30T17:48:20.5466040+08:00
從上面的 Memory Dump 區節中可以看到,數據在進程內存的 000000F840DF8064 ~ 000000F840DF8078 範圍內,這裡要吐槽的是內存地址按照 大端布局 的,看起來很不習慣,可以用 windbg 用 小端布局 來顯示。
0:116> dp 000000F840DF8064
000000f8`40df8064 6e686f6a`00000001 000d0010`00000220
000000f8`40df8074 7972616d`00000002 21210000`00000220
000000f8`40df8084 21212121`21212121 21212121`21212121
000000f8`40df8094 21212121`21212121 21212121`21212121
000000f8`40df80a4 21212121`21212121 21212121`21212121
000000f8`40df80b4 21212121`21212121 21212121`21212121
000000f8`40df80c4 21212121`21212121 21212121`21212121
000000f8`40df80d4 21212121`21212121 21212121`21212121
「sql 請求源碼研究」
喜歡玩 windbg 的朋友肯定想對 sqlserver 進行彙編級洞察,比如研究下 sql 請求在 sqlserver 裡面的執行流是什麼樣的?其實很簡單,我們可以這樣處理,使用 ba 對 john 的內存地址下一個硬體斷點,即 ba r4 000000f840df8064+0x4,然後在 SSMS 上執行一條 SELECT * FROM person 語句,因為要提取 john 自然就會命中。
0:104> ba r4 000000f840df8064+0x4 0:104> g
Breakpoint 0 hit
sqlmin!BTreeMgr::GetHPageIdWithKey+0x4a:00007ff8`d4ea121a 48894c2478 mov qword ptr [rsp+78h],rcx ss:000000f8`45278028=00000248000000250:102> k
# Child-SP RetAddr Call Site00 000000f8`45277fb0 00007ff8`d4ea0b59 sqlmin!BTreeMgr::GetHPageIdWithKey+0x4a01 000000f8`45278450 00007ff8`d4ea08b7 sqlmin!IndexPageManager::GetPageWithKey+0x11902 000000f8`45278d20 00007ff8`d4eb22d1 sqlmin!GetRowForKeyValue+0x20303 000000f8`45279880 00007ff8`d4eb2a39 sqlmin!IndexDataSetSession::GetRowByKeyValue+0x14104 000000f8`45279a70 00007ff8`d4eb279b sqlmin!IndexDataSetSession::FetchRowByKeyValueInternal+0x23005 000000f8`45279ed0 00007ff8`d4eb2883 sqlmin!RowsetNewSS::FetchRowByKeyValueInternal+0x43706 000000f8`4527a000 00007ff8`d4eaadab sqlmin!RowsetNewSS::FetchRowByKeyValue+0x9607 000000f8`4527a050 00007ff8`d4f93d60 sqlmin!CMEDScan::StartSearch+0x4f808 000000f8`4527a170 00007ff8`d4f93f3a sqlmin!CMEDCatYukonObject::GetTemporalCurrentTableId+0x10e09 000000f8`4527a380 00007ff8`d801f0d1 sqlmin!CMEDProxyRelation::GetTemporalCurrentTableId+0x7a0a 000000f8`4527a3c0 00007ff8`d801dfb8 sqllang!CAlgTableMetadata::FPartialBind+0xb580b 000000f8`4527a580 00007ff8`d80394b3 sqllang!CAlgTableMetadata::Bind+0x3170c 000000f8`4527a620 00007ff8`d800415d sqllang!CRelOp_Get::BindTree+0x78f0d 000000f8`4527a890 00007ff8`d80418a1 sqllang!COptExpr::BindTree+0x850e 000000f8`4527a8c0 00007ff8`d800415d sqllang!CRelOp_FromList::BindTree+0x310f 000000f8`4527a920 00007ff8`d802c2a3 sqllang!COptExpr::BindTree+0x8510 000000f8`4527a950 00007ff8`d800415d sqllang!CRelOp_QuerySpec::BindTree+0x2e811 000000f8`4527aa60 00007ff8`d80042dd sqllang!COptExpr::BindTree+0x8512 000000f8`4527aa90 00007ff8`d800415d sqllang!CRelOp_SelectQuery::BindTree+0x48913 000000f8`4527ab80 00007ff8`d8003f23 sqllang!COptExpr::BindTree+0x8514 000000f8`4527abb0 00007ff8`d8004e47 sqllang!CRelOp_Query::FAlgebrizeQuery+0x4bd15 000000f8`4527ae30 00007ff8`d7ff5576 sqllang!CProchdr::FNormQuery+0x8f16 000000f8`4527ae70 00007ff8`d7ff4a79 sqllang!CProchdr::FNormalizeStep+0x5bd17 000000f8`4527b4b0 00007ff8`d7ff5124 sqllang!CSQLSource::FCompile+0xea518 000000f8`4527e1b0 00007ff8`d7e659c3 sqllang!CSQLSource::FCompWrapper+0xcb19 000000f8`4527e280 00007ff8`d7e6387a sqllang!CSQLSource::Transform+0x7211a 000000f8`4527e3e0 00007ff8`d7e6e67b sqllang!CSQLSource::Execute+0x4fa1b 000000f8`4527e8c0 00007ff8`d7e6d815 sqllang!process_request+0xca61c 000000f8`4527efc0 00007ff8`d7e6d5ef sqllang!process_commands_internal+0x4b71d 000000f8`4527f0f0 00007ff8`d4096523 sqllang!process_messages+0x1d61e 000000f8`4527f2d0 00007ff8`d4096e6d sqldk!SOS_Task::Param::Execute+0x2321f 000000f8`4527f8d0 00007ff8`d4096c75 sqldk!SOS_Scheduler::RunTask+0xa520 000000f8`4527f940 00007ff8`d40bb160 sqldk!SOS_Scheduler::ProcessTasks+0x39d21 000000f8`4527fa60 00007ff8`d40baa5b sqldk!SchedulerManager::WorkerEntryPoint+0x2a122 000000f8`4527fb30 00007ff8`d40bafa4 sqldk!SystemThreadDispatcher::ProcessWorker+0x3ed23 000000f8`4527fe30 00007ff8`f6d86fd4 sqldk!SchedulerManager::ThreadEntryPoint+0x3b524 000000f8`4527ff20 00007ff8`f865cec1 KERNEL32!BaseThreadInitThunk+0x1425 000000f8`4527ff50 00000000`00000000 ntdll!RtlUserThreadStart+0x21
從線程棧上看,有 SQLSERVER 核心的 Scheduler ,Task 以及 命令分析器,查詢優化器,查詢執行器 等各種核心元素,後續再慢慢研究吧。
「總結」
深入的理解數據,索引在數據頁上的布局,可以從根本上幫助我們理解如何減少請求在數據頁上的流轉,減少邏輯讀,從而提升 sql 的查詢性能。
更多好文:
金三銀四,你還不知道軟體測試刷題APP的天花板就晚了!
月薪20k以上的軟體測試工程師的必備知識點?拿來吧你!