Mysql(三)数据在磁盘上的存储结构(mysql物理数据结构)

上文mysql(二)中主要对buffer pool的内存结构进行说明,这篇对数据页等在磁盘上的存储结构进行说明。

之前对于mysql存储结构的一些表空间、区(数据区)、数据页等概念进行说明。

一、一行数据在磁盘中的存储形式

1、这里先提一个问题:为什么要引入数据页的概念?


————如果每次buffer pool从磁盘随机读取数据、buffer pool随机写数据到磁盘。这两个过程数据的交换都是每次交换一行数据,那么这样明显是效率不高的。所以需要适当的加大每次交互的数据量,所以引进了数据页的概念(每个数据页大小16k)

2、我们都知道mysql的存储是将数据一行一行的往数据页中存储的(一页不够存其他页)。那么每行数据在数据页(磁盘)中是如何存储的?

————这里需要涉及一个概念:行格式。我们可以知道一个表的行存储格式,例如指定为compact格式

create table table_name  (columns)  ROW_FORMAT=COMPACT

alter table table_name ROW_FORMAT=COMPACT

如上面你既可以在建表的时候指定行存储格式,也可以后续修改存储的格式。那么在compact的行格式下每行数据在数据页中的存储格式如下:


变长字段的长度列表,null值列表,数据头,column01的值,column02的值,column03的值........

除了compact格式外,还有其他几种行存储格式,基本也是类似的

(1)、变长字段

在mysql中存在一些字段类型的长度是变长的,不固定的。例如varchar(10)他是可以在0-10的长度变化的。

正因为变成字段的长度是可变化的,所以变长字段的数据存储到磁盘后,我们要读取该行数据,此时怎么知道我们要读多长的数据才能正确的读到该字段的对应数据。

此时就需要上面所讲的行数据的结构中的 “变长字段的长度列表” 来指定每个变长字段的长度。

例如一行数据中存在varchar(10)、char(1)、varchar(10)这三个字段:hello a haha

此时有两个变长字段,分别是第一个和第三个(hello 和 haha)。那么用变长字段的长度列表来表示

0x04 0x05 null值列表  数据头 hello a haha

注意:多个变长字段时列表是逆序存储的,如上面haha(4个)的长度表示放置比hello(5个)的前面。其中0x05代表变长字段的长度。十六进制的0x05对应十进制的5。0x04代表4。

这里提出一个疑问:mysql多行数据在mysql中是紧凑存储的,这样的好处?为什么mysql不能像java那样将多行数据做成一个大对象,然后需要的时候反序列化下就可以拿出数据了?

(2)、null字段

在mysql中一行数据中如存在null值是不能直接存储的。而是用一个null值列表来记录。

因为如果将null值存成“NULL”则会占据较大的空间,而null值列表是用0、1来表示是否为null值的(减少存储空间),那么他是如何去表示多个null值的:
我们假设有一个表对应字段:name、address、gender、job、school。其中分别代表名字、地址、性别、工作、学校。

其中性别是定长字段,其他的都是变长字段。且name是not null,其他都可以为null。

那么我现在存储一行数据:zhangsan  null   m  null  xxschool

那么在磁盘的存储如下:
0x08 0x08 00000101 头信息  zhangsan  m  xxschool

其中其中变长字段列表和null值列表都是逆序存储的,其中00000101中其实五个字段中有四个是可以为null的,而null值列表是8个bit的倍数,所以前面的0000是补上去的,而0101才是真正的null值列表。1代表为空,逆序排序四个可以为空的字段的值则对应的是————0101。

此时我们进行读取的时候就可以根据null值列表得到不为空的字段,再根据变长字段列表信息再正确的读取到字段对应的值。

(3)数据头(40bit)

第一个和第二个bit————预留位,没任何含义。

delete_mask(一个bit位)————标识该行数据是否被删除(所以我们在删除一行数据时,并不是立即把他从磁盘中清楚)

min_red_mask(一个bit)————B+树中每一层的非叶子节点的最小值都有这个标记

n_owned(4个bit)————

heap_no(13个bit)————记录该行数据咋爱记录堆里的位置

record_type(3个bit)————该行数据的类型。0代表普通类型;1代表b+树非叶子节点;2代表最小值数据;3代表最大值数据。

next_record(16个bit)————指向下一条数据的指针

到此我们假如向(2)中的表插入一行数据“jack  null  m null  xx_school”那么他的大致存储是这样的

0x09 0x04 00000101 00000000000000000000100000000000011001 jack m xx_school

然后mysql会将字段值根据我们数据库指定的字符集编码,进行编码后存储,大致如下:

0x09 0x04 00000101 00000000000000000000100000000000011001  616161  636320  6262626262

其实mysql还会在真实数据部分加入一些隐藏字段,这个字段跟后续的一些内容有关。

1)、DB_ROW_ID :一个行的唯一标识(不是主键id字段),如果我们没有指定主键和unique key 唯一索引时,mysql就会内部自定加一个ROW作为主键(内部自己选一个字段作为主键)

2)、DB_TRX_ID:跟事务相关,事务id,表示哪个事务更新的数据

3)、DB_ROLL_PTR:回滚指针,用来进行事务回滚的。

那么加上这些隐藏字段,最后这行数据的存储格式如下

0x09 0x04 00000101 00000000000000000000100000000000011001  0000000000094C  00000000032D  EA000010078E  616161  636320  6262626262

其中0000000000094C  为DB_ROW_ID ,00000000032D 为DB_TRX_ID,EA000010078E 为DB_ROLL_PTR。



3、行溢出

假如我们有一个字段类型时varchar(65532),此时存到65532个字符了,那其大小则超过了一个缓存页/数据页的大小(16k)。

此时mysql就会将这个数据存到多个数据页中,一个数据页存不完则在真实数据部分用20个字节去指向下一个数据页,依次类推直到存完。

上面的过程则被称为行溢出。其他的数据页也可称为溢出页。

二、数据页、表空间、数据区的基本结构(含多个数据行)

1、数据页

一个数据页的大小默认16k。而一个数据页不是只包含行数据。一个数据页可以拆分为多个部分,例如文件头、数据页头、最小记录和最大记录、多个数据行、空闲空间、数据页目录、文件尾部等。

其中文件头占据38个字节,数据页头占据56个字节,最大记录和最小记录26个字节,数据行区域大小是不固定的,空闲区域的大小是不固定的,数据页目录大小不固定,文件尾部占据8个字节。

虽然一个数据页大小是固定的,但数据行区域、空闲区域、数据页目录是不固定的。具体看下方:

1)、刚开始时,整个数据页时空的,则里面没有数据行区域了。

2)、当我们往数据页中加入一行数据时,此时就会从空闲区域中分出一些空间给到该数据行,依次类推。最后数据行的部分就叫做数据行区域。知道空闲区域被用完。

2、表空间

简单来讲,我们平常创建的那些表,其实都是有一个表空间的概念,在磁盘上都会对应着“表名.ibd” 这样的一个磁盘数据文件。

所以在物理层面,表空间就对应磁盘上的数据文件。一般我们创建的表对应的表空间对应一个数据文件,而系统表空间可能对应多个磁盘文件。

而因为一个表空间包含多个数据页,数据页太多时不便管理,所以又在表空间中引入了一个数据区的概念(extent)

3、数据区

一个数据区对应64个连续的数据页,也就是一个数据区有1Mb,然后256个数据区又划分为一个组。

对于表空间来说,第一组数据区的第一个数据区的前3个数据页是固定的,里面存放了一些描述性的数据。

例如FSP_BITMAP数据页存放了表空间和这一组数据区的一些属性

IBUF_BITMAP数据页存放这一组数据页的所有insert buffer的一些信息

INNODE数据页页存放了一些特色的信息

然后这个表空间的其他各个组,每组的第一个数据全去的头两个数据页,都是存放一些特殊信息的,例如XDES数据页就是用来存放这一组数据区的一些相关属性的。

4、两种数据读写机制

在mysql的实际工作中,对于磁盘的读写存在两种机制。

一种是对redo log、binlog这种日志进行磁盘顺序读写,一种对表空间的磁盘文件里的数据页进行磁盘的随机读写。(undo日志是顺序还是随机?)

对于磁盘随机读来说,主要关注的性能指标是IOPS和响应延迟

IOPS表示每秒支持执行几次磁盘读写操作,这个指标对于对数据库的crud操作的QPS影响是非常大的。底层存储的IOPS越高数据库的并发能力越高。响应延迟也是衡量数据库curd操作的QPS的重要指标。

我们常听到的SSD固态硬盘随机读写并发能力和响应延迟比机械硬盘好很多,能大幅度的提升数据库的QPS和性能。

redo log等日志写入磁盘时,是必须要经过os cache的。(配置参数是配置os何时刷入磁盘)

顺序/随机读写在Linux是怎么运行的?

(1)、linux的存储系统分为VFS层、文件系统层、Page Cache缓存层、通用Block层、IO调度层、Block设备驱动层、Block设备层,如下图:

(1-1)、每当mysql发起一次数据页的随机读写、或者redo log文件的顺序读写时,实际会把磁盘IO请求交给VFS层。VFS层是根据你对哪个目录中的文件进行磁盘IO操作,就把IO请求交给具体的文件系统。

例如:目录为/xx1/xx2由NFS文件系统管理、/xx3/xx4由Ext3文件系统管理,那么此时VFS层根据此次IO对哪个目录下的文件发起IO请求,然后将IO请求转发到相应的文件系统。

(1-2)、然后文件系统会先在Page Cache(基于内存的缓存)中找是否有对应的缓存数据。如有则执行读写。没有则继续往下走,此时将请求交给Block层。

(1-3)、在Block层会把对文件的IO请求转换为Block IO(bio)请求,然后将Block IO请求交给IO调度层。

(1-4)、IO调度层拿到请求后会使用默认的CFQ公平调度算法(不管执行时间,公平算法)来决定哪个Block IO执行,而实际生产环境中一般调整为deadline IO调度算法(任何一个IO不能一直等待阻塞,在指定时间内都必须让他去执行,这样不会因为一个执行时间长/死锁IO导致执行时间端的IO不能执行)。

(1-5)、IO完成调度后,就会决定哪个IO请求先执行,哪个慢执行。然后会将要执行的Block IO请求交给Block设备驱动层,然后最后经过驱动把IO请求发给真正的存储硬件(即Block设备层)。

(1-6)、执行完IO请求后,最后把响应经过上面的层级反向依次返回。

5、Mysql的生产故障案例

5.1、RAID锂电池充放电导致的mysql性能抖动

(1)RAID多磁盘阵列技术

其实我们可以把mysql看成一个数据库软件安装在linux中,mysql运行时不是直接调用底层硬件的,都是通过调用操作系统提供的接口,依托于操作系统来使用和运行的。然后操作系统负责操作底层的硬件。

一般来说,很多数据库部署在机器上的时候,存储都是搭建的RAID存储架构,而RAID就是一个磁盘冗余阵列

假设我们的服务器里的磁盘就一块,那万一 一块磁盘的容量不够怎么办?此时是不是就可以再搞几块磁盘出来放在服务器里

现在多搞了几块磁盘,机器里有很多块磁盘了,不好管理啊,怎么在多块磁盘上存放数据呢?

所以就是针对这个问题,在存储层面往往会在机器里搞多块磁盘,然后引入RAID这个技术,大致理解为用来管理机器里的多块磁盘的一种磁盘阵列技术!

有了他以后,你在往磁盘里读写数据的时候,他会告诉你应该在哪块磁盘上读写数据,如下图。

有了RAID技术后,我们就可以在一台服务器中加多块磁盘,然后对磁盘进行读写的时候通过RAID技术帮我们选择一块磁盘进行读写。RAID除了管理多磁盘外,还有一个很重要的作用,就是可以实现数据冗余机制。

例如你写入一批数据到RAID的一块磁盘中,然后磁盘坏了无法读取了,那你不就丢失了这些数据了?此时RAID的数据冗余机制就是用来解决该情况的,在RAID磁盘冗余阵列技术中,会将你写入的数据写入到两块硬盘中,这样两块硬盘上该部分的数据就一模一样,当你一块磁盘坏了时,可以从另一块硬盘读取冗余数据。(这个过程都是RAID自动管理的)

RAID技术实际就是管理多磁盘阵列技术,可以有软件层面的实现,也可以有硬件层面的实现(例如RAID卡这种硬件设备)

具体来说,RAID还可以分成不同的技术方案,比如RAID 0、RAID 1、RAID 0+1、RAID2,等等,一直到RAID 10,很多种不同的多磁盘管理技术方案。

(2)RAID存储架构的电池充放电原理

服务器使用多块磁盘组成的RAID阵列时,一般会有一个RAID卡,这个卡是有带缓存的,这个缓存不是服务器主内存的那种模式,而是一种跟内存类似的SDRAM。

我们可以把RAID的缓存模式设置为write back,这样所有写入磁盘阵列的数据,会先缓存在RAID卡的缓存里,然后慢慢的写入磁盘阵列里去。(这样可以大大提升我们数据库的磁盘写的性能)。

那么此时问题来了,如果此时断电,我们知道这个缓存是类似于内存的,如果断电数据也是会丢失的。那么这个时候怎么办?

此时为了解决这个问题,RAID卡一般会配置自己独立的锂电池或者是电容,如果突然断电,RAID卡就会基于自己独立的锂电池来供电运行,然后再把数据写入阵列中的磁盘上。

但是锂电池是存在性能衰减的问题的,所以一般锂电池需要配置定时充放电,一般每隔30-90天(不同厂商不一样),就会对锂电池进行充放电依次,这样能延长锂电池的寿命和校准电量容量。

不这么做的话可以会在停电后,因为锂电池电量不足,导致部分数据没写入磁盘锂电池就没电了,最后也是导致数据丢失。

在锂电池充放电时,RAID的缓存级别会从write back变为write through,该级别我们通过RAID写数据时直接写到磁盘了,不经过内存缓存了。此时性能会降低很多。

(所以一般使用了RAID多磁盘阵列存储技术的公司需要注意,一旦锂电池进行充放电时,一般会道中系统服务器的几十倍的抖动)

(3)、RAID技术中锂电池充放电导致的性能抖动问题及解决方案

在说明该案例前先说下RAID的三种磁盘阵列架构RAID 1、RAID 0、RAID 10

RAID 0:多个磁盘,但没有冗余机制,一块磁盘坏了就丢失那部分的数据

RAID 1:多个磁盘,有冗余机制,两块磁盘互为镜像关系,这样一块磁盘坏了可以从另一个磁盘中读取冗余数据(读请求可以分配到两个磁盘的任何一个)(那两块磁盘间怎么进行数据同步的??????)(RAID 1是所有磁盘的冗余数据一致????还是说某个数据会在阵列中存在两份数据(随机存在某两个磁盘中)

RAID 10:就是RAID 1+RAID 0的组合。即每N块磁盘组成一个RAID 1的架构组,然后对每组进行写入时采用RAID 0的架构,此时就会形成不同组的磁盘的数据不一样,但同一组的磁盘的数据是冗余且一致的(简单讲就是每N块为一组,每组的数据不一致,但组内各个磁盘的数据一致)

所以对于这样的一个使用了RAID 10架构的服务器,他必然内部是有一个锂电池的,然后这个锂电池的厂商设定的默认是30天进行一次充放电,每次锂电池充放电就会导致RAID写入时不经过缓存,性能会急剧下降,所以我们发现线上数据库每隔30天就会有一次剧烈性能抖动,数据库性能下降了10倍。

当时为了排查这个问题,我们使用linux命令查看了RAID硬件设备的日志,这个具体什么命令不说了,因为你用不同的厂商的RAID设备,这个命令实际上是不一样的,发现RAID就是每隔30天有一次充放电的日志,所以就是由于这个定期的充放电导致了线上数据库的性能定期抖动!

那么后续如何解决这个问题呢?对于RAID锂电池充放电问题导致的存储性能抖动,一般有三种解决方案:

  1. 给RAID卡把锂电池换成电容,电容是不用频繁充放电的,不会导致充放电的性能抖动,还有就是电容可以支持透明充放电,就是自动检查电量,自动进行充电,不会说在充放电的时候让写IO直接走磁盘,但是更换电容很麻烦,而且电容比较容易老化,这个其实一般不常用

  2. 手动充放电,这个比较常用,包括一些大家知道的顶尖互联网大厂的数据库服务器的RAID就是用了这个方案避免性能抖动,就是关闭RAID自动充放电,然后写一个脚本,脚本每隔一段时间自动在晚上凌晨的业务低峰时期,脚本手动触发充放电,这样可以避免业务高峰期的时候RAID自动充放电引起性能抖动

  3. 充放电的时候不要关闭write back,就是设置一下,锂电池充放电的时候不要把缓存级别从write back修改为write through,这个也是可以做到的,可以和第二个策略配合起来使用

5.2、数据库无法连接故障的定位,Too many connections

在生产中经常会发现连接数据库时,会报出异常信息“ERROR  1040(HY000):Too  many connections”。这个错误代表数据库的连接池里已经没有连接和你建立连接了。(客户端(自己配置连接池)和服务端都有自己的连接池)

因为linux内核的参数(文件句柄不足)而导致mysql的最大连接数被限制:
案例:数据库部署在64G的大内存物理机上,机器配置各方面都很高,然后该mysql连接两个java系统,这两个java系统设置连接池的最大大小是200,也就是两台加起来有最多400个连接。但是在java系统连接数据库时报异常Too many Connections。(证明此时mysql服务端的连接池连接数不够了)

于是开始排查mysql的配置文件my.cnf(windows是my.ini,linux是my.cnf),里面有一个参数max_connections,

就是mysql能建立的最大连接数,发现该项被设置为800。

那为什么400都连不上?
此时执行下面命令,查看此时mysql的连接数:

show status like 'Threads%';

发现连接数只是214个连接而已,那么看来我们的那个max_connections设置最大连接数是没起作用的。

此时我们去看mysql的启动日志,可以看到如下字样:

日志中已说明无法创建为我们设置的800的连接数,所有只能强行限制为214。(其中max_open_files表示的就是句柄)

那这是为什么?其实就是因为linux系统把进程可以打开的句柄数限制为1024,导致mysql的连接数被限制。mysql源码中就对连接数的计算方法写死了,linux对每个进程会限制其对机器资源的使用(防止某个进程消耗太大),包括文件句柄、子进程数、网络缓存限制、可锁定的内存大小等资源。那么此时怎么将mysql的连接数提高?

此时需要对linux的一些内核参数(句柄)进行配置,命令:ulimit -HSn 65535

然后就可以用如下命令检查最大文件句柄数是否被修改了

cat /etc/security/limits.conf

cat /etc/rc.local

重启mysql及服务器,此时最大句柄+连接数都会生效。

所以往往在生产中部署一个系统,比如数据库系统、消息中间件系统、存储系统、缓存系统之后,都需要调整下linux的一些内核参数,这个

文件句柄的数量是一定要调整的,通常都得设置为65535

还有比如Kafka之类的消息中间件,在生产环境部署的时候,如果你不优化一些linux内核参数,会导致Kafka可能无法创建足够的线程,此时也是无法运行的。

所以我们平时可以用ulimit命令来设置每个进程被限制使用的资源量,用ulimit -a就可以看到进程被限制使用的各种资源的量。

比如 core file size 代表的进程崩溃时候的转储文件的大小限制,max locked memory就是最大锁定内存大小,open files就是最大可以打开的文件句柄数量,max user processes就是最多可以拥有的子进程数量。

设置之后,我们要确保变更落地到/etc/security/limits.conf文件里,永久性的设置进程的资源限制

所以执行ulimit -HSn 65535命令后,要用如下命令检查一下是否落地到配置文件里去了。

cat /etc/security/limits.conf

cat /etc/rc.local

查看mysql的最大连接数、活跃数、并发数等命令:Mysql查看连接数(连接总数、活跃数、最大并发数) - caoss - 博客园

  • 1
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值