Sqoop是Hadoop生態圈中的ETL抽取工具,可以從關係型數據庫抽取數據至HDFS、HBase、Hive中,其內在機制利用了MapReduce進行多節點並行抽取,可以有效地提升抽取速度。
1. Sqoop抽取原理
Sqoop抽取的核心思想是對sql語句進行分割,例如對錶A進行抽取時,首先要指定一個抽取字段,默認是表的主鍵,假設爲objectId,首先會計算出min(objectId)和max(objectId),假設抽取線程數爲N,則每個抽取線程的抽取範圍大小爲(max(objectId)-min(objectId))/N,最後將每個子抽取提交到Hadoop,利用MapReduce並行抽取,這樣可以極大地提升抽取速度。
從Sqoop的抽取原理可以看出,Sqoop是對某個字段進行分割,因此如果選擇的字段非常不均勻,則每個抽取線程抽取的數據量相差懸殊,這樣會導致某些節點的負載過大,而某些節點的負載不足,從而影響整體抽取速度。
2 Sqoop2的基本架構
Sqoop2與Sqoop1的架構完全不同,Sqoop2採用的主從服務模式,引入了Sqoop Server(採用Jetty),而Sqoop2的核心部分就是Connector,具體來說,Sqoop2對各種數據源都開發了相應的訪問接口,包括導入/導出,Sqoop Server對接口進行統一管理,進行數據抽取時,需要創建任務,指定從哪一個數據源抽取到哪一個數據源,提交的Sqoop Server上運行,而任務的分割、調度都都將由Sqoop Server完成。
3 Connector開發方法
目前Sqoop2官網上最新版本是1.99.7,其支持的Connector包括:JDBC、HDFS、FTP、SFTP、Kafka,後續版本中應該會添加對其他數據源的支持,由於項目需求,對Sqoop2的源碼進行了分析,並獨立開發了支持HBase、Hive、Solr的數據源接口,在此對開發接口一般方法進行分享,希望對從事Sqoop的開發人員起到拋磚引玉的作用,以Hive接口爲例:
3.1 整體Connector源碼預覽:
要寫的代碼還是比較多的,其實主要分爲兩部分:From和To,下面分這兩個部分分別進行闡述:
3.2 From 部分實現
該部分即實現了對某個數據源進行抽取
首先看LinkConfig.java
@ConfigClass(validators = {@Validator(LinkConfig.ConfigValidator.class)})
public class LinkConfig {
/**
* HDFS地址
*/
@Input(size = 255)
public String hdfsURI;
/**
* HiveServer地址
*/
@Input(size = 255)
public String hiveSeverHost;
public LinkConfig(){
}
public static class ConfigValidator extends AbstractValidator<LinkConfig>{
@Override
public void validate(LinkConfig config) {
if(StringUtils.isEmpty(config.hdfsURI)){
addMessage(Status.ERROR, "Must Specify HDFS URI!");
}
if(StringUtils.isEmpty(config.hiveSeverHost)){
addMessage(Status.ERROR, "Must Specify HiveServer Address!");
}
}
}
}
這部分代碼實現了Hive的連接配置,擋在Sqoop Shell中創建link時,需要指定相關配置參數,在本例中,我們需要指定HDFS地址和HiverServer地址,以註解@Input標定。
再看FromJobConfig.java
@ConfigClass(validators = { @Validator(FromJobConfig.FromJobConfigValidator.class)})
public class FromJobConfig {
/**
* 要導出的Hive表名
*/
@Input(size=255)
public String tableName;
public FromJobConfig(){
}
public static class FromJobConfigValidator extends AbstractValidator<FromJobConfig>{
@Override
public void validate(FromJobConfig config) {
if(StringUtils.isEmpty(config.tableName)){
addMessage(Status.ERROR, "Must Specify tableName!");
}
}
}
}
這部分代碼指定了需要抽取Hive的哪張表
下面是Sqoop抽取最關鍵的實現部分:HivePartition.java和HivePartitioner.java ,即實現抽取的分割策略:
首先說明一下對Hive抽取分割策略的基本實現,Hive數據表存儲的HDFS路徑爲/user/hive/warehouse/hive表名,在本例我是以文本文件存儲的,因此核心思路是如何對文本文件進行多線程讀寫。
假設路徑下共有M個文本文件,抽取線程爲N,在此我們需要對每個文件按行進行分割,即每個文件按行分成N部分,這樣每個文件都可以被並行抽取,具體的分割我將在另一篇博客中詳細闡述。
HivePartition指定一些分區參數,包括Hive存儲路徑下各個文件路徑集合,每個文件的其實讀寫點和終止讀寫點等
下面看下HivePartitioner.java
public class HivePartitioner extends Partitioner<LinkConfiguration, FromJobConfiguration>{
private static Configuration config = null;
@Override
public List<Partition> getPartitions(PartitionerContext context,
LinkConfiguration linkConfig, FromJobConfiguration jobConfig) {
assert linkConfig != null;
assert jobConfig != null;
List<Partition> partitions = new LinkedList<Partition>();
long numberPartitions = context.getMaxPartitions();
String hdfsUri = linkConfig.linkConfig.hdfsURI;
String tableName = jobConfig.fromJobConfig.tableName;
FileSystem fs = getFileSystem(hdfsUri);
Path directory = new Path("/user/hive/warehouse/"+tableName);
try {
//獲取hive表目錄下的所有文件
FileStatus[] files = fs.listStatus(directory);
//目錄下的文件數量
int numFiles = files.length;
Path[] filePaths = new Path[numFiles];
List<List<Long>> filePartitions = new ArrayList<List<Long>>();
for(int i=0; i<numFiles; i++){
//獲取每個文件的分區
List<Long> filePartition = getLocations(hdfsUri, files[i].getPath(), numberPartitions);
filePartitions.add(filePartition);
}
for(int i=0; i<numFiles; i++){
filePaths[i] = files[i].getPath();
}
for(int i=0; i<numberPartitions; i++){
Long[] starts = new Long[numFiles];
Long[] ends = new Long[numFiles];
int s=0, t=0;
for(List<Long> filePartition : filePartitions){
starts[s++] = filePartition.get(i);
ends[t++] = filePartition.get(i+1);
}
HivePartition partition = new HivePartition(numFiles, filePaths, starts, ends);
partitions.add(partition);
}
} catch (Exception e) {
throw new SqoopException(HiveConnectorError.HIVE_CONNECTOR_0002, e.getMessage());
}
return partitions;
}
}
HivePartitioner需要實現getPartitions方法,即根據抽取線程數量生成多個HivePartitionHiveExtractor即抽取部分的實現:
public class HiveExtractor extends Extractor<LinkConfiguration, FromJobConfiguration, HivePartition>{
private static final Logger logger = Logger.getLogger(HiveExtractor.class);
private long rowsRead = 0L;
@Override
public void extract(ExtractorContext context, LinkConfiguration linkConfig,
FromJobConfiguration jobConfig, HivePartition partition) {
String hdfsUri = linkConfig.linkConfig.hdfsURI;
String tableName = jobConfig.fromJobConfig.tableName;
Schema schema = context.getSchema();
DataWriter writer = context.getDataWriter();
int numFiles = partition.getNumFiles();
Path[] filePaths = partition.getPaths();
Long[] starts = partition.getStarts();
Long[] ends = partition.getEnds();
Configuration config = new Configuration();
config.set("fs.default.name", hdfsUri);
try {
FileSystem fs = FileSystem.get(config);
Text text = new Text();
for(int i=0; i<numFiles; i++){
Path path = filePaths[i];
long start = starts[i];
long end = ends[i];
logger.info(String.format("準備讀取文件%s, 從%d至%d", path.getName(), start, end));
FSDataInputStream filestream = fs.open(path);
LineReader filereader = new LineReader(filestream);
filestream.seek(start);
long next = start;
while(next<end){
next += filereader.readLine(text);
Object[] data = SqoopIDFUtils.fromCSV(text.toString(), schema);
writer.writeArrayRecord(data);
rowsRead++;
}
filereader.close();
}
} catch (IOException e) {
logger.info(String.format("讀取表%s出錯:%s", tableName, e.getMessage()));
throw new SqoopException(HiveConnectorError.HIVE_CONNECTOR_0003, e.getMessage());
}
}
@Override
public long getRowsRead() {
return rowsRead;
}
}
HiveExtractor需要實現extract函數,其中的參數partition即之前生成的HivePartition,context.getDataWriter()可以獲取上下文的輸出流,在此我們對源文件進行按行讀取,並寫到輸出流中。
HiveFromInitializer和HiveFromDestroyer即實現抽取的初始化與清理工作,包括連接HDFS和HiveServer,斷開與HDFS、HiveServer的連接等
3.3 To 部分的實現:
包括ToJobConfig.java、HiveToInitializer.java、HiveToDestroy.java、HiveLoader.java等,其中的配置、初始化、清理與From相同,在此看下HiveLoader.java:
public class HiveLoader extends Loader<LinkConfiguration, ToJobConfiguration>{
private static final Logger logger = Logger.getLogger(HiveLoader.class);
private long rowsWritten = 0;
@Override
public void load(LoaderContext context, LinkConfiguration linkConfig,
ToJobConfiguration toJobConfig) throws Exception {
String hdfsURI = linkConfig.linkConfig.hdfsURI;
String hiveServer = linkConfig.linkConfig.hiveSeverHost;
String tableName = toJobConfig.toJobConfig.tableName;
String fileName = "/user/hive/warehouse/"+tableName+"/"+UUID.randomUUID()+".txt";
Path filepath = new Path(fileName);
logger.info("準備導入HDFS文件:"+fileName);
Schema schema = context.getSchema();
Column[] columns = schema.getColumnsArray();
DataReader reader = context.getDataReader();
logger.info("輸入的字段列表:"+Arrays.toString(columns));
Configuration config = new Configuration();
config.set("fs.default.name", hdfsURI);
/**
* 創建HDFS讀寫流
*/
FileSystem fs = filepath.getFileSystem(config);
DataOutputStream filestream = fs.create(filepath, false);
BufferedWriter filewriter = new BufferedWriter(new OutputStreamWriter(filestream, "UTF-8"));
Object[] record;
while((record=reader.readArrayRecord())!=null){
String line = SqoopIDFUtils.toCSV(record, schema);
filewriter.write(line+"\n");
rowsWritten++;
}
filewriter.close();
filestream.close();
fs.close();
Statement stmt = HiveConfig.getStatement(hiveServer);
HiveUtils.loadDataFromDfs(stmt, fileName, tableName);
}
@Override
public long getRowsWritten() {
return rowsWritten;
}
}
3.4 Connector的部署
完成From與To部分的實現之後,最後需要實現HiveConnector對From與To進行整合,該類繼承自SqoopConnector,需要實現的方法包括:
public class HiveConnector extends SqoopConnector{
private static final From FROM = new From(HiveFromInitializer.class,
HivePartitioner.class,
HivePartition.class,
HiveExtractor.class,
HiveFromDestroyer.class);
private static final To TO = new To(HiveToInitializer.class,
HiveLoader.class,
HiveToDestroyer.class);
@Override
public List<Direction> getSupportedDirections(){
return Arrays.asList(Direction.FROM, Direction.TO);
}
@Override
public ResourceBundle getBundle(Locale locale) {
return ResourceBundle.getBundle("hive-connector-config", locale);
}
@Override
public ConnectorConfigurableUpgrader getConfigurableUpgrader(String arg0) {
return null;
}
@Override
public From getFrom() {
return FROM;
}
@Override
public Class<?> getJobConfigurationClass(Direction jobType) {
switch(jobType){
case FROM:
return FromJobConfiguration.class;
case TO:
return ToJobConfiguration.class;
}
throw new SqoopException(HiveConnectorError.HIVE_CONNECTOR_0004, jobType.name());
}
@Override
public Class<?> getLinkConfigurationClass() {
return LinkConfiguration.class;
}
@Override
public To getTo() {
return TO;
}
@Override
public String getVersion() {
return VersionInfo.getBuildVersion();
}
}
HiveConnector中的FROM和TO對導出與導入部分進行了封裝,getBundle方法返回一個配置文件hive-connector-config.properties,在該文件的配置信息指定了Sqoop Shell命令中看到的Connector配置信息:
connector.name = Hive Connector
linkConfig.label = Hive Link
linkConfig.help = Configuration options describing Hive Link.
linkConfig.hdfsURI.label = HDFS URI
linkConfig.hdfsURI.example = hdfs://192.168.47.136:9000
linkConfig.hiveSeverHost.label = HiveServer Address
linkConfig.hiveSeverHost.example = jdbc:hive2://192.168.47.136:10000
fromJobConfig.label = Hive Input Configuration
fromJobConfig.help = Configuration necessary when extracting data from Hive.
fromJobConfig.tableName.label = Hive TableName
fromJobConfig.tableName.help = The Hive Table to Extrat
toJobConfig.label = Hive Output Configuration
toJobConfig.help = Configuration necessary when writing data to Hive.
toJobConfig.tableName.label = Hive Table Name
toJobConfig.tableName.help = The Hive Table to Load
最後還需要一個配置文件sqoopconnector.properties:
# Hive Connector Properties
org.apache.sqoop.connector.class = com.iflytek.sqoop.connector.hive.HiveConnector
org.apache.sqoop.connector.name = hive-connector
該配置文件指定了Connector的實現類與名稱,該文件是必須存在,因爲Sqoop Server會掃描首先Jar包的該文件,再對Connector進行初始化。
編寫好所有的類後,將Java工程打包成jar包,需要注意的是需要將該工程依賴的jar包放在lib文件夾下,並將lib文件夾打進jar包中。將打好的jar複製到sqoop2安裝目錄中server/lib目錄中,重啓Sqoop2,Sqoop2會自動對添加的Connector進行註冊。
在Sqoop Shell中輸入命令 show connector後,即可顯示已註冊的Connector,由圖可見,編寫的HBase、Hive、Solr已被成功註冊至Server。