最近把hbase-storage-plugin代碼分享到github 上俗他,為了記錄筆者當(dāng)時(shí)的思路例隆,所以寫了這篇文章。
1、初衷:做一個(gè)集中的大容量存儲(chǔ)引擎
1.1渠旁、起因
自從進(jìn)入公司運(yùn)維部以后攀例,雖然一直在做開發(fā)的工作,但是跟DBA同學(xué)可以“親密”接觸顾腊,從而可以體會(huì)到個(gè)中的各種酸甜苦辣肛度。在我們這邊,DBA同學(xué)遇到的很多告警是磁盤空間告警投慈,半夜起來處理這種故障實(shí)在是讓人狼心承耿。
在處理這種磁盤故障的過程中,發(fā)現(xiàn)很多業(yè)務(wù)庫(kù)中存儲(chǔ)了日志型的數(shù)據(jù)伪煤,定期就需要?jiǎng)h除加袋,近期的數(shù)據(jù)訪問就不是很頻繁,至于很多歷史數(shù)據(jù)抱既,就更是很少訪問了职烧。
考慮存在冷熱數(shù)據(jù)的不同,一直琢磨在MySQL基礎(chǔ)上實(shí)現(xiàn)一個(gè)大容量存儲(chǔ)引擎防泵。熱數(shù)據(jù)用Innodb存儲(chǔ)蚀之,等它變成冷數(shù)據(jù),就改成大容量引擎捷泞。
1.2 第一次嘗試
剛開始筆者考慮基于HDFS上做一個(gè)MySQL存儲(chǔ)引擎足删,由于HDFS文件不能修改。正好利用LevelDB的存儲(chǔ)特性锁右,只會(huì)生成文件失受,而不會(huì)修改文件,于是改造了LevelDB的代碼咏瑟,讓LevelDB運(yùn)行在HDFS之上拂到,然后基于LevelDB做了一個(gè)MySQL存儲(chǔ)引擎。
這樣在開發(fā)環(huán)境可以跑起來了码泞,但是實(shí)際運(yùn)行中兄旬,經(jīng)常出現(xiàn)內(nèi)存問題,因?yàn)镠DFS的C-API是基于JVM的余寥,沒有純C的庫(kù)领铐,內(nèi)存問題無法解決,最后只能放棄劈狐。
因?yàn)閔base提供了一個(gè)thrift服務(wù)罐孝,可以支持c++語言呐馆,而且hbase天然有索引特性肥缔,這樣我們?cè)趯?shí)現(xiàn)主鍵功能時(shí)會(huì)非常簡(jiǎn)單,所以最后敲定了hbase汹来。
1.3续膳、打算解決的問題
如果我們有了hbase這樣一個(gè)海量的MySQL存儲(chǔ)引擎改艇,我們就可以解決以下幾個(gè)難題。
1坟岔、冷熱數(shù)據(jù)采用不同引擎
如下圖所示:把近期的熱數(shù)據(jù)先用Innodb引擎存儲(chǔ)谒兄,隨著時(shí)間的推移,逐步把一些老數(shù)據(jù)表社付,通過alter table 表名 engine hbase改成用Hbase來存儲(chǔ)承疲。
通過這種方式,可以在數(shù)據(jù)的高效訪問與數(shù)據(jù)保存周期上達(dá)到雙贏鸥咖,重復(fù)利用了Innodb的性能和hbase海量容量的特性燕鸽。
2、主從庫(kù)采用不同引擎
主從庫(kù)采用不同的引擎啼辣,在主庫(kù)中采用Innodb啊研,并且只保留近期數(shù)據(jù)。從庫(kù)中用hbase引擎存儲(chǔ)所有數(shù)據(jù)鸥拧,歷史數(shù)據(jù)從主庫(kù)刪除的時(shí)候党远,不刪除從庫(kù)中的表。
這樣也可以達(dá)到數(shù)據(jù)長(zhǎng)期保存的效果富弦,而且還可以防止因hbase引擎代理問題沟娱,影響線上業(yè)務(wù)。
3腕柜、集中存儲(chǔ)花沉,數(shù)據(jù)共享
一套Hbase存儲(chǔ)多套業(yè)務(wù)數(shù)據(jù),甚至于媳握,可以讓不同業(yè)務(wù)訪問相同的Hbase的表碱屁。一個(gè)業(yè)務(wù)的表也輕松的轉(zhuǎn)移到另外一個(gè)業(yè)務(wù)中來。
2蛾找、hbase存儲(chǔ)引擎的開發(fā)
2.1 主數(shù)據(jù)存儲(chǔ)格式
首先娩脾,每一張MySQL表,對(duì)應(yīng)在Hbase中建立一張對(duì)應(yīng)的表打毛,所以在MySQL的增刪改查都會(huì)對(duì)應(yīng)到Hbase表中的操作柿赊。
Bbase只有一個(gè)rowkey用來定位數(shù)據(jù),而MySQL的鍵可以有多個(gè)字段組成幻枉,為了實(shí)現(xiàn)鍵查詢和鍵 前綴查詢碰声,筆者首先按照MySQL主鍵字段順序逐個(gè)組織成一個(gè)字節(jié)數(shù)組,也就是最后要存儲(chǔ)到Hbase中的RowKey熬甫。
MySQL中主鍵的字段類型胰挑,這里只列了整數(shù)型和字符串型,開發(fā)Hbase存儲(chǔ)引擎的時(shí)候,筆者只支持以下的數(shù)據(jù)類型成為主鍵:
MYSQL_TYPE_LONG
MYSQL_TYPE_LONGLONG
MYSQL_TYPE_TINY
MYSQL_TYPE_SHORT
MYSQL_TYPE_INT24
MYSQL_TYPE_TIME
MYSQL_TYPE_DATETIME
MYSQL_TYPE_TIMESTAMP
MYSQL_TYPE_VAR_STRING
MYSQL_TYPE_VARCHAR
MYSQL_TYPE_BIT
MYSQL_TYPE_STRING
這些字段類型除了后面的4個(gè)是字符串以外瞻颂,前面的都可以轉(zhuǎn)換成為整數(shù)型數(shù)據(jù)豺谈。按照?qǐng)D中的格式存儲(chǔ)主鍵,主要是為了實(shí)現(xiàn)鍵字段數(shù)據(jù)還原贡这、鍵順序查詢(order by)等功能茬末。
由于hbase支持字段,所以數(shù)據(jù)字段就按照hbase的字段來存儲(chǔ)盖矫。
由于Hbase天然具有順序丽惭,所以筆者按照主鍵存儲(chǔ)在Rowkey,數(shù)據(jù)字段存儲(chǔ)在hbase的列中辈双,這樣主數(shù)據(jù)存儲(chǔ)了根據(jù)主鍵定位數(shù)據(jù)的能力吐根,所以Hbase引擎表是一種列簇表。從代碼中我們就可以看出來:
virtual bool primary_key_is_clustered() { return TRUE; }
2.2 第二索引功能
第二索引功能的實(shí)現(xiàn)有賴于Hbase對(duì)一個(gè)表有批量寫操作的支持辐马,下面我們先看一下Hbase支持的批量寫操作API拷橘。
/**
* Performs multiple mutations atomically on a single row. Currently
* {@link Put} and {@link Delete} are supported.
*
* @param rm object that specifies the set of mutations to perform atomically
* @throws IOException
*/
void mutateRow(final RowMutations rm) throws IOException;
這個(gè)API可以保證這些變更操作的原子性,基于這個(gè)保證喜爷,筆者就能夠輕易的實(shí)現(xiàn)第二索引功能了冗疮。
2.2.1 第二索引存儲(chǔ)格式
為了保證操作的原子性,筆者把第二索引的存儲(chǔ)也存儲(chǔ)在主數(shù)據(jù)對(duì)應(yīng)的這張Hbase表中檩帐,格式為:
RowKey:格式是有組成鍵值的字段按照順序組成
entry:key 字段存儲(chǔ)了主鍵的數(shù)據(jù)术幔。
2.2.2 第二索引數(shù)據(jù)變更
在增刪改查時(shí),和主數(shù)據(jù)一起生成一批Mutation湃密,在Hbase中一次性對(duì)表進(jìn)行操作诅挑,從而保證了原子性。
2.3.2 TODOList
開源的代碼中實(shí)現(xiàn)了唯一性的第二索引泛源,對(duì)于非唯一的第二索引拔妥,可以考慮把重復(fù)的鍵值存放在相同的第二索引Rowkey下。
2.3 批量數(shù)據(jù)插入
MySQL存儲(chǔ)引擎提供了很多優(yōu)化的操作能力达箍,譬如批量數(shù)據(jù)插入没龙,當(dāng)我們load數(shù)據(jù)、批量插入或者做一些表變更(如:更換存儲(chǔ)引擎)的時(shí)候缎玫,會(huì)用到批量數(shù)據(jù)操作硬纤。
批量數(shù)據(jù)操作會(huì)先緩存一些數(shù)據(jù)行,當(dāng)達(dá)到緩存大小時(shí)赃磨,把這些數(shù)據(jù)一次性的寫入底層存儲(chǔ)中筝家,這里也利用了Hbase的批量操作能力。
2.4 基于主鍵的查詢優(yōu)化
當(dāng)一條SQL語句中邻辉,指定了所有的主鍵字段的情況下溪王, 這時(shí)候腮鞍,是可以避免采用范圍查詢,而是直接采用基于rowkey的定位查詢功能的在扰。筆者實(shí)現(xiàn)了下面的函數(shù):
virtual int index_read_idx_map(uchar * buf, uint index, const uchar * key,
key_part_map keypart_map, enum ha_rkey_function find_flag);
在這個(gè)函數(shù)中,是直接調(diào)用了ScannerID HbaseClient::scannerOpenWithScan(const Text& tableName, const TScan& scan, const std::map<Text, Text> & attributes)函數(shù)來快速定位到主鍵的雷客。
2.5 其他
由于MySQL實(shí)例訪問Hbase是通過網(wǎng)絡(luò)來訪問的芒珠,所以這里做一些底層的優(yōu)化處理,如:連接池搅裙、連接重建等皱卓,還有很多優(yōu)化的空間。
3部逮、改造thrift server
開發(fā)完引擎以后娜汁,與hbase一起聯(lián)調(diào),一旦建立幾個(gè)連接兄朋,后續(xù)的連接請(qǐng)求就無法服務(wù)了掐禁,主要原因是thrift server才用了傳統(tǒng)的半同步半異步設(shè)計(jì)模式,每個(gè)新的連接颅和,會(huì)啟動(dòng)一個(gè)獨(dú)立的線程來為它服務(wù)傅事,一旦線程用完就無法再為后續(xù)的連接請(qǐng)求服務(wù)了。
如何解決這個(gè)問題呢峡扩,可以把這種模式改造成反應(yīng)器設(shè)計(jì)模式蹭越,就能夠提供高并發(fā)的服務(wù)了。
于是基于swift重新實(shí)現(xiàn)了hbase的thrift server教届,swift是一套基于netty實(shí)現(xiàn)的thrift服務(wù)框架响鹃,開發(fā)的步驟主要是:
1)基于thrift協(xié)議文件,生成服務(wù)框架:
java -jar .\swift-generator-cli-0.19.3-standalone.jar -override_package org.apache.hadoop.hbase.swift.generated -use_java_namespace org\apache\hadoop\hbase\thrift\Hbase.thrift -out ..\java
2)在生成的框架中實(shí)現(xiàn)hbase的訪問邏輯案训。
3)重寫thrift server之后买置,還有一個(gè)好處是我們可以擴(kuò)展thrift server的能力,筆者在原有的API的基礎(chǔ)上添加了幾個(gè)API强霎,如下圖所示:
有了這些api堕义,我們就可以利用它們來實(shí)現(xiàn)一些額外的功能,如:更改引擎脆栋,truncate table語法等倦卖。
有興趣研究swift的可以看一下筆者很早以前記錄的一篇文章(今天放到簡(jiǎn)書):http://www.reibang.com/p/49c619d33307
4、總結(jié)
筆者在公司內(nèi)部沒有采用這個(gè)方案椿争,最終選擇了mariadb來解決這種日志型存儲(chǔ)的問題怕膛,日志性的表可以選擇tokudb引擎,一般能達(dá)到4倍以上的壓縮比秦踪,好的情況下可以達(dá)到10倍褐捻。在公司現(xiàn)有業(yè)務(wù)場(chǎng)景下基本上能解決絕大多數(shù)問題了掸茅。畢竟Mariadb的成熟度高,使用廣柠逞,穩(wěn)定性好昧狮。當(dāng)然仍然無法解決海量的存儲(chǔ)問題。
后來筆者基于思路完成了大部分代碼板壮,近期把它開源了放在了github上:
https://github.com/herry2038/mysql-hbase-storage-plugin
主要是筆者覺得hbase這個(gè)思路不錯(cuò)逗鸣,一方面交流學(xué)習(xí),另一方面希望有機(jī)會(huì)能繼續(xù)完善項(xiàng)目绰精。