1、配置参数和jar包
1、将构建好的Iceberg的spark模块jar包,复制到spark jars下
cp /opt/module/iceberg-apache-iceberg-0.11.1/spark3-extensions/build/libs/* /opt/module/spark-3.0.1-bin-hadoop2.7/jars/
cp /opt/module/iceberg-apache-iceberg-0.11.1/spark3-runtime/build/libs/* /opt/module/spark-3.0.1-bin-hadoop2.7/jars/
2、配置spark参数,配置Spark Sql Catlog,可以用两种方式,基于hive和基于hadoop,这里先选择基于hadoop。修改spark conf目录下的spark-default.conf
spark.sql.catalog.hive_prod = org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.hive_prod.type = hive
spark.sql.catalog.hive_prod.uri = thrift://hadoop1:9083
spark.sql.catalog.hadoop_prod = org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.hadoop_prod.type = hadoop
spark.sql.catalog.hadoop_prod.warehouse = hdfs://mycluster/spark/warehouse
spark.sql.catalog.catalog-name.type = hadoop
spark.sql.catalog.catalog-name.default-namespace = db
spark.sql.catalog.catalog-name.uri = thrift://hadoop1:9083
spark.sql.catalog.catalog-name.warehouse= hdfs://mycluster/spark/warehouse
2、Spark sql操作
2.1、建表
使用spark sql创建iceberg表,配置完毕后,会多出一个hadoop_prod.db数据库,但是注意这个数据库通过show tables是看不到的。可以直接通过use hadoop_prod;没有报错则表示配置成功。然后可以create database db;并通过以下命令建表:
use hadoop_prod.db;
create table testA(
id bigint,
name string,
age int,
dt string)
USING iceberg
PARTITIONED by(dt);
当向表中执行插入语句,可以直接通过select查询数据
insert into testA values(1,'张三',18,'2021-06-21');
select * from hadoop_prod.db.testA;
2.2、overwrite操作
当向表中执行overwrite覆盖操作是,与 hive 一样,会将原始数据重新刷新。注意覆盖的不是整张表的数据,只是这个分区的数据。被覆盖的数据不会被物理删除,还会存在HDFS上,除非是对表进行drop操作。
2.3、动态覆盖
1、Spark的默认覆盖模式是静态的,但在写入iceberg时建议使用动态覆盖模式(执行insert overwrite不用指定分区列。会将A表的数据全部覆盖B表的数据)。静态覆盖模式需要制定分区列,动态覆盖模式不需要。
2、设置动态覆盖模式,修改 spark-default.conf,添加对应参数
spark.sql.sources.partitionOverwriteMode=dynamic
2.4、静态覆盖
类似于每天的跑批,跟 hive 插入时手动指定分区一致,需要手动指定分区列的值。会将A表中的所有数据复制到B表的指定分区中。
insert overwrite testB Partition(dt='2021-06-26') select id,name,age from testA;
2.5、删除数据
iceberg并不会物理删除数据,当执行删除操作,提示删除成功,再次查询数据。发现表中已无数据,但是存在hdfs上的物理并没有实际删除。
2.6、历史快照
每张表都拥有一张历史表,历史表的表名为当前表加上后缀.history,注意:查询历史表的时候必须是表的全称,不可用先切到 hadoop.db 库中再查 testB。
select * from hadoop_prod.db.testB.history;
可以查看到每次操作后的对应的快照记录,也可以查询对应快照表,快照表的表名在原表基础上加上.snapshots,也是一样必须是表的全称不能简写。
select * from hadoop_prod.db.testB.snapshots;
可以在看到commit的时间,snapshot快照的id,parent_id父节点,operation操作类型,已经summary概要,summary概要字段中可以看到数据量大小,总条数,路径等信息。两张表也可以根据snapshot_id快照id进行join关联查询。
select *from hadoop_prod.db.testB.history a join hadoop_prod.db.testB.snapshots b on a.snapshot_id=b.snapshot_id ;
知道了快照表和快照信息后,可以根据快照 id 来查询具体的历史信息,发进行检测是否误操作,如果是误操作则可通过 spark 重新刷新数据。查询方式如下
spark.read.option("snapshot-id","5549650043576786799").format("iceberg").load("/hive/w arehouse/db/testB").show
2.7、隐藏分区(有 bug 时区不对)
隐藏分区支持的函数有 years,months,days,hours,bucket,truncate。比如接下来创建一张 testC 表,表中有id,name 和 ts时间戳。
create table hadoop_prod.db.testC(
id bigint,
name string,
ts timestamp
)
using iceberg
partitioned by (days(ts));
insert into hadoop_prod.db.testC values(1,'张三',cast(1624773600 as timestamp)),(2,'李四',cast(1624860000 as timestamp));
发现时区不对(比方说13点的数据,会放在5点的分区),修改 /opt/module/spark-3.0.1-bin-hadoop2.7/conf/spark-defaults.conf中的spark.sql.session.timeZone=GMT+8无效。
2.8、bucket 函数(有 bug)
创建testD表使用bucket函数。分桶hash算法采用Murmur3 hash,官网介绍https://iceberg.apache.org/spec/#partition-transforms。
create table hadoop_prod.db.testD(
id bigint,
name string,
ts timestamp
) using iceberg
partitioned by (bucket(16,id));
当向同一分区中写入多条数据,会报错,并发写的问题。
2.9、truncate函数
截取长度作为分区。(待续)
create table hadoop_prod.db.testE(
id bigint,
name string,
ts timestamp)
using iceberg
partitioned by (truncate(4,id));
3、DataFrame操作
引入依赖:
<properties>
<spark.version>3.0.1</spark.version>
<scala.version>2.12.10</scala.version>
<log4j.version>1.2.17</log4j.version>
<slf4j.version>1.7.22</slf4j.version>
<iceberg.version>0.11.1</iceberg.version>
</properties>
<dependency>
<groupId>org.apache.iceberg</groupId>
<artifactId>iceberg-spark3-runtime</artifactId>
<version>${iceberg.version}</version>
</dependency>
<dependency>
<groupId>org.apache.iceberg</groupId>
<artifactId>iceberg-spark3-extensions</artifactId>
<version>${iceberg.version}</version>
</dependency>
3.1、Spark DF读取iceberg的三种方式
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf()
.set("spark.sql.catalog.hadoop_prod", "org.apache.iceberg.spark.SparkCatalog")
.set("spark.sql.catalog.hadoop_prod.type", "hadoop")
.set("spark.sql.catalog.hadoop_prod.warehouse", "hdfs://mycluster/hive/warehouse")
.set("spark.sql.catalog.catalog-name.type", "hadoop")
.set("spark.sql.catalog.catalog-name.default-namespace", "default")
.set("spark.sql.sources.partitionOverwriteMode", "dynamic")
.set("spark.sql.session.timeZone", "GMT+8")
.setMaster("local[*]").setAppName("table_operations")
val sparkSession = SparkSession.builder().config(sparkConf).getOrCreate()
readTale(sparkSession)
}
/**
* 读取iceberg 的表
* @param sparkSession
*/
def readTale(sparkSession: SparkSession) = {
//三种方式
sparkSession.table("hadoop_prod.db.testA").show()
sparkSession.read.format("iceberg").load("hadoop_prod.db.testA").show()
sparkSession.read.format("iceberg").load("/hive/warehouse/db/testA").show()// 路径到表就行,不要到具体文件
}
3.2、Spark DF读取Iceberg快照表的两种方式
def readSnapShots(sparkSession: SparkSession) = {
//根据查询 hadoop_prod.db.testA.snapshots 快照表可以知道快照时间和快照id
//根据时间戳读取,必须是时间戳 不能使用格式化后的时间
sparkSession.read
.option("as-of-timestamp", "1624961454000") //毫秒时间戳,查询比该值时间更早的快照
.format("iceberg")
.load("hadoop_prod.db.testA").show()
//根据快照 id 查询
sparkSession.read
.option("snapshot-id", "9054909815461789342")
.format("iceberg")
.load("hadoop_prod.db.testA").show()
}
3.3、写入数据并自动创建表
def writeAndCreateTable(sparkSession: SparkSession) = {
import sparkSession.implicits._
import org.apache.spark.sql.functions._
val data = sparkSession.createDataset[Student](Array(Student(1001, " 张 三 ", 18, "2021-06-28"),
Student(1002, "李四", 19, "2021-06-29"), Student(1003, "王五", 20, "2021-06-29")))
data.writeTo("hadoop_prod.db.test1").partitionedBy(col("dt")) //指定dt为分区列
.create()
}
case class Student(id: Int, name: String, age: Int, dt: String)
3.4、写数据
3.4.1、Append
def AppendTable(sparkSession: SparkSession) = {
//两种方式
import sparkSession.implicits._
val data = sparkSession.createDataset(Array(Student(1003, "王五", 11, "2021-06-29"), Student(1004, "赵六", 10, "2021-06-30")))
data.writeTo("hadoop_prod.db.test1").append() // 使用DataFrameWriterV2 API spark3.0
data.write.format("iceberg").mode("append").save("hadoop_prod.db.test1") //使用DataFrameWriterV1 API spark2.4
}
3.4.2、Overwrite
1、动态覆盖,只会刷新所属分区数据
// 动态覆盖,只会刷新所属分区数据
def OverWriteTable(sparkSession: SparkSession) = {
import sparkSession.implicits._
val data = sparkSession.createDataset(Array(Student(1003, " 王五", 11, "2021-06-29"),
Student(1004, "赵六", 10, "2021-06-30")))
data.writeTo("hadoop_prod.db.test1").overwritePartitions()
}
2、静态覆盖,手动指定分区
def OverWriteTable2(sparkSession: SparkSession) = {
import sparkSession.implicits._
val data = sparkSession.createDataset(Array(Student(1, "s1", 1, "111"), Student(2, "s2", 2, "111")))
data.writeTo("hadoop_prod.db.test1").overwrite($"dt" === "2021-06-30")
}
注意:输出表不能存在多个分区。不然会报错
4、存在的问题和缺点
4.1、问题
- 时区无法设置,见2.7。
- Spark Sql客户端打开,缓存无法更新,修改表数据后,得需要关了黑窗口再重新打开,查询才是更新后的数据。
- 表分区如果指定多个分区或分桶,那么插入批量数据时,如果这一批数据有多条数据在统一分区内,会报错
4.2、缺点
- 与hudi相比,没有解决小文件问题
- 与hudi相比,缺少行级更新,只能对标的数据按照分区进行overwrite全量覆盖