写入Hdfs支持指定用户方案

背景

目前 hive写操作和 hdfs写操作都依赖byzer的 FS 插件,存在写数据时权限不足导致报错的问题。

Permission denied: user=root,access=WRITE,inode="/user":hdfs:supergroup:drwxr-xr-x

HDFS 使用的是hadoop用户认证机制(ProxyUser),包括 simple 认证机制和 Kerberos 认证机制;我们当前需要支持 simple 认证机制。simple 认证机制会校验 ProxyUser 中的用户信息,用于判断该用户是否拥有要操作的 hdfs 文件权限。

在 byzer 中用户系统是没有跟第三方的hdfs打通的,仅支持默认的用户。默认用户是在spark启动之前设置的 HADOOP_USER_NAME 系统环境变量来控制,默认值为当前的宿主机用户。因为我们需要支持接入多个第三方托管的 hdfs 数据源,所以不能在启动前一次性指定,需要支持用户配置动态变化。

经调研,有如下2个可行的改造方案:

  1. 改造 byzer 支持 hadoop-hdfs 文件流 API (不使用 spark 写)

  1. 改造 hadoop SDK 支持写操作时指定用户 (使用 spark 写)

下面会列举方案可行性和方案对比。

方案一 : 改造 byzer 支持 hadoop-hdfs 文件流 API

这里只是记录过程,看结论可跳过方案一

现状:byzer提供的 save <table> as FS.`<path>`语法是通过spark实现,其底层通过 parquet-hadoop 实现,没有提供设置用户的方式。

预期:该方案一通过改造 byzer 的save 语法,判断地址的协议如果为第三方hdfs文件系统(非内置文件系统),则使用hadoop-hdfs 文件流 API 写数据,使用该方式可以显示的设置用户。

示例代码

    public void create() throws URISyntaxException, IOException, InterruptedException {
//        1、创建配置文件
        Configuration conf=new Configuration();
//        2、获取文件系统
        FileSystem fs=FileSystem.get(new URI("hdfs://localhost:9000"),conf,"root");
//        3、调用ApI操作,创建文件并写入数据
        FSDataOutputStream in=fs.create(new Path("/user/java/b.txt"));
        in.write("Hello,HDFS".getBytes());
        in.flush();
//       4、关闭流
        fs.close();

    }

该方式不支持parquet格式,如果需要支持该格式则需要借助 parquet-hadoop SDK 。

示例代码

publicstaticvoidcreateFile1() throws Exception {
                Stringfile = "/user/test/test_parquet_file7.parquet";
                Pathpath = newPath(file);
                FileSystemfs = path.getFileSystem(conf);
            if (fs.exists(path)) {
              fs.delete(path, true);
            }
                GroupWriteSupport.setSchema(FILE_SCHEMA, conf);
            SimpleGroupFactoryf = newSimpleGroupFactory(FILE_SCHEMA);
                ParquetWriter<Group> writer = newParquetWriter<Group>(path, newGroupWriteSupport(),
                                CODEC, 1024, 1024, 512, true, false, WriterVersion.PARQUET_2_0, conf);
                for (inti = 0; i < 100; i++) {
          writer.write(
              f.newGroup()
              .append("id", i)
              .append("name", UUID.randomUUID().toString()));
        }
        writer.close();

同样的也会遇到没有办法设置用户的问题,ParquetWriter 本身没有提供文件系统的用户设置,需要对 ParquetWriter进行改造(改造方式见方案二)。

处理流程如下:

该方式需要把spark处理中的数据汇总到driver,由driver通过HDFS 文件流 API 与第三方HDFS文件系统进行交互,获取 NameNode元数据和DataNode中的数据。

实现方式资料:https://blog.csdn.net/IT_liuzhiyuan/article/details/120672642

方案二 : 改造 hadoop SDK 支持写操作时指定用户

byzer在写操作时,使用的是spark提供的save DataFrameWriter API,该函数中会判断我们设置的 format 格式为关系型数据库还是文件类型,代码如下:

/**
   * Returns a logical plan to write the given [[LogicalPlan]] out to this [[DataSource]].
   */
  def planForWriting(mode: SaveMode, data: LogicalPlan): LogicalPlan = {
    if (data.schema.map(_.dataType).exists(_.isInstanceOf[CalendarIntervalType])) {
      throw new AnalysisException("Cannot save interval data type into external storage.")
    }

    providingClass.newInstance() match {
      case dataSource: CreatableRelationProvider =>
        SaveIntoDataSourceCommand(data, dataSource, caseInsensitiveOptions, mode)
      case format: FileFormat =>
        DataSource.validateSchema(data.schema)
        planForWritingFileFormat(format, mode, data)
      case _ =>
        sys.error(s"${providingClass.getCanonicalName} does not allow create table as select.")
    }
  }

在FileFormat会通过InsertIntoHadoopFsRelationCommand执行数据写入逻辑,主要流程包括2部分,其一判断SaveMode确定是否要创建目录或者忽略(这里 spark-core 会调用文件系统),其二调用 FileFormatWriter 真正写入 hdfs(这里 parquet-hadoop 会调用文件系统)。

第一步判断SaveMode中,可以看到使用的为默认的用户创建的文件系统:

override def run(sparkSession: SparkSession, child: SparkPlan): Seq[Row] = {
  // Most formats don't do well with duplicate columns, so lets not allow that
  SchemaUtils.checkColumnNameDuplication(
    outputColumnNames,s"when inserting into $outputPath",sparkSession.sessionState.conf.caseSensitiveAnalysis)

  val hadoopConf = sparkSession.sessionState.newHadoopConfWithOptions(options)
  val fs= outputPath.getFileSystem(hadoopConf)
  val qualifiedOutputPath= outputPath.makeQualified(fs.getUri, fs.getWorkingDirectory)

第二步中,我们DP运行结果需要保存为 parquet 格式,spark-core 此时会调用对应的parquet数据源实现 ParquetOutputFormat,在输出类中,可以看到使用的为默认的用户创建的文件系统:

public static HadoopInputFile fromPath(Path path, Configuration conf)
    throws IOException {
  FileSystem fs= path.getFileSystem(conf);return new HadoopInputFile(fs, fs.getFileStatus(path), conf);
}

通过对 SDK 中文件系统操作的改造,可以支持动态的修改用户设置。其中用户设置是可以通过Configuration传入的,为避免对公共配置HadoopConfiguration修改产生静态配置覆盖的影响,我们通过获取spark传入的Options即可实现。spark在调用文件系统前会创建一个新的Configuration并设置我们传入的Options。

val hadoopConf= sparkSession.sessionState.newHadoopConfWithOptions(options)
val fs= outputPath.getFileSystem(hadoopConf)

在newHadoopConfWithOptions可以看到Configuration是每次都会新建的。

private[sql] object SessionState {
  def newHadoopConf(hadoopConf: Configuration, sqlConf: SQLConf): Configuration = {
    val newHadoopConf= new Configuration(hadoopConf)
    sqlConf.getAllConfs.foreach { case (k, v) => if (v ne null) newHadoopConf.set(k, v) }
    newHadoopConf}
}

在该方案中,写流程是一个分布式的处理,FileFormatWriter 会通过 sparkContext.runJob 提交一个spark job分布式处理写操作,基于spark 前面RDD分区的基础上对每个分区做写操作。

val jobIdInstant= new Date().getTime
val ret= new Array[WriteTaskResult](rddWithNonEmptyPartitions.partitions.length)
sparkSession.sparkContext.runJob(
  rddWithNonEmptyPartitions,(taskContext: TaskContext, iter: Iterator[InternalRow]) => {
    executeTask(
      description = description,jobIdInstant = jobIdInstant,sparkStageId = taskContext.stageId(),sparkPartitionId = taskContext.partitionId(),sparkAttemptNumber = taskContext.taskAttemptId().toInt & Integer.MAX_VALUE,committer,iterator = iter,concurrentOutputWriterSpec = concurrentOutputWriterSpec)
  },rddWithNonEmptyPartitions.partitions.indices,(index, res: WriteTaskResult) => {
    committer.onTaskCommit(res.commitMsg)
    ret(index) = res})

每个写文件的任务在resultTask执行完之后把文件的元数据(包括,文件个数,文件大小,文件行数)回传给driver.

方案对比

推荐采用方案二,原因如下:

  1. 方案一无法实现parquet格式输出,会导致数据出现1~10倍的膨胀。

  1. 方案一需要数据(运行实例的结果)先中间落地到juicefs中,然后基于落地的文件在driver中开启hdfs client写入hdfs集群,是一个单线程的操作,大数据量会出现卡死情况。方案二中spark原生支持分布式写操作,spark会将中间数据文件直接开启ParquetFileWriter进行分布式写,改造其SDK更灵活可控。

代码实现

代码就比较简单了,我这里做一个简单的实现,如果需要更细节的控制,请尽量通过 Kerberos 认证机制,或者完整改造hadoop ugi,而不应该仅在实例创建入口做控制,下面使用方式:

  1. 第一步编译jar包或下载jar包,下载我git代码 `https://github.com/hellozepp/hadoop`,分支为addCommonUgiCnf, mvn命令 `mvn clean install -Dmaven.test.skip=true` 打包hadoop-common 和 hadoop-client-api。该分支基于hadoop-3.2.2分支,如果你用的和我一样可以直接下载jar,我丢在debug目录了。

  1. 扔到spark/jars目录,并排除自带的hadoop-common和hadoop-client-api jar,注意检查fat jar。

  1. 把要设置的user加到spark的options里面,搞定。

writer.option("user", "xxx").format(format).save(dbtable)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值