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源码预览:

        Sqoop2中Connectors开发方法_第1张图片

          要写的代码还是比较多的,其实主要分为两部分: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{
		@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{

		@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{
	
	private static Configuration config = null;
	

	@Override
	public List getPartitions(PartitionerContext context,
			LinkConfiguration linkConfig, FromJobConfiguration jobConfig) {
		
		assert linkConfig != null;
		assert jobConfig != null;
		
		List partitions = new LinkedList();
		
		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> filePartitions = new ArrayList>();
			for(int i=0; i filePartition = getLocations(hdfsUri, files[i].getPath(), numberPartitions);
				filePartitions.add(filePartition);
			}
			
			for(int i=0; i 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{

	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

      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{
	
	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 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 = org.apache.sqoop.connector.hive.HiveConnector
org.apache.sqoop.connector.name = hive-connector

   该配置文件指定了Connector的实现类与名称,该文件是必须存在,因为Sqoop Server会扫描首先Jar包的该文件,再对Connector进行初始化。

Sqoop2中Connectors开发方法_第2张图片

 

   编写好所有的类后,将Java工程打包成jar包,需要注意的是需要将该工程依赖的jar包放在lib文件夹下,并将lib文件夹打进jar包中。将打好的jar复制到sqoop2安装目录中server/lib目录中,重启Sqoop2,Sqoop2会自动对添加的Connector进行注册。

    

   在Sqoop Shell中输入命令 show connector后,即可显示已注册的Connector,由图可见,编写的HBase、Hive、Solr已被成功注册至Server。

   

 

你可能感兴趣的:(Sqoop2中Connectors开发方法)