昨天晚上一个朋友打电话咨询一个Oracle数据库无法启动的问题,是因为之前出现的异常宕机引发的。这是一个因为数据库IO丢失引发的数据库不一致问题,Oracle在数据库启动的 时候发现了一些比较严重的不一致问题,就会无法打开数据库。在数据库原理中,数据库通过Write Ahead Log(WAL)机制来确保数据库在实例宕机之后不会出现数据的不一致问题,不会丢失已经提交过的事务。从基础原理上,似乎数据库能够自己保证自己的一致性,为什么还会出现类似的问题呢?
实际上这种设计是有一个前提的,那就是每个已经写盘的IO,都真实地物理落盘了。这是数据库的数据永远保持一致性的前提,如果这个前提出现了漏洞,那么这个理论上的永远一致性就不存在了。事实上,数据库的IO链路很长,操作系统、RAID卡、HBA卡、SAN交换机缓冲区、集中式存储机头、集中式存储的写缓冲、磁盘的写缓冲、磁盘,等等。我可能还没有罗列完整,因为在不同的环境中,这个链路还会略微不同。在很多层面,IO都会被优化,因此真实的IO落盘并非和我们想象的一样。如果在云环境中,这个IO路径就更为复杂了。
当IO在这些环节中的某个缓冲中丢失了,那么数据库的底层就会丢失IO了,这个IO丢失会引发一系列的数据不一致。比如一个8K的数据块,前半部分已经写盘,但是后半部分的写IO丢失了,这样就会出现“块断裂”,一个数据块的数据不一致了。MySQL等数据库使用DOUBLE WRITE BUFFER来解决这个问题,PG则采用FULL PAGE WRITE LOG的机制。Oracle则比较粗犷,完全不管这个问题,让底层存储系统来确保写IO的原子性。究其原因,Oracle自从出生开始,就是和高端硬件关联的,和MySQL/PG这些草根的设计思路完全不同。
数据库面对复杂的底层环境,所以无法确保其基础理论的实现,这个可能会出乎一些朋友的意料。Michael Stonebraker老爷子要搞DBOS,其目的是为数据库提供一个完全以数据库的设计理念为基础的底层环境。这个理想很宏大,实现起来恐怕也是困难重重的。因为这些违背数据库设计理念的设计都是为了优化。一个通用的DBOS可能无法适应不同的数据库产品的通用优化需求。DBOS作为一个数据库SAAS服务的基础平台是可行的,成为一个通用的数据库底座任重道远。
当底层IO出现丢失的时候,Oracle处置起来是最为麻烦的,我想很多Oracle的老DBA也都因此赚了不少钱,昨晚我那个朋友就因为帮人打开了一个出现ORA-600[2662]的数据库而赚了1万块钱的外快。这是因为Oracle的控制文件、REDO、UNDO、数据文件一旦因为IO丢失而出现不一致会引发数据库无法打开的问题。ORA-01113、ORA-600[2662]、ORA-600[3020]、ORA-600 [4000]、ORA-600[4193]、ORA-600[4194]等错误的出现往往就与这些有关。20年前我在ORACLEFANS网站上也发过不少处置类似问题的文章,在微信公众号里我也写过一篇《如何强制打开无法启动的Oracle数据库》,里面简单地介绍了一些处理方法。
如果出现ORA-600[2662],整个处理过程会麻烦一些,因为数据库启动的时候发现某个数据文件的SCN已经高于数据库的当前SCN。早期我们可以通过adjust_scn事件来往前推进数据库的当前SCN,从而解决这个问题。Oracle 11.2.0.2.6以后,adjust_scn等待事件被废弃了,如果遇到这种情况,用oradebug 修改内存中的CURRENT SCN也可以起作用。从Oracle 12c开始废弃了这种做法,Oracle又提供了一个EVENT 21307096来解决这个问题(详情请参考《Force Open Database after applying Patch 21307096 (Doc ID 2674196.1)》)。
实际上Oracle数据库IO丢失在SYSTEM表空间中才是最麻烦的,因为在SYSTEM表空间中存在bootstrap objects,还有一个system rollback segment。如果这些对象丢失了IO,那么就需要使用BBED这样的工具去修复才能避开问题,强行打开数据库。强行打开了丢失IO的数据库之后,大部分数据可以顺利导出,如果某些表上还是存在坏块,需要通过跳过坏块的技术来导出表中的数据。一般来说这个数据库已经不能作为生产库使用了,导出数据后重建数据库是最好的做法。
一般来说这些处置手段都是作为最后的处置手段,如果数据库存在备份,而且备份中丢失的数据在合理的范围内,通过备份恢复数据库可能是更好的做法。遇到这样问题的客户往往是比较慷慨的,因此DBA掌握这些技术,还是有价值的。