【4.分布式存储】-mysql及proxy

概述

文章https://segmentfault.com/a/11... 中介绍了存储应该考虑的方向。本文详细介绍其中的mysq,主要是INNODB。总体架构,启动流程,一条语句的执行过程带你快速深刻mysql源码。再从性能(缓存,数据结构),功能(ACID实现,索引)如何实现介绍了mysql中核心点。第二部分为分布式,介绍原生mysql的同步过程。第三部分是proxy,由于proxy多数会自研,只介绍proxy应该包含的功能。mysql

关键词:innodb,MVCC,ACID实现,索引,主从同步,proxy

第一章 总体架构/流程

clipboard.png
mysql为单进程多线程,由于元数据用Innodb保存,启动后除了mysql的处理链接请求/超时等还会启动Innodb的全部线程。
主流程:linux

主函数在sql/amin.cc中
调用Mysqld.cc中mysqld_main

 1. 首先载入日志,信号注册,plugin_register
    (mysql是插件式存储引擎设计,innodb,myisam等都是插件,在这里注册),核心为mysqld_socket_acceptor->connection_event_loop();

 2. 监听处理循环poll。
    process_new_connection处理handler有三种:线程池方式只用于商业,一个线程处理全部请求,一个链接一个线程(大多数选择Per_thread_connection_handler)。
 3. 若thread_cache中有空闲直接获取,不然建立新的用户线程。进入用户线程的handle_connection
    3.1 mysql网络通讯一共有这几层:`THD` | Protocol | NET | VIO | SOCKET,protocol对数据的协议格式化,NET封装了net buf读写刷到网络的操做,VIO是对全部链接类型网络操做的一层封装(TCP/IP, Socket, Name Pipe, SSL, SHARED MEMORY),handle_connection初始化THD(线程),开始do_command   (关于THD,有个很好的图:http://mysql.taobao.org/monthly/2016/07/04/)
    3.2.do_command=>dispatch_comand=>mysql_parse=》【检查query_cache有缓存直接返回不然=》】parse_sql=》mysql_execute_cmd判断insert等调用mysql_insert,每条记录调用write_record,这个是各个引擎的基类,根据操做表类型调用引擎层的函数=》写binlog日志=》提交/回滚。注意你们可能都觉得是有query_cache的。可是从8.0开启废弃了query_cache。第二正会讲一下
 4. 除了用户线程和主线程,在启动时,还建立了timer_notify线程。因为为了解决DDL没法作到atomic等,从MySQL8.0开始取消了FRM文件及其余server层的元数据文件(frm, par, trn, trg, isl,db.opt),全部的元数据都用InnoDB引擎进行存储, 另一些诸如权限表之类的系统表也改用InnoDB引擎。所以在加载这些表时,建立了innodb用到的一系列线程。

从插入流程开始

总体流程图以下:
clipboard.pnggit

必须有这些步骤的缘由:
clipboard.png
[1]为了快,全部数据先写入内存,再刷脏
[2]为了防止数据页写过程当中崩溃数据的持久性=》先写redo保证重启后能够恢复。日志写不成功不操做,日志是顺序写,内容少,能够同步等。(最好是物理重作)。
[3]异常回滚=》物理回滚反解复杂,须要一个逻辑日志。
基于undo log又实现了MVCC
unlog等也要保证操做持久化原子化。
[4]为了删除不每次整理页,只标记,为了真正删除/undo不须要的清除=》purge
[5]flush对一个pageid屡次操做合并在一块儿减小随机操做=》二级索引非惟一change buff
[6]Flush过程当中一个页部分写成功就崩溃,没法正确后恢复=》二次写
[7]为完整的主链路。
[8]为异步的刷盘链路github

详细步骤:算法

  1. 外层 handle_connection=>do_commannd=>dispatch_command=>mysql_parse=>mysql_execute_commannd=>sql_cmd_dml::execute=> execute_inner while{对每条记录执行write_record} =>ha_write_row【返回到这里不出错记录binlog】,调用引擎table->file->ha_write_row(table->record[0])
  2. 引擎层:
    row_insert_for_mysql_using_ins_graph开始,有开启事务的操做,trx_start_low。
    首先,须要分配回滚段,由于会修改数据,就须要找地方把老版本的数据给记录下来,其次,须要经过全局事务id产生器产生一个事务id,最后,把读写事务加入到全局读写事务链表(trx_sys->rw_trx_list),把事务id加入到活跃读写事务数组中(trx_sys->descriptors)
    在InnoDB看来全部的事务在启动时候都是只读状态,只有接受到修改数据的SQL后(InnoDB接收到才行。由于在start transaction read only模式下,DML/DDL都被Serve层挡掉了)才调用trx_set_rw_mode函数把只读事务提高为读写事务。
    以后开始事务内部处理。图中所示,细节不少,先不写了。

第二章 性能

磁盘,B+树

  • 准确的B+比B为什么更适合?
    区别两点,一个是B树搜索是能够止于非页节点的,包含数据(包含数据在磁盘中页的位置),且数据只出如今树中一次。另外一点是叶子节点有双向链表。第一点使得节点能够包含更多路(由于不存数据在磁盘中页的位置,只包含下一层的指针页位置,B树这两个都要包含),层高会更少;只能到页节点搜索结束,性能稳定。第二点为了扫描和范围索引。

    clipboard.png

    clipboard.png

内存buffer

全部数据页。都走这套。包括undo等sql

name desc
buf_pool_t::page_hash page_hash用于存储已经或正在读入内存的page。根据<space_id, page_no>快速查找。当不在page hash时,才会去尝试从文件读取
buf_pool_t::LRU LRU上维持了全部从磁盘读入的数据页,该LRU上又在链表尾部开始大约3/8处将链表划分为两部分,新读入的page被加入到这个位置;当咱们设置了innodb_old_blocks_time,若两次访问page的时间超过该阀值,则将其挪动到LRU头部;这就避免了相似一次性的全表扫描操做致使buffer pool污染
buf_pool_t::free 存储了当前空闲可分配的block
buf_pool_t::flush_list 存储了被修改过的page,根据oldest_modification(即载入内存后第一次修改该page时的Redo LSN)排序
buf_pool_t::flush_rbt 在崩溃恢复阶段在flush list上创建的红黑数,用于将apply redo后的page快速的插入到flush list上,以保证其有序
buf_pool_t::unzip_LRU 压缩表上解压后的page被存储到unzip_LRU。 buf_block_t::frame存储解压后的数据,buf_block_t::page->zip.data指向原始压缩数据。
buf_pool_t::zip_free[BUF_BUDDY_SIZES_MAX] 用于管理压缩页产生的空闲碎片page。压缩页占用的内存采用buddy allocator算法进行分配。

page_hash查找。
LRU只是用于淘汰。一份block。指针保存在hash和lru上(全部的数据页)
flush_list 修改过的block被加到flush_list上,
unzip_LRU 解压的数据页被放到unzip_LRU链表上。数据库

  • cache过程:
    当一个线程请求page时,首先根据space id 和page no找到对应的buffer pool instance。而后查询page hash。若是看到page hash中已经有对应的block了,说明page已经或正在被读入buffer pool,若是io_fix为BUF_IO_READ,说明正在进行IO,就经过加X锁的方式作一次sync(buf_wait_for_read),确保IO完成。

    若是没有则表示须要从磁盘读取。在读盘前首先咱们须要为即将读入内存的数据页分配一个空闲的block。当free list上存在空闲的block时,能够直接从free list上摘取;若是没有,就须要从unzip_lru 或者 lru上驱逐page。先unzip lru。再lru是否有可替换page,直接释放,不然多是脏页多,再线程在LRU上作脏页刷新。后台线程也会按期作脏页刷新。segmentfault

    一个流程对buffer的操做步骤:
    clipboard.png数组

内存整体流程缓存

clipboard.png

索引:聚簇索引

见上B+树

二级索引 change buffer

对非惟一二级索引页,delete_mark,delete,insert顺序插入缓冲区,合并减小随机IO。

  • 物理:ibdata第4个page B+ Tree(key:spaceid,offset,counter)
  • 内存:ibuf,B+树
  • 内容: space,offset,发生change的数据
  • 写入:
    1 不会致使空page:delete时只有一条记录 拒绝
    2 不会致使分裂,insert时检查IBUF BITMAP标识剩余空间大小,超出触发merge 拒绝
  • merge:(在不少状况都须要把ibuf里的页进行合并)
    1.辅助索引页被读取到缓冲池时
    2.插入时预估page no空间不足
    3.ibuf空间不足
    4.插入ibuf可能产生ibuf Tree的索引分裂
    5.Master (IDLE ,ACTIVE,SHUTDOWN)
    ……
  • Purge操做和insert在ibuf并发问题
    在purge模式下,用ibuf同时将watch插入到hash table中,若是都在内存里,会给同一份page加锁,没问题,可是要两个线程都写入ibuf_insert时,是没办法控制顺序的(原本就容许这种无序,由于非惟一)。因此须要一个进入后,另外一个就放弃,不能都写入ibuf。
    在purge模式下,用ibuf同时将watch插入到hash table中,insert就不会再放入ibuf中了
    其余读取清除这个buf.

第三章 功能——事务

  • A undolog
  • C(一个事务中间状态可见性) MVCC
  • I (多个事物之间可见性/操做不干扰) MVCC
  • D redolog

undolog

  • 物理:回滚段,rseg0在ibdata第6个page,1~32临时表空间,33~128独立表空间或ibdata,存储在ibdata,临时表空间或单独表空间。每一个表空间能够包含若干个段。每一个段有1024个控制页slot和历史表。每一个slot对应一个undo log对象,有一个undo log header.
  • 内存:全局trx_sys->rseg_array。每一个事务trx->rsegs
  • 内容: 逻辑日志
    Insert undo日志记录插入的惟一键值的len和value。
    Update undo日志在insert undo基础上,同时记录了旧记录事务id,以及被更新字段的旧数据
  • 写入

    入口函数:btr_cur_ins_lock_and_undo
     a) 从chached_list或分配一个空闲slot建立undo页
     b) 顺序写undo log header和记录
     c) 在事务提交阶段,加入到history list或释放【见事务提交】
     Undo log的写入在一个单独的mtr中,受redo log的保护,先讲一个子事务mtr。Mtr是InnoDB对物理数据文件  操做的最小原子单元,保证持久性,用于管理对Page加锁、修改、释放、以及日志提交到公共buffer等工做。
    开启时初始化m_impl,好比mlog用于存储redo log记录
    提交时须要将本地产生的日志拷贝到公共缓冲区,将修改的脏页放到flush list上。
  • 回滚:
    入口函数:row_indo_step
    解析老版本记录,作逆向操做
  • 事务提交时undolog
    1.入口函数:trx_commit_low-->trx_write_serialisation_history
    2.事务提交总体流程(写完redo就能够提交了。)
    clipboard.png
    生成事务no。若是有update类的undo日志 。加入到purge_queue(清理垃圾),history链表(维护历史版本)
    子事务提交。Redo log写到公共缓存
    释放MVCC的readview;insert的undo日志释放(可cache重用,不然所有释放包括page页)
    刷日志

    3.在该函数中,须要将该事务包含的Undo都设置为完成状态,先设置insert undo,再设置update undo(trx_undo_set_state_at_finish),完成状态包含三种:

    若是当前的undo log只占一个page,且占用的header page大小使用不足其3/4时(TRX_UNDO_PAGE_REUSE_LIMIT),则状态设置为TRX_UNDO_CACHED,该undo对象会随后加入到undo cache list上;
       若是是Insert_undo(undo类型为TRX_UNDO_INSERT),则状态设置为TRX_UNDO_TO_FREE;
       若是不知足a和b,则代表该undo可能须要Purge线程去执行清理操做,状态设置为TRX_UNDO_TO_PURGE。

    对于undate undo须要调用trx_undo_update_cleanup进行清理操做。
    注意上面只清理了update_undo,insert_undo直到事务释放记录锁、从读写事务链表清除、以及关闭read view后才进行,
    这里的slot,undo page ,history关系:
    每一个rseg控制页有1024个slot和history。undo page释放后或者移到history list后,就能够把slot清空、undo page转为cache不释放则不动slot

  • purge:删除(更新数据的真正删除),清除过时undo。入口函数srv_do_purge
    做用: 对于用户删除的数据,InnoDB并非马上删除,而是标记一下,后台线程批量的真正删除。相似的还有InnoDB的二级索引的更新操做,不是直接对索引进行更新,而是标记一下,而后产生一条新的。这个线程就是后台的Purge线程。此外,清除过时的undo,histroy list,指的是undo不须要被用来构建以前的版本,也不须要用来回滚事务。
    咱们先来分析一下Purge Coordinator的流程。启动线程后,会进入一个大的循环,循环的终止条件是数据库关闭。在循环内部,首先是自适应的sleep,而后才会进入核心Purge逻辑。sleep时间与全局历史链表有关系,若是历史链表没有增加,且总数小于5000,则进入sleep,等待事务提交的时候被唤醒(srv_purge_coordinator_suspend)。退出循环后,也就是数据库进入关闭的流程,这个时候就须要依据参数innodb_fast_shutdown来肯定在关闭前是否须要把全部记录给清除。接下来,介绍一下核心Purge逻辑。 srv_do_purge

    clipboard.png

    1)首先依据当前的系统负载来肯定须要使用的Purge线程数(srv_do_purge),即若是压力小,只用一个Purge Cooridinator线程就能够了。若是压力大,就多唤醒几个线程一块儿作清理记录的操做。若是全局历史链表在增长,或者全局历史链表已经超过innodb_max_purge_lag,则认为压力大,须要增长处理的线程数。若是数据库处于不活跃状态(srv_check_activity),则减小处理的线程数。
    2)若是历史链表很长,超过innodb_max_purge_lag,则须要从新计算delay时间(不超过innodb_max_purge_lag_delay)。若是计算结果大于0,则在后续的DML中须要先sleep,保证不会太快产生undo(row_mysql_delay_if_needed)。
    3)从全局视图链表中,克隆最老的readview(快照、拿视图为了拿事务id.undo日志中upadte记了事务id),全部在这个readview开启以前提交的事务所产生的undo都被认为是能够清理的。克隆以后,还须要把最老视图的建立者的id加入到view->descriptors中,由于这个事务修改产生的undo,暂时还不能删除(read_view_purge_open)。
    4)从undo segment的最小堆中(堆存放每一个段未被purge的最老的undo页),找出最先提交事务的undolog(trx_purge_get_rseg_with_min_trx_id),若是undolog标记过delete_mark(表示有记录删除操做),则把先关undopage信息暂存在purge_sys_t中(trx_purge_read_undo_rec)。
    5)依据purge_sys_t中的信息,读取出相应的undo,同时把相关信息加入到任务队列中。同时更新扫描过的指针,方便后续truncate undolog。
    6)循环第4步和第5步,直到为空,或者接下到view->low_limit_no,即最老视图建立时已经提交的事务,或者已经解析的page数量超过innodb_purge_batch_size。(把delete和Undopage分别存放,detele给工做线程删除)
    7)把全部的任务都放入队列后,就能够通知全部Purge Worker线程(若是有的话)去执行记录删除操做了。删除记录的核心逻辑在函数row_purge_record_func中。有两种状况,一种是数据记录被删除了,那么须要删除全部的汇集索引和二级索引(row_purge_del_mark),另一种是二级索引被更新了(老是先删除+插入新记录),因此须要去执行清理操做。
    8)在全部提交的任务都已经被执行完后,就能够调用函数trx_purge_truncate去删除update undo(insert undo在事务提交后就被清理了)。每一个undo segment分别清理,从本身的histrory list中取出最先的一个undo,进行truncate(trx_purge_truncate_rseg_history)。truncate中,最终会调用fseg_free_page来清理磁盘上的空间。

MVCC

undo+read view 写时并发读
ReadView::id 建立该视图的事务ID;

m_ids 建立ReadView时,活跃的读写事务ID数组,有序存储;记录trx_id不在m_ids中可见
m_low_limit_id  当前最大事务ID;记录rx_id>=ReadView::m_low_limit_id,则说明该事务是建立readview以后开启的,不可见
Rem_up_limit_id ;m_ids 集合中的最小值;记录trx_id< m_up_limit_id该事务在建立ReadView时已经提交了,可见

二级索引回聚簇索引中。
若不可见,则经过undo构建老版本记录。

redolog

  • 物理:log文件,ib_logfile 覆盖写
  • 内存:log buffer log_sys(记了日志在磁盘和内存中用到的信息,好比总大小,一些须要刷盘的阈值等)
  • 内容:记录物理位置spaceid,page,offset上要操做的逻辑日志
  • 写入
    每一个子事务的操做都会写入log(mtr.m_impl.m_log中)
    mlog_open_and_write_index=》memcpy=》mlog_close
  • 提交 子事务提交写入缓冲区

提交时,准备log内容,提交到公共buffer中,并将对应的脏页加到flush list上
    Step 1: mtr_t::Command::prepare_write()
        1.若当前mtr的模式为MTR_LOG_NO_REDO 或者MTR_LOG_NONE,则获取log_sys->mutex,从函数返回
        2.若当前要写入的redo log记录的大小超过log buffer的二分之一,则去扩大log buffer,大小约为原来的两倍。
        3.持有log_sys->mutex
        4.调用函数log_margin_checkpoint_age检查本次写入:若是本次产生的redo log size的两倍超过redo log文件capacity,则打印一条错误信息;若本次写入可能覆盖检查点,还须要去强制作一次同步*chekpoint*
        5.检查本次修改的表空间是不是上次checkpoint后第一次修改(fil_names_write_if_was_clean)
         若是space->max_lsn = 0,表示自上次checkpoint后第一次修改该表空间:
            a. 修改space->max_lsn为当前log_sys->lsn;
            b. 调用fil_names_dirty_and_write将该tablespace加入到fil_system->named_spaces链表上;
            c. 调用fil_names_write写入一条类型为MLOG_FILE_NAME的日志,写入类型、spaceid, page no(0)、文件路径长度、以及文件路径名(将本次的表空间和文件信息加入到一个内存链表上 (去除恢复中对数据字典的依赖))。
            在mtr日志末尾追加一个字节的MLOG_MULTI_REC_END类型的标记,表示这是多个日志类型的mtr。
            若是不是从上一次checkpoint后第一次修改该表,则根据mtr中log的个数,或标识日志头最高位为MLOG_SINGLE_REC_FLAG,或附加一个1字节的MLOG_MULTI_REC_END日志。
            
    Step 2: 拷贝
       若日志不够,log_wait_for_space_after_reserving

    Step 3:若是本次修改产生了脏页,获取log_sys->log_flush_order_mutex,随后释放log_sys->mutex。
    
    Step 4. 将当前Mtr修改的脏页加入到flush list上,脏页上记录的lsn为当前mtr写入的结束点lsn。基于上述加锁逻辑,可以保证flush list上的脏页老是以LSN排序。
    
    Step 5. 释放log_sys->log_flush_order_mutex锁
    
    Step 6. 释放当前mtr持有的锁(主要是page latch)及分配的内存,mtr完成提交。
    • 刷盘 整个事务的提交 trx_commit. 参数innodb_flush_log_at_trx_commit

      当设置该值为1时,每次事务提交都要作一次fsync,这是最安全的配置,即便宕机也不会丢失事务
      当设置为2时,则在事务提交时只作write操做,只保证写到系统的page cache,所以实例crash不会丢失事务,但宕机则可能丢失事务
      当设置为0时,事务提交不会触发redo写操做,而是留给后台线程每秒一次的刷盘操做,所以实例crash将最多丢失1秒钟内的事务,写入一条MLOG_FILE_NAME

    • 刷脏。刷脏后调用log checkpoint把点写入(刷脏就是内存到磁盘和redo不要紧,redo写多了须要清除checkpoint写入刷脏点,以前的能够不要了),之后崩溃恢复今后点开始
      1.刷脏会在如下情形被触发

      启动和关闭时会唤醒刷脏线程
        每10s后、按如下比对落后点决定是否要刷脏。
        redo log可能覆盖写时,调用单独线程把未提交LSN对应的点放入log的checkpoint点,只是redolog写checkpoint点。如下参数控制checkpoint和flush刷脏点
            log_sys->log_group_capacity = 15461874893 (90%)
            log_sys->max_modified_age_async = 12175607164 (71%)
            log_sys->max_modified_age_sync = 13045293390 (76%)
            log_sys->max_checkpoint_age_async = 13480136503 (78%)
            log_sys->max_checkpoint_age = 13914979615 (81%)
        
        LRU LIST在未能本身释放时,先本身刷脏一页,不行再 唤醒刷脏线程

      2.刷脏线程
      clipboard.png
      innodb_page_cleaners设置为4,那么就是一个协调线程(自己也是工做线程),加3个工做线程,工做方式为生产者-消费者。工做队列长度为buffer pool instance的个数,使用一个全局slot数组表示。
      1)buf_flush_page_cleaner_coordinator协调线程

      主循环主线程以最多1s的间隔或者收到buf_flush_event事件就会触发进行一轮的刷脏。
      协调线程首先会调用pc_request()函数,这个函数的做用就是为每一个slot表明的缓冲池实例计算要刷脏多少页,
      而后把每一个slot的state设置PAGE_CLEANER_STATE_REQUESTED, 唤醒等待的工做线程。
      因为协调线程也会和工做线程同样作具体的刷脏操做,因此它在唤醒工做线程以后,会调用pc_flush_slot(),和其它的工做线程并行去作刷脏页操做。
      一但它作完本身的刷脏操做,就会调用pc_wait_finished()等待全部的工做线程完成刷脏操做。
      完成这一轮的刷脏以后,协调线程会收集一些统计信息,好比这轮刷脏所用的时间,以及对LRU和flush_list队列刷脏的页数等。
      而后会根据当前的负载计算应该sleep的时间、以及下次刷脏的页数,为下一轮的刷脏作准备。

      2)buf_flush_page_cleaner_worker工做线程

      主循环启动后就等在page_cleaner_t的is_requested事件上,
      一旦协调线程经过is_requested唤醒全部等待的工做线程,
      工做线程就调用pc_flush_slot()函数去完成刷脏动做。
      
      pc_flush_slot:
          先找到一个空间的slot,
          page_cleaner->n_slots_requested--; // 代表这个slot开始被处理,将未被处理的slot数减1 
          page_cleaner->n_slots_flushing++; //这个slot开始刷脏,将flushing加1 
          slot->state = PAGE_CLEANER_STATE_FLUSHING;
          
          刷LRU,FLUSH LIST
          
          page_cleaner->n_slots_flushing--; // 刷脏工做线程完成次轮刷脏后,将flushing减1 p
          age_cleaner->n_slots_finished++; //刷脏工做线程完成次轮刷脏后,将完成的slot加一 
          slot->state = PAGE_CLEANER_STATE_FINISHED; // 设置此slot的状态为FINISHED
          如果最后一个,os_event_set(page_cleaner->is_finished)
      pc_wait_finished:
          os_event_wait(page_cleaner->is_finished);
          统计等
      
      每次刷多少srv_max_buf_pool_modified_pct决定

      3.log_checkpoint

      clipboard.png

      入口函数为log_checkpoint,其执行流程以下:
      Step1. 持有log_sys->mutex锁,并获取buffer pool的flush list链表尾的block上的lsn,这个lsn是buffer pool中未写入数据文件的最老lsn,在该lsn以前的数据都保证已经写入了磁盘。checkpoint 点,        在crash recover重启时,会读取记录在checkpoint中的lsn信息,而后从该lsn开始扫描redo日志。
      Step 2. 调用函数fil_names_clear
          扫描fil_system->named_spaces上的fil_space_t对象,若是表空间fil_space_t->max_lsn小于当前准备作checkpoint的Lsn,则从链表上移除并将max_lsn重置为0。同时为每一个被修改的表空间构建MLOG_FILE_NAME类型的redo记录。(这一步将来可能会移除,只要跟踪第一次修改该表空间的min_lsn,而且min_lsn大于当前checkpoint的lsn,就能够忽略调用fil_names_write)
          写入一个MLOG_CHECKPOINT类型的CHECKPOINT REDO记录,并记入当前的checkpoint LSN
      Step3 . fsync 被修改的redo log文件
          更新相关变量:
          log_sys->next_checkpoint_no++
          log_sys->last_checkpoint_lsn = log_sys->next_checkpoint_lsn
      Step4. 写入checkpoint信息
          函数:log_write_checkpoint_info --> log_group_checkpoint
          checkpoint信息被写入到了第一个iblogfile的头部,但写入的文件偏移位置比较有意思,当log_sys->next_checkpoint_no为奇数时,写入到LOG_CHECKPOINT_2(3 *512字节)位置,为偶数时,写入到LOG_CHECKPOINT_1(512字节)位置。
    • 崩溃恢复

      1.从第一个iblogfile的头部定位要扫描的LSN(数据落盘点)
      2.扫描redo log
      1) 第一次redo log的扫描,主要是查找MLOG_CHECKPOINT,不进行redo log的解析,
      2) 第二次扫描是在第一次找到MLOG_CHECKPOINT(获取表和路径)基础之上进行的,该次扫描会把redo log解析到哈希表中,若是扫描完整个文件,哈希表尚未被填满,则不须要第三次扫描,直接进行recovery就结束
      3)第二次扫描把哈希表填满后,还有redo log剩余,则须要循环进行扫描,哈希表满后当即进行recovery,直到全部的redo log被apply完为止。
      3.具体redo log的恢复

      MLOG_UNDO_HDR_CREATE:解析事务ID,为其重建undo log头;
         MLOG_REC_INSERT 解析出索引信息(mlog_parse_index)和记录信息(    page_cur_parse_insert_rec)等
         在完成修复page后,须要将脏页加入到buffer pool的flush list上;查找红黑树找到合适的插入位置
         MLOG_FILE_NAME用于记录在checkpoint以后,全部被修改过的信息(space, filepath);        MLOG_CHECKPOINT用于标志MLOG_FILE_NAME的结束。
      
         在恢复过程当中,只须要打开这些ibd文件便可,固然因为space和filepath的对应关系经过redo存了下来,恢复的时候也再也不依赖数据字典。
      
         在恢复数据页的过程当中不产生新的redo 日志;
    • 二次写
      MySQL 一直使用double write buffer来解决一个page写入的partial write问题,但在linux系统上的Fusion-io Non-Volatile Memory (NVM) file system支持原子的写入。这样就能够省略掉double write buffer的使用, 5.7.4之后,若是Fusion-io devices支持atomic write,那么MySQL自动把dirty block直接写入到数据文件了。这样减小了一次内存copy和IO操做。
      redo会记spaceid,pageno,偏移量内的逻辑日志只记录:’这是一个插入操做’和’这行数据的内容‘,这是一个更新操做,更新内容(为了省地方)。可是这样就有个问题。在redo log恢复执行时,若是页逻辑时一半断电了,redo log在恢复时没法正确恢复更新操做。这就须要在脏页落盘时采起二次写。数据写入ibd前先顺序写入ibdata.在崩溃恢复时,先检验checksum.不合法载入ibdata的数据。

      clipboard.png

      Redo为了保证原子性,要求一块一写。不够的话要先读旧的而后改而后写。以512字节(最小扇区)对其方式写入,不须要二次写。设置一个值innodb_log_write_ahead_size,不须要这个过程,超过该值补0到一块直接插入
      [ps 数据须要二次写,由于可能夸多扇区,leveldb的log增长头直接跳过坏页,redo log固定大小,正常日志都是写成功才会被回放,写内存与写坏后丢只能丢失,解析错误跳过到下一块吧,问题就是要有个大小找到下一个位置]

    server与innodb的事务保证

    • server和引擎层事务的界限
      1.开启事务。server只会调用引擎层。
      server层若是不以命令,是不会显示开启事务的。在SQLCOM_BEGIN等命令会调用trans_begin 分布式事务会调trans_begin(跟踪下)
      证实是正确的,在外层trans_begin并无调用。并不研究了
      提交会在server层调用各个引擎的事务提交。
      下面说下innodb层的trx
      2.提交事务。根据是否开启binlog和是否有多个引擎执行不一样。好比开了Binlog且使用了事务引擎,用Mysql_bin_log的两阶段和组提交。若是没有用事务引擎,直接记log等就能够
      3.事务回滚:分为真正xa回滚仍是普通回滚。普通回滚调用引擎层回滚
      4.崩溃恢复:没有server层的崩溃恢复
    • 开启 分配回滚段,获取事务id,加入事务链表
    • 提交 入口: MYSQL_BIN_LOG::commit,若是是分布式事务,用xa,两阶段。prepare和commit。咱们先研究普通的提交。XA不做为重点。可是因为server层和Innodb层两个日志,须要保证顺序,也按照XA的两阶段设计。也叫内部xa
      1) xa两阶段
      Prepare

      undo log写入xid,设置状态为PREPARED

      Commit  

      Flush Stage:由leader依次为别的线程对flush redo log到LSN,再写binlog文件
      Sync Stage:若是sync_binlog计数超过配置值,以组为维度文件fsync
      Commit Stage:队列中的事务依次进行innodb commit,修改undo头的状态为完成;并释放事务锁,清理读写事务链表、readview等一系列操做,落盘redo。

      2) 缘由
      两阶段是为了保证binlog和redo log一致性。server和备库用binlog来恢复同步。innodb用undo和redo恢复。
      1落undo 2flush redo 3 flush binlog 4fsync binlog 5fsync redo [ps:sync可能只是内核缓冲放入磁盘队列,fsync只保证放入磁盘,都是同步] 6 undo D
      保证binlog若成功了,根据Undo的p结果不会回滚出现主从不一致的状况
      3) 组提交:两阶段提交,在并发时没法保证顺序一致,用ordered_commit控制
      clipboard.png
      一个On-line的backup程序新建一个slave来作replication,那么事务T1在slave机器restore MySQL数据库的时候发现未在存储引擎内提交,T1事务被roll back,此时主备数据不一致(搭建Slave时,change master to的日志偏移量记录T3在事务位置以后)。
      若是关闭binlog_order_commits。事务各自提交。这时候没有设置不写redo log。不能保证Innodb commit顺序和Binlog写入顺序一直,可是不会影响数据一致性。只是物理备份数据不一致。可是依赖于事务页上记录binlog恢复的,好比xtrabackup就会发生备份数据不一致的状况。
      每一个stage阶段都有各自的队列,使每一个session的事务进行排队。,leader控制,当一组事务在进行Commit阶段时,其余新的事务能够进行Flush阶段

    • 回滚
      两阶段:正常应该根据undo非DONE回滚,但看到undo为prepare且binlog有,就不回滚
      当因为各类缘由(例如死锁,或者显式ROLLBACK)须要将事务回滚时,ha_rollback_trans=》ha_rollback_low,进而调用InnoDB函数trx_rollback_for_mysql来回滚事务。对于innodb回滚的方式是提取undo日志,作逆向操做。
      提交失败会回滚。走的非xa,调用trx_rollback_for_mysql。原来一直纠结binlog会不会删除。。。跟踪了很久也没看出来,实际上是undo中的在提交时从新写一下binlog。这里在子事务里会介绍。

    第四章 分布式

    主从复制

    • 三种日志模式
      1.基于行的复制  row
      优势:符合幂等性,高度保障数据一致。
      缺点:数据量大

      2.基于语句的复制  statement
      优势:日志量少
      缺点:特定功能函数致使主从数据不一致,重复执行时没法保证幂等

      3.混合类型的复制  mixed  (默认语句,语句没法精准复制,则基于行)

    • 主从同步过程
      clipboard.png

      其中1. Slave 上面的IO线程链接上 Master,并请求从指定日志文件的指定位置(或者从最开始的日志)以后的日志内容;
      重放过程和master同样,也redolog

    • GTID

      MySQL 5.6引入全局事务ID的首要目的,是保证Slave在并行复制(并行顺序会乱)的时候不会重复执行相同的事务操做;用全局事务IDs代替由文件名和物理偏移量组成的复制位点(每一个日志包含GID_Sets,xx:1-100形式)。

      GTID的组成部分:

      前面是server_uuid:后面是一个串行号
       例如:server_uuid:sequence number
       7800a22c-95ae-11e4-983d-080027de205a:10
       UUID:每一个mysql实例的惟一ID,因为会传递到slave,因此也能够理解为源ID。
       Sequence number:在每台MySQL服务器上都是从1开始自增加的串行,一个数值对应一个事务。

      当事务提交时,无论是STATEMENT仍是ROW格式的binlog,都会添加一个XID_EVENT事件做为事务的结束。该事件记录了该事务的id(这个是存储引擎里的事务id,崩溃恢复时决是否提交存储引擎中状态为prepared的事务)。

    • 同步方案
      1.同步复制 所谓的同步复制,意思是master的变化,必须等待slave-1,slave-2,...,slave-n完成后才能返回。
      2.异步复制 master只须要完成本身的数据库操做便可。至于slaves是否收到二进制日志,是否完成操做,不用关心
      3.半同步复制 master只保证slaves中的一个操做成功,就返回,其余slave无论。

      这里有个不一致的问题。
       开始提交事务 =>write binlog => sync binlog => engine commit => send events =>返回 commit后崩溃,send_events失败,会致使master有slave没有,须要靠binlog同步补一下。
       开始提交事务 =>write binlog => sync binlog => send events => engine commit =>返回 send_events失败,若sync binlog未落盘,致使XA不会重作,slave领先,若binlog落盘则没有问题,可接受和单机redo同样。

      master既要负责写操做,还的维护N个线程,负担会很重。能够这样,slave-1是master的从,slave-1又是slave-2,slave-3,...的主,同时slave-1再也不负责select。slave-1将master的复制线程的负担,转移到本身的身上。这就是所谓的多级复制的概念。

    • 并行复制

      clipboard.png
      按期checkout-point将队列中执行结束的删除。记录checkpoint后每一个worker是否执行过的bitmap。崩溃恢复时执行Bitmap未执行的部分。按db分粒度大能够换成table

    扩展性

    当主库支撑不了。水平扩展。拆表。

    可靠性

    须要proxy保证

    一致性

    同步策略影响。
    XA分为外部和内部。对于外部。要应用程序或proxy做为协调者。(二阶段提交协调者判断全部prepare后commit)。对于内部,binlog控制。

    第五章 proxy功能

    • 数据库分片的合并
    • 共享式的缓存
    • 读写分离路由
    • 可靠性的保证,主从切换,故障发现与定位
    • XA一致性的实现
    • 过滤加注释

    例如:https://github.com/mariadb-co...

    相关文章
    相关标签/搜索