未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合已有Spark、Scala和DataFrame基础的读者参考多文件格式读写、多数据源连接、分区调优及ETL组织方式,不是可直接运行或已经验证的生产工程;本次仅作全文结构审阅,未作外部事实核验或运行测试。正文未提供外部连接器、JDBC驱动及其版本兼容配置,MongoDB旧式配置、SQLConf内部API和Delta维护接口需按实际环境确认;Avro注册中心函数仅返回静态Schema,并未实现远程集成。Kafka窗口聚合采用近似用户去重计数,但未设置水位线,不能据此推定长期运行时旧窗口状态可被清理。文件流首例未给出输入Schema,写出所需year、month、day字段未在该链路中准备,后例eventSchema未定义且schemaLocation未使用,因此不构成Schema演进实现;Elasticsearch的aggs请求也不能作为桶聚合结果读取方案。路径、凭据、部分DataFrame及输入字段需由读者准备,文件大小估算不保证实际输出大小;覆盖写入与Delta清理应先确认目标数据可替换,并评估长运行任务及历史读取需求。 已取消无法确定目标的可点击引用「【技术与工具】/Kafka/Kafka连接器」,保留原提及;Missing link target: 【技术与工具】/Kafka/Kafka连接器
Spark数据读取与写入完全指南
概述
数据读取和写入是Spark应用程序的基础操作。Spark支持多种数据源和文件格式,提供了统一的DataFrameReader和DataFrameWriter API。本指南将深入介绍各种数据读写场景、性能优化技巧和最佳实践。
数据读取架构
数据源架构图
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
graph TB
classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
subgraph "Spark数据读取架构"
Reader[DataFrameReader]
subgraph "文件系统"
HDFS[HDFS]
S3[Amazon S3]
GCS[Google Cloud Storage]
Azure[Azure Blob Storage]
Local[本地文件系统]
end
subgraph "文件格式"
CSV[CSV]
JSON[JSON]
Parquet[Parquet]
ORC[ORC]
Avro[Avro]
Text[Text]
XML[XML]
end
subgraph "数据库"
MySQL[MySQL]
PostgreSQL[PostgreSQL]
Oracle[Oracle Database]
MongoDB[MongoDB]
Cassandra[Cassandra]
HBase[HBase]
end
subgraph "流数据源"
Kafka[Apache Kafka]
Kinesis[AWS Kinesis]
EventHub[Azure Event Hub]
Socket[TCP Socket]
end
subgraph "大数据生态"
Hive[Hive Tables]
Delta[Delta Lake]
Iceberg[Apache Iceberg]
Elasticsearch[Elasticsearch]
end
Reader --> HDFS
Reader --> S3
Reader --> CSV
Reader --> JSON
Reader --> Parquet
Reader --> MySQL
Reader --> Kafka
Reader --> Hive
Reader --> Delta
end
class Reader,HDFS,S3,GCS,Azure,Local,CSV,JSON,Parquet,ORC,Avro,Text,XML,MySQL,PostgreSQL,Oracle,MongoDB,Cassandra,HBase,Kafka,Kinesis,EventHub,Socket,Hive,Delta,Iceberg,Elasticsearch default
文件格式读写
CSV文件操作
import org.apache.spark.sql.{SparkSession, DataFrame}
import org.apache.spark.sql.types._
import org.apache.spark.sql.functions._
val spark = SparkSession.builder()
.appName("Data IO Operations")
.master("local[*]")
.getOrCreate()
// 1. 读取CSV文件
val csvDF = spark.read
.option("header", "true") // 包含表头
.option("inferSchema", "true") // 自动推断数据类型
.option("sep", ",") // 分隔符
.option("quote", "\"") // 引用字符
.option("escape", "\\") // 转义字符
.option("nullValue", "NULL") // 空值表示
.option("dateFormat", "yyyy-MM-dd") // 日期格式
.option("timestampFormat", "yyyy-MM-dd HH:mm:ss") // 时间戳格式
.csv("path/to/file.csv")
// 2. 使用显式Schema读取CSV
val csvSchema = StructType(Array(
StructField("id", IntegerType, nullable = false),
StructField("name", StringType, nullable = true),
StructField("age", IntegerType, nullable = true),
StructField("salary", DoubleType, nullable = true),
StructField("hire_date", DateType, nullable = true)
))
val csvWithSchemaDF = spark.read
.schema(csvSchema)
.option("header", "true")
.option("dateFormat", "yyyy-MM-dd")
.csv("employees.csv")
// 3. 读取多个CSV文件
val multiCsvDF = spark.read
.option("header", "true")
.option("inferSchema", "true")
.csv("path/to/csv/files/*.csv")
// 4. 写入CSV文件
csvDF.write
.mode("overwrite") // append, overwrite, ignore, error
.option("header", "true")
.option("sep", ",")
.option("quote", "\"")
.option("escape", "\\")
.option("nullValue", "")
.option("dateFormat", "yyyy-MM-dd")
.option("timestampFormat", "yyyy-MM-dd HH:mm:ss")
.csv("output/path")
// 5. 单文件输出
csvDF.coalesce(1).write
.mode("overwrite")
.option("header", "true")
.csv("output/single_file")
// 6. 处理CSV中的复杂数据
val complexCsvDF = spark.read
.option("header", "true")
.option("multiline", "true") // 支持多行记录
.option("escape", "\"")
.csv("complex_data.csv")
// 清理和转换数据
val cleanedDF = complexCsvDF
.withColumn("name", trim(col("name")))
.withColumn("age", col("age").cast(IntegerType))
.filter(col("age").isNotNull && col("age") > 0)
JSON文件操作
// 1. 读取JSON文件
val jsonDF = spark.read
.option("multiLine", "true") // 支持多行JSON
.option("allowComments", "true") // 允许注释
.option("allowUnquotedFieldNames", "true") // 允许不带引号的字段名
.option("dateFormat", "yyyy-MM-dd")
.option("timestampFormat", "yyyy-MM-dd HH:mm:ss")
.json("data.json")
// 2. 处理嵌套JSON
val nestedJsonDF = spark.read.json("nested_data.json")
// 展开嵌套结构
val flattenedDF = nestedJsonDF
.select(
col("id"),
col("name"),
col("address.street").alias("street"),
col("address.city").alias("city"),
col("address.zipcode").alias("zipcode"),
explode(col("skills")).alias("skill")
)
// 3. JSON字符串列处理
// 假设CSV表头包含id和json_column,且JSON字段按CSV规则转义。
val jsonStringDF = spark.read
.option("header", "true")
.csv("data_with_json_column.csv")
val parsedJsonDF = jsonStringDF
.withColumn("parsed_json",
from_json(col("json_column"),
schema = StructType(Array(
StructField("name", StringType, true),
StructField("age", IntegerType, true)
))
)
)
.select(
col("id"),
col("parsed_json.name").alias("name"),
col("parsed_json.age").alias("age")
)
// 4. 写入JSON
jsonDF.write
.mode("overwrite")
.option("dateFormat", "yyyy-MM-dd")
.option("timestampFormat", "yyyy-MM-dd HH:mm:ss")
.json("output/json")
// 5. 复杂JSON数据处理
case class Address(street: String, city: String, zipcode: String)
case class Person(id: Int, name: String, age: Int, address: Address, skills: Seq[String])
import spark.implicits._
val complexJsonData = Seq(
Person(1, "Alice", 25, Address("123 Main St", "NYC", "10001"), Seq("Java", "Python")),
Person(2, "Bob", 30, Address("456 Oak Ave", "LA", "90210"), Seq("Scala", "SQL"))
).toDF()
// 转换为JSON格式
val jsonOutput = complexJsonData
.select(to_json(struct(col("*"))).alias("json_string"))
jsonOutput.write
.mode("overwrite")
.text("output/json_strings")
Parquet文件操作
// 1. 读取Parquet文件
val parquetDF = spark.read
.option("mergeSchema", "true") // 合并Schema变更
.parquet("data.parquet")
// 2. 读取分区Parquet文件
val partitionedParquetDF = spark.read
.option("basePath", "partitioned_data")
.parquet("partitioned_data/year=*/month=*")
// 查看分区信息
partitionedParquetDF.select("year", "month").distinct().show()
// 3. Schema演进处理
val schemaEvolutionDF = spark.read
.option("mergeSchema", "true")
.parquet("evolving_schema/")
// 4. 写入Parquet文件
parquetDF.write
.mode("overwrite")
.option("compression", "snappy") // gzip, lzo, brotli, lz4, zstd
.parquet("output/parquet")
// 5. 分区写入
parquetDF.write
.mode("overwrite")
.partitionBy("year", "month")
.option("compression", "snappy")
.parquet("output/partitioned_parquet")
// 6. 控制文件大小
parquetDF
.repartition(10) // 控制输出文件数量
.write
.mode("overwrite")
.option("compression", "snappy")
.parquet("output/sized_parquet")
// 7. Parquet性能优化
import org.apache.spark.sql.internal.SQLConf
// 启用列式向量化
spark.conf.set(SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key, "true")
// 设置批处理大小
spark.conf.set(SQLConf.PARQUET_VECTORIZED_READER_BATCH_SIZE.key, "4096")
// 启用谓词下推
spark.conf.set(SQLConf.PARQUET_FILTER_PUSHDOWN_ENABLED.key, "true")
// 读取优化示例
val optimizedParquetDF = spark.read
.option("pushDownPredicate", "true")
.parquet("large_parquet_dataset")
.filter(col("status") === "active")
.select("id", "name", "amount")
Avro文件操作
// 1. 读取Avro文件
val avroDF = spark.read
.format("avro")
.option("avroSchema", avroSchemaString) // 可选:指定Schema
.load("data.avro")
// 2. 写入Avro文件
avroDF.write
.mode("overwrite")
.format("avro")
.option("compression", "snappy")
.save("output/avro")
// 3. Avro Schema注册中心集成
val schemaRegistryAvroDF = spark.read
.format("avro")
.option("avroSchema", getSchemaFromRegistry("user-schema"))
.load("data.avro")
def getSchemaFromRegistry(schemaName: String): String = {
// 从Schema Registry获取Schema的逻辑
// 这里是示例代码
"""
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null}
]
}
"""
}
ORC文件操作
// 1. 读取ORC文件
val orcDF = spark.read
.option("mergeSchema", "true")
.orc("data.orc")
// 2. 写入ORC文件
orcDF.write
.mode("overwrite")
.option("compression", "zlib") // none, zlib, snappy, lzo, lz4, zstd
.orc("output/orc")
// 3. 普通ORC文件读取(不提供事务表快照语义)
// 事务表读取需采用对应表格式或平台支持的读取方式。
val ordinaryOrcDF = spark.read
.orc("path/to/orc_files")
数据库连接
JDBC数据源
// 1. 基础JDBC连接
val jdbcDF = spark.read
.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306/mydb")
.option("dbtable", "employees")
.option("user", "username")
.option("password", "password")
.option("driver", "com.mysql.cj.jdbc.Driver")
.load()
// 2. 高级JDBC选项
val advancedJdbcDF = spark.read
.format("jdbc")
.option("url", "jdbc:postgresql://localhost:5432/analytics")
.option("dbtable", "sales_data")
.option("user", "analyst")
.option("password", "password")
.option("driver", "org.postgresql.Driver")
// 性能优化选项
.option("fetchsize", "1000") // 批量获取大小
.option("batchsize", "1000") // 批量插入大小
.option("numPartitions", "8") // 并行度
.option("partitionColumn", "id") // 分区列
.option("lowerBound", "1") // 分区下界
.option("upperBound", "1000000") // 分区上界
// PostgreSQL JDBC连接参数;此处不配置连接池
.option("ssl", "false")
.load()
// 3. 自定义SQL查询
val customQueryDF = spark.read
.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306/ecommerce")
.option("query", """
SELECT
o.order_id,
o.customer_id,
o.order_date,
SUM(oi.quantity * oi.price) as total_amount
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.order_date >= '2024-01-01'
GROUP BY o.order_id, o.customer_id, o.order_date
""")
.option("user", "reader")
.option("password", "password")
.load()
// 4. 写入数据库
jdbcDF.write
.mode("append") // append, overwrite, ignore, error
.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306/warehouse")
.option("dbtable", "processed_data")
.option("user", "writer")
.option("password", "password")
.option("driver", "com.mysql.cj.jdbc.Driver")
.option("batchsize", "1000")
.option("truncate", "false") // 覆盖模式下是否截断表
.save()
// 5. 使用Properties传递JDBC连接参数(不实现连接池)
import java.util.Properties
val connectionProperties = new Properties()
connectionProperties.put("user", "username")
connectionProperties.put("password", "password")
connectionProperties.put("driver", "com.mysql.cj.jdbc.Driver")
connectionProperties.put("fetchsize", "1000")
connectionProperties.put("batchsize", "1000")
val pooledJdbcDF = spark.read
.jdbc("jdbc:mysql://localhost:3306/mydb", "large_table", connectionProperties)
// 6. 分区读取大表
def readLargeTable(spark: SparkSession, tableName: String): DataFrame = {
spark.read
.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306/bigdata")
.option("dbtable", s"(SELECT *, MOD(id, 10) as partition_key FROM $tableName) t")
.option("user", "reader")
.option("password", "password")
.option("partitionColumn", "partition_key")
.option("lowerBound", "0")
.option("upperBound", "9")
.option("numPartitions", "10")
.load()
}
val largeTableDF = readLargeTable(spark, "transactions")
NoSQL数据库
// 1. MongoDB连接
val mongoDF = spark.read
.format("com.mongodb.spark.sql.DefaultSource")
.option("uri", "mongodb://localhost:27017/mydb.mycollection")
.option("partitioner", "MongoShardedPartitioner")
.option("partitionerOptions.shardkey", "_id")
.load()
// MongoDB聚合管道
val mongoAggDF = spark.read
.format("mongo")
.option("uri", "mongodb://localhost:27017/analytics.events")
.option("pipeline", """[
{"$match": {"event_date": {"$gte": {"$date": "2024-01-01"}}}},
{"$group": {"_id": "$event_type", "count": {"$sum": 1}}}
]""")
.load()
// 写入MongoDB
mongoDF.write
.mode("append")
.format("mongo")
.option("uri", "mongodb://localhost:27017/warehouse.processed_events")
.save()
// 2. Cassandra连接
import org.apache.spark.sql.cassandra._
val cassandraDF = spark.read
.cassandraFormat("events", "analytics")
.option("cluster", "cassandra-cluster")
.option("confirm.truncate", "true")
.load()
// Cassandra写入
cassandraDF.write
.cassandraFormat("processed_events", "warehouse")
.mode("append")
.save()
// 3. HBase连接
val hbaseDF = spark.read
.format("org.apache.spark.sql.execution.datasources.hbase")
.option("catalog", s"""{
"table":{"namespace":"default", "name":"user_profiles"},
"rowkey":"key",
"columns":{
"rowkey":{"cf":"rowkey", "col":"key", "type":"string"},
"name":{"cf":"personal", "col":"name", "type":"string"},
"age":{"cf":"personal", "col":"age", "type":"int"}
}
}""")
.load()
流数据处理
Kafka集成
import org.apache.spark.sql.streaming.{StreamingQuery, Trigger}
// 1. 从Kafka读取流数据
val kafkaStreamDF = spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribe", "user-events,system-logs")
.option("startingOffsets", "earliest") // earliest, latest, 或指定offset
.option("maxOffsetsPerTrigger", "1000") // 每个批次最大消息数
.option("kafkaConsumer.pollTimeoutMs", "2000")
.load()
// 2. 处理Kafka消息
val processedStreamDF = kafkaStreamDF
.select(
col("key").cast("string"),
col("value").cast("string"),
col("topic"),
col("partition"),
col("offset"),
col("timestamp")
)
.withColumn("event_data",
from_json(col("value"),
StructType(Array(
StructField("user_id", StringType, true),
StructField("event_type", StringType, true),
StructField("event_time", TimestampType, true),
StructField("properties", MapType(StringType, StringType), true)
))
)
)
.select(
col("topic"),
col("partition"),
col("offset"),
col("timestamp").alias("kafka_timestamp"),
col("event_data.*")
)
// 3. 流数据聚合
val streamAggDF = processedStreamDF
.filter(col("event_type").isNotNull)
.groupBy(
window(col("event_time"), "1 minute", "30 seconds"),
col("event_type")
)
.agg(
count("*").alias("event_count"),
approx_count_distinct("user_id").alias("approx_unique_users")
)
// 4. 写入流数据到Kafka
val query = streamAggDF
.select(
to_json(struct(col("*"))).alias("value")
)
.writeStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("topic", "aggregated-events")
.option("checkpointLocation", "/tmp/kafka-checkpoint")
.outputMode("update")
.trigger(Trigger.ProcessingTime("30 seconds"))
.start()
// 5. 多Topic处理
val multiTopicDF = spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribePattern", "events-.*") // 正则表达式匹配Topic
.load()
// 根据Topic路由处理
val routedStreamDF = multiTopicDF
.withColumn("event_category",
when(col("topic") === "events-user", "user_event")
.when(col("topic") === "events-system", "system_event")
.otherwise("unknown")
)
query.awaitTermination()
文件流处理
// 1. 文件流监听
val fileStreamDF = spark
.readStream
.format("json") // csv, json, parquet, text
.option("path", "/input/streaming")
.option("maxFilesPerTrigger", "10") // 每个批次处理的最大文件数
.option("latestFirst", "true") // 优先处理最新文件
.load()
// 2. Schema演进处理
val schemaLocation = "/schemas/events"
val fileStreamWithSchemaDF = spark
.readStream
.format("json")
.option("path", "/input/events")
.option("maxFilesPerTrigger", "5")
.schema(eventSchema) // 明确指定Schema
.load()
// 3. 写入文件流
val fileOutput = fileStreamDF
.writeStream
.format("parquet")
.option("path", "/output/processed")
.option("checkpointLocation", "/checkpoints/file-stream")
.partitionBy("year", "month", "day")
.trigger(Trigger.ProcessingTime("1 minute"))
.start()
// 4. 增量处理
val incrementalDF = spark
.readStream
.format("delta") // 使用Delta Lake进行增量处理
.option("path", "/delta/source")
.option("maxFilesPerTrigger", "100")
.load()
fileOutput.awaitTermination()
高级数据源
Delta Lake操作
// 1. 读取Delta表
val deltaDF = spark.read
.format("delta")
.load("/path/to/delta-table")
// 2. 时间旅行查询
val historicalDF = spark.read
.format("delta")
.option("timestampAsOf", "2024-01-01 00:00:00")
.load("/path/to/delta-table")
val versionDF = spark.read
.format("delta")
.option("versionAsOf", 10)
.load("/path/to/delta-table")
// 3. Delta表合并操作
import io.delta.tables._
val deltaTable = DeltaTable.forPath(spark, "/path/to/delta-table")
deltaTable.as("target")
.merge(
newDataDF.as("source"),
"target.id = source.id"
)
.whenMatched()
.updateAll()
.whenNotMatched()
.insertAll()
.execute()
// 4. 写入Delta表
newDataDF.write
.format("delta")
.mode("overwrite")
.option("overwriteSchema", "true")
.partitionBy("date")
.save("/path/to/new-delta-table")
// 5. Delta表维护
// 优化文件布局
deltaTable.optimize()
.executeCompaction()
// Z-Order优化
deltaTable.optimize()
.executeZOrderBy("customer_id", "order_date")
// 清理超过保留期且不再被当前表引用的数据文件
// 执行前确认长运行任务及历史读取不再需要这些文件;日志保留另有配置。
deltaTable.vacuum(168) // 数据文件保留阈值为168小时,不保证完整保留7天时间旅行能力
Elasticsearch集成
// 1. 读取Elasticsearch数据
val esDF = spark.read
.format("es")
.option("es.nodes", "localhost")
.option("es.port", "9200")
.option("es.resource", "sales/orders")
.option("es.query", """{"term": {"status": "completed"}}""")
.option("es.read.metadata", "true")
.load()
// 2. 写入Elasticsearch
esDF.write
.format("es")
.option("es.nodes", "localhost")
.option("es.port", "9200")
.option("es.resource", "analytics/processed-orders")
.option("es.mapping.id", "order_id")
.mode("append")
.save()
// 3. Elasticsearch聚合查询
val esAggDF = spark.read
.format("es")
.option("es.resource", "logs/application")
.option("es.query", """{
"query": {"range": {"@timestamp": {"gte": "2024-01-01"}}},
"aggs": {
"error_types": {
"terms": {"field": "level.keyword"}
}
}
}""")
.load()
性能优化策略
文件格式选择指南
object FileFormatOptimization {
// 1. 根据用途选择格式
def chooseFileFormat(useCase: String): String = useCase match {
case "analytics_queries" => "parquet" // 列式存储,查询友好
case "archival_storage" => "parquet" // 高压缩率
case "streaming_ingestion" => "avro" // Schema演进支持
case "human_readable" => "json" // 可读性好
case "simple_export" => "csv" // 通用性好
case "transactional_data" => "delta" // ACID事务支持
case _ => "parquet"
}
// 2. 压缩算法选择
def chooseCompression(priority: String): String = priority match {
case "speed" => "lz4" // 快速压缩解压
case "size" => "brotli" // 高压缩率
case "balanced" => "snappy" // 平衡选择
case "cpu_intensive" => "gzip" // 高CPU使用
case _ => "snappy"
}
// 3. 文件大小优化
def optimizeFileSize(df: DataFrame, targetSizeMB: Int = 128): DataFrame = {
val currentPartitions = df.rdd.getNumPartitions
require(targetSizeMB > 0, "targetSizeMB必须大于0")
val estimatedSizeMB = estimateDataFrameSize(df).toDouble / 1024 / 1024
val optimalPartitions = Math.max(1, Math.ceil(estimatedSizeMB / targetSizeMB).toInt)
if (optimalPartitions < currentPartitions) {
df.coalesce(optimalPartitions)
} else {
df.repartition(optimalPartitions)
}
}
def estimateDataFrameSize(df: DataFrame): Long = {
// 简化的大小估算逻辑
val sampleDF = df.sample(0.01)
val sampleSize = sampleDF.count()
val totalSize = df.count()
// 估算每行字节数
val avgRowSize = estimateRowSize(df.schema)
totalSize * avgRowSize
}
def estimateRowSize(schema: StructType): Long = {
schema.fields.map { field =>
field.dataType match {
case IntegerType => 4
case LongType => 8
case DoubleType => 8
case StringType => 50 // 假设平均字符串长度
case TimestampType => 8
case _ => 20 // 其他类型默认估算
}
}.sum
}
}
分区策略优化
// 1. 时间分区策略
def createTimePartitions(df: DataFrame, dateColumn: String): DataFrame = {
df.withColumn("year", year(col(dateColumn)))
.withColumn("month", month(col(dateColumn)))
.withColumn("day", dayofmonth(col(dateColumn)))
}
val timePartitionedDF = createTimePartitions(salesDF, "order_date")
timePartitionedDF.write
.mode("overwrite")
.partitionBy("year", "month")
.parquet("partitioned_sales")
// 2. 哈希分区策略
def createHashPartitions(df: DataFrame, columns: Seq[String], numPartitions: Int): DataFrame = {
val hashColumn = columns.map(col).reduce((c1, c2) =>
hash(c1, c2)
) % numPartitions
df.withColumn("hash_partition", when(hashColumn < 0, hashColumn + numPartitions).otherwise(hashColumn))
}
// 3. 动态分区裁剪
spark.conf.set("spark.sql.optimizer.dynamicPartitionPruning.enabled", "true")
val efficientJoin = spark.sql("""
SELECT s.*, c.customer_name
FROM partitioned_sales s
JOIN customers c ON s.customer_id = c.id
WHERE c.registration_date > '2023-01-01'
""")
// 4. 分区数量控制
def controlPartitionCount(df: DataFrame, maxPartitions: Int = 200): DataFrame = {
val currentPartitions = df.rdd.getNumPartitions
if (currentPartitions > maxPartitions) {
df.coalesce(maxPartitions)
} else {
df
}
}
读写性能调优
// 1. 并行度优化
val optimizedRead = spark.read
.option("multiline", "false")
.option("maxCharsPerColumn", "1000000")
.option("maxColumns", "20000")
.option("mode", "FAILFAST") // PERMISSIVE, DROPMALFORMED, FAILFAST
.option("columnNameOfCorruptRecord", "_corrupt_record")
.csv("large_dataset.csv")
// 2. 预读优化
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
// 3. 缓存策略
val frequentlyAccessedDF = expensiveDF.cache()
val result1 = frequentlyAccessedDF.count()
val result2 = frequentlyAccessedDF.groupBy("category").count().collect()
// 使用完后清理缓存
frequentlyAccessedDF.unpersist()
// 4. 写入优化
def optimizedWrite(df: DataFrame, outputPath: String, format: String): Unit = {
val optimizedDF = df
.repartition(spark.conf.get("spark.sql.shuffle.partitions").toInt / 4)
.sortWithinPartitions("sort_column") // 如果有排序需求
optimizedDF.write
.mode("overwrite")
.option("compression", FileFormatOptimization.chooseCompression("balanced"))
.format(format)
.save(outputPath)
}
// 5. 内存管理优化
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "134217728") // 128MB
spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionNum", "1")
spark.conf.set("spark.sql.adaptive.coalescePartitions.parallelismFirst", "true")
实战案例
ETL管道构建
// 构建完整的ETL管道
class DataPipeline(spark: SparkSession) {
// 1. 数据提取阶段
def extractData(): Map[String, DataFrame] = {
Map(
"sales" -> spark.read
.option("header", "true")
.option("inferSchema", "true")
.csv("raw_data/sales/*.csv"),
"customers" -> spark.read
.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306/crm")
.option("dbtable", "customers")
.option("user", "reader")
.option("password", "password")
.load(),
"products" -> spark.read
.json("raw_data/products.json")
)
}
// 2. 数据转换阶段
def transformData(rawData: Map[String, DataFrame]): DataFrame = {
val salesDF = rawData("sales")
val customersDF = rawData("customers")
val productsDF = rawData("products")
// 数据清洗
val cleanedSalesDF = salesDF
.filter(col("amount") > 0)
.filter(col("quantity") > 0)
.withColumn("sale_date", to_date(col("sale_date"), "yyyy-MM-dd"))
.dropDuplicates("order_id")
val cleanedCustomersDF = customersDF
.filter(col("email").rlike("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"))
.withColumn("full_name", concat_ws(" ", col("first_name"), col("last_name")))
// 数据关联
val enrichedSalesDF = cleanedSalesDF
.join(cleanedCustomersDF, "customer_id")
.join(productsDF, "product_id")
.select(
col("order_id"),
col("customer_id"),
col("full_name").alias("customer_name"),
col("product_id"),
col("product_name"),
col("category"),
col("amount"),
col("quantity"),
col("sale_date"),
(col("amount") * col("quantity")).alias("total_revenue")
)
// 聚合计算
enrichedSalesDF
.withColumn("year", year(col("sale_date")))
.withColumn("month", month(col("sale_date")))
}
// 3. 数据加载阶段
def loadData(transformedDF: DataFrame): Unit = {
// 写入数据仓库
transformedDF.write
.mode("overwrite")
.partitionBy("year", "month")
.option("compression", "snappy")
.parquet("warehouse/sales_fact")
// 写入分析数据库
val aggregatedDF = transformedDF
.groupBy("year", "month", "category")
.agg(
sum("total_revenue").alias("monthly_revenue"),
count("*").alias("order_count"),
countDistinct("customer_id").alias("unique_customers")
)
aggregatedDF.write
.mode("overwrite")
.format("jdbc")
.option("url", "jdbc:postgresql://localhost:5432/analytics")
.option("dbtable", "monthly_sales_summary")
.option("user", "writer")
.option("password", "password")
.save()
}
// 4. 运行完整管道
def runPipeline(): Unit = {
try {
val rawData = extractData()
val transformedData = transformData(rawData)
loadData(transformedData)
println("ETL pipeline completed successfully")
} catch {
case e: Exception =>
println(s"ETL pipeline failed: ${e.getMessage}")
throw e
}
}
}
// 使用管道
val pipeline = new DataPipeline(spark)
pipeline.runPipeline()
最佳实践总结
数据读写清单
object DataIOBestPractices {
// 1. 文件格式选择准则
val formatGuidelines = Map(
"large_analytical_datasets" -> "parquet",
"streaming_data" -> "avro",
"transactional_data" -> "delta",
"archival_storage" -> "parquet_with_high_compression",
"data_exchange" -> "json_or_csv"
)
// 2. 性能优化检查清单
def performanceChecklist(): List[String] = List(
"✓ 选择合适的文件格式",
"✓ 使用适当的压缩算法",
"✓ 控制文件大小(100MB-1GB)",
"✓ 合理设计分区策略",
"✓ 启用列剪枝和谓词下推",
"✓ 配置适当的并行度",
"✓ 使用缓存策略",
"✓ 监控资源使用情况"
)
// 3. 错误处理模式
def handleDataQualityIssues(df: DataFrame): DataFrame = {
df.filter(col("_corrupt_record").isNull) // 过滤损坏记录
.drop("_corrupt_record") // 删除错误列
.dropDuplicates() // 去重
.na.fill(Map( // 填充空值
"string_column" -> "unknown",
"numeric_column" -> 0
))
}
}
相关链接
- Spark SQL
- Spark性能调优
- Hadoop/HDFS分布式文件系统
- Kafka/Kafka连接器
tip 学习建议
数据读写是Spark应用的基础,建议根据实际数据特点选择合适的文件格式和存储策略,重点掌握性能优化技巧。