Permalink: 2014-08-20 10:07:08 by ning in redis tags: all

replication 这个问题, 包括同步和failover两个方面:

  1. 如何保存oplog & 如何实现同步.
    1. oplog是否幂等
      • 存操作: 不幂等(mysql 的binlog STATEMENT模式, redis的aof)
      • 存结果: (MySQL binlog ROW模式)
    2. 同步是master主动还是slave主动?
      • master主动推
      • slave主动拉
    3. 怎么索引一条oplog
      • 常见三种方法:
        • 时间ts(mongo)
        • 字节数(redis/mysql)
        • idx
      • 这个问题其实涉及到发生主从切换后, 老主继续同步的问题.
        • 假设A=>master, B=>slave, 如果A挂掉, 发生主从切换, B变成主,
        • 等A恢复后, A如何找到从B的什么位置去同步.
  2. 如何failover.
    1. master挂了之后, 如何选择新的master.

    2. master挂了又回来后的一致性问题
      • 还是刚才的例子, 假设A=>master, B=>slave, 如果A挂掉, 发生主从切换, A上面的写是比B新的,
        • 当A恢复后, A再去找B同步时, A上多余的数据, 怎么办.
      • 对redis来说, 这时候A会从B做一次全量同步, 从而保证一致

关于一致性, 我们可以看这个例子:

-------------------------------------------------------------

   +-------+        +-------+
   |   A   |        |   B   |
   +-------+        +-------+

t1 insert 1 --\
      |        \
t2 insert 2 --\ \-- insert 1
      |        \
t3 insert 3 --\ \-- insert 2
      |        \
t4 insert 4     \-- insert 3
--------------------------------> A down, B is master now

t5              /-- insert 5
               /       |
t6 insert 5 --/ /-- insert 6
               /
t7 insert 6 ---

#到这里, 我们已经看到了不一致:

1,2,3,4,5,6          1,2,3,5,6

接下来我们从这几个方面, 研究redis/mysql/mongo如何实现

1   幂等

1.1   redis

  • redis 的主从同步和aof记录的都是操作, 而不是结果, 也就是非幂等

  • 这造成重放aof时不可重入.

  • 仔细考虑, redis这样的设计是对的, 因为redis支持set, hash, list等数据结构.
    • 以list为例
    • list push操作转化为幂等aof需要将整个list记录到aof中, 这太浪费了.

1.2   mysql binlog格式

  • 关于格式
    • 三种格式
      • STATEMENT-BASED format
      • ROW-BASED format
      • mised: 是上面两种的混合.
    • STATEMENT模式记录操作, ROW模式记录结果.

  • 考虑下面一些不同场景:
    • 每个sql操作从索引命中一条, 并更新这一条中一个字段:
      • 如果使用ROW模式, 每次要记录整个行, 较大, 在从库上插入也较慢, 所以会导致跟不上同步.
    • 如果一个sql操作很多条记录 update xxx while x > 10 , 或者改表(alter)
      • ROW: 产生很多条binlog
      • STATEMENT: 只有一条binlog
    • 一个sql需要扫描很多条, 最后才更新一条(比如不能命中索引), 然后更新.
      • 如果用STATEMENT模式, 着一个操作在从库上执行也会慢.
      • 如果用ROW模式, 就很快.
  • 一般来说, mixed性能可能更好, 多数都是用mixed模式.

  • 存在问题(jira报的一些bug):
    • INSERT DELAYED ... VALUES(LAST_INSERT_ID()) inserts a different value on the master and the slave. (Bug #20819)
      • This is fixed in MySQL 5.1 when using row-based or mixed-format binary logging.
    • Adding an AUTO_INCREMENT column to a table with ALTER TABLE might not produce the same ordering of the rows on the slave and the master

    • Replication of LIMIT clauses in DELETE, UPDATE, and INSERT ... SELECT statements is not guaranteed, since the order of the rows affected is not defined. Such statements can be replicated correctly only if they also contain an ORDER BY clause.
      • 应该是 STATEMENT-BASED 格式有这个问题, ROW 格式应该没有这个问题.
  • 不像redis那样全量同步数据, 很难避免这些问题.

具体保存方法:

  • 多个文件, 每次启动或者写满一个max_binlog_size, 就新开一个.
  • binlog index文件, 记录当前哪些binlog文件在使用.
  • PURGE BINARY LOGS命令用于清除binlog
  • binlog也可以自动清理, expire_logs_days .
shell> mysqlbinlog binlog.0000003
The output includes events contained in binlog.000003. Event information includes the SQL statement, the ID of the server on which it was executed, the timestamp when the statement was executed, how much time it took, and so forth.

Events are preceded by header comments that provide additional information. For example:

# at 141
#100309  9:28:36 server id 123  end_log_pos 245
  Query thread_id=3350  exec_time=11  error_code=0
In the first line, the number following at indicates the file offset, or starting position, of the event in the binary log file.

The second line starts with a date and time indicating when the statement started on the server where the event originated. For replication, this timestamp is propagated to slave servers. server id is the server_id value of the server where the event originated. end_log_pos indicates where the next event starts (that is, it is the end position of the current event + 1). thread_id indicates which thread executed the event. exec_time is the time spent executing the event, on a master serve

1.3   mongo

  • mongo 是记录记录结果, 幂等.

2   推/拉

2.1   redis

  • slave通过sync命令连上来后, master 每次做完一个写操作, 就会 调用replicationFeedSlaves, 向这个socket 发送命令

  • 这个机制和 monitor一样:
    • replicationFeedMonitors 方法是把cmd通过一定的格式把命令发到monitor客户端.
  • 也就是说, 是master主动推到slave

2.2   mysql

slave 来拉

  • slave pulls the data from the master, rather than the master pushing the data to the slave
    • 好处是: slave可以随时决定停止或重启repl, slave可以决定用什么样的速度.
  • 三个线程
    • master:
      • Binlog Dump线程: slave连上来时启动, 加锁读binlog, 读完释放锁.
    • slave:
      • slave IO线程: 从master读取, 写到slave's relay log
      • slave SQL 线程: 从relay log 读, apply

2.3   mongo

slave拉

  • mongo的oplog里面是带时间戳的, 从库来同步的时候, 相当于: 找到这个时间点后的更新序列.

  • 主库上的一个操作oplog的time为t1, 同步到丛库上后, 丛库oplog的time应该也是t1

  • 而redis, mysql 的同步都是用一个位置来记录, 比如redis是一个offset, mysql是file + pos

  • 用时间戳 的好处:
    • 从库不需要像mysql一样, 开一个文件来记录 当前同步到了主库的哪条oplog, 只需要记录 每条记录的ts, ts在主库和从库上是一样的.

3   如何索引oplog

3.1   redis

repl_backlog用字节数做索引:

+------------------------------------------------+
|                                                |
|                                                |
+------------------------------------------------+
                           ^                     ^
                           |                     |
                           |                     |
                           repl_backlog_idx      repl_backlog_size

feed 10个byte::

+------------------------------------------------+
|                                                |
|                                                |
+------------------------------------------------+
                                      ^          ^
                                      |          |
                                      |          |
                                      idx        repl_backlog_size

客户端连上后, 通过 addReplyReplicationBacklog 发送backlog:

/* Feed the slave 'c' with the replication backlog starting from the
 * specified 'offset' up to the end of the backlog. */
long long addReplyReplicationBacklog(redisClient *c, long long offset) {

s和m之间有一个runid 来确认, 上次是从它开始同步的:

int masterTryPartialResynchronization(redisClient *c) {

如果master收到slave的sync命令, 要求的runid和master不同, 或者要求的oplog不在当前oplog范围内, 都要求客户端做全量同步.

3.2   mysql

  • mysql 5.6以前, 主从同步依赖于每个mysql 实例的binlog file 和binlog pos(用binlog-file + binlog-pos作为索引)

3.3   mysql5.6 GTID

  • mysql 5.6 开始的GTID方案.
    • 一个binlog在master和所有slave上都有一个全局唯一的ID
    • 由GTID索引.

3.4   mongo

  • mongo的做法其实类似于GTID.
    • mongo里面每个oplog都有一个ts子段, 它的值为写操作发生的时间戳 + 秒内自增id.
    • mongo主从同步逻辑保证在一个replset中, 一个操作的ts保持不变.

4   failover

假设这样一个场景:

.  A
 / | \
B  C  D

A是主库, BCD是丛库, A机器宕机后, 我们决定B成为新的主库:

.  B
   | \
   C  D

这里涉及第一个问题:

问题一: 如何从三个丛库B, C, D里面选择B作为新的master

我们考虑 主从切换后, 老主重新成为从库时的行为, 当A复活后, 我们希望把A挂回去, 这时候存在第二个问题:

问题二: A上可能有领先于BCD的数据, 如何让A和BCD上数据一致
  1. 最简单的做法是全量同步, 如redis/ledisdb, 实现简单, 但是太慢, 网卡打满等问题, 对于磁盘存储的数据来说, 不可接受

  2. 较好的办法是让A挂回去后, 利用已有数据, 从B继续同步, 这存在两个问题
    • A上领先的数据怎么办?
    • A挂回去后, 从B的什么位置开始继续同步?

4.1   redis

  • 而redis的同步是用一个位置来记录(offset)
  • 不保证主从上offset一致
  • slave上记录同步到主的什么offset, 网络瞬断后可以继续.
  • 如果发生主从切换, 全量同步.
  • 全量同步, 所以不存在第二个问题.

4.2   mysql

4.2.1   第一个问题:

4.2.1.1   mysql 5.6以前
  • mysql 5.6以前, 主从同步依赖于每个mysql 实例的binlog file 和binlog pos

  • 发生主从切换时,
    • 通常BCD上都已经把A的binlog读到本地, 保存为relaylog
      • 这种情况下, 可以等待BCD都apply完本地的relaylog, 这样数据就达到一个一致的点, 可以从BCD上随便选一个作为新的master
    • 如果BCD上没有把binlog都拉到本地.
      • 需要通过人工找到同步点, 即找到B,C,D上哪个的binlog/relaylog最新.
  • 假设我们找到B的binlog最新, 在通过下面命令继续令CD向B同步:

    stop slave;
    change master to master_host='',master_port=,master_user='',master_password='',master_log_file='',master_log_pos=;
    start slave;
    
  • 至于如何找到这个同步点, 有很多方法, 不过目前基本都是靠人工, 依靠DBA的经验,
    • MHA貌似也能实现
4.2.1.2   mysql GTID
  • mysql 5.6 开始的GTID方案.
    • 针对上面这个问题(可运维性差), mysql设计了GTID, 保证同一个事务, 在一个主从结构中的log_idx是一致的.
    • 这样发生主从切换时, 只需要从BCD上找到GTID最新的一个binlog, 作为新的master.
    • 之后CD只需要知道自己最后一条binlog的GTID, 然后到B上去找这个GTID之后的binlog同步过来即可.

4.2.2   第二个问题

  • 当A又加回来后,无论是使用人工找同步点, 还是GTID自动找点. 都可能出现A领先于B的情况(A上的数据没有被同步到B)

  • 这时候可以有2个办法:
    1. 让A全量重新同步, 达成一致.
    2. 把A上多余的数据人工补充到B(这需要人工操作, 操作的结果很难保证一致)

4.3   mongo

  • 通过rollback, 把A上多余的数据删掉/rollback, 达到于BCD一致的状态
    • 参看notes/mongo/rollback.rst

    • mongo的做法其实类似于GTID.
      • mongo里面每个oplog都有一个ts子段, 它的值为写操作发生的时间戳 + 秒内自增id.
      • mongo主从同步逻辑保证在一个replset中, 一个操作的ts保持不变.
    • 关于A上领先的数据, mongo会通过一个回滚机制
      • 把A上多余的数据删掉/rollback, 达到于BCD一致的状态
      • 参看notes/mongo/rollback.rst

5   小结

我们从推拉模式, binlog格式, 如何发生主从切换来考察

-               mysql           redis       mongo       hadooop
模式            推              推          拉          推.
格式            row/statement   statement   row         ?
主从切换机制    人肉            sentinel    选举        ?
主从切换        人肉找点/       全量同步    自动GTID    ?
后如何继续同步  用GTID同步
  • 在mysql这样复杂, 操作丰富的数据库里面实现binlog同步有很多难点, 比如LAST_INSERT_ID() 等.

  • oplog记录操作和记录结果, 各有好处.

  • failover时的一致性问题有很多解决方法
    1. 忽略
    2. 重做
    3. rollback.

6   参考:

6.1   关于GTID

GTID的全称为 global transaction identifier , 可以翻译为全局事务标示符,GTID在原始master上的事务提交时被创建。GTID需要在全局的主-备拓扑结构中保持唯一性,GTID由两部分组成:

A global transaction identifier (GTID) is a unique identifier created and associated with each transaction committed on the server of origin (master). This identifier is unique not only to the server on which it originated, but is unique across all servers in a given replication setup. There is a 1-to-1 mapping between all transactions and all GTIDs.

GTID = source_id:transaction_id

  • source_id用于标示源服务器,用server_uuid来表示,这个值在第一次启动时生成,并写入到配置文件data/auto.cnf中
  • transaction_id则是根据在源服务器上第几个提交的事务来确定。

一个GTID的生命周期包括: 1.事务在主库上执行并提交 给事务分配一个gtid(由主库的uuid和该服务器上未使用的最小事务序列号),该GTID被写入到binlog中。 2.备库读取relaylog中的gtid,并设置session级别的gtid_next的值,以告诉备库下一个事务必须使用这个值 3.备库检查该gtid是否已经被其使用并记录到他自己的binlog中。slave需要担保之前的事务没有使用这个gtid,也要担保此时已分读取gtid,但未提交的事务也不恩呢过使用这个gtid. 4.由于gtid_next非空,slave不会去生成一个新的gtid,而是使用从主库获得的gtid。这可以保证在一个复制拓扑中的同一个事务gtid不变。

支持启用GTID,对运维人员来说应该是一件令人高兴的事情,在配置主从复制,传统的方式里,你需要找到binlog和POS点,然后change master to指向,而不是很有经验的运维,往往会将其找错,造成主从同步复制报错,在mysql5.6里,无须再知道binlog和POS点,需要知道master的IP、端口,账号密码即可,因为同步复制是自动的,mysql通过内部机制GTID自动找点同步。

mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.18.201
                  Master_User: repluser
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: master-bin.000001
          Read_Master_Log_Pos: 151
               Relay_Log_File: relay-log.000002
                Relay_Log_Pos: 363
        Relay_Master_Log_File: master-bin.000001
             Slave_IO_Running: Yes  #IO线程与SQL线程都是yes,说明复制启动完成。
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 151
              Relay_Log_Space: 561
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_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:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 1
                  Master_UUID: 6b27d8b7-0e14-11e3-9eab-000c291192e4
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 1
1 row in set (0.00 sec)

Comments