Group Replication 是一種 Shared-Nothing 的架構,每個節點都會保留一份資料。
雖然支援多點寫入,但實際上系統的吞吐量是由處理能力最弱的那個節點決定的。
如果各個節點的處理能力參差不齊,那處理能力慢的節點就會出現事務堆積。
在事務堆積的時候,如果處理能力快的節點出現了故障,這個時候能否讓處理能力慢的節點(存在事務堆積)接受業務流量呢?
如果不等待堆積事務應用完,直接接受業務流量。
一方面會讀到舊資料,另一方面也容易出現寫衝突。
為什麼容易出現寫衝突呢?因為基於舊資料進行的寫操作,它的 snapshot_version 小於衝突檢測資料庫中對應記錄的 snapshot_version,這個時候,衝突檢測會失敗。
如果等待堆積事務應用完才接受業務流量,又會影響資料庫服務的可用性。
為了避免出現上述兩難場景,Group Replication 引入了流控機制。
在實現上,Group Replication 的流控模組會定期檢查各個節點的事務堆積情況,如果超過一定值,則會觸發流控。
流控會基於上一週期各個節點的事務認證情況和事務應用情況,決定當前節點(注意是當前節點,不是其它節點)下個週期的寫入配額。
超過寫入配額的事務操作會被阻塞,等到下個週期才能執行。
接下來,我們通過原始碼分析下流控的實現原理。
本文主要包括以下幾部分:
預設情況下,節點的狀態資訊是每秒傳送一次(節點的狀態資訊是在 flow_control_step 中傳送的,傳送週期由 group_replication_flow_control_period 決定)。
當接受到其它節點的狀態資訊時,會呼叫 Flow_control_module::handle_stats_data 來處理。
下面我們看看 Flow_control_module::handle_stats_data 函數的處理邏輯。
int Flow_control_module::handle_stats_data(const uchar *data, size_t len,
const std::string &member_id) {
DBUG_TRACE;
int error = 0;
Pipeline_stats_member_message message(data, len);
m_flow_control_module_info_lock->wrlock();
// m_info 是個字典,定義是 std::map<std::string, Pipeline_member_stats>
// 其中,key 是節點的地址,value 是節點的狀態資訊。
Flow_control_module_info::iterator it = m_info.find(member_id);
// 如果 member_id 對應節點的狀態資訊在 m_info 中不存在,則插入。
if (it == m_info.end()) {
Pipeline_member_stats stats;
std::pair<Flow_control_module_info::iterator, bool> ret = m_info.insert(
std::pair<std::string, Pipeline_member_stats>(member_id, stats));
error = !ret.second;
it = ret.first;
}
// 更新節點的統計資訊
it->second.update_member_stats(message, m_stamp);
// 檢查是否需要流控
if (it->second.is_flow_control_needed()) {
++m_holds_in_period;
#ifndef NDEBUG
it->second.debug(it->first.c_str(), m_quota_size.load(),
m_quota_used.load());
#endif
}
m_flow_control_module_info_lock->unlock();
return error;
}
首先判斷節點的狀態資訊是否在 m_info 中存在。如果不存在,則插入。
接著通過 update_member_stats 更新節點的統計資訊。
更新後的統計資訊包括以下兩部分:
當前資料:如 m_transactions_waiting_certification(當前等待認證的事務數),m_transactions_waiting_apply(當前等待應用的事務數)。
上一週期的增量資料:如 m_delta_transactions_certified(上一週期進行認證的事務數)。
m_delta_transactions_certified 等於 m_transactions_certified (這一次的採集資料) - previous_transactions_certified (上一次的採集資料)
最後會通過is_flow_control_needed判斷是否需要流控。如果需要流控,則會將 m_holds_in_period 自增加 1。
如果是 Debug 版本,且將 log_error_verbosity 設定為 3。當需要流控時,會在錯誤紀錄檔中列印以下資訊。
[Note] [MY-011726] [Repl] Plugin group_replication reported: 'Flow control - update member stats: 127.0.0.1:33071 stats certifier_queue 0, applier_queue 20 certified 387797 (308), applied 387786 (289), local 0 (0), quota 400 (274) mode=1'
什麼時候會觸發流控呢?
接下來我們看看 is_flow_control_needed 函數的處理邏輯。
bool Pipeline_member_stats::is_flow_control_needed() {
return (m_flow_control_mode == FCM_QUOTA) &&
(m_transactions_waiting_certification >
get_flow_control_certifier_threshold_var() ||
m_transactions_waiting_apply >
get_flow_control_applier_threshold_var());
}
由此來看,觸發流控需滿足以下條件:
group_replication_flow_control_mode 設定為 QUOTA。
當前等待認證的事務數大於 group_replication_flow_control_certifier_threshold。
當前等待認證的事務數可通過 performance_schema.replication_group_member_stats 中的 COUNT_TRANSACTIONS_IN_QUEUE 檢視。
當前等待應用的事務數大於 group_replication_flow_control_applier_threshold。
當前等待應用的事務數可通過 performance_schema.replication_group_member_stats 中的 COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE 檢視。
除了條件 1,條件 2,3 滿足其一即可。
當需要流控時,會將 m_holds_in_period 自增加 1。
m_holds_in_period 這個變數會用在 Flow_control_module::flow_control_step 中。
而 Flow_control_module::flow_control_step 是在 Certifier_broadcast_thread::dispatcher() 中呼叫的,每秒執行一次。
void Certifier_broadcast_thread::dispatcher() {
...
while (!aborted) {
...
applier_module->run_flow_control_step();
...
struct timespec abstime;
// 定義超時時長 1s。
set_timespec(&abstime, 1);
mysql_cond_timedwait(&broadcast_dispatcher_cond, &broadcast_dispatcher_lock,
&abstime);
mysql_mutex_unlock(&broadcast_dispatcher_lock);
broadcast_counter++;
}
}
void run_flow_control_step() override {
flow_control_module.flow_control_step(&pipeline_stats_member_collector);
}
接下來我們重點分析下 flow_control_step 函數的處理邏輯。
這個函數非常關鍵,它是整個流控模組的核心。
它主要是用來計算 m_quota_size 和 m_quota_used。
其中,m_quota_size 決定了下個週期允許提交的事務數,即我們所說的配額。
m_quota_used 用來統計下個週期已經提交的事務數,在該函數中會重置為 0。
void Flow_control_module::flow_control_step(
Pipeline_stats_member_collector *member) {
// 這裡的 seconds_to_skip 實際上就是 group_replication_flow_control_period,後面會有定義。
// 雖然 flow_control_step 是一秒呼叫一次,但實際起作用的還是 group_replication_flow_control_period。
if (--seconds_to_skip > 0) return;
// holds 即 m_holds_in_period
int32 holds = m_holds_in_period.exchange(0);
// get_flow_control_mode_var() 即 group_replication_flow_control_mode
Flow_control_mode fcm =
static_cast<Flow_control_mode>(get_flow_control_mode_var());
// get_flow_control_period_var() 即 group_replication_flow_control_period
seconds_to_skip = get_flow_control_period_var();
// 計數器
m_stamp++;
// 傳送當前節點的狀態資訊
member->send_stats_member_message(fcm);
switch (fcm) {
case FCM_QUOTA: {
// get_flow_control_hold_percent_var() 即 group_replication_flow_control_hold_percent,預設是 10
// 所以 HOLD_FACTOR 預設是 0.9
double HOLD_FACTOR =
1.0 -
static_cast<double>(get_flow_control_hold_percent_var()) / 100.0;
// get_flow_control_release_percent_var() 即 group_replication_flow_control_release_percent,預設是 50
// 所以 RELEASE_FACTOR 預設是 1.5
double RELEASE_FACTOR =
1.0 +
static_cast<double>(get_flow_control_release_percent_var()) / 100.0;
// get_flow_control_member_quota_percent_var() 即 group_replication_flow_control_member_quota_percent,預設是 0
// 所以 TARGET_FACTOR 預設是 0
double TARGET_FACTOR =
static_cast<double>(get_flow_control_member_quota_percent_var()) /
100.0;
// get_flow_control_max_quota_var() 即 group_replication_flow_control_max_quota,預設是 0
int64 max_quota = static_cast<int64>(get_flow_control_max_quota_var());
// 將上一個週期的 m_quota_size,m_quota_used 賦值給 quota_size,quota_used,同時自身重置為 0
int64 quota_size = m_quota_size.exchange(0);
int64 quota_used = m_quota_used.exchange(0);
int64 extra_quota = (quota_size > 0 && quota_used > quota_size)
? quota_used - quota_size
: 0;
if (extra_quota > 0) {
mysql_mutex_lock(&m_flow_control_lock);
// 傳送一個訊號,釋放 do_wait() 處等待的事務
mysql_cond_broadcast(&m_flow_control_cond);
mysql_mutex_unlock(&m_flow_control_lock);
}
// m_holds_in_period 大於 0,則意味著需要進行流控
if (holds > 0) {
uint num_writing_members = 0, num_non_recovering_members = 0;
// MAXTPS 是 INT 的最大值,即 2147483647
int64 min_certifier_capacity = MAXTPS, min_applier_capacity = MAXTPS,
safe_capacity = MAXTPS;
m_flow_control_module_info_lock->rdlock();
Flow_control_module_info::iterator it = m_info.begin();
// 迴圈遍歷所有節點的狀態資訊
while (it != m_info.end()) {
// 這一段原始碼中沒有,加到這裡可以直觀的看到觸發流控時,每個節點的狀態資訊。
#ifndef NDEBUG
it->second.debug(it->first.c_str(), quota_size,
quota_used);
#endif
if (it->second.get_stamp() < (m_stamp - 10)) {
// 如果節點的狀態資訊在最近 10 個週期內都沒有更新,則清掉
m_info.erase(it++);
} else {
if (it->second.get_flow_control_mode() == FCM_QUOTA) {
// 如果 group_replication_flow_control_certifier_threshold 大於 0,
// 且上一個週期進行認證的事務數大於 0,
// 且當前等待認證的事務數大於 group_replication_flow_control_certifier_threshold,
// 且上一個週期進行認證的事務數小於 min_certifier_capacity
// 則會將上一個週期進行認證的事務數賦予 min_certifier_capacity
if (get_flow_control_certifier_threshold_var() > 0 &&
it->second.get_delta_transactions_certified() > 0 &&
it->second.get_transactions_waiting_certification() -
get_flow_control_certifier_threshold_var() >
0 &&
min_certifier_capacity >
it->second.get_delta_transactions_certified()) {
min_certifier_capacity =
it->second.get_delta_transactions_certified();
}
if (it->second.get_delta_transactions_certified() > 0)
// safe_capacity 取 safe_capacity 和 it->second.get_delta_transactions_certified() 中的較小值
safe_capacity =
std::min(safe_capacity,
it->second.get_delta_transactions_certified());
// 針對的是 applier,邏輯同 certifier 一樣
if (get_flow_control_applier_threshold_var() > 0 &&
it->second.get_delta_transactions_applied() > 0 &&
it->second.get_transactions_waiting_apply() -
get_flow_control_applier_threshold_var() >
0) {
if (min_applier_capacity >
it->second.get_delta_transactions_applied())
min_applier_capacity =
it->second.get_delta_transactions_applied();
if (it->second.get_delta_transactions_applied() > 0)
// 如果上一個週期有事務應用,說明該節點不是 recovering 節點
num_non_recovering_members++;
}
if (it->second.get_delta_transactions_applied() > 0)
// safe_capacity 取 safe_capacity 和 it->second.get_delta_transactions_applied() 中的較小值
safe_capacity = std::min(
safe_capacity, it->second.get_delta_transactions_applied());
if (it->second.get_delta_transactions_local() > 0)
// 如果上一個週期有本地事務,則意味著該節點存在寫入
num_writing_members++;
}
++it;
}
}
m_flow_control_module_info_lock->unlock();
num_writing_members = num_writing_members > 0 ? num_writing_members : 1;
// min_capacity 取 min_certifier_capacity 和 min_applier_capacity 的較小值
int64 min_capacity = (min_certifier_capacity > 0 &&
min_certifier_capacity < min_applier_capacity)
? min_certifier_capacity
: min_applier_capacity;
// lim_throttle 是最小配額
int64 lim_throttle = static_cast<int64>(
0.05 * std::min(get_flow_control_certifier_threshold_var(),
get_flow_control_applier_threshold_var()));
// get_flow_control_min_recovery_quota_var() 即 group_replication_flow_control_min_recovery_quota
if (get_flow_control_min_recovery_quota_var() > 0 &&
num_non_recovering_members == 0)
lim_throttle = get_flow_control_min_recovery_quota_var();
// get_flow_control_min_quota_var() 即 group_replication_flow_control_min_quota
if (get_flow_control_min_quota_var() > 0)
lim_throttle = get_flow_control_min_quota_var();
// min_capacity 不能太小,不能低於 lim_throttle
min_capacity =
std::max(std::min(min_capacity, safe_capacity), lim_throttle);
// HOLD_FACTOR 預設是 0.9
quota_size = static_cast<int64>(min_capacity * HOLD_FACTOR);
// max_quota 是由 group_replication_flow_control_max_quota 定義的,即 quota_size 不能超過 max_quota
if (max_quota > 0) quota_size = std::min(quota_size, max_quota);
// num_writing_members 是有實際寫操作的節點數
if (num_writing_members > 1) {
// 如果沒有設定 group_replication_flow_control_member_quota_percent,則按照節點數平分 quota_size
if (get_flow_control_member_quota_percent_var() == 0)
quota_size /= num_writing_members;
else
// 如果有設定,則當前節點的 quota_size 等於 quota_size * group_replication_flow_control_member_quota_percent / 100
quota_size = static_cast<int64>(static_cast<double>(quota_size) *
TARGET_FACTOR);
}
// quota_size 還會減去上個週期超額使用的 quota
quota_size =
(quota_size - extra_quota > 1) ? quota_size - extra_quota : 1;
#ifndef NDEBUG
LogPluginErr(INFORMATION_LEVEL, ER_GRP_RPL_FLOW_CONTROL_STATS,
quota_size, get_flow_control_period_var(),
num_writing_members, num_non_recovering_members,
min_capacity, lim_throttle);
#endif
} else {
// 對應 m_holds_in_period = 0 的場景,RELEASE_FACTOR 預設是 1.5
if (quota_size > 0 && get_flow_control_release_percent_var() > 0 &&
(quota_size * RELEASE_FACTOR) < MAXTPS) {
// 當流控結束後,quota_size = 上一個週期的 quota_size * 1.5
int64 quota_size_next =
static_cast<int64>(quota_size * RELEASE_FACTOR);
quota_size =
quota_size_next > quota_size ? quota_size_next : quota_size + 1;
} else
quota_size = 0;
}
if (max_quota > 0)
// quota_size 會取 quota_size 和 max_quota 中的較小值
quota_size =
std::min(quota_size > 0 ? quota_size : max_quota, max_quota);
// 最後,將 quota_size 賦值給 m_quota_size,m_quota_used 重置為 0
m_quota_size.store(quota_size);
m_quota_used.store(0);
break;
}
// 如果 group_replication_flow_control_mode 為 DISABLED,
// 則會將 m_quota_size 和 m_quota_used 置為 0,這個時候會禁用流控。
case FCM_DISABLED:
m_quota_size.store(0);
m_quota_used.store(0);
break;
default:
assert(0);
}
if (local_member_info->get_recovery_status() ==
Group_member_info::MEMBER_IN_RECOVERY) {
applier_module->get_pipeline_stats_member_collector()
->compute_transactions_deltas_during_recovery();
}
}
程式碼的邏輯看上去有點複雜。
接下來,我們通過一個具體的範例看看 flow_control_step 函數的實現邏輯。
測試叢集有三個節點組成:127.0.0.1:33061,127.0.0.1:33071 和 127.0.0.1:33081。
執行在多主模式下。
使用 sysbench 對 127.0.0.1:33061 進行插入測試(oltp_insert)。
為了更容易觸發流控,這裡將 127.0.0.1:33061 節點的 group_replication_flow_control_applier_threshold 設定為了 10。
以下是觸發流控時 127.0.0.1:33061 的紀錄檔資訊。
[Note] [MY-011726] [Repl] Plugin group_replication reported: 'Flow control - update member stats: 127.0.0.1:33061 stats certifier_queue 0, applier_queue 0 certified 7841 (177), applied 0 (0), local 7851 (177), quota 146 (156) mode=1'
[Note] [MY-011726] [Repl] Plugin group_replication reported: 'Flow control - update member stats: 127.0.0.1:33071 stats certifier_queue 0, applier_queue 0 certified 7997 (186), applied 8000 (218), local 0 (0), quota 146 (156) mode=1'
[Note] [MY-011726] [Repl] Plugin group_replication reported: 'Flow control - update member stats: 127.0.0.1:33081 stats certifier_queue 0, applier_queue 15 certified 7911 (177), applied 7897 (195), local 0 (0), quota 146 (156) mode=1'
[Note] [MY-011727] [Repl] Plugin group_replication reported: 'Flow control: throttling to 149 commits per 1 sec, with 1 writing and 1 non-recovering members, min capacity 177, lim throttle 0'
以 127.0.0.1:33081 的狀態資料為例,我們看看輸出中各項的具體含義:
因為 127.0.0.1:33081 中 applier_queue 的長度(15)超過 127.0.0.1:33061 中的 group_replication_flow_control_applier_threshold 的設定(10),所以會觸發流控。
觸發流控後,會呼叫 flow_control_step 計算下一週期的 m_quota_size。
1. 迴圈遍歷各節點的狀態資訊。叢集的吞吐量(min_capacity)取各個節點 m_delta_transactions_certified 和 m_delta_transactions_applied 的最小值。具體在本例中, min_capacity = min(177, 186, 218, 177, 195) = 177。
2. min_capacity 不能太小,不能低於 lim_throttle。im_throttle 的取值邏輯如下:
初始值是 0.05 * min (group_replication_flow_control_applier_threshold, group_replication_flow_control_certifier_threshold)。
具體在本例中,min_capacity = 0.05 * min(10, 25000) = 0.5。
如果設定了 group_replication_flow_control_min_recovery_quota 且 num_non_recovering_members 為 0,則會將 group_replication_flow_control_min_recovery_quota 賦值給 min_capacity。
num_non_recovering_members 什麼時候會為 0 呢?在新節點加入時,因為認證佇列中積壓的事務過多而觸發的流控。
如果設定了 group_replication_flow_control_min_quota,則會將 group_replication_flow_control_min_quota 賦值給 min_capacity。
3. quota_size = min_capacity * 0.9 = 177 * 0.9 = 159。這裡的 0.9 是 1 - group_replication_flow_control_hold_percent /100。之所以要預留部分配額,主要是為了處理積壓事務。
4. quota_size 不能太大,不能超過 group_replication_flow_control_max_quota。
5. 注意,這裡計算的 quota_size 是叢集的吞吐量,不是單個節點的吞吐量。如果要計算當前節點的吞吐量,最簡單的辦法是將 quota_size / 有實際寫操作的節點數(num_writing_members)。怎麼判斷一個節點是否進行了實際的寫操作呢?很簡單,上一週期有本地事務提交,即 m_delta_transactions_local > 0。具體在本例中,只有一個寫節點,所以,當前節點的 quota_size 就等於叢集的 quota_size,即 159。除了均分這個簡單粗暴的方法,如果希望某些節點比其它節點承擔更多的寫操作,也可通過 group_replication_flow_control_member_quota_percent 設定權重。這個時候,當前節點的吞吐量就等於 quota_size * group_replication_flow_control_member_quota_percent / 100。
6. 最後,當前節點的 quota_size 還會減去上個週期超額使用的 quota(extra_quota)。上個週期的 extra_quota 等於上個週期的 quota_used - quota_size = 156 - 146 = 10。所以,當前節點的 quota_size 就等於 159 - 10 = 149,和紀錄檔中的輸出完全一致。為什麼會出現 quota 超額使用的情況呢?這個後面會提到。
7. 當 m_holds_in_period 又恢復為 0 時,就意味著流控結束。流控結束後,MGR 不會完全放開 quota 的限制,否則寫入量太大,容易出現突刺。MGR 採取的是一種漸進式的恢復策略,即下一週期的 quota_size = 上一週期的 quota_size * (1 + group_replication_flow_control_release_percent / 100)。
8. group_replication_flow_control_mode 是 DISABLED ,則會將 m_quota_size 和 m_quota_used 置為 0。m_quota_size 置為 0,實際上會禁用流控。為什麼會禁用流控,這個後面會提到。
既然我們已經計算出下一週期的 m_quota_size,什麼時候使用它呢?事務提交之後,GCS 廣播事務訊息之前。
int group_replication_trans_before_commit(Trans_param *param) {
...
// 判斷事務是否需要等待
applier_module->get_flow_control_module()->do_wait();
// 廣播事務訊息
send_error = gcs_module->send_transaction_message(*transaction_msg);
...
}
接下來,我們看看 do_wait 函數的處理邏輯。
int32 Flow_control_module::do_wait() {
DBUG_TRACE;
// 首先載入 m_quota_size
int64 quota_size = m_quota_size.load();
// m_quota_used 自增加 1。
int64 quota_used = ++m_quota_used;
if (quota_used > quota_size && quota_size != 0) {
struct timespec delay;
set_timespec(&delay, 1);
mysql_mutex_lock(&m_flow_control_lock);
mysql_cond_timedwait(&m_flow_control_cond, &m_flow_control_lock, &delay);
mysql_mutex_unlock(&m_flow_control_lock);
}
return 0;
}
可以看到,如果 quota_size 等於 0,do_wait 會直接返回,不會執行任何等待操作。這也就是為什麼當 m_quota_size 等於 0 時,會禁用流控操作。
如果 quota_used 大於 quota_size 且 quota_size 不等於 0,則意味著當前週期的配額用完了。這個時候,會呼叫 mysql_cond_timedwait 觸發等待。
這裡的 mysql_cond_timedwait 會在兩種情況下退出:
需要注意的是,m_quota_used 是自增在前,然後才進行判斷,這也就是為什麼 quota 會出現超額使用的情況。
在等待的過程中,如果使用者端是多執行緒並行寫入(且單個執行緒的下個操作會等待上個操作完成),這裡會等待多個事務,並且超額使用的事務數不會多於使用者端並行執行緒數。
所以,在上面的範例中,為什麼 quota_used(156) 比 quota_size(146)多 10,這個實際上是 sysbench 並行執行緒數的數量。
接下來,我們看看範例中這 156 個事務在 do_wait 處的等待時間。
...
0.000020
0.000017
0.000023
0.000073
0.000023
0.000018
0.570180
0.567999
0.561916
0.561162
0.558930
0.557714
0.556683
0.550581
0.548102
0.547176
前 146 個事務的平均等待時間是 0.000035s,後 10 個事務的平均等待時間是 0.558044s。
很顯然,後 10 個事務是被流控了,最後被 flow_control_step(預設一秒執行一次)中傳送的 m_flow_control_cond 訊號釋放的。
group_replication_flow_control_mode
是否開啟流控。預設是 QUOTA,基於配額進行流控。如果設定為 DISABLED ,則關閉流控。
group_replication_flow_control_period
流控週期。有效值 1 - 60,單位秒。預設是 1。注意,各個節點的流控週期應保持一致,否則的話,就會將週期較短的節點配額作為叢集配額。
看下面這個範例,127.0.0.1:33061 這個節點的 group_replication_flow_control_period 是 10,而其它兩個節點的 group_replication_flow_control_period 是 1。
2022-08-27T19:01:50.699939+08:00 63 [Note] [MY-011726] [Repl] Plugin group_replication reported: 'Flow control - update member stats: 127.0.0.1:33061 stats certifier_queue 0, applier_queue 0 certified 217069 (1860), applied 1 (0), local 217070 (1861), quota 28566 (1857) mode=1'
2022-08-27T19:01:50.699955+08:00 63 [Note] [MY-011726] [Repl] Plugin group_replication reported: 'Flow control - update member stats: 127.0.0.1:33071 stats certifier_queue 0, applier_queue 2 certified 218744 (157), applied 218746 (165), local 0 (0), quota 28566 (1857) mode=1'
2022-08-27T19:01:50.699967+08:00 63 [Note] [MY-011726] [Repl] Plugin group_replication reported: 'Flow control - update member stats: 127.0.0.1:33081 stats certifier_queue 16383, applier_queue 0 certified 0 (0), applied 0 (0), local 0 (0), quota 28566 (1857) mode=1'
2022-08-27T19:01:50.699979+08:00 63 [Note] [MY-011727] [Repl] Plugin group_replication reported: 'Flow control: throttling to 141 commits per 10 sec, with 1 writing and 0 non-recovering members, min capacity 157, lim throttle 100'
最後,會將 127.0.0.1:33071 這個節點 1s 的配額(157 * 0.9)當作 127.0.0.1:33061 10s 的配額。
所以,我們會觀察到下面這個現象:
執行時間 TPS
19:01:50 49
19:01:51 93
19:01:52 1
19:01:53 1
19:01:54 1
19:01:55 1
19:01:56 1
19:01:57 1
19:01:58 1
19:01:59 1
19:02:00 1
127.0.0.1:33061 在頭兩秒就使用完了所有配額,導致後面的事務會等待 1s(mysql_cond_timedwait 的超時時長)才處理。因為模擬時指定的並行執行緒數是 1,所以這裡的 TPS 會是 1。
為什麼不是被 flow_control_step 中的m_flow_control_cond 訊號釋放呢?因為127.0.0.1:33061 這個節點的 group_replication_flow_control_period 是 10,所以 flow_control_step 10s 才會執行一次。
group_replication_flow_control_applier_threshold
待應用的事務數如果超過 group_replication_flow_control_applier_threshold 的設定,則會觸發流控,該引數預設是 25000。
group_replication_flow_control_certifier_threshold
待認證的事務數如果超過 group_replication_flow_control_certifier_threshold 的設定,則會觸發流控,該引數預設是 25000。
group_replication_flow_control_min_quota
group_replication_flow_control_min_recovery_quota
兩個引數都會決定當前節點下個週期的最小配額,只不過 group_replication_flow_control_min_recovery_quota 適用於新節點加入時的分散式恢復階段。group_replication_flow_control_min_quota 則適用於所有場景。如果兩者同時設定了, group_replication_flow_control_min_quota 的優先順序更高。兩者預設都為 0,即不限制。
group_replication_flow_control_max_quota
當前節點下個週期的最大配額。預設是 0,即不限制。
group_replication_flow_control_member_quota_percent
分配給當前成員的配額比例。有效值 0 - 100。預設為 0,此時,節點配額 = 叢集配額 / 上個週期寫節點的數量。
注意,這裡的寫節點指的是有實際寫操作的節點,不是僅指 PRIMARY 節點。畢竟不是所有的 PRIMARY 節點都會有寫操作。
另外,設定配額比例時,不要求所有節點的配額比例加起來等於 100。
group_replication_flow_control_hold_percent
預留配額的比例。有效值 0 - 100,預設是 10。預留的配額可用來處理落後節點積壓的事務。
group_replication_flow_control_release_percent
當流控結束後,會逐漸增加吞吐量以避免出現突刺。
下一週期的 quota_size = 上一週期的 quota_size * (1 + group_replication_flow_control_release_percent / 100)。有效值 0 - 1000,預設是 50。
1. 從可用性的角度出發,不建議線上關閉流控。雖然主節點出現故障的概率很小,但墨菲定律告訴我們,任何有可能發生的事情最後一定會發生。線上上還是不要心存僥倖。
2. 流控限制的是當前節點的流量,不是其它節點的。
3. 流控引數在各節點應保持一致,尤其是 group_replication_flow_control_period。
[1] WL#9838: Group Replication: Flow-control fine tuning: https://dev.mysql.com/worklog/task/?id=9838
[2] MySQL Group Replication流控實現分析: https://zhuanlan.zhihu.com/p/39541394