1. 主從復制&讀寫分離 簡介
- 主從同步延遲
- 分配機制
- 解決單點故障
- 總結
2. 主從復制&讀寫分離 搭建
- 搭建主從復制(雙主)
- 搭建讀寫分離
3. 分庫分表 簡介
1. 主從復制&讀寫分離 簡介
讀寫分離顧名思義就是讀和寫分離,對應到數據庫集群一般都是一主一從(一個主庫,一個從庫)或者一主多從(一個主庫,多個從庫),業務服務器把需要寫的操作都寫到主數據庫中,讀的操作都去從庫查詢。主庫會同步數據到從庫保證數據的一致性。
這種集群方式的本質是把訪問的壓力從主庫轉移到從庫,也就是在單機數據庫無法支撐並發讀寫,並且讀的請求很多的情況下適合這種讀寫分離的數據庫集群。如果寫的操作很多的話不適合這種集群方式,因為你的數據庫壓力還是在寫操作上,即使主從了之后壓力還是在主庫上,這樣和單機的區別就不大了。
在單機的情況下,一般我們做數據庫優化都會加索引,但是加了索引對查詢有優化,但會影響寫入,因為寫入數據會更新索引。所以做了主從之后,我們可以單獨地針對從庫(讀庫)做索引上的優化,而主庫(寫庫)可以減少索引而提高寫的效率。
實現原理
初始狀態時,master 和 slave 的數據要保持一致。
- master 提交完事務后,寫入 binlog。
- slave 連接到 master,獲取 binlog。
- master創建 dump 線程,推送 binlog 到 slave。
- slave 啟動一個 I/O 線程讀取同步過來的 master 的 binlog,記錄到 relay log(中繼日志)中。
- slave 再開啟一個 SQL 線程從 relay log 中讀取內容並在 slave 執行(從 Exec_Master_Log_Pos 位置開始執行讀取到的更新事件),完成同步。
- slave 記錄自己的 binlog。
由於 MySQL 默認的復制方式是異步的,主庫把日志發送給從庫后不關心從庫是否已經處理。這樣會產生一個問題,假設主庫掛了,從庫處理失敗了,這時從庫升為主庫后,日志就丟失了。由此產生以下兩個概念:
全同步復制
主庫寫入 binlog 后,強制同步日志到從庫,等所有的從庫都執行完成后,才返回結果給客戶端,顯然這個方式的性能會受到嚴重影響。
半同步復制
從庫寫入日志成功后返回 ACK(確認)給主庫,主庫收到至少一個從庫的確認就可以認為寫操作完成,返回結果給客戶端。
主從同步延遲
主庫有數據寫入之后,同時也寫入在 binlog(二進制日志文件)中,從庫是通過 binlog 文件來同步數據的,這期間會有一定時間的延遲,可能是 1 秒,如果同時有大量數據寫入的話,時間可能更長。
這會導致什么問題呢?比如有一個付款操作,你付款了,主庫是已經寫入數據,但是查詢是到從庫查,從庫里還沒有你的付款記錄,所以頁面上查詢的時候你還沒付款。那可不急眼了啊,吞錢了這還了得!打電話給客服投訴!
所以為了解決主從同步延遲的問題有以下幾個方法:
1)二次讀取
二次讀取的意思就是讀從庫沒讀到之后再去主庫讀一下,只要通過對數據庫訪問的 API 進行封裝就能實現這個功能。很簡單,並且和業務之間沒有耦合。但是有個問題,如果有很多二次讀取相當於壓力還是回到了主庫身上,等於讀寫分離白分了。而且如有人惡意攻擊,就一直訪問沒有的數據,那主庫就可能爆了。
2)寫之后的馬上的讀操作訪問主庫
也就是寫操作之后,立馬的讀操作指定為訪問主庫,之后的讀操作則訪問從庫。這就等於寫死了,和業務強耦合了。
3)關鍵業務讀寫都由主庫承擔,非關鍵業務讀寫分離
類似付錢的這種業務,讀寫都到主庫,避免延遲的問題,但是例如改個頭像啊,個人簽名這種比較不重要的就讀寫分離,查詢都去從庫查,畢竟延遲一下影響也不大,不會立馬打客服電話投訴。
分配機制
分配機制的考慮也就是怎么制定寫操作是去主庫寫,讀操作是去從庫讀。
一般有兩種方式:代碼封裝、數據庫中間件。
1)代碼封裝
代碼封裝的實現很簡單,就是抽出一個中間層,讓這個中間層來實現讀寫分離和數據庫連接。講白點就是搞個 provider 封裝了 save、select 等通常數據庫操作,內部 save 操作的 dataSource 是主庫的,select 操作的 dataSource 是從庫的。
優點:
- 實現簡單。
- 可以根據業務定制化變化,隨心所欲。
缺點:
- 如果哪個數據庫宕機了,發生主從切換了之后,就得修改配置重啟。
- 如果系統很大,一個業務可能包含多個子系統,一個子系統是 java 寫的,一個子系統用 go 寫的,這樣的話得分別為不同語言實現一套中間層,重復開發。
2)數據庫中間件
就是有一個獨立的系統,專門來實現讀寫分離和數據庫連接管理,業務服務器和數據庫中間件之間是通過標准的 SQL 協議交流的,所以在業務服務器看來數據庫中間件其實就是個數據庫。
優點:
- 因為是通過 SQL 協議的所以可以兼容不同的語言不需要單獨寫一套。
- 由中間件來實現主從切換,業務服務器不需要關心這點。
缺點:
- 多了一個系統其實就等於多了一個關心,比如數據庫中間件掛了。
- 多了一個系統就等於多了一個瓶頸,所以對中間件的性能要求也高,因為所有的數據庫操作都要先經過它。
- 中間件實現較為復雜,難度比代碼封裝高多了。
常用的開源數據庫中間件有 Mysql Proxy、Atlas、LVS 等。
為什么使用 MySQL-Proxy 而不是 LVS?
- LVS:分不清讀還是寫;不支持事務。
- MySQL-Proxy:自動區分讀操作和寫操作;支持事務(注意在 MySQL-Proxy 中不要使用嵌套查詢,否則會造成讀和寫的混亂)。
解決單點故障
解決 Proxy 單點故障問題
MySQL-Proxy 實際上非常不穩定,在高並發或有錯誤連接的情況下,進程很容易自動關閉,因此打開 --keepalive 參數讓進程自動恢復是個比較好的辦法,但還是不能從根本上解決問題,通常最穩妥的做法是在每個應用服務器(如 Tomcat)上安裝一個 MySQL-Proxy 供自身使用(解決 Proxy 單點故障問題),雖然比較低效但卻能保證穩定性。
雙(多)主機制
Proxy 之后搭 LVS,LVS 為兩台主數據庫做負載均衡,從數據庫從兩台主數據庫同步。此方案旨在解決:
- 主數據庫的單點故障問題(服務不可用、備份問題)
- 分擔寫操作的訪問壓力。
不過多主需要考慮自增長 ID 問題,這個需要特別設置配置文件,比如雙主可以使用奇偶。總之,主之間設置自增長 ID 相互不沖突就能解決自增長 ID 沖突問題。
總結
讀寫分離相對而言是比較簡單的,比分表分庫簡單,但是它只能分擔訪問的壓力,分擔不了存儲的壓力,也就是你的數據庫表的數據逐漸增多,但是面對一張表海量的數據,查詢還是很慢的,所以如果業務發展的快數據暴增,到一定時間還是得分庫分表。
正常情況下,只有當單機真的頂不住壓力了才會集群,不要一上來就集群,沒這個必要。有關於軟件的東西都是越簡單越好,復雜都是形勢所迫。
一般我們是先優化單機,如優化一些慢查詢,優化業務邏輯的調用或者加入緩存等。如果真的優化到沒東西優化了,然后才上集群。先讀寫分離,讀寫分離之后頂不住就再分庫分表。
2. 主從復制&讀寫分離 搭建
主從同步的方式也分很多種,一主多從、鏈式主從、多主多從,根據你的需要來進行設置。
搭建主從復制(雙主)
1)服務器二台:分別安裝 MySQL 數據庫
- 安裝命令:yum -y install mysql-server
- 配置登錄用戶的密碼:mysqladmin -u root password '密碼'
- 配置允許第三方機器訪問本機 MySQL:
delete from user where password = ''; update user set host='%'; flush privileges;
2)分別修改 MySQL 配置
配置 masterA:
[root@adailinux ~]# vim /etc/my.cnf [mysqld] datadir = /data/mysql socket = /tmp/mysql.sock server_id = 1 # 指定server-id,必須保證主從服務器的server-id不同 auto_increment_increment = 2 # 設置主鍵單次增量 auto_increment_offset = 1 # 設置單次增量中主鍵的偏移量:奇數 log_bin = mysql-bin # 創建主從需要開啟log-bin日志文件 log-slave-updates # 把更新的日志寫到二進制文件(binlog)中,台服務器既做主庫又做從庫此選項必須要開啟
配置 masterB:
[root@adailinux ~]# vim /etc/my.cnf [mysqld] datadir = /data/mysql socket = /tmp/mysql.sock server_id = 2 # 指定server-id,必須保證主從服務器的server-id不同 auto_increment_increment = 2 # 設置主鍵單次增量 auto_increment_offset = 2 # 設置單次增量中主鍵的偏移量:偶數 log_bin = mysql-bin # 創建主從需要開啟log-bin日志文件 log-slave-updates = True # 把更新的日志寫到二進制文件(binlog)中
以上為同步配置的核心參數。
server_id 有兩個用途:
- 用來標記 binlog event 的源產地,就是 SQL 語句最開始源自於哪里。
- 用於 IO_thread 對主庫 binlog 的過濾。如果沒有設置 replicate-same-server-id=1 ,那么當從庫的 IO_thread 發現 event 的源與自己的 server-id 相同時,就會跳過該 event,不把該 event 寫入到 relay log 中。從庫的 sql_thread 自然就不會執行該 event。這在鏈式或雙主結構中可以避免 sql 語句的無限循環。
注意:如果設置多個從服務器,每個從服務器必須有一個唯一的 server-id 值,且與主服務器的以及其它從服務器的都不相同。
分別重啟 masterA 和 masterB 並查看主庫狀態:
[root@adailinux ~]# /etc/init.d/mysqld restart Shutting down MySQL.. SUCCESS! Starting MySQL. SUCCESS! masterA: [root@adailinux ~]# mysql -uroot mysql> show master status; +------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +------------------+----------+--------------+------------------+-------------------+ | mysql-bin.000001 | 419 | TSC | mysql | | +------------------+----------+--------------+------------------+-------------------+ 1 row in set (0.00 sec) masterB: [root@adailinux ~]# mysql -uroot mysql> show master status -> ; +------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +------------------+----------+--------------+------------------+-------------------+ | mysql-bin.000001 | 419 | TSC | mysql | | +------------------+----------+--------------+------------------+-------------------+ 1 row in set (0.00 sec)
3)配置同步信息
masterA:
[root@adailinux ~]# mysql -uroot mysql> change master to master_host='192.168.8.132',master_port=3306,master_user='repl',master_password='123456',master_log_file='mysql-bin.000001',master_log_pos=419; #注:IP為masterB的IP(即,從服務器的IP) mysql> start slave; Query OK, 0 rows affected (0.05 sec) mysql> show slave status\G; 在此查看有如下狀態說明配置成功: Slave_IO_Running: Yes Slave_SQL_Running: Yes
masterB:
[root@adailinux ~]# mysql -uroot mysql>change master to master_host='192.168.8.131',master_port=3306,master_user='repl',master_password='123456',master_log_file='mysql-bin.000001',master_log_pos=419; Query OK, 0 rows affected, 2 warnings (0.06 sec) mysql> start slave; Query OK, 0 rows affected (0.04 sec) mysql> show slave status\G 在此查看有如下狀態說明配置成功: Slave_IO_Running: Yes Slave_SQL_Running: Yes
4)測試主從同步
在 masterA 上創建一個庫,驗證 masterB 是否同步創建了該庫。
搭建讀寫分離
場景描述:
- 數據庫 Master 主服務器
- 數據庫 Slave 從服務器
- MySQL-Proxy 調度服務器
以下操作,均是在 MySQL-Proxy 調度服務器上進行的。
1)MySQL 服務器安裝
2)檢查/安裝系統所需軟件包
yum -y install gcc* gcc-c++* autoconf* automake* zlib* libxml* ncurses-devel* libmcrypt* libtool* flex* pkgconfig* libevent* glib* readline*
3)編譯安裝 lua
MySQL-Proxy 的讀寫分離主要是通過 rw-splitting.lua 腳本實現的,因此需要安裝 lua。
方式 1:一般系統自帶。
方式 2:手工安裝。
這里我們建議采用源碼包進行安裝:
- cd /opt/install
- wget http://www.lua.org/ftp/lua-5.1.4.tar.gz
- tar zvfxlua-5.2.3.tar.gz
- cd lua-5.1.4
- vi src/Makefile
- 在CFLAGS= -O2 -Wall $(MYCFLAGS) 這一行記錄里加上-fPIC,更改為 CFLAGS= -O2 -Wall -fPIC$(MYCFLAGS) 來避免編譯過程中出現錯誤。
- make linux(編譯到內存)
- make install
4)安裝 Mysql-Proxy
MySQL-Proxy 可通過此網址獲得:http://mysql.cdpa.nsysu.edu.tw/Downloads/MySQL-Proxy/
推薦采用已經編譯好的二進制版本,因為采用源碼包進行編譯時,最新版的 MySQL-Proxy 對 automake、glib 以及 libevent 的版本都有很高的要求,而這些軟件包都是系統的基礎套件,不建議強行進行更新。
並且這些已經編譯好的二進制版本在解壓后都在統一的目錄內,因此建議選擇以下版本:
- 32 位 RHEL5 平台:http://mysql.cdpa.nsysu.edu.tw/Downloads/MySQL-Proxy/mysql-proxy-0.8.4-linux-rhel5-x86-32bit.tar.gz
- 64 位 RHEL5 平台:http://mysql.cdpa.nsysu.edu.tw/Downloads/MySQL-Proxy/mysql-proxy-0.8.4-linux-rhel5-x86-64bit.tar.gz
tar -xzvf mysql-proxy-0.8.3-linux-rhel5-x86-64bit.tar.gz
mv mysql-proxy-0.8.3-linux-rhel5-x86-64bit /opt/mysql-proxy
創建 Mysql-Proxy 服務管理腳本:
cd /opt/mysql-proxy mkdir scripts cp share/doc/mysql-proxy/rw-splitting.lua scripts/ chmod +x /opt/mysql-proxy/scripts mkdir /opt/mysql-proxy/run mkdir /opt/mysql-proxy/log mkdir /opt/mysql-proxy/scripts
5)修改讀寫分離腳本 rw-splitting.lua
修改默認連接,進行快速測試,不修改的話要達到連接數為 4 時才啟用讀寫分離
vi /opt/mysql-proxy/scripts/rw-splitting.lua
-- connection pool if not proxy.global.config.rwsplitthen proxy.global.config.rwsplit = { min_idle_connections = 1, //默認為4 max_idle_connections = 1, //默認為8 is_debug = false } end
proxy.conf:
[mysql-proxy] admin-username=root admin-password=admin proxy-read-only-backend-addresses=192.168.188.143,192.168.188.139 proxy-backend-addresses=192.168.188.142 proxy-lua-script=/opt/mysql-proxy/bin/rw-splitting.lua admin-lua-script=/opt/mysql-proxy/lib/mysql-proxy/lua/admin.lua
bin 目錄下執行:
./mysql-proxy --defaults-file=/opt/mysql-proxy/proxy.conf
mysql-proxy 腳本參數詳解:
- --proxy_path=/opt/mysql-proxy/bin:定義 mysql-proxy 服務二進制文件路徑。
- --proxy-backend-addresses=192.168.10.130:3306:定義后端主服務器地址。
- --proxy-lua-script=/opt/mysql-proxy/scripts/rw-splitting.lua:定義 lua 讀寫分離腳本路徑。
- --proxy-id=/opt/mysql-proxy/run/mysql-proxy.pid:定義 mysql-proxy PID 文件路徑。
- --daemon:定義以守護進程模式啟動。
- --keepalive:使進程在異常關閉后能夠自動恢復。
- --pid-file=$PROXY_PID:定義 mysql-proxy PID 文件路徑。
- --user=mysql:以 mysql 用戶身份啟動服務。
- --log-level=warning:定義 log 日志級別,由高到低分別有(error|warning|info|message|debug)。
- --log-file=/opt/mysql-proxy/log/mysql-proxy.log:定義 log 日志文件路徑。
6)測試讀寫分離
- 登錄 Proxy:mysql -u -p -h<proxy_ip> -P4040
- stop slave
- 在 Proxy 上插數據后,驗證主數據庫新增數據,而從沒有新增數據。
3. 分庫分表 簡介
當訪問用戶越來越多,寫請求暴漲,對於上面的單 Master 節點肯定扛不住,那么該怎么辦呢?多加幾個 Master?不行,這樣會帶來更多的數據不一致的問題,且增加系統的復雜度。那該怎么辦?就只能對庫表進行拆分了。
常見的拆分類型有垂直拆分和水平拆分,一般來說我們拆分的順序是先垂直后水平。
垂直分庫
以表為依據,按照業務歸屬不同,將不同的表拆分到不同的庫中。
基於現在微服務的拆分來說,都是已經做到了垂直分庫了:
垂直分表
以字段為依據,按照字段的活躍性、數據長度等,將表中字段拆到不同的表(主表和擴展表)中。
水平分庫/分表
以字段為依據,按照一定策略(hash、range 等),將一個庫/表中的數據拆分到多個庫/表中。
首先結合業務場景來決定使用什么字段作為分庫/分表字段(sharding_key),比如我們現在日訂單 1000 萬,我們大部分的場景來源於 C 端,那么我們可以用 user_id 作為 sharding_key,數據查詢支持到最近 3 個月的訂單,超過 3 個月的做歸檔處理,那么 3 個月的數據量就是 9 億,可以分 1024 張表,每張表的數據大概就在 100 萬左右。
比如用戶 id 為 100,那我們都經過 hash(100),然后對 1024 取模,就可以落到對應的表上了。
示例
以拼夕夕電商系統為例,一般有訂單表、用戶表、支付表、商品表、商家表等,最初這些表都在一個數據庫里。后來隨着砍一刀帶來的海量用戶,拼夕夕后台扛不住了!於是緊急從阿狸粑粑那里挖來了幾個 P8、P9 大佬對系統進行重構。
- P9 大佬第一步先對數據庫進行垂直分庫,根據業務關聯性強弱,將它們分到不同的數據庫,比如訂單庫,商家庫、支付庫、用戶庫。
- 第二步是對一些大表進行垂直分表,將一個表按照字段分成多表,每個表存儲其中一部分字段。比如商品詳情表可能最初包含了幾十個字段,但是往往最多訪問的是商品名稱、價格、產地、圖片、介紹等信息,所以我們將不常訪問的字段單獨拆成一個表。
由於垂直分庫已經按照業務關聯切分到了最小粒度,但數據量仍然非常大,於是 P9 大佬開始水平分庫,比如可以把訂單庫分為訂單 1 庫、訂單 2 庫、訂單 3 庫……那么如何決定某個訂單放在哪個訂單庫呢?可以考慮對主鍵通過哈希算法計算放在哪個庫。
分完庫,單表數據量任然很大,查詢起來非常慢,P9 大佬決定按日或者按月將訂單分表,叫做日表、月表。
分庫分表同時會帶來一些問題,比如平時單庫單表使用的主鍵自增特性將作廢,因為某個分區庫表生成的主鍵無法保證全局唯一。
經過一番大刀闊斧的重構,拼夕夕恢復了往日的活力,大家又可以愉快的在上面互相砍一刀了。
常用的分庫分表中間件
- sharding-jdbc
- Mycat
分庫分表可能遇到的問題
- 事務問題:使用分布式事務。
- 跨節點 Join 的問題:可以分兩次查詢實現。
- 跨節點的 order by、group by 聚合函數、排序等問題:分別在各個節點上得到結果后在應用程序端進行合並。
- 數據遷移,容量規划,擴容等問題。
- ID 唯一性問題:數據庫被切分后,不能再依賴數據庫自身的主鍵生成機制。
- ……
分表后的 ID 怎么保證唯一性?
因為我們的主鍵默認都是自增的,那么分表之后的主鍵在不同表就肯定會有沖突了。有幾個方案可以考慮:
- 設定步長。比如 1-1024 張表我們可以分別設定 1-1024 的基礎步長,這樣主鍵落到不同的表就不會沖突了。
- 分布式 ID。自己實現一套分布式 ID 生成算法,或者使用開源的比如雪花算法這種。
- 分表后不使用主鍵作為查詢依據,而是每張表單獨新增一個字段作為唯一主鍵使用,比如訂單表訂單號是唯一的,那么不管最終落在哪張表,都可以基於訂單號作為查詢依據,更新也一樣。
分表后非 sharding_key 的查詢怎么處理呢?
- 可以做一個 mapping 表,比如這時候商家要查詢訂單列表怎么辦呢?不帶 user_id 查詢的話總不能掃全表吧?所以我們可以做一個映射關系表,保存商家和用戶的關系,查詢的時候先通過商家查詢到用戶列表,再通過 user_id 去查詢。
- 打寬表。一般而言,商戶端對數據實時性要求並不是很高,比如查詢訂單列表,可以把訂單表同步到離線(實時)數據倉庫,再基於數倉去做成一張寬表,再基於其他如 es 提供查詢服務。
- 數據量不是很大的話,比如后台的一些查詢之類,也可以通過多線程掃表,然后再聚合結果的方式來做,或者異步的形式也是可以的。
架構實現
在代碼層面實現分庫分表邏輯:
使用分布式/分庫分表的中間件:
使用分布式數據庫:
分庫分表算法簡介
路由算法:
路由算法——擴容:
路由算法——非均勻分布:
各庫的服務器性能不一定相同,因此可以根據各庫的性能情況使用非均勻分布。
拆分表的數據訪問——SQL 轉發:
MySQL 集群替代 Oracle 單點:
- 基於表的水平拆分和分布:根據字段值的一致性 Hash 分布。
- 數據查詢方式:根據 where 中的拆分字段分發。
后台數據訪問邏輯層次:
- 一組內的主從數據是同步一致的;
- 每組的數據是不一致的。
解決備機空閑時浪費機器資源的問題:
如下圖所示,可由 12 台機器節省為 6 台。