簡介:本文主要闡述在分庫分表改造過程中需要考慮的因素以及對應的解法,還有踩過的那些坑。
本文適合閱讀群眾:需要從單庫單表改造為多庫多表的新手。
本文主要闡述在分庫分表改造過程中需要考慮的因素以及對應的解法,還有踩過的那些坑。
我們既然要做分庫分表,那總要有個做事的動機。那麼,在動手之前,首先就要弄明白下面兩個問題。
其實就是字面意思,很好理解:
關鍵字:提升效能、增加可用性。
隨著單庫中的資料量越來越大、資料庫的查詢QPS越來越高,相應的,對資料庫的讀寫所需要的時間也越來越多。資料庫的讀寫效能可能會成為業務發展的瓶頸。對應的,就需要做資料庫效能方面的優化。本文中我們只討論資料庫層面的優化,不討論快取等應用層優化的手段。
如果資料庫的查詢QPS過高,就需要考慮拆庫,通過分庫來分擔單個資料庫的連線壓力。比如,如果查詢QPS為3500,假設單庫可以支撐1000個連線數的話,那麼就可以考慮拆分成4個庫,來分散查詢連線壓力。
如果單表資料量過大,當資料量超過一定量級後,無論是對於資料查詢還是資料更新,在經過索引優化等純資料庫層面的傳統優化手段之後,還是可能存在效能問題。這是量變產生了質變,這時候就需要去換個思路來解決問題,比如:從資料生產源頭、資料處理源頭來解決問題,既然資料量很大,那我們就來個分而治之,化整為零。這就產生了分表,把資料按照一定的規則拆分成多張表,來解決單表環境下無法解決的存取效能問題。
單個資料庫如果發生意外,很可能會丟失所有資料。尤其是雲時代,很多資料庫都跑在虛擬機器器上,如果虛擬機器器/宿主機發生意外,則可能造成無法挽回的損失。因此,除了傳統的 Master-Slave、Master-Master 等部署層面解決可靠性問題外,我們也可以考慮從資料拆分層面解決此問題。
此處我們以資料庫宕機為例:
當然,我們也不能無限制的拆庫,這也是犧牲儲存資源來提升效能、可用性的方式,畢竟資源總是有限的。
從第一部分了解到的資訊來看,分庫分表方案可以分為下面3種:
由於所有的技術都是為業務服務的,那麼,我們就先從資料方面回顧下業務背景。
比如,我們這個業務系統是為了解決會員的諮詢訴求,通過我們的XSpace客服平臺系統來服務會員,目前主要以同步的離線工單資料作為我們的資料來源來構建自己的資料。
假設,每一筆離線工單都會產生對應一筆會員的諮詢問題(我們簡稱:問題單),如果:
合計共 1.5w + 2w + 3w = 6.5w 筆/天
考慮到以後可能要繼續覆蓋的新的業務場景,需要提前預留部分擴充套件空間,這裡我們假設為每天產生 8w 筆問題單。
除問題單外,還有另外2張常用的業務表:使用者操作紀錄檔表、使用者提交的表單資料表。
其中,每筆問題單都會產生多條使用者操作紀錄檔,根據歷史統計資料來可以看到,平均每個問題單大約會產生8條操作紀錄檔,我們預留一部分空間,假設每個問題單平均產生約10條使用者操作紀錄檔。
如果系統設計使用年限5年,那麼問題單資料量大約 = 5年 365天/年 8w/天 = 1.46億,那麼估算出的表數量如下:
分庫的時候除了要考慮平時的業務峰值讀寫QPS外,還要考慮到諸如雙11大促期間可能達到的峰值,需要提前做好預估。
根據我們的實際業務場景,問題單的資料查詢來源主要來自於阿里客服小蜜首頁。因此,可以根據歷史QPS、RT等資料評估,假設我們只需要3500資料庫連線數,如果單庫可以承擔最高1000個資料庫連線,那麼我們就可以拆分成4個庫。
根據行業慣例,通常按照 水平切分、垂直切分 兩種方式進行切分,當然,有些複雜業務場景也可能選擇兩者結合的方式。
(1)水平切分
這是一種橫向按業務維度切分的方式,比如常見的按會員維度切分,根據一定的規則把不同的會員相關的資料散落在不同的庫表中。由於我們的業務場景決定都是從會員視角進行資料讀寫,所以,我們就選擇按照水平方式進行資料庫切分。
(2)垂直切分
垂直切分可以簡單理解為,把一張表的不同欄位拆分到不同的表中。
比如:假設有個小型電商業務,把一個訂單相關的商品資訊、買賣家資訊、支付資訊都放在一張大表裡。可以考慮通過垂直切分的方式,把商品資訊、買家資訊、賣家資訊、支付資訊都單獨拆分成獨立的表,並通過訂單號跟訂單基本資訊關聯起來。
也有一種情況,如果一張表有10個欄位,其中只有3個欄位需要頻繁修改,那麼就可以考慮把這3個欄位拆分到子表。避免在修改這3個資料時,影響到其餘7個欄位的查詢行鎖定。
比如,當熱點事件出現後,怎麼避免熱點資料集中存取到某個特定庫/表,造成各分庫分表讀寫壓力不均的問題。
其實,細思之下可以發現這個問題其實跟負載均衡的問題很相似,所以,我們可以去借鑑下負載均衡的解法來解決。我們常見的負責均衡演演算法如下:
我們的選擇:基於 一致性Hash演演算法 裁剪,相較於一致性Hash演演算法,我們裁剪後的演演算法
主要區別在以下幾個點:
(1)Hash環節點數量的不同
一致性Hash有2^32-1個節點,考慮到我們按照buyerId切分,而且buyerId基數本就很龐大,整體已經具備一定的均勻度,所以,我們把Hash環的數量降低到4096個;
(2)DB索引演演算法的不同
一致性Hash通過類似 hash(DB的IP) % 2^32 公式計算DB在Hash環的位置。如果DB數量較少,需要通過增加虛擬節點來解決Hash環偏斜問題,而且DB的位置可能會隨著IP的變動而變化,尤其是在雲環境下。
資料均勻分佈到Hash環的問題,經過之前的判斷,我們可以通過 Math.abs(buyerId.hashCode()) % 4096 計算定位到Hash環位置,那麼剩下的問題就是讓DB也均勻分佈到這個Hash環上即可。由於我們都是使用阿里的TDDL中介軟體,只需要通過邏輯上的分庫索引號定位DB,因此,我們把分庫DB均分到這個Hash環上即可,如果是hash環有4096個環節,拆分4庫的話,那麼4個庫分別位於第1、1025、2049、3073個節點上。分庫的索引定位可通過 (Math.abs(buyerId.hashCode()) % 4096) / (4096 / DB_COUNT) 這個公式計算得出。
分庫索引的 Java 虛擬碼實現如下:
/**
* 分庫數量
*/
public static final int DB_COUNT = 4;
/**
* 獲取資料庫分庫索引號
*
* @param buyerId 會員ID
* @return
*/
public static int indexDbByBuyerId(Long buyerId) {
return (Math.abs(buyerId.hashCode()) % 4096) / (4096 / DB_COUNT);
}
在單庫環境下,我們的問題單主表的ID採用的MySQL自增的方式。但是,分庫之後如果還繼續使用資料庫自增的方式,就很容易出現各門口的主鍵ID重複問題。
對於這種情況,有很多種解決方案,比如採用UUID的方式,不過UUID太長,查詢效能太差,佔用空間也大,而且主鍵的型別也變了,也不利於應用平滑遷移。
其實,我們也可以對ID繼續拆分,比如對ID進行分段,不同的庫表使用不同的ID段,但也會產生新的問題,這個ID段要多長才合適?如果ID段分配完了,那可能會佔用第二個庫的ID段,產生ID不唯一問題。
但是,如果我們讓所有的分庫使用的ID段按照等差數列進行分隔,每次ID段用完之後,再按照固定的步長比遞增的話,那是不是就可以解決這個問題了。
比如,像下面這樣,假設每次分配的ID間隔為1000,也就是步長1000,那麼每次分配的ID段起止索引則可以按照下面的公式計算得出:
X * 步長 + (Y-1) * (庫數量 * 步長)
X * 步長 + (Y-1) * (庫數量 * 步長) + (1000 -1)
如果是分4庫,那麼最終分配的ID段就會是下面這個樣子:
我們的問題單庫採用的就是這種先對ID分段,再按固定步長遞增的方式。這也是TDDL官方提供的解決方案。
除此之外,實際場景下,通常為了分析排查問題方便,往往會在ID中增加一些額外資訊,比如我們自己的問題單ID就包含了日期、版本、分庫索引等資訊。
問題單 ID 生成 Java 虛擬碼參考:
import lombok.Setter;
import org.apache.commons.lang3.time.DateFormatUtils;
/**
* 問題單ID構建器
* <p>
* ID格式(18位元):6位日期 + 2位版本號 + 2位庫索引號 + 8位元序列號
* 範例:180903010300001111
* 說明這個問題單是2018年9月3號生成的,採用的01版本的ID生成規則,資料存放在03庫,最後8位元00001111是生成的序列號ID。* 採用這種ID格式還有個好處就是每天都有1億(8位元)的序列號可用。* </p>
*/
@Setter
public class ProblemOrdIdBuilder {
public static final int DB_COUNT = 4;
private static final String DATE_FORMATTER = "yyMMdd";
private String version = "01";
private long buyerId;
private long timeInMills;
private long seqNum;
public Long build() {
int dbIndex = indexDbByBuyerId(buyerId);
StringBuilder pid = new StringBuilder(18)
.append(DateFormatUtils.format(timeInMills, DATE_FORMATTER))
.append(version)
.append(String.format("%02d", dbIndex))
.append(String.format("%08d", seqNum % 10000000));
return Long.valueOf(pid.toString());
}
/**
* 獲取資料庫分庫索引號
*
* @param buyerId 會員ID
* @return
*/
public int indexDbByBuyerId(Long buyerId) {
return (Math.abs(buyerId.hashCode()) % 4096) / (4096 / DB_COUNT);
}
}
由於分散式環境下,一個事務可能跨多個分庫,所以,處理起來相對複雜。目前常見的有2種解決方案:
(1)使用分散式事務
(2)由應用程式+資料庫共同控制
針對上面2種分散式事務解決方案,我們該如何選擇?
首先,沒有萬能的解決方案,只有適合自己的方案。那就先看看我們的業務中,事務的使用場景有哪些吧。
無論是來諮詢問題的會員,還是為會員解決問題的客服小二,亦或者從第三方系統同步相關資料。主要有2個核心動作:
由於問題單資料、操作紀錄檔都是分開查詢,所以,不涉及分散式關聯查詢場景,這個可以忽略不考慮。
那麼就剩下使用者提交資料場景了,可能會同時寫入問題單以及操作紀錄檔資料。
既然使用場景確定了,那麼可以選擇事務解決方案了。雖然分散式事務實現簡單,但這個簡單是因為中介軟體幫我們解決了它本身的複雜性。複雜性越高,必然會帶來一定的效能損耗。而且,目前大部分應用都是基於 SpringBoot 開發,預設使用的都是內嵌 tomcat 容器,不像 IBM 提供的 WebSphere Application Server、Oracle 的 WebLogic 這些重量級應用伺服器,都提供了內建的分散式事務管理器。因此,如果我們要接入,必然要自己引入額外的分散式事務管理器,這個接入成本就更高了。所以,這種方案就暫不考慮了。那麼,就只能自己想辦法把大事務切分成單庫可以解決的小事務了。
所以,現在問題就成了,如何讓同一個會員的問題單資料和這個問題單相關的操作紀錄檔資料寫入到同一個分庫中。其實,解決方案也比較簡單,由於都是使用會員ID做切分,那麼使用相同的分庫路由規則即可。
最後,我來看下最終的 TDDL 分庫分表規則設定:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="vtabroot" class="com.taobao.tddl.interact.rule.VirtualTableRoot" init-method="init">
<property name="dbType" value="MYSQL" />
<property name="defaultDbIndex" value="PROBLEM_0000_GROUP" />
<property name="tableRules">
<map>
<entry key="problem_ord" value-ref="problem_ord" />
<entry key="problem_operate_log" value-ref="problem_operate_log" />
</map>
</property>
</bean>
<!-- 問題(訴求)單表 -->
<bean id="problem_ord" class="com.taobao.tddl.interact.rule.TableRule">
<property name="dbNamePattern" value="PROBLEM_{0000}_GROUP" />
<property name="tbNamePattern" value="problem_ord_{0000}" />
<property name="dbRuleArray" value="((Math.abs(#buyer_id,1,4#.hashCode()) % 4096).intdiv(1024))" />
<property name="tbRuleArray">
<list>
<value>
<![CDATA[
def hashCode = Math.abs(#buyer_id,1,32#.hashCode());
int dbIndex = ((hashCode % 4096).intdiv(1024)) as int;
int tableCountPerDb = 32 / 4;
int tableIndexStart = dbIndex * tableCountPerDb;
int tableIndexOffset = (hashCode % tableCountPerDb) as int;
int tableIndex = tableIndexStart + tableIndexOffset;
return tableIndex;
]]>
</value>
</list>
</property>
<property name="allowFullTableScan" value="false" />
</bean>
<!-- 問題操作紀錄檔表 -->
<bean id="problem_operate_log" class="com.taobao.tddl.interact.rule.TableRule">
<property name="dbNamePattern" value="PROBLEM_{0000}_GROUP" />
<property name="tbNamePattern" value="problem_operate_log_{0000}" />
<!-- 【#buyer_id,1,4#.hashCode()】 -->
<!-- buyer_id 代表分片欄位;1代表分庫步長;4代表一共4個分庫,當執行全表掃描時會用到 -->
<property name="dbRuleArray" value="((Math.abs(#buyer_id,1,4#.hashCode()) % 4096).intdiv(1024))" />
<property name="tbRuleArray">
<list>
<value>
<![CDATA[
def hashCode = Math.abs(#buyer_id,1,512#.hashCode());
int dbIndex = ((hashCode % 4096).intdiv(1024)) as int;
int tableCountPerDb = 512 / 4;
int tableIndexStart = dbIndex * tableCountPerDb;
int tableIndexOffset = (hashCode % tableCountPerDb) as int;
int tableIndex = tableIndexStart + tableIndexOffset;
return tableIndex;
]]>
</value>
</list>
</property>
<property name="allowFullTableScan" value="false" />
</bean>
</beans>
資料庫複製方案,阿里雲上面也開放了以前阿里內部使用的資料庫複製、遷移方案《資料傳輸服務(Data Transmission Service)》[1],詳情可諮詢阿里雲客服或者阿里雲資料庫專家。
分庫切換髮布流程可選擇停機、不停機發布兩種:
(1)如果選擇停機發布
(2)如果選擇不停機發布話,流程會略微複雜點
由於阿里的TDDL中介軟體使用groovy指令碼計算分庫分表路由,而 groovy 的 / 運運算元 或者 /= 運運算元 可能會產生一個 double 型別的結果,並非像 Java 那樣得出一個整數,因此需要使用 x.intdiv(y) 函數做整除運算。
// 在 Java 中
System.out.println(5 / 3); // 結果 = 1
// 在 Groovy 中
println (5 / 3); // 結果 = 1.6666666667
println (5.intdiv(3)); // 結果 = 1(Groovy整除正確用法)
詳情可檢視 Groovy 官方說明 《The case of the division operator》:
參考資料
[1] https://baijiahao.baidu.com/s?id=1622441635115622194&wfr=spider&for=pc
[2] http://www.zsythink.net/archives/1182
[3] https://www.aliyun.com/product/dts
[4] https://docs.groovy-lang.org/latest/html/documentation/core-syntax.html
[5] https://github.com/alibaba/tb_tddl
原文連結:https://developer.aliyun.com/article/782489?
版權宣告:本文內容由阿里雲實名註冊使用者自發貢獻,版權歸原作者所有,阿里雲開發者社群不擁有其著作權,亦不承擔相應法律責任。具體規則請檢視《阿里雲開發者社群使用者服務協定》和《阿里雲開發者社群智慧財產權保護指引》。如果您發現本社群中有涉嫌抄襲的內容,填寫侵權投訴表單進行舉報,一經查實,本社群將立刻刪除涉嫌侵權內容。