最近把hbase-storage-plugin代碼分享到github 上,為了記錄筆者當時的思路,所以寫了這篇文章。
1、初衷:做一個集中的大容量存儲引擎
1.1、起因
自從進入公司運維部以后,雖然一直在做開發的工作,但是跟DBA同學可以“親密”接觸,從而可以體會到個中的各種酸甜苦辣。在我們這邊,DBA同學遇到的很多告警是磁盤空間告警,半夜起來處理這種故障實在是讓人狼心。
在處理這種磁盤故障的過程中,發現很多業務庫中存儲了日志型的數據,定期就需要刪除,近期的數據訪問就不是很頻繁,至于很多歷史數據,就更是很少訪問了。
考慮存在冷熱數據的不同,一直琢磨在MySQL基礎上實現一個大容量存儲引擎。熱數據用Innodb存儲,等它變成冷數據,就改成大容量引擎。
1.2 第一次嘗試
剛開始筆者考慮基于HDFS上做一個MySQL存儲引擎,由于HDFS文件不能修改。正好利用LevelDB的存儲特性,只會生成文件,而不會修改文件,于是改造了LevelDB的代碼,讓LevelDB運行在HDFS之上,然后基于LevelDB做了一個MySQL存儲引擎。
這樣在開發環境可以跑起來了,但是實際運行中,經常出現內存問題,因為HDFS的C-API是基于JVM的,沒有純C的庫,內存問題無法解決,最后只能放棄。
因為hbase提供了一個thrift服務,可以支持c++語言,而且hbase天然有索引特性,這樣我們在實現主鍵功能時會非常簡單,所以最后敲定了hbase。
1.3、打算解決的問題
如果我們有了hbase這樣一個海量的MySQL存儲引擎,我們就可以解決以下幾個難題。
1、冷熱數據采用不同引擎
如下圖所示:把近期的熱數據先用Innodb引擎存儲,隨著時間的推移,逐步把一些老數據表,通過alter table 表名 engine hbase改成用Hbase來存儲。
通過這種方式,可以在數據的高效訪問與數據保存周期上達到雙贏,重復利用了Innodb的性能和hbase海量容量的特性。
2、主從庫采用不同引擎
主從庫采用不同的引擎,在主庫中采用Innodb,并且只保留近期數據。從庫中用hbase引擎存儲所有數據,歷史數據從主庫刪除的時候,不刪除從庫中的表。
這樣也可以達到數據長期保存的效果,而且還可以防止因hbase引擎代理問題,影響線上業務。
3、集中存儲,數據共享
一套Hbase存儲多套業務數據,甚至于,可以讓不同業務訪問相同的Hbase的表。一個業務的表也輕松的轉移到另外一個業務中來。
2、hbase存儲引擎的開發
2.1 主數據存儲格式
首先,每一張MySQL表,對應在Hbase中建立一張對應的表,所以在MySQL的增刪改查都會對應到Hbase表中的操作。
Bbase只有一個rowkey用來定位數據,而MySQL的鍵可以有多個字段組成,為了實現鍵查詢和鍵 前綴查詢,筆者首先按照MySQL主鍵字段順序逐個組織成一個字節數組,也就是最后要存儲到Hbase中的RowKey。
MySQL中主鍵的字段類型,這里只列了整數型和字符串型,開發Hbase存儲引擎的時候,筆者只支持以下的數據類型成為主鍵:
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個是字符串以外,前面的都可以轉換成為整數型數據。按照圖中的格式存儲主鍵,主要是為了實現鍵字段數據還原、鍵順序查詢(order by)等功能。
由于hbase支持字段,所以數據字段就按照hbase的字段來存儲。
由于Hbase天然具有順序,所以筆者按照主鍵存儲在Rowkey,數據字段存儲在hbase的列中,這樣主數據存儲了根據主鍵定位數據的能力,所以Hbase引擎表是一種列簇表。從代碼中我們就可以看出來:
virtual bool primary_key_is_clustered() { return TRUE; }
2.2 第二索引功能
第二索引功能的實現有賴于Hbase對一個表有批量寫操作的支持,下面我們先看一下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;
這個API可以保證這些變更操作的原子性,基于這個保證,筆者就能夠輕易的實現第二索引功能了。
2.2.1 第二索引存儲格式
為了保證操作的原子性,筆者把第二索引的存儲也存儲在主數據對應的這張Hbase表中,格式為:
RowKey:格式是有組成鍵值的字段按照順序組成
entry:key 字段存儲了主鍵的數據。
2.2.2 第二索引數據變更
在增刪改查時,和主數據一起生成一批Mutation,在Hbase中一次性對表進行操作,從而保證了原子性。
2.3.2 TODOList
開源的代碼中實現了唯一性的第二索引,對于非唯一的第二索引,可以考慮把重復的鍵值存放在相同的第二索引Rowkey下。
2.3 批量數據插入
MySQL存儲引擎提供了很多優化的操作能力,譬如批量數據插入,當我們load數據、批量插入或者做一些表變更(如:更換存儲引擎)的時候,會用到批量數據操作。
批量數據操作會先緩存一些數據行,當達到緩存大小時,把這些數據一次性的寫入底層存儲中,這里也利用了Hbase的批量操作能力。
2.4 基于主鍵的查詢優化
當一條SQL語句中,指定了所有的主鍵字段的情況下, 這時候,是可以避免采用范圍查詢,而是直接采用基于rowkey的定位查詢功能的。筆者實現了下面的函數:
virtual int index_read_idx_map(uchar * buf, uint index, const uchar * key,
key_part_map keypart_map, enum ha_rkey_function find_flag);
在這個函數中,是直接調用了ScannerID HbaseClient::scannerOpenWithScan(const Text& tableName, const TScan& scan, const std::map<Text, Text> & attributes)函數來快速定位到主鍵的。
2.5 其他
由于MySQL實例訪問Hbase是通過網絡來訪問的,所以這里做一些底層的優化處理,如:連接池、連接重建等,還有很多優化的空間。
3、改造thrift server
開發完引擎以后,與hbase一起聯調,一旦建立幾個連接,后續的連接請求就無法服務了,主要原因是thrift server才用了傳統的半同步半異步設計模式,每個新的連接,會啟動一個獨立的線程來為它服務,一旦線程用完就無法再為后續的連接請求服務了。
如何解決這個問題呢,可以把這種模式改造成反應器設計模式,就能夠提供高并發的服務了。
于是基于swift重新實現了hbase的thrift server,swift是一套基于netty實現的thrift服務框架,開發的步驟主要是:
1)基于thrift協議文件,生成服務框架:
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)在生成的框架中實現hbase的訪問邏輯。
3)重寫thrift server之后,還有一個好處是我們可以擴展thrift server的能力,筆者在原有的API的基礎上添加了幾個API,如下圖所示:
有了這些api,我們就可以利用它們來實現一些額外的功能,如:更改引擎,truncate table語法等。
有興趣研究swift的可以看一下筆者很早以前記錄的一篇文章(今天放到簡書):http://www.lxweimin.com/p/49c619d33307
4、總結
筆者在公司內部沒有采用這個方案,最終選擇了mariadb來解決這種日志型存儲的問題,日志性的表可以選擇tokudb引擎,一般能達到4倍以上的壓縮比,好的情況下可以達到10倍。在公司現有業務場景下基本上能解決絕大多數問題了。畢竟Mariadb的成熟度高,使用廣,穩定性好。當然仍然無法解決海量的存儲問題。
后來筆者基于思路完成了大部分代碼,近期把它開源了放在了github上:
https://github.com/herry2038/mysql-hbase-storage-plugin
主要是筆者覺得hbase這個思路不錯,一方面交流學習,另一方面希望有機會能繼續完善項目。