對于任何一個企業來說,數據安全的重要性是不言而喻的。我在開篇詞中也曾經強調過,凡是涉及到數據的問題,都是損失慘重的大問題。
能夠影響數據安全的事件,都是極小概率的事件,比如說:數據庫宕機、磁盤損壞甚至機房著火,還有最近頻繁出現在段子中“程序員不滿老板刪庫跑路”的梗兒,但這些事兒一旦發生了,我們的業務就會損失慘重。
一般來說,存儲系統導致的比較嚴重的損失主要有兩種情況,一是數據丟失造成的直接財產損失,比如大量的壞賬;二是由于存儲系統損壞,造成整個業務系統停止服務而帶來的損失。
所謂防患于未然,你從設計一個系統的第一天起,就需要考慮在出現各種問題的時候,如何來保證這個系統的數據安全性。今天我們來聊一聊,如何提前預防,將“刪庫跑路”等這類問題導致的損失盡量降到最低。
如何更安全地做數據備份和恢復?
保證數據安全,最簡單而且有效的手段就是定期備份數據,這樣出現任何問題導致的數據損失,都可以通過備份來恢復數據。但是,如何備份,才能最大程度地保證數據安全,并不是一個簡單的事兒。
2018 年還出現過某個著名的云服務商因為硬盤損壞,導致多個客戶數據全部丟失的重大故障。這么大的云服務商,數據是不可能沒有備份的,按說硬盤損壞,不會導致數據丟失的,但是因為各種各樣的原因,最終的結果是數據的三個副本都被刪除,數據丟失無法找回。
所以說,不是簡單地定期把數據備份一下就可以高枕無憂了。接下來我們還是以大家最常用的 MySQL 為例來說一下,如何更安全地來做數據備份和恢復。
簡單的備份方式就是全量備份。備份的時候,把所有的數據復制一份,存放到文件中,恢復的時候再把文件中的數據復制回去,這樣可以保證恢復之后數據庫中的數據和備份時是完全一樣的。在 MySQL 中,你可以使用mysqldump命令來執行全量備份。
比如我們要全量備份數據庫 test:
$mysqldump -uroot -p test > test.sql
備份出來的文件就是一個 SQL 文件,就是創建數據庫、表,寫入數據等等這些 SQL,如果要恢復數據,直接執行這個備份的 SQL 文件就可以了:
$mysql -uroot test < test.sql
不過,全量備份的代價非常高,為什么這么說呢?
首先,備份文件包含數據庫中的所有數據,占用的磁盤空間非常大;其次,每次備份操作都要拷貝大量數據,備份過程中會占用數據庫服務器大量的 CPU、磁盤 IO 資源,并且為了保證數據一致性,還有可能會鎖表,這些都會導致備份期間,數據庫本身的性能嚴重下降。所以,我們不能經常對數據庫執行全量備份。
一般來說,每天執行一次全量備份已經是非常頻繁了。那這就意味著,如果數據庫中的數據丟了,那只能恢復到最近一次全量備份的那個時間點,這個時間點之后的數據還是丟了。也就是說,全量備份不能做到完全無損地恢復。
既然全量備份代價太高,不能頻繁執行,那有沒有代價低一點兒的備份方法,能讓我們少丟甚至不丟數據呢?還真有,那就是增量備份。相比于全量備份,增量備份每次只備份相對于上一次備份變化的那部分數據,所以每次增量備份速度更快。
MySQL 自帶了 Binlog,就是一種實時的增量備份。Binlog 里面記錄的就是 MySQL 數據的變更的操作日志,開啟 Binlog 之后,我們對 MySQL 中的每次更新數據操作,都會被記錄到 Binlog 中。
Binlog 是可以回放的,回放 Binlog,就相當于把之前對數據庫所有數據更新操作按照順序重新執行了一遍,回放完成之后數據自然就恢復了。這就是 Binlog 增量備份的基本原理。很多數據庫都有類似于 MySQL Binlog 的日志,原理和 Binlog 是一樣的,備份和恢復方法也是類似的。
下面通過一個例子看一下如何使用 Binlog 進行備份和恢復。首先使用“show variables like ‘%log_bin%’”命令確認一下是否開啟了 Binlog 功能:
mysql> show variables like '%log_bin%';
+---------------------------------+-----------------------------------+
| Variable_name | Value |
+---------------------------------+-----------------------------------+
| log_bin | ON |
| log_bin_basename | /usr/local/var/mysql/binlog |
+---------------------------------+-----------------------------------+
mysql> show master status;
+---------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+-------------------+
| binlog.000001 | 18745 | | | |
+---------------+----------+--------------+------------------+-------------------+
以看到當前這個數據庫已經開啟了 Binlog,log_bin_basename 表示 Binlog 文件在服務器磁盤上的具體位置。然后用“show master status”命令可查看當前 Binlog 的狀態,顯示正在寫入的 Binlog 文件,及當前的位置。假設我們每天凌晨用 mysqldump 做一個全量備份,然后開啟了 Binlog,有了這些,我們就可以把數據恢復到全量備份之后的任何一個時刻。
下面我們做一個簡單的備份恢復演示。我們先模擬一次“刪庫跑路”的場景,直接把賬戶余額表清空:
mysql> truncate table account_balance;
Query OK, 0 rows affected (0.02 sec)
mysql> select * from account_balance;
Empty set (0.00 sec)
然后我們來進行數據恢復,首先執行一次全量恢復,把數據庫恢復到今天凌晨的狀態。
$mysql -uroot test < dump.sql
mysql> select * from account_balance;
+---------+---------+---------------------+--------+
| user_id | balance | timestamp | log_id |
+---------+---------+---------------------+--------+
| 0 | 100 | 2020-02-13 20:24:33 | 3 |
+---------+---------+---------------------+--------+
可以看到,表里面的數據已經恢復了,但還是比較舊的數據。然后我們再用 Binlog 把數據恢復到刪庫跑路之前的那個時刻:
$mysqlbinlog --start-datetime "2020-02-20 00:00:00" --stop-datetime "2020-02-20 15:09:00" /usr/local/var/mysql/binlog.000001 | mysql -uroot
mysql> select * from account_balance;
+---------+---------+---------------------+--------+
| user_id | balance | timestamp | log_id |
+---------+---------+---------------------+--------+
| 0 | 200 | 2020-02-20 15:08:12 | 0 |
+---------+---------+---------------------+--------+
這時候,數據已經恢復到當天的 15 點了。通過定期的全量備份,配合 Binlog,我們就可以把數據恢復到任意一個時間點,再也不怕程序員刪庫跑路了。
在執行備份和恢復的時候,有幾個要點你需要特別的注意。
第一,也是最重要的,“不要把所有的雞蛋放在同一個籃子中”,無論是全量備份還是 Binlog,都不要和數據庫存放在同一個服務器上。最好能做到不同機房,甚至不同城市,離得越遠越好。這樣即使出現機房著火、光纜被挖斷甚至地震也不怕。
第二,在回放 Binlog 的時候,指定的起始時間可以比全量備份的時間稍微提前一點兒,確保全量備份之后的所有操作都在恢復的 Binlog 范圍內,這樣可以保證恢復的數據的完整性。
配置 MySQL HA 實現高可用
通過全量備份加上 Binlog,我們可以將數據庫恢復到任何一個時間點,這樣至少不會丟數據了。如果說,數據庫服務器宕機了,因為我們有備份數據,完全可以啟動一個新的數據庫服務器,把備份數據恢復到新的數據庫上,這樣新的數據庫就可以替代宕機的數據庫,繼續提供服務。
但是,這個恢復數據的時間是很長的,如果數據量比較大的話,有可能需要恢復幾個小時。這幾個小時,我們的系統是一直不可用的,這樣肯定不行。
這個問題怎么解決?很簡單,你不要等著數據庫宕機了,才開始做恢復,我們完全可以提前來做恢復這些事兒。
我們準備一臺備用的數據庫,把它的數據恢復成主庫一樣,然后實時地在主備數據庫之間來同步 Binlog,主庫做了一次數據變更,生成一條 Binlog,我們就把這一條 Binlog 復制到備用庫并立即回放,這樣就可以讓備用庫里面的數據和主庫中的數據一直保持是一樣的。一旦主庫宕機,就可以立即切換到備用庫上繼續提供服務。這就是 MySQL 的高可用方案,也叫 MySQL HA。
MySQL 自身就提供了主從復制的功能,通過配置就可以讓一主一備兩臺 MySQL 的數據庫保持數據同步。
接下來我們說這個方案的問題。當我們對主庫執行一次更新操作的時候,主從兩個數據庫更新數據實際的時序是這樣的:
- 在主庫的磁盤上寫入 Binlog;
- 主庫更新存儲引擎中的數據;
- 給客戶端返回成功響應;
- 主庫把 Binlog 復制到從庫;
- 從庫回放 Binlog,更新存儲引擎中的數據。
也就是說,從庫的數據是有可能比主庫上的數據舊一些的,這個主從之間復制數據的延遲,稱為“主從延遲”。正常情況下,主從延遲基本都是毫秒級別,你可以認為主從就是實時保持同步的。麻煩的是不正常的情況,一旦主庫或者從庫繁忙的時候,有可能會出現明顯的主從延遲。
而很多情況下,數據庫都不是突然宕機的,而是先繁忙,性能下降,最終宕機。這種情況下,很有可能主從延遲很大,如果我們把業務直接切到從庫上繼續讀寫,主從延遲這部分數據就丟了,并且這個數據丟失是不可逆的。即使事后你找回了當時主庫的 Binlog 也是沒法做到自動恢復的,因為它和從庫的數據是沖突的。
簡單地說,如果主庫宕機并且主從存在延遲的情況下,切換到從庫繼續讀寫,可以保證業務的可用性,但是主從延遲這部分數據就丟失了。
這個時候你就需要做一個選擇題了,第一個選項是,保證不丟數據,犧牲可用性,暫時停止服務,想辦法把主庫的 Binlog 恢復到從庫上之后再提供服務。第二個選項就是,冒著丟一些數據的風險,保證可用性,第一時間切換到從庫繼續提供服務。
那能不能既保證數據不丟,還能做到高可用呢?也是可以的,那你就要犧牲一些性能。MySQL 也支持同步復制,開啟同步復制時,MySQL 主庫會等待數據成功復制到從庫之后,再給客戶端返回響應。
如果說,犧牲的這點兒性能我不在乎,這個方案是不是就完美了呢?也不是,新的問題又來了!你想一下,這種情況下從庫宕機了怎么辦?本來從庫宕機對主庫是完全沒影響的,因為現在主庫要等待從庫寫入成功再返回,從庫宕機,主庫就會一直等待從庫,主庫也卡死了。
這個問題也有解決辦法,那就是再加一個從庫,把主庫配置成:成功復制到任意一個從庫就返回,只要有一個從庫還活著,就不會影響主庫寫入數據,這樣就解決了從庫宕機阻塞主庫的問題。如果主庫發生宕機,在兩個從庫中,至少有一個從庫中的數據是和主庫完全一樣的,可以把這個庫作為新的主庫,繼續提供服務。為此你需要付出的代價是,你要至少用三臺數據庫服務器,并且這三臺服務器提供的服務性能,還不如一臺服務器高。
我把上面這三種典型的 HA 方案總結成下面這個表格,便于你對比選擇:
小結
今天這節課講了兩件事兒,一是如何備份和恢復數據庫中的數據,確保數據安全;二是如何來實現數據庫的高可用,避免宕機停服。
雖然這是兩個不同的問題,但你要知道,解決這兩個問題背后的實現原理是一樣的。高可用依賴的是數據復制,數據復制的本質就是從一個庫備份數據,然后恢復到另外一個庫中去。
數據備份時,使用低頻度的全量備份配合 Binlog 增量備份是一種常用而且非常實用的方法,使用這種備份方法,我們可以把數據庫的數據精確地恢復到歷史上任意一個時刻,不僅能解決數據損壞的問題,也不用怕誤操作、刪庫跑路這些事兒了。特別要注意的是,讓備份數據盡量地遠離數據庫。我們今天講到的幾種 MySQL 典型的 HA 方案,在數據可靠性、數據庫可用性、性能和成本幾個方面,各有利弊,你需要根據業務情況,做一個最優的選擇,并且為可能存在的風險做好準備。