数据库磁盘分区真的丢失了?

2022-06-14 14:33:30 浏览数 (2)

裹挟前行:

1周前的周四,中途被业务方拉过去解决一次DB故障。由于不太了解当时的业务场景,只是听DBA说数据库服务器数据分区的磁盘丢失(笔者从来没有经历过磁盘突然丢失的场景),拿着同事的账号登录到发生故障的数据库服务器上,根据进程找到对应的磁盘目录,执行touch /data/mysql/abc, 可以正常执行,说明挂载的/data分区所在的文件系统是可以写的,MySQL命令行进入test库中,执行create table id_a(id int); 卡主, 在另外的一个mysql会话终端中,show processlist是可以正常执行的, show table|show databases都是可以正常执行。现象上看只要是DDL的语句执行均被阻塞,正当准备跟踪MySQL 的所有线程的时候,数据库进程已经被DBA 命令kill掉了。DBA重新挂载了一次/data分区后,启动数据库后,问题得到解决(这种做法大概率存在数据丢失,看后续分析)。

DBA给出的原因:

  1. 服务器磁盘丢失

2.系统配置问题

业务运维给出猜测因素:

1.内核mount丢失/mount缓存失效

在现场破坏掉的情况下,再复盘当时发生问题的过程是很有困难的。但好在同一批数据库服务器中,DBA执行同样操作的数据库实例还有2个,目前没有出现故障。以这2台数据库当前配置入手分析,应该可以发现有些蛛丝马迹。

事后现象分析:

登录到数据库服务器,首先发现

同一个挂载点/data被配置挂载过2块不同的磁盘。

再查看/proc/mounts的信息后,确认/data被挂载过2次。

当时内核的报错信息:

再次检查的时候,发现数据库进程同时在写ssd和sas磁盘:(ssd和sas磁盘都挂载到同一个/data上,数据库同时在写入两个磁盘)

df查看磁盘的时候:

/data分区显示是1.1T (而ssd的磁盘容量是896G)

(难怪说磁盘丢失!!!)

现状查看到此,数据库同时在写sas磁盘,ssd磁盘, 其中binlog,Myisam表文件写sas磁盘,ibd文件写ssd磁盘。鉴于数据库数据的重要性,建议:原因没有搞清楚前,再次故障的时候,DBA不能简单粗暴地重新挂载/data,重启数据库。

思考和分析:

因为有以下的问题没有搞清楚:

  1. 是怎么造成/data多次挂载的?
  2. 多个磁盘挂载到同一个/data, 什么原因造成同时写多块磁盘的现象?

第1个问题:

通过查找多个文件系统挂载的时间和数据库启动的时间即可看出端倪:

sas磁盘的文件系统挂载时间:

ssd磁盘的文件系统挂载时间:

MYSQL启动时间:

对比时间后,明显看到ssd磁盘先挂载后,sas磁盘后挂载,数据库的启动时间在两次挂载磁盘的中间。 从业务运维那边了解DBA有执行mount -a的操作命令, 到此问题1的原因已经清楚了。ssd磁盘挂载后,DBA启动了数据库,然后执行了mount -a , 该操作将sas磁盘文件系统第二次挂载到了/data(ssd磁盘也挂载在/data)。 嗯,DBA的骚操作。

第2个问题:

不同磁盘同时挂载到相同的/data后,数据库同时写2个磁盘, 最开始确实令人困惑。于是重新搭建环境:debian8.9 x64, 配置2块磁盘,将数据库的安装版本部署到测试环境,按照DBA的操作手法,分2次挂载到相同的/data目录下:

然后通过sysbench压测数据库,观察binlog的系统调用

同样的手法,观察创建表过程:

仔细观察,相对路径写文件的方式(以.开头),那么文件就会写到第一次mount的ssd磁盘的文件系统中,如果采用绝对路径写文件,那么文件就会写入到第二次mount的sas磁盘的文件系统中。

为了验证以上抓取到的现象,写一个测试程序:验证一下:

代码语言:javascript复制
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <errno.h>

//80G的硬盘第二次挂载到/data,开启一个线程写入
void thread_80g(void)
{
        int i;
        FILE* fout;  
        fout=fopen("/data/mysql/vdc_80g","a" );   
        //绝对路径写文件,不依赖进程中的fs_struct。
        if( fout==( FILE* )NULL )  
        {  
                printf( "80G open failed: %sn",strerror( errno ) );  
                exit( -1 );  
        }  
        printf("80G Writing...n" );  
        for(;;)
        {
                time_t timep;
                time (&timep);
                sleep(1);
                printf("%s",asctime(gmtime(&timep)));
                fprintf(fout,"%s",asctime(gmtime(&timep))); 
                fflush(fout);
        }  
        fclose( fout );  
}


//等到80G的硬盘第二次挂载到/data, 再使用相对路径写文件
//观察新创建的文件在哪个磁盘上。
void chdir_write_newfile()
{

        FILE* fout; 
        fout=fopen("vdd_100g.second","a" );   
        //相对路径写文件,受到主进程的pWD影响
        //相对路径有要求,事后诸葛亮
        if( fout==( FILE* )NULL )  
        {  
                printf( "vdd_100G.second open failed: %sn",strerror( errno ) );  
                exit( -1 );  
        }  
        printf("vdd_100G.second  Writing...n" );  
        time_t timep;
        time (&timep);
        sleep(1);
        printf("%s",asctime(gmtime(&timep))); 
        fprintf(fout,"%s",asctime(gmtime(&timep))); 
        fflush(fout);
        fclose( fout );  
}

//100G的磁盘分区挂载到/data, 开启一个写入线程
void thread_100g(void)
{
        int i,count=1;
        FILE* fout; 
        fout=fopen("vdd_100g","a" );    
        //相对路径写文件,受到主进程的pWD影响:fs_struct
        //相对路径有要求的,有点事后诸葛亮
        if( fout==( FILE* )NULL )  
        {  
                printf( "vdd_100G open failed: %sn",strerror( errno ) );  
                exit( -1 );  
        }  
        printf("vdd_100G Writing...n" );  
        for( ;;)  
        {
                if(count == 180){ // 180s create new file
                        chdir_write_newfile();
                }
                time_t timep;
                time (&timep);
                sleep(1);
                printf("%s",asctime(gmtime(&timep))); 
                fprintf(fout,"%s",asctime(gmtime(&timep))); 
                fflush(fout);
                count  ;
        }  
        fclose( fout );  
}


int main(void)
{
        pthread_t id,id2;
        int i,ret,ret2;
        char *dir = "/data/mysql";  
        int cret = chdir(dir);
         //先设置进程的pWD, 后期线程写相对路径的时候需要使用这个
        ret=pthread_create(&id,NULL,(void *) thread_100g,NULL);
        if(ret!=0){
                printf ("Create pthread error!/n");
                exit(EXIT_FAILURE);
        }
        sleep(30);
    // 间隔的时间内,100G硬盘已经挂载到/data,再执行一次mount -a , 让80G的硬盘再挂载到/data目录
        ret2=pthread_create(&id2,NULL,(void *) thread_80g,NULL);
        if(ret!=0){
                printf ("Create pthread error!/n");
                exit(EXIT_FAILURE);
        }
        printf("This is the main process./n");
        pthread_join(id,NULL);
        pthread_join(id2,NULL);
        return ;
}

#gcc -o muti_mnt_thread muti_mnt_thread.c -lpthread

#./muti_mnt_thread

运行后,完全可以复现mysql同时写2个磁盘的相同现象,相对路径写第一次挂载的文件系统(ssd磁盘),绝对路径写第二次挂载的文件系统(sas磁盘)。(为什么单独写程序验证,目的是为了后续跟踪kernel)

到此第二个问题的表面现象的原因已经知道,深层次的原因呢?因此引入第3个疑问:

深度分析:

3:相对路径和绝对路径为什么会引发写入不同的文件系统?

内核比较庞杂,但针对此次单个问题,只需要了以下方面即可:

3.1 使用相对路径open和使用绝对路径open创建文件的过程差异点在哪个地方?

先看3.1 相对路径写文件

由于这个过程逃不开进程,文件系统相关的常识。

一图抵千言:

进程和文件系统,文件的关联关系

图是摘抄的(Linux深入内核架构中的)

文件系统和super_block的关联关系

超级块和inode的关联关系

dentry(目录项)和inode的关联关系

其中task_struct进程中的成员变量: fs_struct

struct fs_struct {

atomic_t count;

rwlock_t lock;

int umask;

struct dentry * root, * pwd, * altroot;// 该进程关联的文件系统的根'目录'

struct vfsmount * rootmnt, * pwdmnt, * altrootmnt; //该进程关联的挂载实例

};

名称解释:

super_block: 文件系统的控制块,inode的分配/空闲信息

inode: 文件系统中的目录或者文件名所占的node编号

dentry:内存中的一个结构体,存放inode和目录或者文件名的关联关系

task_struct: 进程的描述符

files_struct: 存放进程打开文件相关信息的结果体

fs_struct: 进程所关联的文件系统相关信息的结构体

以上的关联关系可以看出:

创建文件,需要创建dentry, 创建dentry需要创建inode,创建inode需要找到super_block, 找到super_block需要找到文件系统,找到文件系统需要找到挂载点实例。进程是和文件系统存在着关联关系(当前使用的),如果以相对路径创建文件(不会跨文件系统), 那么依据当前进程中的文件系统信息(根dentry)(不需要查找挂载点的)创建文件足够。

在我们的案例中,创建文件均是.开头的相对路径, 那么不用查找挂载点(mountpiont),即具备创建文件所需的所有条件。多线程的场景下,所有的线程和主进程共享fs_struct,files_struct等。

也就意味着,写文件的时候,写线程依赖当前主进程的fs_struct, 而该fs_struct是在挂载第一次文件系统后且进程启动后就已经初始化好,所以相对路径写文件的时候,会使用当前进程使用的文件系统(第一次挂载的文件系统:即ssd磁盘)。因为Mysql(5.6.28)的IO线程打开事务日志文件(ibdata,iblogfile,innodb引擎的表文件)都是以. 相对路径打开,因此mysqld的IO线程 flush的时候,仍然使用第一次挂载的文件系统(ssd的磁盘)。

(真实发生的过程远比图中要复杂,但不影响原理问题的解释。

详解参看:(https://blog.csdn.net/wh8_2011/article/details/50669628) )

试想一下: 如果是 ../../../data/mysql/var这种相对路径的方式open写文件,那么会使用第一次挂载的文件系统还是第二次挂载的文件系统?

这个问题和绝对路径写文件本质是同一个问题

再看3.2 绝对路径写文件:

差异主要发生在:挂载点mount_hashtable

以下示意图 是 醉卧沙场大佬的(一图抵万言)

当多个文件系统挂载mount 到相同路径的时候,新挂载的文件系统会指向上一次挂载文件系统实例的根dentry,不准确的描述,mount过程象单向链表尾部添加元素的过程,第二次挂载的文件系统是第一次挂载文件系统的子文件系统, 第三次挂载的文件系统是第二次挂载文件系统的子系统。而以绝对路径open写文件的过程中,会触发检索从'/'到目标目录'之间'的文件系统挂载点的遍历,

挂载点的搜索函数__lookup_mnt()会递归地检索mount_hashtable,直到最后的一个文件系统。切回到故障的场景,用通俗易懂讲(不够严谨), MySQL重新写binlog的时候,采用的是绝对路径/data/mysql/binlog/binlog-20210107.log, 由于文件名是'/'开头,递归__lookup_mnt来检索全局mount_hashtable, 此时的挂载树:

根/ ---> /data(第一次挂载的文件系统) ---> /data 第二次挂载的文件系统), 递归查询后最后将第二次挂载的文件系统的dentry返回(返回最后一次挂载的mount实例),因此后续的文件系统操作,将会在第二次挂载的文件系统上进行。也就导致绝对路径写文件写到了第二次挂载的sas磁盘。(每次文件系统的切换,都需要重新定位根dentry)

详解见:

https://zhuanlan.zhihu.com/p/76419740?utm_source=wechat_session&utm_medium=social&utm_oi=810573874830934016&utm_campaign=shareopn

剩下就是通过ftrace来跟进open和lookup_mnt的过程。

对于进程已经打开的文件,意味着已经和fs,super_block,file,inode,dentry 建立了联系,已经不依赖mountpoint了(即使强制umount -lf /data), 仍然可以读写文件(小伙伴们自己可以模拟实现和跟踪)。

小结和解决办法:

综上所述:

在案例中,只要在open()的文件路径中,存在着多个mountpoint(挂载点),那么就一定会触发查询全局mount_hashtable的动作,那么就会使用最后一次mount的文件系统,否则会使用当前进程中的fs_struct的文件系统(第一次挂载的)。

解释最开始现象:

其中df命令底层实现使用stat(),是通过stat ("/data")获取分区大小的,stat("/data")通过绝对路径的方式访问,内核遍历全局mount_hashtable,导致会访问最后一次挂载的文件系统(sas磁盘), 获取的是sas磁盘的大小,所以从容量大小上看象是ssd磁盘丢失了。

MySQL使用open(/data/mysql/binlog/binlog-20210107.log),内核遍历全局mount_hashtable, 定位到第二次挂载/data的文件系统(sas磁盘的文件系统),文件写入到sas磁盘。 以./test/test.ibd相对路径的innodb的表文件,无需遍历全局mount_hashtable, 仍旧使用第一次挂载的/data文件系统,即ibd文件写入到ssd磁盘。

再回到故障场景中,

DBA同学说磁盘掉线,业务运维同学说内核mount丢失的各种阴谋论, 在实际的论证过程中已经全部排除。

临时解决办法:

将数据库停止后,需要将第二次挂载的文件系统上新增文件copy到第一次挂载的文件系统中(涉及到binlog和Myisam的MYI文件), 确认文件一致性无误后启动数据库。【不能简单remount重启,否则丢失数据】

0 人点赞