Oracle裡面為什麼沒有double write?

原文鏈接:https://www.modb.pro/db/23489?ywm= (複製鏈接至瀏覽器,查看原文)

溫馨提醒:本文代碼較多,為方便您閱讀,可掃碼下方二維碼,查看原文。

Oracle裡面為什麼沒有double write?

導讀:MySQL有double write機制,PostgreSQL有full page write機制,那麼Oracle裡面為什麼沒有類似機制呢?


近期看到朋友圈轉發了幾篇關於MySQL innodb double write的文章,感覺都還不錯。突然想到為什麼Oracle沒有這個東西?PostgreSQL是否也有類似機制?

在網上搜了一下,發現有人之前簡單寫過類似文章,但是沒有一篇能夠完全分析透徹的的。

所以,我想來好好說一下這個問題。

首先MySQL 的double write的機制,是為了解決partial write的問題;那麼為什麼會存在partial write呢?這本質上是因為操作系統的最小IO單位一般為4kb;以前大家使用的sas、sata的sector 都是512。現在如果是nvme ssd之類,已經有4kb甚至16kb的sector了。之所以sector越來越大就是為了解決ssd容量的問題。

這裡不扯太遠了。簡單地講,由於數據庫的最小IO單位是page或者block,MySQL 默認是16kb,而Oracle 大家通常默認使用8kb的block size。這就導致數據庫的一次最小IO對於操作系統來講,需要調用多次完成。這裡以MySQL為例,每一次寫16kb,假定os 最小IO為4kb;那麼需要調用4次。那麼問題就來了。

假設操作系統只執行了2次IO操作就crash了,這個page 不就寫壞了嗎?這就是partial write 。

為什麼說MySQL 的double write 可以解決partial write的問題呢?在回答這個問題之前,我們先來看一下MySQL整個數據讀取和寫入的基本流程(這只是大致流程,實際要要複雜的多):

1、data page1從idb數據文件中被讀取到buffer pool中(這裡也涉及到各種LRU);

2、user A發起事務T1 開始修改 page1,此時page1 會變成一個髒頁(dirty page);

3、同時MySQL會開始寫redo log,當然這裡是先將page1的變更寫入redo buffer,並不會立刻落盤;

4、User A 開始發起commit提交這個事務T1;

5、此時MySQL會將redo buffer內容flush到redo log中,即完成此時的redo落盤操作;

6、完成日誌落盤後,Page1 會被先通過memcopy到double write buffer中;注意其實double write buffer並不是內存結構,其實是在共享表空間ibdata中分配的一塊連續區域,相當於是把磁盤當內存使用,大小默認是2M,分為2個部分,每個部分1M;根據16kb單位進行計算,大概就是128個page;

7、最後double write buffer被寫滿或者用戶本身發起了刷髒行為,那麼會把數據從double write buffer寫入到數據文件中。


上述是大致流程;那麼為什麼說能解決partial write問題呢?

1)假設page1 寫入到double write buffer 就失敗了,那麼這個page1 根本就不會被寫入到數據文件落盤;如果此時數據庫crash了,按照正常的恢復流程找檢查的,應用redo,undo進行恢復即可。這沒什麼可說的。

2)假設寫入到double write buffer成功,但是page1 在寫入到數據文件中時失敗,如何進行恢復呢?這就是我們要說的關鍵地方。由於double write buffer中是寫成功了,數據文件寫失敗,那麼在恢復時MySQL在檢查這個page1時發現checksum有問題,會直接從double write buffer中copy 已經寫完成的page1覆蓋到數據文件對應位置,然後再去應用redo等等。

所以簡單來講,double write機制更像在數據落盤之前多了一層緩衝。那麼這個機制是否有問題呢 ?我認為是存在一定問題的。

比如為了確保機制的實現,double write buffer並不是真正的內存不buffer,是從共享表空間中分配一塊連續區域出來。這本質上是用磁盤當內存使用,雖然讀寫大都是順序讀寫(實際上可能也存在隨機single page flush的情況)。磁盤的操作畢竟比純內存中的操作要慢一些,當然這個機制本身來講並不會帶來多大的性能衰減,因為就是普通的SAS 硬盤,順序讀寫性能都是非常不錯的。

另外為什麼會出現這個問題呢?其實是因為很多存儲設備不支持原子寫,MySQL 目前暫時也不支持。不過MariaDB目前是支持這個特性的。(https://mariadb.com/kb/en/atomic-write-support/)

當然這是過去的時代,現在都是各種Fusion IO/PCIE SSD,大多都已經支持了原子寫了。既然硬件已經支持,是否我們就可以在SSD方面關閉這個功能呢?很明顯是可以的,既然都支持原子寫了,還需要double write這種雞肋的功能幹什麼呢?這本身就是一種不算太完美的方案。其實MySQL官方文章也直接說明了這個問題:

If system tablespace files (“ibdata files”) are located on Fusion-io devices that support atomic writes, doublewrite buffering is automatically disabled and Fusion-io atomic writes are used for all data files. Because the doublewrite buffer setting is global, doublewrite buffering is also disabled for data files residing on non-Fusion-io hardware. This feature is only supported on Fusion-io hardware and is only enabled for Fusion-io NVMFS on Linux. To take full advantage of this feature, an innodb_flush_method setting of O_DIRECT is recommended.

<code>如果直接看源碼你會看到如下的類似內容:
#if !defined(NO_FALLOCATE) && defined(UNIV_LINUX)
/* Note: This should really be per node and not per
tablespace because a tablespace can contain multiple
files (nodes). The implication is that all files of
the tablespace should be on the same medium. */
if (fil_fusionio_enable_atomic_write(it->m_handle)) {
if (srv_use_doublewrite_buf) {
ib::info(ER_IB_MSG_456) << "FusionIO atomic IO enabled,"
" disabling the double write buffer";
srv_use_doublewrite_buf = false;
}
it->m_atomic_write = true;
} else {
it->m_atomic_write = false;
}
#else
it->m_atomic_write = false;
#endif /* !NO_FALLOCATE && UNIV_LINUX*//<code>

簡單地講,就是如果都使用了支持原子寫的fusion-io 等存儲設備,那麼double write機制會被自動disable。同時官方建議將innodb_fush_method設置為o_direct,這樣可以充分發揮硬件性能。

除了硬件等支持,那麼傳統等文件系統比如ext4/xfs/zfs/VxFS 是否支持原子寫呢?

實際上ext4/xfs對原子寫支持目前都不是非常優化,zfs是天然支持的,很早的版本就支持了,另類的文件系統;另外veritas的VxFS也是支持的,其官網有相關的解釋。(https://sort.veritas.com/public/documents/vis/7.0/linux/productguides/html/infoscale_solutions/ch05s07.htm)

對存儲這方面瞭解不多,不敢寫太多,免得被碰。我們還是回到數據庫這個層面上來。

國內也有一些硬件廠家在不斷努力優化這個問題;例如Memblaze的混合介質的多命名空間管理解決方案(這裡我不是打廣告,只是網上搜到一點材料 https://cloud.tencent.com/developer/news/404974 )。從這個測試來看,percona mysql 8.0…15在啟用parallel double write的情況下,性能有超過30%的提升,這還是非常不錯的。注意這裡的parallel 是根據buffer pool instance來的,提高併發處理能力。

實際上MySQL 原生版本8.0.20在double write方面做了重大改變(看上去有點抄襲xxx的做法),比如也可以把double write files進行單獨存放了。

Prior to MySQL 8.0.20, the doublewrite buffer storage area is located in the InnoDB system tablespace. As of MySQL 8.0.20, the doublewrite buffer storage area is located in doublewrite files.

也引入了類似Parallel的機制。如果查看官方文檔能看到引入了類似如下的相關參數:

innodb_doublewrite_files(這個跟buffer pool instacne有關)

innodb_doublewrite_pages

innodb_doublewrite_batch_size

不難看出MySQL 8.0.20 在這方面做的很不錯了,想必性能必定又上升一個臺階。對於這個我一直感覺比較奇怪:為什麼會出現?

那麼這個有沒有可能跟MySQL 自身page的結構有關係呢?開始我以為可能是這個問題,後來發現其實並不是。從MySQL的源碼來看,本身MySQL 也是需要檢查page checksum的,否則怎麼判斷壞頁呢?

<code>/** Issue a warning when the checksum that is stored in the page is valid,
but different than the global setting innodb_checksum_algorithm.
@param[in] curr_algo current checksum algorithm
@param[in] page_checksum page valid checksum
@param[in] page_id page identifier */
void page_warn_strict_checksum(srv_checksum_algorithm_t curr_algo,
srv_checksum_algorithm_t page_checksum,
const page_id_t &page_id);/<code>

說完了MySQL,我們再來看看PostgreSQL(開源數據庫永遠的老二)是否也存在這個問題。由於我本身對PostgreSQL是不太熟的,所以查閱了相關文檔後發現,PG在很早的版本就引入了full page write機制,來避免這種partial write問題。


Oracle裡面為什麼沒有double write?


這裡我盜用一張圖片來簡單解釋一下PostgreSQL的數據寫入流程:

1、假設T1 時間點觸發了checkpoint;

2、T2時間點開始將DML操作產生的變化刷到xlog(關係型數據庫都是日誌寫優先原則);

3、T3時間點事務發起了commit提交操作;這個時候會將髒頁變化落盤xlog(這時候仍然還沒開始寫髒頁);

4、T3之後準備將髒數據flush 到datafile時,如果數據庫這時候crash可能會是什麼現象?

那麼PostgreSQL 的full page write機制為什麼說可以避免在T4時間點crash 數據不丟的問題呢 ?實際上是因為在T2時間點,如果啟用了full page write(實際上默認就啟用了),會將整個髒頁寫入到wal 日誌。我靠……這是什麼鬼???

這當然解決了partial write的問題。crash就直接用wal日誌進行恢復就完了,因為裡面有髒頁的完整記錄。

從這裡我們也能看到問題的所在。如果寫入整個page 的內容到wal日誌(注意:這裡是說每次checkpoint後的數據塊第一次變髒落盤之前,整個page都會寫入wal日誌,後面再次被修改就不需要寫整個page了),那麼勢必會導致wal日誌的瘋狂增長,同時對性能也會有一定影響。

關於啟用full page write 之後存在寫放大問題,如何優化wal日誌,可以參考這個文章:http://www.postgres.cn/v2/news/viewone/1/273

上述文章中提到可以通過降低checkpoint 以及壓縮等方式來降低寫放大的影響。其中關於full page write的源碼機制分析,可參考阿里內核數據庫組的這篇文檔(http://mysql.taobao.org/monthly/2015/11/05/),很贊!

PostgreSQL官方文檔其實對這個描述是不太充分的。如下是我摘自PostgreSQL 12的官方文檔內容:

full_page_writes ( boolean)

When this parameter is on, the PostgreSQL server writes the entire content of each disk page to WAL during the first modification of that page after a checkpoint. This is needed because a page write that is in process during an operating system crash might be only partially completed, leading to an on-disk page that contains a mix of old and new data. The row-level change data normally stored in WAL will not be enough to completely restore such a page during post-crash recovery. Storing the full page image guarantees that the page can be correctly restored, but at the price of increasing the amount of data that must be written to WAL. (Because WAL replay always starts from a checkpoint, it is sufficient to do this during the first change of each page after a checkpoint. Therefore, one way to reduce the cost of full-page writes is to increase the checkpoint interval parameters.)

Turning this parameter off speeds normal operation, but might lead to either unrecoverable data corruption, or silent data corruption, after a system failure. The risks are similar to turning off fsync, though smaller, and it should be turned off only based on the same circumstances recommended for that parameter.

Turning off this parameter does not affect use of WAL archiving for point-in-time recovery (PITR) (see Section 25.3).

This parameter can only be set in the postgresql.conf file or on the server command line. The default is on.

那麼這裡有個問題。現在都是各種PCIE SSD 滿天飛了,比如寶存的PCIE就支持原子寫了,那麼這種情況下我們能否關閉full page write機制?

從技術上來講,假設PCIE支持的原子寫是4k,那麼PG 默認使用8k page的話,仍然存在這個問題。前提是數據庫使用的page size小於或者等於硬件設備所支持的原子寫IO大小。如果能夠滿足這個條件,我認為完全是可以關閉的。

以寶存PCIE 為例子,其Nand Flash的最小寫單位是page,NandFlash寫page的操作是原子操作。我們只要將上層應用程序發過來的寫操作放在一個Nand Flash的page也就實現了原子寫支持;目前Nand Flash 都是32k;這基本上都是大於數據庫的通用block size /page size的。因此,如果你使用寶存的PCIE,那麼你可以disable 這個特性。(這裡不是廣告!)

這裡我再補充一下,類似阿里的PolarDB 似乎也取消了double write的機制。因為其底層的PolarFS分佈式文件系統據說實現了原子寫支持,那自然也就不需要這個機制了。

前面鋪墊了這麼多,之前也有MySQL大佬吐槽挖苦說……Oracle DBA 連原子寫都不知道。這裡我想說的是,不知道是因為Oracle 圈子大家從來沒有care過這個東西,或者說Oracle 本身早已解決了這個問題,所以大家從來沒有提過。那麼為什麼Oracle 數據庫裡面很少提及這個問題呢?是真的Oracle 不存在這個問題嗎?

雖然Oracle 號稱是這個星球最強關係型OLTP數據庫,但是我認為仍然存在這個問題,因為DB層畢竟難以感知OS層的IO操作。

我們先來看下Oracle data block的結構:

<code>BBED> set file 7 block 347
FILE# 7
BLOCK# 347

BBED> map
File: /opt/oracle/oradata/ENMOTECH/users01.dbf (7)
Block: 347 Dba:0x01c0015b
------------------------------------------------------------
KTB Data Block (Table/Cluster)

struct kcbh, 20 bytes @0

struct ktbbh, 96 bytes @20

struct kdbh, 14 bytes @124

struct kdbt[1], 4 bytes @138

sb2 kdbr[66] @142

ub1 freespace[911] @274

ub1 rowdata[7003] @1185

ub4 tailchk @8188

BBED> p kcbh
struct kcbh, 20 bytes @0
ub1 type_kcbh @0 0x06
ub1 frmt_kcbh @1 0xa2
ub2 wrp2_kcbh @2 0x0000
ub4 rdba_kcbh @4 0x01c0015b
ub4 bas_kcbh @8 0x00791f1a
ub2 wrp_kcbh @12 0x0000
ub1 seq_kcbh @14 0x02
ub1 flg_kcbh @15 0x04 (KCBHFCKV)
ub2 chkval_kcbh @16 0xfe84
ub2 spare3_kcbh @18 0x0000

BBED> p tailchk
ub4 tailchk @8188 0x1f1a0602/<code>

從結構上來看,我們知道Oracle 這裡有一種機制來判斷Block是否屬於斷裂block,即使在block尾部寫入一個tailchk值,其中tailchk 的value=bas_kcbh(後4位)+type_kcbh+seq_kcbh。

正常業務邏輯的情況下,如果檢查發現塊頭部和尾部的值不匹配,則認為是斷裂塊。Oracle中的標準叫法為Fractured Block。有人說產生斷裂塊是因為通過begin bakup的熱備場景……其實並不僅僅是這個場景。我們先來看下標準的解釋:

A Fractured Block means that the block is incomplete and considered a Physical Corruption. Information from the block header does not match the block end / tail. It is a clear symptom about issues within the Operating System (OS) / Hardware layers.

In order to understand why a Fractured Block happens, we need to understand how a block is written into disk. The block size at OS level does not match the block size at Oracle level, so in order to write an Oracle block, the Operating System needs to perform more than one write. As an example: if OS block size is 512 bytes and Oracle block size is 8K, the OS performs 16 writes in order to complete the write process.

Oracle keeps track off the header of each block and before writing down to disk updates a 4 byte field/value in the tail of each block (tailchk) to guarantee afterwards consistency check that the block is complete after written.

This value was right on the block when the RDBMS sent the write to the Operating System; this is also confirmed by the fact that the checksum result is different than 0x0 which means that the checksum is different than when it was calculated before write and the block is not the same version that the RDBMS request to write before. The write did not complete as a whole and only partial write was done or the write completed but invalid information was actually written in the block.

The tail check only verifies if the header and the tail of the block have been written correctly, but does not warranty that the complete block was written. In order to warranty this, the database has parameter DB_BLOCK_CHECKSUM. When the parameter is set to TRUE / TYPICAL (default value), a checksum value is calculated for the complete block and this value is stored in the block header before writing the block. When the block is read again, this value is recomputed and compared with the one at block header.

There are checks that may be run against datafiles to ensure the validity of all tail values on all blocks of them. RMAN Validate or DBVerify catch this kind of failures and may be used against the Database file(s) to check for Physical Corruption.

Identically, there is a clear path to follow when this happens. These blocks are incorrectly written by the Operating System / Hardware and as such, Oracle operations over the blocks affected are correct (otherwise, a different kind of error would have been printed out).

In addition, restoring and recovering the block is not introducing the issue again, what indicates that the redo changes generated are correct, so the issue is clearly related with other layer (most probably hardware layer)。

從上面的內容可以看出,Oracle 通過在數據塊(index block也類似)尾部寫入一個tailchk的方式來判斷這個block是否完整;但是僅僅判斷這個值,並不能確保整個block的數據是完好的,不過可以通過db_block_checksum來進一步進行檢查(在讀寫時)。

但是在最後,看Oracle的說法是仍然無法徹底避免這個問題,畢竟最終數據落盤是由OS層來完成的,而不是Oracle自己。

對於上文提到的checksum值,其實Oracle是通過異或算法得出來的,是如果其他值不變的情況下,你是無法修改的——Oracle會自動進行計算。

<code>BBED> modify /x 86 offset 16
Warning: contents of previous BIFILE will be lost. Proceed? (Y/N) y
File: /opt/oracle/oradata/ENMOTECH/users01.dbf (7)
Block: 347 Offsets: 16 to 19 Dba:0x01c0015b
---------------------------------------------------------
86fe0000
<32 bytes per line>
BBED> sum apply
Check value for File 7, Block 347:
current = 0xfe84, required = 0xfe84
BBED> p kcbh
struct kcbh, 20 bytes @0
ub1 type_kcbh @0 0x06
ub1 frmt_kcbh @1 0xa2
ub2 wrp2_kcbh @2 0x0000
ub4 rdba_kcbh @4 0x01c0015b
ub4 bas_kcbh @8 0x00791f1a
ub2 wrp_kcbh @12 0x0000
ub1 seq_kcbh @14 0x02

ub1 flg_kcbh @15 0x04 (KCBHFCKV)
ub2 chkval_kcbh @16 0xfe84
ub2 spare3_kcbh @18 0x0000/<code>

既然我們認為Oracle也存在這個問題,那麼是否有相關的解決方案呢?如果你查詢如下視圖會看到如下幾條信息:

<code>SQL> l
1* select name,class from v$statname where name like ‘%partial%’
SQL> /
NAME CLASS
----------------------------------------------------------------
physical read partial requests 8
cell partial writes in flash cache 8
IM ADG invalidated pdb partial transaction 128
HSC OLTP partial compression 128/<code>

可以看到Oracle 這裡是有個關於partial writes的統計數據的。既然有,那麼就說明Oracle認為存在這個問題。不過大家也可以看到,上述幾個數據都是針對Oracle exadata的。那麼我們可以認為如果你使用Oracle exadata的話,其實不是不存在這個partial write問題的。

從這個層面來看,Oracle ACFS文件系統本身應該也還不支持原子寫。我看網上很多文章說Oracle不需要類似double write機制,是因為Oracle有什麼檢查點,控制文件機制……這完全是瞎扯。

不過這裡我想說的是Oracle的checkpoint 機制確實要比MySQL要強一些。我記得在Oracle 8i的時候就提供了增量檢查點,在Oracle 10g就提供實現了檢查點的自動調節。這一定程度緩解了刷髒的問題。

那麼這裡我在想,我們能否通過一些方式來驗證一個數據庫IO在系統內核層是調用了2次呢?假設數據庫block 是8k,os block是4k。

為了進行這個測試驗證,我特意為我的虛擬機單獨增加了一個盤,用來跟蹤特定數據文件的IO情況,避免干擾。

<code>SQL> create table test0405 as select * from dba_objects;
Table created.
SQL> alter table test0405 move tablespace users;
Table altered.
SQL> select header_file,header_block from dba_segments where segment_name=‘TEST0405’;
HEADER_FILE HEADER_BLOCK
-------------------------
12 130
SQL> select file#,name,CON_ID from v$datafile;
FILE# NAME CON_ID
------------------------------------------------------------------------------------------------
9 /opt/oracle/oradata/ENMOTECH/killdb/system01.dbf 3
10 /opt/oracle/oradata/ENMOTECH/killdb/sysaux01.dbf 3
11 /opt/oracle/oradata/ENMOTECH/killdb/undotbs01.dbf 3
12 /opt/oracle/oradata/ENMOTECH/killdb/users01.dbf 3
SQL> alter database datafile 12 offline drop;
Database altered.
SQL> !
[oracle@mysqldb1 ~]$ mv /opt/oracle/oradata/ENMOTECH/killdb/users01.dbf /data3/users01.dbf
[oracle@mysqldb1 ~]$ exit
exit
SQL> alter database rename file ‘/opt/oracle/oradata/ENMOTECH/killdb/users01.dbf’ to ‘/data3/users01.dbf’;
Database altered.
SQL> recover datafile 12;
Media recovery complete.
SQL> alter database datafile 12 online;
Database altered.
SQL> select file#,name,CON_ID from v$datafile;
FILE# NAME CON_ID
-------------------------------------------------------------------------------------------------
9 /opt/oracle/oradata/ENMOTECH/killdb/system01.dbf 3
10 /opt/oracle/oradata/ENMOTECH/killdb/sysaux01.dbf 3
11 /opt/oracle/oradata/ENMOTECH/killdb/undotbs01.dbf 3
12 /data3/users01.dbf 3
SQL> select dbms_rowid.rowid_relative_fno(rowid) file_id,
2 dbms_rowid.rowid_block_number(rowid) block_id,
3 dbms_rowid.rowid_row_number(rowid) row_number from test0405
4 where object_id=100;
FILE_ID BLOCK_ID ROW_NUMBER
--------------------------------
12 132 32
SQL> alter system flush buffer_Cache;
System altered.

SQL> alter system checkpoint;
System altered.
SQL> alter system flush buffer_Cache;
System altered./<code>

下面開始進行相關的測試和驗證。

+++session 1

<code>SQL> delete from test0405 where object_id=100;
1 row deleted.
SQL> commit;
Commit complete.
SQL> alter system checkpoint;
System altered./<code>

+++session 2

<code>[root@mysqldb1 ~]# ps -ef|grep dbw|grep -v grep
oracle 3664 1 0 17:10 ? 00:00:00 ora_dbw0_enmotech
[root@mysqldb1 ~]#
[root@mysqldb1 ~]#
[root@mysqldb1 ~]# strace -fr -o /tmp/dbw0.log -p 3664
Process 3664 attached
^CProcess 3664 detached
[root@mysqldb1 ~]#
[root@mysqldb1 ~]# cat /tmp/dbw0.log |grep ‘pwrite’
3664 0.000221 pwrite(259,
“\\6\\242\\0\\0S8\\300\\0\\222f\\211\\0\\0\\0\\1\\6#\\305\\0\\0\\1\\0\\n\\0\\341/\\0\\0\\217f\\211\\0”…,
8192, 118120448) = 8192
3664 0.000298 pwrite(259,
“\\6\\242\\0\\0\\2279\\300\\0\\226f\\211\\0\\0\\0\\1\\6\\322\\204\\0\\0\\1\\0\\24\\0\\0240\\0\\0\\225f\\211\\0”…,
8192, 120774656) = 8192

3664 0.000135 pwrite(260, “\\2\\242\\0\\0\\202\\34\\0\\1If\\211\\0\\0\\0\\1\\4\\3f\\0\\0\\t\\0\\25\\0U\\3\\0\\0\\271\\0$$”…, 8192, 59785216) = 8192
3664 0.000124 pwrite(260,
“\\2\\242\\0\\0\\1\\35\\0\\1Kf\\211\\0\\0\\0\\1\\4c\\24\\0\\0\\7\\0\\4\\0v\\3\\0\\0\\257\\0\\31\\31”…,
16384, 60825600) = 16384
3664 0.000162 pwrite(263,
“\\6\\242\\0\\0N\\3\\0\\1\\233f\\211\\0\\0\\0\\1\\6\\227\\322\\0\\0\\1\\0\\3\\0\\200$\\0\\0\\232f\\211\\0”…,
8192, 6930432) = 8192
3664 0.000186 pwrite(264,
“&\\242\\0\\0\\240\\0@\\2\\246f\\211\\0\\0\\0\\2\\4O\\277\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0”…,
8192, 1310720) = 8192
3664 0.000162 pwrite(264,
“&\\242\\0\\0\\260\\0@\\2\\233f\\211\\0\\0\\0\\1\\4\\336\\234\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0”…,
8192, 1441792) = 8192
3664 0.000106 pwrite(264,

“&\\242\\0\\0\\300\\0@\\2\\231f\\211\\0\\0\\0\\1\\4;\\314\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0”…,
8192, 1572864) = 8192
3664 0.000168 pwrite(264,
“&\\242\\0\\0\\320\\0@\\2\\220f\\211\\0\\0\\0\\1\\4”\\277\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0"…,
8192, 1703936) = 8192
3664 0.000129 pwrite(264,
“\\2\\242\\0\\0\\314\\3@\\2\\217f\\211\\0\\0\\0\\1\\4\\270\\302\\0\\0\\6\\0\\6\\0\\217\\2\\0\\0\\342\\00066”…,
8192, 7962624) = 8192
3664 0.000123 pwrite(264,
“\\2\\242\\0\\0\\202\\6@\\2\\246f\\211\\0\\0\\0\\1\\4_M\\0\\0\\3\\0\\33\\0\\205\\2\\0\\0\\337\\0\\27\\27”…,
8192, 13647872) = 8192
3664 0.000116 pwrite(264,
“\\2\\242\\0\\0}\\20@\\2\\230f\\211\\0\\0\\0\\1\\4\\204*\\0\\0\\5\\0\\20\\0\\237\\2\\0\\0\\242\\0\\33\\33”…,
8192, 34578432) = 8192
3664 0.000109 pwrite(264,
“\\2\\242\\0\\0\\334\\22@\\2\\232f\\211\\0\\0\\0\\1\\4\\223\\355\\0\\0\\4\\0\\24\\0\\246\\2\\0\\0\\16\\1\\36\\36”…,
8192, 39550976) = 8192
3664 0.000108 pwrite(265,
“\\6\\242\\0\\0\\204\\0\\0\\3\\246f\\211\\0\\0\\0\\2\\6\\\\20\\0\\0\\1\\0\\0\\0e#\\1\\0\\234d\\211\\0”…,
8192, 1081344) = 8192
[root@mysqldb1 ~]# ls -ltr /proc/3664/fd
total 0
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 6 -> /opt/oracle/dbs/hc_enmotech.dat
lr-x------ 1 oracle oinstall 64 Apr 5 17:28 5 -> /proc/3664/fd
lr-x------ 1 oracle oinstall 64 Apr 5 17:28 4 -> /opt/soft/rdbms/mesg/oraus.msb
lr-x------ 1 oracle oinstall 64 Apr 5 17:28 3 -> /dev/null
l-wx------ 1 oracle oinstall 64 Apr 5 17:28 2 -> /dev/null
l-wx------ 1 oracle oinstall 64 Apr 5 17:28 1 -> /dev/null
lr-x------ 1 oracle oinstall 64 Apr 5 17:28 0 -> /dev/null
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 9 -> anon_inode:[eventpoll]
lr-x------ 1 oracle oinstall 64 Apr 5 17:28 8 -> /opt/soft/rdbms/mesg/oraus.msb
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 7 -> /opt/oracle/dbs/lkENMOTECH
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 271 -> /opt/oracle/oradata/ENMOTECH/pdbseed/temp012020-02-18_11-54-10-098-AM.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 270 -> /opt/oracle/oradata/ENMOTECH/pdbseed/system01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 269 -> /opt/oracle/oradata/ENMOTECH/pdbseed/sysaux01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 268 -> /opt/oracle/oradata/ENMOTECH/pdbseed/undotbs01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 267 -> /opt/oracle/oradata/ENMOTECH/killdb/temp01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 266 -> /opt/oracle/oradata/ENMOTECH/temp01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 265 -> /data3/users01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 264 -> /opt/oracle/oradata/ENMOTECH/killdb/undotbs01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 263 -> /opt/oracle/oradata/ENMOTECH/killdb/sysaux01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 262 -> /opt/oracle/oradata/ENMOTECH/killdb/system01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 261 -> /opt/oracle/oradata/ENMOTECH/users01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 260 -> /opt/oracle/oradata/ENMOTECH/undotbs01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 259 -> /opt/oracle/oradata/ENMOTECH/sysaux01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 258 -> /opt/oracle/oradata/ENMOTECH/system01.dbf
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 257 -> /opt/oracle/fast_recovery_area/ENMOTECH/control02.ctl
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 256 -> /opt/oracle/oradata/ENMOTECH/control01.ctl
lrwx------ 1 oracle oinstall 64 Apr 5 17:28 10 -> socket:[38121]/<code>

可以看到fd 265是我們的users數據文件,dbw0進程寫了一個數據塊8192。

+++session 3

<code>[root@mysqldb1 ~]# blktrace -d /dev/sdc1
^C=== sdc1 ===
CPU 0: 50 events, 3 KiB data
Total: 50 events (dropped 0), 3 KiB data/<code>


正常情況下的blktrace的信息不太好閱讀,這裡為用blkparse和btt來進行分別格式化一下。

<code>[root@mysqldb1 ~]# blkparse -i sdc1.blktrace.0
Input file sdc1.blktrace.0 added
8,32 0 1 0.000000000 4401 A FWFSM 1049672 + 2 8,33 0 2 0.000001830 4401 Q WSM 1049672 + 2 [kworker/0:1H]
8,33 0 3 0.000006576 4401 G WSM 1049672 + 2 [kworker/0:1H]
8,33 0 4 0.000007516 4401 P N [kworker/0:1H]
8,33 0 5 0.000009001 4401 I WSM 1049672 + 2 [kworker/0:1H]
8,33 0 6 0.000009865 4401 U N [kworker/0:1H] 1
8,33 0 7 0.000010825 4401 D WSM 1049672 + 2 [kworker/0:1H]
8,33 0 8 0.000271593 0 C WSM 1049672 + 2 [0]
8,32 0 9 29.999396763 2965 A WM 2112 + 16 8,33 0 10 29.999399134 2965 Q WM 2112 + 16 [xfsaild/sdc1]
8,33 0 11 29.999404432 2965 G WM 2112 + 16 [xfsaild/sdc1]
8,33 0 12 29.999406166 2965 P N [xfsaild/sdc1]
8,33 0 13 29.999407821 2965 I WM 2112 + 16 [xfsaild/sdc1]
8,33 0 14 29.999409058 2965 U N [xfsaild/sdc1] 1
8,33 0 15 29.999410349 2965 D WM 2112 + 16 [xfsaild/sdc1]
8,33 0 16 29.999778179 0 C WM 2112 + 16 [0]
8,32 0 17 31.374864114 3664 A WS 4256 + 16 8,33 0 18 31.374865091 3664 Q WS 4256 + 16 [ora_dbw0_enmote] —注意這裡
8,33 0 19 31.374866243 3664 G WS 4256 + 16 [ora_dbw0_enmote] —注意這裡
8,33 0 20 31.374866536 3664 P N [ora_dbw0_enmote]
8,33 0 21 31.374867424 3664 I WS 4256 + 16 [ora_dbw0_enmote] —注意這裡
8,33 0 22 31.374867676 3664 U N [ora_dbw0_enmote] 1
8,33 0 23 31.374868189 3664 D WS 4256 + 16 [ora_dbw0_enmote] —注意這裡
8,33 0 24 31.374934458 0 C WS 4256 + 16 [0]
8,32 0 25 31.379428011 3674 A WS 2160 + 16 8,33 0 26 31.379429174 3674 Q WS 2160 + 16 [ora_ckpt_enmote]
8,33 0 27 31.379431163 3674 G WS 2160 + 16 [ora_ckpt_enmote]
8,33 0 28 31.379431820 3674 P N [ora_ckpt_enmote]
8,33 0 29 31.379433132 3674 I WS 2160 + 16 [ora_ckpt_enmote]
8,33 0 30 31.379433653 3674 U N [ora_ckpt_enmote] 1

8,33 0 31 31.379434122 3674 D WS 2160 + 16 [ora_ckpt_enmote]
8,33 0 32 31.379686376 0 C WS 2160 + 16 [0] —表示寫完成(complete)
8,32 0 33 59.999440028 4401 A FWFSM 1049674 + 2 8,33 0 34 59.999442910 4401 Q WSM 1049674 + 2 [kworker/0:1H]
8,33 0 35 59.999448871 4401 G WSM 1049674 + 2 [kworker/0:1H]
8,33 0 36 59.999450211 4401 P N [kworker/0:1H]
8,33 0 37 59.999451575 4401 I WSM 1049674 + 2 [kworker/0:1H]
8,33 0 38 59.999452445 4401 U N [kworker/0:1H] 1
8,33 0 39 59.999453333 4401 D WSM 1049674 + 2 [kworker/0:1H]
8,33 0 40 59.999724225 0 C WSM 1049674 + 2 [0]
CPU0 (sdc1):
Reads Queued: 0, 0KiB Writes Queued: 5, 26KiB
Read Dispatches: 0, 0KiB Write Dispatches: 5, 26KiB
Reads Requeued: 0 Writes Requeued: 0
Reads Completed: 0, 0KiB Writes Completed: 5, 26KiB
Read Merges: 0, 0KiB Write Merges: 0, 0KiB
Read depth: 0 Write depth: 1
IO unplugs: 5 Timer unplugs: 0
Throughput (R/W): 0KiB/s / 0KiB/s
Events (sdc1): 40 entries
Skips: 0 forward (0 - 0.0%)/<code>


這裡我們把上述dbw0進程的幾行信息拿出來單獨分析說明:

<code>8,33  0      18    31.374865091  3664  Q  WS 4256 + 16 [ora_dbw0_enmote]    —注意這裡
8,33 0 19 31.374866243 3664 G WS 4256 + 16 [ora_dbw0_enmote] —注意這裡
8,33 0 20 31.374866536 3664 P N [ora_dbw0_enmote]
8,33 0 21 31.374867424 3664 I WS 4256 + 16 [ora_dbw0_enmote] —注意這裡
8,33 0 22 31.374867676 3664 U N [ora_dbw0_enmote] 1
8,33 0 23 31.374868189 3664 D WS 4256 + 16 [ora_dbw0_enmote] —注意這裡/<code>


其中第7列表示事件類型:

Q queued I/O進入block layer,將要被request代碼處理(即將生成IO請求)

G get request I/O請求(request)生成,為I/O分配一個request 結構體。

M back merge 之前已經存在的I/O request的終止block號,和該I/O的起始block號一致,就會合並。也就是向後合併

F front merge 之前已經存在的I/O request的起始block號,和該I/O的終止block號一致,就會合並。也就是向前合併

I inserted I/O請求被插入到I/O scheduler隊列

S sleep 沒有可用的request結構體,也就是I/O滿了,只能等待有request結構體完成釋放

P plug 當一個I/O入隊一個空隊列時,Linux會鎖住這個隊列,不處理該I/O,這樣做是為了等待一會,看有沒有新的I/O進來,可以合併

U unplug 當隊列中已經有I/O request時,會放開這個隊列,準備向磁盤驅動發送該I/O。

這個動作的觸發條件是:超時(plug的時候,會設置超時時間);或者是有一些I/O在隊列中(多於1個I/O)

D issued I/O將會被傳送給磁盤驅動程序處理

C complete I/O處理被磁盤處理完成。

第8列的W表示write,S表示sync。

第9列表示磁盤起始塊+操作的塊的數量

第10列表示進程名名稱

因此我們這裡重點看第8列就行了;同時確認第7列的狀態為D即可。那麼這一行的具體信息為:4256 + 16

其中4256是起始Block號,後面表示操作的Block數量。甚至我們還能看到具體的block 範圍:

<code>[root@mysqldb1 ~]# btt -i sdc1.blktrace.0  -B sdc1.off

[root@mysqldb1 ~]# cat sdc1.off_8,33_w.dat
1614.232741923 1049672 1049674
1644.232141447 2112 2128
1645.607599287 4256 4272 —這裡即是dbw0寫的block開始位置和結束位置;分別是4256和4276;兩者相減就是16
1645.612165220 2160 2176
1674.232184431 1049674 1049676/<code>

這裡為16,表上實際上os層完成了16個IO操作。比較怪異了……

我們的dbw0進程實際上只寫了一個8k的block,主機底層為什麼完成了16次IO操作?這是因為我這裡測試環境磁盤的物理扇區是512。

<code>[root@mysqldb1 ~]# cat /sys/block/sdc/queue/physical_block_size
512
[root@mysqldb1 ~]# cat /sys/block/sdc/queue/logical_block_size
512
[root@mysqldb1 ~]#/<code>

那麼這裡其實還有個小疑問。扇區雖然是512,但是並不代表8k的數據需要寫16次;這裡的16其實是表示os層寫了16個block,其中每個block是512。

但是這裡寫16個block,可以是一次原子寫或者兩次或者16次? 實際上查看blktrace的數據是可以進一步驗證的:

<code>[root@mysqldb1 ~]# cat 8,33_iops_fp.dat
1614 1
1644 1
1645 2 — 這裡2 表示IO 個數
1674 1/<code>

換句話說dbw0進程寫8k數據,os層實際上寫入了16個block,一共進行了2次IO操作,也就是兩次原子寫,那麼每次寫入就是4k。

本質上來講,Oracle這裡的8k在Linux 內核層面來看其實是2次原子寫操作完成的。如果2次操作只完成了1次,主機就crash了,怎麼辦?

最後簡單的總結一下:

Oracle 中不是沒有partial write的問題,而是Oracle 本身具有很多數據塊的完整校驗機制,寫失敗就直接回滾掉了,甚至在Oracle 11gR2版本還有寫數據文件發現IO異常直接crash 實例的特性(當然是為了更好的保護數據庫的完整性)。總的來講我個人認為Oracle的安全性還是非常高的。通過簡單分析,也能說明為什麼很多存儲複製軟件來做Oracle容災,在關鍵時刻備庫不一定能正常打開就的原因就是這樣。


分享到:


相關文章: