云中的数据库主从复制

这是一篇来自 Jelastic 的客座文章。

许多开发者使用主从复制来解决一系列不同的问题,包括性能问题、支持不同数据库的备份,以及作为缓解系统故障的大型解决方案的一部分。传统上,主从复制是使用物理服务器完成的,但也可以使用云数据库服务器完成。这篇来自 Jelastic 的客座文章(最初发布于此处)描述了如何使用他们的 Jelastic PaaS(平台即服务)设置 MariaDB 主从复制。

复制概述

主从复制使得一个数据库服务器(主库)的数据可以复制到一个或多个其他数据库服务器(从库)。主库记录更新,这些更新随后传播到从库。从库输出一条消息,表明已成功接收更新,从而允许发送后续更新。主从复制可以是同步的或异步的。区别仅仅在于变更传播的时机。如果更改在主库和从库上同时进行,则是同步的。如果更改被排队并在稍后写入,则是异步的。

dbreplication173

MariaDB 中复制的应用场景包括

  • 横向扩展解决方案 — 将负载分散到多个从库以提高性能。在这种环境下,所有写入和更新都必须在主服务器上进行。但是,读取可以在一个或多个从服务器上进行。这种模式可以提高写入性能(因为主库专门用于更新),同时随着从库数量的增加而大幅提高读取速度。
  • 数据安全 — 由于数据会复制到从库,并且从库可以暂停复制过程,因此可以在从库上运行备份服务而不会损坏相应的主数据。
  • 分析 — 可以在主库上创建实时数据,同时可以在从库上进行信息分析,而不会影响主服务器的性能。
  • 长距离数据分发 — 如果分支机构希望使用我们主数据的一个副本,我们可以使用复制为其创建本地数据副本供其使用,而无需永久访问主库。

让我们看几个如何使用并利用这种复制的示例

  • 备份:要使用复制作为备份解决方案,请将数据从主库复制到从库,然后在数据从库上进行备份。可以暂停并关闭从库,而不会影响主库的正常运行,因此我们可以生成一个“实时”数据的有效快照,否则这将需要关闭主库。
  • 横向扩展:我们可以使用复制作为横向扩展解决方案;也就是说,在合理的限制范围内,我们希望将数据库查询的负载分散到多个数据库服务器上。由于复制是从一个主库分发到一个或多个从库的工作方式,因此将复制用于横向扩展最适用于读操作次数多而写/更新次数少的环境。大多数网站都属于此类,用户浏览网站、阅读文章、帖子或查看产品。更新仅在会话管理期间发生,或在进行购买或向论坛添加评论/消息时发生。在这种情况下,复制使我们可以将读取分散到复制从库上,同时仍使我们的 Web 服务器在需要写入时与复制主库通信。
  • 分散负载:有时我们可能只有一个主库,并希望将不同的数据库复制到不同的从库。例如,我们可能希望将不同的销售数据分发给不同的部门,以帮助在数据分析期间分散负载。
  • 提高性能:随着连接到主库的从库数量增加,负载虽然很小,但也会增加,因为每个从库都使用客户端连接到主库。此外,由于每个从库都必须接收主库二进制日志的完整副本,主库上的网络负载也可能增加并形成瓶颈。如果我们将大量从库连接到一个主库,并且该主库也很忙碌处理请求(例如,作为横向扩展解决方案的一部分),那么我们可能希望提高复制过程的性能。一种提高复制过程性能的方法是创建更深层次的复制结构,使主库只复制到一个从库,而其余从库则连接到这个主要从库以满足其各自的复制需求。
  • 故障转移缓解:我们可以设置一个主库和一个(或几个)从库,然后编写一个脚本来监控主库检查它是否正常运行。然后指示我们的应用程序和从库在发生故障时切换主库。
  • 安全性:我们可以使用 SSL 加密复制所需的二进制日志传输,但主库和从库都必须支持 SSL 网络连接。如果任一主机不支持 SSL 连接,则无法通过 SSL 连接进行复制。使用 SSL 连接设置复制类似于使用 SSL 设置服务器和客户端。我们必须获得(或创建)一个适合在主库上使用的安全证书,并在每个从库上获得一个类似的证书(来自同一证书颁发机构)。

现在让我们来看一个在 Jelastic PaaS 上配置主从复制的简单示例。

创建环境

*以下说明仅提及 MariaDB,但它们完全适用于 MySQL 数据库服务器。

首先,我们在 Jelastic 中为我们的主从数据库创建两个环境。

  1. 转到 jelastic.com,如果我们还没有注册,请注册;或通过点击页面上的“Sign In”链接使用我们的 Jelastic 凭据登录。
  2. 请求 Jelastic 创建一个新环境。
    alf-11
  3. 在**环境拓扑**对话框中,选择 **MariaDB** 作为我们要使用的数据库。设置 cloudlet 限制并输入我们第一个环境的名称,例如 masterbase
    replication1等待片刻,直到我们的环境创建完成。

    replication2

  4. 以同样的方式再创建一个 MariaDB 环境,或者直接克隆它。我们将其命名为 slavebase。顺便说一句,它将位于另一个硬盘节点上,这对于存储我们的数据来说更加安全可靠。现在我们有两个具有两个数据库的相同环境。

    replication4

配置主数据库

现在让我们配置我们的主数据库。

  1. 点击我们主数据库的**配置**按钮。
    replication5
  2. 导航到 **my.cnf** 文件并添加以下属性
    server-id = 1
    log-bin = mysql-bin
    binlog-format=mixed
    

    dbreplication15

    我们使用 binlog 格式“mixed” (binlog-format=mixed) 来允许复制带有外键的操作。

    **注意:请勿使用 binlog 格式“statement”**(否则我们之后会遇到错误!)

  3. 保存更改并重启 MariaDB 以应用新的配置参数。

    replication7

  4. 点击 **MariaDB** 的**在浏览器中打开**按钮。创建数据库时,Jelastic 会向我们发送包含其凭据的电子邮件。使用这些凭据登录。
  5. 导航到**复制**选项卡并点击**添加从库复制用户**。

    dbreplication1

  6. 指定从库复制用户的名称和密码,然后点击**前往**。

    dbreplication2

    现在我们的从库用户已成功创建。

    dbreplication3

配置从数据库

让我们回到 Jelastic 控制面板并配置我们的从数据库。

  1. 点击我们从数据库的**配置**按钮。

    dbreplication4

  2. 导航到 **my.cnf** 文件并添加以下字符串
    server-id = 2
    slave-skip-errors = all
    

    dbreplication14

    我们允许从数据库跳过来自主库的所有错误 (slave-skip-errors = all),以便在主库发生错误时不会停止正常的从库操作。在开发过程中不要设置此选项,因为我们希望在将代码从开发环境迁移到生产环境之前发现并捕获所有大大小小的错误。生产代码不应有任何重大错误,因此设置此选项可能很有用,因为它允许复制在微小、无害的错误发生时继续工作,而这些错误可能会导致生产停止并造成经济损失(停机时间、销售损失等)。然而,此选项可能很危险,因此请谨慎操作并定期进行备份。

  3. 保存更改并重启我们的从数据库服务器以应用新的配置参数。

    dbreplication6

  4. 使用 Jelastic 在我们创建从数据库环境时发送给我们的凭据导航到 phpMyAdmin。
  5. 转到**复制**选项卡,点击**配置从库复制**。

    dbreplication71

  6. 配置我们的主服务器(输入从库复制用户的名称、密码和主机)。

    dbreplication8

    现在我们的主服务器已配置完成。

    dbreplication9

  7. 点击**控制从库 -> 完全启动**,以运行**从库 SQL** 和**从库 IO** 线程。

    dbreplication10

  8. 检查从库状态表以确保一切正常。

    dbreplication11

检查结果

我们现在必须确保主从复制适用于我们的数据库。

  1. 让我们在主数据库中创建一个新数据库(例如 Jelastic)。

    dbreplication12

  2. 导航到从数据库,我们会看到新数据库已成功复制。

    dbreplication13

连接主从

这里有两个示例,展示如何从 Java 和 PHP 应用程序连接到我们的主从数据库。

  1. 我们的第一个示例是连接到主从数据库的 Java 应用程序代码。

    Database_config.cfg

    master_host=jdbc:mysql://mariadb-master-host/mysql
    master_username=root
    master_password=abcABC123
    slave_host=jdbc:mysql://mariadb-slave-host/mysql
    slave_username=root
    slave_password=abcABC123
    
    driver=com.mysql.jdbc.Driver
    

    Dbmanager.java

    package com.jelastic.test;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.sql.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Properties;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    public class DbManager {
    
    private final static String createDatabase = "CREATE SCHEMA IF NOT EXISTS jelastic";
    private final static String showDatabases = "SHOW DATABASES";
    
    public Connection createMasterConnection() throws IOException, ClassNotFoundException, SQLException {
    Connection masterConnection;
    Properties prop = new Properties();
    prop.load(new FileInputStream(System.getProperty("user.home") + "/database_config.cfg"));
    String master_host = prop.getProperty("master_host").toString();
    String master_username = prop.getProperty("master_username").toString();
    String master_password = prop.getProperty("master_password").toString();
    String driver = prop.getProperty("driver").toString();
    
    Class.forName(driver);
    masterConnection = DriverManager.getConnection(master_host, master_username, master_password);
    return masterConnection;
    }
    
    public Connection createSlaveConnection() throws IOException, ClassNotFoundException, SQLException {
    Connection slaveConnection;
    Properties prop = new Properties();
    prop.load(new FileInputStream(System.getProperty("user.home") + "/database_config.cfg"));
    String slave_host = prop.getProperty("slave_host").toString();
    String slave_username = prop.getProperty("slave_username").toString();
    String slave_password = prop.getProperty("slave_password").toString();
    String driver = prop.getProperty("driver").toString();
    
    Class.forName(driver);
    slaveConnection = DriverManager.getConnection(slave_host, slave_username, slave_password);
    return slaveConnection;
    }
    
    public boolean runSqlStatementOnMaster() {
    boolean execute = false;
    Statement statement = null;
    try {
    statement = createMasterConnection().createStatement();
    execute = statement.execute(createDatabase);
    } catch (IOException ex) {
    Logger.getLogger(DbManager.class.getName()).log(Level.SEVERE, null, ex);
    } catch (ClassNotFoundException ex) {
    Logger.getLogger(DbManager.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SQLException ex) {
    ex.printStackTrace();
    } finally {
    if (statement != null) {
    try {
    statement.close();
    } catch (SQLException e) {
    e.printStackTrace();
    }
    }
    }
    return execute;
    }
    
    public List<String> runSqlStatementOnSlave() {
    List<String> stringList = new ArrayList<String>();
    Statement statement = null;
    ResultSet resultSet = null;
    try {
    statement = createSlaveConnection().createStatement();
    resultSet = statement.executeQuery(showDatabases);
    while (resultSet.next()) {
    stringList.add(resultSet.getString(1));
    }
    } catch (IOException ex) {
    Logger.getLogger(DbManager.class.getName()).log(Level.SEVERE, null, ex);
    } catch (ClassNotFoundException ex) {
    Logger.getLogger(DbManager.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SQLException ex) {
    ex.printStackTrace();
    } finally {
    if (resultSet != null) {
    try {
    resultSet.close();
    } catch (SQLException e) {
    e.printStackTrace();
    }
    }
    if (statement != null) {
    try {
    statement.close();
    } catch (SQLException e) {
    e.printStackTrace();
    }
    }
    }
    return stringList;
    }
    }
    
  2. 我们的第二个示例是在 PHP 应用程序中连接到主从数据库。
    <?php
    /* Master settings */
    $master_server = "xx.xxx.x.xx";
    $master_username = "root";
    $master_password = "abcABC123"; /* Slave settings */
    $slave_server = "xx.xxx.x.xx";
    $slave_username = "root";
    $slave_password = "abcABC123";$link_to_master = mysqli_connect(
    $master_server,
    $master_username,
    $master_password,
    'mysql');
    
    if (!$link_to_master) {
    printf("Unable to connect master database server. Error: %sn", mysqli_connect_error());
    exit;
    }
    
    $link_to_slave = mysqli_connect(
    $slave_server,
    $slave_username,
    $slave_password,
    'mysql');
    
    if (!$link_to_slave) {
    printf("Unable to connect slave database server. Error: %sn", mysqli_connect_error());
    exit;
    }
    
    print("
    Creating database with name Jelastic on Master node ");
    
    $result = mysqli_query($link_to_master, 'CREATE DATABASE JelasticX');
    
    sleep (3);
    
    print("
    Checking if created database was replciated to slave ");
    
    if ($result = mysqli_query($link_to_slave, 'SHOW DATABASES LIKE "JelasticX"')) {
    $result_text = mysqli_fetch_array($result);
    print ("
    Replicated database is ".$result_text[0]);
    }
    
    mysqli_close($link_to_master);
    mysqli_close($link_to_slave);
    ?>
    

结论

MariaDB 数据库复制增加了冗余,有助于确保高可用性,简化了某些管理任务(如备份),可以提高性能,等等。在云中配置数据库复制变得非常容易,只需几分钟即可完成。尽情享受吧!

发布者 MariaDB Foundation

Daniel Bartholomew 是 MariaDB 发布经理。他撰写了两本与 MariaDB 相关的书籍:《MariaDB 入门》(现已推出第二版)和《MariaDB Cookbook》,均由 Packt 出版。