背景
喜馬拉雅成立之初哼鬓,各個(gè)業(yè)務(wù)管理各自的數(shù)據(jù)庫(kù)监右、緩存,個(gè)業(yè)務(wù)都要了解中間件的各種部署情況异希,導(dǎo)致業(yè)務(wù)間的合作健盒,需要運(yùn)維、開(kāi)發(fā)等方面的人工介入称簿,效率較低扣癣,擴(kuò)展困難,安全風(fēng)險(xiǎn)也很高憨降,資源利用率也不高父虑。喜馬拉雅在發(fā)展中,逐漸意識(shí)到需要在公司層面券册,提供統(tǒng)一的定制化的數(shù)據(jù)訪問(wèn)平臺(tái)的重要性频轿。為此垂涯,我們推出了自己的PaaS化平臺(tái)烁焙,PaaS化就是對(duì)資源的使用做了統(tǒng)一的入口,業(yè)務(wù)只需要申請(qǐng)一個(gè)資源ID耕赘,就能使用數(shù)據(jù)庫(kù)骄蝇,達(dá)到對(duì)資源使用的全部系統(tǒng)化,其中對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)我們基于Apache ShardingSphere來(lái)實(shí)現(xiàn)操骡,并基于Apache ShardingSphere強(qiáng)大功能做些優(yōu)化和增強(qiáng)九火。
整體架構(gòu)
我們PaaS平臺(tái)建設(shè)中赚窃,負(fù)責(zé)和數(shù)據(jù)層通信的dal層中間件我們叫Arena,其中對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)我們叫Arean-Jdbc
Arena-Jdbc 層的能力基本是基于Apache ShardingSphere的能力建設(shè),我們只是基于喜馬拉雅需要的特性做了增強(qiáng)和優(yōu)化,整體架構(gòu)如下:
Pull Frame
Consul Pull Frame 是我們對(duì)Consul 的配置自動(dòng)拉起封裝為統(tǒng)一的Pull框架岔激,我們除了數(shù)據(jù)庫(kù)勒极,還有緩存,每種還有不同的使用方式虑鼎,我們對(duì)不同的使用方式只需要實(shí)現(xiàn)對(duì)應(yīng)的實(shí)現(xiàn)類和初始化辱匿,更新,切好這些接口就行炫彩,框架會(huì)統(tǒng)一把解析好的數(shù)據(jù)給到匾七,具體一種場(chǎng)景不需要關(guān)心和Consul的交互,為后面的資源PaaS化提供了簡(jiǎn)單的接入能力江兢。
故障容災(zāi)
- 自動(dòng)重連
我們對(duì)故障容災(zāi)在設(shè)計(jì)時(shí)就考慮了平時(shí)通用的一些故障場(chǎng)景昨忆,比如數(shù)據(jù)庫(kù)server 掛了,我們最做自動(dòng)重鏈杉允,不需要業(yè)務(wù)做重啟操作邑贴。 - 本地快照
本地快照是為了防止Consul不可用時(shí),業(yè)務(wù)不能啟動(dòng)叔磷,所以我們?cè)诶竭h(yuǎn)程配置后痢缎,會(huì)本地存儲(chǔ)一份,在拉配置時(shí)世澜,如果遠(yuǎn)程失敗独旷,就用本地的配置,保證Consul掛了寥裂,不影響業(yè)務(wù)嵌洼,每次拉到新的配置時(shí),會(huì)更新本地的快照封恰。 - 灰度更新
灰度更新是為了支持配置變更時(shí)找灰度的邏輯麻养,對(duì)于數(shù)據(jù)庫(kù)層面的變更,是非常危險(xiǎn)的诺舔,如果一下就全量變更鳖昌,有可能會(huì)觸發(fā)線上事故,所以通過(guò)灰度變更的機(jī)制低飒,業(yè)務(wù)可以先選擇一個(gè)容器實(shí)例來(lái)變更许昨,沒(méi)有問(wèn)題后,再全量變更褥赊,把風(fēng)險(xiǎn)降到最低糕档。 - 密碼安全
沒(méi)有PaaS化之前,我們的數(shù)據(jù)庫(kù)密碼都是dba統(tǒng)一管理的拌喉,但PaaS化后速那,訪問(wèn)數(shù)據(jù)庫(kù)的密碼就存在配置文件中俐银,如果明文,就太不安全端仰,所以我們對(duì)密碼統(tǒng)一做了加密處理捶惜,在Arean-Jdbc層統(tǒng)一做解密,確保密碼不回泄露出去荔烧。
統(tǒng)一數(shù)據(jù)源
為了讓業(yè)務(wù)做最低成本的改造售躁,Arena-jdbc 需要提供一個(gè)統(tǒng)一的數(shù)據(jù)源,不論上層用什么框架茴晋,不影響,業(yè)務(wù)只需要替換數(shù)據(jù)源接入即可陪捷,對(duì)于數(shù)據(jù)庫(kù)連接池我們默認(rèn)使用HikariCP DataSource也支持個(gè)性化的業(yè)務(wù),業(yè)務(wù)可以通過(guò)配置指定連接池诺擅。
我們基于Apache ShardingSphere的連接池封裝了一個(gè)我們自己的DataSource,我們叫ArenaDataSource
,通過(guò)ArenaDataSource封裝了各種不同場(chǎng)景聚合市袖,的使用一個(gè)ArenaDataSource支持三種使用方式:
- 支持原生直接連接
- 支持Proxy模式,也是Apache ShardingSphere的proxy
- 支持直接連接分庫(kù)分表
業(yè)務(wù)只需要一個(gè)DataSource烁涌,即支持分庫(kù)分表苍碟,也支持簡(jiǎn)單的直接連接的模式,這樣的好處是業(yè)務(wù)以后要分庫(kù)分表撮执,就不再需要升級(jí)中間件了微峰,為了徹底解決業(yè)務(wù)升級(jí)的成本,我們做了配置自動(dòng)升級(jí)抒钱,就是你之前是簡(jiǎn)單直接鏈接使用蜓肆,為了PaaS化,后來(lái)業(yè)務(wù)發(fā)展了谋币,需要分庫(kù)分表了仗扬,以及從分庫(kù)分表需要多活部署了,這些都不要再升級(jí)依賴了蕾额,只需要配置動(dòng)即可早芭。
資源動(dòng)態(tài)變更
資源動(dòng)態(tài)變更是PaaS平臺(tái)基本的能力,接入PaaS后诅蝶,業(yè)務(wù)修改數(shù)據(jù)庫(kù)的任何屬性退个,都不再需要業(yè)務(wù)方代碼變更,重新發(fā)布
Apache ShardingSphere也支持?jǐn)?shù)據(jù)庫(kù)屬性的動(dòng)態(tài)變更调炬,我們基于自己的內(nèi)部系統(tǒng)的特征语盈,實(shí)現(xiàn)了基于Consul的資源變根通知。我們的資源存在Consul筐眷。
Arena-Jdbc支持對(duì)使用的資源做無(wú)損的變更黎烈,Arena-Jdbc 收到資源變更時(shí)习柠,會(huì)先對(duì)新下發(fā)的資源做預(yù)熱處理匀谣,預(yù)熱后照棋,再切換使用的數(shù)據(jù)源,切換成功后武翎,再銷毀老的數(shù)據(jù)源烈炭,業(yè)務(wù)無(wú)感知。
如果新的資源預(yù)熱失敗宝恶,則不會(huì)做變更處理符隙,保證下發(fā)的資源時(shí)可用的,規(guī)避錯(cuò)誤下發(fā)的問(wèn)題垫毙。
擴(kuò)容和縮容也是同理霹疫,一期數(shù)據(jù)需要運(yùn)維手動(dòng)遷移,遷移好了后综芥,直接在PaaS平臺(tái)下發(fā)新的配置即可丽蝎,二期支持自動(dòng)遷移數(shù)據(jù)和配置變更結(jié)合。
同時(shí)支持Proxy的無(wú)損上下線機(jī)制膀藐,通過(guò)PaaS平臺(tái)對(duì)Proxy的變更屠阻,把需要下線的Proxy節(jié)點(diǎn)去掉,通知Arena-Jdbc额各,Arena-Jdbc會(huì)把縮容的Proxy節(jié)點(diǎn)去掉国觉,做到無(wú)損下線。
讀寫(xiě)分離
讀寫(xiě)分離我們完全基于Apache ShardingSphere的來(lái)實(shí)現(xiàn)虾啦,我們根據(jù)喜馬拉雅業(yè)務(wù)的特性麻诀,對(duì)強(qiáng)制路由做了增強(qiáng),不需要規(guī)則配置為Hint模式傲醉,只要線程上下文帶有強(qiáng)制路由的標(biāo)志针饥,就可以路由到指定的庫(kù)和表,不受分表規(guī)則的影響需频,我們重寫(xiě)了ShardingStandardRoutingEngine
的Sharding時(shí)路由庫(kù)和表的邏輯:
private Collection<String> routeDataSources(final TableRule tableRule, final ShardingStrategy databaseShardingStrategy, final List<ShardingConditionValue> databaseShardingValues) {
//先判斷是否存在Hint上下文路由標(biāo)丁眼,如果有,則優(yōu)先根據(jù)用戶指定的規(guī)則路由庫(kù)
Collection<Comparable<?>> databaseShardings = HintManager.getDatabaseShardingValues(tableRule.getLogicTable());
if (databaseShardings != null && databaseShardings.size() > 0) {
List<String> list = new ArrayList<>(4);
for (Comparable<?> databaseSharding : databaseShardings) {
list.add((String) databaseSharding);
}
if (log.isDebugEnabled()) {
log.debug("route dataSources, find HintManager, so hint to: {}", list);
}
return list;
}
//沒(méi)有Hint路由規(guī)則,則按Sharding 規(guī)則路由
if (databaseShardingValues.isEmpty()) {
return tableRule.getActualDatasourceNames();
}
Collection<String> result = new LinkedHashSet<>(databaseShardingStrategy.doSharding(tableRule.getActualDatasourceNames(), databaseShardingValues, properties));
Preconditions.checkState(!result.isEmpty(), "no database route info");
Preconditions.checkState(tableRule.getActualDatasourceNames().containsAll(result),
"Some routed data sources do not belong to configured data sources. routed data sources: `%s`, configured data sources: `%s`", result, tableRule.getActualDatasourceNames());
return result;
}
路由表也是同樣的邏輯昭殉,我們重寫(xiě)了ShardingStandardRoutingEngine
的routeTables
方法苞七,和上面一樣。先從Hint 的上下文獲取挪丢。這樣通過(guò)上下文的方式能很好的滿足業(yè)務(wù)個(gè)性化的路由規(guī)則蹂风,能和Sharding 規(guī)則共存。
Database Plus
Apache ShardingSphere 除了提供基本的分庫(kù)分表乾蓬,讀寫(xiě)分離的能力外惠啄,在上層還提供了很多的插件和擴(kuò)展的機(jī)制,這讓我們?cè)诨跀?shù)據(jù)庫(kù)提供更偏向業(yè)務(wù)的起的能力非常容易,成本非常低,這叫Database Plus
Database Plus簡(jiǎn)單的說(shuō)就是你用Apache ShardingSphere的數(shù)據(jù)庫(kù)中間件撵渡,不僅僅是提供了分庫(kù)分表這一基本能力融柬,通過(guò)對(duì)底層數(shù)據(jù)的封裝為統(tǒng)一的交互標(biāo)準(zhǔn)插件模式,可以在上面實(shí)現(xiàn)很多業(yè)務(wù)的通用的場(chǎng)景的需求趋距,比如喜馬拉雅除了用到Apache ShardingSphere 基礎(chǔ)的能力外粒氧,我們也享受了Database Plus 的威力,我們?cè)谒幕A(chǔ)上輕松實(shí)現(xiàn)了支持壓測(cè)的影子庫(kù)和影子表节腐,數(shù)據(jù)加解密外盯,機(jī)房級(jí)別容災(zāi)的同城雙讀,分布式唯一ID
影子庫(kù)和影子表
影子庫(kù)影子表我們對(duì)Apache ShardingSphere做了改動(dòng)翼雀,Apache ShardingSphere 需要修改sql饱苟,我們認(rèn)為對(duì)業(yè)務(wù)有改造成本,同時(shí)結(jié)合我們自己的壓測(cè)平臺(tái)狼渊,我們和業(yè)界一樣掷空,我們也實(shí)現(xiàn)了影子標(biāo)記,通過(guò)全鏈路壓測(cè)標(biāo)的傳遞來(lái)判斷是否路由到影子庫(kù)/影子表囤锉,業(yè)務(wù)無(wú)需任何改造圣絮,即可使用影子庫(kù)影子表來(lái)做壓測(cè)跨释,同時(shí)不需要在運(yùn)行時(shí)對(duì)sql改寫(xiě)蹭劈,提升了性能炉媒,我們重寫(xiě)了ShadowSQLRouter
,
public class ArenaShadowSQLRouter extends ShadowSQLRouter {
@Override
public boolean isShadow(final SQLStatementContext<?> sqlStatementContext, final List<Object> parameters, final ShadowRule rule) {
if (sqlStatementContext instanceof InsertStatementContext || sqlStatementContext instanceof WhereAvailable
|| sqlStatementContext instanceof UpdateStatementContext) {
//這里就是判斷是否有壓測(cè)標(biāo),如果有驱入,ShardingSphere則會(huì)找影子的邏輯赤炒。
return ArenaUtilities.checkPeakRequest();
}
return false;
}
}
通過(guò)spi的方式把我們自定義的ArenaShadowSQLRouter 給Apache ShardingSphere加載使用,不得不說(shuō)Apache ShardingSphere的插件設(shè)計(jì)很贊亏较,很方便自定義和擴(kuò)展莺褒。
配置還是和Apache ShardingSphere的一樣:
# 配置影子庫(kù)規(guī)則
- !SHADOW
# true-影子表,false-影子庫(kù)(默認(rèn))
enableShadowTable: true
# 源庫(kù)名稱(對(duì)應(yīng)DataSources數(shù)據(jù)源配置中的名稱)雪情,影子庫(kù)才需要配遵岩,影子表不需要配置
sourceDataSourceNames:
- ds0 # 源庫(kù),與影子庫(kù)shadow_ds0對(duì)應(yīng)
- ds1 # 源庫(kù)巡通,與影子庫(kù)shadow_ds1對(duì)應(yīng)
# 影子庫(kù)名稱(對(duì)應(yīng)dataSources數(shù)據(jù)源配置中的名稱)尘执,影子庫(kù)才需要配,影子表不需要配置
shadowDataSourceNames:
- shadow_ds0 # 影子庫(kù)宴凉,與源庫(kù)ds0對(duì)應(yīng)
- shadow_ds1 # 影子庫(kù)誊锭,與源庫(kù)ds1對(duì)應(yīng)
enableShadowTable 我們新增了該屬性,來(lái)確定是使用影子庫(kù)還是影子表弥锄。
影子庫(kù):
影子庫(kù)丧靡,一定要填sourceDataSourceNames和shadowDataSourceNames蟆沫,enableShadowTable不用設(shè)置,或者設(shè)置為false温治。
sourceDataSourceNames
按順序映射饭庞,一一對(duì)應(yīng):
ds --> shadow_ds
ds1--> shadow_ds1
影子庫(kù)/影子表是最后一個(gè)路由規(guī)則,如果發(fā)現(xiàn)有影子庫(kù)/影子表罐盔,則根據(jù)實(shí)際的庫(kù)找到對(duì)應(yīng)的影子庫(kù)/影子表但绕,執(zhí)行sql
同城多活
基于喜馬拉雅的業(yè)務(wù)特性救崔,讀多寫(xiě)少惶看,我們只實(shí)現(xiàn)了對(duì)讀業(yè)務(wù)的雙機(jī)房部署,寫(xiě)業(yè)務(wù)還是路由到主機(jī)房六孵。
為了增強(qiáng)容災(zāi)能力纬黎,喜馬拉雅搭建了雙機(jī)房,同時(shí)承載業(yè)務(wù)流量劫窒,當(dāng)一個(gè)機(jī)房故障時(shí)本今,可以把流量快速切換到另一個(gè)機(jī)房,我們?cè)赿al層設(shè)計(jì)上支持雙寫(xiě)主巍,這
里充分利用了Apache ShardingSphere的讀寫(xiě)分離功能冠息,讀和寫(xiě)可以配置獨(dú)立的數(shù)據(jù)源,我們只需要在上面做了一層封裝孕索,在切換時(shí)候動(dòng)態(tài)變更對(duì)應(yīng)的數(shù)據(jù)源即可逛艰,為了切換時(shí)不影響業(yè)務(wù)的流量,我們是先預(yù)熱新的數(shù)據(jù)源搞旭,再銷毀老的數(shù)據(jù)源散怖。
架構(gòu)圖如下:
另外我們也對(duì)雙寫(xiě)做了研究和探索,關(guān)鍵在于數(shù)據(jù)庫(kù)的雙向同步肄渗,基于阿里開(kāi)源的otter做了改造镇眷,支持基于gtid模式同步,不依賴打標(biāo)翎嫡,打標(biāo)回有性能開(kāi)銷欠动,在一些業(yè)務(wù)做了試用。
分布式唯一ID
分庫(kù)分表后惑申,唯一id是必須要滿足的需求翁垂,Apache ShardingSphere默認(rèn)提供了snowfake和uuid 算法,但不是很時(shí)候db的場(chǎng)景硝桩,db需要保證順序和格式沿猜,所以我們基于Apache ShardingSphere提供的接口,也實(shí)現(xiàn)自己的唯一id生成策略:
數(shù)據(jù)分片后碗脊,不同Mysql實(shí)例生成全局唯一主鍵是非常棘手的問(wèn)題啼肩。Arena-Jdbc實(shí)現(xiàn)了Apache ShardingSphere的分布式主鍵生成器接口橄妆,通過(guò)集成喜馬拉雅內(nèi)部的全局唯一id生成服務(wù),提供了適用于喜馬拉雅內(nèi)部的自增主鍵生成算法-BoushId主鍵生成策略祈坠。
監(jiān)控和報(bào)警:
做一個(gè)數(shù)據(jù)庫(kù)中間件害碾,監(jiān)控是必不可少的部分,就像我們的眼睛赦拘,沒(méi)有監(jiān)控就是瞎抓慌随,以及對(duì)異常情況的報(bào)警也是非常重要的部分,只有完善的監(jiān)控和報(bào)警才能算是一個(gè)完整的產(chǎn)品躺同,得益于Apache ShardingSphere在設(shè)計(jì)時(shí)就提供了鉤子阁猜,我們能非常小的成本就能實(shí)現(xiàn)對(duì)sql層面的監(jiān)控和報(bào)警。
Arena-Jdbc客戶端通過(guò)鉤子回調(diào)蹋艺,從多維度數(shù)據(jù)來(lái)分析使用數(shù)據(jù)庫(kù)的運(yùn)行情況剃袍,以30s為一次統(tǒng)計(jì)周期,每個(gè)周期統(tǒng)計(jì)的數(shù)據(jù)包括:Mysql總請(qǐng)求量捎谨,新增民效、刪除、修改和查詢的請(qǐng)求量涛救,失敗的請(qǐng)求量和慢請(qǐng)求量畏邢,影子庫(kù)的流量,以及統(tǒng)計(jì)響應(yīng)時(shí)間的TP百分比检吆,還有連接池的等待時(shí)間舒萎、建連時(shí)間、連接數(shù)等信息咧栗。這些指標(biāo)會(huì)發(fā)送給專門(mén)的收集指標(biāo)服務(wù)逆甜,并持久化到時(shí)序數(shù)據(jù)庫(kù),PaaS平臺(tái)可以從時(shí)序數(shù)據(jù)庫(kù)中查詢數(shù)據(jù)致板,展示給各個(gè)業(yè)務(wù)交煞,對(duì)于異常sql和慢sql,做報(bào)警等后續(xù)處理斟或。
其他
我們除了基于Apache ShardingSphere實(shí)現(xiàn)上述關(guān)鍵特性外素征,我們還對(duì)Apache ShardingSphere做了一些優(yōu)化和改進(jìn),以更適合喜馬拉雅的業(yè)務(wù)萝挤。
優(yōu)化分片規(guī)則御毅,啟動(dòng)時(shí),如果分片的真實(shí)表不存在的情況則報(bào)錯(cuò)怜珍,將配置錯(cuò)誤前置
有的業(yè)務(wù)方有幾百端蛆,甚至幾千的分表,這種情況下酥泛,由于Apache ShardingSphere中的聯(lián)邦查詢需要依次掃表今豆,啟動(dòng)速度很慢嫌拣,達(dá)到了分鐘級(jí)別。針對(duì)這種情況呆躲,我們新增了props配置項(xiàng)异逐,不再初始化聯(lián)邦查詢,打打加快了啟動(dòng)速度插掂,并且再使用中灰瞻,也沒(méi)有用聯(lián)邦查詢
優(yōu)化了Apache ShardingSphere,執(zhí)行sql異常不報(bào)錯(cuò)誤的情況
由于有的業(yè)務(wù)方辅甥,對(duì)重要的表采用了大寫(xiě)的表名和列名酝润,我們?nèi)サ鬉pache ShardingSphere中,對(duì)配置中的大寫(xiě)的表名列名強(qiáng)制小寫(xiě)的情況肆氓,允許大寫(xiě)的表名和列名
新增了props配置項(xiàng)袍祖,可以調(diào)節(jié)Apache ShardingSphere的編譯緩存的大小
優(yōu)化Apache ShardingSphere復(fù)合分片算法底瓣,精確匹配分片字段
在ComplexShardingStrategyConfiguration中谢揪,添加shardingColumnList字段:
修復(fù)Apache ShardingSphere,批量insert捐凭,不返回主鍵的問(wèn)題拨扶,這個(gè)問(wèn)題在mybatis-plus中比較常見(jiàn)不分片的表,支持使用默認(rèn)的主鍵id生成策略
總結(jié)
基于Apache ShardingSphere實(shí)現(xiàn)的數(shù)據(jù)庫(kù)中間件Arena-Jdbc茁肠,經(jīng)過(guò)半年的時(shí)間患民,已經(jīng)覆蓋了喜馬拉雅的70%的核心業(yè)務(wù),目前沒(méi)有發(fā)現(xiàn)任何問(wèn)題垦梆,表現(xiàn)的非常穩(wěn)定匹颤,通過(guò)和我們的PaaS平臺(tái)結(jié)合,業(yè)務(wù)也非常愿意接入托猩,另外我們使用Apache ShardingSphere時(shí)印蓖,社區(qū)還沒(méi)有發(fā)布stable的版本,所以我們?cè)谑褂眠^(guò)程中也遇到了些問(wèn)題京腥,基本上我們都解決了赦肃,有的社區(qū)也有對(duì)應(yīng)的解決方案,得益于社區(qū)非彻耍活躍他宛,我們以后也希望把我們做的一些feature能回饋到社區(qū),為Apache ShardingSphere的發(fā)展做出一點(diǎn)點(diǎn)小貢獻(xiàn)欠气。
另外非常感謝亮哥親自來(lái)喜馬拉雅對(duì)Apache ShardingSphere的技術(shù)內(nèi)幕和規(guī)則做了一次全面的分享厅各,非常關(guān)心我們?cè)谑褂肁pache ShardingSphere過(guò)程中遇到的問(wèn)題,在現(xiàn)場(chǎng)對(duì)小伙伴提的問(wèn)題都一一作了 深入的解答预柒,非常感謝亮哥队塘,祝Apache ShardingSphere越來(lái)越好琐鲁。