MHA是一位日本MySQL大牛用Perl写的一套MySQL故障切换方案,来保证数据库系统的高可用。近期,在田老师的推动下,开始一步步深入了解这个HA方案,并也计划在公司线上尝试部署。下面的东西是这段时间的学习笔记和个人理解,没有具体的实战经验,只是人为测试模拟故障的发生,通过日志来分析MHA背后的自动切换过程。

首先,介绍下它的一些特点,以及为什么用它,在哪种场合更适合用它。

1. 10-30s实现master failover(9-12s可以检测到主机故障,7-10s可以关闭主机避免SB,在用很短的时间应用差异日志)

2. 部署简单,无需对现有M-S结构做任何改动(至少3台,保证切换后仍保持M-S结构)

3. 支持手动在线切换(主机硬件维护),downtime几乎很短0.5-2s

4. 保证故障切换后多从库数据的一致性

5. 完全自动化的failover及快速复制架构恢复方案(一主多从)

6. 恢复过程包括:选择新主库、确认从库间relay log差异、新主库应用必要语句、其他从库同步差异语句、重新建立复制链接

上面是我对wiki里面信息的剪辑归纳。在实际测试中,手动切换与自动切换所需时间都能控制在他所描述的范围呢。在一主多从的情况下,当主库故障,需要提升一台从库作为新的主库,其余从库则需要重新指向新的主库建立复制,亲身过这个恢复过程的同志,应该记忆深刻,又费时又费事的(想想有3-4个从库在哪儿等着你0_0…)。可能你会说不是有这样的结构吗:M-m-S(n),大M掉了,可以马上指向小m,但是这个结构也存在致命的问题,如果是小m遇到点什么意外,后面拖家带口的S可就瞎眼了,这也是为什么大家都很渴望的一个特性(global transaction ID)出现的原因。MHA可以很好的帮我们解决从库数据的一致性问题,同时最大化挽回故障发生后的数据。

接下,我们了解下MHA方案里的两个角色。

node host:原有的MySQL复制结构中的主机,至少3台,即1主2从,当master failover后,还能保证主从结构;只需安装node包。

manager server:运行监控脚本,负责monitoring 和 auto-failover;需要安装node包和manager包。

MHA manager server可以是专门的一台机器,这样所有的业务线上的MHA都可以由其统一监控,配置文件也便于统一管理;或者为了节省机器,可以从现有复制架构中选一台“闲置”从库作为manager server,比如:某台从库不对外提供读的服务,只是作为候选主库,或是专门用于备份。

下面有价值的部分开始了,我将带着大家一步一步的分析整个failover的过程,使大家对MHA有个清晰了解,如果是我们自己的脚本又是如何去实现的呢。

背景介绍

主从结构:

10.0.1.48()

|

———- 10.0.1.37(slave1)

———- 10.0.1.38(slave2)

Sysbench主机:10.0.1.49:: master manager monitor

在sysbench压测机上持续对主库发起更新,通过关闭其中一个主库的IO_THREAD,造成个从库之间的跟新差异,最后暴力kill掉mysqld进程,引起自动master failover发生。

模拟故障

Step1: 10.0.1.49

# sysbench -mysql-host=10.0.1.48

Step2: 10.0.1.37 (1min later)

mysql> stop slave io_thead;

Step3: 10.0.1.37(around 10min later)

mysql> start slave io_thread;

Step4: 10.0.1.48

# killall -9 mysqld_safe mysqld

Failover过程分析

当master_manager监控到主库mysqld服务停止后,首先对主库进行SSH登录检查(save_binary_logs -command=test),然后对mysqld服务进行健康检查(PING(SELECT)每个3秒检查一次,持续3次),最后作出Master is down!的判断,master failover开始。

Phase 1: Configuration Check Phase..

确认主从主机状态,从库中最新的主机有哪些。

Phase 2: Dead Master Shutdown Phase..

前提是你需要指定相关的脚本,比如:master_ip_failover_script、shutdown_script,在安装包的samples/scriptes目录下。

Phase 3: Master Phase..
Phase 3.1: Getting Latest Slaves Phase..

根据从库同步主库的binlog的位置,分出latest slaves和oldest slaves。

Phase 3.2: Saving Dead Master’s Binlog Phase..

在主库上执行以下命令获得lastest slave与master间的binlog差异:

save_binary_logs -command=save -start_file=mysql-bin.000010  -start_pos=3716 -binlog_dir=/data/mha_48 -output_file=/var/log/masterha/saved_master_binlog__10.0.1.48_3306_20120326174946.binlog -handle_raw_binlog=1 -disable_log_bin=0 -manager_version=0.53

然后,通过scp将生成的差异binlog文件拷贝到monitor server上。

Phase 3.3: Determining New Master Phase..

执行如下命令,找出latest slave,并确认relay log是否全部应用,最后根据候选规则,选出新的主库(会检查是否有设置candidate_master=1和no_master=1):

apply_diff_relay_logs -command=find -latest_mlf=mysql-bin.000019 -latest_rmlp=238437084 -target_mlf=mysql-bin.000019 -target_rmlp=116056791 -server_id=1 -workdir=/var/log/masterha -timestamp=20120330124742 -manager_version=0.53 -relay_log_info=/data/mha_38/relay-log.info  -relay_dir=/data/mha_38/

Phase 3.4: New Master Diff Log Generation Phase..

新主库需要判断自己的relay log是否与latest slave有差异,产生差异relay log;之后Monitor server会通过scp将主库差异binlog拷贝到新主库上。

Phase 3.5: Master Log Apply Phase..

在新主库上应用relay log差异和主库binlog差异;最后,获得新主库的binlog文件及位置信息,并设置read_only=0。

Phase 4: Slaves Recovery Phase..
Phase 4.1: Starting Parallel Slave Diff Log Generation Phase..

判断从库与lastest slave是否存在relay log差异,在latest slave上执行如下命令,生成差异relay log文件,并通过scp拷贝到对应的从库上:

apply_diff_relay_logs -command=generate_and_send -scp_user=root -scp_host=10.0.1.37 -latest_mlf=mysql-bin.000019 -latest_rmlp=238437084 -target_mlf=mysql-bin.000019 -target_rmlp=116056791 -server_id=1 -diff_file_readtolatest=/var/log/masterha/relay_from_read_to_latest_10.0.1.37_3306_20120330124742.binlog -workdir=/var/log/masterha -timestamp=20120330124742 -handle_raw_binlog=1 -disable_log_bin=0 -manager_version=0.53 -relay_log_info=/data/mha_38/relay-log.info  -relay_dir=/data/mha_38/

Phase 4.2: Starting Parallel Slave Log Apply Phase..

首先,将monitor server上生成的binlog差异拷贝到个从库上;

然后,判断从库执行relay log位置(Exec_Master_Log_Pos)是否与已读到的relay log位置(Read_Master_Log_Pos)一致,执行以下命令获得差异文件:

save_binary_logs -command=save -start_file=relay-bin.000003  -start_pos=33701765 -output_file=/var/log/masterha/relay_from_exec_to_read_10.0.1.37_3306_20120330124742.binlog -handle_raw_binlog=1 -disable_log_bin=0 -manager_version=0.53 -relay_log_info=/data/mha_37/relay-log.info  -binlog_dir=/data/mha_37/

接下来,应用所有差异日志,同时差异日志文件合并到一个文件(total_binlog_for_10.0.1.37_3306.20120330124742.binlog)中,执行的语句及结果会被输出一个标有err的日志文件(relay_log_apply_for_10.0.1.37_3306_20120330124742_err.log)中:

apply_diff_relay_logs -command=apply -slave_user=root -slave_host=10.0.1.37 -slave_ip=10.0.1.37  -slave_port=3306

-apply_files=/var/log/masterha/relay_from_exec_to_read_10.0.1.37_3306_20120330124742.binlog, \

/var/log/masterha/relay_from_read_to_latest_10.0.1.37_3306_20120330124742.binlog, \

/var/log/masterha/saved_master_binlog_from_10.0.1.48_3306_20120330124742.binlog \

-workdir=/var/log/masterha -target_version=5.1.57-log -timestamp=20120330124742 -handle_raw_binlog=1 -disable_log_bin=0 -manager_version=0.53 -slave_pass=xxx

最后,执行reset slave,并重新CHANG MASTER。

Phase 5: New master cleanup phease..

执行reset slave操作清除之前slave信息。

以上每个阶段的分析,是根据master_manager监控脚本的输出日志分析得来,它的日志非常详细。看着日志不停的感叹,如此严谨细致的方案;从故障的反复确认,到binlog/relay日志的层层比对,多重差异日志的应用,到最后复制位置的准确选择做得每一步都是十分仔细,如果中途有意外发生会终止failover操作,并产生mha_manager.failover.error的文件,下一次必须要删除该文件才能正常failover,再或是当从库的差异日志太大落后太多(100M),默认情况会终止failover随后报错退出,除非设置check_repl_relay=0,因为它要保证快速切换,downtime不能过长。

其实,在这个方案里面还有好多好多我们只得学习的地方,比如,关于binlog/relaylog文件中的信息,还有show slave status的输出信息,想说的真是太多了,大家可以从以下两个地方获得更加详尽的介绍。

非常非常详尽的文档:http://code.google.com/p/mysql-master-ha/wiki/TableOfContents?tm=6

非常非常有料的PPT:http://www.slideshare.net/matsunobu/automated-master-failover/

测试环境:

Type           OS               Mysql

Master       rhel3.5           5.1.22-rc-log

Slave1       rhel3.5           5.1.22-rc-log

Slave2       rhel4.4           5.1.22-rc-log

切换测试过程是:1) Master down

               2) Slave1 切换成新的Master

               3) Slave2 更换Master配置为原Slave2

详细过程如下:

1、从slave(slave1)群众中选定一个slave,准备钱换成master;

2、检查slave1的复制状态:

mysql> PROCESSLIST ;
+—-+————-+———–+——+———+————+———————————————————————–+——————+
| Id | User        | Host      | db   | Command | Time       | State                                                                 | Info             |
+—-+————-+———–+——+———+————+———————————————————————–+——————+
|  1 | system user |           | NULL | Connect |      54733 | Waiting for master to send event                                      | NULL             |
|  2 | system user |           | NULL | Connect | 4294965772 | Has read all relay log; waiting for the slave thread to update it | NULL             |
|  8 | root        | localhost | NULL | Query   |          0 | NULL                                                                  | SHOW PROCESSLIST |
+—-+————-+———–+——+———+————+———————————————————————–+——————+

这里主要是检测Slave1是否已经应用完从Master读取过来的在relay log中的操作,如果未应用完不能stop slave,否则数据肯定会有丢失。

3、停止slave1的slave进程,并reset称master:

mysql> STOP SLAVE ;
Query OK, 0 rows affected (0.00 sec)

mysql> RESET MASTER;
Query OK, 0 rows affected, 8 warnings (0.02 sec)

4、将slave 群中的其他slave(slave2)的Master切换成新的master(由原slave1reset成的):

    1) 停slave进程:
      mysql> stop slave;
      Query OK, 0 rows affected (0.00 sec)
    2) 更换master:
      mysql> CHANGE MASTER TO
          ->   MASTER_HOST=’10.0.65.106′,
          ->     MASTER_USER=’repl’,
          ->     MASTER_PASSWORD=’slavepass’
          -> ;
      Query OK, 0 rows affected (0.00 sec)
    3) 开启slave:
      mysql> start slave;
      Query OK, 0 rows affected (0.00 sec)

5、检查slave的状态:

mysql> show slave status \G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 10.0.65.106
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 106
               Relay_Log_File: oindeve-relay-bin.000002
                Relay_Log_Pos: 251
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 106
              Relay_Log_Space: 408
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master__Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
1 row in (0.00 sec)

6、最后变换实际数据监测新的复制是否能成功进行:

    1) 在新的Master创建测试表:

      mysql> table t3 as * from test;
      Query OK, 100003 rows affected (0.11 sec)
      Records: 100003  Duplicates: 0  Warnings: 0

      mysql> show tables;
      +—————-+
      | Tables_in_test |
      +—————-+
      | t1             |
      | t2             |
      | t3             |
      | test           |
      +—————-+
      4 rows in set (0.00 sec)

      mysql> select count(*) from t3;
      +———-+
      | count(*) |
      +———-+
      |   100003 |
      +———-+
      1 row in set (0.00 sec)
    2) 在slave2上面看是否已经复制过去:

      mysql> show tables;
      +—————-+
      | Tables_in_test |
      +—————-+
      | t1             |
      | t2             |
      | t3             |
      | test           |
      +—————-+
      4 rows in set (0.00 sec)

      mysql> select count(*) from t3;
      +———-+
      | count(*) |
      +———-+
      |   100003 |
      +———-+
      1 row in set (0.00 sec)

7、到此确认复制正常,master切换完成。

注:整个切换测试过程时间较短,大家从每一步操作后面反馈的执行时间也可以看出,所需要的操作步骤也不太多。但是此测试并不是数据库整体环境在提供服务的压力环境下所测试进行的,该过程数据库整体环境较为空闲,所以目前还不敢确保在压力环境下也会如此的顺利,等后面还会找机会在有应用压力的情况下再测试。

提到MySQL高可用性,很多人会想到MySQL Cluster,亦或者Heartbeat+DRBD,不过这些方案的复杂性常常让人望而却步,与之相对,利用MySQL复制实现高可用性则显得容易很多,目前大致有MMMPRMMHA等方案可供选择:MMM是最常见的方案,可惜它带来的问题往往比解决的问题还多(What’s wrong with MMM?);至于PRM,它还是个新项目,暂时不推荐用于产品环境,不过作为Percona的作品,它值得期待;如此看来目前只能选MHA了,好在经过DeNA大规模的实践应用证明它是个靠谱的工具。

安装:

作为前提条件,应先配置MySQL复制,并设置SSH公钥免密码登录。下面以CentOS为例来说明,最好先安装EPEL,不然YUM可能找不到某些软件包。

MHA由Node和Manager组成,Node运行在每一台MySQL服务器上,也就是说,不管是MySQL主服务器,还是MySQL从服务器,都要安装Node,而Manager通常运行在独立的服务器上,但如果硬件资源吃紧,也可以用一台MySQL从服务器来兼职Manager的角色。

安装Node:

shell> yum install perl-DBD-MySQL
shell> rpm -Uvh ://mysql-master-ha.googlecode.com/files/mha4mysql-node-0.52-0.noarch.rpm

安装Manager:

shell> yum install perl-DBD-MySQL
shell> yum install perl-Config-Tiny
shell> yum install perl-Log-Dispatch
shell> yum install perl-Parallel-ForkManager
shell> rpm -Uvh http://mysql-master-ha.googlecode.com/files/mha4mysql-node-0.52-0.noarch.rpm
shell> rpm -Uvh http://mysql-master-ha.googlecode.com/files/mha4mysql-manager-0.52-0.noarch.rpm

配置:

配置全局设置:

shell> cat /etc/masterha_default.cnf
[server default]
user=...
password=...
ssh_user=...

配置应用设置:

shell> cat /etc/masterha_application.cnf
[server_1]
hostname=...

[server_2]
hostname=...

注:MHA配置文件中参数的详细介绍请参考官方文档

检查

检查MySQL复制:

shell> masterha_check_repl --conf=/etc/masterha_application.cnf

检查SSH公钥免密码登录:

shell> masterha_check_ssh --conf=/etc/masterha_application.cnf

实战

首先启动MHA进程:

shell> masterha_manager --conf=/etc/masterha_application.cnf

注:视配置情况而定,可能会提示read_only,relay_log_purge等警告信息。

然后检查MHA状态:

shell> masterha_check_status --conf=/etc/masterha_application.cnf

注:如果正常,会显示『PING_OK』,否则会显示『NOT_RUNNING』。

到此为止,一个基本的MHA例子就能正常运转了,不过一旦当前的MySQL主服务器发生故障,MHA把某台MySQL从服务器提升为新的MySQL主服务器后,如何通知应用呢?这就需要在配置文件里加上如下两个参数:

说到Failover,通常有两种方式:一种是虚拟IP地址,一种是全局配置文件。MHA并没有限定使用哪一种方式,而是让用户自己选择,虚拟IP地址的方式会牵扯到其它的软件,这里就不赘述了,以下简单说说全局配置文件,以PHP为实现语言,代码如下:

#!/usr/bin/env 
<?
$longopts = array(
    'command:',
    'ssh_user:',
    'orig_master_:',
    'orig_master_ip:',
    'orig_master_:',
    'new_master_host::',
    'new_master_ip::',
    'new_master_::',
);

$options = getopt(null, $longopts);

if ($options['command'] == 'start') {
    $params = array(
        'ip'   => $options['new_master_ip'],
        '' => $options['new_master_'],
    );

    $string = '<? return ' . var_export($params, true) . '; ?>';

    file_put_contents('config.', $string, LOCK_EX);
}

exit(0);
?>

注:用其它语言实现这个脚本也是OK的,最后别忘了给脚本加上可执行属性。

如果要测试效果的话,可以kill掉当前的MySQL主服务器,稍等片刻,MHA就会把某台MySQL从服务器提升为新的MySQL主服务器,并调用master_ip_failover_script脚本,如上所示,我们在master_ip_failover_script脚本里可以把新的MySQL主服务器的ip和port信息持久化到配置文件里,这样应用就可以使用新的配置了。

有时候需要手动切换MySQL主服务器,可以使用masterha_master_switch命令,不过它调用的不是master_ip_failover_script脚本,而是master_ip_online_change_script脚本,但调用参数类似,脚本可以互用。

shell> masterha_master_switch --conf=/etc/masterha_application.cnf --master_state=dead --dead_master_host=...
shell> masterha_master_switch --conf=/etc/masterha_application.cnf --master_state=alive --new_master_host=...

注:针对原来的MySQL主服务器是否已经宕机,执行命令所需的参数有所不同。

需要说明的是,如果MHA检测到MySQL主服务器连续发生宕机,且两次宕机时间间隔不足八小时的话,则不会进行Failover,之所以这样限制是为了避免ping-pong效应。不过为了自动化,我们往往希望能取消这种限制,此时可以用如下方式启动Manager:

shell> nohup masterha_manager --conf=/etc/masterha_application.cnf --ignore_last_failover --remove_dead_master_conf &

注:请确保Manager的运行用户对masterha_application.cnf有写权限。

本文只是MHA的一个简要介绍,至于详细说明,建议大家阅读官方文档

测试环境:rhel3.5, 8.0.7,5.0.51-rc-log

1、首先从www.drbd.org下载了源代码包(我下载的8.0.7版本的包)

2、检查主机上面有没有linux的内核源代码,如果没有,需要找到相对应版本的源代码包安装上去。

3、开始安装drbd:

    1) 解压:tar -zxvf drbd-8.0.7.tar.gz

    2) 进入drbd源码目录,根据kernel源码位置来编译drbd:

    make KDIR=/usr/src/kernel/    (如果没有更改过内核,可以直接运行make,编译程序会到/lib/module里面去自己根据相关配置寻找到kernel源码)

    make install

    如果没有报错,应该基本install好了,检查是否生成了相应的文件:/etc/drbd.conf ; /etc/init.d/drbd ; 以及./drbd/drbd.ko

    同时系统应该至少多了以下两个命令:drbdadm和drbdsetup

    不要删除此源码目录,后面还会用到里面的./scripts/drbd.conf 和 ./drbd/drbd.ko

4、现在可以加载安装drbd模块了

    insmod drbd.ko 或者 modprobe drbd

    通过lsmod检查是否已经成功

    #lsmod |grep drbd

    如果有,则表示成功了

5、更改drbd配置文件:

    /etc/drbd.conf

    [root@mysql1 ha.d]# cat /etc/drbd.conf

    …

    on mysql1 {
      device     /dev/drbd0;
      disk       /dev/i2o/hda9;
      address    10.0.65.45:8888;
      flexible-meta-disk  internal;
    }

    on mysql2 {
      device    /dev/drbd0;
      disk      /dev/sde2;
      address   10.0.65.106:8888;
      meta-disk internal;
    }

    …

6、primary node设置:

    1) 创建matadata:

    #drbdadm create-md all

    2) 启动drbd:

    #/etc/init.d/drbd start

    3) 设置为主节点:

    #drbdadm — –overwrite-data-of-peer primary all

    4) 在新设备上面创建文件系统

    #mkfs.ext3 /dev/drbd0

    5) 将文件系统mount上

    #mkdir /drbddata

    #mount /dev/drbd0 /drbddata

7、secondary node设置:

    1) 创建matadata:

    #drbdadm create-md all

    2) 启动drbd:

    #/etc/init.d/drbd start

    注:这里不要创建文件系统(因为这些信息都会从主节点同步过来的)。

8、primary和secondary节点都配置完并且都启动后,开始检查配置是否成功

    1) 检查进程:

      a) primary :
      [root@mysql1 /]# ps -auxf |grep drbd
      Warning: bad syntax, perhaps a bogus ‘-’? See /usr/share/doc/procps-3.2.3/FAQ
      root      5454  0.0  0.0  3744  672 pts/0    S+   17:36   0:00          \_ grep drbd
      root      5389  0.6  0.0     0    0 ?        S    17:16   0:07 [drbd0_worker]
      root      5403  1.1  0.0     0    0 ?        S    17:16   0:14 [drbd0_receiver]
      root      5448  0.3  0.0     0    0 ?        S    17:35   0:00 [drbd0_asender]

      b) secondary:
root@mysql2:/>ps -auxf |grep drbd
      Warning: bad syntax, perhaps a bogus ‘-’? See /usr/share/doc/procps-3.2.3/FAQ
      root      5272  0.0  0.0  4752  640 pts/1    S+   16:27   0:00          \_ grep drbd
      root      5168  0.0  0.0     0    0 ?        S    16:07   0:00 [drbd0_worker]
      root      5182  2.3  0.0     0    0 ?        S    16:07   0:29 [drbd0_receiver]
      root      5270  1.9  0.0     0    0 ?        S    16:25   0:03 [drbd0_asender]

      可以看到两个节点的进程都起来了,每个drbd设备会有三个进程:drbd0_worker是drbd0的主要进城,drbd0_asender是primary上drbd0的数据发送进程,drbd0_receiver是secondary上drbd0的数据接收进程。

    2) 查看/dev/drbd文件的输出:

      [root@mysql1 /]# cat /proc/drbd
      version: 8.0.7 (api:86/proto:86)
      GIT-: cf14288833afe95db396075f8530a5960d29e498 build by root@mysql1, 2008-03-01 17:02:58
       0: cs:SyncSource st:Primary/Secondary ds:UpToDate/Inconsistent r—
          ns:6575584 nr:36 dw:289636 dr:6298278 al:142 bm:515 lo:1 pe:227 ua:30 ap:0
              [====>...............] sync’ed: 21.8% (9336/11932)M
              finish: 0:52:36 speed: 3,024 (4,176) K/sec
              resync: used:1/31 hits:454805 misses:514 starving:0 dirty:0 changed:514
              act_log: used:0/257 hits:72258 misses:154 starving:0 dirty:12 changed:142

root@mysql2:/>cat /proc/drbd
      version: 8.0.7 (api:86/proto:86)
      GIT-hash: cf14288833afe95db396075f8530a5960d29e498 build by root@mysql2, 2008-02-29 21:21:46
       0: cs:SyncTarget st:Secondary/Primary ds:Inconsistent/UpToDate C r—
          ns:36 nr:5550548 dw:5550552 dr:89 al:2 bm:326 lo:2 pe:97 ua:1 ap:0
              [==>.................] sync’ed: 13.5% (10330/11932)M
              finish: 0:28:37 speed: 6,148 (4,248) K/sec
              resync: used:2/31 hits:391148 misses:451 starving:0 dirty:0 changed:451
              act_log: used:0/257 hits:7 misses:2 starving:0 dirty:0 changed:2

        输出文件上面最开始是drbd的版本信息,然后就是数据同步的一些状态信息,从mysql的文档上介绍了每一个状态的意思如下:

      cs — connection state
      st — node state (local/remote)
      ld — local data consistency
      ds — data consistency
      ns — network send
      nr — network receive
      dw — disk write
      dr — disk read
      pe — pending (waiting for ack)
      ua — unack’d (still need to send ack)
      al — access log write count

    3) 更进一步验证数据是否同步正确了:

        a) 在mysql1上将该文件系统umount,然后执行drbdadm secondary all,改成secondary模式

        [root@mysql1 /]# umount /drbddata

        [root@mysql1 /]# drbdadm secondary all

        b) 在原mysql2 执行drbdadm primary all 改成primary模式,再mount文件系统

root@mysql2:/>drbdadm  primary all

root@mysql2:/>mount /dev/drbd0 /drbddata

        检查之前在mysql1下写入的文件是否已经完全同步到mysql2下面,经验证,数据已经同步好了

    4) 最后连同mysql一起手工测试一次切换:

a) 主节点mysql1下关闭mysql,释放资源,将资源切换成secondary模式:

      [root@mysql1 ha.d]# mysqladmin -u root shutdown
      [root@mysql1 ha.d]# umount /drbddata
      [root@mysql1 ha.d]# drbdadm secondary all

b) 次节点mysql2下获取资源,mount上,切换成primary模式,并启动mysql:

root@mysql2:/root/mysql-5.0.51a>drbdadm primary all
root@mysql2:/root/mysql-5.0.51a>mount /dev/drbd0 /drbddata
root@mysql2:/usr/local/mysql/bin>./mysqld_safe –user=mysql &
      [1] 27900
root@mysql2:/usr/local/mysql/bin>Starting mysqld daemon with databases /drbddata/mysqldata
root@mysql2:/usr/local/mysql/bin>
root@mysql2:/usr/local/mysql/bin>
root@mysql2:/usr/local/mysql/bin>tail -f /drbddata/mysqldata/mysql2.err
      080303 13:53:25  mysqld started
      080303 13:53:26  InnoDB: Started; log sequence number 0 43656
      080303 13:53:26 [Note] /usr/local/mysql/libexec/mysqld: ready for connections.
      Version: ‘5.0.51a-log’  socket: ‘/usr/local/mysql/sock/mysql.sock’  port: 3306  Source distribution

c) 登入数据库中检查数据,这里我同时在新的主节点上面测试了写,以便在后面切换回主节点后检查数据是否也正常:

root@mysql2:/usr/local/mysql/bin>mysql -u root
      Welcome to the MySQL monitor.  Commands end with ; or \g.
      Your MySQL connection id is 1 to version: 5.0.51a-log

      Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the buffer.

      mysql> show databases;
      +——————–+
      | Database           |
      +——————–+
      | information_schema |
      | mysql              |
      | test               |
      +——————–+
      3 rows in set (0.03 sec)

      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> show tables;
      +—————-+
      | Tables_in_test |
      +—————-+
      | t1             |
      | t2             |
      | t3             |
      +—————-+
      3 rows in set (0.00 sec)

      mysql> create table t4(id int);
      Query OK, 0 rows affected (0.07 sec)

      mysql> exit
      Bye
root@mysql2:/usr/local/mysql/bin>

d) 再通过之前相同的切换步骤切换回各自原来的模式,启动主节点的mysql,并检查数据:

[root@mysql1 ha.d]# drbdadm primary all

[root@mysql1 ha.d]# mount /dev/drbd0 /drbddata

      [root@mysql1 ha.d]# mysqld_safe –user=mysql &
      [1] 8451
      [root@mysql1 ha.d]# Starting mysqld daemon with databases from /drbddata/mysqldata

      [root@mysql1 ha.d]#
      [root@mysql1 ha.d]#
      [root@mysql1 ha.d]#
      [root@mysql1 ha.d]#
      [root@mysql1 ha.d]# mysql -uroot
      Welcome to the MySQL monitor.  Commands end with ; or \g.
      Your MySQL connection id is 1 to server version: 5.0.51a-log

      Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the buffer.

      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> show tables;
      +—————-+
      | Tables_in_test |
      +—————-+
      | t1             |
      | t2             |
      | t3             |
      | t4             |
      +—————-+
      4 rows in set (0.00 sec)

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

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

      mysql> * from t4;
      +——+
      | id   |
      +——+
      |  111 |
      +——+
      1 row in set (0.00 sec)

几个注意点:

1、除了primary节点的文件系统是手工创建的之外,其他的所有secondary节点的文件系统都是通过同步完成的,两边的设备大小好像并不要求完全一样的大小,在我测试的过程中,两边用于drbd设备的分区大小相差很大,但好像并没有出现错误,从drbd相关文档上看到这个并不会导致问题。但是如果主节点比备节点大,而且用的空间也超出了备节点空间大小会有问题,具体会怎样还没有测试到。

2、在mount drbd的设备之前,该节点必须已经设置为primary模式,而且如果一边没有umount,另一边是无法mount上的,直接mount会报如下错误:

root@mysql2:/>mount /dev/drbd0 /drbddata
      mount: block device /dev/drbd0 is write-protected, mounting read-only

  也就是说,在同步的过程中,只有一个节点的数据可见,也就是不能通过我们人直接简单的查看数据文件等方式之间验证数据是否正常。不过看文档说好像可以通过手动设置一些信息来查看是否正常(You can manually set this information for a number of reasons, including when you want to check the physical of the secondary device (since you cannot mount a DRBD device in primary mode), or when you are temporarily moving the responsibility of keeping the data in check to a different machine (for example, during an upgrade or physical move of the normal primary node). )。这里的内容等后面再详细研究一下。

接上,我们继续来探讨数据一致性这个问题。

谈到数据完整性和数据可用性问题的第二个方法。

简单回溯上一次的异步模式。

好处是性能,坏处是丢数据。但其实不会丢很多,所以大部分业务是可以接受一年丢几条记录这种case的。所以这也是大部分业务主要在跑的模型。

那么,对于一些对数据丢失case要求较高的业务,有什么办法能够保持数据的绝对安全呢?

这就要涉及到第一个关键的技术了:两段提交协议。

两段提交协议,其实是个非常简单的协议,有两种主要的子实现,能够分别的针对两种不同的场景,当年我还是花了很久的时间才把这些事情联系到一起的,所以我也希望能够在这里把看似完全不一样的东西联系到一起,让后面的人少走弯路 : )

需求:保证两个节点能够保证,能够拥有同一个状态。

下面来做一些分析,从最简单的开始,看看最小需求是什么:

我们假定有两个节点:A节点和B节点,他们之间要保证数据的一致,所谓一致,就是A:0的时候,b的那个数据最终也会变为B。请注意“最终”两字,很关键,后面分析的时候也会深入探讨。

“同一个状态”,如果要保证这一点,最简单的做法,就是,写A以后,再去写B. 用简化的语言来说,就是Commit A -> Commit B.

这时候需要面临以下几个问题,(我们在这里只丢问题,和解决路径,不会展开,因为展开会导致整个逻辑结构过于复杂)

一.如果有两个client,他们都要更新A节点和B节点,那么这时候就面临了一个问题,他们的更新顺序可能是完全不同的。举个例子,Client1 发送了一个数据的版本0给A和B.而Client 2 发送了一个数据的版本0’给B和A,很容易可以想到,假定数据在同一个时间更新,那么这两个更新的版本到达A和B的时间可能是不同的,这就导致一些不可预估的错误,如A是0版本的数据,而B则是0’版本的数据。这种情况肯定是做不到一致性的。

为了解决这个问题,有无数种模型。。。他们主要是:

第一种:有一台机器是主机,其他机器都从唯一的主机上拉取已经被排好序的数据。

第二种:vector clock.维持多个版本,利用多版本来判断数据是否发生了冲突。

第三种:维持全局时间序,出现冲突的时候,last write win.

第四种:使用事务提交方式,利用锁来防止出现这种冲突。

第五种:使用MVCC的方式,申请全局序号,一切数据更新的先后顺序关系,以全局序号为准。

二.Commit A后,如果发生网络问题或者其他问题,数据还没有更新到B的时候,A挂了,并且不幸的是,A挂之前,A里面的最新版本的数据还被外部用户读到了,这时候还是会悲剧的。想想吧,某日你中了500w ,人家把钱打到了你银行的户头,然后那台机器出现了上面说道的这种问题,你在第一天还能看到自己的500w..下一次刷新的时候却没了。。。

为了解决这个问题,我们就需要引入一个两段提交的优化版本,来处理这个问题了,在以前的文章中,我们其实已经讲过两段提交了。

《海量存储系列之六》

两段提交的核心,其实是在A和B更新的时候,对A和B都加一把锁,让其他人不能看到中间状态过程,然后,只有在全部提交后,才算整个事务的提交成功。流程是

Prepare A (lock)->Prepare B (lock) ->commit A(unlock) -> commit B(unlock)

         但在所有场景中都使用以上的流程,实际上是很高消耗的了。比如:

“第一种:有一台机器是主机,其他机器都从唯一的主机上拉取已经被排好序的数据。”

在这种场景中,实际上A和B机器是不会产生数据冲突的,所以四步的流程明显会变得很重。解决这个问题的方法是,在不会出现数据冲突的地方,可以对两段提交进行一次优化,去掉一次prepare ,对应在上面流程,就是去掉Prepare B(lock)这个过程,将流程简化为 PA-> CB -> CA。

好了,赶紧收回来。。不能在这里再次深入下去了,原因是,每个东西展开都能写个千把字,而且因素与因素相互关系相互影响,不是简单地树形结构,从这里展开说出去,这样会让整个文章结构出现问题。

那么,写了上面的这些东西,核心的地方是希望能够引出以下的几个关键的因素,他们的true or false 直接决定了很多种不同的存储产品的核心不同点哦 icon smile 海量存储系列十五

一,是否有主机概念?

二,是否要保证数据不丢?

三,读取是否要求高可用?

四,延迟要求?

五,写入是否要求高可用?

明眼人一看,这是啥?哈哈 CAP的变种么不是,没错的,CAP就是描述这类现象的一个英文缩写而已…

回到同步模式:为了解决CA->CB中出现的问题,我们就要演进我们的架构,到第二级别:

先来讨论有主机结构的,保证数据不丢的方案,目前这种有主机结构的是最为主流的一种模式,使用的人,就我所知有:zoo keeper, mongoDB , bdb edition  etc.在淘宝内部也有一些人在研究让mysql的semi-replication也能做到类似效果,这里我们期待他们的成果。

在这类解决方案中,主要的做法就是利用两段提交协议的优化版本:PA-> CB -> CA的方案,来实现A和B的强一致的。下面我们针对这一流程进行一些分析。

在PA阶段,数据只对提出修改的人可见,对外部其他用户不可见(所使用的方式主要是copy-on-write 或者直接用锁),所以这时候出现问题,不会对其他人造成影响的,直接回滚即可。

在CB阶段,如果数据更新成功,那么等于事务提交成功,因为如果这时候A机器挂掉,那么B的数据是可以直接可见的。而如果A没有挂掉,这时候A还在PA阶段呢,所以其他人看不见改动,而发起改动的人也没有获取commit 成功的信号。那么如果B挂掉,这时候是有问题的,A的策略只有两种,一类是等待直到成功,一类是等待超时后返回。

请各位看客们注意了,这就是两段提交协议最大的问题所在了。无论是一直等待,直到成功,还是等待超时后返回,其实都不安全。如果一直等,那么前端也在一直等,整个业务流程实际上是终止掉的,想想吧,如果您急等着用钱买17架波音呢,但这时候银行提示您,请等待付款,无论付多少次都是同一个提示的时候,您的想法是?。。

所以一般的做法都是等待超时后返回,但这样也带来问题,就是,在这时候机器其实是没有被保护的,存在着理论上丢数据的可能性。但比事务性(也就是pa,pb,ca,cb流程)的两段提交来说,有一个好处是数据的恢复相对的容易,因为只需要从主机拉最新数据就可以了。

为了解决两段提交协议在传输过程中出现的B挂掉导致的问题,牛人们就又开始新一轮的研究竞赛,其中呢,有个人在90年的时候提出了一种算法,说是能解决类似问题,但当时没有获得什么影响力,然后他就写了一篇论文,里面描述了一个有一群议员在只有信使进行通知的时候,能够对法令保持一致的理想国。不幸的是,这篇论文因为其晦涩也被丢到了垃圾桶,索性有明眼人发现了其中的价值。又把它找了回来,这论文描述的就是PAXOS,而这悲催的哥们就是Lamport ..

好,我们今天以Paxos作为本次文章的结尾吧。

在两段提交协议里面,存在着B机器挂掉导致等待和不安全的问题,那么有没有一种算法,可以保证在任何情况下,数据都可以读写,并且在任何情况下,数据的安全性都可以通过加入更多的机器的方式来提升安全级别呢?(比如,如果用了某种算法,那么就算地球被炸了,只要月球上有机器,数据就不会丢?:)

有的,PAXOS or fast PAXOS

但PAXOS论文很难懂,我们不用他来说明这个问题,我们以zoo keeper的实现思路,用更简单的话语来尝试说明什么叫zoo keeper.

Fast paxos里面有以下几个关键的组成部分:

1.      两段提交协议

这是基石,当然是使用简化版本的,也就是PA-CB-CA的版本的。这是我们能够保证两个机器数据强一致的唯一方法。

2.      Paxos算法核心– Leaderelection

Paxos算法的目标,其实就是为了解决任意机器都会挂掉,如何能用一种自动的方式,在挂掉任何几个机器的时候,都能够自动的从剩余的机器里面找到没有任何数据丢失的那几台复制的样板儿,并且以样板儿里面的一台,将整个集群的数据进行恢复,并且不影响前面用户的正常读写。

那我们还是回到两段提交,如果B 没有响应了,如何能够让A正常返回成功呢?两台机器的情况下肯定做不出来,但有一点好的地方,我可以加机器。

如果我们把机器加到三台A,B,C,那么其实我就可以很容易的发现,B没返回?那C返回给A说成功,A也是可以返回的。

但,到底多少个返回成功才算数呢?这就是paxos的核心了。简单来说,N个里面如果有超过半数返回成功,那么就应该算是成功的了,注意哦,是加了自己的。

如,三个里面两个有最新数据,那么任意一个挂掉,数据不会丢失。

五个里面有三个返回最新数据,那么任意两个挂掉,数据不会丢失.etc…

那么在主节点挂掉的时候,请求应该被提交到哪里呢?很简单,因为更新只发生在主节点,由主节点生成一个全局的id号,让其他节点去学习这些带id号的数据更新。那么,假定有五个节点,任意两个挂掉,哪怕挂掉的里面有主节点,这时候也只需要从新从余下的三个里面选出那个id号最大的节点作为主节点,数据就一定是最新的。

当然,leader election是paxos协议的核心,对这块感兴趣的,可以具体看一下lamport的论文。

这就是paxos..简单来说,可以保证一直提供服务,集群任意节点宕机都不会影响数据的可用性。

可惜,代价是性能。。两段提交协议需要多次网络交互,所以延迟较高,就我们的测试来说,200ms一次提交总还是要的,你可以优化,但在当前网络结构下,延迟就是最大的祸首,CAP的之所以成为定律,原因其实就是网络延迟。。

擦汗,这篇文章我写了4个小时。3500字。。是我写的最累的一次,真希望有块白板,给大家简单的画一下草图,20分钟就能讲明白。。这里码字用了4小时。。

Anyway,这块是个非常复杂但非常好玩的领域,如果您不觉得厌烦,那请继续follow我,下一期让我们来看一下无主机结构的其他解决数据冲突和可用性的方案吧。

Redis官方路线图来看,大概会在Redis3.0左右正式支持Cluster。不过即便是乐观的估计,至少也得等几个月的时间,为了让我的应用在这段时间内能保持高可用性,我以主从服务器为基础实现了一个Failover过渡方案。

从理论上解释,一旦主服务器下线,可以在从服务器里挑选出新的主服务器,同时重新设置主从关系,并且当下线服务器重新上线后能自动加入到主从关系中去,内容如下:

<?

class RedisFailover
{
    public $config = array();
    public $map    = array();

    const CONFIG_FILE = 'config.';
    const MAP_FILE    = 'map.';

    public function __construct()
    {
        $config = include self::CONFIG_FILE;

        foreach ((array)$config as $name => $nodes) {
            foreach ($nodes as $node) {
                $node = new RedisNode($node['host'], $node['port']);

                if ($node->isValid()) {
                    $this->config[$name][] = $node;
                }
            }

            if (empty($this->config[$name])) {
                throw new Exception('Invalid config.');
            }

            $this->map[$name] = $this->config[$name][0];
        }

        if (file_exists(self::MAP_FILE)) {
            $map = include self::MAP_FILE;

            foreach ((array)$map as $name => $node) {
                $node = new RedisNode($node['host'], $node['port']);

                $this->map[$name] = $node;
            }
        }
    }

    public function run()
    {
        $set_nodes_master = function($nodes, $master) {
            foreach ($nodes as $node) {
                $node->setMaster($master->host, $master->port);
            }
        };

        foreach ($this->config as $name => $nodes) {
            $is_master_valid = false;

            foreach ($nodes as $node) {
                if ($node == $this->map[$name]) {
                    $is_master_valid = true;

                    break;
                }
            }

            if ($is_master_valid) {
                $set_nodes_master($nodes, $this->map[$name]);

                continue;
            }

            foreach ($nodes as $node) {
                $master = $node->getMaster();

                if (empty($master)) {
                    continue;
                }

                if ($master['master_host'] != $this->map[$name]->host) {
                    continue;
                }

                if ($master['master_port'] != $this->map[$name]->port) {
                    continue;
                }

                if ($master['master_sync_in_progress']) {
                    continue;
                }

                $node->clearMaster();

                $set_nodes_master($nodes, $node);

                $this->map[$name] = $node;

                break;
            }
        }

        $map = array();

        foreach ($this->map as $name => $node) {
            $map[$name] = array(
                'host' => $node->host, 'port' => $node->port
            );
        }

        $content = '<? return ' . var_export($map, true) . '; ?>';

        file_put_contents(self::MAP_FILE, $content);
    }
}

class RedisNode
{
    public $host;
    public $port;

    const CLI = '/usr/local/bin/redis-cli';

    public function __construct($host, $port)
    {
        $this->host = $host;
        $this->port = $port;
    }

    public function setMaster($host, $port)
    {
        if ($this->host != $host || $this->port != $port) {
            return $this->execute("SLAVEOF {$host} {$port}") == 'OK';
        }

        return false;
    }

    public function getMaster()
    {
        $result = array();

        $this->execute('INFO', $rows);

        foreach ($rows as $row) {
            if (preg_match('/^master_/', $row)) {
                list($key, $value) = explode(':', $row);

                $result[$key] = $value;
            }
        }

        return $result;
    }

    public function clearMaster()
    {
        return $this->execute('SLAVEOF NO ONE') == 'OK';
    }

    public function isValid()
    {
        return $this->execute('PING') == 'PONG';
    }

    public function execute($command, &$output = null)
    {
        return exec(
            self::CLI . " -h {$this->host} -p {$this->port} {$command}", $output
        );
    }
}

?>

其中提到了两个文件,先说一下config.php

<?php

return array(
    'redis_foo' => array(
        array('host' => '192.168.0.1', 'port' => '6379'),
        array('host' => '192.168.0.2', 'port' => '6379'),
        array('host' => '192.168.0.3', 'port' => '6379'),
    ),
);

?>

说明:每个别名对应一组服务器,在这组服务器中,有一个是主服务器,其余都是从服务器,主从关系不要在配置文件里硬编码,而应该通过SLAVEOF命令动态设定。

再说一下map.php文件,内容如下:

<?php

return array (
    'redis_foo' => array (
        'host' => '192.168.0.1', 'port' => '6379'
    ),
);

?>

说明:别名对应的是当前有效的服务器。需要注意的是这个文件是自动生成的!程序在使用Redis的时候,都配置成别名的形式,具体的host,port通过此文件映射获得。

明白了以上代码之后,运行就很简单了:

<?php

$failover = new RedisFailover();
$failover->run();

?>

说明:实际部署时,最严格的方式是以守护进程的方式来执行,不过如果要求不是很苛刻的话,CRON就够了。测试时可以手动杀掉主服务器进程,再通过INFO查看效果。

再补充一些命令行用法的相关说明,本文都是使用redis-cli来发送命令的,通常这也是最佳选择,不过如果因为某些原因不能使用redis-cli的话,也可以使用nc(netcat)命令按照Redis协议实现一个简单的客户端工具,比如说PING命令可以这样实现:

shell> (echo -en "PING\r\n"; sleep 1) | nc localhost 6379

说明:之所以需要sleep一下是因为Redis的请求响应机制是Pipelining方式的。

既然说到这里了,就再唠十块钱儿的,通常,我们可以使用telnet命令和服务交互,但是telnet有一点非常不爽的是命令行不支持上下键历史,还好可以借助rlwrap来达成这个目的,视操作系统,可以很容易的用APT或YUM来安装,运行也很简单:

shell> rlwrap telnet localhost 6379

说明:通过使用rlwrap,不仅支持上下键历史,而且连Ctrl+r搜索也一并支持了,强!

在Redis Cluster释出前,希望这个脚本能帮到你,其实其他的服务也可以使用类似的方案,比如MySQL,不过复杂性会加大很多,好在已经有类似MHA之类的方案了。

1、下载对应版本的heartbeat包     由于安装beartbeat的rpm包需要其他一些包为前提条件,所以可能还需要下载对应版本的其他的几个rpm包,像如下:     [root@mysql1 heartbeat]# rpm -ivh heartbeat-2.1.3-3.el4.centos.i386.rpm     warning: heartbeat-2.1.3-3.el4.centos.i386.rpm: V3 DSA signature: NOKEY, key ID 443e1821     error: Failed dependencies:             heartbeat-pils = 2.1.3-3.el4.centos is needed by heartbeat-2.1.3-3.el4.centos.i386             heartbeat-stonith = 2.1.3-3.el4.centos is needed by heartbeat-2.1.3-3.el4.centos.i386             libpils.so.1 is needed by heartbeat-2.1.3-3.el4.centos.i386             libstonith.so.1 is needed by heartbeat-2.1.3-3.el4.centos.i386
    然后下载heartbeat-pils-2.1.3-3.el4.centos.i386.rpm和heartbeat-stonith-2.1.3-3.el4.centos.i386.rpm,
在安装这两个包之后,即可正常安装heartbeat了。
2、配置相关文件     1) 找到安装后heartbeat的文档目录,将三个需要的配置文件样例copy到/etc/ha.d目录下准备后面的配置设
置(这样会更方便,而且有较为详细的配置说明):     [root@mysql1 ha.d]# rpm -q heartbeat -d     ...     /usr/share/doc/heartbeat-2.1.3/AUTHORS     ...     [root@mysql1 ha.d]# cp /usr/share/doc/heartbeat-2.1.3/ha.cf .     [root@mysql1 ha.d]# cp /usr/share/doc/heartbeat-2.1.3/authkeys .     [root@mysql1 ha.d]# cp /usr/share/doc/heartbeat-2.1.3/haresources .          2) 配置ha.cf(ha主要配置文件):     logfacility             local0        #这个是设置heartbeat的日志,这里是用的系统日志     keepalive               500ms         #多长时间检测一次     deadtime                10            #连续多长时间联系不上后认为对方挂掉了(单位是妙)     warntime                5             #连续多长时间联系不上后开始警告提示         initdead                100           #这里主要是给重启后预留的一段忽略时间段(比如:重启后启动网络等,
					如果在网络还没有通,keepalive检测肯定通不过,但这时候并不能切换)     bcast                   eth0     auto_failback           off           #恢复正常后是否需要再自动切换回来     node                    mysql1        #节点名(必须是集群中机器的主机名,通过uname -n取得)     node                    mysql2        #节点名(必须是集群中机器的主机名,通过uname -n取得)    ping                    10.0.65.250         respawn                 hacluster     /usr/lib/heartbeat/ipfail    #这里是配置ip绑定和切换的功能,
								ipfail就是控制ip切换的程序     apiauth                 ipfail        gid=haclient    uid=hacluster    #控制ip切换的时候所使用的用户     deadping 5
    2) haresources 资源组文件配置(v1 style):     [root@mysql1 ha.d]# cat haresources     mysql1 drbddisk Filesystem::/dev/drbd0::/drbddata::ext3 mysql 10.0.65.44
    资源组配置文件主要是配置切换过程需要管理的各种资源的,有一个很关键的点,那就是一个资源组中的各个资源
的排列顺序是需要注意的,在hearbeat管理资源组的时候,获取资源的过程是从左往右依次处理,释放资源的时候是从
右往左依次处理。     资源组里面的资源可以是ip的管理,可以是各种服务,也可以是我们自己写的各种脚本,甚至可以是需要传参数的
(通过::来分割参数)。每一行代表一个资源组,每个资源组之间没有必然的关系。     资源组的第一列是我们在ha.cf配置文件中的node之一,而且应该是当前准备作为primary节点的那一个node。     上面资源组中的各项含义如下:         mysql1        当前primary节点名(uname -n)         drbddisk      告诉heartbeat要管理drbd的资源         Filesystem    这里是告诉heartbeat需要管理文件系统资源,其实实际上就是执行mount/umount命令,
			后面的“::”符号之后是跟的Filesystem的参数(设备名和mount点)         mysql         告诉需要管理mysql         10.0.65.44    这里是让heartbeat帮你管理一个service ip,会跟着主节点一起漂移
    3) authkeys 通信认证配置文件     root@mysql2:/root>cat /etc/ha.d/authkeys     auth 2            #认证方式,有如下三种     #1 crc            #     2 sha1 HI!     #3 md5 Hello!
3、测试切换:     1) 手工调用heartbeat的节点切换脚本:     执行/usr/lib/heartbeat/hb_standby 脚本,让heartbeat通知对方节点自己请求变成standby节点,
请求对方成为primary节点,切换工作在10s左右即完成.     2) 拔掉网线,测试在primary节点的网络断开后的切换情况     通过测试,在拔掉网线后,当主节点发现无法和standby节点无法通信后,会在log中记录warn信息,
如果延续时间达到在ha.cf中设定的时长后,会开始释放资源,standby节点发现无法和主节点通信一段时间(ha.cf设定)后,
开始尝试启动资源并将自己active成primary节点。切换过程除开ha.cf中设定的时长之外的时间段同样非常短。     3) shutdown primary主机,测试是否能够正常切换,基本上和上面测试2差不多。     4) primary node 掉电测试,这里还没有到机房实际操作过,后面会继续测试这个操作。     
注:以上测试都是基于Heartbeat v1 style的设置情况下所作,由于v1 style配置的heartbeat没办法做到对资源状态的监控,
主要职能通过监控与对方节点以及集群对外的网络状况的监控,而v2 style的配置已经提控了对资源状态的监控,所以后面准备再
针对v2 style的heartbeat进行详细一点的测试。不过,在linux-ha网站上面发现有一个声明,说drbd的作者建议用户继续
使用v1 style来让heartbeat管理drbd资源(http://www.linux-ha.org/DRBD/HowTov2),详细的原因并没有说明。
原文如下:
Note: as of 2008-02-15, the DRBD developers recommend to use the v1 drbddisk RA, although
the v2 drbd RA has been reported to work by some users (decide on your own!)

比如一个index entry 是18bytes,一个8k的block,PCTFREE=10%,则可以存放8*1024*0.9/18=410,约为400个index entries
如果一个表有100,000,000 rows,那么需要250,000 leaf blocks
在算算branch:每个branch 记录了每个leaf的最大值和address,大约16 bytes。这样可以存储大约450 个 branch entries
也就是说需要250000/450=556个branch block。
但是556个branch又需要多少个再上一级的branch block 来管理呢?
因为一个branch最多存储450个leaf entries,所以至少又需要2个branch blocks。
然而这两个branch blocks,还需要一个block来管理,这个block就是root 了。
1 = 1 branch block
BLEVEL 2  = 2 branch blocks
BLEVEL 3 = 556 branch blocks
Leaf level(BLEVEL 4) = 250,000 leaf blocks
rows = 100,000,000 rows
所以一个4层的索引,至少就可以管理1亿条记录。

这一次,我们来讲讲数据安全和读写高可用

oh no,亲,于是我们又掉入了CAP所描述的陷阱。

好吧,那么我们也就进入这个领域,来看看这数据安全所代表的一切。

在20年以前,数据安全对于大部分用户来说,只意味着数据库ACID中的”D”,数据写入到数据库,并返回成功后,这个数据也就是安全的了,在老师教给我们的计算机原理课上,似乎最多也就讲到,数据库有冷备份,也有热备份,因此写入数据库内的数据是安全的。

然而,真的如此么? 

最简单的问题,就是,如果这台机器的硬盘挂掉,那应该怎么办呢?

于是,有些人就想到,那我们用多块硬盘来备份不就好了?于是,RAID技术就应运而生了。Raid技术的核心,就是利用磁盘阵列的手段,提升数据的写入效率和安全性。

那么,raid也不是完美的,首先,磁盘放在机器上,这机器的磁盘就不可能无限增加的。单机的磁盘容量受到磁盘架个数的限制。

于是,有一些人就想到:那我们就专门的设计一种可以挂无数磁盘的柜子来好了。这就是盘柜技术的产生,关键词 SAN,不过这东西不是我们要讨论的东西,所以我们就不在这里细说了。 

但因为盘柜技术本身有其优势也有一定的局限性,所以目前这套东西不大为人所知了。

似乎所有人谈起存储必谈GFS,HDFS…但其实个人认为在原教旨主义的文件系统上,更多的依靠硬件的盘柜,也不失为一套非常好的解决方案。

正所谓分久必合合久必分,目前Oracle携ExtraData 一体机技术在市场上杀的风生水起,不也是盘柜技术的新时代体现么?呵呵。。

好了,废话不多扯,盘柜在目前不大容易成为主流,主要原因是

1.      冗余容错性不好

2.      价格较贵

3.      核心技术把持在大公司手里

Etc.

那么,既然盘柜技术不大容易成为主流,那么目前的主流是什么呢?

这就是多机的同步技术,利用廉价的tcp/ip网络,将多台pc server联系到一起,使用软件逻辑而非硬件逻辑来进行数据的多磁盘备份。

这其实就已经涉及到了问题的核心:什么是数据安全和数据高可用?我们将数据安全的级别从低到高,做成表格列在下面。

693f0847jw1dr5klx6kgmj 海量存储之十四

可见,从目前来看,解决数据安全的唯一办法是将数据同步的写到多个不同的地方(可以是用raid5的方式写,也可以用raid10的方式,不过核心都是一个— 冗余。

而且不能简单的就用单机的冗余,必须要多机冗余才靠得住。

如果要最安全,那么数据就要同步的复制多机。

下面,我们就专注于这同步复制多机的case,来看看目前我所知的几种常见的,以解决高可用为基础的数据冗余方案吧。

需要强调的是,这些方案本身,没有绝对的一家独大之说,每一种方案,都有其自己的特性和适用场景,所以不存在某一种方案一定比其他方案好的这种说法,每一种模式都有其自己的优势和劣势。

所谓可用性,就是尽可能的保证,无论发生什么变故,数据库都能够正常的提供读写访问的这种方式。

而所谓安全性,就是尽可能的保证,无论发生什么变故,数据库内,持久化的数据都不会丢失。

这里的数据丢失,其实是个很宽泛的词汇,为了表达的更为清楚,我们需要仔细的描述一个最关键的问题:什么时候能够叫做“数据写入成功”,换句话说,也就是,数据存储系统,与前端的无状态调用者之间的承诺关系是什么呢?

认清这个问题,对于我们定义数据安全,至关重要。

从一个请求走到网络,提交给存储系统的过程,细化下来可以认为是以下几个动作的分解:

693f0847jw1dr5kl1ayj6j 海量存储之十四

可以认为,在时间线上来说,所有的这些操作都可能出现!异常!,导致写失败。这里又涉及到以前我们提到的网络三种反馈状态问题了,让我分阶段来进行讨论:

A. 客户端发起请求出现失败 -> 客户端明确的知道自己失败,所以所有操作都未进行,对整个系统的一致性没有影响。

B. 发起请求后,因为网络因素,导致请求未被server接受 -> 客户端等待,直到超时,但Server未接受请求。客户端应认为失败。

C. Server端接受到写入请求,但内部操作失败-> Server端应该保证操作的原子性,并反馈client操作失败。

D. Server端一系列操作成功,反馈Client,但因为网络异常导致Client无法接收请求 ->Server端成功,但Client端等待超时,则client端对操作有疑问。

E. Client端收到Server端反馈的成功 -> 认为成功。

可以认为,只有最后一步成功的时候,才算做成功,而其他操作,因为网络因素导致的异常,都认为是失败的。

从上面的分析中可以看出,存储对于完整性的保证,只存在于E步骤成功时。

而client端能明确的知道操作失败的,是A,,C场景。

Client端不能明确知道操作成功或失败的,是B,D,场景,需要人工验证,或默认丢超时异常,并被认为是失败。

而我们所定义的数据安全性,就是指,当E完成时,也就是Server对client端反馈成功时,则数据在各种变故出现时,所能保证的完整性的一种体现( T_T ..真绕。。。)

而我们要在后面,花费大量篇幅来讨论的,就是“进行一系列操作处理”这个过程,如何能保证数据的完整性的问题。

因为篇幅的关系,今次就介绍第一种,也是最简单的一种,使用异步复制队列的方式来提升可用性和安全性吧。

这是所有数据存储中基本都提供的一种模式,而大部分的其他方案的核心和基础都是这个异步的复制的模型的权衡版本。所以,我们就从这里开始。

要讲清这个问题,我们先来看一张图

693f0847jw1dr5kmdazakj 海量存储之十四

所谓异步传输,简单来说就是数据写入主机后,不等待其他机器反馈结果请求,直接反馈用户写入成功的一种策略。

好处:

数据写入速度快(因为只需要保证一台机器写成功,那么就算成功了)。

副作用:

如果主机写了位置102,但备机还没来得及收102的数据到备机,这时候主机down机。数据实际上还未写入备机呢。于是就出现了数据丢失。

好,就到这里。。下次我们来讨论,在这种情况下,如何能够保证可用性。

DRBD笔记

2012/05/02

一、主要功能

    DRBD实际上是一种块设备的实现,主要被用于Linux平台下的高可用(HA)方案之中。他是有内核模块和相关程序而组成,通过网络通信来同步镜像整个设备,有点类似于一个网络RAID的功能。也就是说当你将数据写入本地的DRBD设备上的文件系统时,数据会同时被发送到网络中的另外一台主机之上,并以完全相同的形式记录在一个文件系统中(实际上文件系统的创建也是由DRBD的同步来实现的)。本地节点(主机)与远程节点(主机)的数据可以保证实时的同步,并保证IO的一致性。所以当本地节点的主机出现故障时,远程节点的主机上还会保留有一份完全相同的数据,可以继续使用,以达到高可用的目的。

    在高可用(HA)解决方案中使用DRBD的功能,可以代替使用一个共享盘阵存储设备。因为数据同时存在于本地主机和远程主机上,在遇到需要切换的时候,远程主机只需要使用它上面的那份备份数据,就可以继续提供服务了。

二、底层设备支持

    DRBD需要构建在底层设备之上,然后构建出一个块设备出来。对于用户来说,一个DRBD设备,就像是一块物理的磁盘,可以在商脉内创建文件系统。DRBD所支持的底层设备有以下这些类:

    1、一个磁盘,或者是磁盘的某一个分区;

    2、一个soft raid 设备;

    3、一个LVM的逻辑卷;

    4、一个EVMS(Enterprise Volume Management System,企业卷管理系统)的卷;

    5、其他任何的块设备。

三、配置简介

    1、全局配置项(global)

        基本上我们可以做的也就是配置usage-count是yes还是no了,usage-count参数其实只是为了让linbit公司收集目前drbd的使用情况。当drbd在安装和升级的时候会通过http协议发送信息到linbit公司的服务器上面。

    2、公共配置项(common)

        这里的common,指的是drbd所管理的多个资源之间的common。配置项里面主要是配置drbd的所有resource可以设置为相同的参数项,比如protocol,syncer等等。

    3、资源配置项(resource)

        resource项中配置的是drbd所管理的所有资源,包括节点的ip信息,底层存储设备名称,设备大小,meta信息存放方式,drbd对外提供的设备名等等。每一个resource中都需要配置在每一个节点的信息,而不是单独本节点的信息。实际上,在drbd的整个集群中,每一个节点上面的drbd.conf文件需要是完全一致的。

        另外,resource还有很多其他的内部配置项:

        net:网络配置相关的内容,可以设置是否允许双主节点(allow-two-primaries)等。

        startup:启动时候的相关设置,比如设置启动后谁作为primary(或者两者都是primary:become-primary-on both)

        syncer:同步相关的设置。可以设置“重新”同步(re-)速度(rate)设置,也可以设置是否在线校验节点之间的数据一致性(verify-alg 检测算法有md5,sha1以及crc32等)。数据校验可能是一个比较重要的事情,在打开在线校验功能后,我们可以通过相关命令(drbdadm verify resource_name)来启动在线校验。在校验过程中,drbd会记录下节点之间不一致的block,但是不会阻塞任何行为,即使是在该不一致的block上面的io请求。当不一致的block发生后,drbd就需要有re-synchronization动作,而syncer里面设置的rate项,主要就是用于re-synchronization的时候,因为如果有大量不一致的数据的时候,我们不可能将所有带宽都分配给drbd做re-,这样会影响对外提提供服务。rate的设置和还需要考虑IO能力的影响。如果我们会有一个千兆网络出口,但是我们的磁盘IO能力每秒只有50M,那么实际的处理能力就只有50M,一般来说,设置网络IO能力和磁盘IO能力中最小者的30%的带宽给re-synchronization是比较合适的(官方说明)。另外,drbd还提供了一个临时的rate更改命令,可以临时性的更改syncer的rate值:drbdsetup /dev/drbd0 syncer -r 100M。这样就临时的设置了re-synchronization的速度为100M。不过在re-synchronization结束之后,你需要通过drbdadm adjust resource_name 来让drbd按照配置中的rate来工作。

五、资源管理

    1、增加resource的大小:

    当遇到我们的drbd resource设备容量不够的时候,而且我们的底层设备支持在线增大容量的时候(比如使用lvm的情况下),我们可以先增大底层设备的大小,然后再通过drbdadm resize resource_name来实现对resource的扩容。但是这里有一点需要注意的就是只有在单primary模式下可以这样做,而且需要先在所有节点上都增大底层设备的容量。然后仅在primary节点上执行resize命令。在执行了resize命令后,将触发一次当前primary节点到其他所有secondary节点的re-synchronization。

    如果我们在drbd非工作状态下对底层设备进行了扩容,然后再启动drbd,将不需要执行resize命令(当然前提是在配置文件中没有对disk参数项指定大小),drbd自己会知道已经增大了容量。

    在进行底层设备的增容操作的时候千万不要修改到原设备上面的数据,尤其是drbd的meta信息,否则有可能毁掉所有数据。

    2、收缩resource容量:

    容量收缩比扩容操作要危险得多,因为该操作更容易造成数据丢失。在收缩resource的容量之前,必须先收缩drbd设备之上的容量,也就是文件系统的大小。如果上层文件系统不支持收缩,那么resource也没办法收缩容量。

    如果在配置drbd的时候将meta信息配置成internal的,那么在进行容量收缩的时候,千万别只计算自身数据所需要的空间大小,还要将drbd的meta信息所需要的空间大小加上。

    当文件系统收缩好以后,就可以在线通过以下命令来重设resource的大小:drbdadm — –size=***G resize resource_name。在收缩的resource的大小之后,你就可以自行收缩释放底层设备空间(如果支持的话)。

    如果打算停机状态下收缩容量,可以通过以下步骤进行:

        a、在线收缩文件系统

        b、停用drbd的resource:drbdadm down resourcec_name

        、导出drbd的metadata信息(在所有节点都需要进行):drbdadm dump-md resource_name > /path_you_want_to_save/file_name

        d、在所有节点收缩底层设备

        e、更改上面dump出来的meta信息的la-size-sect项到收缩后的大小(是换算成sector的数量后的数值)

f、如果使用的是internal来配置meta-data信息,则需要重新创建meta-data:drbdadm create-md resource_name

g、将之前导出并修改好的meta信息重新导入drbd(摘录自linbit官方网站的一段导入代码):

    drbdmeta_cmd=$(drbdadm -d dump-md test-disk)

            ${drbdmeta_cmd/dump-md/restore-md} /path_you_want_to_save/file_name
        h、启动resource:drbdadm up resource_name

六、磁盘损坏

    1、detach resource

        如果在resource的disk配置项中配置了on_io_error为pass_on的话,那么drbd在遇到磁盘损坏后不会自己detach底层设备。也就是说需要我们手动执行detach的命令(drbdadm detach resource_name),然后再查看当前各节点的ds信息。可以通过cat /proc/drbd来查看,也可以通过专有命令来查看:drbdadm dstat resource_name。当发现损坏的那方已经是Diskless后,即可。如果我们没有配置on_io_error或者配置成detach的话,那么上面的操作将会由自动进行。

        另外,如果磁盘损坏的节点是当前主节点,那么我们需要进行节点切换的操作后再进行上面的操作。

    2、更换磁盘

        当detach了resource之后,就是更换磁盘了。如果我们使用的是internal的meta-data,那么在换好磁盘后,只需要重新创建mata-data(drbdadm create-md resource_name),再将resource attach上(drbdadm attach resource_name),然后drbd就会马上开始从当前primary节点到本节点的re-synchronisation。数据同步的实时状况可以通过 /proc/drbd文件的内容获得。

        不过,如果我们使用的不是internal的meta-data保存方式,也就是说我们的meta-data是保存在resource之外的地方的。那么我们在完成上面的操作(重建meta-data)之后,还需要进行一项操作来触发re-synchnorisation,所需命令为:drbdadm invalidate resource_name 。

七、节点crash(或计划内维护)

    1、secondary节点

        如果是secondary接待你crash,那么primary将临时性的与secondary断开连接,cs状态应该会变成WFConnection,也就是等待连接的状态。这时候primary会继续对外提供服务,并在meta-data里面记录下从失去secondary连接后所有变化过的block的信息。当secondary重新启动并连接上primary后,primary –> secondary的re-synchnorisation会自动开始。不过在re-synchnorisation过程中,primary和secondary的数据是不一致状态的。也就是说,如果这个时候primary节点也crash了的话,secondary是没办法切换成primary的。也就是说,如果没有其他备份的话,将丢失所有数据。

    2、primary节点

        一般情况下,primary的crash和secondary的crash所带来的影响对drbd来说基本上是差不多的。唯一的区别就是需要多操作一步将secondary节点switch成primary节点先对外提供服务。这个switch的过程drbd自己是不会完成的,需要我们人为干预进行一些操作才能完成。当crash的原primary节点修复并重新启动连接到现在的primary后,会以secondary存在,并开始re-synchnorisation这段时间变化的数据。

        在primary节点crash的情况下,drbd可以保证同步到原secondary的数据的一致性,这样就避免了当primary节点crash之后,secondary因为数据的不一致性而无法wcitch成primary或者即使切换成primary后因为不一致的数据无法提供正常的服务的问题。

    3、节点永久性损坏(需要更换机器或重新安装相关软件的情况)

        当某一个节点因为硬件(或软件)的问题,导致某一节点已经无法再轻易修复并提供服务,也就是说我们所面对的是需要更换主机(或从OS层开始重新安装)的问题。在遇到这样的问题后,我们所需要做的是重新提供一台和原节点差不多的机器,重新开始安装os,安装相关软件,从现有整提供服务的节点上copy出drbd的配置文件(/etc/drbd.conf),创建meta-data信息,然后启动drbd服务,以一个secondary的身份连接到现有的primary上面,后面就会自动开始re-synchnorisation。

八、split brain的处理

    split brain实际上是指在某种情况下,造成drbd的两个节点断开了连接,都以primary的身份来运行。当drbd某primary节点连接对方节点准备发送信息的时候如果发现对方也是primary状态,那么会会立刻自行断开连接,并认定当前已经发生split brain了,这时候他会在系统日志中记录以下信息:“Split-Brain detected,dropping connection!”当发生split brain之后,如果查看连接状态,其中至少会有一个是StandAlone状态,另外一个可能也是StandAlone(如果是同时发现split brain状态),也有可能是WFConnection的状态。

    如果我们在配置文件中配置了自动解决split brain(好像linbit不推荐这样做),drbd会自行解决split brain问题,具体解决策略是根据配置中的设置来进行的。

    如果没有配置split brain自动解决方案,我们可以手动解决。首先我们必须要确定哪一边应该作为解决问题后的primary,一旦确定好这一点,那么我们同时也就确定接受丢失在split brain之后另外一个节点上面所做的所有数据变更了。当这些确定下来后,我们就可以通过以下操作来恢复了:

    a、首先在确定要作为secondary的节点上面切换成secondary并放弃该资源的数据:

        drbdadm secondary resource_name

        drbdadm — –discard-my-data connect resource_name

    b、在要作为primary的节点重新连接secondary(如果这个节点当前的连接状态为WFConnection的话,可以省略)

        drbdadm connect resource_name

    当作完这些动作之后,从新的primary到secondary的re-synchnorisation会自动开始。

八、meta data存放地点的比较

    1、internal meta-data(meta-data和数据存放在同一个底层设备之上)

    优点:一旦meta-data创建之后,就和实际数据绑在了一起,在维护上会更简单方便,不用担心meta-data会因为某些操作而丢失。另外在硬盘损坏丢失数据的同时,meta-data也跟着一起丢失,当更换硬盘之后,只需要执行重建meta-data的命令即可,丢失的数据会很容易的从其他节点同步过来。

    缺点:如果底层设备是单一的磁盘,没有做raid,也不是lvm等,那么可能会造成性能影响。因为每一次写io都需要更新meta-data里面的信息,那么每次写io都会有两次,而且肯定会有磁头的较大寻道移动,因为meta-data都是记录在dice设备的最末端的,这样就会造成写io的性能降低。

    2、external meta data(meta-data存放在独立的,与存放数据的设备分开的设备之上)

    优点:与internal meta-data的缺点完全相对,可以解决写io的争用问题。

    缺点:由于meta-data存放在与数据设备分开的地方,就意味着当磁盘损坏更换磁盘之后,必须手动发起全量同步的操作。也就是管理维护会稍微麻烦那么一点点,很小的一点点。

    如果我们希望在已经存在数据的设备上面建立drbd的资源,并且不希望丢失该设备上面的数据,又没办法增大底层设备的容量,而且上层文件系统又没办法收缩的话,我们就只能将meta data创建成external方式。