1. 程式人生 > >DBA 小記 — 分庫分表、主從、讀寫分離

DBA 小記 — 分庫分表、主從、讀寫分離

場景 歸並排序 ade 優化 ive names shard 能力 syn

前言

我在上篇博客 “Spring Boot 的實踐與思考” 中比對不同規範的 ORM 框架應用場景的時候提到過主從與讀寫分離,本篇隨筆將針對此和分庫分表進行更深入地探討。
技術分享圖片

1. 漫談

在進入正題之前,我想先隨意談談對架構的拓展周期的想法(僅個人觀點)。首先,我認為初期規劃不該太復雜或者龐大,無論項目的中長期可能會發展地如何如何,前期都應該以靈活為優先,像分庫分表等操作不應該在開始的時候就考慮進去。其次,我認為需求變更是非常正常的,這點在我等開發的圈子裏吐槽的最多,其中自然有 “領導們” 在業務方面欠缺整體考慮的因素,但我們也不該局限在一個觀點內,市場中變則通,不變則死,前期更是如此,因此在前幾版的架構中我們必須要考慮較高的可擴展性。最後,當項目經過幾輪市場的洗禮和叠代開發,核心業務趨於穩定了,此時我們再結合中長期的規劃給系統來一次重構,細致地去劃分領域邊界,該解耦的解耦,該拆分的拆分。

2. 分庫分表

2.1 概述

當數據庫達到一定規模後(比如說大幾千萬以上),切分是必須要考慮的。一般來說我們首先要進行垂直切分,即按業務分割,比如說用戶相關、訂單相關、統計相關等等都可以單獨成庫。圖片來源 →

技術分享圖片

但僅僅如此這是完全不夠的,垂直切分雖然剝離了一定的數據,但每個業務還是那個數量級,因此我們還得采取水平切分進一步分散數據,這也是本節論述的重點。
技術分享圖片

分庫分表的優點相信上述兩圖都一目了然了,一個是專庫專用,業務更集中,另一個是提升數據庫服務的負載能力。But there are always two sides to a coin。 從此以後你要接受你的系統復雜度將提升一個檔次,叠代、遷移、運維等都不再容易。

2.2 切分策略

垂直切分在實現上就是一個多數據源的問題,沒啥好講的。以下 Demo 為水平切分,基於 Sharding-JDBC 中間件,我只做邏輯上的陳述,有關其更詳細的信息和配置請移步 “官方文檔”。

首先,我們得在配置文件中定義分片策略,application.yml:

server:
  port: 8001
  
mybatis:
  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mappers/*.xml

sharding:
  jdbc:
    datasource:
      names: youclk_0,youclk_1
      youclk_0:
        type: org.apache.commons.dbcp.BasicDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://mysql:3306/youclk_0?useSSL=false
        username: root
        password: youclk
      youclk_1:
        type: org.apache.commons.dbcp.BasicDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://mysql:3306/youclk_1?useSSL=false
        username: root
        password: youclk
    config:
      sharding:
        default-database-strategy:
          inline:
            sharding-column: number
            algorithm-expression: youclk_${number % 2}
        tables:
          user:
            actual-data-nodes: youclk_${0..1}.user

具體每個參數的含義在官方文檔有詳細解釋,其實看名稱也能理解個大概了,我定義將 number 為偶數的數據存入 youclk_0,奇數存入 youclk_1。

User:

@Data
public class User {
    private String id;
    private Integer number;
    private Date createTime;
}

UserRepository:

@Mapper
public interface UserRepository {
    void insert(User user);
}

UserMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youclk.data.repository.UserRepository">
    <resultMap id="BaseResultMap" type="com.youclk.data.entity.User">
        <id column="id" property="id" jdbcType="CHAR"/>
        <result column="number" property="number" jdbcType="INTEGER"/>
        <result column="createTime" property="create_time" jdbcType="DATE"/>
    </resultMap>

    <sql id="Base_Column_List">
        id, number, createTime
    </sql>
    
    <insert id="insert">
        INSERT INTO user (
          id, number
        )
        VALUES (
            uuid(),
            #{number,jdbcType=INTEGER}
        )
    </insert>
</mapper>

UserService:

@Service
public class UserService {

    @Resource
    private UserRepository userRepository;

    public void insert() {
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setNumber(i);
            userRepository.insert(user);
        }
    }
}

Result:
技術分享圖片
技術分享圖片

以上做了一個簡單的循環插入,可以看到數據已經按策略分庫存儲,結果符合我們的預期。

分庫之後在查詢方面要比之前更加謹慎,既然按策略去切了,那最好就是按策略去查,否則...比如我水平切分了 100個庫,若不按策略去查詢 LIMIT 100000, 10 這麽一組數據,那最後掃描的數量級別是 100 * (100000 + 10), 這是比較恐怖的,雖然 Sharding-JDBC 做了一些優化,比如他不是一次性去查詢到內存中,而是采用流式處理 + 歸並排序的方式,但仍然比較耗資源,能避免還是盡量去避免吧。

2.3 分布式事務

在任何系統中事務都是頂要緊的事情,面對已分庫的系統更是如此,保證誇庫事務的安全從來不容易。分布式事務的場景有兩種,一個是在分布式服務中,這個後續有機會再探討,本節重點關註誇庫事務。

Sharding-JDBC 自動包含了弱XA事務支持,即能夠保證邏輯上的事務安全,但因網絡或硬件導致的異常無法回滾,實現上與一般事務無異:

@Test
@Transactional
public void insertTest() {
    userService.insert();
    int error = Integer.parseInt("I want error");
    userService.insert();
}

技術分享圖片

可以看到誇庫事務已回滾,除此之外 Sharding-JDBC 還提供了最大努力送達型柔性事務(將執行過程記錄到日誌中,失敗重試,成功後刪除,若最終還是失敗則保留事務日誌,供人工幹預),雖然安全性更高,但無法保證時效,限制也很多,這裏留個待續吧,後續有空再深入探討(主要是比較晚了,想早點寫完休息??)。

3. 主從與讀寫分離

3.1 概述

為什麽要做主從?我們先來探討以下這幾個場景:

  • 我們知道每臺數據庫服務器有他的最大連接數和 IOPS,若有一天他無法再滿足我們的業務需求,那相比於在單臺服務器上去做性能堆疊,是是否橫向去擴展幾臺 Slave 去分擔 Master 的壓力更加合理。
  • 如果服務對數據庫的需求是 IO 密集型的,那可能會經常遇到行鎖等待等問題,若要魚與熊掌兼得,讀寫分離是否是更好的選擇。
  • 如果我們的系統需要做很多報表,或者統計和數據分析,這些業務往往相當地耗費資源但又不是很重要,那針對此,我們是否應該開幾臺 Slave,讓他們去小黑屋裏慢慢執行,別來影響我處理核心業務的效率。

我大致能想到這麽幾點,歡迎各位繼續留言補充。

3.2 主從部署

我以 MySQL 為例,一般部署架構為一臺 Master 和 n 臺 Slave,Master 的主責為寫,並將數據同步至 Slave,Slave 主要提供查詢功能。

為了測試方便,我直接使用 Docker 來部署,首先創建主從的配置文件,master.cnf:

[mysqld]
server_id = 1

character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default-storage-engine=INNODB

#Optimize omit

sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

log-bin = /var/lib/mysql/binlog
log_bin_trust_function_creators=1
binlog_format = ROW
expire_logs_days = 99
sync_binlog = 0

slow-query-log=1
slow-query-log-file=/var/log/mysql/slow-queries.log
long_query_time = 3
log-queries-not-using-indexes

slave.cnf:

[mysqld]
server_id = 2

character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default-storage-engine=INNODB

#Optimize omit

sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

log-bin = /var/lib/mysql/binlog
log_bin_trust_function_creators=1
binlog_format = ROW
expire_logs_days = 99
sync_binlog = 0

relay_log=slave-relay-bin
log-slave-updates=1
slave-skip-errors=all

slow-query-log=1
slow-query-log-file=/var/log/mysql/slow-queries.log
long_query_time = 3

然後進行 compose 編排,加入 warm 集群,docker-compose.yml:

version: ‘3.5‘

services:

  mysql-master:
    image: mysql
    ports:
      - 3301:3306
    networks:
      - proxy
      - youclk
    volumes:
      - /Users/Jermey/Documents/data/db/cluster/master/mysql:/var/lib/mysql
      - /Users/Jermey/Documents/data/db/cluster/master/conf.d:/etc/mysql/conf.d
    environment:
      MYSQL_ROOT_PASSWORD: youclk

  mysql-slave:
    image: mysql
    ports:
      - 3302:3306
    networks:
      - proxy
      - youclk
    volumes:
      - /Users/Jermey/Documents/data/db/cluster/slave/mysql:/var/lib/mysql
      - /Users/Jermey/Documents/data/db/cluster/slave/conf.d:/etc/mysql/conf.d
    environment:
      MYSQL_ROOT_PASSWORD: youclk

networks:
  proxy:
    external: true
  youclk:
    external: true

再次感激 Docker, 從編排配置文件到最後啟動服務整個過程不到一分鐘:
技術分享圖片

接下來就是配置主從關系:

docker exec -it cluster_mysql-master mysql -p

CREATE USER ‘reader‘@‘%‘ IDENTIFIED BY ‘youclk‘;
GRANT REPLICATION SLAVE ON *.* TO ‘reader‘@‘%‘;

show master status\G
docker exec -it cluster_mysql-slave mysql -p

CHANGE MASTER TO MASTER_HOST=‘mysql-master‘,MASTER_PORT=3306,MASTER_USER=‘reader‘,MASTER_PASSWORD=‘youclk‘,MASTER_LOG_FILE=‘binlog.000004‘,MASTER_LOG_POS=154;

start slave;

show slave status\G

Test:
技術分享圖片
上圖中左邊連的是 Master, 右邊為 Slave, 我在 Master 中執行 create database youclk_0; 可以看到 Slave 中也生成了 youclk_0,至此主從配置測試完成。

3.3 讀寫分離

基於 Sharding-JDBC 的讀寫分離實現非常簡單,改一下配置文件,其余幾乎是無感知的,application.yml:

server:
  port: 8001

mybatis:
  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mappers/*.xml

sharding:
  jdbc:
    datasource:
      names: ds_master,ds_slave
      ds_master:
        type: org.apache.commons.dbcp.BasicDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://mysql:3301/youclk_0?useSSL=false
        username: root
        password: youclk
      ds_slave:
        type: org.apache.commons.dbcp.BasicDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://mysql:3302/youclk_0?useSSL=false
        username: root
        password: youclk
    config:
      masterslave:
        load-balance-algorithm-type: round_robin
        name: ds_ms
        master-data-source-name: ds_master
        slave-data-source-names: ds_slave
      sharding:
        props:
          sql.show: true

Test:

@Test
public void selectAndInsertTest() {
    userService.selectAll();
    userService.insert();
}

Result:
技術分享圖片

跟蹤 MySQL 的日誌可以發現主從庫分別執行了插入與查詢,實現了讀寫分離。

DBA 小記 — 分庫分表、主從、讀寫分離