Sqoop2中Connectors開發方法

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方法,即根據抽取線程數量生成多個HivePartition

      HiveExtractor即抽取部分的實現:

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。

   


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章