04_Hudi は Spark を統合し、データを Hudi に保存し、Hive クエリと MergeInto ステートメントを統合します。

この記事は「ダークホース プログラマー」hudi コースからのものです

4. 第 4 章 Hudi Integrate Spark
4.1 環境準備
4.1.1 MySQL 5.7.31 のインストール
4.1.2 Hive 2.1 のインストール
4.1.3 Zookeeper 3.4.6 のインストール
4.1.4 Kafka 2.4.1 のインストール
4.2 Didi の動作分析
4.2.1 要件の説明
4.2 .2 環境の準備
4.2.2.1 ツール SparkUtils
4.2.2.2 日付変換週
4.2.3 データ ETL の保存
4.2.3.1 開発手順
4.2.3.2 CSV データのロード
4.2.3.3 データ ETL 変換
4.2.3.4 Hudi へのデータの保存
4.2. 3.5 Hudi テーブルストレージ構造
4.2.4 インデックスクエリ分析
4.2.4.1 開発ステップ
4.2.4.2 Hudi テーブルデータのロード
4.2.4.3 インデックス 1: 注文タイプ統計
4.2.4.4 インデックス 2: 注文適時性統計
4.2.4.5 インデックス 3: 注文トラフィックタイプ統計
4.2. 4.6 指標 4: 注文価格統計
4.2.4.7 指標 5: 注文距離統計
4.2.4.8 指標 6: 注文週統計
4.2.5 統合 Hive クエリ
4.2.5.1 テーブルとクエリの作成
4.2.5.2 HiveQL 分析
4.3 Hudi への構造化ストリームの書き込み
4.3.1 トランザクション順序のシミュレート
4.3.2 ストリーム プログラム開発
4.3.3 Spark クエリ分析
4.3.4 DeltaStreamer ツール
4.4 SparkSQL の統合
4.4.1 スパーク SQL の開始
4.4.2 クイック スタート
4.4 .2.1 テーブルの作成
4.4.2.2 データの挿入
4.4.2.3 データのクエリ
4.4.2.4 データの更新
4.4.2.5 データの削除
4.4.3 DDL テーブルの作成
4.4.4 MergeInto ステートメント
4.4.4.1 Merge Into Insert
4.4.4.2 Merge Into Update
4.4.4.3結合して削除

4. 第 4 章 Hudi Integrate Spark

データ レイク フレームワーク Hudi は、当初から操作に Spark をサポートしており、その後 Flink もサポートしました。次に、Spark との統合を見てみましょう。バージョン 0.9.0 では、SparkSQL サポートを提供し、データを操作するための DDL と DML を記述します。

4.1 環境の準備

Hudi データ レイク フレームワークは、Spark 分析エンジン フレームワークとの統合を開始し、Spark を通じてデータを Hudi テーブルに保存し、Spark を使用して分析のために Hudi テーブル データをロードし、バッチ処理とフロー コンピューティングをサポートするだけでなく、データ分析のために Hive を統合し、インストールします。ビッグデータなど フレームワーク: MySQL、Hive、Zookeeper、ケースの統合や統合に便利な Kafka。
ここに画像の説明を挿入

4.1.1 MySQL 5.7.31 のインストール

tar を使用して MySQL データベースをインストールします。具体的なコマンドと関連手順は次のとおりです。

# 1. 检查系统是否安装过mysql 
rpm -qa|grep mysql

# 2. 卸载CentOS7系统自带mariadb
rpm -qa|grep mariadb
rpm -e --nodeps mariadb-libs.xxxxxxx

# 3. 删除etc目录下的my.cnf ,一定要删掉,等下再重新建
rm /etc/my.cnf

# 4. 创建mysql 用户组和用户
groupadd mysql
useradd -r -g mysql mysql

# 5. 下载安装,从官网安装下载,位置在/usr/local/
wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.31-linux-glibc2.12-x86_64.tar.gz


# 6. 解压安装mysql
tar -zxvf mysql-5.7.31-linux-glibc2.12-x86_64.tar.gz -C /usr/local/
cd /usr/local/
mv mysql-5.7.31-linux-glibc2.12-x86_64 mysql

# 7. 进入mysql/bin/目录,编译安装并初始化mysql,务必记住数据库管理员临时密码
cd mysql/bin/
./mysqld --initialize --user=mysql --datadir=/usr/local/mysql/data --basedir=/usr/local/mysql

# 8. 编写配置文件 my.cnf ,并添加配置
vi /etc/my.cnf
    [mysqld]
    datadir=/usr/local/mysql/data
    port = 3306
    sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
    symbolic-links=0
    max_connections=400
    innodb_file_per_table=1
    lower_case_table_names=1

# 9. 启动mysql 服务器
/usr/local/mysql/support-files/mysql.server start

# 10. 添加软连接,并重启mysql 服务
ln -s /usr/local/mysql/support-files/mysql.server /etc/init.d/mysql
ln -s /usr/local/mysql/bin/mysql /usr/bin/mysql
service mysql restart

# 11. 登录mysql ,密码就是初始化时生成的临时密码
mysql -u root -p

# 12、修改密码,因为生成的初始化密码难记
set password for root@localhost = password('123456');

# 13、开放远程连接
use mysql;
update user set user.Host='%' where user.User='root';
flush privileges;

# 14. 设置开机自启
cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
chmod +x /etc/init.d/mysqld
chkconfig --add mysqld
chkconfig --list

最後に、MySQL データベース クライアントを使用してデータベースにリモート接続し、成功したかどうかをテストします。

4.1.2 Hive 2.1 のインストール

Hive フレームワーク tar パッケージを直接解凍し、HDFS 依存関係とメタデータ ストレージ MySQL データベース情報を構成し、最後にメタデータ サービス Hive MetaStore および HiveServer2 サービスを開始します。

# 1. 上传,解压
[root@node1 ~]# cd /export/server/
[root@node1 server]# rz
[root@node1 server]# chmod u+x apache-hive-2.1.0-bin.tar.gz      
[root@node1 server]# tar -zxf apache-hive-2.1.0-bin.tar.gz
[root@node1 server]# mv apache-hive-2.1.0-bin hive-2.1.0-bin
[root@node1 server]# ln -s hive-2.1.0-bin hive

# 2. 配置环境变量
[root@node1 server]# cd hive/conf/
[root@node1 conf]# mv hive-env.sh.template hive-env.sh
[root@node1 conf]# vim hive-env.sh 
    HADOOP_HOME=/export/server/hadoop
    export HIVE_CONF_DIR=/export/server/hive/conf
    export HIVE_AUX_JARS_PATH=/export/server/hive/lib
   
# 3. 创建HDFS目录
[root@node1 ~]# hadoop-daemon.sh start namenode
[root@node1 ~]# hadoop-daemon.sh start datanode

[root@node1 ~]# hdfs dfs -mkdir -p /tmp
[root@node1 ~]# hdfs dfs -mkdir -p /usr/hive/warehouse
[root@node1 ~]# hdfs dfs -chmod g+w /tmp
[root@node1 ~]# hdfs dfs -chmod g+w /usr/hive/warehouse

# 4. 配置文件hive-site.xml
[root@node1 ~]# cd /export/server/hive/conf
[root@node1 conf]# vim hive-site.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
	<property>
		<name>javax.jdo.option.ConnectionURL</name>
		<value>jdbc:mysql://node1.itcast.cn:3306/hive_metastore?createDatabaseIfNotExist=true</value>
	</property>
	<property>
		<name>javax.jdo.option.ConnectionDriverName</name>
		<value>com.mysql.jdbc.Driver</value>
	</property>
	<property>
		<name>javax.jdo.option.ConnectionUserName</name>
		<value>root</value>
	</property>
	<property>
		<name>javax.jdo.option.ConnectionPassword</name>
		<value>123456</value>
	</property>
	<property>
		<name>hive.metastore.warehouse.dir</name>
		<value>/usr/hive/warehouse</value>        
	</property>
	<property>
		<name>hive.metastore.uris</name>
		<value>thrift://node1.itcast.cn:9083</value>
	</property>
	<property>
		<name>hive.mapred.mode</name>
		<value>strict</value>
	</property>
	<property>
		<name>hive.exec.mode.local.auto</name>
		<value>true</value>
	</property>
	<property>
		<name>hive.fetch.task.conversion</name>
		<value>more</value>
	</property>
	    <property>
        <name>hive.server2.thrift.client.user</name>
        <value>root</value>
    </property>
    <property>
        <name>hive.server2.thrift.client.password</name>
        <value>123456</value>
    </property>
</configuration>

# 5. 添加用户权限配置
[root@node1 ~]# cd /export/server/hadoop/etc/hadoop
[root@node1 hadoop] vim core-site.xml
<property>
        <name>hadoop.proxyuser.root.hosts</name>
        <value>*</value>
    </property>
    <property>
        <name>hadoop.proxyuser.root.groups</name>
        <value>*</value>
    </property>

# 6. 初始化数据库
[root@node1 ~]# cd /export/server/hive/lib
[root@node1 lib]# rz
	 mysql-connector-java-5.1.48.jar

[root@node1 ~]# cd /export/server/hive/bin
[root@node1 bin]# ./schematool -dbType mysql -initSchema

# 7. 启动HiveMetaStore服务
[root@node1 ~]# cd /export/server/hive
[root@node1 hive]# nohup bin/hive --service metastore >/dev/null &

# 8. 启动HiveServer2服务
[root@node1 ~]# cd /export/server/hive
[root@node1 hive]# bin/hive --service hiveserver2 >/dev/null &

# 9. 启动beeline命令行
[root@node1 ~]# cd /export/server/hive
[root@node1 hive]# bin/beeline -u jdbc:hive2://node1.itcast.cn:10000 -n root -p 123456

サービスが正常に開始されたら、beeline クライアントを使用して接続し、データベースとテーブルを作成し、データをインポートし、テストをクエリします。

4.1.3 Zookeeper 3.4.6 のインストール

Zookeeper ソフトウェアをインストール ディレクトリにアップロードし、解凍して環境を構成します。コマンドは次のとおりです。

# 上传软件
[root@node1 ~]# cd /export/software
[root@node1 software]# rz
	zookeeper-3.4.6.tar.gz

# 给以执行权限
[root@node1 software]# chmod u+x zookeeper-3.4.6.tar.gz

# 解压tar包
[root@node1 software]# tar -zxf zookeeper-3.4.6.tar.gz -C /export/server

# 创建软链接
[root@node1 ~]# cd /export/server
[root@node1 server]# ln -s zookeeper-3.4.6 zookeeper

# 配置zookeeper
[root@node1 ~]# cd /export/server/zookeeper/conf
[root@node1 conf]# mv zoo_sample.cfg zoo.cfg
[root@node1 conf]# vim zoo.cfg
	修改内容:
	dataDir=/export/server/zookeeper/datas
[root@node1 conf]# mkdir -p /export/server/zookeeper/datas

# 设置环境变量
[root@node1 ~]# vim /etc/profile
添加内容:
        export ZOOKEEPER_HOME=/export/server/zookeeper
        export PATH=$PATH:$ZOOKEEPER_HOME/bin
[root@node1 ~]# source /etc/profile
启动Zookeeper服务,查看状态,命令如下:
# 启动服务
[root@node1 ~]# cd /export/server/zookeeper/
[root@node1 zookeeper]# bin/zkServer.sh start 
JMX enabled by default
Using config: /export/server/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

[root@node1 zookeeper]# bin/zkServer.sh status
JMX enabled by default
Using config: /export/server/zookeeper/bin/../conf/zoo.cfg
Mode: standalone

4.1.4 Kafka 2.4.1 のインストール

Kafka ソフトウェアをインストール ディレクトリにアップロードし、解凍して環境を構成します。コマンドは次のとおりです。

# 上传软件
[root@node1 ~]# cd /export/software
[root@node1 software~]#  rz
	kafka_2.12-2.4.1.tgz
[root@node1 software]# chmod u+x kafka_2.12-2.4.1.tgz 

# 解压tar包
[root@node1 software]# tar -zxf kafka_2.12-2.4.1.tgz -C /export/server
[root@node1 ~]# cd /export/server
[root@node1 server]# ln -s kafka_2.12-2.4.1 kafka

# 配置kafka
[root@node1 ~]# cd /export/server/kafka/config
[root@node1 conf]# vim server.properties
	修改内容:
	listeners=PLAINTEXT://node1.itcast.cn:9092		log.dirs=/export/server/kafka/kafka-logs
	zookeeper.connect=node1.itcast.cn:2181/kafka
# 创建存储目录		
[root@node1 ~]# mkdir -p /export/server/kafka/kafka-logs

# 设置环境变量
[root@node1 ~]# vim /etc/profile
添加内容:
        export KAFKA_HOME=/export/server/kafka
        export PATH=$PATH:$KAFKA_HOME/bin
[root@node1 ~]# source /etc/profile
启动Kafka服务,查看状态,命令如下:
# 启动服务
[root@node1 ~]# cd /export/server/kafka
[root@node1 kafka]# bin/kafka-server-start.sh -daemon config/server.properties
[root@node1 kafka]# jps
2188 QuorumPeerMain
2639 Kafka

4.2 Didi の運用分析

Didi が主導するインターネット配車プラットフォームの出現は、オフライン配車市場を再構築しただけでなく、市場の他の遊休リソースにさらなる利益の可能性をもたらしました。Kuaidiとの合併とUber Chinaの買収以来、Didiは国内旅行市場でNo.1の地位を確固たるものにしており、急速に発展する一方、ユーザーに多様なサービスを提供し続け、ソーシャルカー旅行のリソース配分を継続的に最適化している。質問です。このサンプルは、2017 年 5 月から 10 月までの海口市の Didi 注文データからランダムに選択したもので、合計 14,160,162 件の注文があります
ここに画像の説明を挿入

海口は南部にある大きな観光都市で、滴滴出行はここでの長い事業展開の歴史があり、大量の受注データを蓄積していますので、ここでは2017年下半期の受注データを使って簡単な統計分析をしてみます。この期間に滴滴は海口でビジネスを展開し、海口ユーザーの旅行特性の一部を明らかにしようとしました。

  • Kuaiche Chuxingは Didi の運営プロセスにおける主流の注文タイプです。
  • 滴滴出行の注文のうち、予約車両のシェアは極めて低く、依然としてリアルタイム予約が主流である。
  • ピックアップおよびドロップオフの注文は、総注文量の 4% にすぎません。
  • ほとんどの注文の距離は 0 ~ 15 キロメートルに集中しており、価格は 0 ~ 100 元に集中しています。
  • 平日は、オンライン配車旅行に対する住民の需要は減少しますが、週末は比較的旺盛です

4.2.1 要件の説明

Didi Chuxing のデータは、2017 年 5 月 1 日から 10 月 31 日まで(半年間)の海口市の毎日の注文データであり、注文の開始点と終了点の緯度経度と注文属性データが含まれています。注文タイプ、旅行カテゴリー、乗客数。特定のフィールドの意味は次のとおりです。
ここに画像の説明を挿入

海口の滴滴出行のデータによると、統計分析は次の要件に従って実行されます。
ここに画像の説明を挿入

4.2.2 環境の準備

先ほどの Maven プロジェクトを基に、関連するディレクトリとパッケージを作成し、次の図のような構造にします。
ここに画像の説明を挿入

このうち、Didi Chuxing データは、Maven プロジェクト [ datas ] ローカル ファイル システム ディレクトリに配置されます。Didi Chuxing の分析では、プログラムはデータ保存 Hudi テーブル [ DidiStorageSpark ] と指標計算統計分析 [ DidiAnalysisSpark ] の 2 つの部分に分かれています。

4.2.2.1 ツールクラス SparkUtils

データ ETL 保存またはデータ読み込み統計に関係なく、SparkSession インスタンス オブジェクトを作成する必要があるため、ツール クラス SparkUtils を記述し、インスタンスを構築するメソッド [ createSparkSession ] を作成します。コードは次のとおりです。

package cn.itcast.hudi.didi

import org.apache.spark.sql.SparkSession

/**
 * SparkSQL操作数据(加载读取和保存写入)时工具类,比如获取SparkSession实例对象等
 */
object SparkUtils {
    
    
   
   /**
    * 构建SparkSession实例对象,默认情况下本地模式运行
    */
   def createSparkSession(clazz: Class[_],
                          master: String = "local[4]", partitions: Int = 4): SparkSession = {
    
    
      SparkSession.builder()
         .appName(clazz.getSimpleName.stripSuffix("$"))
         .master(master)
         .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
         .config("spark.sql.shuffle.partitions", partitions)
         .getOrCreate()
   }
   
}

4.2.2.2 日付換算週

クエリ分析インデックスでは、営業日と休日の滴滴旅行の統計を容易にするために、日付と時刻フィールドの値を週に変換する必要があります。テストコードは次のとおりです。文字列が渡され、週に変換されます。

package cn.itcast.hudi.test

import java.util.{
    
    Calendar, Date}

import org.apache.commons.lang3.time.FastDateFormat

/**
 * 将日期转换星期,例如输入:2021-06-24  -> 星期四
 *      https://www.cnblogs.com/syfw/p/14370793.html
 */
object DayWeekTest {
    
    
   
   def main(args: Array[String]): Unit = {
    
    
      
      val dateStr: String = "2021-06-24"
      
      val format: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd")
      val calendar: Calendar = Calendar.getInstance()
      
      val date: Date = format.parse(dateStr)
      calendar.setTime(date)
      
      val dayWeek: String = calendar.get(Calendar.DAY_OF_WEEK) match {
    
    
         case 1 => "星期日"
         case 2 => "星期一"
         case 3 => "星期二"
         case 4 => "星期三"
         case 5 => "星期四"
         case 6 => "星期五"
         case 7 => "星期六"
      }
      
      println(dayWeek)
   }
   
}

コードを分析して作成し、Didi Chuxing データをローカル ファイル システムにロードして Hudi テーブルに保存し、最後に指標に従って分析します。

4.2.3 データETLストレージ

海口市の滴滴出行データをローカル ファイル システム LocalFS からロードし、対応する ETL 変換を実行し、最後に Hudi テーブルを保存します。

4.2.3.1 開発手順

データ ETL 変換とストレージを実現する SparkSQL プログラムを作成します。これは次の 5 つのステップに分かれています。

  • step1. SparkSessionインスタンスオブジェクトの構築(HudiとHDFSの統合)
  • step2. Didi Chuxing データをローカル CSV ファイル形式でロードする
  • step3. DidiトラベルデータETL処理
  • stpe4. 変換したデータをHudiテーブルに保存
  • step5. アプリケーションが終了し、リソースが閉じられます
    データ ETL 変換および保存プログラム: DidiStorageSpark、MAIN メソッドのコードは次のとおりです。
package cn.itcast.hudi.didi

import org.apache.spark.sql.{
    
    DataFrame, SaveMode, SparkSession}
import org.apache.spark.sql.functions._

/**
 * 滴滴海口出行运营数据分析,使用SparkSQL操作数据,先读取CSV文件,保存至Hudi表。
 *    -1. 数据集说明
 *        2017年5月1日-10月31日海口市每天的订单数据,包含订单的起终点经纬度以及订单类型、出行品类、乘车人数的订单属性数据。
 *        数据存储为CSV格式,首行为列名称
 *    -2. 开发主要步骤
 *      step1. 构建SparkSession实例对象(集成Hudi和HDFS)
 *      step2. 加载本地CSV文件格式滴滴出行数据
 *      step3. 滴滴出行数据ETL处理
 *      stpe4. 保存转换后数据至Hudi表
 *      step5. 应用结束关闭资源
 */
object DidiStorageSpark {
    
    
   
   // 滴滴数据路径
   val datasPath: String = "datas/didi/dwv_order_make_haikou_2.txt"
   
   // Hudi中表的属性
   val hudiTableName: String = "tbl_didi_haikou"
   val hudiTablePath: String = "/hudi-warehouse/tbl_didi_haikou"
   
   def main(args: Array[String]): Unit = {
    
    
      // step1. 构建SparkSession实例对象(集成Hudi和HDFS)
      val spark: SparkSession = SparkUtils.createSparkSession(this.getClass)
      import spark.implicits._
      
      // step2. 加载本地CSV文件格式滴滴出行数据
      val didiDF: DataFrame = readCsvFile(spark, datasPath)
      // didiDF.printSchema()
      // didiDF.show(10, truncate = false)
      
      // step3. 滴滴出行数据ETL处理并保存至Hudi表
      val etlDF: DataFrame = process(didiDF)
      //etlDF.printSchema()
      //etlDF.show(10, truncate = false)
      
      // stpe4. 保存转换后数据至Hudi表
      saveToHudi(etlDF, hudiTableName, hudiTablePath)
      
      // stpe5. 应用结束,关闭资源
      spark.stop()
   }

CSVデータの読み込み、データETL変換、データ保存の3つのメソッドをそれぞれMAINに実装します。

4.2.3.2 CSVデータの読み込み

書き込みメソッドは SparkSQL をカプセル化して、Didi 旅行データを CSV 形式で読み込みます。具体的なコードは次のとおりです。

/**
 * 读取CSV格式文本文件数据,封装到DataFrame数据集
 */
def readCsvFile(spark: SparkSession, path: String): DataFrame = {
    
    
   spark.read
      // 设置分隔符为逗号
      .option("sep", "\\t")
      // 文件首行为列名称
      .option("header", "true")
      // 依据数值自动推断数据类型
      .option("inferSchema", "true")
      // 指定文件路径
      .csv(path)
}

4.2.3.3 データETL変換

書き込み方法は、滴滴出行データのETL変換にフィールド[ts]と[partitionpath]を追加し、Hudiテーブルにデータを保存する際にフィールド名を指定します。具体的なコードは次のとおりです。

/**
 * 对滴滴出行海口数据进行ETL转换操作:指定ts和partitionpath 列
 */
def process(dataframe: DataFrame): DataFrame = {
    
    
   dataframe
      // 添加分区列:三级分区 -> yyyy/MM/dd
       .withColumn(
          "partitionpath",  // 列名称
          concat_ws("/", col("year"), col("month"), col("day")) //
       )
      // 删除列:year, month, day
      .drop("year", "month", "day")
      // 添加timestamp列,作为Hudi表记录数据与合并时字段,使用发车时间
       .withColumn(
          "ts",
          unix_timestamp(col("departure_time"), "yyyy-MM-dd HH:mm:ss")
       )
}

4.2.3.4 Hudi へのデータの保存

ETLで変換したデータをHudiテーブルに保存するメソッドをCOWモードで記述します。具体的なコードは以下のとおりです。

/**
 * 将数据集DataFrame保存值Hudi表中,表的类型:COW
 */
def saveToHudi(dataframe: DataFrame, table: String, path: String): Unit = {
    
    
   // 导入包
   import org.apache.hudi.DataSourceWriteOptions._
   import org.apache.hudi.config.HoodieWriteConfig._
   
   // 保存数据
   dataframe.write
      .mode(SaveMode.Overwrite)
      .format("hudi") // 指定数据源为Hudi
      .option("hoodie.insert.shuffle.parallelism", "2")
      .option("hoodie.upsert.shuffle.parallelism", "2")
      // Hudi 表的属性设置
      .option(RECORDKEY_FIELD_OPT_KEY, "order_id")
      .option(PRECOMBINE_FIELD_OPT_KEY, "ts")
      .option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath")
      // 表的名称和路径
      .option(TABLE_NAME, table)
      .save(path)
}

4.2.3.5 Hudiテーブルのストレージ構造

Spark プログラムを実行し、CSV 形式のデータを読み取り、ETL 変換後にそれを Hudi テーブルに保存し、次のように HDFS ディレクトリ構造を表示します。
ここに画像の説明を挿入

4.2.4 インデックスクエリ分析

クエリ分析インジケータに従って、Hudi テーブルからデータをロードし、グループ集計統計を実行し、結果を分析して結論を​​出します。
ここに画像の説明を挿入

4.2.4.1 開発手順

オブジェクト DidiAnalysisSpark を作成し、MAIN メソッドを記述し、最初に Hudi テーブルからデータをロードし、次にインジケーターに従ってグループ化および集計します。

package cn.itcast.hudi.didi

import java.util.{
    
    Calendar, Date}

import org.apache.commons.lang3.time.FastDateFormat
import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.{
    
    DataFrame, SparkSession}
import org.apache.spark.sql.functions._

/**
 * 滴滴海口出行运营数据分析,使用SparkSQL操作数据,从加载Hudi表数据,按照业务需求统计。
 *    -1. 数据集说明
 *        海口市每天的订单数据,包含订单的起终点经纬度以及订单类型、出行品类、乘车人数的订单属性数据。
 *        数据存储为CSV格式,首行为列名称
 *    -2. 开发主要步骤
 *      step1. 构建SparkSession实例对象(集成Hudi和HDFS)
 *      step2. 依据指定字段从Hudi表中加载数据
 *      step3. 按照业务指标进行数据统计分析
 *      step4. 应用结束关闭资源
 */
object DidiAnalysisSpark {
    
    
   
   // Hudi中表的属性
   val hudiTablePath: String = "/hudi-warehouse/tbl_didi_haikou"
   
   def main(args: Array[String]): Unit = {
    
    
      // step1. 构建SparkSession实例对象(集成Hudi和HDFS)
      val spark: SparkSession = SparkUtils.createSparkSession(this.getClass, partitions = 8)
      import spark.implicits._
      
      // step2. 依据指定字段从Hudi表中加载数据
      val hudiDF: DataFrame = readFromHudi(spark, hudiTablePath)
      
      // step3. 按照业务指标进行数据统计分析
      // 指标1:订单类型统计
      // reportProduct(hudiDF)
      // 指标2:订单时效统计
      // reportType(hudiDF)
      // 指标3:交通类型统计
      //reportTraffic(hudiDF)
      // 指标4:订单价格统计
      //reportPrice(hudiDF)
      // 指标5:订单距离统计
      //reportDistance(hudiDF)
      // 指标6:日期类型:星期,进行统计
      //reportWeek(hudiDF)
      
      // step4. 应用结束关闭资源
      spark.stop()
   }

その中には、Hudi テーブル データとさまざまなインジケーター統計がロードされ、簡単にテストできるようにさまざまなメソッドにカプセル化されます。

4.2.4.2 Hudiテーブルデータのロード

SparkSQL をカプセル化して Hudi テーブルからデータをロードし、インジケーター統計の取得に必要なフィールドをフィルターするメソッドを作成します。コードは次のとおりです。

/**
 * 从Hudi表加载数据,指定数据存在路径
 */
def readFromHudi(spark: SparkSession, path: String): DataFrame = {
    
    
   // a. 指定路径,加载数据,封装至DataFrame
   val didiDF: DataFrame = spark.read.format("hudi").load({
    
    path)
   
   // b. 选择字段
   didiDF
      // 选择字段
           .select(
          "order_id", "product_id", "type", "traffic_type", //
            "pre_total_fee", "start_dest_distance", "departure_time" //
       )
}

4.2.4.3 指標 1: 注文タイプの統計

海口市の滴滴出行のデータについては、フィールドproduct_id、中央値 [ 1 滴滴出行車、2 滴滴出行車、3 滴滴出行エクスプレス、4 滴滴出行企業エクスプレス]、梱包方法を使用して、注文タイプに従って統計が作成されます。 reportProduct は次のようにコード化します。

/**
 *  订单类型统计,字段:product_id
 */
def reportProduct(dataframe: DataFrame): Unit = {
    
    
   // a. 按照产品线ID分组统计
   val reportDF: DataFrame = dataframe.groupBy("product_id").count()
   
   // b. 自定义UDF函数,转换名称
   val to_name = udf(
      // 1滴滴专车, 2滴滴企业专车, 3滴滴快车, 4滴滴企业快车
      (productId: Int) => {
    
    
         productId match {
    
    
            case 1 =>  "滴滴专车"
            case 2 =>  "滴滴企业专车"
            case 3 =>  "滴滴快车"
            case 4 =>  "滴滴企业快车"
         }
      }
   )
   
   // c. 转换名称,应用函数
   val resultDF: DataFrame = reportDF.select(
      to_name(col("product_id")).as("order_type"), //
      col("count").as("total") //
   )
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

ヒストグラムを使用して統計結果を表示すると、 2017 年の海口市の滴滴出行では快車出行が主流の注文タイプであることがわかります
ここに画像の説明を挿入

統計サンプルによって提供された滴滴出行の注文データによると、ほとんどすべての注文は滴滴出行の製品ラインからのものであり、企業向け特別車の注文量と比較すると、滴滴出行の特別車は注文量のほんの一部にすぎません。製品ライン。重要ではありません。Didi Express は、Didi の伝統的な看板ビジネスとして、Didi の柱です。2014年末にサービスを開始した滴滴伝車(その後、2018年に「李城伝車」に改名)については、2017年のデータから判断すると、少なくとも海口市では利用率はそれほど高くない。これも当然ですが、結局のところ、Didi Zhuanche のターゲット層は小規模な高級ビジネス旅行グループであり、ビジネス旅行向けに高品質のサービスを提供することを目的としており、Didi Express と比較すると価格が高く、一般大衆には受け入れられません。国民の第一選択。

4.2.4.4 指標 2: 注文適時性統計

ユーザーの注文の適時性タイプ (タイプ、グループ集計統計) に従って、コードは次のようになります。

/**
 *  订单时效性统计,字段:type
 */
def reportType(dataframe: DataFrame): Unit = {
    
    
   // a. 按照产品线ID分组统计
   val reportDF: DataFrame = dataframe.groupBy("type").count()
   
   // b. 自定义UDF函数,转换名称
   val to_name = udf(
      // 0实时,1预约
      (realtimeType: Int) => {
    
    
         realtimeType match {
    
    
            case 0 =>  "实时"
            case 1 =>  "预约"
         }
      }
   )
   
   // c. 转换名称,应用函数
   val resultDF: DataFrame = reportDF.select(
      to_name(col("type")).as("order_realtime"), //
      col("count").as("total") //
   )
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

ヒストグラムを使用して結果を表示すると、2017 年の海口市の滴滴出行の注文のうち、予約車の市場シェアが非常に低く、依然としてリアルタイム予約が主流であることがわかります。
ここに画像の説明を挿入

統計サンプルによって提供された滴滴出行の注文データによると、滴滴出行はすでに車の予約事業を開始しているものの、依然としてリアルタイムの需要がほとんどの車の注文状況にありますが、これは予約車に価値がないことを意味するものではありません。消費者にとって、リアルタイムの自動車利用は柔軟性が高くなりますが、自動車の予約では事前に手配するオプションが提供され、特別な状況下で自動車を入手できないというオプションが回避され、消費者は自動車旅行を毎日のスケジュールに組み込むことができます。

4.2.4.5 指標 3: 注文トラフィック タイプの統計

海口市の滴滴出行データの場合、トラフィック タイプ: Traffic_type、グループおよび集計統計に従って、コードは次のようになります。

/**
 *  交通类型统计,字段:traffic_type
 */
def reportTraffic(dataframe: DataFrame): Unit = {
    
    
   // a. 按照产品线ID分组统计
   val reportDF: DataFrame = dataframe.groupBy("traffic_type").count()
   
   // b. 自定义UDF函数,转换名称
   val to_name = udf(
      // 1企业时租,2企业接机套餐,3企业送机套餐,4拼车,5接机,6送机,302跨城拼车
      (trafficType: Int) => {
    
    
         trafficType match {
    
    
case 0 =>  "普通散客"
            case 1 =>  "企业时租"
            case 2 =>  "企业接机套餐"
            case 3 =>  "企业送机套餐"
            case 4 =>  "拼车"
            case 5 =>  "接机"
            case 6 =>  "送机"
            case 302 =>  "跨城拼车"
            case _ => "未知"
         }
      }
   )
   
   // c. 转换名称,应用函数
   val resultDF: DataFrame = reportDF.select(
      to_name(col("traffic_type")).as("traffic_type"), //
      col("count").as("total") //
   )
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

ヒストグラムを使用して結果を表示すると、ピックアップ注文とドロップオフ注文が総注文量の 4% しか占めていないことがわかります
ここに画像の説明を挿入

空港交通の接続は滴滴出行にとって潜在的な市場であり、2017年5月から11月までの統計サンプルに記録された注文のうち、海口市の個人乗客の送迎注文は全体の4%を占めた。注文数は約5,600万件。しかし、法人送迎事業の実績はない。

4.2.4.6 指標 4: 注文価格統計

Didi Chuxing の注文データは、価格に応じてさまざまなレベルに分けられ、統計がグループ化されて集計されています。コードは次のとおりです。

/**
 * 订单价格统计,将价格分阶段统计,字段:pre_total_fee
 */
def reportPrice(dataframe: DataFrame): Unit = {
    
    
   val resultDF: DataFrame = dataframe
          .agg(
          // 价格:0 ~ 15
          sum(
             when(
                col("pre_total_fee").between(0, 15), 1
             ).otherwise(0)
          ).as("0~15"),
          // 价格:16 ~ 30
          sum(
             when(
                col("pre_total_fee").between(16, 30), 1
             ).otherwise(0)
          ).as("16~30"),
          // 价格:31 ~ 50
          sum(
             when(
                col("pre_total_fee").between(31, 50), 1
             ).otherwise(0)
          ).as("31~50"),
          // 价格:50 ~ 100
          sum(
             when(
                col("pre_total_fee").between(51, 100), 1
             ).otherwise(0)
          ).as("51~100"),
          // 价格:100+
          sum(
             when(
                col("pre_total_fee").gt(100), 1
             ).otherwise(0)
          ).as("100+")
       )
   
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

このうち、when条件関数とsum累積関数を利用して条件判定と累積統計を巧みに行います。また、結果からもわかるように【価格は0~50元に集中している】。

4.2.4.7 指標 5: 順序距離統計

Didi Chuxing データの場合、各注文の移動距離に応じて、異なるセグメント範囲を分割し、統計をグループ化して集計します。コードは次のとおりです。

/**
 * 订单距离统计,将价格分阶段统计,字段:start_dest_distance
 */
def reportDistance(dataframe: DataFrame): Unit = {
    
    
   val resultDF: DataFrame = dataframe
      .agg(
         // 价格:0 ~ 15
         sum(
            when(
               col("start_dest_distance").between(0, 10000), 1
            ).otherwise(0)
         ).as("0~10km"),
         // 价格:16 ~ 30
         sum(
            when(
               col("start_dest_distance").between(10001, 20000), 1
            ).otherwise(0)
         ).as("10~20km"),
         // 价格:31 ~ 50
         sum(
            when(
               col("start_dest_distance").between(200001, 30000), 1
            ).otherwise(0)
         ).as("20~30km"),
         // 价格:50 ~ 100
         sum(
            when(
               col("start_dest_distance").between(30001, 5000), 1
            ).otherwise(0)
         ).as("30~50km"),
         // 价格:100+
         sum(
            when(
               col("start_dest_distance").gt(50000), 1
            ).otherwise(0)
         ).as("50+km")
      )
   
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

このインジケーターはインジケーター 4 と似ており、統計に when 条件関数と合計合計関数を使用します。

4.2.4.8 指標 6: 注文週の統計

日付を週に変換し、統計をグループ化して集計し、稼働日と休憩を確認し、状況をドロップアウトするコードは次のとおりです。

/**
 *  订单星期分组统计,字段:departure_time
 */
def reportWeek(dataframe: DataFrame): Unit = {
    
    
   
   // a. 自定义UDF函数,转换日期为星期
   val to_week: UserDefinedFunction = udf(
      // 0实时,1预约
      (dateStr: String) => {
    
    
         val format: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd")
         val calendar: Calendar = Calendar.getInstance()
         
         val date: Date = format.parse(dateStr)
         calendar.setTime(date)
         
         val dayWeek: String = calendar.get(Calendar.DAY_OF_WEEK) match {
    
    
            case 1 => "星期日"
            case 2 => "星期一"
            case 3 => "星期二"
            case 4 => "星期三"
            case 5 => "星期四"
            case 6 => "星期五"
            case 7 => "星期六"
         }
         // 返回星期
         dayWeek
      }
   )
   
   // b. 转换日期为星期,并分组和统计
   val resultDF: DataFrame = dataframe
      .select(
         to_week(col("departure_time")).as("week")
      )
           .groupBy(col("week")).count()
           .select(
          col("week"), col("count").as("total") //
       )
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

結果を見ると、【平日は海口住民のタクシー旅行需要が減少するが、週末は比較的強い】ことが分かります

4.2.5 Hive クエリの統合

以前は、Didi の旅行データは Hudi テーブルに保存され、SparkSQL を使用してデータを読み取り、Hive テーブルのデータを統合して Hudi テーブルからデータを読み取りました。
ここに画像の説明を挿入

4.2.5.1 テーブルとクエリの作成

Hive でテーブルを作成し、それを Hudi テーブルに関連付けるには、統合 JAR パッケージhudi-hadoop-mr-bundle-0.9.0.jar を**$HIVE_HOME/lib** ディレクトリに配置する必要があります。

[root@node1 ~]# cp hudi-hadoop-mr-bundle-0.9.0.jar /export/server/hive/lib/

依存関係パッケージを Hive パスにコピーすると、Hive が Hudi データを正常に読み取ることができ、サーバー環境の準備が整います。
以前、Spark は Didi Chuxing データを Hudi テーブルに書き込みました。Hive 経由でこのデータにアクセスするには、Hive 外部テーブルを作成する必要があります。Hudi はパーティションで構成されているため、すべてのデータを読み取ることができるように、外部テーブルはまたスコアゾーン、ゾーンフィールド名も自由に設定可能です。

# 1. 创建数据库
create database db_hudi ;

# 2. 使用数据库
use db_hudi ;

# 3. 创建外部表
CREATE EXTERNAL TABLE tbl_hudi_didi(
    order_id bigint          ,
    product_id int           ,
    city_id int              ,
    district int             ,
    county int               ,
    type int                 ,
    combo_type int           ,
    traffic_type int         ,
    passenger_count int      ,
    driver_product_id int    ,
    start_dest_distance int  ,
    arrive_time string       ,
    departure_time string    ,
    pre_total_fee double     ,
    normal_time string       ,
    bubble_trace_id string   ,
    product_1level int       ,
    dest_lng double          ,
    dest_lat double          ,
    starting_lng double      ,
    starting_lat double      ,
    partitionpath string     ,
    ts bigint                
)
PARTITIONED BY ( 
  `yarn_str` string, `month_str` string, `day_str` string)
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe' 
STORED AS INPUTFORMAT 
  'org.apache.hudi.hadoop.HoodieParquetInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat'
LOCATION
  '/hudi-warehouse/tbl_didi_haikou' ;
  
# 5. 添加分区  
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='22') location '/hudi-warehouse/tbl_didi_haikou/2017/5/22' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='23') location '/hudi-warehouse/tbl_didi_haikou/2017/5/23' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='24') location '/hudi-warehouse/tbl_didi_haikou/2017/5/24' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='25') location '/hudi-warehouse/tbl_didi_haikou/2017/5/25' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='26') location '/hudi-warehouse/tbl_didi_haikou/2017/5/26' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='27') location '/hudi-warehouse/tbl_didi_haikou/2017/5/27' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='28') location '/hudi-warehouse/tbl_didi_haikou/2017/5/28' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='29') location '/hudi-warehouse/tbl_didi_haikou/2017/5/29' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='30') location '/hudi-warehouse/tbl_didi_haikou/2017/5/30' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='31') location '/hudi-warehouse/tbl_didi_haikou/2017/5/31' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='1') location '/hudi-warehouse/tbl_didi_haikou/2017/6/1' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='2') location '/hudi-warehouse/tbl_didi_haikou/2017/6/2' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='3') location '/hudi-warehouse/tbl_didi_haikou/2017/6/3' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='4') location '/hudi-warehouse/tbl_didi_haikou/2017/6/4' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='5') location '/hudi-warehouse/tbl_didi_haikou/2017/6/5' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='6') location '/hudi-warehouse/tbl_didi_haikou/2017/6/6' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='7') location '/hudi-warehouse/tbl_didi_haikou/2017/6/7' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='8') location '/hudi-warehouse/tbl_didi_haikou/2017/6/8' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='9') location '/hudi-warehouse/tbl_didi_haikou/2017/6/9' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='10') location '/hudi-warehouse/tbl_didi_haikou/2017/6/10' ;

# 查看分区信息
show partitions tbl_hudi_didi ;

上記のコマンドを実行すると、Hive テーブルのデータが Hudi テーブルのデータに正常に関連付けられます。Hive で SQL ステートメントを作成して Hudi データを分析し、SELECT ステートメントを使用してテーブル内のデータをクエリできます。

# 设置非严格模式
set hive.mapred.mode = nonstrict ;

# SQL查询前10条数据
select order_id, product_id, type, traffic_type, pre_total_fee, start_dest_distance, departure_time 
from db_hudi.tbl_hudi_didi limit 10 ;

表示される結果は次のとおりです。
ここに画像の説明を挿入

4.2.5.2 HiveQL 分析

Hive フレームワークの beeline コマンド ラインで HiveQL ステートメントを作成し、上記のセクション 5.4 のインジケーターの統計分析を実行します。

# 设置Hive本地模式
set hive.exec.mode.local.auto=true;

set hive.exec.mode.local.auto.tasks.max=10;
set hive.exec.mode.local.auto.inputbytes.max=50000000;

指標 1: 注文タイプの統計

WITH tmp AS (
  SELECT product_id, COUNT(1) AS total FROM db_hudi.tbl_hudi_didi GROUP BY product_id
)
SELECT 
  CASE product_id
    WHEN 1 THEN "滴滴专车"
    WHEN 2 THEN "滴滴企业专车"
    WHEN 3 THEN "滴滴快车"
    WHEN 4 THEN "滴滴企业快车"
  END AS order_type,
  total
FROM tmp ;

次の図に示す分析結果 (滴滴出行データのごく一部のみが Hudi テーブルにインポートされます)。
ここに画像の説明を挿入

指標 2: 注文の適時性の統計

WITH tmp AS (
  SELECT type AS order_realtime, COUNT(1) AS total FROM db_hudi.tbl_hudi_didi GROUP BY type
)
SELECT 
  CASE order_realtime
    WHEN 0 THEN "实时"
    WHEN 1 THEN "预约"
  END AS order_realtime,
  total
FROM tmp ;

次の図に示す分析結果 (滴滴出行データのごく一部のみが Hudi テーブルにインポートされます)。
ここに画像の説明を挿入

指標 3: 注文トラフィック タイプの統計

WITH tmp AS (
  SELECT traffic_type, COUNT(1) AS total FROM db_hudi.tbl_hudi_didi GROUP BY traffic_type
)
SELECT 
  CASE traffic_type
   WHEN 0 THEN  "普通散客" 
   WHEN 1 THEN  "企业时租"
   WHEN 2 THEN  "企业接机套餐"
   WHEN 3 THEN  "企业送机套餐"
   WHEN 4 THEN  "拼车"
   WHEN 5 THEN  "接机"
   WHEN 6 THEN  "送机"
   WHEN 302 THEN  "跨城拼车"
   ELSE "未知"
  END AS traffic_type,
  total
FROM tmp ;

次の図に示す分析結果 (滴滴出行データのごく一部のみが Hudi テーブルにインポートされます)。
ここに画像の説明を挿入

指標 4: 注文価格統計

SELECT 
  SUM(
    CASE WHEN pre_total_fee BETWEEN 1 AND 15 THEN 1 ELSE 0 END
  ) AS 0_15,
  SUM(
    CASE WHEN pre_total_fee BETWEEN 16 AND 30 THEN 1 ELSE 0 END
  ) AS 16_30,
  SUM(
    CASE WHEN pre_total_fee BETWEEN 31 AND 50 THEN 1 ELSE 0 END
  ) AS 31_150,
  SUM(
    CASE WHEN pre_total_fee BETWEEN 51 AND 100 THEN 1 ELSE 0 END
  ) AS 51_100,
  SUM(
    CASE WHEN pre_total_fee > 100 THEN 1 ELSE 0 END
  )  AS 100_
FROM 
  db_hudi.tbl_hudi_didi;

次の図に示す分析結果 (滴滴出行データのごく一部のみが Hudi テーブルにインポートされます)。
ここに画像の説明を挿入

4.3 構造化ストリームを Hudi に書き込む

Spark Structured Streaming と Hudi を統合し、リアルタイムでストリーミング データを Hudi テーブルに書き込み、Spark DataSource を使用してデータ バッチ DataFrame の各バッチのデータを書き込みます。
ここに画像の説明を挿入
属性パラメータの説明: https://hudi.apache.org/docs/writing_data#datasource-writer

4.3.1 模擬取引注文

プログラミング シミュレーションでは、トランザクション注文データが生成され、リアルタイムで Kafka トピックが送信されます。わかりやすくするために、トランザクション注文データ フィールドは次のようになり、サンプル クラス OrderRecord にカプセル化されています。

/**
 * 订单实体类(Case Class)
 *
 * @param orderId     订单ID
 * @param userId      用户ID
 * @param orderTime   订单日期时间
 * @param ip          下单IP地址
 * @param orderMoney  订单金额
 * @param orderStatus 订单状态
 */
case class OrderRecord(
                         orderId: String,
                         userId: String,
                         orderTime: String,
                         ip: String,
                         orderMoney: Double,
                         orderStatus: Int
                      )

トランザクション注文データをリアルタイムで生成するプログラム [ MockOrderProducer ] を作成し、Json4J クラス ライブラリを使用してデータを JSON 文字に変換し、それを Kafka Topic に送信します。コードは次のとおりです

import java.util.Properties

import org.apache.commons.lang3.time.FastDateFormat
import org.apache.kafka.clients.producer.{
    
    KafkaProducer, ProducerRecord}
import org.apache.kafka.common.serialization.StringSerializer
import org.json4s.jackson.Json

import scala.util.Random

/**
 * 模拟生产订单数据,发送到Kafka Topic中
 *      Topic中每条数据Message类型为String,以JSON格式数据发送
 * 数据转换:
 *      将Order类实例对象转换为JSON格式字符串数据(可以使用json4s类库)
 */
object MockOrderProducer {
    
    
   
   def main(args: Array[String]): Unit = {
    
    
      
      var producer: KafkaProducer[String, String] = null
      try {
    
    
         // 1. Kafka Client Producer 配置信息
         val props = new Properties()
         props.put("bootstrap.servers", "node1.itcast.cn:9092")
         props.put("acks", "1")
         props.put("retries", "3")
         props.put("key.serializer", classOf[StringSerializer].getName)
         props.put("value.serializer", classOf[StringSerializer].getName)
         
         // 2. 创建KafkaProducer对象,传入配置信息
         producer = new KafkaProducer[String, String](props)
         
         // 随机数实例对象
         val random: Random = new Random()
         // 订单状态:订单打开 0,订单取消 1,订单关闭 2,订单完成 3
         val allStatus = Array(0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
         
         while (true) {
    
    
            // 每次循环 模拟产生的订单数目
            val batchNumber: Int = random.nextInt(1) + 5
            (1 to batchNumber).foreach {
    
     number =>
               val currentTime: Long = System.currentTimeMillis()
               val orderId: String = s"${getDate(currentTime)}%06d".format(number)
               val userId: String = s"${1 + random.nextInt(5)}%08d".format(random.nextInt(1000))
               val orderTime: String = getDate(currentTime, format = "yyyy-MM-dd HH:mm:ss.SSS")
               val orderMoney: String = s"${5 + random.nextInt(500)}.%02d".format(random.nextInt(100))
               val orderStatus: Int = allStatus(random.nextInt(allStatus.length))
               // 3. 订单记录数据
               val orderRecord: OrderRecord = OrderRecord(
                  orderId, userId, orderTime, getRandomIp, orderMoney.toDouble, orderStatus
               )
               // 转换为JSON格式数据
               val orderJson = new Json(org.json4s.DefaultFormats).write(orderRecord)
               println(orderJson)
               // 4. 构建ProducerRecord对象
               val record = new ProducerRecord[String, String]("order-topic", orderId, orderJson)
               // 5. 发送数据:def send(messages: KeyedMessage[K,V]*), 将数据发送到Topic
               producer.send(record)
            }
            Thread.sleep(random.nextInt(500))
         }
      } catch {
    
    
         case e: Exception => e.printStackTrace()
      } finally {
    
    
         if (null != producer) producer.close()
      }
   }
   
   /** =================获取当前时间================= */
   def getDate(time: Long, format: String = "yyyyMMddHHmmssSSS"): String = {
    
    
      val fastFormat: FastDateFormat = FastDateFormat.getInstance(format)
      val formatDate: String = fastFormat.format(time) // 格式化日期
      formatDate
   }
   
   /** ================= 获取随机IP地址 ================= */
   def getRandomIp: String = {
    
    
      // ip范围
      val range: Array[(Int, Int)] = Array(
         (607649792, 608174079), //36.56.0.0-36.63.255.255
         (1038614528, 1039007743), //61.232.0.0-61.237.255.255
         (1783627776, 1784676351), //106.80.0.0-106.95.255.255
         (2035023872, 2035154943), //121.76.0.0-121.77.255.255
         (2078801920, 2079064063), //123.232.0.0-123.235.255.255
         (-1950089216, -1948778497), //139.196.0.0-139.215.255.255
         (-1425539072, -1425014785), //171.8.0.0-171.15.255.255
         (-1236271104, -1235419137), //182.80.0.0-182.92.255.255
         (-770113536, -768606209), //210.25.0.0-210.47.255.255
         (-569376768, -564133889) //222.16.0.0-222.95.255.255
      )
      // 随机数:IP地址范围下标
      val random = new Random()
      val index = random.nextInt(10)
      val ipNumber: Int = range(index)._1 + random.nextInt(range(index)._2 - range(index)._1)
      
      // 转换Int类型IP地址为IPv4格式
      number2IpString(ipNumber)
   }
   
   /** =================将Int类型IPv4地址转换为字符串类型================= */
   def number2IpString(ip: Int): String = {
    
    
      val buffer: Array[Int] = new Array[Int](4)
      buffer(0) = (ip >> 24) & 0xff
      buffer(1) = (ip >> 16) & 0xff
      buffer(2) = (ip >> 8) & 0xff
      buffer(3) = ip & 0xff
      // 返回IPv4地址
      buffer.mkString(".")
   }
   
}

フォーマットした後、アプリケーションを実行してトランザクション注文データの生成をシミュレートします。
ここに画像の説明を挿入

4.3.2 ストリーミングプログラムの開発

構造化ストリーミング アプリケーション: HudiStructuredDemoを作成します。これは、Kafka の [ order-topic ]から JSON 形式のデータをリアルタイムで消費し、ETL 変換後に Hudi テーブルに保存します。

package cn.itcast.hudi.streaming

import org.apache.spark.internal.Logging
import org.apache.spark.sql._
import org.apache.spark.sql.functions._
import org.apache.spark.sql.streaming.OutputMode

/**
 * 基于StructuredStreaming结构化流实时从Kafka消费数据,经过ETL转换后,存储至Hudi表
 */
object HudiStructuredDemo extends Logging{
    
    
   
   def main(args: Array[String]): Unit = {
    
    
      // step1、构建SparkSession实例对象
      val spark: SparkSession = createSparkSession(this.getClass)
      
      // step2、从Kafka实时消费数据
      val kafkaStreamDF: DataFrame = readFromKafka(spark, "order-topic")
      
      // step3、提取数据,转换数据类型
      val streamDF: DataFrame = process(kafkaStreamDF)
      
      // step4、保存数据至Hudi表中:COW(写入时拷贝)和MOR(读取时保存)
      saveToHudi(streamDF)
      
      // step5、流式应用启动以后,等待终止
      spark.streams.active.foreach(query => println(s"Query: ${query.name} is Running ............."))
      spark.streams.awaitAnyTermination()
   }
   
   /**
    * 创建SparkSession会话实例对象,基本属性设置
    */
   def createSparkSession(clazz: Class[_]): SparkSession = {
    
    
      SparkSession.builder()
         .appName(this.getClass.getSimpleName.stripSuffix("$"))
         .master("local[2]")
         // 设置序列化方式:Kryo
         .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
         // 设置属性:Shuffle时分区数和并行度
         .config("spark.default.parallelism", 2)
         .config("spark.sql.shuffle.partitions", 2)
         .getOrCreate()
   }
   
   /**
    * 指定Kafka Topic名称,实时消费数据
    */
   def readFromKafka(spark: SparkSession, topicName: String): DataFrame = {
    
    
      spark
         .readStream
         .format("kafka")
         .option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
         .option("subscribe", topicName)
         .option("startingOffsets", "latest")
         .option("maxOffsetsPerTrigger", 100000)
         .option("failOnDataLoss", "false")
          .load()
   }
   
   /**
    * 对Kafka获取数据,进行转换操作,获取所有字段的值,转换为String,以便保存Hudi表
    */
   def process(streamDF: DataFrame): DataFrame = {
    
    
      /* 从Kafka消费数据后,字段信息如
         key -> binary,value -> binary
         topic -> string, partition -> int, offset -> long
         timestamp -> long, timestampType -> int
       */
      streamDF
         // 选择字段,转换类型为String
         .selectExpr(
            "CAST(key AS STRING) order_id", //
            "CAST(value AS STRING) message", //
            "topic", "partition", "offset", "timestamp"//
         )
         // 解析Message,提取字段内置
          .withColumn("user_id", get_json_object(col("message"), "$.userId"))
          .withColumn("order_time", get_json_object(col("message"), "$.orderTime"))
          .withColumn("ip", get_json_object(col("message"), "$.ip"))
          .withColumn("order_money", get_json_object(col("message"), "$.orderMoney"))
          .withColumn("order_status", get_json_object(col("message"), "$.orderStatus"))
         // 删除Message列
          .drop(col("message"))
         // 转换订单日期时间格式为Long类型,作为Hudi表中合并数据字段
          .withColumn("ts", to_timestamp(col("order_time"), "yyyy-MM-dd HH:mm:ss.SSSS"))
          // 订单日期时间提取分区日期:yyyyMMdd
          .withColumn("day", substring(col("order_time"), 0, 10))
   }
   
   /**
    * 将流式数据集DataFrame保存至Hudi表,分别表类型:COW和MOR
    */
   def saveToHudi(streamDF: DataFrame): Unit = {
    
    
      streamDF.writeStream
         .outputMode(OutputMode.Append())
          .queryName("query-hudi-streaming")
         // 针对每微批次数据保存
          .foreachBatch((batchDF: Dataset[Row], batchId: Long) => {
    
    
            println(s"============== BatchId: ${batchId} start ==============")
             writeHudiMor(batchDF) // TODO:表的类型MOR
          })
         .option("checkpointLocation", "/datas/hudi-spark/struct-ckpt-100")
          .start()
   }
   
   /**
    * 将数据集DataFrame保存到Hudi表中,表的类型:MOR(读取时合并)
    */
   def writeHudiMor(dataframe: DataFrame): Unit = {
    
    
      import org.apache.hudi.DataSourceWriteOptions._
      import org.apache.hudi.config.HoodieWriteConfig._
      import org.apache.hudi.keygen.constant.KeyGeneratorOptions._
      
      dataframe.write
         .format("hudi")
         .mode(SaveMode.Append)
         // 表的名称
         .option(TBL_NAME.key, "tbl_kafka_mor")
         // 设置表的类型
         .option(TABLE_TYPE.key(), "MERGE_ON_READ")
         // 每条数据主键字段名称
         .option(RECORDKEY_FIELD_NAME.key(), "order_id")
         // 数据合并时,依据时间字段
         .option(PRECOMBINE_FIELD_NAME.key(), "ts")
         // 分区字段名称
         .option(PARTITIONPATH_FIELD_NAME.key(), "day")
         // 分区值对应目录格式,是否与Hive分区策略一致
         .option(HIVE_STYLE_PARTITIONING_ENABLE.key(), "true")
         // 插入数据,产生shuffle时,分区数目
         .option("hoodie.insert.shuffle.parallelism", "2")
         .option("hoodie.upsert.shuffle.parallelism", "2")
         // 表数据存储路径
         .save("/hudi-warehouse/tbl_order_mor")
   }
   
}

上記のコードには、ストリーミング アプリケーションにとって重要な 2 つの詳細があります。

  • まず、Kafka からデータを使用するときに、属性 [maxOffsetsPerTrigger] を介してバッチあたりのデータの最大量を設定します。実際の運用プロジェクトは、ストリーム データのピークとアプリケーション実行リソースを考慮して設定する必要があります。
  • 次に、ETL 後のデータを Hudi に保存し、チェックポイントの場所 Checkpoint Location を設定します。これにより、ストリーミング アプリケーションの実行に失敗した後、チェックポイントから回復し、最後の消費データを継続して、リアルタイム処理を実行できます。

上記のプログラムを実行して、HDFS 上の Hudi テーブル ストレージ トランザクション順序データ ストレージ ディレクトリ構造を表示します。
ここに画像の説明を挿入

4.3.3 Sparkクエリ分析

Spark-Shell コマンドラインを開始し、Hudi テーブルをクエリしてトランザクション注文データを保存します。コマンドは次のとおりです。

spark-shell \
--master local[2] \
--jars /root/hudi-jars/hudi-spark3-bundle_2.12-0.9.0.jar,\
/root/hudi-jars/spark-avro_2.12-3.0.1.jar,/root/hudi-jars/spark_unused-1.0.0.jar \
--conf "spark.serializer=org.apache.spark.serializer.KryoSerializer"  

Hudi テーブル データ ストレージ ディレクトリを指定し、データをロードします。

val orderDF = spark.read.format("hudi").load("/hudi-warehouse/tbl_order_mor")

スキーマ情報の表示

orderDF.printSchema()

ここに画像の説明を挿入

注文テーブルのデータの最初の 10 項目を表示し、注文の関連フィールドを選択します。

orderDF.select("order_id", "user_id", "order_time", "ip", "order_money", "order_status", "day").show(false)

ここに画像の説明を挿入

データ エントリの総数を表示します。

orderDF.count()

取引注文データの基本的な集計統計: 最大金額 max、最小金額 min、平均金額 avg

spark.sql("""
  with tmp AS (
    SELECT CAST(order_money AS DOUBLE) FROM view_tmp_order WHERE order_status = '0'
  )
  select 
    max(order_money) as max_money, 
    min(order_money) as min_money, 
    round(avg(order_money), 2) as avg_money 
  from tmp 
""").show()

ここに画像の説明を挿入

4.3.4 DeltaStreamer ツールクラス

HoudiDeltaStreamerツール (hudi-utilities-bundle の一部) は、DFS や Kafka などのさまざまなソースから取り込む方法を提供し、次の機能があります。

  • Kafka からの新しいイベントの単一取り込み
  • json、avro、またはカスタム レコード タイプの受信データをサポート
  • チェックポイント、ロールバック、リカバリを管理する
  • DFS または Confluent スキーマ レジストリを利用した Avro スキーマ
  • カスタム変換操作をサポートします。
    ツール クラス:HoodieDeltaStreamer は、基本的に Spark ストリーミング プログラムを実行し、リアルタイムでデータを取得し、それをオーストリアの Hudi テーブルに保存し、次のコマンドを実行して、ヘルプ ドキュメントを表示します。
spark-submit --master local[2] \
--class org.apache.hudi.utilities.deltastreamer.HoodieDeltaStreamer \
/root/hudi-utilities-bundle_2.11-0.9.0.jar \
--help

注: ツールが配置されている jar パッケージ [hudi-utilities-bundle_2.11-0.9.0.jar] を CLASSPATH に追加します。
公式ケースが提供されています。Kafka でのデータのリアルタイム消費、データ形式は Avro で、Hudi テーブルに保存されます。
ここに画像の説明を挿入

4.4 SparkSQL の統合

Hudi の最新バージョン 0.9.0 は、SparkSQL との統合をサポートしており、spark-sql 対話型コマンド ラインで SQL ステートメントを直接作成するため、Hudi テーブルに対するユーザーの DDL/DML 操作が大幅に容易になります。ドキュメント: https://hudi.apache.org/docs/quick-start-guide
ここに画像の説明を挿入

4.4.1 スパーク SQL を開始する

Hudi テーブル データは HDFS ファイル システムに保存されます。まず、NameNode サービスと DataNode サービスを開始します。

[root@node1 ~]# hadoop-daemon.sh start namenode
[root@node1 ~]# hadoop-daemon.sh start datanode

Spark-sql 対話型コマンド ラインを開始し、依存する jar パッケージと関連するプロパティ パラメーターを設定します。

spark-sql \
--master local[2] \
--jars /root/hudi-jars/hudi-spark3-bundle_2.12-0.9.0.jar,\
/root/hudi-jars/spark-avro_2.12-3.0.1.jar,/root/hudi-jars/spark_unused-1.0.0.jar \
--conf 'spark.serializer=org.apache.spark.serializer.KryoSerializer' \
--conf 'spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension'

Hudi のデフォルトの upsert/insert/delete 同時実行数は 1500 ですが、小規模なデータ セットのデモンストレーション用には、より小さい同時実行数が設定されています。

set hoodie.upsert.shuffle.parallelism = 1;
set hoodie.insert.shuffle.parallelism = 1;
set hoodie.delete.shuffle.parallelism = 1;

Hudi テーブルのメタデータを同期しないように設定します。

set hoodie.datasource.meta.sync.enable=false;

4.4.2 クイックスタート

DDL および DML ステートメントを使用して、テーブルの作成、テーブルの削除、データに対する CURD 操作の実行を行います。

4.4.2.1 テーブルの作成

DDL ステートメントを作成し、Hudi テーブルを作成します。テーブル タイプは MOR およびパーティション テーブル、主キーは id、パーティション フィールドは dt、マージ フィールドのデフォルトは ts です

create table test_hudi_table (
  id int,
  name string,
  price double,
  ts long,
  dt string
) using hudi
 partitioned by (dt)
 options (
  primaryKey = 'id',
  type = 'mor'
 )
location 'hdfs://node1.itcast.cn:8020/hudi-warehouse/test_hudi_table' ;

Hudi テーブルを作成した後、作成された Hudi テーブルを表示する

show create table test_hudi_table 

ここに画像の説明を挿入

4.4.2.2 データの挿入

INSERT INTO を使用してデータを Hudi テーブルに挿入します。

insert into test_hudi_table select 1 as id, 'hudi' as name, 10 as price, 1000 as ts, '2021-11-01' as dt;

挿入が完了したら、Hudi テーブルのローカル ディレクトリ構造を確認してください。生成されたメタデータ、パーティション、データは、Spark データソースによって書き込まれたものと同じです。
ここに画像の説明を挿入

ISNERT INTO ステートメントを使用して、さらにいくつかのデータを挿入します。コマンドは次のとおりです。

insert into test_hudi_table select 2 as id, 'spark' as name, 20 as price, 1100 as ts, '2021-11-01' as dt;
insert into test_hudi_table select 3 as id, 'flink' as name, 30 as price, 1200 as ts, '2021-11-01' as dt;
insert into test_hudi_table select 4 as id, 'sql' as name, 40 as price, 1400 as ts, '2021-11-01' as dt;

4.4.2.3 クエリデータ

SQL を使用して Hudi テーブル データ、フル テーブル スキャン クエリをクエリします。

select * from test_hudi_table ;

ここに画像の説明を挿入

テーブル内のフィールド構造を表示するには、DESC ステートメントを使用します。

desc test_hudi_table ;

ここに画像の説明を挿入

テーブル内の過去数日間のデータをクエリするクエリ フィールドを指定します。

SELECT _hoodie_record_key,_hoodie_partition_path, id, name, price, ts, dt FROM test_hudi_table ;

ここに画像の説明を挿入

4.4.2.4 データの更新

update ステートメントを使用して、id=1 データの価格を 100 に更新します。ステートメントは次のとおりです。

update test_hudi_table set price = 100.0 where id = 1 ;

Hudi テーブル データを再度クエリして、データが更新されているかどうかを確認します。

SELECT id, name, price, ts, dt FROM test_hudi_table WHERE id = 1;

ここに画像の説明を挿入

4.4.2.5 データの削除

DELETE ステートメントを使用して、id=1 のレコードを削除します。コマンドは次のとおりです。

delete from test_hudi_table where id = 1 ;

Hudi テーブル データを再度クエリして、データが更新されているかどうかを確認します。

SELECT COUNT(1) AS total from test_hudi_table WHERE id = 1;

クエリ結果は次のとおりで、データをクエリできず、Hudi テーブルにレコードがないことがわかります。
ここに画像の説明を挿入

4.4.3 DDL テーブル作成

Spark-SQL で DDL ステートメントを作成し、Hudi テーブル データとコア 3 つの属性パラメータを作成します。
ここに画像の説明を挿入

  • Hudi テーブルのタイプを指定します。
    ここに画像の説明を挿入

公式ケース:COW型のHudiテーブルを作成します。
ここに画像の説明を挿入

  • 管理テーブルと外部テーブル: テーブルを作成するときに、場所の保存パスを指定します。テーブルは外部テーブルです。
    ここに画像の説明を挿入

  • テーブルを作成するときは、パーティションテーブルとして設定します:パーティションテーブル
    ここに画像の説明を挿入

  • CTAS の使用をサポート: テーブルを作成するための選択としてテーブルを作成
    ここに画像の説明を挿入

実際のアプリケーションでは、テーブルを作成する合理的な方法を選択してください。データ管理とセキュリティを容易にするために、外部テーブルとパーティション分割されたテーブルを作成することをお勧めします。

4.4.4 MergeInto ステートメント

HudiにはMergeIntoステートメントが用意されており、データを操作する際に判定条件に応じてデータを挿入・更新・削除するかを決定します。構文は以下の通りです。
ここに画像の説明を挿入

4.4.4.1 挿入へのマージ

条件を満たさない場合(関連付け条件が一致しない場合)、Hudiテーブルにデータを挿入します

merge into test_hudi_table as t0
using (
 select 1 as id, 'hadoop' as name, 1 as price, 9000 as ts, '2021-11-02' as dt
) as s0
on t0.id = s0.id
when not matched then insert * ;

Hudi テーブルのデータをクエリすると、Hudi テーブルにレコードがあることがわかります。
ここに画像の説明を挿入

4.4.4.2 アップデートにマージ

条件が満たされる (関連付け条件が一致する) 場合、データを更新します。

merge into test_hudi_table as t0
using (
 select 1 as id, 'hadoop3' as name, 1000 as price, 9999 as ts, '2021-11-02' as dt
) as s0
on t0.id = s0.id
when matched then update set *

Hudi テーブルをクエリすると、Hudi テーブルのパーティションが更新されたことがわかります。
ここに画像の説明を挿入

4.4.4.3 結合して削除

条件が満たされた場合 (関連付け条件が一致した場合)、データを削除します。

merge into test_hudi_table t0
using (
 select 1 as s_id, 'hadoop3' as s_name, 8888 as s_price, 9999 as s_ts, '2021-11-02' as dt
) s0
on t0.id = s0.s_id
when matched and s_ts = 9999 then delete

クエリ結果は次のようになります。Hudi テーブルにデータがないことがわかります。
ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/toto1297488504/article/details/132241113