Hi, I am Steven Huang (@steventtud).
A backend developer working for Splashtop.
Splashtop 是一家遠端桌面公司。
(remote desktop access solution)
OK! 接下來回到今天的主題!
migration 中針對已經建立的的 table 做修改的動作,就會使用到 Mysql 的 alter table 指令。
a migration
change_table(:users) do |t|
t.string :company_name
t.change :birthdate, :datetime
end
輸入 rake db:migrate
後會產生這樣的 sql 指令
ALTER TABLE `users` ADD `im_handle` varchar(255)
ALTER TABLE `users` ADD `company_id` int(11)
ALTER TABLE `users` CHANGE `updated_at` `updated_at` datetime DEFAULT NULL
從以上可以了解
alter table 就是修改已存在的 table
資料量小的時候 alter table 很簡單
但如果 Table 資料量達千萬級(rows > 10 millions)
而且有讀寫分離的情況下(has read replicas)
Alter Table 可能成為你的惡夢。
(altering table would be your nightmare)
這次的 Talk 將會分享如何克服讀寫分離的 slave lag,成功的alter table 大型資料庫。
通常我們不會透漏公司資料量的規模
但是我剛剛在逛官網的時候…
這樣大致了解目前敝公司處理的資料量
Solution:
lhm - Online MySQL schema migrations
一般的 migration 會使用 alter table
來修改資料庫
在 alter table 時,原本的 Table 是無法寫入的。
新 Table 修改完畢後,將原本的 Table 與新 Table 交換名稱,並將原始的 Table 刪除。
Lhm is a solution for this situation.
Lhm 的作法是使用 MySQL Trigger,當原始 Table 寫入的時候,mysql trigger 會同步到新的 Database。
等到結束的時候兩個 Table 互換,write lock 持續的時間很短。
Example of lhm migration
require 'lhm'
class MigrateUsers < ActiveRecord::Migration
def self.up
Lhm.change_table :users do |m|
m.add_column :arbitrary, "INT(12)"
m.add_index [:arbitrary_id, :created_at]
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
end
end
def self.down
Lhm.change_table :users do |m|
m.remove_index [:arbitrary_id, :created_at]
m.remove_column :arbitrary
end
end
end
What is read replica?
在大型系統中,為了讓主要的 Database 記憶體和CPU的使用量更穩定,通常會使用讀寫分離來分擔壓力
如果你在讀寫分離的 Database 中使用 lhm,並且沒有實際去了解 lhm 的機制。很可能遇到的問題是:
lhm 在新版中推出了一個 SlaveLag Throttler
SlaveLag Throttler 大致上的概念是:
這邊可以看 Lhm::Throttler::SlaveLag 的原始碼
其中最關鍵的一段
def execute
sleep(throttle_seconds)
end
def throttle_seconds
lag = max\_current\_slave_lag
if lag > @allowed_lag && @timeout_seconds < MAX_TIMEOUT
Lhm.logger.info("Increasing timeout between strides from #{@timeout_seconds} to #{@timeout_seconds * 2} because #{lag} seconds of slave lag detected is greater than the maximum of #{@allowed_lag} seconds allowed.")
@timeout_seconds = @timeout_seconds * 2
elsif lag <= @allowed_lag && @timeout_seconds > INITIAL_TIMEOUT
Lhm.logger.info("Decreasing timeout between strides from #{@timeout_seconds} to #{@timeout_seconds / 2} because #{lag} seconds of slave lag detected is less than or equal to the #{@allowed_lag} seconds allowed.")
@timeout_seconds = @timeout_seconds / 2
else
@timeout_seconds
end
end
步驟拆解
初始化:
# INITIAL_TIMEOUT 是最小的等待時間,同時也是執行初始的等待時間。
INITIAL_TIMEOUT = 0.1
# MAX_TIMEOUT 是最長的等待時間,後面我用100秒來簡化之。
MAX_TIMEOUT = 0.1 * INITIAL_TIMEOUT
# allow_lag 是我們允許 slave 的延遲時間。
# 這是我們唯一可以自行設定的值。
allow_lag = 10
# 初始的等待時間
timeout_seconds = INITIAL_TIMEOUT
每次執行的時候會偵測最大的 slave lag
# 現在最大的 slave lag
lag = max_current_slave_lag
第一次
如果 lag > allow_lag 且 @timeout_seconds < MAX_TIMEOUT(100秒)
@timeout_seconds = 0.1 * 2 = 0.2
睡 0.2 秒
第二次
如果 lag > allow_lag 且 @timeout_seconds < MAX_TIMEOUT(100秒)
@timeout_seconds = 0.2 * 2 = 0.4
睡 0.4 秒
第三次
如果 lag > allow_lag 且 @timeout_seconds < MAX_TIMEOUT(100秒)
@timeout_seconds = 0.4 * 2 = 0.8
睡 0.8 秒
第4次
如果 lag > allow_lag 且 @timeout_seconds < MAX_TIMEOUT(100秒)
@timeout_seconds = 0.8 * 2 = 1.6
睡 1.6 秒
第5次
如果 lag <= allow_lag 且 @timeout_seconds > INITIAL_TIMEOUT(0,1)
@timeout_seconds = 0.8 / 2 = 0.8
睡 0.8
95%以上都會如前面兩個方式執行,例外為:
most pictures in this slide are from a article Rails Migrations at Scale
Thanks for listening.