MySQL InnoDB存储引擎崩溃恢复原理介绍

前言

数据库系统与文件系统最大的区别在于数据库能保证操作的原子性,一个操作要么不做要么都做,即使在数据库宕机的情况下,也不会出现操作一半的情况,这个就需要数据库的日志和一套完善的崩溃恢复机制来保证。本文仔细剖析了InnoDB的崩溃恢复流程,代码基于5.6分支。

基础知识

  • lsn: 可以理解为数据库从创建以来产生的redo日志量,这个值越大,说明数据库的更新越多,也可以理解为更新的时刻。此外,每个数据页上也有一个lsn,表示最后被修改时的lsn,值越大表示越晚被修改。比如,数据页A的lsn为100,数据页B的lsn为200,checkpoint lsn为150,系统lsn为300,表示当前系统已经更新到300,小于150的数据页已经被刷到磁盘上,因此数据页A的最新数据一定在磁盘上,而数据页B则不一定,有可能还在内存中。

  • redo日志: 现代数据库都需要写redo日志,例如修改一条数据,首先写redo日志,然后再写数据。在写完redo日志后,就直接给客户端返回成功。这样虽然看过去多写了一次盘,但是由于把对磁盘的随机写入(写数据)转换成了顺序的写入(写redo日志),性能有很大幅度的提高。当数据库挂了之后,通过扫描redo日志,就能找出那些没有刷盘的数据页(在崩溃之前可能数据页仅仅在内存中修改了,但是还没来得及写盘),保证数据不丢。

  • undo日志: 数据库还提供类似撤销的功能,当你发现修改错一些数据时,可以使用rollback指令回滚之前的操作。这个功能需要undo日志来支持。此外,现代的关系型数据库为了提高并发(同一条记录,不同线程的读取不冲突,读写和写读不冲突,只有同时写才冲突),都实现了类似MVCC的机制,在InnoDB中,这个也依赖undo日志。为了实现统一的管理,与redo日志不同,undo日志在Buffer Pool中有对应的数据页,与普通的数据页一起管理,依据LRU规则也会被淘汰出内存,后续再从磁盘读取。与普通的数据页一样,对undo页的修改,也需要先写redo日志。

  • 检查点: 英文名为checkpoint。数据库为了提高性能,数据页在内存修改后并不是每次都会刷到磁盘上。checkpoint之前的数据页保证一定落盘了,这样之前的日志就没有用了(由于InnoDB redolog日志循环使用,这时这部分日志就可以被覆盖),checkpoint之后的数据页有可能落盘,也有可能没有落盘,所以checkpoint之后的日志在崩溃恢复的时候还是需要被使用的。InnoDB会依据脏页的刷新情况,定期推进checkpoint,从而减少数据库崩溃恢复的时间。检查点的信息在第一个日志文件的头部。

  • 崩溃恢复: 用户修改了数据,并且收到了成功的消息,然而对数据库来说,可能这个时候修改后的数据还没有落盘,如果这时候数据库挂了,重启后,数据库需要从日志中把这些修改后的数据给捞出来,重新写入磁盘,保证用户的数据不丢。这个从日志中捞数据的过程就是崩溃恢复的主要任务,也可以成为数据库前滚。当然,在崩溃恢复中还需要回滚没有提交的事务,提交没有提交成功的事务。由于回滚操作需要undo日志的支持,undo日志的完整性和可靠性需要redo日志来保证,所以崩溃恢复先做redo前滚,然后做undo回滚。

我们从源码角度仔细剖析一下数据库崩溃恢复过程。整个过程都在引擎初始化阶段完成(innobase_init),其中最主要的函数是innobase_start_or_create_for_mysql,innodb通过这个函数完成创建和初始化,包括崩溃恢复。首先来介绍一下数据库的前滚。

redo日志前滚数据库

前滚数据库,主要分为两阶段,首先是日志扫描阶段,扫描阶段按照数据页的space_id和page_no分发redo日志到hash_table中,保证同一个数据页的日志被分发到同一个哈希桶中,且按照lsn大小从小到大排序。扫描完后,再遍历整个哈希表,依次应用每个数据页的日志,应用完后,在数据页的状态上至少恢复到了崩溃之前的状态。我们来详细分析一下代码。

  • 首先,打开所有的ibdata文件(open_or_create_data_files)(ibdata可以有多个),每个ibdata文件有个flush_lsn在头部,计算出这些文件中的max_flush_lsn和min_flush_lsn,因为ibdata也有可能有数据没写完整,需要恢复,后续(recv_recovery_from_checkpoint_start_func)通过比较checkpont_lsn和这两个值来确定是否需要对ibdata前滚。

  • 接着,打开系统表空间和日志表空间的所有文件(fil_open_log_and_system_tablespace_files),防止出现文件句柄不足,清空buffer pool(buf_pool_invalidate)。接下来就进入最最核心的函数:recv_recovery_from_checkpoint_start_func,注意,即使数据库是正常关闭的,也会进入。

虽然recv_recovery_from_checkpoint_start_func看过去很冗长,但是很多代码都是为了LOG_ARCHIVE特性而编写的,真正数据崩溃恢复的代码其实不多。

  • 首先,初始化一些变量,查看srv_force_recovery这个变量,如果用户设置跳过前滚阶段,函数直接返回。

  • 接着,初始化recv_sys结构,分配hash_table的大小,同时初始化flush list rbtree。recv_sys结构主要在崩溃恢复前滚阶段使用。hash_table就是之前说的用来存不同数据页日志的哈希表,哈希表的大小被初始化为buffer_size_in_bytes/512, 这个是哈希表最大的长度,超过就存不下了,幸运的是,需要恢复的数据页的个数不会超过这个值,因为buffer poll最多(数据库崩溃之前脏页的上线)只能存放buffer_size_in_bytes/16KB个数据页,即使考虑压缩页,最多也只有buffer_size_in_bytes/1KB个,此外关于这个哈希表内存分配的大小,可以参考bug#53122。flush list rbtree这个主要是为了加入插入脏页列表,InnoDB的flush list必须按照数据页的最老修改lsn(oldest_modifcation)从小到大排序,在数据库正常运行时,可以通过log_sys->mutex和log_sys->log_flush_order_mutex保证顺序,在崩溃恢复则没有这种保证,应用数据的时候,是从第一个元素开始遍历哈希表,不能保证数据页按照最老修改lsn(oldest_modifcation)从小到大排序,这样就需要线性遍历flush_list来寻找插入位置,效率太低,因此引入红黑树,加快查找插入的位置。

  • 接着,从ib_logfile0的头中读取checkpoint信息,主要包括checkpoint_lsn和checkpoint_no。由于InnoDB日志是循环使用的,且最少要有2个,所以ib_logfile0一定存在,把checkpoint信息存在里面很安全,不用担心被删除。checkpoint信息其实会写在文件头的两个地方,两个checkpoint域轮流写。为什么要两个地方轮流写呢?假设只有一个checkpoint域,一直更新这个域,而checkpoint域有512字节(OS_FILE_LOG_BLOCK_SIZE),如果刚好在写这个512字节的时候,数据库挂了,服务器也挂了(先不考虑硬件的原子写特性,早期的硬件没有这个特性),这个512字节可能只写了一半,导致整个checkpoint域不可用。这样数据库将无法做崩溃恢复,从而无法启动。如果有两个checkpoint域,那么即使一个写坏了,还可以用另外一个尝试恢复,虽然有可能这个时候日志已经被覆盖,但是至少提高了恢复成功的概率。两个checkpoint域轮流写,也能减少磁盘扇区故障带来的影响。checkpoint_lsn之前的数据页都已经落盘,不需要前滚,之后的数据页可能还没落盘,需要重新恢复出来,即使已经落盘也没关系,因为redo日志时幂等的,应用一次和应用两次都一样(底层实现: 如果数据页上的lsn大于等于当前redo日志的lsn,就不应用,否则应用。

  • checkpoint_no可以理解为checkpoint域写盘的次数,每次刷盘递增1,同时这个值取模2可以用来实现checkpoint_no域的轮流写。正常逻辑下,选取checkpoint_no值大的作为最终的checkpoint信息,用来做后续崩溃恢复扫描的起始点。
    接着,使用checkpoint域的信息初始化recv_sys结构体的一些信息后,就进入日志解析的核心函数recv_group_scan_log_recs,这个函数后续我们再分析,主要作用就是解析redo日志,如果内存不够了,就直接调用应用(recv_apply_hashed_log_recs)日志,然后再接着解析。如果需要应用的日志很少,就仅仅解析分发日志,到recv_recovery_from_checkpoint_finish函数中在应用日志。

  • 接着,依据当前刷盘的数据页状态做一次checkpoint,因为在recv_group_scan_log_recs里可能已经应用部分日志了。至此recv_recovery_from_checkpoint_start_func函数结束。

  • 在recv_recovery_from_checkpoint_finish函数中,如果srv_force_recovery设置正确,就开始调用函数recv_apply_hashed_log_recs应用日志,然后等待刷脏的线程退出(线程是崩溃恢复时临时启动的),最后释放recv_sys的相关资源以及hash_table占用的内存。

  • 至此,数据库前滚结束。接下来,我们详细分析一下redo日志解析函数以及redo日志应用函数的实现细节。

redo日志解析函数

解析函数的最上层是recv_group_scan_log_recs,这个函数调用底层函数(log_group_read_log_seg),按照RECV_SCAN_SIZE(64KB)大小分批读取。读取出来后,首先通过block_no和lsn之间的关系以及日志checksum判断是否读到了日志最后(所以可以看出,并没一个标记在日志头标记日志的有效位置,完全是按照上述两个条件判断是否到达了日志尾部),如果读到最后则返回(之前说了,即使数据库是正常关闭的,也要走崩溃恢复逻辑,那么在这里就返回了,因为正常关闭的checkpoint值一定是指向日志最后),否则则把日志去头掐尾放到一个recv_sys->buf中,日志头里面存了一些控制信息和checksum值,只是用来校验和定位,在真正的应用中没有用。在放到recv_sys->buf之前,需要检验一下recv_sys->buf有没有满(RECV_PARSING_BUF_SIZE,2M),满了就报错(如果上一批解析有不完整的日志,日志解析函数不会分发,而是把这些不完整的日志留在recv_sys->buf中,直到解析到完整的日志)。接下的事情就是从recv_sys->buf中解析日志(recv_parse_log_recs)。日志分两种:single_rec和multi_rec,前者表示只对一个数据页进行一种操作,后者表示对一个或者多个数据页进行多种操作。日志中还包括对应数据页的space_id,page_no,操作的type以及操作的内容(recv_parse_log_rec)。解析出相应的日志后,按照space_id和page_no进行哈希(如果对应的表空间在内存中不存在,则表示表已经被删除了),放到hash_table里面(日志真正存放的位置依然在buffer pool)即可,等待后续应用。这里有几个点值得注意:

  • 如果是multi_rec类型,则只有遇到MLOG_MULTI_REC_END这个标记,日志才算完整,才会被分发到hash_table中。查看代码,我们可以发现multi_rec类型的日志被解析了两次,一次用来校验完整性(寻找MLOG_MULTI_REC_END),第二次才用来分发日志,感觉这是一个可以优化的点。

  • 目前日志的操作type有50多种,每种操作后面的内容都不一样,所以长度也不一样,目前日志的解析逻辑,需要依次解析出所有的内容,然后确定长度,从而定位下一条日志的开始位置。这种方法效率略低,其实可以在每种操作的头上加上一个字段,存储后面内容的长度,这样就不需要解析太多的内容,从而提高解析速度,进一步提高崩溃恢复速度,从结果看,可以提高一倍的速度(从38秒到14秒,详情可以参见bug#82937)。

  • 如果发现checkpoint之后还有日志,说明数据库之前没有正常关闭,需要做崩溃恢复,因此需要做一些额外的操作(recv_init_crash_recovery),比如在错误日志中打印我们常见的“Database was not shutdown normally!”和“Starting crash recovery.”,还要从double write buffer中检查是否发生了数据页半写,如果有需要恢复(buf_dblwr_process),还需要启动一个线程用来刷新应用日志产生的脏页(因为这个时候buf_flush_page_cleaner_thread还没有启动)。最后还需要打开所有的表空间。。注意是所有的表。。。我们在阿里云RDS MySQL的运维中,常常发现数据库hang在了崩溃恢复阶段,在错误日志中有类似“Reading tablespace information from the .ibd files…”字样,这就表示数据库正在打开所有的表,然后一看表的数量,发现有几十甚至上百万张表。。。数据库之所以要打开所有的表,是因为在分发日志的时候,需要确定space_id对应哪个ibd文件,通过打开所有的表,读取space_id信息来确定,另外一个原因是方便double write buffer检查半写数据页。针对这个表数量过多导致恢复过慢的问题,MySQL 5.7做了优化,WL#7142, 主要思想就是在每次checkpoint后,在第一次修改某个表时,先写一个新日志mlog_file_name(包括space_id和filename的映射),来表示对这个表进行了操作,后续对这个表的操作就不用写这个新日志了,当需要崩溃恢复时候,多一次扫描,通过搜集mlog_file_name来确定哪些表被修改过,这样就不需要打开所有的表来确定space_id了。

  • 最后一个值得注意的地方是内存。之前说过,如果有太多的日志已经被分发,占用了太多的内存,日志解析函数会在适当的时候应用日志,而不是等到最后才一起应用。那么问题来了,使用了多大的内存就会出发应用日志逻辑。答案是:buffer_pool_size_in_bytes – 512 * buffer_pool_instance_num * 16KB。由于buffer_pool_instance_num一般不会太大,所以可以任务,buffer pool的大部分内存都被用来存放日志。剩下的那些主要留给应用日志时读取的数据页,因为目前来说日志应用是单线程的,读取一个日志,把所有日志应用完,然后就可以刷回磁盘了,不需要太多的内存。

redo日志应用函数

应用日志的上层函数为recv_apply_hashed_log_recs(应用日志也可能在io_helper函数中进行),主要作用就是遍历hash_table,从磁盘读取对每个数据页,依次应用哈希桶中的日志。应用完所有的日志后,如果需要则把buffer_pool的页面都刷盘,毕竟空间有限。有以下几点值得注意:

  • 同一个数据页的日志必须按照lsn从小到大应用,否则数据会被覆盖。只应用redo日志lsn大于page_lsn的日志,只有这些日志需要重做,其余的忽略。应用完日志后,把脏页加入脏页列表,由于脏页列表是按照最老修改lsn(oldest_modification)来排序的,这里通过引入一颗红黑树来加速查找插入的位置,时间复杂度从之前的线性查找降为对数级别。

  • 当需要某个数据页的时候,如果发现其没有在Buffer Pool中,则会查看这个数据页周围32个数据页,是否也需要做恢复,如果需要则可以一起读取出来,相当于做了一次io合并,减少io操作(recv_read_in_area)。由于这个是异步读取,所以最终应用日志的活儿是由io_helper线程来做的(buf_page_io_complete),此外,为了防止短时间发起太多的io,在代码中加了流量控制的逻辑(buf_read_recv_pages)。如果发现某个数据页在内存中,则直接调用recv_recover_page应用日志。由此我们可以看出,InnoDB应用日志其实并不是单线程的来应用日志的,除了崩溃恢复的主线程外,io_helper线程也会参与恢复。并发线程数取决于io_helper中读取线程的个数。

执行完了redo前滚数据库,数据库的所有数据页已经处于一致的状态,undo回滚数据库就可以安全的执行了。数据库崩溃的时候可能有一些没有提交的事务或者已经提交的事务,这个时候就需要决定是否提交。主要分为三步,首先是扫描undo日志,重新建立起undo日志链表,接着是,依据上一步建立起的链表,重建崩溃前的事务,即恢复当时事务的状态。最后,就是依据事务的不同状态,进行回滚或者提交。

undo日志回滚数据库

在recv_recovery_from_checkpoint_start_func之后,recv_recovery_from_checkpoint_finish之前,调用了trx_sys_init_at_db_start,这个函数做了上述三步中的前两步。

  • 第一步在函数trx_rseg_array_init中处理,遍历整个undo日志空间(最多TRX_SYS_N_RSEGS(128)个segment),如果发现某个undo segment非空,就进行初始化(trx_rseg_create_instance)。整个每个undo segment,如果发现undo slot非空(最多TRX_RSEG_N_SLOTS(1024)个slot),也就行初始化(trx_undo_lists_init)。在初始化undo slot后,就把不同类型的undo日志放到不同链表中(trx_undo_mem_create_at_db_start)。undo日志主要分为两种:TRX_UNDO_INSERT和TRX_UNDO_UPDATE。前者主要是提供给insert操作用的,后者是给update和delete操作使用。之前说过,undo日志有两种作用,事务回滚时候用和MVCC快照读取时候用。由于insert的数据不需要提供给其他线程用,所以只要事务提交,就可以删除TRX_UNDO_INSERT类型的undo日志。TRX_UNDO_UPDATE在事务提交后还不能删除,需要保证没有快照使用它的时候,才能通过后台的purge线程清理。

  • 第二步在函数trx_lists_init_at_db_start中进行,由于第一步中,已经在内存中建立起了undo_insert_list和undo_update_list(链表每个undo segment独立),所以这一步只需要遍历所有链表,重建起事务的状态(trx_resurrect_insert和trx_resurrect_update)。简单的说,如果undo日志的状态是TRX_UNDO_ACTIVE,则事务的状态为TRX_ACTIVE,如果undo日志的状态是TRX_UNDO_PREPARED,则事务的状态为TRX_PREPARED。这里还要考虑变量srv_force_recovery的设置,如果这个变量值为非0,所有的事务都会回滚(即事务被设置为TRX_ACTIVE),即使事务的状态应该为TRX_STATE_PREPARED。重建起事务后,按照事务id加入到trx_sys->trx_list链表中。最后,在函数trx_sys_init_at_db_start中,会统计所有需要回滚的事务(事务状态为TRX_ACTIVE)一共需要回滚多少行数据,输出到错误日志中,类似:5 transaction(s) which must be rolled back or cleaned up。InnoDB: in total 342232 row operations to undo的字样。

  • 第三步的操作在两个地方被调用。一个是在recv_recovery_from_checkpoint_finish的最后,另外一个是在recv_recovery_rollback_active中。前者主要是回滚对数据字典的操作,也就是回滚DDL语句的操作,后者是回滚DML语句。前者是在数据库可提供服务之前必须完成,后者则可以在数据库提供服务(也即是崩溃恢复结束)之后继续进行(通过新开一个后台线程trx_rollback_or_clean_all_recovered来处理)。因为InnoDB认为数据字典是最重要的,必须要回滚到一致的状态才行,而用户表的数据可以稍微慢一点,对外提供服务后,慢慢恢复即可。因此我们常常在会发现数据库已经启动起来了,然后错误日志中还在不断的打印回滚事务的信息。事务回滚的核心函数是trx_rollback_or_clean_recovered,逻辑很简单,只需要遍历trx_sys->trx_list,按照事务不同的状态回滚或者提交即可(trx_rollback_resurrected)。这里要注意的是,如果事务是TRX_STATE_PREPARED状态,那么在InnoDB层,不做处理,需要在Server层依据binlog的情况来决定是否回滚事务,如果binlog已经写了,事务就提交,因为binlog写了就可能被传到备库,如果主库回滚会导致主备数据不一致,如果binlog没有写,就回滚事务。

崩溃恢复相关参数解析

  • innodb_fast_shutdown:
    innodb_fast_shutdown = 0。这个表示在MySQL关闭的时候,执行slow shutdown,不但包括日志的刷盘,数据页的刷盘,还包括数据的清理(purge),ibuf的合并,buffer pool dump以及lazy table drop操作(如果表上有未完成的操作,即使执行了drop table且返回成功了,表也不一定立刻被删除)。
    innodb_fast_shutdown = 1。这个是默认值,表示在MySQL关闭的时候,仅仅把日志和数据刷盘。
    innodb_fast_shutdown = 2。这个表示关闭的时候,仅仅日志刷盘,其他什么都不做,就好像MySQL crash了一样。
    这个参数值越大,MySQL关闭的速度越快,但是启动速度越慢,相当于把关闭时候需要做的工作挪到了崩溃恢复上。另外,如果MySQL要升级,建议使用第一种方式进行一次干净的shutdown。

  • innodb_force_recovery:
    这个参数主要用来控制InnoDB启动时候做哪些工作,数值越大,做的工作越少,启动也更加容易,但是数据不一致的风险也越大。当MySQL因为某些不可控的原因不能启动时,可以设置这个参数,从1开始逐步递增,知道MySQL启动,然后使用SELECT INTO OUTFILE把数据导出,尽最大的努力减少数据丢失。
    innodb_force_recovery = 0。这个是默认的参数,启动的时候会做所有的事情,包括redo日志应用,undo日志回滚,启动后台master和purge线程,ibuf合并。检测到了数据页损坏了,如果是系统表空间的,则会crash,用户表空间的,则打错误日志。
    innodb_force_recovery = 1。如果检测到数据页损坏了,不会crash也不会报错(buf_page_io_complete),启动的时候也不会校验表空间第一个数据页的正确性(fil_check_first_page),表空间无法访问也继续做崩溃恢复(fil_open_single_table_tablespace、fil_load_single_table_tablespace),ddl操作不能进行(check_if_supported_inplace_alter),同时数据库也被不能进行写入操作(row_insert_for_mysql、row_update_for_mysql等),所有的prepare事务也会被回滚(trx_resurrect_insert、trx_resurrect_update_in_prepared_state)。这个选项还是很常用的,数据页可能是因为磁盘坏了而损坏了,设置为1,能保证数据库正常启动。
    innodb_force_recovery = 2。除了设置1之后的操作不会运行,后台的master和purge线程就不会启动了(srv_master_thread、srv_purge_coordinator_thread等),当你发现数据库因为这两个线程的原因而无法启动时,可以设置。
    innodb_force_recovery = 3。除了设置2之后的操作不会运行,undo回滚数据库也不会进行,但是回滚段依然会被扫描,undo链表也依然会被创建(trx_sys_init_at_db_start)。srv_read_only_mode会被打开。
    innodb_force_recovery = 4。除了设置3之后的操作不会运行,ibuf的操作也不会运行(ibuf_merge_or_delete_for_page),表信息统计的线程也不会运行(因为一个坏的索引页会导致数据库崩溃)(info_low、dict_stats_update等)。从这个选项开始,之后的所有选项,都会损坏数据,慎重使用。
    innodb_force_recovery = 5。除了设置4之后的操作不会运行,回滚段也不会被扫描(recv_recovery_rollback_active),undo链表也不会被创建,这个主要用在undo日志被写坏的情况下。
    innodb_force_recovery = 6。除了设置5之后的操作不会运行,数据库前滚操作也不会进行,包括解析和应用(recv_recovery_from_checkpoint_start_func)。

『浅入浅出』MySQL及InnoDB存储引擎

作为一名开发人员,在日常的工作中会难以避免地接触到数据库,无论是基于文件的 sqlite 还是工程上使用非常广泛的 MySQL、PostgreSQL,但是一直以来也没有对数据库有一个非常清晰并且成体系的认知,所以最近两个月的时间看了几本数据库相关的书籍并且阅读了 MySQL 的官方文档,希望对各位了解数据库的、不了解数据库的有所帮助。

未分类

本文中对于数据库的介绍以及研究都是在 MySQL 上进行的,如果涉及到了其他数据库的内容或者实现会在文中单独指出。

数据库的定义

很多开发者在最开始时其实都对数据库有一个比较模糊的认识,觉得数据库就是一堆数据的集合,但是实际却比这复杂的多,数据库领域中有两个词非常容易混淆,也就是数据库和实例:

  • 数据库:物理操作文件系统或其他形式文件类型的集合;
  • 实例:MySQL 数据库由后台线程以及一个共享内存区组成;

对于数据库和实例的定义都来自于 MySQL 技术内幕:InnoDB 存储引擎(https://book.douban.com/subject/24708143/) 一书,想要了解 InnoDB 存储引擎的读者可以阅读这本书籍。

数据库和实例

在 MySQL 中,实例和数据库往往都是一一对应的,而我们也无法直接操作数据库,而是要通过数据库实例来操作数据库文件,可以理解为数据库实例是数据库为上层提供的一个专门用于操作的接口。

未分类

在 Unix 上,启动一个 MySQL 实例往往会产生两个进程,mysqld 就是真正的数据库服务守护进程,而 mysqld_safe 是一个用于检查和设置 mysqld 启动的控制程序,它负责监控 MySQL 进程的执行,当 mysqld 发生错误时,mysqld_safe 会对其状态进行检查并在合适的条件下重启。

MySQL 的架构

MySQL 从第一个版本发布到现在已经有了 20 多年的历史,在这么多年的发展和演变中,整个应用的体系结构变得越来越复杂:

未分类

最上层用于连接、线程处理的部分并不是 MySQL 『发明』的,很多服务都有类似的组成部分;第二层中包含了大多数 MySQL 的核心服务,包括了对 SQL 的解析、分析、优化和缓存等功能,存储过程、触发器和视图都是在这里实现的;而第三层就是 MySQL 中真正负责数据的存储和提取的存储引擎,例如:InnoDB、MyISAM 等,文中对存储引擎的介绍都是对 InnoDB 实现的分析。

数据的存储

在整个数据库体系结构中,我们可以使用不同的存储引擎来存储数据,而绝大多数存储引擎都以二进制的形式存储数据;这一节会介绍 InnoDB 中对数据是如何存储的。

在 InnoDB 存储引擎中,所有的数据都被逻辑地存放在表空间中,表空间(tablespace)是存储引擎中最高的存储逻辑单位,在表空间的下面又包括段(segment)、区(extent)、页(page):

未分类

同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 innodb_page_size 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同:

未分类

从图中可以看出,在 InnoDB 存储引擎中,一个区的大小最小为 1MB,页的数量最少为 64 个。

如何存储表

MySQL 使用 InnoDB 存储表时,会将表的定义和数据索引等信息分开存储,其中前者存储在 .frm 文件中,后者存储在 .ibd 文件中,这一节就会对这两种不同的文件分别进行介绍。

未分类

.frm 文件

无论在 MySQL 中选择了哪个存储引擎,所有的 MySQL 表都会在硬盘上创建一个 .frm 文件用来描述表的格式或者说定义;.frm 文件的格式在不同的平台上都是相同的。

CREATE TABLE test_frm(
    column1 CHAR(5),
    column2 INTEGER
);

当我们使用上面的代码创建表时,会在磁盘上的 datadir 文件夹中生成一个 test_frm.frm 的文件,这个文件中就包含了表结构相关的信息:

未分类

MySQL 官方文档中的 11.1 MySQL .frm File Format(https://dev.mysql.com/doc/internals/en/frm-file-format.html) 一文对于 .frm 文件格式中的二进制的内容有着非常详细的表述,在这里就不展开介绍了。

.ibd 文件

InnoDB 中用于存储数据的文件总共有两个部分,一是系统表空间文件,包括 ibdata1、ibdata2 等文件,其中存储了 InnoDB 系统信息和用户数据库表数据和索引,是所有表公用的。

当打开 innodb_file_per_table 选项时,.ibd 文件就是每一个表独有的表空间,文件存储了当前表的数据和相关的索引数据。

如何存储记录

与现有的大多数存储引擎一样,InnoDB 使用页作为磁盘管理的最小单位;数据在 InnoDB 存储引擎中都是按行存储的,每个 16KB 大小的页中可以存放 2-200 行的记录。

当 InnoDB 存储数据时,它可以使用不同的行格式进行存储;MySQL 5.7 版本支持以下格式的行存储方式:
未分类

Antelope 是 InnoDB 最开始支持的文件格式,它包含两种行格式 Compact 和 Redundant,它最开始并没有名字;Antelope 的名字是在新的文件格式 Barracuda 出现后才起的,Barracuda 的出现引入了两种新的行格式 Compressed 和 Dynamic;InnoDB 对于文件格式都会向前兼容,而官方文档中也对之后会出现的新文件格式预先定义好了名字:Cheetah、Dragon、Elk 等等。

两种行记录格式 Compact 和 Redundant 在磁盘上按照以下方式存储:

未分类

Compact 和 Redundant 格式最大的不同就是记录格式的第一个部分;在 Compact 中,行记录的第一部分倒序存放了一行数据中列的长度(Length),而 Redundant 中存的是每一列的偏移量(Offset),从总体上上看,Compact 行记录格式相比 Redundant 格式能够减少 20% 的存储空间。

行溢出数据

当 InnoDB 使用 Compact 或者 Redundant 格式存储极长的 VARCHAR 或者 BLOB 这类大对象时,我们并不会直接将所有的内容都存放在数据页节点中,而是将行数据中的前 768 个字节存储在数据页中,后面会通过偏移量指向溢出页。

未分类

但是当我们使用新的行记录格式 Compressed 或者 Dynamic 时都只会在行记录中保存 20 个字节的指针,实际的数据都会存放在溢出页面中。

未分类

当然在实际存储中,可能会对不同长度的 TEXT 和 BLOB 列进行优化,不过这就不是本文关注的重点了。

想要了解更多与 InnoDB 存储引擎中记录的数据格式的相关信息,可以阅读 InnoDB Record Structure(https://dev.mysql.com/doc/internals/en/innodb-record-structure.html)

数据页结构

页是 InnoDB 存储引擎管理数据的最小磁盘单位,而 B-Tree 节点就是实际存放表中数据的页面,我们在这里将要介绍页是如何组织和存储记录的;首先,一个 InnoDB 页有以下七个部分:

未分类

每一个页中包含了两对 header/trailer:内部的 Page Header/Page Directory 关心的是页的状态信息,而 Fil Header/Fil Trailer 关心的是记录页的头信息。

在页的头部和尾部之间就是用户记录和空闲空间了,每一个数据页中都包含 Infimum 和 Supremum 这两个虚拟的记录(可以理解为占位符),Infimum 记录是比该页中任何主键值都要小的值,Supremum 是该页中的最大值:

未分类

User Records 就是整个页面中真正用于存放行记录的部分,而 Free Space 就是空余空间了,它是一个链表的数据结构,为了保证插入和删除的效率,整个页面并不会按照主键顺序对所有记录进行排序,它会自动从左侧向右寻找空白节点进行插入,行记录在物理存储上并不是按照顺序的,它们之间的顺序是由 next_record 这一指针控制的。

B+ 树在查找对应的记录时,并不会直接从树中找出对应的行记录,它只能获取记录所在的页,将整个页加载到内存中,再通过 Page Directory 中存储的稀疏索引和 n_owned、next_record 属性取出对应的记录,不过因为这一操作是在内存中进行的,所以通常会忽略这部分查找的耗时。

InnoDB 存储引擎中对数据的存储是一个非常复杂的话题,这一节中也只是对表、行记录以及页面的存储进行一定的分析和介绍,虽然作者相信这部分知识对于大部分开发者已经足够了,但是想要真正消化这部分内容还需要很多的努力和实践。

索引

索引是数据库中非常非常重要的概念,它是存储引擎能够快速定位记录的秘密武器,对于提升数据库的性能、减轻数据库服务器的负担有着非常重要的作用;索引优化是对查询性能优化的最有效手段,它能够轻松地将查询的性能提高几个数量级。

索引的数据结构

在上一节中,我们谈了行记录的存储和页的存储,在这里我们就要从更高的层面看 InnoDB 中对于数据是如何存储的;InnoDB 存储引擎在绝大多数情况下使用 B+ 树建立索引,这是关系型数据库中查找最为常用和有效的索引,但是 B+ 树索引并不能找到一个给定键对应的具体值,它只能找到数据行对应的页,然后正如上一节所提到的,数据库把整个页读入到内存中,并在内存中查找具体的数据行。

未分类

B+ 树是平衡树,它查找任意节点所耗费的时间都是完全相同的,比较的次数就是 B+ 树的高度;在这里,我们并不会深入分析或者动手实现一个 B+ 树,只是对它的特性进行简单的介绍。

聚集索引和辅助索引

数据库中的 B+ 树索引可以分为聚集索引(clustered index)和辅助索引(secondary index),它们之间的最大区别就是,聚集索引中存放着一条行记录的全部信息,而辅助索引中只包含索引列和一个用于查找对应行记录的『书签』。

  • 聚集索引

InnoDB 存储引擎中的表都是使用索引组织的,也就是按照键的顺序存放;聚集索引就是按照表中主键的顺序构建一颗 B+ 树,并在叶节点中存放表中的行记录数据。

CREATE TABLE users(
    id INT NOT NULL,
    first_name VARCHAR(20) NOT NULL,
    last_name VARCHAR(20) NOT NULL,
    age INT NOT NULL,
    PRIMARY KEY(id),
    KEY(last_name, first_name, age)
    KEY(first_name)
);

如果使用上面的 SQL 在数据库中创建一张表,B+ 树就会使用 id 作为索引的键,并在叶子节点中存储一条记录中的所有信息。

未分类

图中对 B+ 树的描述与真实情况下 B+ 树中的数据结构有一些差别,不过这里想要表达的主要意思是:聚集索引叶节点中保存的是整条行记录,而不是其中的一部分。

聚集索引与表的物理存储方式有着非常密切的关系,所有正常的表应该有且仅有一个聚集索引(绝大多数情况下都是主键),表中的所有行记录数据都是按照聚集索引的顺序存放的。

当我们使用聚集索引对表中的数据进行检索时,可以直接获得聚集索引所对应的整条行记录数据所在的页,不需要进行第二次操作。

  • 辅助索引

数据库将所有的非聚集索引都划分为辅助索引,但是这个概念对我们理解辅助索引并没有什么帮助;辅助索引也是通过 B+ 树实现的,但是它的叶节点并不包含行记录的全部数据,仅包含索引中的所有键和一个用于查找对应行记录的『书签』,在 InnoDB 中这个书签就是当前记录的主键。

辅助索引的存在并不会影响聚集索引,因为聚集索引构成的 B+ 树是数据实际存储的形式,而辅助索引只用于加速数据的查找,所以一张表上往往有多个辅助索引以此来提升数据库的性能。

一张表一定包含一个聚集索引构成的 B+ 树以及若干辅助索引的构成的 B+ 树。

未分类

如果在表 users 中存在一个辅助索引 (first_name, age),那么它构成的 B+ 树大致就是上图这样,按照 (first_name, age) 的字母顺序对表中的数据进行排序,当查找到主键时,再通过聚集索引获取到整条行记录。

未分类

上图展示了一个使用辅助索引查找一条表记录的过程:通过辅助索引查找到对应的主键,最后在聚集索引中使用主键获取对应的行记录,这也是通常情况下行记录的查找方式。

索引的设计

索引的设计其实是一个非常重要的内容,同时也是一个非常复杂的内容;索引的设计与创建对于提升数据库的查询性能至关重要,不过这不是本文想要介绍的内容,有关索引的设计与优化可以阅读 数据库索引设计与优化 一书,书中提供了一种非常科学合理的方法能够帮助我们在数据库中建立最适合的索引,当然作者也可能会在之后的文章中对索引的设计进行简单的介绍和分析。

我们都知道锁的种类一般分为乐观锁和悲观锁两种,InnoDB 存储引擎中使用的就是悲观锁,而按照锁的粒度划分,也可以分成行锁和表锁。

并发控制机制

乐观锁和悲观锁其实都是并发控制的机制,同时它们在原理上就有着本质的差别;

  • 乐观锁是一种思想,它其实并不是一种真正的『锁』,它会先尝试对资源进行修改,在写回时判断资源是否进行了改变,如果没有发生改变就会写回,否则就会进行重试,在整个的执行过程中其实都没有对数据库进行加锁;
  • 悲观锁就是一种真正的锁了,它会在获取资源前对资源进行加锁,确保同一时刻只有有限的线程能够访问该资源,其他想要尝试获取资源的操作都会进入等待状态,直到该线程完成了对资源的操作并且释放了锁后,其他线程才能重新操作资源;

虽然乐观锁和悲观锁在本质上并不是同一种东西,一个是一种思想,另一个是一种真正的锁,但是它们都是一种并发控制机制。

未分类

乐观锁不会存在死锁的问题,但是由于更新后验证,所以当冲突频率和重试成本较高时更推荐使用悲观锁,而需要非常高的响应速度并且并发量非常大的时候使用乐观锁就能较好的解决问题,在这时使用悲观锁就可能出现严重的性能问题;在选择并发控制机制时,需要综合考虑上面的四个方面(冲突频率、重试成本、响应速度和并发量)进行选择。

锁的种类

对数据的操作其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操作使用不同的锁;InnoDB 实现了标准的行级锁,也就是共享锁(Shared Lock)和互斥锁(Exclusive Lock);共享锁和互斥锁的作用其实非常好理解:

  • 共享锁(读锁):允许事务对一条行数据进行读取;
  • 互斥锁(写锁):允许事务对一条行数据进行删除或更新;

而它们的名字也暗示着各自的另外一个特性,共享锁之间是兼容的,而互斥锁与其他任意锁都不兼容:

未分类

稍微对它们的使用进行思考就能想明白它们为什么要这么设计,因为共享锁代表了读操作、互斥锁代表了写操作,所以我们可以在数据库中并行读,但是只能串行写,只有这样才能保证不会发生线程竞争,实现线程安全。

锁的粒度

无论是共享锁还是互斥锁其实都只是对某一个数据行进行加锁,InnoDB 支持多种粒度的锁,也就是行锁和表锁;为了支持多粒度锁定,InnoDB 存储引擎引入了意向锁(Intention Lock),意向锁就是一种表级锁。

与上一节中提到的两种锁的种类相似的是,意向锁也分为两种:

  • 意向共享锁:事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁;
  • 意向互斥锁:事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁;

随着意向锁的加入,锁类型之间的兼容矩阵也变得愈加复杂:

未分类

意向锁其实不会阻塞全表扫描之外的任何请求,它们的主要目的是为了表示是否有人请求锁定表中的某一行数据。

有的人可能会对意向锁的目的并不是完全的理解,我们在这里可以举一个例子:如果没有意向锁,当已经有人使用行锁对表中的某一行进行修改时,如果另外一个请求要对全表进行修改,那么就需要对所有的行是否被锁定进行扫描,在这种情况下,效率是非常低的;不过,在引入意向锁之后,当有人使用行锁对表中的某一行进行修改之前,会先为表添加意向互斥锁(IX),再为行记录添加互斥锁(X),在这时如果有人尝试对全表进行修改就不需要判断表中的每一行数据是否被加锁了,只需要通过等待意向互斥锁被释放就可以了。

锁的算法

到目前为止已经对 InnoDB 中锁的粒度有一定的了解,也清楚了在对数据库进行读写时会获取不同的锁,在这一小节将介绍锁是如何添加到对应的数据行上的,我们会分别介绍三种锁的算法:Record Lock、Gap Lock 和 Next-Key Lock。

  • Record Lock

记录锁(Record Lock)是加到索引记录上的锁,假设我们存在下面的一张表 users:

CREATE TABLE users(
    id INT NOT NULL AUTO_INCREMENT,
    last_name VARCHAR(255) NOT NULL,
    first_name VARCHAR(255),
    age INT,
    PRIMARY KEY(id),
    KEY(last_name),
    KEY(age)
);

如果我们使用 id 或者 last_name 作为 SQL 中 WHERE 语句的过滤条件,那么 InnoDB 就可以通过索引建立的 B+ 树找到行记录并添加索引,但是如果使用 first_name 作为过滤条件时,由于 InnoDB 不知道待修改的记录具体存放的位置,也无法对将要修改哪条记录提前做出判断就会锁定整个表。

  • Gap Lock

记录锁是在存储引擎中最为常见的锁,除了记录锁之外,InnoDB 中还存在间隙锁(Gap Lock),间隙锁是对索引记录中的一段连续区域的锁;当使用类似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE; 的 SQL 语句时,就会阻止其他事务向表中插入 id = 15 的记录,因为整个范围都被间隙锁锁定了。

间隙锁是存储引擎对于性能和并发做出的权衡,并且只用于某些事务隔离级别。

虽然间隙锁中也分为共享锁和互斥锁,不过它们之间并不是互斥的,也就是不同的事务可以同时持有一段相同范围的共享锁和互斥锁,它唯一阻止的就是其他事务向这个范围中添加新的记录。

  • Next-Key Lock

Next-Key 锁相比前两者就稍微有一些复杂,它是记录锁和记录前的间隙锁的结合,在 users 表中有以下记录:

+------+-------------+--------------+-------+
|   id | last_name   | first_name   |   age |
|------+-------------+--------------+-------|
|    4 | stark       | tony         |    21 |
|    1 | tom         | hiddleston   |    30 |
|    3 | morgan      | freeman      |    40 |
|    5 | jeff        | dean         |    50 |
|    2 | donald      | trump        |    80 |
+------+-------------+--------------+-------+

如果使用 Next-Key 锁,那么 Next-Key 锁就可以在需要的时候锁定以下的范围:

(-∞, 21]
(21, 30]
(30, 40]
(40, 50]
(50, 80]
(80, ∞)

既然叫 Next-Key 锁,锁定的应该是当前值和后面的范围,但是实际上却不是,Next-Key 锁锁定的是当前值和前面的范围。

当我们更新一条记录,比如 SELECT * FROM users WHERE age = 30 FOR UPDATE;,InnoDB 不仅会在范围 (21, 30] 上加 Next-Key 锁,还会在这条记录后面的范围 (30, 40] 加间隙锁,所以插入 (21, 40] 范围内的记录都会被锁定。

Next-Key 锁的作用其实是为了解决幻读的问题,我们会在下一节谈事务的时候具体介绍。

死锁的发生

既然 InnoDB 中实现的锁是悲观的,那么不同事务之间就可能会互相等待对方释放锁造成死锁,最终导致事务发生错误;想要在 MySQL 中制造死锁的问题其实非常容易:

未分类

两个会话都持有一个锁,并且尝试获取对方的锁时就会发生死锁,不过 MySQL 也能在发生死锁时及时发现问题,并保证其中的一个事务能够正常工作,这对我们来说也是一个好消息。

事务与隔离级别

在介绍了锁之后,我们再来谈谈数据库中一个非常重要的概念 —— 事务;相信只要是一个合格的软件工程师就对事务的特性有所了解,其中被人经常提起的就是事务的原子性,在数据提交工作时,要么保证所有的修改都能够提交,要么就所有的修改全部回滚。

但是事务还遵循包括原子性在内的 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability);文章不会对这四大特性全部展开进行介绍,相信你能够通过 Google 和数据库相关的书籍轻松获得有关它们的概念,本文最后要介绍的就是事务的四种隔离级别。

几种隔离级别

事务的隔离性是数据库处理数据的几大基础之一,而隔离级别其实就是提供给用户用于在性能和可靠性做出选择和权衡的配置项。

ISO 和 ANIS SQL 标准制定了四种事务隔离级别,而 InnoDB 遵循了 SQL:1992 标准中的四种隔离级别:READ UNCOMMITED、READ COMMITED、REPEATABLE READ 和 SERIALIZABLE;每个事务的隔离级别其实都比上一级多解决了一个问题:

  • RAED UNCOMMITED:使用查询语句不会加锁,可能会读到未提交的行(Dirty Read);
  • READ COMMITED:只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read);
  • REPEATABLE READ:多次读取同一范围的数据会返回第一次查询的快照,不会返回不同的数据行,但是可能发生幻读(Phantom Read);
  • SERIALIZABLE:InnoDB 隐式地将全部的查询语句加上共享锁,解决了幻读的问题;

MySQL 中默认的事务隔离级别就是 REPEATABLE READ,但是它通过 Next-Key 锁也能够在某种程度上解决幻读的问题。

未分类

接下来,我们将数据库中创建如下的表并通过个例子来展示在不同的事务隔离级别之下,会发生什么样的问题:

CREATE TABLE test(
    id INT NOT NULL,
    UNIQUE(id)
);

脏读

当事务的隔离级别为 READ UNCOMMITED 时,我们在 SESSION 2 中插入的未提交数据在 SESSION 1 中是可以访问的。

未分类

不可重复读

当事务的隔离级别为 READ COMMITED 时,虽然解决了脏读的问题,但是如果在 SESSION 1 先查询了一个范围的数据,在这之后 SESSION 2 中插入一条数据并且提交了修改,在这时,如果 SESSION 1 中再次使用相同的查询语句,就会发现两次查询的结果不一样。

未分类

不可重复读的原因就是,在 READ COMMITED 的隔离级别下,存储引擎不会在查询记录时添加间隙锁,锁定 id < 5 这个范围。

幻读

重新开启了两个会话 SESSION 1 和 SESSION 2,在 SESSION 1 中我们查询全表的信息,没有得到任何记录;在 SESSION 2 中向表中插入一条数据并提交;由于 REPEATABLE READ 的原因,再次查询全表的数据时,我们获得到的仍然是空集,但是在向表中插入同样的数据却出现了错误。

未分类

这种现象在数据库中就被称作幻读,虽然我们使用查询语句得到了一个空的集合,但是插入数据时却得到了错误,好像之前的查询是幻觉一样。

在标准的事务隔离级别中,幻读是由更高的隔离级别 SERIALIZABLE 解决的,但是它也可以通过 MySQL 提供的 Next-Key 锁解决:

未分类

REPERATABLE READ 和 READ UNCOMMITED 其实是矛盾的,如果保证了前者就看不到已经提交的事务,如果保证了后者,就会导致两次查询的结果不同,MySQL 为我们提供了一种折中的方式,能够在 REPERATABLE READ 模式下加锁访问已经提交的数据,其本身并不能解决幻读的问题,而是通过文章前面提到的 Next-Key 锁来解决。

总结

文章中的内容大都来自于 高性能 MySQL、MySQL 技术内幕:InnoDB 存储引擎、数据库索引设计与优化 以及 MySQL 的 官方文档。

python3.6使用sqlalchemy读取mysql中的数据并进行多进程并发处理

1. 介绍 SQLALChemy

SQLALChemy 是一个 python 的 ORM(Object Relational Mapper) 框架,开发人员可以快速开发操作数据库的程序,
它提供完整的数据库访问层,提供高性能的数据库访问能力。
它支持 SQLite、MySQL、Postgres、Oracle 等常用的数据库访问

2. 安装 SQLAlChemy

pip install sqlalchemy

2.1 创建测试数据库

# 建立数据库
CREATE DATABASE `test` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

2.2 用 SQLALChemy 创建数据库表

2.2.1 程序关键点

  • 创建操作数据库的 engine,使用 pymysql 库访问 mysql 数据库
  • 创建操作数据库的 session,绑定到 engine 上
  • 从 Base 继承定义 User,Article 类,对应 mapping 到数据库的 member,article 表
  • 使用 session.create_all 创建数据库表结构
  • session.add_all 新增数据到数据库
  • session.commit 提交所有变更到数据库,此时可以再数据库中查询插入的数据
  • 查询数据使用 session.query 方法,也可以在后面连接使用 filter 进行条件过滤
#!/usr/bin/env python
# coding: utf-8


from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker


# 创建数据库 engine 使用 utf8 编码
eng = create_engine('mysql+pymysql://root:1@localhost:3306/test?charset=utf8')
Base = declarative_base()

# 创建 session 类,绑定engine
Session = sessionmaker(bind=eng)
session = Session()


class User(Base):
    '''
    用户类,对应数据库的 member 表
    '''
    __tablename__ = 'member'

    # 定义表字段
    mid = Column(Integer, primary_key=True)
    nickname = Column(String(50))
    email = Column(String(128))

    def __repl__(self):
        return '<User(name={}, email={}, nickname{}>'.format(mid,
                                                             email,
                                                             nickname)


class Article(Base):
    '''
    文章类,对应数据库中的 article 表
    '''
    __tablename__ = 'article'

    # 定义表字段
    arid = Column(Integer, primary_key=True)
    tags = Column(String(128))
    description = Column(String(256))
    title = Column(String(256))


def create_table():
    '''
    创建数据库表结构,导入初始数据
    '''
    # 创建表
    Base.metadata.create_all(eng)

    # 插入数据
    session.add_all([
        User(mid=1, nickname='测试数据 test hello', email='[email protected]'),
        User(mid=2, nickname='测试数据 china hello', email='[email protected]'),
        User(mid=3, nickname='测试数据 上海 hello', email='[email protected]'),
        User(mid=4, nickname='测试数据 北京 hello', email='[email protected]'),
        User(mid=5, nickname='测试数据 上海 hello', email='[email protected]'),
        User(mid=6, nickname='测试数据 山东 hello', email='[email protected]'),
        User(mid=7, nickname='测试数据 武夷山 hello', email='[email protected]'),
        User(mid=8, nickname='测试数据 黄山 hello', email='[email protected]'),

        Article(arid=1, tags='测试数据 test hello', title='销售额度', description='测试 test ok'),
        Article(arid=2, tags='测试数据 china hello', title='成功转型', description='测试 test ok'),
        Article(arid=3, tags='测试数据 上海 hello', title='蓝蓝的天上白云飘', description='测试 test ok'),
        Article(arid=4, tags='测试数据 背景 hello', title='在水一方', description='测试 test ok'),
        Article(arid=5, tags='测试数据 上海 hello', title='晴天,阴天,雨天,大风天', description='测试 test ok'),
        Article(arid=6, tags='测试数据 山东 hello', title='每年365天,每天24小时', description='测试 test ok'),
        Article(arid=7, tags='测试数据 武夷山 hello', title='高效工作的秘密', description='测试 test ok'),
        Article(arid=8, tags='测试数据 黄山 hello', title='战狼2', description='测试 test ok'),
    ]
    )

    # 提交到数据库
    session.commit()


def modify_data():
    '''
    测试修改数据
    '''

    # 查询用户表
    users = session.query(User).all()
    print(users)

    # 查询文章表
    # articles = session.query(Article).all()
    articles = session.query(Article).filter(Article.arid==2)
    print(articles)

    # 修改文章表
    articles[0].description = '程度,修改描述可以成功'
    print(session.dirty)

    # 提交到数据库
    session.commit()


if __name__ == '__main__':
    create_engine()
    # modify_data()

3. 多进程搜索程序

3.1 程序关键点

  • 创建 DbMgr 类,封装数据库访问操作
  • 用 sqlalchemy 从数据库获取 member,article 表中的数据
  • 使用 automap 自动反射出 member,article 表对应的类
  • 创建 Searcher 类,提供进程调用函数,用来查询符合条件的结果,并且提供进程执行完的回调展示方法
  • 创建 10 个进程的进程池
  • 循环获取用户输入,创建 searcher 对象,多进程并发执行过滤
  • 多进程调用使用 python multiprocessing
#!/usr/bin/env python
# coding: utf-8


import os


from sqlalchemy import create_engine, MetaData
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.automap import automap_base
from multiprocessing import Pool


class DbMgr():
    '''
    连接数据库,从数据库获取 用户表,文章表数据
    '''

    def __init__(self):
        eng = create_engine('mysql+pymysql://root:1@localhost/test?charset=utf8')

        # 使用 automap 自动反射出 member,article 表对应的类
        meta = MetaData()
        meta.reflect(eng, only=['member', 'article'])
        Base = automap_base(metadata=meta)
        Base.prepare()

        self._Member = Base.classes.member
        self._Article = Base.classes.article

        # 获取操作数据库的 session
        Session = sessionmaker(eng, autocommit=True)
        self._ses = Session()

    def get_data(self):
        '''
        查询用户表,文章表
        '''
        self._users = self._ses.query('"user"', self._Member.mid,
                                      self._Member.email,
                                      self._Member.nickname).all()

        self._articles = self._ses.query(self._Article).all()
        self._articles = [('ar', i.arid, i.title, i.tags, i.description)
                          for i in self._articles]

        return list(self._users) + list(self._articles)


class Searcher():
    '''
    进城处理函数,查找符合条件的结果,找到后返回结果
    '''
    def __init__(self, keyword):
        self._keyword = keyword

    def run(self, data):
        '''
        查找字符串
        '''
        try:
            if self._keyword in str(data):
                return 'ret: ' + str(os.getpid()) + '->' + str(data)
            else:
                return None
        except Exception as e:
            return e

    def callback(self, data):
        '''
        全部执行完后回调函数,展示结果
        '''
        try:
            for i in data:
                if i:
                    print('match: {}'.format(i))
        except Exception as e:
            print(e)


def main():
    # 从数据库读取数据
    mgr = DbMgr()

    # 创建过滤进程池
    pool = Pool(4)

    # 创建搜索器
    while True:
        keyword = input('n输入搜索词:  ')
        if keyword == 'q':
            break

        searcher = Searcher(keyword)

        # 从数据库获取数据
        data = mgr.get_data()
        res = pool.map_async(searcher.run,
                             data,
                             10,
                             callback=searcher.callback)

        # 等待所有进程执行完成
        res.wait()
        print('all done', res.successful())


if __name__ == '__main__':
    main()

3.2 程序运行

(py36env) servadmin@debian:~/test # python mysql2es.py

输入搜索词:  test
match: ret: 10013->('user', 1, '[email protected]', '测试数据 test hello')
match: ret: 10013->('user', 2, '[email protected]', '测试数据 china hello')
match: ret: 10013->('user', 3, '[email protected]', '测试数据 上海 hello')
match: ret: 10013->('user', 4, '[email protected]', '测试数据 背景 hello')
match: ret: 10013->('user', 5, '[email protected]', '测试数据 上海 hello')
match: ret: 10013->('user', 6, '[email protected]', '测试数据 山东 hello')
match: ret: 10013->('user', 7, '[email protected]', '测试数据 武夷山 hello')
match: ret: 10013->('user', 8, '[email protected]', '测试数据 黄山 hello')
match: ret: 10013->('ar', 1, '销售额度', '测试数据 test hello', '程度,修改描述可以成功')
match: ret: 10013->('ar', 3, '蓝蓝的天上白云飘', '测试数据 上海 hello', '测试 test ok')
match: ret: 10013->('ar', 4, '在水一方', '测试数据 背景 hello', '测 test ok')
match: ret: 10013->('ar', 5, '晴天,阴天,雨天,大风天', '测试数据 上海 hello', '测试 test ok')
match: ret: 10013->('ar', 6, '每年365天,每天24小时', '测试数据 山东 hello', '测试 test ok')
match: ret: 10013->('ar', 7, '高效工作的秘密', '测试数据 武夷山 hello', '测试 test ok')
match: ret: 10013->('ar', 8, '战狼2', '测试数据 黄山 hello', '测试 test ok')
all done True

输入搜索词:  转型
match: ret: 10013->('ar', 2, '成功转型', '测试数据 china hello', '程度,修改描述可以成功')
all done True

输入搜索词:  test
match: ret: 10013->('user', 1, '[email protected]', '测试数据 test hello')
match: ret: 10013->('user', 2, '[email protected]', '测试数据 china hello')
match: ret: 10013->('user', 3, '[email protected]', '测试数据 上海 hello')
match: ret: 10013->('user', 4, '[email protected]', '测试数据 背景 hello')
match: ret: 10013->('user', 5, '[email protected]', '测试数据 上海 hello')
match: ret: 10013->('user', 6, '[email protected]', '测试数据 山东 hello')
match: ret: 10013->('user', 7, '[email protected]', '测试数据 武夷山 hello')
match: ret: 10013->('user', 8, '[email protected]', '测试数据 黄山 hello')
match: ret: 10013->('ar', 1, '销售额度', '测试数据 test hello', '程度,修改描述可以成功')
match: ret: 10013->('ar', 3, '蓝蓝的天上白云飘', '测试数据 上海 hello', '测试 test ok')
match: ret: 10013->('ar', 4, '在水一方', '测试数据 背景 hello', '测试 test ok')
match: ret: 10013->('ar', 5, '晴天,阴天,雨天,大风天', '测试数据 上海 hello', '测试 test ok')
match: ret: 10013->('ar', 6, '每年365天,每天24小时', '测试数据 山东 hello', '测试 test ok')
match: ret: 10013->('ar', 7, '高效工作的秘密', '测试数据 武夷山 hello', '测试 test ok')
match: ret: 10013->('ar', 8, '战狼2', '测试数据 黄山 hello', '测试 test ok')
all done True

输入搜索词:

搭建nginx gunicorn mysql环境部署django应用

说实在的第一次用服务器来部署django确实有点不知所措,上网查了一些资料,准备部署一个nginx+gunicorn+django+mysql的一个博客系统。

用户环境

  • 服务器:阿里云服务器ECS
  • 镜像系统:ubuntu16.04 64位
  • 准备建立:nginx+gunicorn+django+mysql的博客系统

首先登陆到云服务器,也不知道先干什么,那么先更新一下吧

sudo apt-get update
sudo apt-get upgrade

中间可能会询问一些问题,输入y即可。

nginx

简单介绍一下,nginx是一个轻量级的高性能的web服务器,反向代理服务器以及邮件服务器。首先来配置一下nginx

sudo apt-get install nginx

一阵等待之后nginx就安装好了,首先来看一下nginx.conf配置

# /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        gzip on;
        gzip_disable "msie6";

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

这一段代码是安装nginx后的nginx的默认配置,关于nginx的配置优化后续再讲,现在重要的是将服务器给搭建起来,首先简单的讲解一下

在默认配置中,nginx总共分为四个部分:

1、全局设置,主要用来设置nginx的相关配置,比如设定nginx运行的用户和用户组、运行的进程数、运行的文件等。该部分设置在{}之外。

2、events设置,从字面理解,events就是事件的意思,这里是设置事件的相关配置,如事件处理方式是epoll还是select、单个进程的最大连接数,网络IO模型等。

3、http设置,这里是http服务器的相关配置,从默认配置看,大致有五种,基本配置、SSL配置、Log配置、Gzip配置以及虚拟端口设置。这里我们先看虚拟端口这只,这里用include引入了两个文件,/etc/nginx/conf.d/.conf 和/etc/nginx/site-enabled/。我这里就直接将我自己的服务器配置放在conf.d/blog.conf文件中。

server {
    listen 80;
    server_name final-skynet.xin;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real_IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

这段代码的意思是nginx监听80端口,服务器的地址是final-skynet.xin,location是一个匹配规则,匹配到监听的url,具体的匹配规则这里就不详细赘述了。匹配成功后,转发到本地的http://127.0.0.1:8080地址,这是gunicorn监听的地址,然后设置其他需要转发的内容。

4、最后一个是mail设置,由于本项目现在还用不着,所以就将默认的代码注释删除了。nginx的基本配置就到这里了。

安装gunicorn和django

sudo pip install gunicorn
sudo apt-get install django

输入上述命令就可以安装好django和gunicorn了,这里先简单的配置一下django,创建一个简单的blog应用。

1、创建新项目

django-admin startproject SkyNet

在根目录输入该命令就可以创建一个新的django项目SkyNet,然后创建一个新的blog应用

进入SkyNet文件夹可以看到该文件夹的目录结构如下

/SkyNet
    manage.py
    /SkyNet
        __init__.py
        settings.py
        urls.py
        wsgi.py

然后在SkyNet目录下运行以下命令

python manage.py startapp blog

然后可以看见SkyNet目录下多了一个blog的文件夹,这个就是新创建的blog app

2、创建index页面

那先创建一个index页面来验证一下我们的配置。

  • 在urls.py中进行配置
from django.conf.urls import url
from django.contrib import admin
from blog.views import *

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$',index,name='index')
]
  • 创建index视图
from __future__ import unicode_literals

from django.shortcuts import render
from django.http import HttpResponse

def index(request):
    return HttpResponse('Hello , this is my first index')

现在需要在INSTALL_APPS配置创建的app和gunicorn

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'gunicorn',
    'blog',
]

下面需要通过gunicorn来启动django

gunicorn Skynet.wsgi:application -b 127.0.0.1:8080

最后在浏览器中输入网址进行验证

未分类

可能你需要重启一下你的nginx
讲到这个地方,云服务的环境部署应该是告一段落了

但是呢,在文章的开头,需要建立的是nginx+gunicorn+django+mysql的一个服务器,剩下的就是mysql的安装了

sudo apt-get install mysql-server

然后在安装的过程中提示需要设置密码,这个时候你可以选择设置密码,或者直接选择OK跳过。

我在这里是选择的跳过,安装完毕验证一下数据是否安装成功。由于我这里没有设置密码,故直接输入mysql就进入了mysql的命令行,设置密码的可以输入

mysql -u root -p

然后输入密码进入命令行。

在mysql创建数据库的时候经常出现编码问题,这里我先解决一下mysql的编码问题,在/etc/mysql/my.cnf中引用了conf.d中的文件,所以直接在/etc/mysql/conf.d/mysql.cnf进行修改。
首先查看一下mysql的编码,进入mysql命令行

mysql> show variables like 'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | latin1                     |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | latin1                     |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)
//或者
mysql> status
--------------
mysql  Ver 14.14 Distrib 5.7.19, for Linux (x86_64) using  EditLine wrapper

Connection id:      4
Current database:
Current user:       root@localhost
SSL:            Not in use
Current pager:      stdout
Using outfile:      ''
Using delimiter:    ;
Server version:     5.7.19-0ubuntu0.16.04.1 (Ubuntu)
Protocol version:   10
Connection:     Localhost via UNIX socket
Server characterset:    latin1
Db     characterset:    latin1
Client characterset:    utf8
Conn.  characterset:    utf8
UNIX socket:        /var/run/mysqld/mysqld.sock
Uptime:         1 min 16 sec

可以看到db和server的characterset都默认为latin1,这里需要设置[mysqld]的character-set-server = utf8即可

//mysql.cnf
[mysqld]
character-set-server = utf8

然后重启mysql服务,在命令行输入

service mysql restart

然后重新查看,可以看到mysql的默认编码已经改过来了。

如果你不想这么麻烦,可以在创建数据库的时候设置编码为utf8即可
如下命令

CREATE DATABSE db_name DEFUALT CHARACTER SET utf8;

下面在django中配置mysql

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'blog',
        'USER': 'root',
        'PASSWORD': '',
        'HOST':'',
        'PORT':'',
    }
}

这里由于我的mysql用户没有设置密码所以,password为空,后续再进行设置
先需要在mysql中创建数据库。

CREATE DATABASE blog;

这个时候django还没有和mysql连接起来,需要安装mysqlclient或者MySQL-python,我在这里安装的是mysqlclient,在安装mysqlclient之前还需要安装libmysqlclient-dev

sudo apt-get install libmysqlclient-dev
pip install mysqlclient

这个时候就可以用django的数据迁移

python manage.py makemigrations
python manage.py migrate

这个时候进入mysql命令行,查看blog数据库中是否插入了django数据迁移来的数据库表

mysql> use blog
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+----------------------------+
| Tables_in_blog             |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
+----------------------------+
10 rows in set (0.00 sec)

看到数据库中的表就可以指导mysql已经配置完成了。
这样SkyNet的服务器配置基本上就完成了。后面需要解决一下代码上传和nginx等优化的问题。

centos 7系统下重置mysql 5.7 root密码的步骤

mysql5.7版本之后,与mariadb不同,在安装之后,在启动之时,会进行自动随机密码的设定,所以在systemctl start mysqld之后,会出现mysql -uroot -p无法登陆的情况

mysql root原始密码查看

实际上mysqld在启动时,会自动设定root密码的,可以在其相关日志里面查看到,如下:

[root@bogon ~]# grep password /var/log/mysqld.log
2017-07-31T10:31:57.368883Z 1 [Note] A temporary password is generated for root@localhost: TjcrBm.j,7eU
2017-07-31T10:32:30.201882Z 3 [Note] Access denied for user 'root'@'localhost' (using password: NO)
2017-07-31T10:34:40.233539Z 0 [Note] Shutting down plugin 'validate_password'
2017-07-31T10:34:41.483983Z 0 [Note] Shutting down plugin 'sha256_password'
2017-07-31T10:34:41.483990Z 0 [Note] Shutting down plugin 'mysql_native_password'

mysql root的初始密码为TjcrBm.j,7eU(每人情况不一样,具体值在mysql日志中查看)

mysql密码重置

当然有些人可能是root密码真忘记了,那样可以通过mysql免密码登陆

在其配置文件/etc/my.cnf中加入skip-grant-tables=1即可

[root@bogon ~]# grep -v ^# /etc/my.cnf | grep -v ^$
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
symbolic-links=0
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
skip-grant-tables=1

然后重启mysql,使用mysql命令即可进入

[root@bogon ~]# systemctl restart mysqld
[root@bogon ~]# ss -tnl
State      Recv-Q Send-Q             Local Address:Port                            Peer Address:Port              
LISTEN     0      128                    127.0.0.1:9000                                       *:*                  
LISTEN     0      128                            *:111                                        *:*                  
LISTEN     0      128                            *:80                                         *:*                  
LISTEN     0      128                            *:22                                         *:*                  
LISTEN     0      100                    127.0.0.1:25                                         *:*                  
LISTEN     0      80                            :::3306                                      :::*                  
LISTEN     0      128                           :::111                                       :::*                  
LISTEN     0      128                           :::22                                        :::*                  
LISTEN     0      100                          ::1:25                                        :::*  
[root@bogon ~]# mysql
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 4
Server version: 5.7.19

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

mysql> 

修改mysql root密码

mysql> use mysql
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

mysql> update user set authentication_string = password("123456") where user="root";
Query OK, 1 row affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 1

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

然后将/etc/my.cnf中的skip-grant-tables=1注释掉,重启mysql服务即可。

在此要注意的是,之前版本密码修改字段为password,在5.7版本之后字段为authentication_string

mysql密码难度修改

mysql密码修改为123456之后,有人发现使用123456能进入mysql,但是却不能使用mysql的任何功能。会出现如下情况:

[root@bogon ~]# mysql -uroot -p123456
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 4
Server version: 5.7.19

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

mysql> show databases;
ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.

实际上,在初始更改root密码时,并不能直接使用update来更改,需要使用alter user命令来更改

mysql> alter user 'root'@'localhost' identified by '123456';
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
## mysql在5.7版本中加了密码安全等级,弱密码不能使用

mysql> alter user 'root'@'localhost' identified by 'QWEqwe123!@#';
Query OK, 0 rows affected (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

mysql 安全策略以及root密码清空

在上述修改密码之后,可查看mysql的密码安全策略

[root@bogon ~]# mysql -uroot -p'QWEqwe123!@#'
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 6
Server version: 5.7.19 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

mysql> show variables like '%password%';
+---------------------------------------+--------+
| Variable_name                         | Value  |
+---------------------------------------+--------+
| default_password_lifetime             | 0      |
| disconnect_on_expired_password        | ON     |
| log_builtin_as_identified_by_password | OFF    |
| mysql_native_password_proxy_users     | OFF    |
| old_passwords                         | 0      |
| report_password                       |        |
| sha256_password_proxy_users           | OFF    |
| validate_password_check_user_name     | OFF    |
| validate_password_dictionary_file     |        |
| validate_password_length              | 8      |
| validate_password_mixed_case_count    | 1      |
| validate_password_number_count        | 1      |
| validate_password_policy              | MEDIUM |
| validate_password_special_char_count  | 1      |
+---------------------------------------+--------+
14 rows in set (0.00 sec)

其中可以看到validate有很多参数:

  • validate_password_dictionary_file:插件用于验证密码强度的字典文件路径。

  • validate_password_length:密码最小长度。

  • validate_password_mixed_case_count:密码至少要包含的小写字母个数和大写字母个数。

  • validate_password_number_count:密码至少要包含的数字个数。

  • validate_password_policy:密码强度检查等级,0/LOW、1/MEDIUM、2/STRONG。

  • validate_password_special_char_count:密码至少要包含的特殊字符数。

其中,关于validate_password_policy-密码强度检查等级:

  • 0/LOW:只检查长度。

  • 1/MEDIUM:检查长度、数字、大小写、特殊字符。

  • 2/STRONG:检查长度、数字、大小写、特殊字符字典文件。

可以直接在mysql中进行参数的修改

mysql> set global validate_password_policy=0;
Query OK, 0 rows affected (0.00 sec)

mysql> set global validate_password_special_char_count=0;
Query OK, 0 rows affected (0.00 sec)

mysql> set global validate_password_number_count=0;
Query OK, 0 rows affected (0.00 sec)

mysql> set global validate_password_mixed_case_count=0;
Query OK, 0 rows affected (0.00 sec)

mysql> set global validate_password_length=0;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like '%password%';
+---------------------------------------+-------+
| Variable_name                         | Value |
+---------------------------------------+-------+
| default_password_lifetime             | 0     |
| disconnect_on_expired_password        | ON    |
| log_builtin_as_identified_by_password | OFF   |
| mysql_native_password_proxy_users     | OFF   |
| old_passwords                         | 0     |
| report_password                       |       |
| sha256_password_proxy_users           | OFF   |
| validate_password_check_user_name     | OFF   |
| validate_password_dictionary_file     |       |
| validate_password_length              | 0     |
| validate_password_mixed_case_count    | 0     |
| validate_password_number_count        | 0     |
| validate_password_policy              | LOW   |
| validate_password_special_char_count  | 0     |
+---------------------------------------+-------+
14 rows in set (0.00 sec)

当然,此种变更,只能在当前环境下生效,重启mysql后会失效

想要长期生效,需要将更改写入配置文件中才可永久生效

也可以直接在配置文件中进行修改禁用validate-password功能

编辑my.cnf配置文件,在mysqld下面加入“validate-password=0”,然后重启mysql即可。

[root@bogon ~]# grep -v ^# /etc/my.cnf  | grep -v ^$
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
validate-password=0
symbolic-links=0
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

将mysql的密码安全等级降低之后,可以将root密码修改回来

mysql> use mysql
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> update user set authentication_string = password("") where user="root";
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

mysql> exit

以上密码root即清除为空了。使用mysql即可直接进入mysql。当然生产环境中不推荐此种做法。

CentOS 6.X RPM安装mysql 5.7.X

准备工作:

下载MySQL:https://dev.mysql.com/downloads/mysql/

下载到/usr/local/src/mysql 目录下(mysql目录是自己创建的,用于存放rpm包)

CentOS 6.X选择版本,如图:

未分类

下载:

  • (mysql-community-server-5.7.19-1.el6.x86_64.rpm)

  • (mysql-community-client-5.7.19-1.el6.x86_64.rpm)

  • (mysql-community-common-5.7.19-1.el6.x86_64.rpm)

  • (mysql-community-libs-5.7.19-1.el6.x86_64.rpm)

或者:

  • (mysql-5.7.19-1.el6.x86_64.rpm-bundle.tar)(里面包括上述所有rpm)

CentOS 7.X选择版本,如图:

未分类

下载:

  • (mysql-community-server-5.7.19-1.el7.x86_64.rpm)

  • (mysql-community-client-5.7.19-1.el7.x86_64.rpm)

  • (mysql-community-common-5.7.19-1.el7.x86_64.rpm)

  • (mysql-community-libs-5.7.19-1.el7.x86_64.rpm)

或者

  • (mysql-5.7.19-1.el7.x86_64.rpm-bundle.tar)(里面包括所有上述rpm)

1、检查是否已安装了MYSQL

rpm -qa | grep -i mysql   //方式1  
yum list installed mysql*   //方式2  

2、删除MYSQL

yum  remove mysql mysql-server mysql-libs compat-mysql51 //删除<a href="http://lib.csdn.net/base/mysql" class='replace_word' title="MySQL知识库" target='_blank' style='color:#df3434; font-weight:bold;'>数据库</a>  
rpm -ev MySQL-server-4.0.14-0 MySQL-client-4.0.14-0    //删除rpm包  

3、查找之前的my.cnf,如果有就删除

whereis my.cnf   //查找方式1  
find / -name my.cnf  //查找方式2  

rm -rf /etc/my.cnf  //删除

3、安装mysql

安装顺序:

  • (mysql-community-common-5.7.19-1.el6.x86_64.rpm)

  • (mysql-community-libs-5.7.19-1.el6.x86_64.rpm)

  • (mysql-community-client-5.7.19-1.el6.x86_64.rpm)

  • (mysql-community-server-5.7.19-1.el6.x86_64.rpm)

cd /usr/local/src/mysql //找到下载的rpm包  
rpm -ivh mysql-community-common-5.7.19-1.el6.x86_64.rpm   
rpm -ivh mysql-community-libs-5.7.19-1.el6.x86_64.rpm   
rpm -ivh mysql-community-client-5.7.19-1.el6.x86_64.rpm   
rpm -ivh mysql-community-server-5.7.19-1.el6.x86_64.rpm  

4、默认安装位置

/var/lib/mysql //数据库目录

/etc/my.cnf //配置文件目录

/usr/bin //相关命令目录

/etc/init.d/mysqld //启动脚本

/usr/sbin/mysqld  //启动脚本

5、启动mysql

service mysqld start   //启动mysql  
service mysqld status  //查看mysql启动状态  

6、找到临时root密码

cat /var/log/mysqld.log |grep password    //查看日志找到密码  
2017-08-08T05:05:55.605159Z 1 [Note] A temporary password is generated for root@localhost: L1kB0wpTo(in  
2017-08-08T05:06:01.287139Z 3 [Note] Access denied for user 'UNKNOWN_MYSQL_USER'@'localhost' (using password: NO)  
2017-08-08T05:10:21.202185Z 4 [Note] Access denied for user 'root'@'localhost' (using password: NO)  
//复制密码  

7、登录MYSQL

mysql -uroot -p   //登录mysql  
alter user 'root'@'localhost' identified by 'Abc@123';  //修改root密码  
//可能会遇到的问题,在5.6后,mysql内置密码增强机制,低强度密码会报错:  
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements  
//修改一个包含大小写及数字的复杂密码即可

8、添加远程用户

use mysql;  
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'abc@123' WITH GRANT OPTION;  
//'%'代表任意地址,也可以指定IP

9、检查用户表,刷新内存权限

select host, user from user;  
FLUSH PRIVILEGES;  

10、设置防火墙

/sbin/iptables -I INPUT -p tcp --dport 3306 -j ACCEPT   //添加3306端口  
/etc/rc.d/init.d/iptables save       //保存配置  
/etc/init.d/iptables save            //也可保存  
/etc/init.d/iptables restart         //重启防火墙  
//或者直接编辑防火墙配置文件  
vim /etc/sysconfig/iptables          //编辑防火墙配置文件  
//其它操作  
/etc/init.d/iptables status          //查看防火墙状态  
/etc/init.d/iptables stop            //关闭防火墙服务  
chkconfig –level 35 iptables off    //永久关闭防火墙  

11、开机启动

chkconfig --list        //查看开机启动项  
chkconfig mysqld on     //设置开机启动  
chkconfig mysqld off    //关闭开机启动  

配置Zabbix监控MySQL详细教程

一、Server端安装

机器 192.168.94.78

1、创建mysql实例

机器:192.168.94.78
目录:/data/mysql6001

2、建立系统用户

[[email protected] zabbix_agent]# groupadd  -g 201 zabbix
[[email protected] zabbix_agent]# useradd -g zabbix -u 201 -m zabbix
[[email protected] zabbix_agent]# passwd zabbix

3、创建zabbix数据库

[[email protected] zabbix]# mysql6001
mysql> create database zabbix; 
Query OK, 1 row affected (0.00 sec)

mysql>  grant all on zabbix.* to zabbix@'%' identified by 'zabbix'; 
Query OK, 0 rows affected (0.00 sec)

mysql>  flush privileges;  
Query OK, 0 rows affected (0.00 sec)

4、上传zabbix包到server端机器

zabbix-2.4.4.tar.gz
解压
tar zxvf zabbix-2.4.4.tar.gz 
目录:zabbix-2.4.4

5、导入SQL

cd /data/zabbix/zabbix-2.4.4/database/mysql/
注:有顺序的
[[email protected] mysql]# mysql6001 zabbix < schema.sql 
[[email protected] mysql]# mysql6001 zabbix < images.sql 
[[email protected] mysql]# mysql6001 zabbix < data.sql 

6、导入网页

安装软件包

[[email protected] mysql]# yum -y  install curl curl-devel net-snmp net-snmp-devel perl-DBI php-gd php-xml php-bcmath
[[email protected] mysql]#yum -y install php 
[[email protected] mysql]#yum -y install httpd

[[email protected] html]# mkdir  /var/www/html/zabbix
[[email protected] html]# cp -a /data/zabbix/zabbix-2.4.4/frontends/php/*     /var/www/html/zabbix/
[[email protected] html]# chown -R  apache.apache /var/www/html/zabbix/

7、安装zabbix服务端

[[email protected] zabbix-2.4.4]# cd /data/zabbix/zabbix-2.4.4
[[email protected] zabbix-2.4.4]#  ./configure --prefix=/usr/local/zabbix --enable-server --enable-agent --enable-proxy --with-mysql=/usr/local/mysql5.5/bin/mysql_config --with-net-snmp --with-libcurl

第二步:make
第三步:make install

修改 vi /etc/services,添加下面几行

zabbix-agent    10050/tcp               #zabbix agent
zabbix-agent    10050/udp               #zabbix agent
zabbix-trapper  10051/tcp               #zabbix trapper
zabbix-trapper  10051/udp               #zabbix trapper

8、修改zabbix的配置:

[[email protected] zabbix-2.4.4]# vi /usr/local/zabbix/etc/zabbix_server.conf
[[email protected] zabbix-2.4.4]# cat /usr/local/zabbix/etc/zabbix_server.conf|grep -v ^# | grep -v ^$
LogFile=/data/zabbix/zabbix_server.log
PidFile=/data/zabbix/zabbix_server.pid
DBHost=192.168.94.78
DBName=zabbix
DBUser=zabbix
DBPassword=zabbix
DBSocket=/tmp/mysql.sock
DBPort=6001
StartPollers=20
StartTrappers=15
 StartPingers=10
User=zabbix

[root@localhost ~]# vi /etc/init.d/zabbix_server

BASEDIR=/usr/local/zabbix   ##########修改这行

[root@localhost ~]# vi /etc/init.d/zabbix_agentd

BASEDIR=/usr/local/zabbix   ##########修改这行

9、修改httpd配置

[[email protected] zabbix-2.4.4]# vi /etc/httpd/conf/httpd.conf 
ServerName 127.0.0.1
<VirtualHost *:80>
 DocumentRoot  "/var/www/html"
 ServerName 192.168.94.78
</VirtualHost>

10、开启httpd

[[email protected] core]# service httpd restart
Stopping httpd: [  OK  ]
Starting httpd: httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName
[  OK  ]

[[email protected] core]# lsof -i:80
COMMAND   PID   USER   FD   TYPE     DEVICE SIZE/OFF NODE NAME
httpd   23923   root    4u  IPv4 2582967064      0t0  TCP *:80(LISTEN)
httpd   23925 apache    4u  IPv4 2582967064      0t0  TCP *:80 (LISTEN)
httpd   23926 apache    4u  IPv4 2582967064      0t0  TCP *:80 (LISTEN)
httpd   23927 apache    4u  IPv4 2582967064      0t0  TCP *:80 (LISTEN)
httpd   23928 apache    4u  IPv4 2582967064      0t0  TCP *:80 (LISTEN)
httpd   23929 apache    4u  IPv4 2582967064      0t0  TCP *:80 (LISTEN)
httpd   23930 apache    4u  IPv4 2582967064      0t0  TCP *:80 (LISTEN)
httpd   23931 apache    4u  IPv4 2582967064      0t0  TCP *:80 (LISTEN)
httpd   23932 apache    4u  IPv4 2582967064      0t0  TCP *:80 (LISTEN)

11、做几个软连接

[root@localhost ~]# ln -s /usr/local/zabbix/bin/* /usr/bin/
[root@localhost ~]# ln -s /usr/local/zabbix/sbin/* /usr/sbin/

[root@localhost ~]# cd  /data/zabbix/zabbix-2.4.4/misc/init.d/fedora/core

[root@localhost ~]# cp * /etc/init.d/

12、开启zabbix server端

[[email protected] subsys]# chown zabbix.zabbix   /var/lock/subsys/ -R
[[email protected] subsys]# chown zabbix.zabbix   /usr/local/zabbix  -R
[[email protected] subsys]# chown zabbix.zabbix  /data/zabbix/  -R

[[email protected] core]# /etc/init.d/zabbix_agentd start
Starting zabbix_agentd:  zabbix_agentd [24613]: user zabbix does not exist
zabbix_agentd [24613]: cannot run as root!
[FAILED]
[[email protected] core]# /etc/init.d/zabbix_server start
Starting zabbix_server:  zabbix_server [24821]: user zabbix does not exist
zabbix_server [24821]: cannot run as root!
[FAILED]

处理失败:
[[email protected] home]#  vi /etc/passwd 
改成:
zabbix:x:505:498::/home/zabbix:/bin/bash
[[email protected] home]# su - zabbix

===============================================
[zabbix_linux@bjs-dbmypacket3301 ~]$  /etc/init.d/zabbix_agentd restart
Shutting down zabbix_agentd: [  OK  ]
Starting zabbix_agentd:  [  OK  ]

[zabbix_linux@bjs-dbmypacket3301 ~]$  /etc/init.d/zabbix_server stop
Shutting down zabbix_server: [  OK  ]
[zabbix_linux@bjs-dbmypacket3301 ~]$  /etc/init.d/zabbix_server start
Starting zabbix_server:  [  OK  ]

13、页面设置zabbix

http://192.168.94.78/zabbix/setup.php

未分类

zabbix PHP mbstring.func_overload 报错

修复方法:

vi /etc/php.ini
mbstring.func_overload=off
lways_populate_raw_post_data=1
 重启httpd与zabbix
[[email protected] html]# service httpd restart
[zabbix@bjs-dbmypacket3301 ~]$ /etc/init.d/zabbix_server restart

未分类

未分类

未分类

未分类

未分类

未分类

未分类

二、客户端安装

客户端机器

192.168.94.144
192.168.94.137

1、安装agent

[[email protected] zabbix]# scp zabbix-2.4.4.tar.gz 192.168.94.144:/data/zabbix_agent/

[[email protected] zabbix_agent]# groupadd  -g 201 zabbix
[[email protected] zabbix_agent]# useradd -g zabbix -u 201 -m zabbix
[[email protected] zabbix_agent]# passwd zabbix
Changing password for user zabbix.
New password: 
BAD PASSWORD: it is based on a dictionary word
BAD PASSWORD: is too simple
Retype new password: 
passwd: all authentication tokens updated successfully.
 [[email protected] zabbix-2.4.4]# cd /data/zabbix_agent/zabbix-2.4.4
 [[email protected] zabbix-2.4.4]#  ./configure --prefix=/usr/local/zabbix  --enable-agent --enable-proxy --with-mysql=/usr/local/mysql5.5/bin/mysql_config 

[[email protected] zabbix-2.4.4]# make && make install 

2、拷贝/创建快捷

[[email protected] zabbix-2.4.4]# pwd
/data/zabbix_agent/zabbix-2.4.4
[[email protected] zabbix-2.4.4]# cp misc/init.d/tru64/zabbix_agentd   /etc/init.d/
[[email protected] zabbix-2.4.4]# chmod +x /etc/init.d/zabbix_agentd
3、修改配置文件
[[email protected] zabbix-2.4.4]# vi  /usr/local/zabbix/etc/zabbix_agentd.conf

[[email protected] zabbix-2.4.4]# cat /usr/local/zabbix/etc/zabbix_agentd.conf | grep -v ^# | grep -v ^$
LogFile=/data/zabbix_agent/zabbix_agentd.log
UnsafeUserParameters=1                   
Include= /usr/local/zabbix/etc/zabbix_agentd.conf.d/
Server=192.168.94.78      #server端IP
ServerActive=192.168.94.78   #server端IP
Hostname=192.168.94.144     #客户端IP

4、开启zabbix 的agent

[[email protected] zabbix-2.4.4]# chown zabbix.zabbix /data/zabbix_agent/ -R
[[email protected] zabbix-2.4.4]# chown zabbix.zabbix /usr/local/zabbix/  -R
[[email protected] zabbix-2.4.4]# chown zabbix.zabbix /soft/mysqlmonitor/port_6190 -R


[[email protected] tmp]# /usr/local/zabbix/sbin/zabbix_agentd -c /usr/local/zabbix/etc/zabbix_agentd.conf
[[email protected] tmp]# ps -ef|grep zabbix
zabbix   53114     1  0 16:14 ?        00:00:00 /usr/local/zabbix/sbin/zabbix_agentd -c /usr/local/zabbix/etc/zabbix_agentd.conf
zabbix   53115 53114  0 16:14 ?        00:00:00 /usr/local/zabbix/sbin/zabbix_agentd: collector [idle 1 sec]                    
zabbix   53116 53114  0 16:14 ?        00:00:00 /usr/local/zabbix/sbin/zabbix_agentd: listener #1 [waiting for connection]      
zabbix   53117 53114  0 16:14 ?        00:00:00 /usr/local/zabbix/sbin/zabbix_agentd: listener #2 [waiting for connection]      
zabbix   53118 53114  0 16:14 ?        00:00:00 /usr/local/zabbix/sbin/zabbix_agentd: listener #3 [waiting for connection]      
zabbix   53119 53114  0 16:14 ?        00:00:00 /usr/local/zabbix/sbin/zabbix_agentd: active checks #1 [idle 1 sec]             
root     53362 62129  0 16:14 pts/0    00:00:00 grep zabbix
[[email protected] tmp]# lsof -i:1005COMMAND     PID   USER   FD   TYPE     DEVICE SIZE/OFF NODE NAME
zabbix_ag 53114 zabbix    4u  IPv4 2301997410      0t0  TCP *:zabbix-agent (LISTEN)
zabbix_ag 53115 zabbix    4u  IPv4 2301997410      0t0  TCP *:zabbix-agent (LISTEN)
zabbix_ag 53116 zabbix    4u  IPv4 2301997410      0t0  TCP *:zabbix-agent (LISTEN)
zabbix_ag 53117 zabbix    4u  IPv4 2301997410      0t0  TCP *:zabbix-agent (LISTEN)
zabbix_ag 53118 zabbix    4u  IPv4 2301997410      0t0  TCP *:zabbix-agent (LISTEN)
zabbix_ag 53119 zabbix    4u  IPv4 2301997410      0t0  TCP *:zabbix-agent (LISTEN)

5、界面添加host

此时可以在界面上面添加host进行监控测试了,因后面添加自定义监控,此处忽略

三、自定义监控配置

1、准备检查脚本

准备检查mysql状态的脚本

[[email protected] ~]# cd /soft/port_6190/
[[email protected] port_6190]# ./mysql_status.pl|grep com_writes
com_writes=0

[[email protected] port_6190]# /soft/port_6190/mysql_status.pl |grep com_writes|awk -F '=' '{print $2}'
0

注:此处可以自定义脚本,返回key=value 即可,或者使用percona的模板,此处以自定义监控脚本为例来进行配置监控

2、修改agent的配置文件

[[email protected] port_6190]# vi /usr/local/zabbix/etc/zabbix_agentd.conf

[[email protected] port_6190]# cat /usr/local/zabbix/etc/zabbix_agentd.conf|grep -v '^#'|grep -v '^$'
LogFile=/data/zabbix_agent/zabbix_agentd.log
UnsafeUserParameters=1                   
Include= /usr/local/zabbix/etc/zabbix_agentd.conf.d/
Server=192.168.94.78
ServerActive=192.168.94.78
Hostname=test
 UserParameter=com_writes,/soft/port_6190/mysql_status.pl |grep com_writes|awk -F '=' '{print $2}'
 UserParameter=com_reads,/soft/port_6190/mysql_status.pl  |grep com_reads |awk -F '=' '{print $2}'
 UserParameter=com_update,/soft/port_6190/mysql_status.pl |grep com_update|awk -F '=' '{print $2}'
 UserParameter=com_insert,/soft/port_6190/mysql_status.pl |grep com_insert|awk -F '=' '{print $2}'
 UserParameter=com_delete,/soft/port_6190/mysql_status.pl |grep com_delete|awk -F '=' '{print $2}'

注:com_delet 为键值名,后面为键值的值

重启agent

/etc/init.d/zabbix_agentd stop
/usr/local/zabbix/sbin/zabbix_agentd -c /usr/local/zabbix/etc/zabbix_agentd.conf

3、server端检查

检查

[[email protected] bin]# pwd
/usr/local/zabbix/bin

[[email protected] bin]# ./zabbix_get -s 192.168.94.144 -k com_writes
0

4、server端进行界面配

4.1添加host

未分类

4.2 添加模版

未分类

4.3 创建应用集

未分类

未分类

4.4创建项目

未分类

4.5 创建图形

未分类

未分类

未分类

5、查看监控图

未分类

6、其他过程

未分类

未分类

未分类

未分类

未分类

未分类

未分类

未分类

未分类

未分类

在单台机器上搭建Mysql的多个实例

随着互联网技术的发展,数据量越来越庞大,我们急需一个大的存储和大的分析系统。虽然有nosql数据库、hadoop文件存储等数据存储方式能够解决该问题,但是,关系型数据库依然有它的优势所在,尤其是对结构化数据的处理,性能仍然很棒。或者,从公司的项目开发成本讲,关系型数据库的使用比nosql数据库使用更加简易,更加便于维护。

因此,本文介绍一下Mycat使用的第一步(当然,这一步不是必须的),学会如何搭建Mysql单机多实例,从而应对大数据量查询慢的问题。

1、启动项

vim /etc/apparmor.d/usr.sbin.mysqld/etc/init.d/apparmor reload

AppArmor(Application Armor)是Linux内核的一个安全模块,AppArmor允许系统管理员将每个程序与一个安全配置文件关联,从而限制程序的功能。简单的说,AppArmor是与SELinux类似的一个访问控制系统,通过它你可以指定程序可以读、写或运行哪些文件,是否可以打开网络端口等。作为对传统Unix的自主访问控制模块的补充,AppArmor提供了强制访问控制机制,它已经被整合到2.6版本的Linux内核中。

详细资料查看:
Apparmor——Linux内核中的强制访问控制系统
http://www.cnblogs.com/-Lei/archive/2013/02/24/2923947.html

2、创建新实例的数据目录

mkdir /var/lib/mysql2   创建目录chown mysql /var/lib/mysql2  给mysql用户权限

3、创建数据库,初始化数据库

mysql 5.7以下 
mysql_install_db –user=mysql –datadir=/var/lib/mysql2
mysql 5.7以上 
mysqld –user=mysql –datadir=/var/lib/mysql2

4、配置多实例配置文件

[mysqld_multi]
mysqld     = /install/mysql/bin/mysqld_safe  
mysqladmin = /install/mysql/bin/mysqladmin  
user       = root  
# The MySQL server  
[mysqld1]  
port            = 3306  
socket          = /tmp/mysql.sock  
datadir         =/var/lib/mysql  
pid-file        =/var/lib/mysql/mysql.pid  
user            =mysql  
log-bin         =master-bin  
log-bin-index           =master-bin.index  
...
[mysqld2]  
port            = 3307  
socket          =/tmp/mysql2.sock  
datadir         =/var/lib/mysql2  
pid-file        =/var/lib/mysql2/mysql.pid  
user            =mysql  
...

5、启动实例

mysqld_multi   --defaults-file=/etc/mysql/my_multi.cnf start 1
mysqld_multi   --defaults-file=/etc/mysql/my_multi.cnf start 2

6、登陆Mysql

# 登陆Mysql服务器,执行mysql命令进入mysql控制台
mysql -uroot -P3307 -p -S/tmp/mysql2.sock
# 不用输入密码,直接回车
# 查看当前用户
select User from mysql.user;
# 创建Mysql用户test,并赋权限
CREATE USER 'test'@'%' IDENTIFIED BY '123456';
GRANT GRANT OPTION ON *.* TO 'test'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER ON *.* TO 'test'@'%';

无备份情况下MySQL innodb表被意外删除的恢复

这里我们首先来测试innodb_file_per_table为off的情况,即表结构和数据存在同一个文件中。这里我分别测试了表存在主键和不存在主键的情况,供参考。

innodb_file_per_table参数为off(有主键的情况)

1、创建测试表

mysql> use recover;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> create table test_drop0801(id int);
Query OK, 0 rows affected (0.02 sec)

mysql> insert into test_drop0801 values(100);
Query OK, 1 row affected (0.01 sec)

mysql> insert into test_drop0801 values(101);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test_drop0801 values(102);
Query OK, 1 row affected (0.00 sec)

mysql> alter table test_drop0801 add primary key(id);
Query OK, 3 rows affected (0.02 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> explain select * from test_drop0801 where id=102;
+----+-------------+---------------+-------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table        | type  | possible_keys | key    | key_len | ref  | rows | Extra      |
+----+-------------+---------------+-------+---------------+---------+---------+-------+------+-------------+
|  1 | SIMPLE      | test_drop0801 | const | PRIMARY      | PRIMARY | 4      | const |    1 | Using index |
+----+-------------+---------------+-------+---------------+---------+---------+-------+------+-------------+
1 row in set (0.00 sec)


mysql> show global variables like '%file_per%';
+-----------------------+-------+
| Variable_name        | Value |
+-----------------------+-------+
| innodb_file_per_table | OFF  |
+-----------------------+-------+
1 row in set (0.00 sec)

mysql> show create table test_drop0801 G;
*************************** 1. row ***************************
      Table: test_drop0801
Create Table: CREATE TABLE `test_drop0801` (
  `id` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

ERROR:
No query specified

2、备份表结构

[root@killdb ~]# mysqldump --opt -d -uroot -proger recover test_drop0801 > /tmp/innodb_recovery/recover/test_drop0801.sql
[root@killdb ~]#
1
2

[root@killdb ~]# mysqldump --opt -d -uroot -proger recover test_drop0801 > /tmp/innodb_recovery/recover/test_drop0801.sql
[root@killdb ~]#

3、删除表

mysql> drop table test_drop0801;
Query OK, 0 rows affected (0.00 sec)

4、扫描数据文件

[root@killdb innodb_recovery]# ./stream_parser -f /var/lib/mysql/ibdata1
Opening file: /var/lib/mysql/ibdata1
File information:

ID of device containing file:        64768
inode number:                      924765
protection:                        100660 (regular file)
number of hard links:                    1
user ID of owner:                      496
group ID of owner:                    491
device ID (if special file):            0
blocksize for filesystem I/O:        4096
number of blocks allocated:          69632
time of last access:            1496441095 Sat Jun  3 06:04:55 2017
time of last modification:      1496464241 Sat Jun  3 12:30:41 2017
time of last status change:    1496464241 Sat Jun  3 12:30:41 2017
total size, in bytes:            35651584 (34.000 MiB)

Size to process:                  35651584 (34.000 MiB)
All workers finished in 1 sec

5、创建用于恢复的数据字典

[root@killdb innodb_recovery]# ./recover_dictionary.sh
Generating dictionary tables dumps... OK
Creating test database ... OK
Creating dictionary tables in database test:
SYS_TABLES ... OK
SYS_COLUMNS ... OK
SYS_INDEXES ... OK
SYS_FIELDS ... OK
All OK
Loading dictionary tables data:
SYS_TABLES ... 234 recs OK
SYS_COLUMNS ... 324 recs OK
SYS_INDEXES ... 123 recs OK
SYS_FIELDS ... 248 recs OK
All OK

6、查询需要恢复表的信息

mysql> use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql>
mysql> select * from SYS_TABLES where name like 'recover/test_drop0801%';
+-----------------------+-----+--------+------+--------+---------+--------------+-------+
| NAME                  | ID  | N_COLS | TYPE | MIX_ID | MIX_LEN | CLUSTER_NAME | SPACE |
+-----------------------+-----+--------+------+--------+---------+--------------+-------+
| recover/test_drop0801 | 187 |      1 |    1 |      0 |      0 |              |    0 |
+-----------------------+-----+--------+------+--------+---------+--------------+-------+
1 row in set (0.00 sec)

mysql> select * from SYS_INDEXES where table_id=187;
+----------+-----+---------+----------+------+-------+------------+
| TABLE_ID | ID  | NAME    | N_FIELDS | TYPE | SPACE | PAGE_NO    |
+----------+-----+---------+----------+------+-------+------------+
|      187 | 184 | PRIMARY |        1 |    3 |    0 | 4294967295 |
+----------+-----+---------+----------+------+-------+------------+
1 row in set (0.00 sec)

7、确认数据page中数据是否存在

[root@killdb innodb_recovery]#  ./c_parser -6f pages-ibdata1/FIL_PAGE_INDEX/0000000000000184.page -t recover/test_drop0801.sql |head -5
Line 22: syntax error at 'DROP'
21:
22: DROP TABLE IF EXISTS `test_drop0801`;
Failed to parse table structure
[root@killdb innodb_recovery]#

这里的报错是因为脚本的问题,需要修改备份脚本(mysqldump产生的).

[root@killdb innodb_recovery]#  ./c_parser -6f pages-ibdata1/FIL_PAGE_INDEX/0000000000000184.page -t recover/test_drop0801.sql |head -5
-- Page id: 562, Format: COMPACT, Records list: Valid, Expected records: (3 3)
000000001517    94000001800110test_drop0801    100
000000001517    9400000180011Dtest_drop0801    101
000000001517    9400000180012Atest_drop0801    102
SET FOREIGN_KEY_CHECKS=0;
LOAD DATA LOCAL INFILE '/tmp/innodb_recovery/dumps/default/test_drop0801' REPLACE INTO TABLE `test_drop0801` FIELDS TERMINATED BY 't' OPTIONALLY ENCLOSED BY '"' LINES STARTING BY 'test_drop0801t' (`id`);
-- Page id: 562, Found records: 3, Lost records: NO, Leaf page: YES
[root@killdb innodb_recovery]#

8、抽取page中的数据

[root@killdb innodb_recovery]#  ./c_parser -6f pages-ibdata1/FIL_PAGE_INDEX/0000000000000184.page -t recover/test_drop0801.sql > dumps/default/test_drop0801 2> dumps/default/test_drop0801_load.sql          
[root@killdb innodb_recovery]#
[root@killdb innodb_recovery]# ls -ltr dumps/default/test_drop0801*
-rw-r--r--. 1 root root 232 Jun  3 12:34 dumps/default/test_drop0801_load.sql
-rw-r--r--. 1 root root 285 Jun  3 12:34 dumps/default/test_drop0801

9、加载数据到数据库

mysql> use recover
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> source recover/test_drop0801.sql
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.02 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)


mysql> source dumps/default/test_drop0801_load.sql
Query OK, 0 rows affected (0.00 sec)

Query OK, 3 rows affected (0.00 sec)
Records: 3  Deleted: 0  Skipped: 0  Warnings: 0

mysql> select * from test_drop0801;
+-----+
| id  |
+-----+
| 100 |
| 101 |
| 102 |
+-----+
3 rows in set (0.00 sec)

mysql> 

我们可以看到,顺利完成了drop table的恢复,而且数据完好无损。实际上我这里还同时测试了无主键的情况,经过测试都类似,可以进行完美的恢复。这里不再累述。

使用Prometheus监控MySQL状态

Prometheus官方提供了mysqld_exporter,我们直接使用即可。

在每个要监控的MySQL中创建监控用户并授予权限。

CREATE USER 'exporter'@'127.0.0.1' IDENTIFIED BY 'XXXXXXXX' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'127.0.0.1';
flush privileges;

mysqld_exporter也是用Go语言写的,安装十分简单,我们的环境MySQL有3个节点,使用ansible部署mysqld_exporter。 这里贴一下其中一个MySQL节点上生成的systemd的单元文件:

[Unit]
Description=mysqld_exporter
After=network.target
[Service]
Type=simple
User=prometheus
Environment=DATA_SOURCE_NAME=exporter:exporterpass@tcp(127.0.0.1:3306)/?loc=Local
ExecStart=/home/prometheus/mysqld_exporter/mysqld_exporter 
 -web.listen-address=:9104
Restart=on-failure
[Install]
WantedBy=multi-user.target
  • mysqld_exporter从环境变量DATA_SOURCE_NAME获取连接MySQL的dns信息,注意以前面我们创建的单独的监控用户
  • -web.listen-address设置mysqld_exporter的监听端口,默认为9104

接下来在Prometheus的配置文件中配置收集MySQL信息的Job和Instance,这里还是贴一下我们的配置文件片段,实际上这个片段也是有ansible编排生成的:

scrape_configs:
  - job_name: 'mysql'
        static_configs:
         - targets:
            - 192.168.1.11:9104
           labels:
             instance: db1
         - targets:
            - 192.168.1.12:9104
           labels:
             instance: db2
         - targets:
            - 192.168.1.13:9104
           labels:
             instance: db3

重启Prometheus之后,Prometheus就可以从mysqld_exporter中收集数据了。

在监控图表上我们使用的Grafana,因此可以直接使用percona grafana-dashboards提供的图表。