細說Oracle資料庫與作業系統儲存管理二三事

楊建榮發表於2016-07-27

在上大學的時候,學習作業系統感覺特別枯燥,都是些條條框框的知識點,感覺和實際應用的關聯不大。發現越是工作以後,在工作中越想深入瞭解,發現作業系統知識越發重要。在實踐中結合理論還是不錯的一種學習方法。自從接觸資料庫以後,越來越感覺到很多東西其實都是相通的,作業系統中的很多設計思想在資料庫中也有借鑑和改進之處。

說到儲存管理,是作業系統中最重要的資源之一。因為任何程式和資料等都需要佔有一定的儲存空間,儲存管理會直接影響到系統的效能。

儲存器是由主存和外存組成。對於外存,可能覆蓋面更廣,像硬碟,行動硬碟,光碟,磁帶,SSD等等都是外存的覆蓋範圍。主存大家很熟悉,這些年主存的大小也有了極高的提升,現在的伺服器配置中幾百GB的記憶體都是很正常的。

關於儲存的管理技術,先討論以下兩個部分。

固定分割槽

先來點作業系統的知識。

關於固定分割槽管理技術,就是把主存分為若干個固定大小的儲存區,每個分割槽提供給某一個作用使用,如果作業完成會把相應的儲存區歸還。

在多道作業系統中,主存中分割槽的個數是固定不變的,而且每個分割槽的大小也是固定不變的。如果分割槽總是大於作業,那麼就有很多分割槽沒有充分使用,產生碎片。

來結合資料庫來看一看(shared pool中的free list)

在資料庫中,shared pool中的free list(bucket)管理和固定分割槽管理很相似。

shared pool中儲存單位是chunk,多個chunk組成一個連結串列,也叫做bucket,每個bucket都對chunk的大小都有一定的範圍,是一個連續的值,沒有交叉。

在10g,11g中都設定了255個bucket,可以通過trace檔案來了解一下。

[ora11g@rac1 trace]$ grep Bucket *18155*.trc
 Bucket 0 size=32
 Bucket 1 size=40
 Bucket 2 size=48
 Bucket 3 size=56
 Bucket 4 size=64
 Bucket 5 size=72
 ...
 Bucket 250 size=12352
 Bucket 251 size=12360
 Bucket 252 size=16408
 Bucket 253 size=32792
 Bucket 254 size=65560

可能對於bucket的大小沒有一個直觀的感受,可以生成一個圖來看看就很清楚了。

隨著bucket的增長,對應的chunk大小都在遞增,絕大多數的bucket中,chunk的大小都在5k以內。只有很小的一部分bucket的支援的chunk size很大,這個也是Oracle在不斷的改進中得到的一個最優值,按照比例來劃分,保證每次訪問需要的chunk大小都能夠合理的分配,儘量減少冗餘。

同時不是每個bucket裡面都是有chunk的,這個chunk的分配還是根據進入shared pool以後申請chunk大小緊密相關,bucket中的chunk數目可不是平均的。

Oracle在早期的版本中也碰到了不少的問題,在10g,11g中都對bucket的數目做了提升(目前都是255個),而且分割槽的大小也做了調整。這是一個比較均衡的比例,能夠保證每次請求的大小都在bucket的範圍之內,儘量提高效率。

回到作業系統中,我們再補充幾點。

在儲存的管理中,儲存的分配和釋放都需要根據分割槽來說明。在固定分割槽中採用了一個儲存分塊表(MBT)來維護而儲存的區的資訊,儲存區的資訊在作業系統中有一個專有名詞叫做資料基,資料基聽起來挺抽象,其實理解起來還是蠻簡單的。
我們用下面的圖示來說明。我們假設下面的這個表格就是儲存分塊表,其中資料基就包括,儲存的分割槽大小,儲存位置還有狀態。

分割槽 大小 儲存位置 狀態
1 8k xxxxx used
2 8k xxxxx free
3 16k xxxxx used
4 16k xxxxx free
5 16k xxxxx used
6 32k xxxxx used

猛一看,上面的方式還是比較簡單而且可行的。但是還是固定分割槽的硬傷,主存利用率不高,對於進入主存中的作業大小我們也沒法預知,而且對於MBT表的管理感覺還是不夠清晰。如果需要查詢哪些分割槽可用,需要重新分配的時候,就得遍歷整個表,遍歷了已經使用的分割槽,這樣分配的過程就比較長了。

這個時候可以參考一下:

可變分割槽的多道管理技術

這種技術在一定程度上解決了固定分割槽帶來的問題,可變分割槽在主存中不會事先建立一個個分割槽,而是在作業進入主存的時候按照作業大小再來建立分割槽。

這樣的話,分割槽個數不固定,分割槽大小不固定,在Oracle中也有一些相似之處。

Oracle中的deferred_segment_creation

比如說對於分割槽的不固定,在11g中有一個引數deferred_segment_creation,如果我們設定為true,那麼在建立之初是不會分配對應的分割槽的,直到開始插入資料之後,它才會根據插入的資料來建立分割槽。

Oracle中的interval partitoning

如果根據需要動態的建立分割槽,而且分割槽的大小也不固定。

比如在資料庫的表空間管理中,我們可以指定分割槽的。

對於可變分割槽的資料基管理,是採用了兩個儲存分割槽表來管理的,已使用分割槽表(UBT)和空閒分割槽表(FBT),這樣就可以減少儲存分配和釋放的效能。

在這點上,Oracle表空間中的資料字典管理方式是一致的。

Oracle中早期是採用FET$,UET$ 兩個資料字典表來維護分割槽的資訊的。只是在資料基上會有一定的差別。
FET$和UET$的結構如下:

這種方式在早期的Oracle版本中採用,這種表空間管理方式叫資料字典管理。

但是在Oracle的不斷改進中,發現這種方式還是存在一定的問題,資源消耗還是比較高的。對於這兩種資料字典表的DML操作,會產生較多的遞迴SQL來間接完成對兩個資料字典表的更新,在更新的過程中也會存在事務,存在事務也就會產生一定的undo和redo。最後就是對於相鄰空閒空間的合併, 在Oracle中是通過SMON程式來實現的。

回到作業系統,作業系統中對於資料基的管理還有一種方式,就是空閒儲存連結串列。

這種方式就是把空閒分割槽通過連結串列的形式串起來,形成了一條空閒儲存塊鏈。

這種技術在資料庫中可有一個很響亮的名字,在buffer cache中叫做LRU連結串列。

在buffer cache中的實現方式也是類似的。當然在Oracle中會採用其它的演算法和策略。Oracle中是把buffer按照被使用的先後順序掛在LRU連結串列 上,先被使用的buffer放在了連結串列的後面,後被使用的buffer掛載LRU連結串列的前面,如果buffer被修改的時候,buffer就會從LRU鏈 表上取出。這樣始終保持LRU連結串列中都是可用的資料塊。

可變分割槽的儲存演算法

然後來簡單說一下可變分割槽的儲存演算法。

目前主要有以下幾種:

  • 最佳適用演算法
    這種方式就是從所有未分配的分割槽中挑選一個最接近於作業尺寸且大於或者等於作業大小的分割槽分配。
  • 最先適應法
    按照分割槽序號從儲存分塊表中的第一個表目找找,把最先找到且大約等於作業大小的分割槽分配。
  • 最壞適應法
    把所有未分配的分割槽中挑選最大的且大於等於作業大小的分割槽分配。
  • 點陣圖法
    把所有的分割槽使用一個位來表示狀態,1表示塊已經被使用,0表示分割槽空閒。

在Oracle中的儲存演算法可能更接近於最佳適應演算法,唯一的不同的是在Oracle中採用了hash來該分配到哪個bucket。但是都會保證分配的空間是大於等於請求的大小。

而點陣圖法在表空間管理中也有相似的使用方式。

表空間的管理有兩種方式,資料字典管理和本地管理。

本地管理中會在資料檔案的頭部採用多個位來存放。這個bitmap類似下面的形式。

11110111001110100.....

1代表分割槽已被使用,0代表分割槽還是空閒,當程式需要分割槽的時候,只要掃描資料檔案的頭部的bitmap,就可以找到值為0的分割槽。分配了分割槽之後把它修 改為1,釋放空間就會從1修改為0. 修改資料檔案頭部的操作速度快且不存在事務,就沒有redo,undo,更不會有遞迴SQL。對於相鄰分割槽的合併來說,兩個連續的0就能說明是連續的空閒 分割槽,所以也不需要再合併相鄰的可用分割槽了。

前面討論了固定分割槽和可變分割槽管理的一些情況,它們的主要缺點就是主存使用的低效率和儲存分配釋放的低速。固定分割槽是分割槽內部的碎片造成主存利用率低,而可變分割槽是分割槽外部的碎片,往往小到無法使用,從而主存利用率不高。對於這個問題,分頁是一種很有效的方法。

分頁技術

分頁技術主要是把主存分為許多同樣大小的儲存塊,並以這種儲存塊作為儲存分配單位。Oracle資料庫中物理儲存單位有段,區,資料塊,這個時候所說的資料塊和作業系統資料塊存在著對映,一般都比作業系統塊要大。資料庫中預設為8K,資料的儲存都是以8K的基本單位來儲存的。如果把這一點繼續延伸,Oracle中的區(extent)就和分頁技術中所說的頁很類似。

分頁儲存中的基本實現過程,有以下幾點:

  1. 把主存分為相同大小的儲存塊,叫做頁架,頁架從0開始,編號依次是0,1,2….
  2. 使用者邏輯地址的分頁,使用者邏輯地址可以劃分為和頁架大小相同的部分,叫做頁。頁號從0開始,依次為0,1,2…
  3. 邏輯地址的表示,既然說到了邏輯地址,表示方法也很重要。每一個邏輯地址都是相對地址,用一個數對(p,d)來表示,p代表頁號,d代表邏輯地址在也好為p的頁中相對的地址,也叫偏移量。

聽起來挺枯燥啊,可以簡單舉個例子,我們常看的書就是一個很好的例子,書有很多大小,四開,八開,十六開,可以理解為頁架,書中的每一頁就是我們所說的頁,邏輯地址可以這麼理解,一本書有很多章節,小結,比如第二章第3頁,我們就能夠很快找到,這個時候,頁號就是2,偏移量就是3,用(p,d)來表示就 是(2,3)

舉一個嚴謹的例子,比如給定一個虛地址3456,假設頁面大小為1000B,則第0頁對應的地址為0-999,第1頁為1000-1999,則虛地址3456=(3,456)

這一點和Oracle中建立表空間時指定的extent management管理方式很相似,比如我們建立一個表空間test指定分割槽大小為1M,表空間大小為100M,則語句如下:

create tablespace test add datafile '/u01/app/db/test01/data01/test01.dbf' size 100M extent management local uniform size 1M ;

這樣我們指定分割槽大小為1M,如果儲存了100M的資料,這樣100M就會分為100個分割槽。如果資料大於分割槽1M,則可以儲存在相應的分割槽上,不一定連續。

可以用下面的表格來說明。

地址 程式 頁號
0-999 程式1 0
1000-1999 程式1 1
2000-2999 程式2 0
3000-3999 程式3 0
4000-4999 程式2 1
5000-5999 程式1 2

對應到每個程式對應的地址,就是我們所說的邏輯地址,比如程式1對應的邏輯地址就是

0-999
 1000-1999
 2000-3999

所以在分頁思想中的難點就是對於地址的表示,我們已經說使用(p,d)來表示,但是這個數在機器指令的地址場中表示還有不同,首先會把地址分為兩部分,一部分表示頁號,一部分表示頁內地址。

這種方式每次訪問一個主存單元都用一次除法得到頁號和頁內地址就很繁瑣,實際上效率要更差。這個時候相比前人也是考慮了很多招數,最後還是使用二進位制來搞定,指定頁面尺寸是2的冪,這樣就會省去很多額外的轉換。
最後一個例子很關鍵,如果看懂了說明你對分頁思想算是明白了。

假設頁的大小為1KB,計算邏輯地地址為4101的頁號,頁內地址。

按照二進位制的思想,4101可以這樣表示 4101=2^12+2^1+2^1+2^0

用0,1來表示就是

0001000000000101

頁的大小是1KB=2^10,則在二進位制串中,後10位就是對應的頁內地址,二進位制0101代表的是5,表示頁內地址為5

0001000000000101

頁號對應的二進位制串000100表示頁號為4

所以4101對應的邏輯地址表示為(4,5)這種方法可以省去除法運算,硬體層面會自動把邏輯地址拆分為兩部分,對應頁號和頁內地址。

問題來了,地址能夠表示了,那使用的時候是怎麼轉換的呢,首先會把邏輯地址抽取出來,像上面的例子,頁號是4,然後根據頁號為索引找到該頁存放的主存頁架號。比如存放的地址為2000-2999,則頁架號為2,然後把頁架號取代邏輯地址,和右邊的頁內地址組成了最終的實體地址去訪問記憶體。

這種思想還是需要些時間去消化一下,優點也是很明顯的,基本上沒有頁內碎片,同時也不會存在小到無法再用的頁外碎片。因為每個碎片都是頁架的整數倍。

分頁中使用的二進位制方式處理地址是一種很值得借鑑的方式,可以減少很多額外的開銷,和Oracle中的rowid儲存方式也很類似。

分段式儲存

分段式儲存管理系統中,會為每個段分配一個連續的分割槽,而程式中的各個段可以離散地移入記憶體中不同的分割槽中,說起分段就會聯想到分頁,我們來聊聊分頁與分段的主要區別。

分頁和分段有許多相似之處,比如兩者都不要求作業連續存放。但在概念上兩者完全不同,主要表現在以下幾個方面:

  1. 頁是資訊的物理單位,分頁是為了實現非連續分配,以便解決記憶體碎片問題,或者說分頁是由於系統管理的需要。段是資訊的邏輯單位,它含有一組意義相對完整的資訊,分段的目的是為了更好地實現共享,滿足使用者的需要。
  2. 頁的大小固定,由系統確定,將邏輯地址劃分為頁號和頁內地址是由機器硬體實現的。而段的長度卻不固定,決定於使用者所編寫的程式,通常由編譯程式在對源程式進行編譯時根據資訊的性質來劃分。
  3. 分頁的作業地址空間是一維的,分段的地址空間是二維的。

從資料庫的角度來看,感覺和資料庫中的段概念還是比較類似的。資料庫中段包含多個分割槽。各個分割槽也可以在不相鄰的分割槽中。

找一個圖來說明。

在分段情況下,會要求每個程式的地址空間劃分為若干個段,每個段都有自己的段名,對應到下圖中就是一個段號。每個段的地地址空間都是從0開始,是一個連續的地址空間。

從地址的儲存情況來說,段和頁的儲存方式都是類似的,都會包含兩部分。分段儲存中是段號和段內地址,和分頁儲存中的頁號和頁內地址類似。

由於一個程式由很多段組成,而且各個段可能被分配在主存中的多個不相鄰的分割槽中,為了將程式的邏輯地址轉換為實體地址,需要有一個短標來指出程式的某段放在主存中的位置以及段長。

這一點從資料庫層面來說有類似的方面,首先是程式由多個段組成,資料庫中可以理解為一個表包含多個段,資料段,索引段,LOB段,LOB索引段等等。這些都是獨立的段,在儲存的時候也可能分佈在不同的表空間中,所以可能不是一個相鄰的分割槽。

而段的資訊在作業系統層面是通過段表來維護的,資料庫層面則是通過資料字典,user_segments,user_extents來維護的,每個表包含的段,每個段包含的區都是很詳實的。

從分段和分頁的優點來說,因為它們涉及的層面和應用方向不同,但是還是有一定的可比性,在段共享方面,分段儲存還是很有優勢,誰讓它是段共享呢。

從作業系統層面舉個例子就是一個多使用者系統,有一個應用程式可能包含的程式段是100K,資料段是40K,按理說需要40K*40+100k*40=1600+4000=5600k

在分段儲存中則需要100k+40k*40=1700k,從這一點上來說還是很大的改進。

從這一點上來說,資料庫中的同義詞就有點分段儲存的味道,每個同義詞都可以訪問源表,相當於共享了資料,同義詞佔用的儲存空間很小,幾乎可以忽略。

可能分段儲存和分頁儲存都各有千秋,但是都是在不斷的使用和改進中主鍵發展起來的,分段儲存沒有段內碎片,只有外部碎片,簡單分段技術也是基於多重分割槽技 術的發展而來。另外簡單分頁對於使用者是不可見的,使用者無法瞭解程式被分頁或者分頁的細節,但是簡單分段對於使用者基本是可見的,當程式被交換出記憶體的時候, 對應的頁表和段表也需要隨著程式一起撤出記憶體。

當然分頁分段方式還在不斷的發展中,要不怎麼有後續的段頁式儲存呢,很多時候類比作業系統方面的知識,就會讓我們對於很多事物有了全新的認識和了解。

當然順帶幫大家複習了作業系統的基礎知識,我的目的也算達到了。

相關文章