未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适合具备Spark、Scala及数仓基础的读者进行电商ODS/DWD/DWS/ADS分层设计和清洗、转换、聚合、调度、监控的模块练习,不是可直接部署的完整工程。正文尚缺调度引用的Python作业、Scala应用入口、构建依赖与产物、演示数据及分区读写串联实现;日汇总中的窗口表达式嵌入聚合、提交参数组织及Scala源码提交方式仍存在执行障碍。行为与订单关联可能放大行数,重算会话未被后续指标采用,商品和类目订单数、转化率及地域关联仍有口径或实现问题,结果不宜直接用于业务报表。记录验证为占位实现,质量评分和schema检查尚不可靠;资源与缓存策略仅供设计讨论,实际使用需明确Spark、Scala、Airflow及Hive执行环境,并补齐幂等写入、失败回滚和补数恢复。本次仅依据全文作结构审阅,未进行外部事实核验或编译、运行验证。

批处理项目实战

概述

本文档通过电商批处理项目的架构和模块实现示例,展示Spark在企业级数据处理中的应用。示例围绕电商数据平台的ETL系统,讨论数据采集、清洗、转换和存储流程,并提供调度、监控与资源配置的设计参考;当前未提供完整工程入口和读写实现,不能直接作为可部署项目或通用生产最佳实践。

info 项目背景

某电商公司需要构建统一的数据平台,整合来自用户行为日志、订单系统、商品库存等多个数据源的数据,为数据分析和机器学习提供高质量的数据服务。

项目架构设计

整体架构

%%{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 "数据源层"
        A[用户行为日志] --> D[Kafka]
        B[订单系统数据库] --> E[MySQL]
        C[商品信息系统] --> F[PostgreSQL]
    end
    
    subgraph "数据采集层"
        D --> G[Flume/Canal]
        E --> H[DataX/Sqoop]
        F --> I[JDBC连接器]
    end
    
    subgraph "数据存储层"
        G --> J[HDFS原始数据]
        H --> J
        I --> J
    end
    
    subgraph "数据处理层"
        J --> K[Spark批处理]
        K --> L[数据清洗]
        L --> M[数据转换]
        M --> N[数据聚合]
    end
    
    subgraph "数据服务层"
        N --> O[Hive数仓]
        N --> P[HBase实时查询]
        N --> Q[ElasticSearch搜索]
    end
    
    subgraph "应用层"
        O --> R[BI报表]
        P --> S[实时推荐]
        Q --> T[用户画像]
    end
    class A,D,B,E,C,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T default

技术栈选型

object TechnologyStack {
  
  case class TechStack(
    dataIngestion: List[String],
    dataStorage: List[String],
    dataProcessing: List[String],
    dataServing: List[String],
    monitoring: List[String]
  )
  
  val projectTechStack = TechStack(
    dataIngestion = List("Kafka", "Flume", "DataX", "Canal"),
    dataStorage = List("HDFS", "Hive", "HBase", "ElasticSearch"),
    dataProcessing = List("Spark", "Scala", "Spark SQL"),
    dataServing = List("Presto", "Kylin", "Druid"),
    monitoring = List("Prometheus", "Grafana", "AlertManager")
  )
  
  // 配置参数
  val sparkConfig = Map(
    "spark.app.name" -> "EcommerceETL",
    "spark.master" -> "yarn",
    "spark.submit.deployMode" -> "cluster",
    
    // 资源配置
    "spark.executor.instances" -> "20",
    "spark.executor.cores" -> "4",
    "spark.executor.memory" -> "8g",
    "spark.driver.memory" -> "4g",
    "spark.driver.cores" -> "2",
    
    // 性能优化
    "spark.serializer" -> "org.apache.spark.serializer.KryoSerializer",
    "spark.sql.adaptive.enabled" -> "true",
    "spark.sql.adaptive.coalescePartitions.enabled" -> "true",
    "spark.sql.adaptive.skewJoin.enabled" -> "true",
    
    // 存储配置
    "spark.sql.parquet.compression.codec" -> "snappy",
    "spark.sql.shuffle.partitions" -> "400",
    
    // Hive集成
    "spark.sql.warehouse.dir" -> "hdfs://namenode:8020/user/hive/warehouse",
    "spark.sql.catalogImplementation" -> "hive"
  )
}

数据模型设计

数据分层架构

object DataModelDesign {
  
  // 原始数据层 (ODS - Operational Data Store)
  case class UserBehaviorLog(
    userId: String,
    sessionId: String,
    timestamp: Long,
    eventType: String,
    pageUrl: String,
    productId: Option[String],
    categoryId: Option[String],
    userAgent: String,
    ip: String
  )
  
  case class OrderRecord(
    orderId: String,
    userId: String,
    orderTime: java.sql.Timestamp,
    totalAmount: BigDecimal,
    orderStatus: String,
    paymentMethod: String,
    shippingAddress: String,
    items: List[OrderItem]
  )
  
  case class OrderItem(
    productId: String,
    productName: String,
    quantity: Int,
    price: BigDecimal,
    categoryId: String
  )
  
  case class ProductInfo(
    productId: String,
    productName: String,
    categoryId: String,
    brandId: String,
    price: BigDecimal,
    stockQuantity: Int,
    createTime: java.sql.Timestamp,
    updateTime: java.sql.Timestamp
  )
  
  // 清洗数据层 (DWD - Data Warehouse Detail)
  case class CleanedUserBehavior(
    userId: String,
    sessionId: String,
    eventTime: java.sql.Timestamp,
    eventType: String,
    pageUrl: String,
    productId: Option[String],
    categoryId: Option[String],
    deviceType: String,
    browserType: String,
    location: String,
    isValid: Boolean
  )
  
  case class CleanedOrderData(
    orderId: String,
    userId: String,
    orderDate: String,
    orderTime: java.sql.Timestamp,
    totalAmount: BigDecimal,
    orderStatus: String,
    paymentMethod: String,
    region: String,
    city: String,
    items: List[OrderItem],
    isValid: Boolean
  )
  
  // 聚合数据层 (DWS - Data Warehouse Summary)
  case class UserDailySummary(
    userId: String,
    date: String,
    pageViews: Long,
    sessionCount: Long,
    sessionDuration: Long,
    orderCount: Long,
    orderAmount: BigDecimal,
    distinctProductViews: Long,
    distinctCategoryViews: Long
  )
  
  case class ProductDailySummary(
    productId: String,
    date: String,
    viewCount: Long,
    uniqueViewers: Long,
    orderCount: Long,
    orderQuantity: Long,
    revenue: BigDecimal,
    conversionRate: Double
  )
  
  // 应用数据层 (ADS - Application Data Service)
  case class UserProfile(
    userId: String,
    registerDate: String,
    totalOrders: Long,
    totalAmount: BigDecimal,
    avgOrderValue: BigDecimal,
    favoriteCategories: List[String],
    lastActiveDate: String,
    userLevel: String,
    tags: List[String]
  )
}

表结构设计

-- 原始数据表结构
CREATE TABLE IF NOT EXISTS ods.user_behavior_log (
    user_id STRING,
    session_id STRING,
    timestamp BIGINT,
    event_type STRING,
    page_url STRING,
    product_id STRING,
    category_id STRING,
    user_agent STRING,
    ip STRING,
    dt STRING
) PARTITIONED BY (dt)
STORED AS PARQUET
LOCATION 'hdfs://data/ods/user_behavior_log';

CREATE TABLE IF NOT EXISTS ods.order_records (
    order_id STRING,
    user_id STRING,
    order_time TIMESTAMP,
    total_amount DECIMAL(10,2),
    order_status STRING,
    payment_method STRING,
    shipping_address STRING,
    items ARRAY<STRUCT<
        product_id: STRING,
        product_name: STRING,
        quantity: INT,
        price: DECIMAL(10,2),
        category_id: STRING
    >>,
    dt STRING
) PARTITIONED BY (dt)
STORED AS PARQUET
LOCATION 'hdfs://data/ods/order_records';

-- 清洗数据表结构
CREATE TABLE IF NOT EXISTS dwd.cleaned_user_behavior (
    user_id STRING,
    session_id STRING,
    event_time TIMESTAMP,
    event_type STRING,
    page_url STRING,
    product_id STRING,
    category_id STRING,
    device_type STRING,
    browser_type STRING,
    location STRING,
    is_valid BOOLEAN,
    dt STRING
) PARTITIONED BY (dt)
STORED AS PARQUET
LOCATION 'hdfs://data/dwd/cleaned_user_behavior';

-- 聚合数据表结构
CREATE TABLE IF NOT EXISTS dws.user_daily_summary (
    user_id STRING,
    date STRING,
    page_views BIGINT,
    session_count BIGINT,
    session_duration BIGINT,
    order_count BIGINT,
    order_amount DECIMAL(10,2),
    distinct_product_views BIGINT,
    distinct_category_views BIGINT
) PARTITIONED BY (date)
STORED AS PARQUET
LOCATION 'hdfs://data/dws/user_daily_summary';

核心ETL流程实现

数据清洗模块

object DataCleaningModule {
  
  import org.apache.spark.sql.{SparkSession, DataFrame}
  import org.apache.spark.sql.functions._
  import org.apache.spark.sql.types._
  
  class DataCleaner(spark: SparkSession) {
    import spark.implicits._
    
    def cleanUserBehaviorData(rawData: DataFrame): DataFrame = {
      rawData
        .filter(col("user_id").isNotNull && col("user_id") =!= "")
        .filter(col("timestamp").isNotNull)
        .withColumn("event_time", from_unixtime(col("timestamp") / 1000).cast(TimestampType))
        .withColumn("device_type", extractDeviceType(col("user_agent")))
        .withColumn("browser_type", extractBrowserType(col("user_agent")))
        .withColumn("location", extractLocation(col("ip")))
        .withColumn("is_valid", validateBehaviorRecord())
        .filter(col("is_valid") === true)
    }
    
    def cleanOrderData(rawData: DataFrame): DataFrame = {
      rawData
        .filter(col("order_id").isNotNull && col("order_id") =!= "")
        .filter(col("user_id").isNotNull && col("user_id") =!= "")
        .filter(col("total_amount") > 0)
        .withColumn("order_date", date_format(col("order_time"), "yyyy-MM-dd"))
        .withColumn("region", extractRegion(col("shipping_address")))
        .withColumn("city", extractCity(col("shipping_address")))
        .withColumn("is_valid", validateOrderRecord())
        .filter(col("is_valid") === true)
    }
    
    // 自定义UDF函数
    private def extractDeviceType = udf { (userAgent: String) =>
      if (userAgent == null) "unknown"
      else if (userAgent.toLowerCase.contains("mobile")) "mobile"
      else if (userAgent.toLowerCase.contains("tablet")) "tablet"
      else "desktop"
    }
    
    private def extractBrowserType = udf { (userAgent: String) =>
      if (userAgent == null) "unknown"
      else if (userAgent.contains("Chrome")) "Chrome"
      else if (userAgent.contains("Firefox")) "Firefox"
      else if (userAgent.contains("Safari")) "Safari"
      else "other"
    }
    
    private def extractLocation = udf { (ip: String) =>
      // 这里应该接入IP地理位置服务
      // 简化实现,实际生产环境需要接入如GeoIP等服务
      if (ip == null) "unknown" else "China"
    }
    
    private def extractRegion = udf { (address: String) =>
      if (address == null) "unknown"
      else {
        // 简化的地址解析逻辑
        val provinces = List("北京", "上海", "广东", "浙江", "江苏")
        provinces.find(address.contains(_)).getOrElse("其他")
      }
    }
    
    private def extractCity = udf { (address: String) =>
      if (address == null) "unknown"
      else {
        // 简化的城市解析逻辑
        val cities = List("北京", "上海", "深圳", "杭州", "南京")
        cities.find(address.contains(_)).getOrElse("其他")
      }
    }
    
    private def validateBehaviorRecord = udf { () =>
      // 数据质量验证逻辑
      true // 简化实现
    }
    
    private def validateOrderRecord = udf { () =>
      // 订单数据质量验证逻辑
      true // 简化实现
    }
  }
  
  // 数据质量监控
  class DataQualityMonitor(spark: SparkSession) {
    
    def generateQualityReport(originalData: DataFrame, cleanedData: DataFrame): DataQualityReport = {
      val originalCount = originalData.count()
      val cleanedCount = cleanedData.count()
      val droppedCount = originalCount - cleanedCount
      val qualityScore = (cleanedCount.toDouble / originalCount * 100).toInt
      
      // 检查空值比例
      val nullColumns = originalData.columns.map { colName =>
        val nullCount = originalData.filter(col(colName).isNull).count()
        (colName, nullCount.toDouble / originalCount * 100)
      }.filter(_._2 > 5).toMap
      
      // 检查重复记录
      val duplicateCount = originalCount - originalData.dropDuplicates().count()
      
      DataQualityReport(
        originalCount = originalCount,
        cleanedCount = cleanedCount,
        droppedCount = droppedCount,
        qualityScore = qualityScore,
        nullColumns = nullColumns,
        duplicateCount = duplicateCount,
        timestamp = System.currentTimeMillis()
      )
    }
    
    case class DataQualityReport(
      originalCount: Long,
      cleanedCount: Long,
      droppedCount: Long,
      qualityScore: Int,
      nullColumns: Map[String, Double],
      duplicateCount: Long,
      timestamp: Long
    )
  }
}

数据转换模块

object DataTransformationModule {
  
  import org.apache.spark.sql.{SparkSession, DataFrame}
  import org.apache.spark.sql.functions._
  import org.apache.spark.sql.expressions.Window
  
  class DataTransformer(spark: SparkSession) {
    import spark.implicits._
    
    def transformUserBehavior(cleanedBehavior: DataFrame, 
                             orderData: DataFrame): DataFrame = {
      // 计算用户会话
      val sessionWindow = Window.partitionBy("user_id")
        .orderBy("event_time")
        .rangeBetween(-1800, 0) // 30分钟会话窗口
      
      val withSessions = cleanedBehavior
        .withColumn("session_start", 
          when(
            lag("event_time", 1).over(Window.partitionBy("user_id").orderBy("event_time"))
              .isNull || 
            unix_timestamp(col("event_time")) - 
            unix_timestamp(lag("event_time", 1).over(Window.partitionBy("user_id").orderBy("event_time"))) > 1800,
            true
          ).otherwise(false)
        )
        .withColumn("session_id_new", 
          sum(when(col("session_start"), 1).otherwise(0))
            .over(Window.partitionBy("user_id").orderBy("event_time"))
        )
      
      // 丰富用户行为数据
      val enrichedBehavior = withSessions
        .join(orderData.select("user_id", "order_time", "total_amount"), "user_id", "left")
        .withColumn("is_buyer", col("total_amount").isNotNull)
        .withColumn("user_value_segment", 
          when(col("total_amount") > 1000, "high_value")
          .when(col("total_amount") > 500, "medium_value")
          .when(col("total_amount") > 0, "low_value")
          .otherwise("non_buyer")
        )
      
      enrichedBehavior
    }
    
    def createUserDailySummary(behaviorData: DataFrame, 
                              orderData: DataFrame): DataFrame = {
      // 用户行为汇总
      val behaviorSummary = behaviorData
        .withColumn("date", date_format(col("event_time"), "yyyy-MM-dd"))
        .groupBy("user_id", "date")
        .agg(
          count("*").as("page_views"),
          countDistinct("session_id").as("session_count"),
          sum(
            coalesce(
              unix_timestamp(lead("event_time", 1).over(
                Window.partitionBy("user_id", "session_id").orderBy("event_time")
              )) - unix_timestamp(col("event_time")),
              lit(0)
            )
          ).as("session_duration"),
          countDistinct("product_id").as("distinct_product_views"),
          countDistinct("category_id").as("distinct_category_views")
        )
      
      // 用户订单汇总
      val orderSummary = orderData
        .withColumn("date", date_format(col("order_time"), "yyyy-MM-dd"))
        .groupBy("user_id", "date")
        .agg(
          count("*").as("order_count"),
          sum("total_amount").as("order_amount")
        )
      
      // 合并汇总数据
      behaviorSummary
        .join(orderSummary, Seq("user_id", "date"), "left")
        .na.fill(0, Seq("order_count", "order_amount"))
    }
    
    def createProductDailySummary(behaviorData: DataFrame,
                                 orderData: DataFrame): DataFrame = {
      // 商品浏览汇总
      val viewSummary = behaviorData
        .filter(col("product_id").isNotNull)
        .withColumn("date", date_format(col("event_time"), "yyyy-MM-dd"))
        .groupBy("product_id", "date")
        .agg(
          count("*").as("view_count"),
          countDistinct("user_id").as("unique_viewers")
        )
      
      // 商品销售汇总
      val salesSummary = orderData
        .select("order_time", explode(col("items")).as("item"))
        .select(
          col("item.product_id").as("product_id"),
          date_format(col("order_time"), "yyyy-MM-dd").as("date"),
          col("item.quantity").as("quantity"),
          col("item.price").as("price")
        )
        .groupBy("product_id", "date")
        .agg(
          count("*").as("order_count"),
          sum("quantity").as("order_quantity"),
          sum(col("quantity") * col("price")).as("revenue")
        )
      
      // 计算转化率
      viewSummary
        .join(salesSummary, Seq("product_id", "date"), "left")
        .na.fill(0, Seq("order_count", "order_quantity", "revenue"))
        .withColumn("conversion_rate", 
          when(col("unique_viewers") > 0, 
            col("order_count").cast("double") / col("unique_viewers")
          ).otherwise(0.0)
        )
    }
    
    def createUserProfile(userSummary: DataFrame,
                         orderData: DataFrame,
                         behaviorData: DataFrame): DataFrame = {
      // 用户整体统计
      val userStats = userSummary
        .groupBy("user_id")
        .agg(
          min("date").as("first_active_date"),
          max("date").as("last_active_date"),
          sum("page_views").as("total_page_views"),
          sum("session_count").as("total_sessions"),
          sum("order_count").as("total_orders"),
          sum("order_amount").as("total_amount"),
          when(sum("order_count") > 0,
            sum("order_amount") / sum("order_count")
          ).otherwise(lit(null)).as("avg_order_value")
        )
      
      // 用户偏好分析
      val userPreferences = behaviorData
        .filter(col("category_id").isNotNull)
        .groupBy("user_id", "category_id")
        .count()
        .withColumn("rank", 
          row_number().over(
            Window.partitionBy("user_id").orderBy(desc("count"))
          )
        )
        .filter(col("rank") <= 3)
        .groupBy("user_id")
        .agg(collect_list("category_id").as("favorite_categories"))
      
      // 用户分级
      userStats
        .join(userPreferences, "user_id", "left")
        .withColumn("user_level",
          when(col("total_amount") > 5000, "VIP")
          .when(col("total_amount") > 2000, "Gold")
          .when(col("total_amount") > 500, "Silver")
          .otherwise("Bronze")
        )
        .withColumn("tags", 
          array(
            when(col("total_orders") > 20, lit("frequent_buyer")).otherwise(lit(null)),
            when(col("avg_order_value") > 200, lit("high_value")).otherwise(lit(null)),
            when(datediff(current_date(), col("last_active_date")) <= 7, lit("active")).otherwise(lit("inactive"))
          ).cast("array<string>")
        )
    }
  }
}

数据聚合模块

object DataAggregationModule {
  
  import org.apache.spark.sql.{SparkSession, DataFrame}
  import org.apache.spark.sql.functions._
  import org.apache.spark.sql.expressions.Window
  
  class DataAggregator(spark: SparkSession) {
    import spark.implicits._
    
    def createHourlyMetrics(behaviorData: DataFrame): DataFrame = {
      behaviorData
        .withColumn("hour", date_format(col("event_time"), "yyyy-MM-dd HH"))
        .groupBy("hour")
        .agg(
          count("*").as("total_events"),
          countDistinct("user_id").as("active_users"),
          countDistinct("session_id").as("active_sessions"),
          countDistinct("product_id").as("viewed_products")
        )
        .orderBy("hour")
    }
    
    def createCategoryMetrics(behaviorData: DataFrame, 
                            orderData: DataFrame): DataFrame = {
      // 类目浏览指标
      val categoryViews = behaviorData
        .filter(col("category_id").isNotNull)
        .withColumn("date", date_format(col("event_time"), "yyyy-MM-dd"))
        .groupBy("category_id", "date")
        .agg(
          count("*").as("view_count"),
          countDistinct("user_id").as("unique_viewers")
        )
      
      // 类目销售指标
      val categorySales = orderData
        .select("order_time", explode(col("items")).as("item"))
        .select(
          col("item.category_id").as("category_id"),
          date_format(col("order_time"), "yyyy-MM-dd").as("date"),
          col("item.quantity").as("quantity"),
          (col("item.quantity") * col("item.price")).as("amount")
        )
        .groupBy("category_id", "date")
        .agg(
          count("*").as("order_count"),
          sum("quantity").as("sales_quantity"),
          sum("amount").as("sales_amount")
        )
      
      categoryViews
        .join(categorySales, Seq("category_id", "date"), "left")
        .na.fill(0, Seq("order_count", "sales_quantity", "sales_amount"))
    }
    
    def createGeographicMetrics(behaviorData: DataFrame,
                              orderData: DataFrame): DataFrame = {
      // 地域用户活跃度
      val geoActivity = behaviorData
        .withColumn("date", date_format(col("event_time"), "yyyy-MM-dd"))
        .groupBy("location", "date")
        .agg(
          countDistinct("user_id").as("active_users"),
          count("*").as("total_events")
        )
      
      // 地域销售指标
      val geoSales = orderData
        .withColumn("date", date_format(col("order_time"), "yyyy-MM-dd"))
        .groupBy("region", "city", "date")
        .agg(
          count("*").as("order_count"),
          sum("total_amount").as("sales_amount"),
          countDistinct("user_id").as("buyers")
        )
      
      geoActivity
        .join(geoSales, 
          col("location") === concat(col("region"), lit("-"), col("city")) && 
          geoActivity.col("date") === geoSales.col("date"), 
          "left")
        .na.fill(0, Seq("order_count", "sales_amount", "buyers"))
    }
    
    def createCohortAnalysis(orderData: DataFrame): DataFrame = {
      // 计算用户首次购买时间
      val userFirstOrder = orderData
        .groupBy("user_id")
        .agg(min("order_time").as("first_order_time"))
        .withColumn("cohort_month", 
          date_format(col("first_order_time"), "yyyy-MM"))
      
      // 计算每月活跃用户
      val monthlyActivity = orderData
        .withColumn("order_month", date_format(col("order_time"), "yyyy-MM"))
        .join(userFirstOrder, "user_id")
        .withColumn("months_since_first", 
          months_between(
            to_date(col("order_month"), "yyyy-MM"),
            to_date(col("cohort_month"), "yyyy-MM")
          )
        )
        .groupBy("cohort_month", "months_since_first")
        .agg(
          countDistinct("user_id").as("active_users"),
          sum("total_amount").as("revenue")
        )
      
      // 计算留存率
      val cohortSize = userFirstOrder
        .groupBy("cohort_month")
        .count()
        .withColumnRenamed("count", "cohort_size")
      
      monthlyActivity
        .join(cohortSize, "cohort_month")
        .withColumn("retention_rate", 
          col("active_users").cast("double") / col("cohort_size")
        )
        .orderBy("cohort_month", "months_since_first")
    }
    
    def createRFMAnalysis(orderData: DataFrame): DataFrame = {
      val analysisDate = current_date()
      
      orderData
        .groupBy("user_id")
        .agg(
          // Recency: 距离最近购买的天数
          datediff(analysisDate, max("order_time")).as("recency"),
          // Frequency: 购买频次
          count("*").as("frequency"),
          // Monetary: 总消费金额
          sum("total_amount").as("monetary")
        )
        .withColumn("r_score", 
          when(col("recency") <= 30, 5)
          .when(col("recency") <= 90, 4)
          .when(col("recency") <= 180, 3)
          .when(col("recency") <= 365, 2)
          .otherwise(1)
        )
        .withColumn("f_score",
          when(col("frequency") >= 20, 5)
          .when(col("frequency") >= 10, 4)
          .when(col("frequency") >= 5, 3)
          .when(col("frequency") >= 2, 2)
          .otherwise(1)
        )
        .withColumn("m_score",
          when(col("monetary") >= 5000, 5)
          .when(col("monetary") >= 2000, 4)
          .when(col("monetary") >= 1000, 3)
          .when(col("monetary") >= 500, 2)
          .otherwise(1)
        )
        .withColumn("rfm_score", 
          concat(col("r_score"), col("f_score"), col("m_score")))
        .withColumn("customer_segment",
          when(col("rfm_score").like("5%5") || col("rfm_score").like("4%5"), "Champions")
          .when(col("rfm_score").like("5%4") || col("rfm_score").like("4%4"), "Loyal Customers")
          .when(col("rfm_score").like("5%2") || col("rfm_score").like("5%3"), "Potential Loyalists")
          .when(col("rfm_score").like("5%1"), "New Customers")
          .when(col("rfm_score").like("4%1") || col("rfm_score").like("3%1"), "Promising")
          .when(col("rfm_score").like("3%3") || col("rfm_score").like("3%4"), "Need Attention")
          .when(col("rfm_score").like("2%3") || col("rfm_score").like("2%4"), "About to Sleep")
          .when(col("rfm_score").like("2%2"), "At Risk")
          .when(col("rfm_score").like("1%5") || col("rfm_score").like("1%4"), "Cannot Lose Them")
          .when(col("rfm_score").like("1%3"), "Hibernating")
          .otherwise("Lost")
        )
    }
  }
}

任务调度与执行

Airflow调度配置

from airflow import DAG
from airflow.providers.apache.spark.operators.spark_submit import SparkSubmitOperator
from airflow.operators.bash_operator import BashOperator
from airflow.operators.email_operator import EmailOperator
from datetime import datetime, timedelta

# 默认参数
default_args = {
    'owner': 'data-team',
    'depends_on_past': False,
    'start_date': datetime(2024, 1, 1),
    'email_on_failure': True,
    'email_on_retry': False,
    'retries': 2,
    'retry_delay': timedelta(minutes=5),
    'email': ['data-team@company.com']
}

# 创建DAG
dag = DAG(
    'ecommerce_etl_daily',
    default_args=default_args,
    description='电商数据ETL日处理任务',
    schedule_interval='0 2 * * *',  # 每天凌晨2点执行
    max_active_runs=1,
    catchup=False
)

# 数据验证任务
data_validation = BashOperator(
    task_id='validate_source_data',
    bash_command="""
    #!/bin/bash
    DATE={{ ds }}
    
    # 检查HDFS路径是否存在
    hadoop fs -test -e /data/ods/user_behavior_log/dt=${DATE}
    if [ $? -ne 0 ]; then
        echo "用户行为日志数据不存在: ${DATE}"
        exit 1
    fi
    
    hadoop fs -test -e /data/ods/order_records/dt=${DATE}
    if [ $? -ne 0 ]; then
        echo "订单数据不存在: ${DATE}"
        exit 1
    fi
    
    echo "数据验证通过"
    """,
    dag=dag
)

# 数据清洗任务
data_cleaning = SparkSubmitOperator(
    task_id='data_cleaning',
    application='/opt/spark/jobs/DataCleaningJob.py',
    name='data-cleaning-{{ ds }}',
    conn_id='spark_default',
    application_args=[
        '--input-date', '{{ ds }}',
        '--input-path', '/data/ods',
        '--output-path', '/data/dwd'
    ],
    conf={
        'spark.executor.instances': '10',
        'spark.executor.cores': '4',
        'spark.executor.memory': '8g',
        'spark.driver.memory': '4g'
    },
    dag=dag
)

# 数据转换任务
data_transformation = SparkSubmitOperator(
    task_id='data_transformation',
    application='/opt/spark/jobs/DataTransformationJob.py',
    name='data-transformation-{{ ds }}',
    conn_id='spark_default',
    application_args=[
        '--input-date', '{{ ds }}',
        '--input-path', '/data/dwd',
        '--output-path', '/data/dws'
    ],
    conf={
        'spark.executor.instances': '15',
        'spark.executor.cores': '4',
        'spark.executor.memory': '8g'
    },
    dag=dag
)

# 数据聚合任务
data_aggregation = SparkSubmitOperator(
    task_id='data_aggregation',
    application='/opt/spark/jobs/DataAggregationJob.py',
    name='data-aggregation-{{ ds }}',
    conn_id='spark_default',
    application_args=[
        '--input-date', '{{ ds }}',
        '--input-path', '/data/dws',
        '--output-path', '/data/ads'
    ],
    conf={
        'spark.executor.instances': '8',
        'spark.executor.cores': '4',
        'spark.executor.memory': '6g'
    },
    dag=dag
)

# 数据质量检查
data_quality_check = BashOperator(
    task_id='data_quality_check',
    bash_command="""
    #!/bin/bash
    DATE={{ ds }}
    
    # 检查处理结果记录数
    CLEANED_COUNT=$(spark-sql -e "SELECT COUNT(*) FROM dwd.cleaned_user_behavior WHERE dt='${DATE}'" 2>/dev/null | tail -1)
    AGGREGATED_COUNT=$(spark-sql -e "SELECT COUNT(*) FROM dws.user_daily_summary WHERE date='${DATE}'" 2>/dev/null | tail -1)
    
    if [ "$CLEANED_COUNT" -lt 1000 ]; then
        echo "清洗后数据量过少: $CLEANED_COUNT"
        exit 1
    fi
    
    if [ "$AGGREGATED_COUNT" -lt 100 ]; then
        echo "聚合后数据量过少: $AGGREGATED_COUNT"
        exit 1
    fi
    
    echo "数据质量检查通过"
    """,
    dag=dag
)

# 成功通知
success_notification = EmailOperator(
    task_id='success_notification',
    to=['data-team@company.com'],
    subject='ETL任务执行成功 - {{ ds }}',
    html_content="""
    <h3>ETL任务执行成功</h3>
    <p>日期: {{ ds }}</p>
    <p>任务完成时间: {{ ts }}</p>
    <p>请查看数据质量报告</p>
    """,
    dag=dag
)

# 设置任务依赖关系
data_validation >> data_cleaning >> data_transformation >> data_aggregation >> data_quality_check >> success_notification

Spark作业提交脚本

#!/bin/bash

# ETL作业提交脚本
# submit_etl_job.sh

set -e

# 配置参数
APP_NAME="EcommerceETL"
MASTER="yarn"
DEPLOY_MODE="cluster"
QUEUE="default"

# 资源配置
EXECUTOR_INSTANCES=20
EXECUTOR_CORES=4
EXECUTOR_MEMORY="8g"
DRIVER_MEMORY="4g"
DRIVER_CORES=2

# Spark配置
SPARK_CONF=(
    "--conf spark.serializer=org.apache.spark.serializer.KryoSerializer"
    "--conf spark.sql.adaptive.enabled=true"
    "--conf spark.sql.adaptive.coalescePartitions.enabled=true"
    "--conf spark.sql.adaptive.skewJoin.enabled=true"
    "--conf spark.sql.parquet.compression.codec=snappy"
    "--conf spark.sql.shuffle.partitions=400"
    "--conf spark.dynamicAllocation.enabled=true"
    "--conf spark.dynamicAllocation.minExecutors=5"
    "--conf spark.dynamicAllocation.maxExecutors=50"
    "--conf spark.sql.warehouse.dir=hdfs://namenode:8020/user/hive/warehouse"
    "--conf spark.sql.catalogImplementation=hive"
)

# 解析命令行参数
while [[ $# -gt 0 ]]; do
    case $1 in
        --date)
            PROCESS_DATE="$2"
            shift 2
            ;;
        --job-type)
            JOB_TYPE="$2"
            shift 2
            ;;
        --input-path)
            INPUT_PATH="$2"
            shift 2
            ;;
        --output-path)
            OUTPUT_PATH="$2"
            shift 2
            ;;
        *)
            echo "未知参数: $1"
            exit 1
            ;;
    esac
done

# 验证必需参数
if [ -z "$PROCESS_DATE" ] || [ -z "$JOB_TYPE" ]; then
    echo "用法: $0 --date YYYY-MM-DD --job-type [cleaning|transformation|aggregation]"
    exit 1
fi

# 根据作业类型设置应用程序
case $JOB_TYPE in
    "cleaning")
        APPLICATION="/opt/spark/jobs/DataCleaningJob.scala"
        CLASS="com.ecommerce.etl.DataCleaningJob"
        ;;
    "transformation")
        APPLICATION="/opt/spark/jobs/DataTransformationJob.scala"
        CLASS="com.ecommerce.etl.DataTransformationJob"
        ;;
    "aggregation")
        APPLICATION="/opt/spark/jobs/DataAggregationJob.scala"
        CLASS="com.ecommerce.etl.DataAggregationJob"
        ;;
    *)
        echo "不支持的作业类型: $JOB_TYPE"
        exit 1
        ;;
esac

echo "提交Spark作业..."
echo "作业类型: $JOB_TYPE"
echo "处理日期: $PROCESS_DATE"
echo "输入路径: $INPUT_PATH"
echo "输出路径: $OUTPUT_PATH"

# 提交Spark作业
spark-submit \
    --name "${APP_NAME}-${JOB_TYPE}-${PROCESS_DATE}" \
    --master $MASTER \
    --deploy-mode $DEPLOY_MODE \
    --queue $QUEUE \
    --num-executors $EXECUTOR_INSTANCES \
    --executor-cores $EXECUTOR_CORES \
    --executor-memory $EXECUTOR_MEMORY \
    --driver-memory $DRIVER_MEMORY \
    --driver-cores $DRIVER_CORES \
    "${SPARK_CONF[@]}" \
    --class $CLASS \
    $APPLICATION \
    --date $PROCESS_DATE \
    --input-path $INPUT_PATH \
    --output-path $OUTPUT_PATH

if [ $? -eq 0 ]; then
    echo "作业提交成功"
else
    echo "作业提交失败"
    exit 1
fi

性能优化实践

资源调优

object ResourceOptimization {
  
  // 动态资源分配策略
  def getOptimalResourceConfig(dataSize: Long, complexity: String): ResourceConfig = {
    val baseCores = 200
    val baseMemoryGB = 400
    
    val (scaleFactor, memoryRatio) = complexity match {
      case "simple" => (1.0, 0.8)
      case "medium" => (1.5, 1.2)
      case "complex" => (2.0, 1.5)
      case _ => (1.0, 1.0)
    }
    
    val dataSizeGB = dataSize / (1024 * 1024 * 1024)
    val extraCores = (dataSizeGB / 10).toInt * 10
    val totalCores = (baseCores + extraCores * scaleFactor).toInt
    val totalMemory = (baseMemoryGB * memoryRatio + dataSizeGB * 0.5).toInt
    
    val executorCores = 4
    val executorMemory = 8
    val numExecutors = totalCores / executorCores
    val driverMemory = Math.max(4, totalMemory / 20)
    
    ResourceConfig(
      numExecutors = numExecutors,
      executorCores = executorCores,
      executorMemory = s"${executorMemory}g",
      driverMemory = s"${driverMemory}g",
      maxExecutors = (numExecutors * 1.5).toInt,
      minExecutors = Math.max(5, numExecutors / 4)
    )
  }
  
  case class ResourceConfig(
    numExecutors: Int,
    executorCores: Int,
    executorMemory: String,
    driverMemory: String,
    maxExecutors: Int,
    minExecutors: Int
  )
  
  // 分区策略优化
  def optimizePartitioning(spark: SparkSession, df: DataFrame, targetSize: Long = 128 * 1024 * 1024): DataFrame = {
    val currentPartitions = df.rdd.getNumPartitions
    val dataSize = df.queryExecution.optimizedPlan.stats.sizeInBytes.toLong
    val optimalPartitions = Math.max(1, (dataSize / targetSize).toInt)
    
    if (currentPartitions > optimalPartitions * 2) {
      println(s"当前分区数 $currentPartitions 过多,合并至 $optimalPartitions")
      df.coalesce(optimalPartitions)
    } else if (currentPartitions < optimalPartitions / 2) {
      println(s"当前分区数 $currentPartitions 过少,重新分区至 $optimalPartitions")
      df.repartition(optimalPartitions)
    } else {
      df
    }
  }
}

缓存策略

object CacheOptimization {
  
  import org.apache.spark.storage.StorageLevel
  
  def optimizeCaching(spark: SparkSession, dataFrames: Map[String, DataFrame]): Map[String, DataFrame] = {
    dataFrames.map { case (name, df) =>
      val storageLevel = selectStorageLevel(df, name)
      val cachedDF = df.persist(storageLevel)
      
      // 触发缓存
      val count = cachedDF.count()
      println(s"$name 缓存完成,记录数: $count,存储级别: $storageLevel")
      
      name -> cachedDF
    }
  }
  
  private def selectStorageLevel(df: DataFrame, name: String): StorageLevel = {
    val estimatedSize = df.queryExecution.optimizedPlan.stats.sizeInBytes.toLong
    val sizeMB = estimatedSize / 1024 / 1024
    
    name match {
      case n if n.contains("lookup") || n.contains("dimension") =>
        // 维度表使用内存存储
        StorageLevel.MEMORY_ONLY
      case n if n.contains("fact") && sizeMB < 1000 =>
        // 小于1GB的事实表使用内存和磁盘
        StorageLevel.MEMORY_AND_DISK_SER
      case n if n.contains("intermediate") =>
        // 中间结果使用序列化存储
        StorageLevel.MEMORY_AND_DISK_SER_2
      case _ =>
        // 默认策略
        if (sizeMB < 500) StorageLevel.MEMORY_ONLY
        else StorageLevel.MEMORY_AND_DISK_SER
    }
  }
  
  def cleanupCache(spark: SparkSession, exceptNames: Set[String] = Set.empty): Unit = {
    val cachedTables = spark.catalog.listTables().collect()
      .filter(_.isTemporary)
      .filterNot(table => exceptNames.contains(table.name))
    
    cachedTables.foreach { table =>
      spark.catalog.uncacheTable(table.name)
      println(s"清理缓存表: ${table.name}")
    }
    
    // 清理RDD缓存
    spark.sparkContext.getRDDStorageInfo
      .filterNot(rdd => exceptNames.exists(rdd.name.contains))
      .foreach { rdd =>
        spark.sparkContext.unpersistRDD(rdd.id)
        println(s"清理RDD缓存: ${rdd.name}")
      }
  }
}

监控与告警

作业监控

object JobMonitoring {
  
  import java.util.Properties
  import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}
  import spray.json._
  import DefaultJsonProtocol._
  
  case class JobMetrics(
    jobId: String,
    jobName: String,
    startTime: Long,
    endTime: Option[Long],
    status: String,
    inputRecords: Long,
    outputRecords: Long,
    processingTime: Long,
    errorMessage: Option[String]
  )
  
  implicit val jobMetricsFormat = jsonFormat9(JobMetrics)
  
  class JobMonitor(kafkaBrokers: String, monitoringTopic: String) {
    
    private val producer = createKafkaProducer()
    
    def reportJobStart(jobId: String, jobName: String): Unit = {
      val metrics = JobMetrics(
        jobId = jobId,
        jobName = jobName,
        startTime = System.currentTimeMillis(),
        endTime = None,
        status = "RUNNING",
        inputRecords = 0,
        outputRecords = 0,
        processingTime = 0,
        errorMessage = None
      )
      
      sendMetrics(metrics)
    }
    
    def reportJobSuccess(jobId: String, 
                        jobName: String, 
                        startTime: Long,
                        inputRecords: Long, 
                        outputRecords: Long): Unit = {
      val endTime = System.currentTimeMillis()
      val metrics = JobMetrics(
        jobId = jobId,
        jobName = jobName,
        startTime = startTime,
        endTime = Some(endTime),
        status = "SUCCESS",
        inputRecords = inputRecords,
        outputRecords = outputRecords,
        processingTime = endTime - startTime,
        errorMessage = None
      )
      
      sendMetrics(metrics)
    }
    
    def reportJobFailure(jobId: String, 
                        jobName: String, 
                        startTime: Long,
                        errorMessage: String): Unit = {
      val endTime = System.currentTimeMillis()
      val metrics = JobMetrics(
        jobId = jobId,
        jobName = jobName,
        startTime = startTime,
        endTime = Some(endTime),
        status = "FAILED",
        inputRecords = 0,
        outputRecords = 0,
        processingTime = endTime - startTime,
        errorMessage = Some(errorMessage)
      )
      
      sendMetrics(metrics)
      sendAlert(jobName, errorMessage)
    }
    
    private def sendMetrics(metrics: JobMetrics): Unit = {
      val record = new ProducerRecord[String, String](
        monitoringTopic,
        metrics.jobId,
        metrics.toJson.toString
      )
      
      producer.send(record)
    }
    
    private def sendAlert(jobName: String, errorMessage: String): Unit = {
      val alertTopic = "spark-job-alerts"
      val alert = JsObject(
        "job_name" -> JsString(jobName),
        "error_message" -> JsString(errorMessage),
        "timestamp" -> JsNumber(System.currentTimeMillis()),
        "severity" -> JsString("HIGH")
      ).compactPrint
      
      val record = new ProducerRecord[String, String](alertTopic, jobName, alert)
      producer.send(record)
    }
    
    private def createKafkaProducer(): KafkaProducer[String, String] = {
      val props = new Properties()
      props.put("bootstrap.servers", kafkaBrokers)
      props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
      props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer")
      props.put("acks", "all")
      props.put("retries", "3")
      props.put("batch.size", "16384")
      props.put("linger.ms", "1")
      props.put("buffer.memory", "33554432")
      
      new KafkaProducer[String, String](props)
    }
    
    def close(): Unit = {
      producer.close()
    }
  }
}

数据质量监控

object DataQualityMonitoring {
  
  import org.apache.spark.sql.{SparkSession, DataFrame}
  import org.apache.spark.sql.functions._
  
  class DataQualityChecker(spark: SparkSession) {
    import spark.implicits._
    
    def checkDataQuality(df: DataFrame, 
                        tableName: String,
                        primaryKeys: List[String] = List.empty,
                        requiredColumns: List[String] = List.empty): DataQualityReport = {
      val totalRows = df.count()
      
      // 1. 空值检查
      val nullCounts = df.columns.map { col =>
        col -> df.filter(df(col).isNull || df(col) === "").count()
      }.toMap
      
      // 2. 重复数据检查
      val duplicateCount = if (primaryKeys.nonEmpty) {
        totalRows - df.dropDuplicates(primaryKeys).count()
      } else {
        totalRows - df.dropDuplicates().count()
      }
      
      // 3. 必需字段检查
      val missingRequiredFields = requiredColumns.filter { colName =>
        !df.columns.contains(colName)
      }
      
      // 4. 数据类型检查
      val schemaIssues = checkSchemaConsistency(df, tableName)
      
      // 5. 数据分布检查
      val distributionStats = df.columns.map { col =>
        try {
          val stats = df.select(col).describe().collect()
          col -> stats.map(row => row.getString(0) -> row.getString(1)).toMap
        } catch {
          case _: Exception => col -> Map.empty[String, String]
        }
      }.toMap
      
      // 计算数据质量分数
      val qualityScore = calculateQualityScore(
        totalRows, nullCounts, duplicateCount, 
        missingRequiredFields, schemaIssues
      )
      
      DataQualityReport(
        tableName = tableName,
        totalRows = totalRows,
        nullCounts = nullCounts,
        duplicateCount = duplicateCount,
        missingRequiredFields = missingRequiredFields,
        schemaIssues = schemaIssues,
        distributionStats = distributionStats,
        qualityScore = qualityScore,
        timestamp = System.currentTimeMillis()
      )
    }
    
    private def checkSchemaConsistency(df: DataFrame, tableName: String): List[String] = {
      val issues = scala.collection.mutable.ListBuffer[String]()
      
      // 检查是否有预期的schema
      // 这里可以从配置文件或元数据存储中获取预期schema
      val expectedSchema = getExpectedSchema(tableName)
      
      expectedSchema.foreach { case (colName, expectedType) =>
        df.schema.find(_.name == colName) match {
          case Some(field) =>
            if (field.dataType.toString != expectedType) {
              issues += s"字段 $colName 类型不匹配: 期望 $expectedType, 实际 ${field.dataType}"
            }
          case None =>
            issues += s"缺少字段: $colName"
        }
      }
      
      issues.toList
    }
    
    private def getExpectedSchema(tableName: String): Map[String, String] = {
      // 这里应该从配置或元数据库中获取预期schema
      // 简化实现
      tableName match {
        case "user_behavior" => Map(
          "user_id" -> "StringType",
          "timestamp" -> "LongType",
          "event_type" -> "StringType"
        )
        case "orders" => Map(
          "order_id" -> "StringType",
          "user_id" -> "StringType",
          "total_amount" -> "DecimalType"
        )
        case _ => Map.empty
      }
    }
    
    private def calculateQualityScore(totalRows: Long,
                                    nullCounts: Map[String, Long],
                                    duplicateCount: Long,
                                    missingFields: List[String],
                                    schemaIssues: List[String]): Int = {
      var score = 100
      
      // 空值惩罚
      val nullRatio = nullCounts.values.sum.toDouble / (totalRows * nullCounts.size)
      score -= (nullRatio * 20).toInt
      
      // 重复数据惩罚
      val duplicateRatio = duplicateCount.toDouble / totalRows
      score -= (duplicateRatio * 30).toInt
      
      // 缺少字段惩罚
      score -= missingFields.size * 10
      
      // Schema问题惩罚
      score -= schemaIssues.size * 15
      
      Math.max(0, score)
    }
    
    def generateQualityReport(reports: List[DataQualityReport]): String = {
      val sb = new StringBuilder
      sb.append("=== 数据质量报告 ===\n")
      sb.append(s"检查时间: ${new java.util.Date()}\n")
      sb.append(s"检查表数: ${reports.size}\n\n")
      
      reports.foreach { report =>
        sb.append(s"表名: ${report.tableName}\n")
        sb.append(s"记录数: ${report.totalRows}\n")
        sb.append(s"质量分数: ${report.qualityScore}/100\n")
        
        if (report.nullCounts.values.sum > 0) {
          sb.append("空值情况:\n")
          report.nullCounts.filter(_._2 > 0).foreach { case (col, count) =>
            val ratio = (count.toDouble / report.totalRows * 100).toInt
            sb.append(s"  $col: $count ($ratio%)\n")
          }
        }
        
        if (report.duplicateCount > 0) {
          sb.append(s"重复记录数: ${report.duplicateCount}\n")
        }
        
        if (report.missingRequiredFields.nonEmpty) {
          sb.append(s"缺少必需字段: ${report.missingRequiredFields.mkString(", ")}\n")
        }
        
        if (report.schemaIssues.nonEmpty) {
          sb.append("Schema问题:\n")
          report.schemaIssues.foreach(issue => sb.append(s"  $issue\n"))
        }
        
        sb.append("\n")
      }
      
      sb.toString()
    }
  }
  
  case class DataQualityReport(
    tableName: String,
    totalRows: Long,
    nullCounts: Map[String, Long],
    duplicateCount: Long,
    missingRequiredFields: List[String],
    schemaIssues: List[String],
    distributionStats: Map[String, Map[String, String]],
    qualityScore: Int,
    timestamp: Long
  )
}

总结

通过本批处理项目实战,我们展示了:

项目架构要点:

  1. 分层架构:ODS、DWD、DWS、ADS四层数据架构
  2. 技术选型:基于Spark的现代化数据处理栈
  3. 模块化设计:清洗、转换、聚合功能模块化
  4. 可扩展性:支持业务增长和技术演进

核心实现亮点:

  • 数据清洗模块示例:包含过滤、转换及质量检查框架,记录验证仍为占位实现,质量评分与schema检查仍有缺陷
  • 灵活的数据转换:支持复杂的业务逻辑
  • 多维度聚合:从用户、商品、地域等维度分析
  • 智能化调度:基于Airflow的自动化任务调度

生产环境设计参考(需结合执行环境与负载调整):

  • 资源优化:动态资源分配和分区优化
  • 缓存策略示例:按数据名称和估算大小选择存储级别,实际缓存收益与兼容性需结合复用次数、统计信息和内存预算判断
  • 监控告警示例:展示作业指标、失败告警及部分数据质量检查,不代表完整的生产监控与质量保障体系
  • 故障处理示例:展示任务重试、失败指标上报与通知;尚未实现分区幂等、失败回滚和补数恢复。

关键成功因素:

  1. 合理的架构设计和技术选型
  2. 完善的数据质量保障体系
  3. 高效的资源利用和性能优化
  4. 全面的监控和运维自动化

相关文档