1. Xtrabackup原理
基于InnoDB的crash-recovery功能,先拷贝InnoDB数据文件,保证数据内部一致性,再通过crash recovery功能保证数据库文件一致性与数据库可用性。
InnoDB引擎启动时,会先检查数据文件与redo log(transaction log),并执行:
- redo log中commit的事务应用到数据文件中;
- 将没有commit的事务回滚。
备份
Xtrabackup开始时会记录当前的LSN(log sequence number),并拷贝数据文件。同时,Xtrabackup会运行一个后台进程监控事务日志文件,并拷贝记录的LSN时间点后执行的事务。
Xtrabackup会使用一个轻量级的Backup Lock替代FTWRL锁(MySQL 5.6+),自动拷贝非InnoDB数据避免阻塞会修改InnoDB表的DML语句。在MySQL支持Backup Lock的情况下,Xtrabackup首先会拷贝InnoDB数据,执行LOCK TABLES FOR BACKUP并拷贝MyISAM表与.frm文件。一旦拷贝操作完成,Xtrabackup将开始备份.frm、.MRG、.MYD、.MYI、.TRG、.TRN、.ARM、.ARZ、.CSM、.CSV、.par、以及.opt文件。
之后,Xtrabackup会使用LOCK BINLOG FOR BACKUP阻塞所有可能修改binlog的log position、exec_master_log_pos或exec_gtid_set数据的操作并执行show master/slave status语句获取当前binlog位置,并停止拷贝redo log文件。至此,xtrabakcup将unlock binlog与表。
最终binlog position会输出到STDERR,若未发生错误Xtrabackup将退出并返回0值的退出状态码(STDERR不会写入任何文件,需要重定向到一个文件,如xtrabackup OPTIONS 2> backupout.log)。备份完成后,将在指定目录下创建如下文件:
backup-my.cnf
xtrabackup_checkpoints
xtrabackup_binlog_info
xtrabackup_binlog_pos_innodb
xtrabackup_binary
xtrabackup_logfile
.delta.meta xtrabackup_slave_info
- xtrabackup_galera_info
恢复
在准备阶段,Xtrabackup使用拷贝的事务日志文件对拷贝的数据文件执行crash recovery,该过程完成后数据库完成备份恢复。
备份的MyISAM与InnoDB表最终会实现相互一致性,因为在prepare阶段,InnoDB的数据会前滚到备份完成的时间点,即FLUSH TABLES WITH READ LOCK执行的时间点,所以MyISAM数据与完成prepare的InnoDB数据能够同步。
使用xtrabackup --copy-back or xtrabackup --move-back 参数恢复备份文件。
- xtrabackup --move-back将移动备份文件到目标位置,并会移除原始备份文件。可用于当磁盘空间不足时保存数据文件。
Xtrabackup会读取my.cnf的变量datadir、innodb_data_home_dir、innodb_data_file_path、innodb_log_group_home_dir变量并检查以上目录是否存在。若目录存在,Xtrabackup首先开始拷贝MyISAM表与索引等文件(.frm、.MRG、.MYD、.MYI、.TRG、.TRN、.ARM、.ARZ、.CSM、.CSV、.par、.opt文件),随后拷贝InnoDB表与索引文件,最后拷贝日志文件。在拷贝过程中,文件的属性不会改变,因此在拷贝前需要修改文件的权限为mysql用户、mysql用户组,否则拷贝后的文件将数据执行Xtrabackup备份的用户。
参考
-
知数堂MySQL实战班-第七课:MySQL物理备份
-
https://www.percona.com/doc/percona-xtrabackup/2.4/how_xtrabackup_works.html
-
https://www.percona.com/doc/percona-xtrabackup/2.4/xtrabackup-files.html#xtrabackup-files
2. pt-online-schema-change原理
pt-online-schema-change模拟MySQL内部修改表结构的方式,但实际的修改操作执行在目标表的拷贝表上。即原始的表并未被锁,客户端仍然可以读取并写入数据到原表上。
pt-online-schema-change先拷贝一张要修改表的空表(仅拷贝表结构),并对新表的表结构进行修改,完成表结构修改后pt-online-schema-change将拷贝原表的行数据到新表中。当以上拷贝操作全部完成后,pt-online-schema-change将用新表替换掉原始表,默认情况下原始表会被drop掉。
原始表的行数据拷贝由每次拷贝一小区块(chunk)数据实现,类似pt-table-checksum。所有在拷贝过程中发生在原始表上的数据修改操作也会通过pt-online-schema-change创建在原始表上的triggers同样执行在新表上,从而保证原始表与新表的数据一致性(若使用的triggers在被创建前在原始表中已存在,pt-online-schema-change将失败)。
pt-online-schema-change完成数据拷贝到新表后,再使用rename table操作同时修改原始表与新表保证原子性,该操作完成后,原始表被drop。
参考
- https://www.percona.com/doc/percona-toolkit/3.0/pt-online-schema-change.html
3. MySQL的原理?(待补充)
4. page一行记录多大,最多能放多少行?(待补充)
简析
InnoDB Page分为叶子节点页与非叶子节点页
叶子节点页
可用空间:一个16KB的page,去掉page header、trailer信息、index header、file segment header、infimum与supremum(两条虚拟记录)等固定字节后,大约只有15212字节可用。
一条数据(INT列)需要消耗的字节计算为22字节:
- DB_TRX_ID:6字节
- DB_ROLL_PTR:7字节
- Record Header:5字节
- 一条INT数据:4字节
大约每4条记录需要一个Directory Slot,每个Slot需要2字节。
由以上条件可计算,假设一个页可以存储N条记录,则N*22 + (N/4)*2=15212,可算出N约等于676。即一个InnoDB叶子节点页可以最大可存储约676条行数据。
非叶子节点页(索引页)
可用空间:去掉固定字节后,剩余16241字节。
一条行记录消耗字节为13字节:
- 非叶子节点中,每个指向叶子节点的指针消耗4字节。
- Record Header占据5字节
- 记录为INT数据,消耗4字节
同理计算N*13 + (N/4)*2=16241,得出N约等于1203。即一个非叶子节点页可存储的行数据约为1203行。
InnoDB页结构
所有页默认为16KB大小,一个page除了行记录(record)以外,也包含有头信息(headers)与尾部信息(trailers)。更细粒度上来看,一个InnoDB页包含8个部分:
- 文件页头信息(Fil Header)
- 页头信息(Page Header)
- Infimum + Supremum Records
- 行记录(User Records)
- 空余空间(Free Space)
- 页目录(Page Directory)
- 文件页尾部信息(Fil Trailer)
其中,1.文件页头信息(Fil Header)与7.文件页尾部信息(Fil Trailer)相对应,2.页头信息(Page Header)与6.页目录(Page Directory)相对应。即为一个InnoDB页的两对header/trailer。在这两对header/trailer中间的空间为行数据空间,行数据空间的起始部分为Infimum与Supremum行记录,这两条记录值固定不变。
一个默认大小为16KB的页,共16384字节,除去Fil Header的38字节、Page Header的56字节、Infimum与Supremum记录的26字节、Fil Trailer的8字节,剩余16256字节。
文件页头信息(Fil Header)
包含8个部分,共38字节:
- 文件页空间(FIL_PAGE_SPACE):4字节,标识当前页所在「空间」的4位ID,「空间」广义上可理解为日志或表空间。FIL_PAGE_SPACE作为必要的标识符以区分在相同日志文件中却属于不同表空间的的页。
- 前序文件页(FIL_PAGE_PREV):4字节,以索引顺序标识前序页的偏移量
- 后序文件页(FIL_PAGE_NEXT):4字节,以索引顺序标识后序页的偏移量
- 文件页日志序列号(FIL_PAGE_LSN):8字节,当前页的最新日志记录的日志序列号(LSN)。
- 文件页类型(FIL_PAGE_TYPE):2字节,当前定义的类型包括FIL_PAGE_INDEX, FIL_PAGE_UNDO_LOG,FIL_PAGE_INODE, FIL_PAGE_IBUF_FREE_LIST。
- 文件页已刷写的日志序列号(FIL_PAGE_FILE_FLUSH_LSN):8字节,当前文件已经至少被刷写到磁盘的日志序列号,只在当前文件的第一个页存在。
- LSN对应日志文件编号(FIL_PAGE_ARCH_LOG_NO):4字节,当前已刷写到磁盘的LSN对应的日志文件编号,同样也只在数据文件的第一个页存在。
页头信息(Page Header)
包含14部分,共56字节:
Name | Size | Remarks |
---|---|---|
PAGE_N_DIR_SLOTS |
2 | number of directory slots in the Page Directory part; initial value = 2 |
PAGE_HEAP_TOP |
2 | record pointer to first record in heap |
PAGE_N_HEAP |
2 | number of heap records; initial value = 2 |
PAGE_FREE |
2 | record pointer to first free record |
PAGE_GARBAGE |
2 | "number of bytes in deleted records" |
PAGE_LAST_INSERT |
2 | record pointer to the last inserted record |
PAGE_DIRECTION |
2 | either PAGE_LEFT , PAGE_RIGHT , or PAGE_NO_DIRECTION |
PAGE_N_DIRECTION |
2 | number of consecutive inserts in the same direction, for example, "last 5 were all to the left" |
PAGE_N_RECS |
2 | number of user records |
PAGE_MAX_TRX_ID |
8 | the highest ID of a transaction which might have changed a record on the page (only set for secondary indexes) |
PAGE_LEVEL |
2 | level within the index (0 for a leaf page) |
PAGE_INDEX_ID |
8 | identifier of the index the page belongs to |
PAGE_BTR_SEG_LEAF |
10 | "file segment header for the leaf pages in a B-tree" (this is irrelevant here) |
PAGE_BTR_SEG_TOP |
10 | "file segment header for the non-leaf pages in a B-tree" (this is irrelevant here) |
注:
- PAGE_FREE:空闲页。被删除或迁移的行记录保存在一个单向链表中,PAGE_FREE指向该列表中的第一条记录。
- PAGE_DIRECTION与PAGE_N_DIRECTION:标示插入操作是否按照一定的递增顺序,该指标会影响InnoDB的效率。
- PAGE_BTR_SEG_LEAF与PAGE_BTR_SEG_TOP:包含索引节点的信息,用于分配新的页。
Infimum与Supremum记录
共26字节,分别代表可能索引值的最下界与最上界,在索引创建时InnoDB自动在根页生成这两条记录且不会被删除。用于确保"get-prev"与"get-next"不会超出上下界的范围。
用户记录(User Records)
存储所有用户插入的记录。
- Heap:无序列表,不按照B+树插入新数据,直接在最近的一条数据后插入新数据(Free Space的顶端),或是插入在已被删除行的空余位置。
- 按照B树的定义,行记录必须按照索引顺序排列,所以每一条行记录中有一个next字段指向索引顺序的下一条记录,即记录形成一条单向链表,InnoDB在定位行数据时按照索引顺序查找。
Free Space
页目录(Page Directory)
存储记录的指针,指针也称为「槽(slots)」或「目录槽(directory slots)」,与其他DBMS不同,InnoDB不会为每一条记录都生成一个单独的槽,而是保存一个稀疏的目录。在一个满记录的页中,一个槽对应6条记录。
槽会记录数据的逻辑顺序(索引顺序),且每个槽的大小固定,因此通过槽使用二分查找定位行记录效率很高。但由于页目录不为每一条记录生成一个槽,所以二分查找只能获取记录的大致位置而非精确位置,InnoDB找到大致位置后再使用next指针查找。
- 一条记录Extra Bytes部分中n_owned字段记录在当前槽中剩余的记录数。
文件页尾部信息(Fil Trailer)
共8字节,前4个字节为当前页的checksum;后4个字节与FIL_PAGE_LSN相同(当前页的最新日志记录的LSN)。用于确保log-recovery恢复后的一致性。
InnoDB行结构
参考
- 知数堂MySQL优化班:InnoDB存储引擎
5. 如何添加从库到一个无从库的繁忙主服务器?(待补充)
定位当前主服务器的性能瓶颈,一般在业务高峰期主服务器的性能瓶颈是I/O
- 使用主库的最新一次全备份构建新的MySQL实例作为从库;
- Clone Plugin?当前思路:使用网络资源而非IO资源。
6. 怎么去理解一个公司的数据库环境?
7. MySQL服务器负载太高,请说下排查诊断思路,并列举几种诊断I/O、CPU、性能瓶颈的方法及其相应优化机制?
性能指标
硬件及系统层指标
CPU
- %user
- %sys
- %idle
- %iowait
内存
- available
- cached
- swap
I/O
- iops
- 吞吐
- 时延
- 利用率(%util)
网卡
- 吞吐(重点关注小包收发效率)
MySQL层指标
- 主要是锁、索引引起的慢查询
- 其他情况,如来不及刷写脏页、redo太小、异常调用
监控工具
系统层监控工具
- top、free、ps、df
- sysstat(sar、mpstat、iostat)、dstat、iotop
- netstat、ethstatus、arping
MySQL层监控
性能
- tps
- qps
- response time
- 当前并发连接数
慢查询
- 平均耗时
- 平均次数
- 表扫描
- 访问页数
锁
- 表锁
- 行锁
- 锁等待
- 死锁
事务
- 是否有大事务、长事务
- unpaged list
内存
- ibp wait free
- youngs/s
- non-youngs/s
- ibp监控
临时表/临时文件
表缓存
线程
8. MySQL中MyISAM与InnoDB引擎的区别
MyISAM | InnoDB | |
---|---|---|
锁 | 表级锁 | 支持行级锁,更好的crash recovery |
业务场景 | OLAP场景下的静态表(主要操为读请求,少量写入操作) | 大量数据读写场景(OLTP)下的最优化性能 |
外键 | 不支持外键(DBMS) | 支持外键(RDBMS) |
事务 | 不支持事务 | 支持事务 |
存储 | 分别存储表结构、数据与索引为.frm、.myd、.myi文件 | 在表空间(tablespace)中存储表与索引 |
9. MySQL 5.6+ MRR与ICP原理
MRR(Multi-Range Read Optimization)
在二级索引上使用范围扫描查询一张未存储在缓存中的大表会导致大量的磁盘随机读,MySQL使用MRR机制降低该情况下的磁盘随机读从而提高查询效率。
- MRR单独扫描索引,获取符合查询条件的索引值;
- 对选取的索引值使用row ID排序,并使用二级索引对应的主键值回表获取行数据。
MRR使用场景与工作流程
MRR用于InnoDB与MyISAM表的索引范围扫描与equi-join操作:
- 累积保存一部分二级索引在buffer中;
- 使用row ID对buffer中的二级索引排序;
- 使用排序后的索引查询完整的数据行。
注
- 使用MRR时在 EXPLAIN 语句的输出的Extra列中会标注"Using MRR"
- 只有在整行数据会被查询的情况下,才可能使用MRR;若结果可以走覆盖索引,不会使用MRR。
- 在optimizer_switch中设置mrr=on打开MRR优化器,默认为on
- MRR不支持创建在虚拟列上的二级索引
参考:
- https://dev.mysql.com/doc/refman/8.0/en/mrr-optimization.html
ICP(Index Condition Pushdown)
没有ICP的情况下,存储引擎遍历索引定位基表中的数据行后,将每一条数据返回server层再使用WHERE条件进行过滤。即没有ICP时的索引扫描流程为:
- 首先读取下一行的索引,再使用索引确定并读取完整的行数据;
- 对数据行进行WHERE条件判断
在ICP优化器开启的条件下,若部分WHERE条件可以通过索引列过滤,则MySQL会将这部分的WHERE条件向下推送到存储引擎层,存储引擎层使用索引过滤出满足条件的行,再返回server层做进一步处理。即ICP优化器条件下的索引扫描流程为:
- 获取下一行的索引,并不回表读取完整的行数据;
- 测试(部分)WHERE条件能否应用于该二级索引,若无法匹配,直接读取下一行的索引;
- 若WHERE条件满足,使用二级索引回表读取完整的数据行;
- 测试剩下的WHERE能否匹配完整的数据行,并根据结果决定是否返回数据行。
ICP能够减少引擎层访问基表的次数和server层访问存储引擎的次数。
限制条件与问题
- 使用ICP时在 EXPLAIN 语句的输出的Extra列中会标注"Using index condition"
- 支持需要访问完整行数据的range、ref、eq_ref以及ref_or_null数据访问方式
- InnoDB表仅支持二级索引的ICP;对于InnoDB聚集索引,完整的行记录已经读入InnoDB Buffer中,这种情况下使用ICP不会降低I/O
- ICP同样不支持创建于虚拟列上的二级索引
- 不支持子查询
参考
- http://mysql.taobao.org/monthly/2015/12/08/
- https://dev.mysql.com/doc/refman/8.0/en/index-condition-pushdown-optimization.html
10. MySQL中VARCHAR与CHAR的区别以及VARCHAR(N)中的N代表的涵义
CHAR列长度固定,值范围为0-255(字符)。若存储的值长度未达到声明的字符长度,会向右补空字符。CHAR列被检索时,尾部的空字符会被移除。
VARCHAR为变长字符串,长度范围为0-65535(字节),受限于最大的行大小(65535字节,与其他列共享)与设定的字符集。VARCHAR需要额外存储1-2字节长度的前缀数据,该数据标示了VARCHAR的字节数。当列长不超过255字节时,前缀数据占额外的1字节;列长超过255字节时,需要额外的2字节标注。
若strict SQL模式未打开,将一个超过CHAR或VARCHAR列最大长度的数据插入到CHAR或VARCHAR列时,超出的长度将会被截断并生成一条告警信息。
对于插入的VARCHAR列,不论strict SQL模式是否打开,尾部超出列长的部分都会在插入前被截断并生成告警信息;对于CHAR列,超出长度的尾部字段会被自动截断不生成告警信息。
CHAR、VARCHAR、TEXT列根据列指定的排序方式(collation)进行排序,排序方式中有一个PAD_ATTRIBUTE属性,决定了对字符串尾部空格的处理方式。
- NO_PAD:将尾部的每一个空格视为一个字符;
- PAD SPACE:字符串在对比时忽略尾部的空格,若将'ab '与'ab'同时插入一个unqiue索引,会报错。
以下表格根据相同的插入值对应的实际存储值标示CHAR(4)与VARCHAR(4)的区别(使用latin1字符集):
Value | CHAR(4) |
Storage Required | VARCHAR(4) |
Storage Required |
---|---|---|---|---|
'' |
' ' |
4 bytes | '' |
1 byte |
'ab' |
'ab ' |
4 bytes | 'ab' |
3 bytes |
'abcd' |
'abcd' |
4 bytes | 'abcd' |
5 bytes |
'abcdefgh' |
'abcd' |
4 bytes | 'abcd' |
5 bytes |